export.py 16.1 KB
Newer Older
huchen's avatar
huchen committed
1
2
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
3
4
Export a YOLOv5 PyTorch model to TorchScript, ONNX, CoreML, TensorFlow (saved_model, pb, TFLite, TF.js,) formats
TensorFlow exports authored by https://github.com/zldrobit
huchen's avatar
huchen committed
5
6

Usage:
7
    $ python path/to/export.py --weights yolov5s.pt --include torchscript onnx coreml saved_model pb tflite tfjs
huchen's avatar
huchen committed
8
9

Inference:
10
11
12
13
14
    $ python path/to/detect.py --weights yolov5s.pt
                                         yolov5s.onnx  (must export with --dynamic)
                                         yolov5s_saved_model
                                         yolov5s.pb
                                         yolov5s.tflite
huchen's avatar
huchen committed
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

TensorFlow.js:
    $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
    $ npm install
    $ ln -s ../../yolov5/yolov5s_web_model public/yolov5s_web_model
    $ npm start
"""

import argparse
import os
import subprocess
import sys
import time
from pathlib import Path

import torch
import torch.nn as nn
from torch.utils.mobile_optimizer import optimize_for_mobile

FILE = Path(__file__).resolve()
ROOT = FILE.parents[0]  # YOLOv5 root directory
if str(ROOT) not in sys.path:
    sys.path.append(str(ROOT))  # add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative

from models.common import Conv
from models.experimental import attempt_load
from models.yolo import Detect
from utils.activations import SiLU
from utils.datasets import LoadImages
45
46
from utils.general import colorstr, check_dataset, check_img_size, check_requirements, file_size, print_args, \
    set_logging, url2file
huchen's avatar
huchen committed
47
48
49
50
51
52
from utils.torch_utils import select_device


def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:')):
    # YOLOv5 TorchScript model export
    try:
53
54
        print(f'\n{prefix} starting export with torch {torch.__version__}...')
        f = file.with_suffix('.torchscript.pt')
huchen's avatar
huchen committed
55
56

        ts = torch.jit.trace(model, im, strict=False)
57
58
59
        (optimize_for_mobile(ts) if optimize else ts).save(f)

        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
huchen's avatar
huchen committed
60
    except Exception as e:
61
        print(f'{prefix} export failure: {e}')
huchen's avatar
huchen committed
62
63
64
65
66
67
68
69


def export_onnx(model, im, file, opset, train, dynamic, simplify, prefix=colorstr('ONNX:')):
    # YOLOv5 ONNX export
    try:
        check_requirements(('onnx',))
        import onnx

70
        print(f'\n{prefix} starting export with onnx {onnx.__version__}...')
huchen's avatar
huchen committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
        f = file.with_suffix('.onnx')

        torch.onnx.export(model, im, f, verbose=False, opset_version=opset,
                          training=torch.onnx.TrainingMode.TRAINING if train else torch.onnx.TrainingMode.EVAL,
                          do_constant_folding=not train,
                          input_names=['images'],
                          output_names=['output'],
                          dynamic_axes={'images': {0: 'batch', 2: 'height', 3: 'width'},  # shape(1,3,640,640)
                                        'output': {0: 'batch', 1: 'anchors'}  # shape(1,25200,85)
                                        } if dynamic else None)

        # Checks
        model_onnx = onnx.load(f)  # load onnx model
        onnx.checker.check_model(model_onnx)  # check onnx model
85
        # print(onnx.helper.printable_graph(model_onnx.graph))  # print
huchen's avatar
huchen committed
86
87
88
89
90
91
92

        # Simplify
        if simplify:
            try:
                check_requirements(('onnx-simplifier',))
                import onnxsim

93
                print(f'{prefix} simplifying with onnx-simplifier {onnxsim.__version__}...')
huchen's avatar
huchen committed
94
95
96
97
98
99
100
                model_onnx, check = onnxsim.simplify(
                    model_onnx,
                    dynamic_input_shape=dynamic,
                    input_shapes={'images': list(im.shape)} if dynamic else None)
                assert check, 'assert check failed'
                onnx.save(model_onnx, f)
            except Exception as e:
101
102
103
                print(f'{prefix} simplifier failure: {e}')
        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
        print(f"{prefix} run --dynamic ONNX model inference with: 'python detect.py --weights {f}'")
huchen's avatar
huchen committed
104
    except Exception as e:
105
        print(f'{prefix} export failure: {e}')
huchen's avatar
huchen committed
106
107
108
109


def export_coreml(model, im, file, prefix=colorstr('CoreML:')):
    # YOLOv5 CoreML export
110
    ct_model = None
huchen's avatar
huchen committed
111
112
113
114
    try:
        check_requirements(('coremltools',))
        import coremltools as ct

115
        print(f'\n{prefix} starting export with coremltools {ct.__version__}...')
huchen's avatar
huchen committed
116
117
        f = file.with_suffix('.mlmodel')

118
        model.train()  # CoreML exports should be placed in model.train() mode
huchen's avatar
huchen committed
119
        ts = torch.jit.trace(model, im, strict=False)  # TorchScript model
120
        ct_model = ct.convert(ts, inputs=[ct.ImageType('image', shape=im.shape, scale=1 / 255.0, bias=[0, 0, 0])])
huchen's avatar
huchen committed
121
122
        ct_model.save(f)

123
        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
huchen's avatar
huchen committed
124
    except Exception as e:
125
        print(f'\n{prefix} export failure: {e}')
huchen's avatar
huchen committed
126

127
    return ct_model
huchen's avatar
huchen committed
128
129
130
131


def export_saved_model(model, im, file, dynamic,
                       tf_nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45,
132
133
134
                       conf_thres=0.25, prefix=colorstr('TensorFlow saved_model:')):
    # YOLOv5 TensorFlow saved_model export
    keras_model = None
huchen's avatar
huchen committed
135
136
137
    try:
        import tensorflow as tf
        from tensorflow import keras
138
        from models.tf import TFModel, TFDetect
huchen's avatar
huchen committed
139

140
        print(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
huchen's avatar
huchen committed
141
142
143
144
145
146
147
148
149
150
151
152
153
        f = str(file).replace('.pt', '_saved_model')
        batch_size, ch, *imgsz = list(im.shape)  # BCHW

        tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz)
        im = tf.zeros((batch_size, *imgsz, 3))  # BHWC order for TensorFlow
        y = tf_model.predict(im, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
        inputs = keras.Input(shape=(*imgsz, 3), batch_size=None if dynamic else batch_size)
        outputs = tf_model.predict(inputs, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
        keras_model = keras.Model(inputs=inputs, outputs=outputs)
        keras_model.trainable = False
        keras_model.summary()
        keras_model.save(f, save_format='tf')

154
        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
huchen's avatar
huchen committed
155
    except Exception as e:
156
157
158
        print(f'\n{prefix} export failure: {e}')

    return keras_model
huchen's avatar
huchen committed
159
160
161
162
163
164
165
166


def export_pb(keras_model, im, file, prefix=colorstr('TensorFlow GraphDef:')):
    # YOLOv5 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow
    try:
        import tensorflow as tf
        from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2

167
        print(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
huchen's avatar
huchen committed
168
169
170
171
172
173
174
175
        f = file.with_suffix('.pb')

        m = tf.function(lambda x: keras_model(x))  # full model
        m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
        frozen_func = convert_variables_to_constants_v2(m)
        frozen_func.graph.as_graph_def()
        tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False)

176
        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
huchen's avatar
huchen committed
177
    except Exception as e:
178
        print(f'\n{prefix} export failure: {e}')
huchen's avatar
huchen committed
179
180
181
182
183
184


def export_tflite(keras_model, im, file, int8, data, ncalib, prefix=colorstr('TensorFlow Lite:')):
    # YOLOv5 TensorFlow Lite export
    try:
        import tensorflow as tf
185
        from models.tf import representative_dataset_gen
huchen's avatar
huchen committed
186

187
        print(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
huchen's avatar
huchen committed
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
        batch_size, ch, *imgsz = list(im.shape)  # BCHW
        f = str(file).replace('.pt', '-fp16.tflite')

        converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
        converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
        converter.target_spec.supported_types = [tf.float16]
        converter.optimizations = [tf.lite.Optimize.DEFAULT]
        if int8:
            dataset = LoadImages(check_dataset(data)['train'], img_size=imgsz, auto=False)  # representative data
            converter.representative_dataset = lambda: representative_dataset_gen(dataset, ncalib)
            converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
            converter.target_spec.supported_types = []
            converter.inference_input_type = tf.uint8  # or tf.int8
            converter.inference_output_type = tf.uint8  # or tf.int8
            converter.experimental_new_quantizer = False
            f = str(file).replace('.pt', '-int8.tflite')

        tflite_model = converter.convert()
        open(f, "wb").write(tflite_model)
207
        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
huchen's avatar
huchen committed
208
209

    except Exception as e:
210
        print(f'\n{prefix} export failure: {e}')
huchen's avatar
huchen committed
211
212
213
214
215
216
217
218
219


def export_tfjs(keras_model, im, file, prefix=colorstr('TensorFlow.js:')):
    # YOLOv5 TensorFlow.js export
    try:
        check_requirements(('tensorflowjs',))
        import re
        import tensorflowjs as tfjs

220
        print(f'\n{prefix} starting export with tensorflowjs {tfjs.__version__}...')
huchen's avatar
huchen committed
221
222
223
224
        f = str(file).replace('.pt', '_web_model')  # js dir
        f_pb = file.with_suffix('.pb')  # *.pb path
        f_json = f + '/model.json'  # *.json path

225
226
        cmd = f"tensorflowjs_converter --input_format=tf_frozen_model " \
              f"--output_node_names='Identity,Identity_1,Identity_2,Identity_3' {f_pb} {f}"
huchen's avatar
huchen committed
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
        subprocess.run(cmd, shell=True)

        json = open(f_json).read()
        with open(f_json, 'w') as j:  # sort JSON Identity_* in ascending order
            subst = re.sub(
                r'{"outputs": {"Identity.?.?": {"name": "Identity.?.?"}, '
                r'"Identity.?.?": {"name": "Identity.?.?"}, '
                r'"Identity.?.?": {"name": "Identity.?.?"}, '
                r'"Identity.?.?": {"name": "Identity.?.?"}}}',
                r'{"outputs": {"Identity": {"name": "Identity"}, '
                r'"Identity_1": {"name": "Identity_1"}, '
                r'"Identity_2": {"name": "Identity_2"}, '
                r'"Identity_3": {"name": "Identity_3"}}}',
                json)
            j.write(subst)

243
        print(f'{prefix} export success, saved as {f} ({file_size(f):.1f} MB)')
huchen's avatar
huchen committed
244
    except Exception as e:
245
        print(f'\n{prefix} export failure: {e}')
huchen's avatar
huchen committed
246
247
248
249
250
251
252
253


@torch.no_grad()
def run(data=ROOT / 'data/coco128.yaml',  # 'dataset.yaml path'
        weights=ROOT / 'yolov5s.pt',  # weights path
        imgsz=(640, 640),  # image (height, width)
        batch_size=1,  # batch size
        device='cpu',  # cuda device, i.e. 0 or 0,1,2,3 or cpu
254
        include=('torchscript', 'onnx', 'coreml'),  # include formats
huchen's avatar
huchen committed
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
        half=False,  # FP16 half-precision export
        inplace=False,  # set YOLOv5 Detect() inplace=True
        train=False,  # model.train() mode
        optimize=False,  # TorchScript: optimize for mobile
        int8=False,  # CoreML/TF INT8 quantization
        dynamic=False,  # ONNX/TF: dynamic axes
        simplify=False,  # ONNX: simplify model
        opset=12,  # ONNX: opset version
        topk_per_class=100,  # TF.js NMS: topk per class to keep
        topk_all=100,  # TF.js NMS: topk for all classes to keep
        iou_thres=0.45,  # TF.js NMS: IoU threshold
        conf_thres=0.25  # TF.js NMS: confidence threshold
        ):
    t = time.time()
    include = [x.lower() for x in include]
270
    tf_exports = list(x in include for x in ('saved_model', 'pb', 'tflite', 'tfjs'))  # TensorFlow exports
huchen's avatar
huchen committed
271
    imgsz *= 2 if len(imgsz) == 1 else 1  # expand
272
    file = Path(url2file(weights) if str(weights).startswith(('http:/', 'https:/')) else weights)
huchen's avatar
huchen committed
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299

    # Load PyTorch model
    device = select_device(device)
    assert not (device.type == 'cpu' and half), '--half only compatible with GPU export, i.e. use --device 0'
    model = attempt_load(weights, map_location=device, inplace=True, fuse=True)  # load FP32 model
    nc, names = model.nc, model.names  # number of classes, class names

    # Input
    gs = int(max(model.stride))  # grid size (max stride)
    imgsz = [check_img_size(x, gs) for x in imgsz]  # verify img_size are gs-multiples
    im = torch.zeros(batch_size, 3, *imgsz).to(device)  # image size(1,3,320,192) BCHW iDetection

    # Update model
    if half:
        im, model = im.half(), model.half()  # to FP16
    model.train() if train else model.eval()  # training mode = no Detect() layer grid construction
    for k, m in model.named_modules():
        if isinstance(m, Conv):  # assign export-friendly activations
            if isinstance(m.act, nn.SiLU):
                m.act = SiLU()
        elif isinstance(m, Detect):
            m.inplace = inplace
            m.onnx_dynamic = dynamic
            # m.forward = m.forward_export  # assign forward (optional)

    for _ in range(2):
        y = model(im)  # dry runs
300
    print(f"\n{colorstr('PyTorch:')} starting from {file} ({file_size(file):.1f} MB)")
huchen's avatar
huchen committed
301
302
303

    # Exports
    if 'torchscript' in include:
304
305
306
        export_torchscript(model, im, file, optimize)
    if 'onnx' in include:
        export_onnx(model, im, file, opset, train, dynamic, simplify)
huchen's avatar
huchen committed
307
    if 'coreml' in include:
308
        export_coreml(model, im, file)
huchen's avatar
huchen committed
309
310
311

    # TensorFlow Exports
    if any(tf_exports):
312
        pb, tflite, tfjs = tf_exports[1:]
huchen's avatar
huchen committed
313
        assert not (tflite and tfjs), 'TFLite and TF.js models must be exported separately, please pass only one type.'
314
315
316
        model = export_saved_model(model, im, file, dynamic, tf_nms=tfjs, agnostic_nms=tfjs,
                                   topk_per_class=topk_per_class, topk_all=topk_all, conf_thres=conf_thres,
                                   iou_thres=iou_thres)  # keras model
huchen's avatar
huchen committed
317
        if pb or tfjs:  # pb prerequisite to tfjs
318
319
320
            export_pb(model, im, file)
        if tflite:
            export_tflite(model, im, file, int8=int8, data=data, ncalib=100)
huchen's avatar
huchen committed
321
        if tfjs:
322
            export_tfjs(model, im, file)
huchen's avatar
huchen committed
323
324

    # Finish
325
326
327
    print(f'\nExport complete ({time.time() - t:.2f}s)'
          f"\nResults saved to {colorstr('bold', file.parent.resolve())}"
          f'\nVisualize with https://netron.app')
huchen's avatar
huchen committed
328
329
330
331
332


def parse_opt():
    parser = argparse.ArgumentParser()
    parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
333
    parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='weights path')
huchen's avatar
huchen committed
334
335
336
337
338
339
340
341
342
343
    parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640, 640], help='image (h, w)')
    parser.add_argument('--batch-size', type=int, default=1, help='batch size')
    parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
    parser.add_argument('--half', action='store_true', help='FP16 half-precision export')
    parser.add_argument('--inplace', action='store_true', help='set YOLOv5 Detect() inplace=True')
    parser.add_argument('--train', action='store_true', help='model.train() mode')
    parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile')
    parser.add_argument('--int8', action='store_true', help='CoreML/TF INT8 quantization')
    parser.add_argument('--dynamic', action='store_true', help='ONNX/TF: dynamic axes')
    parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model')
344
    parser.add_argument('--opset', type=int, default=13, help='ONNX: opset version')
huchen's avatar
huchen committed
345
346
347
348
349
350
    parser.add_argument('--topk-per-class', type=int, default=100, help='TF.js NMS: topk per class to keep')
    parser.add_argument('--topk-all', type=int, default=100, help='TF.js NMS: topk for all classes to keep')
    parser.add_argument('--iou-thres', type=float, default=0.45, help='TF.js NMS: IoU threshold')
    parser.add_argument('--conf-thres', type=float, default=0.25, help='TF.js NMS: confidence threshold')
    parser.add_argument('--include', nargs='+',
                        default=['torchscript', 'onnx'],
351
                        help='available formats are (torchscript, onnx, coreml, saved_model, pb, tflite, tfjs)')
huchen's avatar
huchen committed
352
353
354
355
356
357
    opt = parser.parse_args()
    print_args(FILE.stem, opt)
    return opt


def main(opt):
358
359
    set_logging()
    run(**vars(opt))
huchen's avatar
huchen committed
360
361
362
363
364


if __name__ == "__main__":
    opt = parse_opt()
    main(opt)