Diff of /export.py [000000] .. [190ca4]

Switch to unified view

a b/export.py
1
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
2
"""
3
Export a YOLOv5 PyTorch model to other formats. TensorFlow exports authored by https://github.com/zldrobit
4
5
Format                      | `export.py --include`         | Model
6
---                         | ---                           | ---
7
PyTorch                     | -                             | yolov5s.pt
8
TorchScript                 | `torchscript`                 | yolov5s.torchscript
9
ONNX                        | `onnx`                        | yolov5s.onnx
10
OpenVINO                    | `openvino`                    | yolov5s_openvino_model/
11
TensorRT                    | `engine`                      | yolov5s.engine
12
CoreML                      | `coreml`                      | yolov5s.mlmodel
13
TensorFlow SavedModel       | `saved_model`                 | yolov5s_saved_model/
14
TensorFlow GraphDef         | `pb`                          | yolov5s.pb
15
TensorFlow Lite             | `tflite`                      | yolov5s.tflite
16
TensorFlow Edge TPU         | `edgetpu`                     | yolov5s_edgetpu.tflite
17
TensorFlow.js               | `tfjs`                        | yolov5s_web_model/
18
PaddlePaddle                | `paddle`                      | yolov5s_paddle_model/
19
20
Requirements:
21
    $ pip install -r requirements.txt coremltools onnx onnx-simplifier onnxruntime openvino-dev tensorflow-cpu  # CPU
22
    $ pip install -r requirements.txt coremltools onnx onnx-simplifier onnxruntime-gpu openvino-dev tensorflow  # GPU
23
24
Usage:
25
    $ python export.py --weights yolov5s.pt --include torchscript onnx openvino engine coreml tflite ...
26
27
Inference:
28
    $ python detect.py --weights yolov5s.pt                 # PyTorch
29
                                 yolov5s.torchscript        # TorchScript
30
                                 yolov5s.onnx               # ONNX Runtime or OpenCV DNN with --dnn
31
                                 yolov5s_openvino_model     # OpenVINO
32
                                 yolov5s.engine             # TensorRT
33
                                 yolov5s.mlmodel            # CoreML (macOS-only)
34
                                 yolov5s_saved_model        # TensorFlow SavedModel
35
                                 yolov5s.pb                 # TensorFlow GraphDef
36
                                 yolov5s.tflite             # TensorFlow Lite
37
                                 yolov5s_edgetpu.tflite     # TensorFlow Edge TPU
38
                                 yolov5s_paddle_model       # PaddlePaddle
39
40
TensorFlow.js:
41
    $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
42
    $ npm install
43
    $ ln -s ../../yolov5/yolov5s_web_model public/yolov5s_web_model
44
    $ npm start
45
"""
46
47
import argparse
48
import contextlib
49
import json
50
import os
51
import platform
52
import re
53
import subprocess
54
import sys
55
import time
56
import warnings
57
from pathlib import Path
58
59
import pandas as pd
60
import torch
61
from torch.utils.mobile_optimizer import optimize_for_mobile
62
63
FILE = Path(__file__).resolve()
64
ROOT = FILE.parents[0]  # YOLOv5 root directory
65
if str(ROOT) not in sys.path:
66
    sys.path.append(str(ROOT))  # add ROOT to PATH
67
if platform.system() != 'Windows':
68
    ROOT = Path(os.path.relpath(ROOT, Path.cwd()))  # relative
69
70
from models.experimental import attempt_load
71
from models.yolo import ClassificationModel, Detect, DetectionModel, SegmentationModel
72
from utils.dataloaders import LoadImages
73
from utils.general import (LOGGER, Profile, check_dataset, check_img_size, check_requirements, check_version,
74
                           check_yaml, colorstr, file_size, get_default_args, print_args, url2file, yaml_save)
75
from utils.torch_utils import select_device, smart_inference_mode
76
77
MACOS = platform.system() == 'Darwin'  # macOS environment
78
79
80
class iOSModel(torch.nn.Module):
81
82
    def __init__(self, model, im):
83
        super().__init__()
84
        b, c, h, w = im.shape  # batch, channel, height, width
85
        self.model = model
86
        self.nc = model.nc  # number of classes
87
        if w == h:
88
            self.normalize = 1. / w
89
        else:
90
            self.normalize = torch.tensor([1. / w, 1. / h, 1. / w, 1. / h])  # broadcast (slower, smaller)
91
            # np = model(im)[0].shape[1]  # number of points
92
            # self.normalize = torch.tensor([1. / w, 1. / h, 1. / w, 1. / h]).expand(np, 4)  # explicit (faster, larger)
93
94
    def forward(self, x):
95
        xywh, conf, cls = self.model(x)[0].squeeze().split((4, 1, self.nc), 1)
96
        return cls * conf, xywh * self.normalize  # confidence (3780, 80), coordinates (3780, 4)
97
98
99
def export_formats():
100
    # YOLOv5 export formats
101
    x = [
102
        ['PyTorch', '-', '.pt', True, True],
103
        ['TorchScript', 'torchscript', '.torchscript', True, True],
104
        ['ONNX', 'onnx', '.onnx', True, True],
105
        ['OpenVINO', 'openvino', '_openvino_model', True, False],
106
        ['TensorRT', 'engine', '.engine', False, True],
107
        ['CoreML', 'coreml', '.mlmodel', True, False],
108
        ['TensorFlow SavedModel', 'saved_model', '_saved_model', True, True],
109
        ['TensorFlow GraphDef', 'pb', '.pb', True, True],
110
        ['TensorFlow Lite', 'tflite', '.tflite', True, False],
111
        ['TensorFlow Edge TPU', 'edgetpu', '_edgetpu.tflite', False, False],
112
        ['TensorFlow.js', 'tfjs', '_web_model', False, False],
113
        ['PaddlePaddle', 'paddle', '_paddle_model', True, True], ]
114
    return pd.DataFrame(x, columns=['Format', 'Argument', 'Suffix', 'CPU', 'GPU'])
115
116
117
def try_export(inner_func):
118
    # YOLOv5 export decorator, i..e @try_export
119
    inner_args = get_default_args(inner_func)
120
121
    def outer_func(*args, **kwargs):
122
        prefix = inner_args['prefix']
123
        try:
124
            with Profile() as dt:
125
                f, model = inner_func(*args, **kwargs)
126
            LOGGER.info(f'{prefix} export success ✅ {dt.t:.1f}s, saved as {f} ({file_size(f):.1f} MB)')
127
            return f, model
128
        except Exception as e:
129
            LOGGER.info(f'{prefix} export failure ❌ {dt.t:.1f}s: {e}')
130
            return None, None
131
132
    return outer_func
133
134
135
@try_export
136
def export_torchscript(model, im, file, optimize, prefix=colorstr('TorchScript:')):
137
    # YOLOv5 TorchScript model export
138
    LOGGER.info(f'\n{prefix} starting export with torch {torch.__version__}...')
139
    f = file.with_suffix('.torchscript')
140
141
    ts = torch.jit.trace(model, im, strict=False)
142
    d = {'shape': im.shape, 'stride': int(max(model.stride)), 'names': model.names}
143
    extra_files = {'config.txt': json.dumps(d)}  # torch._C.ExtraFilesMap()
144
    if optimize:  # https://pytorch.org/tutorials/recipes/mobile_interpreter.html
145
        optimize_for_mobile(ts)._save_for_lite_interpreter(str(f), _extra_files=extra_files)
146
    else:
147
        ts.save(str(f), _extra_files=extra_files)
148
    return f, None
149
150
151
@try_export
152
def export_onnx(model, im, file, opset, dynamic, simplify, prefix=colorstr('ONNX:')):
153
    # YOLOv5 ONNX export
154
    check_requirements('onnx>=1.12.0')
155
    import onnx
156
157
    LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__}...')
158
    f = str(file.with_suffix('.onnx'))
159
160
    output_names = ['output0', 'output1'] if isinstance(model, SegmentationModel) else ['output0']
161
    if dynamic:
162
        dynamic = {'images': {0: 'batch', 2: 'height', 3: 'width'}}  # shape(1,3,640,640)
163
        if isinstance(model, SegmentationModel):
164
            dynamic['output0'] = {0: 'batch', 1: 'anchors'}  # shape(1,25200,85)
165
            dynamic['output1'] = {0: 'batch', 2: 'mask_height', 3: 'mask_width'}  # shape(1,32,160,160)
166
        elif isinstance(model, DetectionModel):
167
            dynamic['output0'] = {0: 'batch', 1: 'anchors'}  # shape(1,25200,85)
168
169
    torch.onnx.export(
170
        model.cpu() if dynamic else model,  # --dynamic only compatible with cpu
171
        im.cpu() if dynamic else im,
172
        f,
173
        verbose=False,
174
        opset_version=opset,
175
        do_constant_folding=True,  # WARNING: DNN inference with torch>=1.12 may require do_constant_folding=False
176
        input_names=['images'],
177
        output_names=output_names,
178
        dynamic_axes=dynamic or None)
179
180
    # Checks
181
    model_onnx = onnx.load(f)  # load onnx model
182
    onnx.checker.check_model(model_onnx)  # check onnx model
183
184
    # Metadata
185
    d = {'stride': int(max(model.stride)), 'names': model.names}
186
    for k, v in d.items():
187
        meta = model_onnx.metadata_props.add()
188
        meta.key, meta.value = k, str(v)
189
    onnx.save(model_onnx, f)
190
191
    # Simplify
192
    if simplify:
193
        try:
194
            cuda = torch.cuda.is_available()
195
            check_requirements(('onnxruntime-gpu' if cuda else 'onnxruntime', 'onnx-simplifier>=0.4.1'))
196
            import onnxsim
197
198
            LOGGER.info(f'{prefix} simplifying with onnx-simplifier {onnxsim.__version__}...')
199
            model_onnx, check = onnxsim.simplify(model_onnx)
200
            assert check, 'assert check failed'
201
            onnx.save(model_onnx, f)
202
        except Exception as e:
203
            LOGGER.info(f'{prefix} simplifier failure: {e}')
204
    return f, model_onnx
205
206
207
@try_export
208
def export_openvino(file, metadata, half, int8, data, prefix=colorstr('OpenVINO:')):
209
    # YOLOv5 OpenVINO export
210
    check_requirements('openvino-dev>=2023.0')  # requires openvino-dev: https://pypi.org/project/openvino-dev/
211
    import openvino.runtime as ov  # noqa
212
    from openvino.tools import mo  # noqa
213
214
    LOGGER.info(f'\n{prefix} starting export with openvino {ov.__version__}...')
215
    f = str(file).replace(file.suffix, f'_openvino_model{os.sep}')
216
    f_onnx = file.with_suffix('.onnx')
217
    f_ov = str(Path(f) / file.with_suffix('.xml').name)
218
    if int8:
219
        check_requirements('nncf>=2.4.0')  # requires at least version 2.4.0 to use the post-training quantization
220
        import nncf
221
        import numpy as np
222
        from openvino.runtime import Core
223
224
        from utils.dataloaders import create_dataloader
225
        core = Core()
226
        onnx_model = core.read_model(f_onnx)  # export
227
228
        def prepare_input_tensor(image: np.ndarray):
229
            input_tensor = image.astype(np.float32)  # uint8 to fp16/32
230
            input_tensor /= 255.0  # 0 - 255 to 0.0 - 1.0
231
232
            if input_tensor.ndim == 3:
233
                input_tensor = np.expand_dims(input_tensor, 0)
234
            return input_tensor
235
236
        def gen_dataloader(yaml_path, task='train', imgsz=640, workers=4):
237
            data_yaml = check_yaml(yaml_path)
238
            data = check_dataset(data_yaml)
239
            dataloader = create_dataloader(data[task],
240
                                           imgsz=imgsz,
241
                                           batch_size=1,
242
                                           stride=32,
243
                                           pad=0.5,
244
                                           single_cls=False,
245
                                           rect=False,
246
                                           workers=workers)[0]
247
            return dataloader
248
249
        # noqa: F811
250
251
        def transform_fn(data_item):
252
            """
253
            Quantization transform function. Extracts and preprocess input data from dataloader item for quantization.
254
            Parameters:
255
               data_item: Tuple with data item produced by DataLoader during iteration
256
            Returns:
257
                input_tensor: Input data for quantization
258
            """
259
            img = data_item[0].numpy()
260
            input_tensor = prepare_input_tensor(img)
261
            return input_tensor
262
263
        ds = gen_dataloader(data)
264
        quantization_dataset = nncf.Dataset(ds, transform_fn)
265
        ov_model = nncf.quantize(onnx_model, quantization_dataset, preset=nncf.QuantizationPreset.MIXED)
266
    else:
267
        ov_model = mo.convert_model(f_onnx, model_name=file.stem, framework='onnx', compress_to_fp16=half)  # export
268
269
    ov.serialize(ov_model, f_ov)  # save
270
    yaml_save(Path(f) / file.with_suffix('.yaml').name, metadata)  # add metadata.yaml
271
    return f, None
272
273
274
@try_export
275
def export_paddle(model, im, file, metadata, prefix=colorstr('PaddlePaddle:')):
276
    # YOLOv5 Paddle export
277
    check_requirements(('paddlepaddle', 'x2paddle'))
278
    import x2paddle
279
    from x2paddle.convert import pytorch2paddle
280
281
    LOGGER.info(f'\n{prefix} starting export with X2Paddle {x2paddle.__version__}...')
282
    f = str(file).replace('.pt', f'_paddle_model{os.sep}')
283
284
    pytorch2paddle(module=model, save_dir=f, jit_type='trace', input_examples=[im])  # export
285
    yaml_save(Path(f) / file.with_suffix('.yaml').name, metadata)  # add metadata.yaml
286
    return f, None
287
288
289
@try_export
290
def export_coreml(model, im, file, int8, half, nms, prefix=colorstr('CoreML:')):
291
    # YOLOv5 CoreML export
292
    check_requirements('coremltools')
293
    import coremltools as ct
294
295
    LOGGER.info(f'\n{prefix} starting export with coremltools {ct.__version__}...')
296
    f = file.with_suffix('.mlmodel')
297
298
    if nms:
299
        model = iOSModel(model, im)
300
    ts = torch.jit.trace(model, im, strict=False)  # TorchScript model
301
    ct_model = ct.convert(ts, inputs=[ct.ImageType('image', shape=im.shape, scale=1 / 255, bias=[0, 0, 0])])
302
    bits, mode = (8, 'kmeans_lut') if int8 else (16, 'linear') if half else (32, None)
303
    if bits < 32:
304
        if MACOS:  # quantization only supported on macOS
305
            with warnings.catch_warnings():
306
                warnings.filterwarnings('ignore', category=DeprecationWarning)  # suppress numpy==1.20 float warning
307
                ct_model = ct.models.neural_network.quantization_utils.quantize_weights(ct_model, bits, mode)
308
        else:
309
            print(f'{prefix} quantization only supported on macOS, skipping...')
310
    ct_model.save(f)
311
    return f, ct_model
312
313
314
@try_export
315
def export_engine(model, im, file, half, dynamic, simplify, workspace=4, verbose=False, prefix=colorstr('TensorRT:')):
316
    # YOLOv5 TensorRT export https://developer.nvidia.com/tensorrt
317
    assert im.device.type != 'cpu', 'export running on CPU but must be on GPU, i.e. `python export.py --device 0`'
318
    try:
319
        import tensorrt as trt
320
    except Exception:
321
        if platform.system() == 'Linux':
322
            check_requirements('nvidia-tensorrt', cmds='-U --index-url https://pypi.ngc.nvidia.com')
323
        import tensorrt as trt
324
325
    if trt.__version__[0] == '7':  # TensorRT 7 handling https://github.com/ultralytics/yolov5/issues/6012
326
        grid = model.model[-1].anchor_grid
327
        model.model[-1].anchor_grid = [a[..., :1, :1, :] for a in grid]
328
        export_onnx(model, im, file, 12, dynamic, simplify)  # opset 12
329
        model.model[-1].anchor_grid = grid
330
    else:  # TensorRT >= 8
331
        check_version(trt.__version__, '8.0.0', hard=True)  # require tensorrt>=8.0.0
332
        export_onnx(model, im, file, 12, dynamic, simplify)  # opset 12
333
    onnx = file.with_suffix('.onnx')
334
335
    LOGGER.info(f'\n{prefix} starting export with TensorRT {trt.__version__}...')
336
    assert onnx.exists(), f'failed to export ONNX file: {onnx}'
337
    f = file.with_suffix('.engine')  # TensorRT engine file
338
    logger = trt.Logger(trt.Logger.INFO)
339
    if verbose:
340
        logger.min_severity = trt.Logger.Severity.VERBOSE
341
342
    builder = trt.Builder(logger)
343
    config = builder.create_builder_config()
344
    config.max_workspace_size = workspace * 1 << 30
345
    # config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace << 30)  # fix TRT 8.4 deprecation notice
346
347
    flag = (1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
348
    network = builder.create_network(flag)
349
    parser = trt.OnnxParser(network, logger)
350
    if not parser.parse_from_file(str(onnx)):
351
        raise RuntimeError(f'failed to load ONNX file: {onnx}')
352
353
    inputs = [network.get_input(i) for i in range(network.num_inputs)]
354
    outputs = [network.get_output(i) for i in range(network.num_outputs)]
355
    for inp in inputs:
356
        LOGGER.info(f'{prefix} input "{inp.name}" with shape{inp.shape} {inp.dtype}')
357
    for out in outputs:
358
        LOGGER.info(f'{prefix} output "{out.name}" with shape{out.shape} {out.dtype}')
359
360
    if dynamic:
361
        if im.shape[0] <= 1:
362
            LOGGER.warning(f'{prefix} WARNING ⚠️ --dynamic model requires maximum --batch-size argument')
363
        profile = builder.create_optimization_profile()
364
        for inp in inputs:
365
            profile.set_shape(inp.name, (1, *im.shape[1:]), (max(1, im.shape[0] // 2), *im.shape[1:]), im.shape)
366
        config.add_optimization_profile(profile)
367
368
    LOGGER.info(f'{prefix} building FP{16 if builder.platform_has_fast_fp16 and half else 32} engine as {f}')
369
    if builder.platform_has_fast_fp16 and half:
370
        config.set_flag(trt.BuilderFlag.FP16)
371
    with builder.build_engine(network, config) as engine, open(f, 'wb') as t:
372
        t.write(engine.serialize())
373
    return f, None
374
375
376
@try_export
377
def export_saved_model(model,
378
                       im,
379
                       file,
380
                       dynamic,
381
                       tf_nms=False,
382
                       agnostic_nms=False,
383
                       topk_per_class=100,
384
                       topk_all=100,
385
                       iou_thres=0.45,
386
                       conf_thres=0.25,
387
                       keras=False,
388
                       prefix=colorstr('TensorFlow SavedModel:')):
389
    # YOLOv5 TensorFlow SavedModel export
390
    try:
391
        import tensorflow as tf
392
    except Exception:
393
        check_requirements(f"tensorflow{'' if torch.cuda.is_available() else '-macos' if MACOS else '-cpu'}")
394
        import tensorflow as tf
395
    from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
396
397
    from models.tf import TFModel
398
399
    LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
400
    if tf.__version__ > '2.13.1':
401
        helper_url = 'https://github.com/ultralytics/yolov5/issues/12489'
402
        LOGGER.info(
403
            f'WARNING ⚠️ using Tensorflow {tf.__version__} > 2.13.1 might cause issue when exporting the model to tflite {helper_url}'
404
        )  # handling issue https://github.com/ultralytics/yolov5/issues/12489
405
    f = str(file).replace('.pt', '_saved_model')
406
    batch_size, ch, *imgsz = list(im.shape)  # BCHW
407
408
    tf_model = TFModel(cfg=model.yaml, model=model, nc=model.nc, imgsz=imgsz)
409
    im = tf.zeros((batch_size, *imgsz, ch))  # BHWC order for TensorFlow
410
    _ = tf_model.predict(im, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
411
    inputs = tf.keras.Input(shape=(*imgsz, ch), batch_size=None if dynamic else batch_size)
412
    outputs = tf_model.predict(inputs, tf_nms, agnostic_nms, topk_per_class, topk_all, iou_thres, conf_thres)
413
    keras_model = tf.keras.Model(inputs=inputs, outputs=outputs)
414
    keras_model.trainable = False
415
    keras_model.summary()
416
    if keras:
417
        keras_model.save(f, save_format='tf')
418
    else:
419
        spec = tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype)
420
        m = tf.function(lambda x: keras_model(x))  # full model
421
        m = m.get_concrete_function(spec)
422
        frozen_func = convert_variables_to_constants_v2(m)
423
        tfm = tf.Module()
424
        tfm.__call__ = tf.function(lambda x: frozen_func(x)[:4] if tf_nms else frozen_func(x), [spec])
425
        tfm.__call__(im)
426
        tf.saved_model.save(tfm,
427
                            f,
428
                            options=tf.saved_model.SaveOptions(experimental_custom_gradients=False) if check_version(
429
                                tf.__version__, '2.6') else tf.saved_model.SaveOptions())
430
    return f, keras_model
431
432
433
@try_export
434
def export_pb(keras_model, file, prefix=colorstr('TensorFlow GraphDef:')):
435
    # YOLOv5 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow
436
    import tensorflow as tf
437
    from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2
438
439
    LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
440
    f = file.with_suffix('.pb')
441
442
    m = tf.function(lambda x: keras_model(x))  # full model
443
    m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
444
    frozen_func = convert_variables_to_constants_v2(m)
445
    frozen_func.graph.as_graph_def()
446
    tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False)
447
    return f, None
448
449
450
@try_export
451
def export_tflite(keras_model, im, file, int8, per_tensor, data, nms, agnostic_nms,
452
                  prefix=colorstr('TensorFlow Lite:')):
453
    # YOLOv5 TensorFlow Lite export
454
    import tensorflow as tf
455
456
    LOGGER.info(f'\n{prefix} starting export with tensorflow {tf.__version__}...')
457
    batch_size, ch, *imgsz = list(im.shape)  # BCHW
458
    f = str(file).replace('.pt', '-fp16.tflite')
459
460
    converter = tf.lite.TFLiteConverter.from_keras_model(keras_model)
461
    converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
462
    converter.target_spec.supported_types = [tf.float16]
463
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
464
    if int8:
465
        from models.tf import representative_dataset_gen
466
        dataset = LoadImages(check_dataset(check_yaml(data))['train'], img_size=imgsz, auto=False)
467
        converter.representative_dataset = lambda: representative_dataset_gen(dataset, ncalib=100)
468
        converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
469
        converter.target_spec.supported_types = []
470
        converter.inference_input_type = tf.uint8  # or tf.int8
471
        converter.inference_output_type = tf.uint8  # or tf.int8
472
        converter.experimental_new_quantizer = True
473
        if per_tensor:
474
            converter._experimental_disable_per_channel = True
475
        f = str(file).replace('.pt', '-int8.tflite')
476
    if nms or agnostic_nms:
477
        converter.target_spec.supported_ops.append(tf.lite.OpsSet.SELECT_TF_OPS)
478
479
    tflite_model = converter.convert()
480
    open(f, 'wb').write(tflite_model)
481
    return f, None
482
483
484
@try_export
485
def export_edgetpu(file, prefix=colorstr('Edge TPU:')):
486
    # YOLOv5 Edge TPU export https://coral.ai/docs/edgetpu/models-intro/
487
    cmd = 'edgetpu_compiler --version'
488
    help_url = 'https://coral.ai/docs/edgetpu/compiler/'
489
    assert platform.system() == 'Linux', f'export only supported on Linux. See {help_url}'
490
    if subprocess.run(f'{cmd} > /dev/null 2>&1', shell=True).returncode != 0:
491
        LOGGER.info(f'\n{prefix} export requires Edge TPU compiler. Attempting install from {help_url}')
492
        sudo = subprocess.run('sudo --version >/dev/null', shell=True).returncode == 0  # sudo installed on system
493
        for c in (
494
                'curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -',
495
                'echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | sudo tee /etc/apt/sources.list.d/coral-edgetpu.list',
496
                'sudo apt-get update', 'sudo apt-get install edgetpu-compiler'):
497
            subprocess.run(c if sudo else c.replace('sudo ', ''), shell=True, check=True)
498
    ver = subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1]
499
500
    LOGGER.info(f'\n{prefix} starting export with Edge TPU compiler {ver}...')
501
    f = str(file).replace('.pt', '-int8_edgetpu.tflite')  # Edge TPU model
502
    f_tfl = str(file).replace('.pt', '-int8.tflite')  # TFLite model
503
504
    subprocess.run([
505
        'edgetpu_compiler',
506
        '-s',
507
        '-d',
508
        '-k',
509
        '10',
510
        '--out_dir',
511
        str(file.parent),
512
        f_tfl, ], check=True)
513
    return f, None
514
515
516
@try_export
517
def export_tfjs(file, int8, prefix=colorstr('TensorFlow.js:')):
518
    # YOLOv5 TensorFlow.js export
519
    check_requirements('tensorflowjs')
520
    import tensorflowjs as tfjs
521
522
    LOGGER.info(f'\n{prefix} starting export with tensorflowjs {tfjs.__version__}...')
523
    f = str(file).replace('.pt', '_web_model')  # js dir
524
    f_pb = file.with_suffix('.pb')  # *.pb path
525
    f_json = f'{f}/model.json'  # *.json path
526
527
    args = [
528
        'tensorflowjs_converter',
529
        '--input_format=tf_frozen_model',
530
        '--quantize_uint8' if int8 else '',
531
        '--output_node_names=Identity,Identity_1,Identity_2,Identity_3',
532
        str(f_pb),
533
        str(f), ]
534
    subprocess.run([arg for arg in args if arg], check=True)
535
536
    json = Path(f_json).read_text()
537
    with open(f_json, 'w') as j:  # sort JSON Identity_* in ascending order
538
        subst = re.sub(
539
            r'{"outputs": {"Identity.?.?": {"name": "Identity.?.?"}, '
540
            r'"Identity.?.?": {"name": "Identity.?.?"}, '
541
            r'"Identity.?.?": {"name": "Identity.?.?"}, '
542
            r'"Identity.?.?": {"name": "Identity.?.?"}}}', r'{"outputs": {"Identity": {"name": "Identity"}, '
543
            r'"Identity_1": {"name": "Identity_1"}, '
544
            r'"Identity_2": {"name": "Identity_2"}, '
545
            r'"Identity_3": {"name": "Identity_3"}}}', json)
546
        j.write(subst)
547
    return f, None
548
549
550
def add_tflite_metadata(file, metadata, num_outputs):
551
    # Add metadata to *.tflite models per https://www.tensorflow.org/lite/models/convert/metadata
552
    with contextlib.suppress(ImportError):
553
        # check_requirements('tflite_support')
554
        from tflite_support import flatbuffers
555
        from tflite_support import metadata as _metadata
556
        from tflite_support import metadata_schema_py_generated as _metadata_fb
557
558
        tmp_file = Path('/tmp/meta.txt')
559
        with open(tmp_file, 'w') as meta_f:
560
            meta_f.write(str(metadata))
561
562
        model_meta = _metadata_fb.ModelMetadataT()
563
        label_file = _metadata_fb.AssociatedFileT()
564
        label_file.name = tmp_file.name
565
        model_meta.associatedFiles = [label_file]
566
567
        subgraph = _metadata_fb.SubGraphMetadataT()
568
        subgraph.inputTensorMetadata = [_metadata_fb.TensorMetadataT()]
569
        subgraph.outputTensorMetadata = [_metadata_fb.TensorMetadataT()] * num_outputs
570
        model_meta.subgraphMetadata = [subgraph]
571
572
        b = flatbuffers.Builder(0)
573
        b.Finish(model_meta.Pack(b), _metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER)
574
        metadata_buf = b.Output()
575
576
        populator = _metadata.MetadataPopulator.with_model_file(file)
577
        populator.load_metadata_buffer(metadata_buf)
578
        populator.load_associated_files([str(tmp_file)])
579
        populator.populate()
580
        tmp_file.unlink()
581
582
583
def pipeline_coreml(model, im, file, names, y, prefix=colorstr('CoreML Pipeline:')):
584
    # YOLOv5 CoreML pipeline
585
    import coremltools as ct
586
    from PIL import Image
587
588
    print(f'{prefix} starting pipeline with coremltools {ct.__version__}...')
589
    batch_size, ch, h, w = list(im.shape)  # BCHW
590
    t = time.time()
591
592
    # YOLOv5 Output shapes
593
    spec = model.get_spec()
594
    out0, out1 = iter(spec.description.output)
595
    if platform.system() == 'Darwin':
596
        img = Image.new('RGB', (w, h))  # img(192 width, 320 height)
597
        # img = torch.zeros((*opt.img_size, 3)).numpy()  # img size(320,192,3) iDetection
598
        out = model.predict({'image': img})
599
        out0_shape, out1_shape = out[out0.name].shape, out[out1.name].shape
600
    else:  # linux and windows can not run model.predict(), get sizes from pytorch output y
601
        s = tuple(y[0].shape)
602
        out0_shape, out1_shape = (s[1], s[2] - 5), (s[1], 4)  # (3780, 80), (3780, 4)
603
604
    # Checks
605
    nx, ny = spec.description.input[0].type.imageType.width, spec.description.input[0].type.imageType.height
606
    na, nc = out0_shape
607
    # na, nc = out0.type.multiArrayType.shape  # number anchors, classes
608
    assert len(names) == nc, f'{len(names)} names found for nc={nc}'  # check
609
610
    # Define output shapes (missing)
611
    out0.type.multiArrayType.shape[:] = out0_shape  # (3780, 80)
612
    out1.type.multiArrayType.shape[:] = out1_shape  # (3780, 4)
613
    # spec.neuralNetwork.preprocessing[0].featureName = '0'
614
615
    # Flexible input shapes
616
    # from coremltools.models.neural_network import flexible_shape_utils
617
    # s = [] # shapes
618
    # s.append(flexible_shape_utils.NeuralNetworkImageSize(320, 192))
619
    # s.append(flexible_shape_utils.NeuralNetworkImageSize(640, 384))  # (height, width)
620
    # flexible_shape_utils.add_enumerated_image_sizes(spec, feature_name='image', sizes=s)
621
    # r = flexible_shape_utils.NeuralNetworkImageSizeRange()  # shape ranges
622
    # r.add_height_range((192, 640))
623
    # r.add_width_range((192, 640))
624
    # flexible_shape_utils.update_image_size_range(spec, feature_name='image', size_range=r)
625
626
    # Print
627
    print(spec.description)
628
629
    # Model from spec
630
    model = ct.models.MLModel(spec)
631
632
    # 3. Create NMS protobuf
633
    nms_spec = ct.proto.Model_pb2.Model()
634
    nms_spec.specificationVersion = 5
635
    for i in range(2):
636
        decoder_output = model._spec.description.output[i].SerializeToString()
637
        nms_spec.description.input.add()
638
        nms_spec.description.input[i].ParseFromString(decoder_output)
639
        nms_spec.description.output.add()
640
        nms_spec.description.output[i].ParseFromString(decoder_output)
641
642
    nms_spec.description.output[0].name = 'confidence'
643
    nms_spec.description.output[1].name = 'coordinates'
644
645
    output_sizes = [nc, 4]
646
    for i in range(2):
647
        ma_type = nms_spec.description.output[i].type.multiArrayType
648
        ma_type.shapeRange.sizeRanges.add()
649
        ma_type.shapeRange.sizeRanges[0].lowerBound = 0
650
        ma_type.shapeRange.sizeRanges[0].upperBound = -1
651
        ma_type.shapeRange.sizeRanges.add()
652
        ma_type.shapeRange.sizeRanges[1].lowerBound = output_sizes[i]
653
        ma_type.shapeRange.sizeRanges[1].upperBound = output_sizes[i]
654
        del ma_type.shape[:]
655
656
    nms = nms_spec.nonMaximumSuppression
657
    nms.confidenceInputFeatureName = out0.name  # 1x507x80
658
    nms.coordinatesInputFeatureName = out1.name  # 1x507x4
659
    nms.confidenceOutputFeatureName = 'confidence'
660
    nms.coordinatesOutputFeatureName = 'coordinates'
661
    nms.iouThresholdInputFeatureName = 'iouThreshold'
662
    nms.confidenceThresholdInputFeatureName = 'confidenceThreshold'
663
    nms.iouThreshold = 0.45
664
    nms.confidenceThreshold = 0.25
665
    nms.pickTop.perClass = True
666
    nms.stringClassLabels.vector.extend(names.values())
667
    nms_model = ct.models.MLModel(nms_spec)
668
669
    # 4. Pipeline models together
670
    pipeline = ct.models.pipeline.Pipeline(input_features=[('image', ct.models.datatypes.Array(3, ny, nx)),
671
                                                           ('iouThreshold', ct.models.datatypes.Double()),
672
                                                           ('confidenceThreshold', ct.models.datatypes.Double())],
673
                                           output_features=['confidence', 'coordinates'])
674
    pipeline.add_model(model)
675
    pipeline.add_model(nms_model)
676
677
    # Correct datatypes
678
    pipeline.spec.description.input[0].ParseFromString(model._spec.description.input[0].SerializeToString())
679
    pipeline.spec.description.output[0].ParseFromString(nms_model._spec.description.output[0].SerializeToString())
680
    pipeline.spec.description.output[1].ParseFromString(nms_model._spec.description.output[1].SerializeToString())
681
682
    # Update metadata
683
    pipeline.spec.specificationVersion = 5
684
    pipeline.spec.description.metadata.versionString = 'https://github.com/ultralytics/yolov5'
685
    pipeline.spec.description.metadata.shortDescription = 'https://github.com/ultralytics/yolov5'
686
    pipeline.spec.description.metadata.author = 'glenn.jocher@ultralytics.com'
687
    pipeline.spec.description.metadata.license = 'https://github.com/ultralytics/yolov5/blob/master/LICENSE'
688
    pipeline.spec.description.metadata.userDefined.update({
689
        'classes': ','.join(names.values()),
690
        'iou_threshold': str(nms.iouThreshold),
691
        'confidence_threshold': str(nms.confidenceThreshold)})
692
693
    # Save the model
694
    f = file.with_suffix('.mlmodel')  # filename
695
    model = ct.models.MLModel(pipeline.spec)
696
    model.input_description['image'] = 'Input image'
697
    model.input_description['iouThreshold'] = f'(optional) IOU Threshold override (default: {nms.iouThreshold})'
698
    model.input_description['confidenceThreshold'] = \
699
        f'(optional) Confidence Threshold override (default: {nms.confidenceThreshold})'
700
    model.output_description['confidence'] = 'Boxes × Class confidence (see user-defined metadata "classes")'
701
    model.output_description['coordinates'] = 'Boxes × [x, y, width, height] (relative to image size)'
702
    model.save(f)  # pipelined
703
    print(f'{prefix} pipeline success ({time.time() - t:.2f}s), saved as {f} ({file_size(f):.1f} MB)')
704
705
706
@smart_inference_mode()
707
def run(
708
        data=ROOT / 'data/coco128.yaml',  # 'dataset.yaml path'
709
        weights=ROOT / 'yolov5s.pt',  # weights path
710
        imgsz=(640, 640),  # image (height, width)
711
        batch_size=1,  # batch size
712
        device='cpu',  # cuda device, i.e. 0 or 0,1,2,3 or cpu
713
        include=('torchscript', 'onnx'),  # include formats
714
        half=False,  # FP16 half-precision export
715
        inplace=False,  # set YOLOv5 Detect() inplace=True
716
        keras=False,  # use Keras
717
        optimize=False,  # TorchScript: optimize for mobile
718
        int8=False,  # CoreML/TF INT8 quantization
719
        per_tensor=False,  # TF per tensor quantization
720
        dynamic=False,  # ONNX/TF/TensorRT: dynamic axes
721
        simplify=False,  # ONNX: simplify model
722
        opset=12,  # ONNX: opset version
723
        verbose=False,  # TensorRT: verbose log
724
        workspace=4,  # TensorRT: workspace size (GB)
725
        nms=False,  # TF: add NMS to model
726
        agnostic_nms=False,  # TF: add agnostic NMS to model
727
        topk_per_class=100,  # TF.js NMS: topk per class to keep
728
        topk_all=100,  # TF.js NMS: topk for all classes to keep
729
        iou_thres=0.45,  # TF.js NMS: IoU threshold
730
        conf_thres=0.25,  # TF.js NMS: confidence threshold
731
):
732
    t = time.time()
733
    include = [x.lower() for x in include]  # to lowercase
734
    fmts = tuple(export_formats()['Argument'][1:])  # --include arguments
735
    flags = [x in include for x in fmts]
736
    assert sum(flags) == len(include), f'ERROR: Invalid --include {include}, valid --include arguments are {fmts}'
737
    jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle = flags  # export booleans
738
    file = Path(url2file(weights) if str(weights).startswith(('http:/', 'https:/')) else weights)  # PyTorch weights
739
740
    # Load PyTorch model
741
    device = select_device(device)
742
    if half:
743
        assert device.type != 'cpu' or coreml, '--half only compatible with GPU export, i.e. use --device 0'
744
        assert not dynamic, '--half not compatible with --dynamic, i.e. use either --half or --dynamic but not both'
745
    model = attempt_load(weights, device=device, inplace=True, fuse=True)  # load FP32 model
746
747
    # Checks
748
    imgsz *= 2 if len(imgsz) == 1 else 1  # expand
749
    if optimize:
750
        assert device.type == 'cpu', '--optimize not compatible with cuda devices, i.e. use --device cpu'
751
752
    # Input
753
    gs = int(max(model.stride))  # grid size (max stride)
754
    imgsz = [check_img_size(x, gs) for x in imgsz]  # verify img_size are gs-multiples
755
    im = torch.zeros(batch_size, 3, *imgsz).to(device)  # image size(1,3,320,192) BCHW iDetection
756
757
    # Update model
758
    model.eval()
759
    for k, m in model.named_modules():
760
        if isinstance(m, Detect):
761
            m.inplace = inplace
762
            m.dynamic = dynamic
763
            m.export = True
764
765
    for _ in range(2):
766
        y = model(im)  # dry runs
767
    if half and not coreml:
768
        im, model = im.half(), model.half()  # to FP16
769
    shape = tuple((y[0] if isinstance(y, tuple) else y).shape)  # model output shape
770
    metadata = {'stride': int(max(model.stride)), 'names': model.names}  # model metadata
771
    LOGGER.info(f"\n{colorstr('PyTorch:')} starting from {file} with output shape {shape} ({file_size(file):.1f} MB)")
772
773
    # Exports
774
    f = [''] * len(fmts)  # exported filenames
775
    warnings.filterwarnings(action='ignore', category=torch.jit.TracerWarning)  # suppress TracerWarning
776
    if jit:  # TorchScript
777
        f[0], _ = export_torchscript(model, im, file, optimize)
778
    if engine:  # TensorRT required before ONNX
779
        f[1], _ = export_engine(model, im, file, half, dynamic, simplify, workspace, verbose)
780
    if onnx or xml:  # OpenVINO requires ONNX
781
        f[2], _ = export_onnx(model, im, file, opset, dynamic, simplify)
782
    if xml:  # OpenVINO
783
        f[3], _ = export_openvino(file, metadata, half, int8, data)
784
    if coreml:  # CoreML
785
        f[4], ct_model = export_coreml(model, im, file, int8, half, nms)
786
        if nms:
787
            pipeline_coreml(ct_model, im, file, model.names, y)
788
    if any((saved_model, pb, tflite, edgetpu, tfjs)):  # TensorFlow formats
789
        assert not tflite or not tfjs, 'TFLite and TF.js models must be exported separately, please pass only one type.'
790
        assert not isinstance(model, ClassificationModel), 'ClassificationModel export to TF formats not yet supported.'
791
        f[5], s_model = export_saved_model(model.cpu(),
792
                                           im,
793
                                           file,
794
                                           dynamic,
795
                                           tf_nms=nms or agnostic_nms or tfjs,
796
                                           agnostic_nms=agnostic_nms or tfjs,
797
                                           topk_per_class=topk_per_class,
798
                                           topk_all=topk_all,
799
                                           iou_thres=iou_thres,
800
                                           conf_thres=conf_thres,
801
                                           keras=keras)
802
        if pb or tfjs:  # pb prerequisite to tfjs
803
            f[6], _ = export_pb(s_model, file)
804
        if tflite or edgetpu:
805
            f[7], _ = export_tflite(s_model,
806
                                    im,
807
                                    file,
808
                                    int8 or edgetpu,
809
                                    per_tensor,
810
                                    data=data,
811
                                    nms=nms,
812
                                    agnostic_nms=agnostic_nms)
813
            if edgetpu:
814
                f[8], _ = export_edgetpu(file)
815
            add_tflite_metadata(f[8] or f[7], metadata, num_outputs=len(s_model.outputs))
816
        if tfjs:
817
            f[9], _ = export_tfjs(file, int8)
818
    if paddle:  # PaddlePaddle
819
        f[10], _ = export_paddle(model, im, file, metadata)
820
821
    # Finish
822
    f = [str(x) for x in f if x]  # filter out '' and None
823
    if any(f):
824
        cls, det, seg = (isinstance(model, x) for x in (ClassificationModel, DetectionModel, SegmentationModel))  # type
825
        det &= not seg  # segmentation models inherit from SegmentationModel(DetectionModel)
826
        dir = Path('segment' if seg else 'classify' if cls else '')
827
        h = '--half' if half else ''  # --half FP16 inference arg
828
        s = '# WARNING ⚠️ ClassificationModel not yet supported for PyTorch Hub AutoShape inference' if cls else \
829
            '# WARNING ⚠️ SegmentationModel not yet supported for PyTorch Hub AutoShape inference' if seg else ''
830
        LOGGER.info(f'\nExport complete ({time.time() - t:.1f}s)'
831
                    f"\nResults saved to {colorstr('bold', file.parent.resolve())}"
832
                    f"\nDetect:          python {dir / ('detect.py' if det else 'predict.py')} --weights {f[-1]} {h}"
833
                    f"\nValidate:        python {dir / 'val.py'} --weights {f[-1]} {h}"
834
                    f"\nPyTorch Hub:     model = torch.hub.load('ultralytics/yolov5', 'custom', '{f[-1]}')  {s}"
835
                    f'\nVisualize:       https://netron.app')
836
    return f  # return list of exported files/dirs
837
838
839
def parse_opt(known=False):
840
    parser = argparse.ArgumentParser()
841
    parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
842
    parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model.pt path(s)')
843
    parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640, 640], help='image (h, w)')
844
    parser.add_argument('--batch-size', type=int, default=1, help='batch size')
845
    parser.add_argument('--device', default='cpu', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
846
    parser.add_argument('--half', action='store_true', help='FP16 half-precision export')
847
    parser.add_argument('--inplace', action='store_true', help='set YOLOv5 Detect() inplace=True')
848
    parser.add_argument('--keras', action='store_true', help='TF: use Keras')
849
    parser.add_argument('--optimize', action='store_true', help='TorchScript: optimize for mobile')
850
    parser.add_argument('--int8', action='store_true', help='CoreML/TF/OpenVINO INT8 quantization')
851
    parser.add_argument('--per-tensor', action='store_true', help='TF per-tensor quantization')
852
    parser.add_argument('--dynamic', action='store_true', help='ONNX/TF/TensorRT: dynamic axes')
853
    parser.add_argument('--simplify', action='store_true', help='ONNX: simplify model')
854
    parser.add_argument('--opset', type=int, default=17, help='ONNX: opset version')
855
    parser.add_argument('--verbose', action='store_true', help='TensorRT: verbose log')
856
    parser.add_argument('--workspace', type=int, default=4, help='TensorRT: workspace size (GB)')
857
    parser.add_argument('--nms', action='store_true', help='TF: add NMS to model')
858
    parser.add_argument('--agnostic-nms', action='store_true', help='TF: add agnostic NMS to model')
859
    parser.add_argument('--topk-per-class', type=int, default=100, help='TF.js NMS: topk per class to keep')
860
    parser.add_argument('--topk-all', type=int, default=100, help='TF.js NMS: topk for all classes to keep')
861
    parser.add_argument('--iou-thres', type=float, default=0.45, help='TF.js NMS: IoU threshold')
862
    parser.add_argument('--conf-thres', type=float, default=0.25, help='TF.js NMS: confidence threshold')
863
    parser.add_argument(
864
        '--include',
865
        nargs='+',
866
        default=['torchscript'],
867
        help='torchscript, onnx, openvino, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle')
868
    opt = parser.parse_known_args()[0] if known else parser.parse_args()
869
    print_args(vars(opt))
870
    return opt
871
872
873
def main(opt):
874
    for opt.weights in (opt.weights if isinstance(opt.weights, list) else [opt.weights]):
875
        run(**vars(opt))
876
877
878
if __name__ == '__main__':
879
    opt = parse_opt()
880
    main(opt)