dgenerate-ultralytics-headless 8.3.137__py3-none-any.whl → 8.3.224__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/METADATA +41 -34
- dgenerate_ultralytics_headless-8.3.224.dist-info/RECORD +285 -0
- {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/WHEEL +1 -1
- tests/__init__.py +7 -6
- tests/conftest.py +15 -39
- tests/test_cli.py +17 -17
- tests/test_cuda.py +17 -8
- tests/test_engine.py +36 -10
- tests/test_exports.py +98 -37
- tests/test_integrations.py +12 -15
- tests/test_python.py +126 -82
- tests/test_solutions.py +319 -135
- ultralytics/__init__.py +27 -9
- ultralytics/cfg/__init__.py +83 -87
- ultralytics/cfg/datasets/Argoverse.yaml +4 -4
- ultralytics/cfg/datasets/DOTAv1.5.yaml +2 -2
- ultralytics/cfg/datasets/DOTAv1.yaml +2 -2
- ultralytics/cfg/datasets/GlobalWheat2020.yaml +2 -2
- ultralytics/cfg/datasets/HomeObjects-3K.yaml +4 -5
- ultralytics/cfg/datasets/ImageNet.yaml +3 -3
- ultralytics/cfg/datasets/Objects365.yaml +24 -20
- ultralytics/cfg/datasets/SKU-110K.yaml +9 -9
- ultralytics/cfg/datasets/VOC.yaml +10 -13
- ultralytics/cfg/datasets/VisDrone.yaml +43 -33
- ultralytics/cfg/datasets/african-wildlife.yaml +5 -5
- ultralytics/cfg/datasets/brain-tumor.yaml +4 -5
- ultralytics/cfg/datasets/carparts-seg.yaml +5 -5
- ultralytics/cfg/datasets/coco-pose.yaml +26 -4
- ultralytics/cfg/datasets/coco.yaml +4 -4
- ultralytics/cfg/datasets/coco128-seg.yaml +2 -2
- ultralytics/cfg/datasets/coco128.yaml +2 -2
- ultralytics/cfg/datasets/coco8-grayscale.yaml +103 -0
- ultralytics/cfg/datasets/coco8-multispectral.yaml +2 -2
- ultralytics/cfg/datasets/coco8-pose.yaml +23 -2
- ultralytics/cfg/datasets/coco8-seg.yaml +2 -2
- ultralytics/cfg/datasets/coco8.yaml +2 -2
- ultralytics/cfg/datasets/construction-ppe.yaml +32 -0
- ultralytics/cfg/datasets/crack-seg.yaml +5 -5
- ultralytics/cfg/datasets/dog-pose.yaml +32 -4
- ultralytics/cfg/datasets/dota8-multispectral.yaml +2 -2
- ultralytics/cfg/datasets/dota8.yaml +2 -2
- ultralytics/cfg/datasets/hand-keypoints.yaml +29 -4
- ultralytics/cfg/datasets/lvis.yaml +9 -9
- ultralytics/cfg/datasets/medical-pills.yaml +4 -5
- ultralytics/cfg/datasets/open-images-v7.yaml +7 -10
- ultralytics/cfg/datasets/package-seg.yaml +5 -5
- ultralytics/cfg/datasets/signature.yaml +4 -4
- ultralytics/cfg/datasets/tiger-pose.yaml +20 -4
- ultralytics/cfg/datasets/xView.yaml +5 -5
- ultralytics/cfg/default.yaml +96 -93
- ultralytics/cfg/trackers/botsort.yaml +16 -17
- ultralytics/cfg/trackers/bytetrack.yaml +9 -11
- ultralytics/data/__init__.py +4 -4
- ultralytics/data/annotator.py +12 -12
- ultralytics/data/augment.py +531 -564
- ultralytics/data/base.py +76 -81
- ultralytics/data/build.py +206 -42
- ultralytics/data/converter.py +179 -78
- ultralytics/data/dataset.py +121 -121
- ultralytics/data/loaders.py +114 -91
- ultralytics/data/split.py +28 -15
- ultralytics/data/split_dota.py +67 -48
- ultralytics/data/utils.py +110 -89
- ultralytics/engine/exporter.py +422 -460
- ultralytics/engine/model.py +224 -252
- ultralytics/engine/predictor.py +94 -89
- ultralytics/engine/results.py +345 -595
- ultralytics/engine/trainer.py +231 -134
- ultralytics/engine/tuner.py +279 -73
- ultralytics/engine/validator.py +53 -46
- ultralytics/hub/__init__.py +26 -28
- ultralytics/hub/auth.py +30 -16
- ultralytics/hub/google/__init__.py +34 -36
- ultralytics/hub/session.py +53 -77
- ultralytics/hub/utils.py +23 -109
- ultralytics/models/__init__.py +1 -1
- ultralytics/models/fastsam/__init__.py +1 -1
- ultralytics/models/fastsam/model.py +36 -18
- ultralytics/models/fastsam/predict.py +33 -44
- ultralytics/models/fastsam/utils.py +4 -5
- ultralytics/models/fastsam/val.py +12 -14
- ultralytics/models/nas/__init__.py +1 -1
- ultralytics/models/nas/model.py +16 -20
- ultralytics/models/nas/predict.py +12 -14
- ultralytics/models/nas/val.py +4 -5
- ultralytics/models/rtdetr/__init__.py +1 -1
- ultralytics/models/rtdetr/model.py +9 -9
- ultralytics/models/rtdetr/predict.py +22 -17
- ultralytics/models/rtdetr/train.py +20 -16
- ultralytics/models/rtdetr/val.py +79 -59
- ultralytics/models/sam/__init__.py +8 -2
- ultralytics/models/sam/amg.py +53 -38
- ultralytics/models/sam/build.py +29 -31
- ultralytics/models/sam/model.py +33 -38
- ultralytics/models/sam/modules/blocks.py +159 -182
- ultralytics/models/sam/modules/decoders.py +38 -47
- ultralytics/models/sam/modules/encoders.py +114 -133
- ultralytics/models/sam/modules/memory_attention.py +38 -31
- ultralytics/models/sam/modules/sam.py +114 -93
- ultralytics/models/sam/modules/tiny_encoder.py +268 -291
- ultralytics/models/sam/modules/transformer.py +59 -66
- ultralytics/models/sam/modules/utils.py +55 -72
- ultralytics/models/sam/predict.py +745 -341
- ultralytics/models/utils/loss.py +118 -107
- ultralytics/models/utils/ops.py +118 -71
- ultralytics/models/yolo/__init__.py +1 -1
- ultralytics/models/yolo/classify/predict.py +28 -26
- ultralytics/models/yolo/classify/train.py +50 -81
- ultralytics/models/yolo/classify/val.py +68 -61
- ultralytics/models/yolo/detect/predict.py +12 -15
- ultralytics/models/yolo/detect/train.py +56 -46
- ultralytics/models/yolo/detect/val.py +279 -223
- ultralytics/models/yolo/model.py +167 -86
- ultralytics/models/yolo/obb/predict.py +7 -11
- ultralytics/models/yolo/obb/train.py +23 -25
- ultralytics/models/yolo/obb/val.py +107 -99
- ultralytics/models/yolo/pose/__init__.py +1 -1
- ultralytics/models/yolo/pose/predict.py +12 -14
- ultralytics/models/yolo/pose/train.py +31 -69
- ultralytics/models/yolo/pose/val.py +119 -254
- ultralytics/models/yolo/segment/predict.py +21 -25
- ultralytics/models/yolo/segment/train.py +12 -66
- ultralytics/models/yolo/segment/val.py +126 -305
- ultralytics/models/yolo/world/train.py +53 -45
- ultralytics/models/yolo/world/train_world.py +51 -32
- ultralytics/models/yolo/yoloe/__init__.py +7 -7
- ultralytics/models/yolo/yoloe/predict.py +30 -37
- ultralytics/models/yolo/yoloe/train.py +89 -71
- ultralytics/models/yolo/yoloe/train_seg.py +15 -17
- ultralytics/models/yolo/yoloe/val.py +56 -41
- ultralytics/nn/__init__.py +9 -11
- ultralytics/nn/autobackend.py +179 -107
- ultralytics/nn/modules/__init__.py +67 -67
- ultralytics/nn/modules/activation.py +8 -7
- ultralytics/nn/modules/block.py +302 -323
- ultralytics/nn/modules/conv.py +61 -104
- ultralytics/nn/modules/head.py +488 -186
- ultralytics/nn/modules/transformer.py +183 -123
- ultralytics/nn/modules/utils.py +15 -20
- ultralytics/nn/tasks.py +327 -203
- ultralytics/nn/text_model.py +81 -65
- ultralytics/py.typed +1 -0
- ultralytics/solutions/__init__.py +12 -12
- ultralytics/solutions/ai_gym.py +19 -27
- ultralytics/solutions/analytics.py +36 -26
- ultralytics/solutions/config.py +29 -28
- ultralytics/solutions/distance_calculation.py +23 -24
- ultralytics/solutions/heatmap.py +17 -19
- ultralytics/solutions/instance_segmentation.py +21 -19
- ultralytics/solutions/object_blurrer.py +16 -17
- ultralytics/solutions/object_counter.py +48 -53
- ultralytics/solutions/object_cropper.py +22 -16
- ultralytics/solutions/parking_management.py +61 -58
- ultralytics/solutions/queue_management.py +19 -19
- ultralytics/solutions/region_counter.py +63 -50
- ultralytics/solutions/security_alarm.py +22 -25
- ultralytics/solutions/similarity_search.py +107 -60
- ultralytics/solutions/solutions.py +343 -262
- ultralytics/solutions/speed_estimation.py +35 -31
- ultralytics/solutions/streamlit_inference.py +104 -40
- ultralytics/solutions/templates/similarity-search.html +31 -24
- ultralytics/solutions/trackzone.py +24 -24
- ultralytics/solutions/vision_eye.py +11 -12
- ultralytics/trackers/__init__.py +1 -1
- ultralytics/trackers/basetrack.py +18 -27
- ultralytics/trackers/bot_sort.py +48 -39
- ultralytics/trackers/byte_tracker.py +94 -94
- ultralytics/trackers/track.py +7 -16
- ultralytics/trackers/utils/gmc.py +37 -69
- ultralytics/trackers/utils/kalman_filter.py +68 -76
- ultralytics/trackers/utils/matching.py +13 -17
- ultralytics/utils/__init__.py +251 -275
- ultralytics/utils/autobatch.py +19 -7
- ultralytics/utils/autodevice.py +68 -38
- ultralytics/utils/benchmarks.py +169 -130
- ultralytics/utils/callbacks/base.py +12 -13
- ultralytics/utils/callbacks/clearml.py +14 -15
- ultralytics/utils/callbacks/comet.py +139 -66
- ultralytics/utils/callbacks/dvc.py +19 -27
- ultralytics/utils/callbacks/hub.py +8 -6
- ultralytics/utils/callbacks/mlflow.py +6 -10
- ultralytics/utils/callbacks/neptune.py +11 -19
- ultralytics/utils/callbacks/platform.py +73 -0
- ultralytics/utils/callbacks/raytune.py +3 -4
- ultralytics/utils/callbacks/tensorboard.py +9 -12
- ultralytics/utils/callbacks/wb.py +33 -30
- ultralytics/utils/checks.py +163 -114
- ultralytics/utils/cpu.py +89 -0
- ultralytics/utils/dist.py +24 -20
- ultralytics/utils/downloads.py +176 -146
- ultralytics/utils/errors.py +11 -13
- ultralytics/utils/events.py +113 -0
- ultralytics/utils/export/__init__.py +7 -0
- ultralytics/utils/{export.py → export/engine.py} +81 -63
- ultralytics/utils/export/imx.py +294 -0
- ultralytics/utils/export/tensorflow.py +217 -0
- ultralytics/utils/files.py +33 -36
- ultralytics/utils/git.py +137 -0
- ultralytics/utils/instance.py +105 -120
- ultralytics/utils/logger.py +404 -0
- ultralytics/utils/loss.py +99 -61
- ultralytics/utils/metrics.py +649 -478
- ultralytics/utils/nms.py +337 -0
- ultralytics/utils/ops.py +263 -451
- ultralytics/utils/patches.py +70 -31
- ultralytics/utils/plotting.py +253 -223
- ultralytics/utils/tal.py +48 -61
- ultralytics/utils/torch_utils.py +244 -251
- ultralytics/utils/tqdm.py +438 -0
- ultralytics/utils/triton.py +22 -23
- ultralytics/utils/tuner.py +11 -10
- dgenerate_ultralytics_headless-8.3.137.dist-info/RECORD +0 -272
- {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/entry_points.txt +0 -0
- {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/licenses/LICENSE +0 -0
- {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/top_level.txt +0 -0
ultralytics/engine/exporter.py
CHANGED
|
@@ -20,6 +20,7 @@ MNN | `mnn` | yolo11n.mnn
|
|
|
20
20
|
NCNN | `ncnn` | yolo11n_ncnn_model/
|
|
21
21
|
IMX | `imx` | yolo11n_imx_model/
|
|
22
22
|
RKNN | `rknn` | yolo11n_rknn_model/
|
|
23
|
+
ExecuTorch | `executorch` | yolo11n_executorch_model/
|
|
23
24
|
|
|
24
25
|
Requirements:
|
|
25
26
|
$ pip install "ultralytics[export]"
|
|
@@ -47,6 +48,8 @@ Inference:
|
|
|
47
48
|
yolo11n.mnn # MNN
|
|
48
49
|
yolo11n_ncnn_model # NCNN
|
|
49
50
|
yolo11n_imx_model # IMX
|
|
51
|
+
yolo11n_rknn_model # RKNN
|
|
52
|
+
yolo11n_executorch_model # ExecuTorch
|
|
50
53
|
|
|
51
54
|
TensorFlow.js:
|
|
52
55
|
$ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
|
|
@@ -62,7 +65,6 @@ import shutil
|
|
|
62
65
|
import subprocess
|
|
63
66
|
import time
|
|
64
67
|
import warnings
|
|
65
|
-
from contextlib import contextmanager
|
|
66
68
|
from copy import deepcopy
|
|
67
69
|
from datetime import datetime
|
|
68
70
|
from pathlib import Path
|
|
@@ -90,6 +92,7 @@ from ultralytics.utils import (
|
|
|
90
92
|
RKNN_CHIPS,
|
|
91
93
|
ROOT,
|
|
92
94
|
SETTINGS,
|
|
95
|
+
TORCH_VERSION,
|
|
93
96
|
WINDOWS,
|
|
94
97
|
YAML,
|
|
95
98
|
callbacks,
|
|
@@ -101,20 +104,32 @@ from ultralytics.utils.checks import (
|
|
|
101
104
|
check_is_path_safe,
|
|
102
105
|
check_requirements,
|
|
103
106
|
check_version,
|
|
107
|
+
is_intel,
|
|
104
108
|
is_sudo_available,
|
|
105
109
|
)
|
|
106
|
-
from ultralytics.utils.downloads import
|
|
107
|
-
from ultralytics.utils.export import
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
from ultralytics.utils.downloads import get_github_assets, safe_download
|
|
111
|
+
from ultralytics.utils.export import (
|
|
112
|
+
keras2pb,
|
|
113
|
+
onnx2engine,
|
|
114
|
+
onnx2saved_model,
|
|
115
|
+
pb2tfjs,
|
|
116
|
+
tflite2edgetpu,
|
|
117
|
+
torch2imx,
|
|
118
|
+
torch2onnx,
|
|
119
|
+
)
|
|
120
|
+
from ultralytics.utils.files import file_size
|
|
121
|
+
from ultralytics.utils.metrics import batch_probiou
|
|
122
|
+
from ultralytics.utils.nms import TorchNMS
|
|
123
|
+
from ultralytics.utils.ops import Profile
|
|
124
|
+
from ultralytics.utils.patches import arange_patch
|
|
125
|
+
from ultralytics.utils.torch_utils import TORCH_1_11, TORCH_1_13, TORCH_2_1, TORCH_2_4, TORCH_2_9, select_device
|
|
111
126
|
|
|
112
127
|
|
|
113
128
|
def export_formats():
|
|
114
129
|
"""Return a dictionary of Ultralytics YOLO export formats."""
|
|
115
130
|
x = [
|
|
116
131
|
["PyTorch", "-", ".pt", True, True, []],
|
|
117
|
-
["TorchScript", "torchscript", ".torchscript", True, True, ["batch", "optimize", "half", "nms"]],
|
|
132
|
+
["TorchScript", "torchscript", ".torchscript", True, True, ["batch", "optimize", "half", "nms", "dynamic"]],
|
|
118
133
|
["ONNX", "onnx", ".onnx", True, True, ["batch", "dynamic", "half", "opset", "simplify", "nms"]],
|
|
119
134
|
[
|
|
120
135
|
"OpenVINO",
|
|
@@ -132,7 +147,7 @@ def export_formats():
|
|
|
132
147
|
True,
|
|
133
148
|
["batch", "dynamic", "half", "int8", "simplify", "nms", "fraction"],
|
|
134
149
|
],
|
|
135
|
-
["CoreML", "coreml", ".mlpackage", True, False, ["batch", "half", "int8", "nms"]],
|
|
150
|
+
["CoreML", "coreml", ".mlpackage", True, False, ["batch", "dynamic", "half", "int8", "nms"]],
|
|
136
151
|
["TensorFlow SavedModel", "saved_model", "_saved_model", True, True, ["batch", "int8", "keras", "nms"]],
|
|
137
152
|
["TensorFlow GraphDef", "pb", ".pb", True, True, ["batch"]],
|
|
138
153
|
["TensorFlow Lite", "tflite", ".tflite", True, False, ["batch", "half", "int8", "nms", "fraction"]],
|
|
@@ -141,15 +156,43 @@ def export_formats():
|
|
|
141
156
|
["PaddlePaddle", "paddle", "_paddle_model", True, True, ["batch"]],
|
|
142
157
|
["MNN", "mnn", ".mnn", True, True, ["batch", "half", "int8"]],
|
|
143
158
|
["NCNN", "ncnn", "_ncnn_model", True, True, ["batch", "half"]],
|
|
144
|
-
["IMX", "imx", "_imx_model", True, True, ["int8", "fraction"]],
|
|
159
|
+
["IMX", "imx", "_imx_model", True, True, ["int8", "fraction", "nms"]],
|
|
145
160
|
["RKNN", "rknn", "_rknn_model", False, False, ["batch", "name"]],
|
|
161
|
+
["ExecuTorch", "executorch", "_executorch_model", True, False, ["batch"]],
|
|
146
162
|
]
|
|
147
163
|
return dict(zip(["Format", "Argument", "Suffix", "CPU", "GPU", "Arguments"], zip(*x)))
|
|
148
164
|
|
|
149
165
|
|
|
166
|
+
def best_onnx_opset(onnx, cuda=False) -> int:
|
|
167
|
+
"""Return max ONNX opset for this torch version with ONNX fallback."""
|
|
168
|
+
version = ".".join(TORCH_VERSION.split(".")[:2])
|
|
169
|
+
if TORCH_2_4: # _constants.ONNX_MAX_OPSET first defined in torch 1.13
|
|
170
|
+
opset = torch.onnx.utils._constants.ONNX_MAX_OPSET - 1 # use second-latest version for safety
|
|
171
|
+
if cuda:
|
|
172
|
+
opset -= 2 # fix CUDA ONNXRuntime NMS squeeze op errors
|
|
173
|
+
else:
|
|
174
|
+
opset = {
|
|
175
|
+
"1.8": 12,
|
|
176
|
+
"1.9": 12,
|
|
177
|
+
"1.10": 13,
|
|
178
|
+
"1.11": 14,
|
|
179
|
+
"1.12": 15,
|
|
180
|
+
"1.13": 17,
|
|
181
|
+
"2.0": 17, # reduced from 18 to fix ONNX errors
|
|
182
|
+
"2.1": 17, # reduced from 19
|
|
183
|
+
"2.2": 17, # reduced from 19
|
|
184
|
+
"2.3": 17, # reduced from 19
|
|
185
|
+
"2.4": 20,
|
|
186
|
+
"2.5": 20,
|
|
187
|
+
"2.6": 20,
|
|
188
|
+
"2.7": 20,
|
|
189
|
+
"2.8": 23,
|
|
190
|
+
}.get(version, 12)
|
|
191
|
+
return min(opset, onnx.defs.onnx_opset_version())
|
|
192
|
+
|
|
193
|
+
|
|
150
194
|
def validate_args(format, passed_args, valid_args):
|
|
151
|
-
"""
|
|
152
|
-
Validate arguments based on the export format.
|
|
195
|
+
"""Validate arguments based on the export format.
|
|
153
196
|
|
|
154
197
|
Args:
|
|
155
198
|
format (str): The export format.
|
|
@@ -170,15 +213,6 @@ def validate_args(format, passed_args, valid_args):
|
|
|
170
213
|
assert arg in valid_args, f"ERROR ❌️ argument '{arg}' is not supported for format='{format}'"
|
|
171
214
|
|
|
172
215
|
|
|
173
|
-
def gd_outputs(gd):
|
|
174
|
-
"""Return TensorFlow GraphDef model output node names."""
|
|
175
|
-
name_list, input_list = [], []
|
|
176
|
-
for node in gd.node: # tensorflow.core.framework.node_def_pb2.NodeDef
|
|
177
|
-
name_list.append(node.name)
|
|
178
|
-
input_list.extend(node.input)
|
|
179
|
-
return sorted(f"{x}:0" for x in list(set(name_list) - set(input_list)) if not x.startswith("NoOp"))
|
|
180
|
-
|
|
181
|
-
|
|
182
216
|
def try_export(inner_func):
|
|
183
217
|
"""YOLO export decorator, i.e. @try_export."""
|
|
184
218
|
inner_args = get_default_args(inner_func)
|
|
@@ -189,9 +223,12 @@ def try_export(inner_func):
|
|
|
189
223
|
dt = 0.0
|
|
190
224
|
try:
|
|
191
225
|
with Profile() as dt:
|
|
192
|
-
f
|
|
193
|
-
|
|
194
|
-
|
|
226
|
+
f = inner_func(*args, **kwargs) # exported file/dir or tuple of (file/dir, *)
|
|
227
|
+
path = f if isinstance(f, (str, Path)) else f[0]
|
|
228
|
+
mb = file_size(path)
|
|
229
|
+
assert mb > 0.0, "0.0 MB output model size"
|
|
230
|
+
LOGGER.info(f"{prefix} export success ✅ {dt.t:.1f}s, saved as '{path}' ({mb:.1f} MB)")
|
|
231
|
+
return f
|
|
195
232
|
except Exception as e:
|
|
196
233
|
LOGGER.error(f"{prefix} export failure {dt.t:.1f}s: {e}")
|
|
197
234
|
raise e
|
|
@@ -199,39 +236,58 @@ def try_export(inner_func):
|
|
|
199
236
|
return outer_func
|
|
200
237
|
|
|
201
238
|
|
|
202
|
-
@contextmanager
|
|
203
|
-
def arange_patch(args):
|
|
204
|
-
"""
|
|
205
|
-
Workaround for ONNX torch.arange incompatibility with FP16.
|
|
206
|
-
|
|
207
|
-
https://github.com/pytorch/pytorch/issues/148041.
|
|
208
|
-
"""
|
|
209
|
-
if args.dynamic and args.half and args.format == "onnx":
|
|
210
|
-
func = torch.arange
|
|
211
|
-
|
|
212
|
-
def arange(*args, dtype=None, **kwargs):
|
|
213
|
-
"""Return a 1-D tensor of size with values from the interval and common difference."""
|
|
214
|
-
return func(*args, **kwargs).to(dtype) # cast to dtype instead of passing dtype
|
|
215
|
-
|
|
216
|
-
torch.arange = arange # patch
|
|
217
|
-
yield
|
|
218
|
-
torch.arange = func # unpatch
|
|
219
|
-
else:
|
|
220
|
-
yield
|
|
221
|
-
|
|
222
|
-
|
|
223
239
|
class Exporter:
|
|
224
|
-
"""
|
|
225
|
-
|
|
240
|
+
"""A class for exporting YOLO models to various formats.
|
|
241
|
+
|
|
242
|
+
This class provides functionality to export YOLO models to different formats including ONNX, TensorRT, CoreML,
|
|
243
|
+
TensorFlow, and others. It handles format validation, device selection, model preparation, and the actual export
|
|
244
|
+
process for each supported format.
|
|
226
245
|
|
|
227
246
|
Attributes:
|
|
228
|
-
args (SimpleNamespace): Configuration for the exporter.
|
|
229
|
-
callbacks (
|
|
247
|
+
args (SimpleNamespace): Configuration arguments for the exporter.
|
|
248
|
+
callbacks (dict): Dictionary of callback functions for different export events.
|
|
249
|
+
im (torch.Tensor): Input tensor for model inference during export.
|
|
250
|
+
model (torch.nn.Module): The YOLO model to be exported.
|
|
251
|
+
file (Path): Path to the model file being exported.
|
|
252
|
+
output_shape (tuple): Shape of the model output tensor(s).
|
|
253
|
+
pretty_name (str): Formatted model name for display purposes.
|
|
254
|
+
metadata (dict): Model metadata including description, author, version, etc.
|
|
255
|
+
device (torch.device): Device on which the model is loaded.
|
|
256
|
+
imgsz (tuple): Input image size for the model.
|
|
257
|
+
|
|
258
|
+
Methods:
|
|
259
|
+
__call__: Main export method that handles the export process.
|
|
260
|
+
get_int8_calibration_dataloader: Build dataloader for INT8 calibration.
|
|
261
|
+
export_torchscript: Export model to TorchScript format.
|
|
262
|
+
export_onnx: Export model to ONNX format.
|
|
263
|
+
export_openvino: Export model to OpenVINO format.
|
|
264
|
+
export_paddle: Export model to PaddlePaddle format.
|
|
265
|
+
export_mnn: Export model to MNN format.
|
|
266
|
+
export_ncnn: Export model to NCNN format.
|
|
267
|
+
export_coreml: Export model to CoreML format.
|
|
268
|
+
export_engine: Export model to TensorRT format.
|
|
269
|
+
export_saved_model: Export model to TensorFlow SavedModel format.
|
|
270
|
+
export_pb: Export model to TensorFlow GraphDef format.
|
|
271
|
+
export_tflite: Export model to TensorFlow Lite format.
|
|
272
|
+
export_edgetpu: Export model to Edge TPU format.
|
|
273
|
+
export_tfjs: Export model to TensorFlow.js format.
|
|
274
|
+
export_rknn: Export model to RKNN format.
|
|
275
|
+
export_imx: Export model to IMX format.
|
|
276
|
+
|
|
277
|
+
Examples:
|
|
278
|
+
Export a YOLOv8 model to ONNX format
|
|
279
|
+
>>> from ultralytics.engine.exporter import Exporter
|
|
280
|
+
>>> exporter = Exporter()
|
|
281
|
+
>>> exporter(model="yolov8n.pt") # exports to yolov8n.onnx
|
|
282
|
+
|
|
283
|
+
Export with specific arguments
|
|
284
|
+
>>> args = {"format": "onnx", "dynamic": True, "half": True}
|
|
285
|
+
>>> exporter = Exporter(overrides=args)
|
|
286
|
+
>>> exporter(model="yolov8n.pt")
|
|
230
287
|
"""
|
|
231
288
|
|
|
232
289
|
def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
|
|
233
|
-
"""
|
|
234
|
-
Initialize the Exporter class.
|
|
290
|
+
"""Initialize the Exporter class.
|
|
235
291
|
|
|
236
292
|
Args:
|
|
237
293
|
cfg (str, optional): Path to a configuration file.
|
|
@@ -244,7 +300,6 @@ class Exporter:
|
|
|
244
300
|
|
|
245
301
|
def __call__(self, model=None) -> str:
|
|
246
302
|
"""Return list of exported files/dirs after running callbacks."""
|
|
247
|
-
self.run_callbacks("on_export_start")
|
|
248
303
|
t = time.time()
|
|
249
304
|
fmt = self.args.format.lower() # to lowercase
|
|
250
305
|
if fmt in {"tensorrt", "trt"}: # 'engine' aliases
|
|
@@ -259,25 +314,41 @@ class Exporter:
|
|
|
259
314
|
# Get the closest match if format is invalid
|
|
260
315
|
matches = difflib.get_close_matches(fmt, fmts, n=1, cutoff=0.6) # 60% similarity required to match
|
|
261
316
|
if not matches:
|
|
262
|
-
|
|
317
|
+
msg = "Model is already in PyTorch format." if fmt == "pt" else f"Invalid export format='{fmt}'."
|
|
318
|
+
raise ValueError(f"{msg} Valid formats are {fmts}")
|
|
263
319
|
LOGGER.warning(f"Invalid export format='{fmt}', updating to format='{matches[0]}'")
|
|
264
320
|
fmt = matches[0]
|
|
265
321
|
flags = [x == fmt for x in fmts]
|
|
266
322
|
if sum(flags) != 1:
|
|
267
323
|
raise ValueError(f"Invalid export format='{fmt}'. Valid formats are {fmts}")
|
|
268
|
-
(
|
|
269
|
-
|
|
270
|
-
|
|
324
|
+
(
|
|
325
|
+
jit,
|
|
326
|
+
onnx,
|
|
327
|
+
xml,
|
|
328
|
+
engine,
|
|
329
|
+
coreml,
|
|
330
|
+
saved_model,
|
|
331
|
+
pb,
|
|
332
|
+
tflite,
|
|
333
|
+
edgetpu,
|
|
334
|
+
tfjs,
|
|
335
|
+
paddle,
|
|
336
|
+
mnn,
|
|
337
|
+
ncnn,
|
|
338
|
+
imx,
|
|
339
|
+
rknn,
|
|
340
|
+
executorch,
|
|
341
|
+
) = flags # export booleans
|
|
271
342
|
|
|
272
343
|
is_tf_format = any((saved_model, pb, tflite, edgetpu, tfjs))
|
|
273
344
|
|
|
274
345
|
# Device
|
|
275
346
|
dla = None
|
|
276
|
-
if
|
|
347
|
+
if engine and self.args.device is None:
|
|
277
348
|
LOGGER.warning("TensorRT requires GPU export, automatically assigning device=0")
|
|
278
349
|
self.args.device = "0"
|
|
279
|
-
if
|
|
280
|
-
dla = self.args.device.
|
|
350
|
+
if engine and "dla" in str(self.args.device): # convert int/list to str first
|
|
351
|
+
dla = self.args.device.rsplit(":", 1)[-1]
|
|
281
352
|
self.args.device = "0" # update device to "0"
|
|
282
353
|
assert dla in {"0", "1"}, f"Expected self.args.device='dla:0' or 'dla:1, but got {self.args.device}."
|
|
283
354
|
if imx and self.args.device is None and torch.cuda.is_available():
|
|
@@ -292,20 +363,21 @@ class Exporter:
|
|
|
292
363
|
if not self.args.int8:
|
|
293
364
|
LOGGER.warning("IMX export requires int8=True, setting int8=True.")
|
|
294
365
|
self.args.int8 = True
|
|
295
|
-
if model.task
|
|
296
|
-
|
|
366
|
+
if not self.args.nms and model.task in {"detect", "pose"}:
|
|
367
|
+
LOGGER.warning("IMX export requires nms=True, setting nms=True.")
|
|
368
|
+
self.args.nms = True
|
|
369
|
+
if model.task not in {"detect", "pose", "classify"}:
|
|
370
|
+
raise ValueError("IMX export only supported for detection, pose estimation, and classification models.")
|
|
297
371
|
if not hasattr(model, "names"):
|
|
298
372
|
model.names = default_class_names()
|
|
299
373
|
model.names = check_class_names(model.names)
|
|
300
374
|
if self.args.half and self.args.int8:
|
|
301
375
|
LOGGER.warning("half=True and int8=True are mutually exclusive, setting half=False.")
|
|
302
376
|
self.args.half = False
|
|
303
|
-
if self.args.half and onnx and self.device.type == "cpu":
|
|
304
|
-
LOGGER.warning("half=True only compatible with GPU export, i.e. use device=0")
|
|
377
|
+
if self.args.half and (onnx or jit) and self.device.type == "cpu":
|
|
378
|
+
LOGGER.warning("half=True only compatible with GPU export, i.e. use device=0, setting half=False.")
|
|
305
379
|
self.args.half = False
|
|
306
380
|
self.imgsz = check_imgsz(self.args.imgsz, stride=model.stride, min_dim=2) # check image size
|
|
307
|
-
if self.args.int8 and engine:
|
|
308
|
-
self.args.dynamic = True # enforce dynamic to export TensorRT INT8
|
|
309
381
|
if self.args.optimize:
|
|
310
382
|
assert not ncnn, "optimize=True not compatible with format='ncnn', i.e. use optimize=False"
|
|
311
383
|
assert self.device.type == "cpu", "optimize=True not compatible with cuda devices, i.e. use device='cpu'"
|
|
@@ -320,15 +392,19 @@ class Exporter:
|
|
|
320
392
|
assert self.args.name in RKNN_CHIPS, (
|
|
321
393
|
f"Invalid processor name '{self.args.name}' for Rockchip RKNN export. Valid names are {RKNN_CHIPS}."
|
|
322
394
|
)
|
|
323
|
-
if self.args.int8 and tflite:
|
|
324
|
-
assert not getattr(model, "end2end", False), "TFLite INT8 export not supported for end2end models."
|
|
325
395
|
if self.args.nms:
|
|
326
396
|
assert not isinstance(model, ClassificationModel), "'nms=True' is not valid for classification models."
|
|
327
|
-
assert not
|
|
328
|
-
|
|
397
|
+
assert not tflite or not ARM64 or not LINUX, "TFLite export with NMS unsupported on ARM64 Linux"
|
|
398
|
+
assert not is_tf_format or TORCH_1_13, "TensorFlow exports with NMS require torch>=1.13"
|
|
399
|
+
assert not onnx or TORCH_1_13, "ONNX export with NMS requires torch>=1.13"
|
|
400
|
+
if getattr(model, "end2end", False) or isinstance(model.model[-1], RTDETRDecoder):
|
|
329
401
|
LOGGER.warning("'nms=True' is not available for end2end models. Forcing 'nms=False'.")
|
|
330
402
|
self.args.nms = False
|
|
331
403
|
self.args.conf = self.args.conf or 0.25 # set conf default value for nms export
|
|
404
|
+
if (engine or coreml or self.args.nms) and self.args.dynamic and self.args.batch == 1:
|
|
405
|
+
LOGGER.warning(
|
|
406
|
+
f"'dynamic=True' model with '{'nms=True' if self.args.nms else f'format={self.args.format}'}' requires max batch size, i.e. 'batch=16'"
|
|
407
|
+
)
|
|
332
408
|
if edgetpu:
|
|
333
409
|
if not LINUX or ARM64:
|
|
334
410
|
raise SystemError(
|
|
@@ -354,9 +430,9 @@ class Exporter:
|
|
|
354
430
|
raise SystemError("TF.js exports are not currently supported on ARM64 Linux")
|
|
355
431
|
# Recommend OpenVINO if export and Intel CPU
|
|
356
432
|
if SETTINGS.get("openvino_msg"):
|
|
357
|
-
if
|
|
433
|
+
if is_intel():
|
|
358
434
|
LOGGER.info(
|
|
359
|
-
"💡 ProTip: Export to OpenVINO format for best performance on Intel
|
|
435
|
+
"💡 ProTip: Export to OpenVINO format for best performance on Intel hardware."
|
|
360
436
|
" Learn more at https://docs.ultralytics.com/integrations/openvino/"
|
|
361
437
|
)
|
|
362
438
|
SETTINGS["openvino_msg"] = False
|
|
@@ -378,9 +454,13 @@ class Exporter:
|
|
|
378
454
|
model = model.fuse()
|
|
379
455
|
|
|
380
456
|
if imx:
|
|
381
|
-
from ultralytics.utils.
|
|
457
|
+
from ultralytics.utils.export.imx import FXModel
|
|
458
|
+
|
|
459
|
+
model = FXModel(model, self.imgsz)
|
|
460
|
+
if tflite or edgetpu:
|
|
461
|
+
from ultralytics.utils.export.tensorflow import tf_wrapper
|
|
382
462
|
|
|
383
|
-
model =
|
|
463
|
+
model = tf_wrapper(model)
|
|
384
464
|
for m in model.modules():
|
|
385
465
|
if isinstance(m, Classify):
|
|
386
466
|
m.export = True
|
|
@@ -390,23 +470,16 @@ class Exporter:
|
|
|
390
470
|
m.format = self.args.format
|
|
391
471
|
m.max_det = self.args.max_det
|
|
392
472
|
m.xyxy = self.args.nms and not coreml
|
|
473
|
+
if hasattr(model, "pe") and hasattr(m, "fuse"): # for YOLOE models
|
|
474
|
+
m.fuse(model.pe.to(self.device))
|
|
393
475
|
elif isinstance(m, C2f) and not is_tf_format:
|
|
394
476
|
# EdgeTPU does not support FlexSplitV while split provides cleaner ONNX graph
|
|
395
477
|
m.forward = m.forward_split
|
|
396
|
-
if isinstance(m, Detect) and imx:
|
|
397
|
-
from ultralytics.utils.tal import make_anchors
|
|
398
|
-
|
|
399
|
-
m.anchors, m.strides = (
|
|
400
|
-
x.transpose(0, 1)
|
|
401
|
-
for x in make_anchors(
|
|
402
|
-
torch.cat([s / m.stride.unsqueeze(-1) for s in self.imgsz], dim=1), m.stride, 0.5
|
|
403
|
-
)
|
|
404
|
-
)
|
|
405
478
|
|
|
406
479
|
y = None
|
|
407
480
|
for _ in range(2): # dry runs
|
|
408
|
-
y = NMSModel(model, self.args)(im) if self.args.nms and not coreml else model(im)
|
|
409
|
-
if self.args.half and onnx and self.device.type != "cpu":
|
|
481
|
+
y = NMSModel(model, self.args)(im) if self.args.nms and not coreml and not imx else model(im)
|
|
482
|
+
if self.args.half and (onnx or jit) and self.device.type != "cpu":
|
|
410
483
|
im, model = im.half(), model.half() # to FP16
|
|
411
484
|
|
|
412
485
|
# Filter warnings
|
|
@@ -445,45 +518,49 @@ class Exporter:
|
|
|
445
518
|
self.metadata["dla"] = dla # make sure `AutoBackend` uses correct dla device if it has one
|
|
446
519
|
if model.task == "pose":
|
|
447
520
|
self.metadata["kpt_shape"] = model.model[-1].kpt_shape
|
|
521
|
+
if hasattr(model, "kpt_names"):
|
|
522
|
+
self.metadata["kpt_names"] = model.kpt_names
|
|
448
523
|
|
|
449
524
|
LOGGER.info(
|
|
450
525
|
f"\n{colorstr('PyTorch:')} starting from '{file}' with input shape {tuple(im.shape)} BCHW and "
|
|
451
526
|
f"output shape(s) {self.output_shape} ({file_size(file):.1f} MB)"
|
|
452
527
|
)
|
|
453
|
-
|
|
528
|
+
self.run_callbacks("on_export_start")
|
|
454
529
|
# Exports
|
|
455
530
|
f = [""] * len(fmts) # exported filenames
|
|
456
|
-
if jit
|
|
457
|
-
f[0]
|
|
531
|
+
if jit: # TorchScript
|
|
532
|
+
f[0] = self.export_torchscript()
|
|
458
533
|
if engine: # TensorRT required before ONNX
|
|
459
|
-
f[1]
|
|
460
|
-
if onnx: # ONNX
|
|
461
|
-
f[2]
|
|
534
|
+
f[1] = self.export_engine(dla=dla)
|
|
535
|
+
if onnx or ncnn: # ONNX
|
|
536
|
+
f[2] = self.export_onnx()
|
|
462
537
|
if xml: # OpenVINO
|
|
463
|
-
f[3]
|
|
538
|
+
f[3] = self.export_openvino()
|
|
464
539
|
if coreml: # CoreML
|
|
465
|
-
f[4]
|
|
540
|
+
f[4] = self.export_coreml()
|
|
466
541
|
if is_tf_format: # TensorFlow formats
|
|
467
542
|
self.args.int8 |= edgetpu
|
|
468
543
|
f[5], keras_model = self.export_saved_model()
|
|
469
544
|
if pb or tfjs: # pb prerequisite to tfjs
|
|
470
|
-
f[6]
|
|
545
|
+
f[6] = self.export_pb(keras_model=keras_model)
|
|
471
546
|
if tflite:
|
|
472
|
-
f[7]
|
|
547
|
+
f[7] = self.export_tflite()
|
|
473
548
|
if edgetpu:
|
|
474
|
-
f[8]
|
|
549
|
+
f[8] = self.export_edgetpu(tflite_model=Path(f[5]) / f"{self.file.stem}_full_integer_quant.tflite")
|
|
475
550
|
if tfjs:
|
|
476
|
-
f[9]
|
|
551
|
+
f[9] = self.export_tfjs()
|
|
477
552
|
if paddle: # PaddlePaddle
|
|
478
|
-
f[10]
|
|
553
|
+
f[10] = self.export_paddle()
|
|
479
554
|
if mnn: # MNN
|
|
480
|
-
f[11]
|
|
555
|
+
f[11] = self.export_mnn()
|
|
481
556
|
if ncnn: # NCNN
|
|
482
|
-
f[12]
|
|
557
|
+
f[12] = self.export_ncnn()
|
|
483
558
|
if imx:
|
|
484
|
-
f[13]
|
|
559
|
+
f[13] = self.export_imx()
|
|
485
560
|
if rknn:
|
|
486
|
-
f[14]
|
|
561
|
+
f[14] = self.export_rknn()
|
|
562
|
+
if executorch:
|
|
563
|
+
f[15] = self.export_executorch()
|
|
487
564
|
|
|
488
565
|
# Finish
|
|
489
566
|
f = [str(x) for x in f if x] # filter out '' and None
|
|
@@ -497,7 +574,7 @@ class Exporter:
|
|
|
497
574
|
f"work. Use export 'imgsz={max(self.imgsz)}' if val is required."
|
|
498
575
|
)
|
|
499
576
|
imgsz = self.imgsz[0] if square else str(self.imgsz)[1:-1].replace(" ", "")
|
|
500
|
-
predict_data = f"data={data}" if model.task == "segment" and
|
|
577
|
+
predict_data = f"data={data}" if model.task == "segment" and pb else ""
|
|
501
578
|
q = "int8" if self.args.int8 else "half" if self.args.half else "" # quantization
|
|
502
579
|
LOGGER.info(
|
|
503
580
|
f"\nExport complete ({time.time() - t:.1f}s)"
|
|
@@ -514,8 +591,6 @@ class Exporter:
|
|
|
514
591
|
"""Build and return a dataloader for calibration of INT8 models."""
|
|
515
592
|
LOGGER.info(f"{prefix} collecting INT8 calibration images from 'data={self.args.data}'")
|
|
516
593
|
data = (check_cls_dataset if self.model.task == "classify" else check_det_dataset)(self.args.data)
|
|
517
|
-
# TensorRT INT8 calibration should use 2x batch size
|
|
518
|
-
batch = self.args.batch * (2 if self.args.format == "engine" else 1)
|
|
519
594
|
dataset = YOLODataset(
|
|
520
595
|
data[self.args.split or "val"],
|
|
521
596
|
data=data,
|
|
@@ -523,7 +598,7 @@ class Exporter:
|
|
|
523
598
|
task=self.model.task,
|
|
524
599
|
imgsz=self.imgsz[0],
|
|
525
600
|
augment=False,
|
|
526
|
-
batch_size=batch,
|
|
601
|
+
batch_size=self.args.batch,
|
|
527
602
|
)
|
|
528
603
|
n = len(dataset)
|
|
529
604
|
if n < self.args.batch:
|
|
@@ -533,12 +608,12 @@ class Exporter:
|
|
|
533
608
|
)
|
|
534
609
|
elif n < 300:
|
|
535
610
|
LOGGER.warning(f"{prefix} >300 images recommended for INT8 calibration, found {n} images.")
|
|
536
|
-
return build_dataloader(dataset, batch=batch, workers=0) # required for batch loading
|
|
611
|
+
return build_dataloader(dataset, batch=self.args.batch, workers=0, drop_last=True) # required for batch loading
|
|
537
612
|
|
|
538
613
|
@try_export
|
|
539
614
|
def export_torchscript(self, prefix=colorstr("TorchScript:")):
|
|
540
|
-
"""YOLO TorchScript
|
|
541
|
-
LOGGER.info(f"\n{prefix} starting export with torch {
|
|
615
|
+
"""Export YOLO model to TorchScript format."""
|
|
616
|
+
LOGGER.info(f"\n{prefix} starting export with torch {TORCH_VERSION}...")
|
|
542
617
|
f = self.file.with_suffix(".torchscript")
|
|
543
618
|
|
|
544
619
|
ts = torch.jit.trace(NMSModel(self.model, self.args) if self.args.nms else self.model, self.im, strict=False)
|
|
@@ -550,21 +625,24 @@ class Exporter:
|
|
|
550
625
|
optimize_for_mobile(ts)._save_for_lite_interpreter(str(f), _extra_files=extra_files)
|
|
551
626
|
else:
|
|
552
627
|
ts.save(str(f), _extra_files=extra_files)
|
|
553
|
-
return f
|
|
628
|
+
return f
|
|
554
629
|
|
|
555
630
|
@try_export
|
|
556
631
|
def export_onnx(self, prefix=colorstr("ONNX:")):
|
|
557
|
-
"""YOLO ONNX
|
|
558
|
-
requirements = ["onnx>=1.12.0
|
|
632
|
+
"""Export YOLO model to ONNX format."""
|
|
633
|
+
requirements = ["onnx>=1.12.0"]
|
|
559
634
|
if self.args.simplify:
|
|
560
|
-
requirements += ["onnxslim>=0.1.
|
|
635
|
+
requirements += ["onnxslim>=0.1.71", "onnxruntime" + ("-gpu" if torch.cuda.is_available() else "")]
|
|
561
636
|
check_requirements(requirements)
|
|
562
|
-
import onnx
|
|
637
|
+
import onnx
|
|
638
|
+
|
|
639
|
+
opset = self.args.opset or best_onnx_opset(onnx, cuda="cuda" in self.device.type)
|
|
640
|
+
LOGGER.info(f"\n{prefix} starting export with onnx {onnx.__version__} opset {opset}...")
|
|
641
|
+
if self.args.nms:
|
|
642
|
+
assert TORCH_1_13, f"'nms=True' ONNX export requires torch>=1.13 (found torch=={TORCH_VERSION})"
|
|
563
643
|
|
|
564
|
-
opset_version = self.args.opset or get_latest_opset()
|
|
565
|
-
LOGGER.info(f"\n{prefix} starting export with onnx {onnx.__version__} opset {opset_version}...")
|
|
566
644
|
f = str(self.file.with_suffix(".onnx"))
|
|
567
|
-
output_names = ["output0", "output1"] if
|
|
645
|
+
output_names = ["output0", "output1"] if self.model.task == "segment" else ["output0"]
|
|
568
646
|
dynamic = self.args.dynamic
|
|
569
647
|
if dynamic:
|
|
570
648
|
dynamic = {"images": {0: "batch", 2: "height", 3: "width"}} # shape(1,3,640,640)
|
|
@@ -576,14 +654,14 @@ class Exporter:
|
|
|
576
654
|
if self.args.nms: # only batch size is dynamic with NMS
|
|
577
655
|
dynamic["output0"].pop(2)
|
|
578
656
|
if self.args.nms and self.model.task == "obb":
|
|
579
|
-
self.args.opset =
|
|
657
|
+
self.args.opset = opset # for NMSModel
|
|
580
658
|
|
|
581
659
|
with arange_patch(self.args):
|
|
582
|
-
|
|
660
|
+
torch2onnx(
|
|
583
661
|
NMSModel(self.model, self.args) if self.args.nms else self.model,
|
|
584
662
|
self.im,
|
|
585
663
|
f,
|
|
586
|
-
opset=
|
|
664
|
+
opset=opset,
|
|
587
665
|
input_names=["images"],
|
|
588
666
|
output_names=output_names,
|
|
589
667
|
dynamic=dynamic or None,
|
|
@@ -608,20 +686,23 @@ class Exporter:
|
|
|
608
686
|
meta = model_onnx.metadata_props.add()
|
|
609
687
|
meta.key, meta.value = k, str(v)
|
|
610
688
|
|
|
689
|
+
# IR version
|
|
690
|
+
if getattr(model_onnx, "ir_version", 0) > 10:
|
|
691
|
+
LOGGER.info(f"{prefix} limiting IR version {model_onnx.ir_version} to 10 for ONNXRuntime compatibility...")
|
|
692
|
+
model_onnx.ir_version = 10
|
|
693
|
+
|
|
611
694
|
onnx.save(model_onnx, f)
|
|
612
|
-
return f
|
|
695
|
+
return f
|
|
613
696
|
|
|
614
697
|
@try_export
|
|
615
698
|
def export_openvino(self, prefix=colorstr("OpenVINO:")):
|
|
616
|
-
"""YOLO OpenVINO
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
check_version(MACOS_VERSION, "<15.4", name="macOS ", hard=True, msg=msg)
|
|
620
|
-
check_requirements("openvino>=2024.0.0")
|
|
699
|
+
"""Export YOLO model to OpenVINO format."""
|
|
700
|
+
# OpenVINO <= 2025.1.0 error on macOS 15.4+: https://github.com/openvinotoolkit/openvino/issues/30023"
|
|
701
|
+
check_requirements("openvino>=2025.2.0" if MACOS and MACOS_VERSION >= "15.4" else "openvino>=2024.0.0")
|
|
621
702
|
import openvino as ov
|
|
622
703
|
|
|
623
704
|
LOGGER.info(f"\n{prefix} starting export with openvino {ov.__version__}...")
|
|
624
|
-
assert
|
|
705
|
+
assert TORCH_2_1, f"OpenVINO export requires torch>=2.1 but torch=={TORCH_VERSION} is installed"
|
|
625
706
|
ov_model = ov.convert_model(
|
|
626
707
|
NMSModel(self.model, self.args) if self.args.nms else self.model,
|
|
627
708
|
input=None if self.args.dynamic else [self.im.shape],
|
|
@@ -680,36 +761,45 @@ class Exporter:
|
|
|
680
761
|
ignored_scope=ignored_scope,
|
|
681
762
|
)
|
|
682
763
|
serialize(quantized_ov_model, fq_ov)
|
|
683
|
-
return fq
|
|
764
|
+
return fq
|
|
684
765
|
|
|
685
766
|
f = str(self.file).replace(self.file.suffix, f"_openvino_model{os.sep}")
|
|
686
767
|
f_ov = str(Path(f) / self.file.with_suffix(".xml").name)
|
|
687
768
|
|
|
688
769
|
serialize(ov_model, f_ov)
|
|
689
|
-
return f
|
|
770
|
+
return f
|
|
690
771
|
|
|
691
772
|
@try_export
|
|
692
773
|
def export_paddle(self, prefix=colorstr("PaddlePaddle:")):
|
|
693
|
-
"""YOLO
|
|
774
|
+
"""Export YOLO model to PaddlePaddle format."""
|
|
694
775
|
assert not IS_JETSON, "Jetson Paddle exports not supported yet"
|
|
695
|
-
check_requirements(
|
|
696
|
-
|
|
697
|
-
|
|
776
|
+
check_requirements(
|
|
777
|
+
(
|
|
778
|
+
"paddlepaddle-gpu"
|
|
779
|
+
if torch.cuda.is_available()
|
|
780
|
+
else "paddlepaddle==3.0.0" # pin 3.0.0 for ARM64
|
|
781
|
+
if ARM64
|
|
782
|
+
else "paddlepaddle>=3.0.0",
|
|
783
|
+
"x2paddle",
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
import x2paddle
|
|
787
|
+
from x2paddle.convert import pytorch2paddle
|
|
698
788
|
|
|
699
789
|
LOGGER.info(f"\n{prefix} starting export with X2Paddle {x2paddle.__version__}...")
|
|
700
790
|
f = str(self.file).replace(self.file.suffix, f"_paddle_model{os.sep}")
|
|
701
791
|
|
|
702
792
|
pytorch2paddle(module=self.model, save_dir=f, jit_type="trace", input_examples=[self.im]) # export
|
|
703
793
|
YAML.save(Path(f) / "metadata.yaml", self.metadata) # add metadata.yaml
|
|
704
|
-
return f
|
|
794
|
+
return f
|
|
705
795
|
|
|
706
796
|
@try_export
|
|
707
797
|
def export_mnn(self, prefix=colorstr("MNN:")):
|
|
708
|
-
"""YOLO MNN
|
|
709
|
-
f_onnx
|
|
798
|
+
"""Export YOLO model to MNN format using MNN https://github.com/alibaba/MNN."""
|
|
799
|
+
f_onnx = self.export_onnx() # get onnx model first
|
|
710
800
|
|
|
711
801
|
check_requirements("MNN>=2.9.6")
|
|
712
|
-
import MNN
|
|
802
|
+
import MNN
|
|
713
803
|
from MNN.tools import mnnconvert
|
|
714
804
|
|
|
715
805
|
# Setup and checks
|
|
@@ -726,17 +816,17 @@ class Exporter:
|
|
|
726
816
|
convert_scratch = Path(self.file.parent / ".__convert_external_data.bin")
|
|
727
817
|
if convert_scratch.exists():
|
|
728
818
|
convert_scratch.unlink()
|
|
729
|
-
return f
|
|
819
|
+
return f
|
|
730
820
|
|
|
731
821
|
@try_export
|
|
732
822
|
def export_ncnn(self, prefix=colorstr("NCNN:")):
|
|
733
|
-
"""YOLO NCNN
|
|
734
|
-
check_requirements("ncnn")
|
|
735
|
-
import ncnn
|
|
823
|
+
"""Export YOLO model to NCNN format using PNNX https://github.com/pnnx/pnnx."""
|
|
824
|
+
check_requirements("ncnn", cmds="--no-deps") # no deps to avoid installing opencv-python
|
|
825
|
+
import ncnn
|
|
736
826
|
|
|
737
827
|
LOGGER.info(f"\n{prefix} starting export with NCNN {ncnn.__version__}...")
|
|
738
828
|
f = Path(str(self.file).replace(self.file.suffix, f"_ncnn_model{os.sep}"))
|
|
739
|
-
|
|
829
|
+
f_onnx = self.file.with_suffix(".onnx")
|
|
740
830
|
|
|
741
831
|
name = Path("pnnx.exe" if WINDOWS else "pnnx") # PNNX filename
|
|
742
832
|
pnnx = name if name.is_file() else (ROOT / name)
|
|
@@ -749,11 +839,11 @@ class Exporter:
|
|
|
749
839
|
system = "macos" if MACOS else "windows" if WINDOWS else "linux-aarch64" if ARM64 else "linux"
|
|
750
840
|
try:
|
|
751
841
|
release, assets = get_github_assets(repo="pnnx/pnnx")
|
|
752
|
-
asset =
|
|
753
|
-
assert isinstance(asset, str), "Unable to retrieve PNNX repo assets" # i.e. pnnx-
|
|
842
|
+
asset = next(x for x in assets if f"{system}.zip" in x)
|
|
843
|
+
assert isinstance(asset, str), "Unable to retrieve PNNX repo assets" # i.e. pnnx-20250930-macos.zip
|
|
754
844
|
LOGGER.info(f"{prefix} successfully found latest PNNX asset file {asset}")
|
|
755
845
|
except Exception as e:
|
|
756
|
-
release = "
|
|
846
|
+
release = "20250930"
|
|
757
847
|
asset = f"pnnx-{release}-{system}.zip"
|
|
758
848
|
LOGGER.warning(f"{prefix} PNNX GitHub assets not found: {e}, using default {asset}")
|
|
759
849
|
unzip_dir = safe_download(f"https://github.com/pnnx/pnnx/releases/download/{release}/{asset}", delete=True)
|
|
@@ -777,7 +867,7 @@ class Exporter:
|
|
|
777
867
|
|
|
778
868
|
cmd = [
|
|
779
869
|
str(pnnx),
|
|
780
|
-
str(
|
|
870
|
+
str(f_onnx),
|
|
781
871
|
*ncnn_args,
|
|
782
872
|
*pnnx_args,
|
|
783
873
|
f"fp16={int(self.args.half)}",
|
|
@@ -789,35 +879,42 @@ class Exporter:
|
|
|
789
879
|
subprocess.run(cmd, check=True)
|
|
790
880
|
|
|
791
881
|
# Remove debug files
|
|
792
|
-
pnnx_files = [x.
|
|
882
|
+
pnnx_files = [x.rsplit("=", 1)[-1] for x in pnnx_args]
|
|
793
883
|
for f_debug in ("debug.bin", "debug.param", "debug2.bin", "debug2.param", *pnnx_files):
|
|
794
884
|
Path(f_debug).unlink(missing_ok=True)
|
|
795
885
|
|
|
796
886
|
YAML.save(f / "metadata.yaml", self.metadata) # add metadata.yaml
|
|
797
|
-
return str(f)
|
|
887
|
+
return str(f)
|
|
798
888
|
|
|
799
889
|
@try_export
|
|
800
890
|
def export_coreml(self, prefix=colorstr("CoreML:")):
|
|
801
|
-
"""YOLO CoreML
|
|
891
|
+
"""Export YOLO model to CoreML format."""
|
|
802
892
|
mlmodel = self.args.format.lower() == "mlmodel" # legacy *.mlmodel export format requested
|
|
803
893
|
check_requirements("coremltools>=8.0")
|
|
804
|
-
import coremltools as ct
|
|
894
|
+
import coremltools as ct
|
|
805
895
|
|
|
806
896
|
LOGGER.info(f"\n{prefix} starting export with coremltools {ct.__version__}...")
|
|
807
897
|
assert not WINDOWS, "CoreML export is not supported on Windows, please run on macOS or Linux."
|
|
808
|
-
assert
|
|
898
|
+
assert TORCH_1_11, "CoreML export requires torch>=1.11"
|
|
899
|
+
if self.args.batch > 1:
|
|
900
|
+
assert self.args.dynamic, (
|
|
901
|
+
"batch sizes > 1 are not supported without 'dynamic=True' for CoreML export. Please retry at 'dynamic=True'."
|
|
902
|
+
)
|
|
903
|
+
if self.args.dynamic:
|
|
904
|
+
assert not self.args.nms, (
|
|
905
|
+
"'nms=True' cannot be used together with 'dynamic=True' for CoreML export. Please disable one of them."
|
|
906
|
+
)
|
|
907
|
+
assert self.model.task != "classify", "'dynamic=True' is not supported for CoreML classification models."
|
|
809
908
|
f = self.file.with_suffix(".mlmodel" if mlmodel else ".mlpackage")
|
|
810
909
|
if f.is_dir():
|
|
811
910
|
shutil.rmtree(f)
|
|
812
911
|
|
|
813
|
-
bias = [0.0, 0.0, 0.0]
|
|
814
|
-
scale = 1 / 255
|
|
815
912
|
classifier_config = None
|
|
816
913
|
if self.model.task == "classify":
|
|
817
914
|
classifier_config = ct.ClassifierConfig(list(self.model.names.values()))
|
|
818
915
|
model = self.model
|
|
819
916
|
elif self.model.task == "detect":
|
|
820
|
-
model = IOSDetectModel(self.model, self.im) if self.args.nms else self.model
|
|
917
|
+
model = IOSDetectModel(self.model, self.im, mlprogram=not mlmodel) if self.args.nms else self.model
|
|
821
918
|
else:
|
|
822
919
|
if self.args.nms:
|
|
823
920
|
LOGGER.warning(f"{prefix} 'nms=True' is only available for Detect models like 'yolo11n.pt'.")
|
|
@@ -825,13 +922,26 @@ class Exporter:
|
|
|
825
922
|
model = self.model
|
|
826
923
|
ts = torch.jit.trace(model.eval(), self.im, strict=False) # TorchScript model
|
|
827
924
|
|
|
925
|
+
if self.args.dynamic:
|
|
926
|
+
input_shape = ct.Shape(
|
|
927
|
+
shape=(
|
|
928
|
+
ct.RangeDim(lower_bound=1, upper_bound=self.args.batch, default=1),
|
|
929
|
+
self.im.shape[1],
|
|
930
|
+
ct.RangeDim(lower_bound=32, upper_bound=self.imgsz[0] * 2, default=self.imgsz[0]),
|
|
931
|
+
ct.RangeDim(lower_bound=32, upper_bound=self.imgsz[1] * 2, default=self.imgsz[1]),
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
inputs = [ct.TensorType("image", shape=input_shape)]
|
|
935
|
+
else:
|
|
936
|
+
inputs = [ct.ImageType("image", shape=self.im.shape, scale=1 / 255, bias=[0.0, 0.0, 0.0])]
|
|
937
|
+
|
|
828
938
|
# Based on apple's documentation it is better to leave out the minimum_deployment target and let that get set
|
|
829
939
|
# Internally based on the model conversion and output type.
|
|
830
940
|
# Setting minimum_depoloyment_target >= iOS16 will require setting compute_precision=ct.precision.FLOAT32.
|
|
831
941
|
# iOS16 adds in better support for FP16, but none of the CoreML NMS specifications handle FP16 as input.
|
|
832
942
|
ct_model = ct.convert(
|
|
833
943
|
ts,
|
|
834
|
-
inputs=
|
|
944
|
+
inputs=inputs,
|
|
835
945
|
classifier_config=classifier_config,
|
|
836
946
|
convert_to="neuralnetwork" if mlmodel else "mlprogram",
|
|
837
947
|
)
|
|
@@ -848,12 +958,7 @@ class Exporter:
|
|
|
848
958
|
config = cto.OptimizationConfig(global_config=op_config)
|
|
849
959
|
ct_model = cto.palettize_weights(ct_model, config=config)
|
|
850
960
|
if self.args.nms and self.model.task == "detect":
|
|
851
|
-
if mlmodel
|
|
852
|
-
weights_dir = None
|
|
853
|
-
else:
|
|
854
|
-
ct_model.save(str(f)) # save otherwise weights_dir does not exist
|
|
855
|
-
weights_dir = str(f / "Data/com.apple.CoreML/weights")
|
|
856
|
-
ct_model = self._pipeline_coreml(ct_model, weights_dir=weights_dir)
|
|
961
|
+
ct_model = self._pipeline_coreml(ct_model, weights_dir=None if mlmodel else ct_model.weights_dir)
|
|
857
962
|
|
|
858
963
|
m = self.metadata # metadata dict
|
|
859
964
|
ct_model.short_description = m.pop("description")
|
|
@@ -873,20 +978,21 @@ class Exporter:
|
|
|
873
978
|
)
|
|
874
979
|
f = f.with_suffix(".mlmodel")
|
|
875
980
|
ct_model.save(str(f))
|
|
876
|
-
return f
|
|
981
|
+
return f
|
|
877
982
|
|
|
878
983
|
@try_export
|
|
879
984
|
def export_engine(self, dla=None, prefix=colorstr("TensorRT:")):
|
|
880
|
-
"""YOLO TensorRT
|
|
985
|
+
"""Export YOLO model to TensorRT format https://developer.nvidia.com/tensorrt."""
|
|
881
986
|
assert self.im.device.type != "cpu", "export running on CPU but must be on GPU, i.e. use 'device=0'"
|
|
882
|
-
f_onnx
|
|
987
|
+
f_onnx = self.export_onnx() # run before TRT import https://github.com/ultralytics/ultralytics/issues/7016
|
|
883
988
|
|
|
884
989
|
try:
|
|
885
|
-
import tensorrt as trt
|
|
990
|
+
import tensorrt as trt
|
|
886
991
|
except ImportError:
|
|
887
992
|
if LINUX:
|
|
888
|
-
|
|
889
|
-
|
|
993
|
+
cuda_version = torch.version.cuda.split(".")[0]
|
|
994
|
+
check_requirements(f"tensorrt-cu{cuda_version}>7.0.0,!=10.1.0")
|
|
995
|
+
import tensorrt as trt
|
|
890
996
|
check_version(trt.__version__, ">=7.0.0", hard=True)
|
|
891
997
|
check_version(trt.__version__, "!=10.1.0", msg="https://github.com/ultralytics/ultralytics/pull/14239")
|
|
892
998
|
|
|
@@ -894,7 +1000,7 @@ class Exporter:
|
|
|
894
1000
|
LOGGER.info(f"\n{prefix} starting export with TensorRT {trt.__version__}...")
|
|
895
1001
|
assert Path(f_onnx).exists(), f"failed to export ONNX file: {f_onnx}"
|
|
896
1002
|
f = self.file.with_suffix(".engine") # TensorRT engine file
|
|
897
|
-
|
|
1003
|
+
onnx2engine(
|
|
898
1004
|
f_onnx,
|
|
899
1005
|
f,
|
|
900
1006
|
self.args.workspace,
|
|
@@ -909,26 +1015,26 @@ class Exporter:
|
|
|
909
1015
|
prefix=prefix,
|
|
910
1016
|
)
|
|
911
1017
|
|
|
912
|
-
return f
|
|
1018
|
+
return f
|
|
913
1019
|
|
|
914
1020
|
@try_export
|
|
915
1021
|
def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):
|
|
916
|
-
"""YOLO TensorFlow SavedModel
|
|
1022
|
+
"""Export YOLO model to TensorFlow SavedModel format."""
|
|
917
1023
|
cuda = torch.cuda.is_available()
|
|
918
1024
|
try:
|
|
919
|
-
import tensorflow as tf
|
|
1025
|
+
import tensorflow as tf
|
|
920
1026
|
except ImportError:
|
|
921
|
-
check_requirements("tensorflow>=2.0.0")
|
|
922
|
-
import tensorflow as tf
|
|
1027
|
+
check_requirements("tensorflow>=2.0.0,<=2.19.0")
|
|
1028
|
+
import tensorflow as tf
|
|
923
1029
|
check_requirements(
|
|
924
1030
|
(
|
|
925
|
-
"tf_keras", # required by 'onnx2tf' package
|
|
1031
|
+
"tf_keras<=2.19.0", # required by 'onnx2tf' package
|
|
926
1032
|
"sng4onnx>=1.0.1", # required by 'onnx2tf' package
|
|
927
1033
|
"onnx_graphsurgeon>=0.3.26", # required by 'onnx2tf' package
|
|
928
|
-
"ai-edge-litert>=1.2.0", # required by 'onnx2tf' package
|
|
929
|
-
"onnx>=1.12.0
|
|
1034
|
+
"ai-edge-litert>=1.2.0" + (",<1.4.0" if MACOS else ""), # required by 'onnx2tf' package
|
|
1035
|
+
"onnx>=1.12.0",
|
|
930
1036
|
"onnx2tf>=1.26.3",
|
|
931
|
-
"onnxslim>=0.1.
|
|
1037
|
+
"onnxslim>=0.1.71",
|
|
932
1038
|
"onnxruntime-gpu" if cuda else "onnxruntime",
|
|
933
1039
|
"protobuf>=5",
|
|
934
1040
|
),
|
|
@@ -947,80 +1053,50 @@ class Exporter:
|
|
|
947
1053
|
if f.is_dir():
|
|
948
1054
|
shutil.rmtree(f) # delete output folder
|
|
949
1055
|
|
|
950
|
-
#
|
|
951
|
-
|
|
952
|
-
if
|
|
953
|
-
|
|
1056
|
+
# Export to TF
|
|
1057
|
+
images = None
|
|
1058
|
+
if self.args.int8 and self.args.data:
|
|
1059
|
+
images = [batch["img"] for batch in self.get_int8_calibration_dataloader(prefix)]
|
|
1060
|
+
images = (
|
|
1061
|
+
torch.nn.functional.interpolate(torch.cat(images, 0).float(), size=self.imgsz)
|
|
1062
|
+
.permute(0, 2, 3, 1)
|
|
1063
|
+
.numpy()
|
|
1064
|
+
.astype(np.float32)
|
|
1065
|
+
)
|
|
954
1066
|
|
|
955
1067
|
# Export to ONNX
|
|
1068
|
+
if isinstance(self.model.model[-1], RTDETRDecoder):
|
|
1069
|
+
self.args.opset = self.args.opset or 19
|
|
1070
|
+
assert 16 <= self.args.opset <= 19, "RTDETR export requires opset>=16;<=19"
|
|
956
1071
|
self.args.simplify = True
|
|
957
|
-
f_onnx
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
images = [batch["img"] for batch in self.get_int8_calibration_dataloader(prefix)]
|
|
966
|
-
images = torch.nn.functional.interpolate(torch.cat(images, 0).float(), size=self.imgsz).permute(
|
|
967
|
-
0, 2, 3, 1
|
|
968
|
-
)
|
|
969
|
-
np.save(str(tmp_file), images.numpy().astype(np.float32)) # BHWC
|
|
970
|
-
np_data = [["images", tmp_file, [[[[0, 0, 0]]]], [[[[255, 255, 255]]]]]]
|
|
971
|
-
|
|
972
|
-
import onnx2tf # scoped for after ONNX export for reduced conflict during import
|
|
973
|
-
|
|
974
|
-
LOGGER.info(f"{prefix} starting TFLite export with onnx2tf {onnx2tf.__version__}...")
|
|
975
|
-
keras_model = onnx2tf.convert(
|
|
976
|
-
input_onnx_file_path=f_onnx,
|
|
977
|
-
output_folder_path=str(f),
|
|
978
|
-
not_use_onnxsim=True,
|
|
979
|
-
verbosity="error", # note INT8-FP16 activation bug https://github.com/ultralytics/ultralytics/issues/15873
|
|
980
|
-
output_integer_quantized_tflite=self.args.int8,
|
|
981
|
-
quant_type="per-tensor", # "per-tensor" (faster) or "per-channel" (slower but more accurate)
|
|
982
|
-
custom_input_op_name_np_data_path=np_data,
|
|
983
|
-
enable_batchmatmul_unfold=True, # fix lower no. of detected objects on GPU delegate
|
|
984
|
-
output_signaturedefs=True, # fix error with Attention block group convolution
|
|
985
|
-
optimization_for_gpu_delegate=True,
|
|
1072
|
+
f_onnx = self.export_onnx() # ensure ONNX is available
|
|
1073
|
+
keras_model = onnx2saved_model(
|
|
1074
|
+
f_onnx,
|
|
1075
|
+
f,
|
|
1076
|
+
int8=self.args.int8,
|
|
1077
|
+
images=images,
|
|
1078
|
+
disable_group_convolution=self.args.format in {"tfjs", "edgetpu"},
|
|
1079
|
+
prefix=prefix,
|
|
986
1080
|
)
|
|
987
1081
|
YAML.save(f / "metadata.yaml", self.metadata) # add metadata.yaml
|
|
988
|
-
|
|
989
|
-
# Remove/rename TFLite models
|
|
990
|
-
if self.args.int8:
|
|
991
|
-
tmp_file.unlink(missing_ok=True)
|
|
992
|
-
for file in f.rglob("*_dynamic_range_quant.tflite"):
|
|
993
|
-
file.rename(file.with_name(file.stem.replace("_dynamic_range_quant", "_int8") + file.suffix))
|
|
994
|
-
for file in f.rglob("*_integer_quant_with_int16_act.tflite"):
|
|
995
|
-
file.unlink() # delete extra fp16 activation TFLite files
|
|
996
|
-
|
|
997
1082
|
# Add TFLite metadata
|
|
998
1083
|
for file in f.rglob("*.tflite"):
|
|
999
|
-
|
|
1084
|
+
file.unlink() if "quant_with_int16_act.tflite" in str(file) else self._add_tflite_metadata(file)
|
|
1000
1085
|
|
|
1001
1086
|
return str(f), keras_model # or keras_model = tf.saved_model.load(f, tags=None, options=None)
|
|
1002
1087
|
|
|
1003
1088
|
@try_export
|
|
1004
1089
|
def export_pb(self, keras_model, prefix=colorstr("TensorFlow GraphDef:")):
|
|
1005
|
-
"""YOLO TensorFlow GraphDef *.pb
|
|
1006
|
-
import tensorflow as tf # noqa
|
|
1007
|
-
from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 # noqa
|
|
1008
|
-
|
|
1009
|
-
LOGGER.info(f"\n{prefix} starting export with tensorflow {tf.__version__}...")
|
|
1090
|
+
"""Export YOLO model to TensorFlow GraphDef *.pb format https://github.com/leimao/Frozen-Graph-TensorFlow."""
|
|
1010
1091
|
f = self.file.with_suffix(".pb")
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
|
|
1014
|
-
frozen_func = convert_variables_to_constants_v2(m)
|
|
1015
|
-
frozen_func.graph.as_graph_def()
|
|
1016
|
-
tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False)
|
|
1017
|
-
return f, None
|
|
1092
|
+
keras2pb(keras_model, f, prefix)
|
|
1093
|
+
return f
|
|
1018
1094
|
|
|
1019
1095
|
@try_export
|
|
1020
1096
|
def export_tflite(self, prefix=colorstr("TensorFlow Lite:")):
|
|
1021
|
-
"""YOLO TensorFlow Lite
|
|
1097
|
+
"""Export YOLO model to TensorFlow Lite format."""
|
|
1022
1098
|
# BUG https://github.com/ultralytics/ultralytics/issues/13436
|
|
1023
|
-
import tensorflow as tf
|
|
1099
|
+
import tensorflow as tf
|
|
1024
1100
|
|
|
1025
1101
|
LOGGER.info(f"\n{prefix} starting export with tensorflow {tf.__version__}...")
|
|
1026
1102
|
saved_model = Path(str(self.file).replace(self.file.suffix, "_saved_model"))
|
|
@@ -1030,11 +1106,44 @@ class Exporter:
|
|
|
1030
1106
|
f = saved_model / f"{self.file.stem}_float16.tflite" # fp32 in/out
|
|
1031
1107
|
else:
|
|
1032
1108
|
f = saved_model / f"{self.file.stem}_float32.tflite"
|
|
1033
|
-
return str(f)
|
|
1109
|
+
return str(f)
|
|
1110
|
+
|
|
1111
|
+
@try_export
|
|
1112
|
+
def export_executorch(self, prefix=colorstr("ExecuTorch:")):
|
|
1113
|
+
"""Exports a model to ExecuTorch (.pte) format into a dedicated directory and saves the required metadata,
|
|
1114
|
+
following Ultralytics conventions.
|
|
1115
|
+
"""
|
|
1116
|
+
LOGGER.info(f"\n{prefix} starting export with ExecuTorch...")
|
|
1117
|
+
assert TORCH_2_9, f"ExecuTorch export requires torch>=2.9.0 but torch=={TORCH_VERSION} is installed"
|
|
1118
|
+
# TorchAO release compatibility table bug https://github.com/pytorch/ao/issues/2919
|
|
1119
|
+
# Setuptools bug: https://github.com/pypa/setuptools/issues/4483
|
|
1120
|
+
check_requirements("setuptools<71.0.0") # Setuptools bug: https://github.com/pypa/setuptools/issues/4483
|
|
1121
|
+
check_requirements(("executorch==1.0.0", "flatbuffers"))
|
|
1122
|
+
|
|
1123
|
+
import torch
|
|
1124
|
+
from executorch.backends.xnnpack.partition.xnnpack_partitioner import XnnpackPartitioner
|
|
1125
|
+
from executorch.exir import to_edge_transform_and_lower
|
|
1126
|
+
|
|
1127
|
+
file_directory = Path(str(self.file).replace(self.file.suffix, "_executorch_model"))
|
|
1128
|
+
file_directory.mkdir(parents=True, exist_ok=True)
|
|
1129
|
+
|
|
1130
|
+
file_pte = file_directory / self.file.with_suffix(".pte").name
|
|
1131
|
+
sample_inputs = (self.im,)
|
|
1132
|
+
|
|
1133
|
+
et_program = to_edge_transform_and_lower(
|
|
1134
|
+
torch.export.export(self.model, sample_inputs), partitioner=[XnnpackPartitioner()]
|
|
1135
|
+
).to_executorch()
|
|
1136
|
+
|
|
1137
|
+
with open(file_pte, "wb") as file:
|
|
1138
|
+
file.write(et_program.buffer)
|
|
1139
|
+
|
|
1140
|
+
YAML.save(file_directory / "metadata.yaml", self.metadata)
|
|
1141
|
+
|
|
1142
|
+
return str(file_directory)
|
|
1034
1143
|
|
|
1035
1144
|
@try_export
|
|
1036
1145
|
def export_edgetpu(self, tflite_model="", prefix=colorstr("Edge TPU:")):
|
|
1037
|
-
"""YOLO Edge TPU
|
|
1146
|
+
"""Export YOLO model to Edge TPU format https://coral.ai/docs/edgetpu/models-intro/."""
|
|
1038
1147
|
cmd = "edgetpu_compiler --version"
|
|
1039
1148
|
help_url = "https://coral.ai/docs/edgetpu/compiler/"
|
|
1040
1149
|
assert LINUX, f"export only supported on Linux. See {help_url}"
|
|
@@ -1048,61 +1157,29 @@ class Exporter:
|
|
|
1048
1157
|
"sudo apt-get install edgetpu-compiler",
|
|
1049
1158
|
):
|
|
1050
1159
|
subprocess.run(c if is_sudo_available() else c.replace("sudo ", ""), shell=True, check=True)
|
|
1051
|
-
ver = subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1]
|
|
1052
1160
|
|
|
1161
|
+
ver = subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().rsplit(maxsplit=1)[-1]
|
|
1053
1162
|
LOGGER.info(f"\n{prefix} starting export with Edge TPU compiler {ver}...")
|
|
1163
|
+
tflite2edgetpu(tflite_file=tflite_model, output_dir=tflite_model.parent, prefix=prefix)
|
|
1054
1164
|
f = str(tflite_model).replace(".tflite", "_edgetpu.tflite") # Edge TPU model
|
|
1055
|
-
|
|
1056
|
-
cmd = (
|
|
1057
|
-
"edgetpu_compiler "
|
|
1058
|
-
f'--out_dir "{Path(f).parent}" '
|
|
1059
|
-
"--show_operations "
|
|
1060
|
-
"--search_delegate "
|
|
1061
|
-
"--delegate_search_step 30 "
|
|
1062
|
-
"--timeout_sec 180 "
|
|
1063
|
-
f'"{tflite_model}"'
|
|
1064
|
-
)
|
|
1065
|
-
LOGGER.info(f"{prefix} running '{cmd}'")
|
|
1066
|
-
subprocess.run(cmd, shell=True)
|
|
1067
1165
|
self._add_tflite_metadata(f)
|
|
1068
|
-
return f
|
|
1166
|
+
return f
|
|
1069
1167
|
|
|
1070
1168
|
@try_export
|
|
1071
1169
|
def export_tfjs(self, prefix=colorstr("TensorFlow.js:")):
|
|
1072
|
-
"""YOLO TensorFlow.js
|
|
1170
|
+
"""Export YOLO model to TensorFlow.js format."""
|
|
1073
1171
|
check_requirements("tensorflowjs")
|
|
1074
|
-
import tensorflow as tf
|
|
1075
|
-
import tensorflowjs as tfjs # noqa
|
|
1076
1172
|
|
|
1077
|
-
LOGGER.info(f"\n{prefix} starting export with tensorflowjs {tfjs.__version__}...")
|
|
1078
1173
|
f = str(self.file).replace(self.file.suffix, "_web_model") # js dir
|
|
1079
1174
|
f_pb = str(self.file.with_suffix(".pb")) # *.pb path
|
|
1080
|
-
|
|
1081
|
-
gd = tf.Graph().as_graph_def() # TF GraphDef
|
|
1082
|
-
with open(f_pb, "rb") as file:
|
|
1083
|
-
gd.ParseFromString(file.read())
|
|
1084
|
-
outputs = ",".join(gd_outputs(gd))
|
|
1085
|
-
LOGGER.info(f"\n{prefix} output node names: {outputs}")
|
|
1086
|
-
|
|
1087
|
-
quantization = "--quantize_float16" if self.args.half else "--quantize_uint8" if self.args.int8 else ""
|
|
1088
|
-
with spaces_in_path(f_pb) as fpb_, spaces_in_path(f) as f_: # exporter can not handle spaces in path
|
|
1089
|
-
cmd = (
|
|
1090
|
-
"tensorflowjs_converter "
|
|
1091
|
-
f'--input_format=tf_frozen_model {quantization} --output_node_names={outputs} "{fpb_}" "{f_}"'
|
|
1092
|
-
)
|
|
1093
|
-
LOGGER.info(f"{prefix} running '{cmd}'")
|
|
1094
|
-
subprocess.run(cmd, shell=True)
|
|
1095
|
-
|
|
1096
|
-
if " " in f:
|
|
1097
|
-
LOGGER.warning(f"{prefix} your model may not work correctly with spaces in path '{f}'.")
|
|
1098
|
-
|
|
1175
|
+
pb2tfjs(pb_file=f_pb, output_dir=f, half=self.args.half, int8=self.args.int8, prefix=prefix)
|
|
1099
1176
|
# Add metadata
|
|
1100
1177
|
YAML.save(Path(f) / "metadata.yaml", self.metadata) # add metadata.yaml
|
|
1101
|
-
return f
|
|
1178
|
+
return f
|
|
1102
1179
|
|
|
1103
1180
|
@try_export
|
|
1104
1181
|
def export_rknn(self, prefix=colorstr("RKNN:")):
|
|
1105
|
-
"""YOLO RKNN
|
|
1182
|
+
"""Export YOLO model to RKNN format."""
|
|
1106
1183
|
LOGGER.info(f"\n{prefix} starting export with rknn-toolkit2...")
|
|
1107
1184
|
|
|
1108
1185
|
check_requirements("rknn-toolkit2")
|
|
@@ -1114,7 +1191,7 @@ class Exporter:
|
|
|
1114
1191
|
|
|
1115
1192
|
from rknn.api import RKNN
|
|
1116
1193
|
|
|
1117
|
-
f
|
|
1194
|
+
f = self.export_onnx()
|
|
1118
1195
|
export_path = Path(f"{Path(f).stem}_rknn_model")
|
|
1119
1196
|
export_path.mkdir(exist_ok=True)
|
|
1120
1197
|
|
|
@@ -1125,27 +1202,22 @@ class Exporter:
|
|
|
1125
1202
|
f = f.replace(".onnx", f"-{self.args.name}.rknn")
|
|
1126
1203
|
rknn.export_rknn(f"{export_path / f}")
|
|
1127
1204
|
YAML.save(export_path / "metadata.yaml", self.metadata)
|
|
1128
|
-
return export_path
|
|
1205
|
+
return export_path
|
|
1129
1206
|
|
|
1130
1207
|
@try_export
|
|
1131
1208
|
def export_imx(self, prefix=colorstr("IMX:")):
|
|
1132
|
-
"""YOLO IMX
|
|
1133
|
-
gptq = False
|
|
1209
|
+
"""Export YOLO model to IMX format."""
|
|
1134
1210
|
assert LINUX, (
|
|
1135
1211
|
"export only supported on Linux. "
|
|
1136
1212
|
"See https://developer.aitrios.sony-semicon.com/en/raspberrypi-ai-camera/documentation/imx500-converter"
|
|
1137
1213
|
)
|
|
1138
1214
|
if getattr(self.model, "end2end", False):
|
|
1139
1215
|
raise ValueError("IMX export is not supported for end2end models.")
|
|
1140
|
-
check_requirements(
|
|
1216
|
+
check_requirements(
|
|
1217
|
+
("model-compression-toolkit>=2.4.1", "sony-custom-layers>=0.3.0", "edge-mdt-tpc>=1.1.0", "pydantic<=2.11.7")
|
|
1218
|
+
)
|
|
1141
1219
|
check_requirements("imx500-converter[pt]>=3.16.1") # Separate requirements for imx500-converter
|
|
1142
|
-
|
|
1143
|
-
import model_compression_toolkit as mct
|
|
1144
|
-
import onnx
|
|
1145
|
-
from edgemdt_tpc import get_target_platform_capabilities
|
|
1146
|
-
from sony_custom_layers.pytorch import multiclass_nms
|
|
1147
|
-
|
|
1148
|
-
LOGGER.info(f"\n{prefix} starting export with model_compression_toolkit {mct.__version__}...")
|
|
1220
|
+
check_requirements("mct-quantizers>=1.6.0") # Separate for compatibility with model-compression-toolkit
|
|
1149
1221
|
|
|
1150
1222
|
# Install Java>=17
|
|
1151
1223
|
try:
|
|
@@ -1157,130 +1229,17 @@ class Exporter:
|
|
|
1157
1229
|
cmd = (["sudo"] if is_sudo_available() else []) + ["apt", "install", "-y", "openjdk-21-jre"]
|
|
1158
1230
|
subprocess.run(cmd, check=True)
|
|
1159
1231
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
if "C2PSA" in self.model.__str__(): # YOLO11
|
|
1170
|
-
layer_names = ["sub", "mul_2", "add_14", "cat_21"]
|
|
1171
|
-
weights_memory = 2585350.2439
|
|
1172
|
-
n_layers = 238 # 238 layers for fused YOLO11n
|
|
1173
|
-
else: # YOLOv8
|
|
1174
|
-
layer_names = ["sub", "mul", "add_6", "cat_17"]
|
|
1175
|
-
weights_memory = 2550540.8
|
|
1176
|
-
n_layers = 168 # 168 layers for fused YOLOv8n
|
|
1177
|
-
|
|
1178
|
-
# Check if the model has the expected number of layers
|
|
1179
|
-
if len(list(self.model.modules())) != n_layers:
|
|
1180
|
-
raise ValueError("IMX export only supported for YOLOv8n and YOLO11n models.")
|
|
1181
|
-
|
|
1182
|
-
for layer_name in layer_names:
|
|
1183
|
-
bit_cfg.set_manual_activation_bit_width([mct.core.common.network_editors.NodeNameFilter(layer_name)], 16)
|
|
1184
|
-
|
|
1185
|
-
config = mct.core.CoreConfig(
|
|
1186
|
-
mixed_precision_config=mct.core.MixedPrecisionQuantizationConfig(num_of_images=10),
|
|
1187
|
-
quantization_config=mct.core.QuantizationConfig(concat_threshold_update=True),
|
|
1188
|
-
bit_width_config=bit_cfg,
|
|
1189
|
-
)
|
|
1190
|
-
|
|
1191
|
-
resource_utilization = mct.core.ResourceUtilization(weights_memory=weights_memory)
|
|
1192
|
-
|
|
1193
|
-
quant_model = (
|
|
1194
|
-
mct.gptq.pytorch_gradient_post_training_quantization( # Perform Gradient-Based Post Training Quantization
|
|
1195
|
-
model=self.model,
|
|
1196
|
-
representative_data_gen=representative_dataset_gen,
|
|
1197
|
-
target_resource_utilization=resource_utilization,
|
|
1198
|
-
gptq_config=mct.gptq.get_pytorch_gptq_config(
|
|
1199
|
-
n_epochs=1000, use_hessian_based_weights=False, use_hessian_sample_attention=False
|
|
1200
|
-
),
|
|
1201
|
-
core_config=config,
|
|
1202
|
-
target_platform_capabilities=tpc,
|
|
1203
|
-
)[0]
|
|
1204
|
-
if gptq
|
|
1205
|
-
else mct.ptq.pytorch_post_training_quantization( # Perform post training quantization
|
|
1206
|
-
in_module=self.model,
|
|
1207
|
-
representative_data_gen=representative_dataset_gen,
|
|
1208
|
-
target_resource_utilization=resource_utilization,
|
|
1209
|
-
core_config=config,
|
|
1210
|
-
target_platform_capabilities=tpc,
|
|
1211
|
-
)[0]
|
|
1212
|
-
)
|
|
1213
|
-
|
|
1214
|
-
class NMSWrapper(torch.nn.Module):
|
|
1215
|
-
def __init__(
|
|
1216
|
-
self,
|
|
1217
|
-
model: torch.nn.Module,
|
|
1218
|
-
score_threshold: float = 0.001,
|
|
1219
|
-
iou_threshold: float = 0.7,
|
|
1220
|
-
max_detections: int = 300,
|
|
1221
|
-
):
|
|
1222
|
-
"""
|
|
1223
|
-
Wrapping PyTorch Module with multiclass_nms layer from sony_custom_layers.
|
|
1224
|
-
|
|
1225
|
-
Args:
|
|
1226
|
-
model (nn.Module): Model instance.
|
|
1227
|
-
score_threshold (float): Score threshold for non-maximum suppression.
|
|
1228
|
-
iou_threshold (float): Intersection over union threshold for non-maximum suppression.
|
|
1229
|
-
max_detections (float): The number of detections to return.
|
|
1230
|
-
"""
|
|
1231
|
-
super().__init__()
|
|
1232
|
-
self.model = model
|
|
1233
|
-
self.score_threshold = score_threshold
|
|
1234
|
-
self.iou_threshold = iou_threshold
|
|
1235
|
-
self.max_detections = max_detections
|
|
1236
|
-
|
|
1237
|
-
def forward(self, images):
|
|
1238
|
-
# model inference
|
|
1239
|
-
outputs = self.model(images)
|
|
1240
|
-
|
|
1241
|
-
boxes = outputs[0]
|
|
1242
|
-
scores = outputs[1]
|
|
1243
|
-
nms = multiclass_nms(
|
|
1244
|
-
boxes=boxes,
|
|
1245
|
-
scores=scores,
|
|
1246
|
-
score_threshold=self.score_threshold,
|
|
1247
|
-
iou_threshold=self.iou_threshold,
|
|
1248
|
-
max_detections=self.max_detections,
|
|
1249
|
-
)
|
|
1250
|
-
return nms
|
|
1251
|
-
|
|
1252
|
-
quant_model = NMSWrapper(
|
|
1253
|
-
model=quant_model,
|
|
1254
|
-
score_threshold=self.args.conf or 0.001,
|
|
1255
|
-
iou_threshold=self.args.iou,
|
|
1256
|
-
max_detections=self.args.max_det,
|
|
1257
|
-
).to(self.device)
|
|
1258
|
-
|
|
1259
|
-
f = Path(str(self.file).replace(self.file.suffix, "_imx_model"))
|
|
1260
|
-
f.mkdir(exist_ok=True)
|
|
1261
|
-
onnx_model = f / Path(str(self.file.name).replace(self.file.suffix, "_imx.onnx")) # js dir
|
|
1262
|
-
mct.exporter.pytorch_export_model(
|
|
1263
|
-
model=quant_model, save_model_path=onnx_model, repr_dataset=representative_dataset_gen
|
|
1264
|
-
)
|
|
1265
|
-
|
|
1266
|
-
model_onnx = onnx.load(onnx_model) # load onnx model
|
|
1267
|
-
for k, v in self.metadata.items():
|
|
1268
|
-
meta = model_onnx.metadata_props.add()
|
|
1269
|
-
meta.key, meta.value = k, str(v)
|
|
1270
|
-
|
|
1271
|
-
onnx.save(model_onnx, onnx_model)
|
|
1272
|
-
|
|
1273
|
-
subprocess.run(
|
|
1274
|
-
["imxconv-pt", "-i", str(onnx_model), "-o", str(f), "--no-input-persistency", "--overwrite-output"],
|
|
1275
|
-
check=True,
|
|
1232
|
+
return torch2imx(
|
|
1233
|
+
self.model,
|
|
1234
|
+
self.file,
|
|
1235
|
+
self.args.conf,
|
|
1236
|
+
self.args.iou,
|
|
1237
|
+
self.args.max_det,
|
|
1238
|
+
metadata=self.metadata,
|
|
1239
|
+
dataset=self.get_int8_calibration_dataloader(prefix),
|
|
1240
|
+
prefix=prefix,
|
|
1276
1241
|
)
|
|
1277
1242
|
|
|
1278
|
-
# Needed for imx models.
|
|
1279
|
-
with open(f / "labels.txt", "w", encoding="utf-8") as file:
|
|
1280
|
-
file.writelines([f"{name}\n" for _, name in self.model.names.items()])
|
|
1281
|
-
|
|
1282
|
-
return f, None
|
|
1283
|
-
|
|
1284
1243
|
def _add_tflite_metadata(self, file):
|
|
1285
1244
|
"""Add metadata to *.tflite models per https://ai.google.dev/edge/litert/models/metadata."""
|
|
1286
1245
|
import zipfile
|
|
@@ -1289,68 +1248,57 @@ class Exporter:
|
|
|
1289
1248
|
zf.writestr("metadata.json", json.dumps(self.metadata, indent=2))
|
|
1290
1249
|
|
|
1291
1250
|
def _pipeline_coreml(self, model, weights_dir=None, prefix=colorstr("CoreML Pipeline:")):
|
|
1292
|
-
"""
|
|
1293
|
-
import coremltools as ct
|
|
1251
|
+
"""Create CoreML pipeline with NMS for YOLO detection models."""
|
|
1252
|
+
import coremltools as ct
|
|
1294
1253
|
|
|
1295
1254
|
LOGGER.info(f"{prefix} starting pipeline with coremltools {ct.__version__}...")
|
|
1296
|
-
_, _, h, w = list(self.im.shape) # BCHW
|
|
1297
1255
|
|
|
1298
1256
|
# Output shapes
|
|
1299
1257
|
spec = model.get_spec()
|
|
1300
|
-
|
|
1301
|
-
if
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
img = Image.new("RGB", (w, h)) # w=192, h=320
|
|
1305
|
-
out = model.predict({"image": img})
|
|
1306
|
-
out0_shape = out[out0.name].shape # (3780, 80)
|
|
1307
|
-
out1_shape = out[out1.name].shape # (3780, 4)
|
|
1308
|
-
else: # linux and windows can not run model.predict(), get sizes from PyTorch model output y
|
|
1309
|
-
out0_shape = self.output_shape[2], self.output_shape[1] - 4 # (3780, 80)
|
|
1310
|
-
out1_shape = self.output_shape[2], 4 # (3780, 4)
|
|
1258
|
+
outs = list(iter(spec.description.output))
|
|
1259
|
+
if self.args.format == "mlmodel": # mlmodel doesn't infer shapes automatically
|
|
1260
|
+
outs[0].type.multiArrayType.shape[:] = self.output_shape[2], self.output_shape[1] - 4
|
|
1261
|
+
outs[1].type.multiArrayType.shape[:] = self.output_shape[2], 4
|
|
1311
1262
|
|
|
1312
1263
|
# Checks
|
|
1313
1264
|
names = self.metadata["names"]
|
|
1314
1265
|
nx, ny = spec.description.input[0].type.imageType.width, spec.description.input[0].type.imageType.height
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
# Define output shapes (missing)
|
|
1319
|
-
out0.type.multiArrayType.shape[:] = out0_shape # (3780, 80)
|
|
1320
|
-
out1.type.multiArrayType.shape[:] = out1_shape # (3780, 4)
|
|
1266
|
+
nc = outs[0].type.multiArrayType.shape[-1]
|
|
1267
|
+
if len(names) != nc: # Hack fix for MLProgram NMS bug https://github.com/ultralytics/ultralytics/issues/22309
|
|
1268
|
+
names = {**names, **{i: str(i) for i in range(len(names), nc)}}
|
|
1321
1269
|
|
|
1322
1270
|
# Model from spec
|
|
1323
1271
|
model = ct.models.MLModel(spec, weights_dir=weights_dir)
|
|
1324
1272
|
|
|
1325
|
-
#
|
|
1273
|
+
# Create NMS protobuf
|
|
1326
1274
|
nms_spec = ct.proto.Model_pb2.Model()
|
|
1327
1275
|
nms_spec.specificationVersion = spec.specificationVersion
|
|
1328
|
-
for i in range(
|
|
1276
|
+
for i in range(len(outs)):
|
|
1329
1277
|
decoder_output = model._spec.description.output[i].SerializeToString()
|
|
1330
1278
|
nms_spec.description.input.add()
|
|
1331
1279
|
nms_spec.description.input[i].ParseFromString(decoder_output)
|
|
1332
1280
|
nms_spec.description.output.add()
|
|
1333
1281
|
nms_spec.description.output[i].ParseFromString(decoder_output)
|
|
1334
1282
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1283
|
+
output_names = ["confidence", "coordinates"]
|
|
1284
|
+
for i, name in enumerate(output_names):
|
|
1285
|
+
nms_spec.description.output[i].name = name
|
|
1337
1286
|
|
|
1338
|
-
|
|
1339
|
-
for i in range(2):
|
|
1287
|
+
for i, out in enumerate(outs):
|
|
1340
1288
|
ma_type = nms_spec.description.output[i].type.multiArrayType
|
|
1341
1289
|
ma_type.shapeRange.sizeRanges.add()
|
|
1342
1290
|
ma_type.shapeRange.sizeRanges[0].lowerBound = 0
|
|
1343
1291
|
ma_type.shapeRange.sizeRanges[0].upperBound = -1
|
|
1344
1292
|
ma_type.shapeRange.sizeRanges.add()
|
|
1345
|
-
ma_type.shapeRange.sizeRanges[1].lowerBound =
|
|
1346
|
-
ma_type.shapeRange.sizeRanges[1].upperBound =
|
|
1293
|
+
ma_type.shapeRange.sizeRanges[1].lowerBound = out.type.multiArrayType.shape[-1]
|
|
1294
|
+
ma_type.shapeRange.sizeRanges[1].upperBound = out.type.multiArrayType.shape[-1]
|
|
1347
1295
|
del ma_type.shape[:]
|
|
1348
1296
|
|
|
1349
1297
|
nms = nms_spec.nonMaximumSuppression
|
|
1350
|
-
nms.confidenceInputFeatureName =
|
|
1351
|
-
nms.coordinatesInputFeatureName =
|
|
1352
|
-
nms.confidenceOutputFeatureName =
|
|
1353
|
-
nms.coordinatesOutputFeatureName =
|
|
1298
|
+
nms.confidenceInputFeatureName = outs[0].name # 1x507x80
|
|
1299
|
+
nms.coordinatesInputFeatureName = outs[1].name # 1x507x4
|
|
1300
|
+
nms.confidenceOutputFeatureName = output_names[0]
|
|
1301
|
+
nms.coordinatesOutputFeatureName = output_names[1]
|
|
1354
1302
|
nms.iouThresholdInputFeatureName = "iouThreshold"
|
|
1355
1303
|
nms.confidenceThresholdInputFeatureName = "confidenceThreshold"
|
|
1356
1304
|
nms.iouThreshold = self.args.iou
|
|
@@ -1359,14 +1307,14 @@ class Exporter:
|
|
|
1359
1307
|
nms.stringClassLabels.vector.extend(names.values())
|
|
1360
1308
|
nms_model = ct.models.MLModel(nms_spec)
|
|
1361
1309
|
|
|
1362
|
-
#
|
|
1310
|
+
# Pipeline models together
|
|
1363
1311
|
pipeline = ct.models.pipeline.Pipeline(
|
|
1364
1312
|
input_features=[
|
|
1365
1313
|
("image", ct.models.datatypes.Array(3, ny, nx)),
|
|
1366
1314
|
("iouThreshold", ct.models.datatypes.Double()),
|
|
1367
1315
|
("confidenceThreshold", ct.models.datatypes.Double()),
|
|
1368
1316
|
],
|
|
1369
|
-
output_features=
|
|
1317
|
+
output_features=output_names,
|
|
1370
1318
|
)
|
|
1371
1319
|
pipeline.add_model(model)
|
|
1372
1320
|
pipeline.add_model(nms_model)
|
|
@@ -1395,7 +1343,7 @@ class Exporter:
|
|
|
1395
1343
|
return model
|
|
1396
1344
|
|
|
1397
1345
|
def add_callback(self, event: str, callback):
|
|
1398
|
-
"""
|
|
1346
|
+
"""Append the given callback to the specified event."""
|
|
1399
1347
|
self.callbacks[event].append(callback)
|
|
1400
1348
|
|
|
1401
1349
|
def run_callbacks(self, event: str):
|
|
@@ -1407,32 +1355,45 @@ class Exporter:
|
|
|
1407
1355
|
class IOSDetectModel(torch.nn.Module):
|
|
1408
1356
|
"""Wrap an Ultralytics YOLO model for Apple iOS CoreML export."""
|
|
1409
1357
|
|
|
1410
|
-
def __init__(self, model, im):
|
|
1411
|
-
"""Initialize the IOSDetectModel class with a YOLO model and example image.
|
|
1358
|
+
def __init__(self, model, im, mlprogram=True):
|
|
1359
|
+
"""Initialize the IOSDetectModel class with a YOLO model and example image.
|
|
1360
|
+
|
|
1361
|
+
Args:
|
|
1362
|
+
model (torch.nn.Module): The YOLO model to wrap.
|
|
1363
|
+
im (torch.Tensor): Example input tensor with shape (B, C, H, W).
|
|
1364
|
+
mlprogram (bool): Whether exporting to MLProgram format to fix NMS bug.
|
|
1365
|
+
"""
|
|
1412
1366
|
super().__init__()
|
|
1413
1367
|
_, _, h, w = im.shape # batch, channel, height, width
|
|
1414
1368
|
self.model = model
|
|
1415
1369
|
self.nc = len(model.names) # number of classes
|
|
1370
|
+
self.mlprogram = mlprogram
|
|
1416
1371
|
if w == h:
|
|
1417
1372
|
self.normalize = 1.0 / w # scalar
|
|
1418
1373
|
else:
|
|
1419
|
-
self.normalize = torch.tensor(
|
|
1374
|
+
self.normalize = torch.tensor(
|
|
1375
|
+
[1.0 / w, 1.0 / h, 1.0 / w, 1.0 / h], # broadcast (slower, smaller)
|
|
1376
|
+
device=next(model.parameters()).device,
|
|
1377
|
+
)
|
|
1420
1378
|
|
|
1421
1379
|
def forward(self, x):
|
|
1422
1380
|
"""Normalize predictions of object detection model with input size-dependent factors."""
|
|
1423
1381
|
xywh, cls = self.model(x)[0].transpose(0, 1).split((4, self.nc), 1)
|
|
1424
|
-
|
|
1382
|
+
if self.mlprogram and self.nc % 80 != 0: # NMS bug https://github.com/ultralytics/ultralytics/issues/22309
|
|
1383
|
+
pad_length = int(((self.nc + 79) // 80) * 80) - self.nc # pad class length to multiple of 80
|
|
1384
|
+
cls = torch.nn.functional.pad(cls, (0, pad_length, 0, 0), "constant", 0)
|
|
1385
|
+
|
|
1386
|
+
return cls, xywh * self.normalize
|
|
1425
1387
|
|
|
1426
1388
|
|
|
1427
1389
|
class NMSModel(torch.nn.Module):
|
|
1428
1390
|
"""Model wrapper with embedded NMS for Detect, Segment, Pose and OBB."""
|
|
1429
1391
|
|
|
1430
1392
|
def __init__(self, model, args):
|
|
1431
|
-
"""
|
|
1432
|
-
Initialize the NMSModel.
|
|
1393
|
+
"""Initialize the NMSModel.
|
|
1433
1394
|
|
|
1434
1395
|
Args:
|
|
1435
|
-
model (torch.nn.
|
|
1396
|
+
model (torch.nn.Module): The model to wrap with NMS postprocessing.
|
|
1436
1397
|
args (Namespace): The export arguments.
|
|
1437
1398
|
"""
|
|
1438
1399
|
super().__init__()
|
|
@@ -1442,14 +1403,14 @@ class NMSModel(torch.nn.Module):
|
|
|
1442
1403
|
self.is_tf = self.args.format in frozenset({"saved_model", "tflite", "tfjs"})
|
|
1443
1404
|
|
|
1444
1405
|
def forward(self, x):
|
|
1445
|
-
"""
|
|
1446
|
-
Performs inference with NMS post-processing. Supports Detect, Segment, OBB and Pose.
|
|
1406
|
+
"""Perform inference with NMS post-processing. Supports Detect, Segment, OBB and Pose.
|
|
1447
1407
|
|
|
1448
1408
|
Args:
|
|
1449
1409
|
x (torch.Tensor): The preprocessed tensor with shape (N, 3, H, W).
|
|
1450
1410
|
|
|
1451
1411
|
Returns:
|
|
1452
|
-
(torch.Tensor): List of detections, each an (N, max_det, 4 + 2 + extra_shape) Tensor where N is the number
|
|
1412
|
+
(torch.Tensor): List of detections, each an (N, max_det, 4 + 2 + extra_shape) Tensor where N is the number
|
|
1413
|
+
of detections after NMS.
|
|
1453
1414
|
"""
|
|
1454
1415
|
from functools import partial
|
|
1455
1416
|
|
|
@@ -1468,11 +1429,11 @@ class NMSModel(torch.nn.Module):
|
|
|
1468
1429
|
scores, classes = scores.max(dim=-1)
|
|
1469
1430
|
self.args.max_det = min(pred.shape[1], self.args.max_det) # in case num_anchors < max_det
|
|
1470
1431
|
# (N, max_det, 4 coords + 1 class score + 1 class label + extra_shape).
|
|
1471
|
-
out = torch.zeros(
|
|
1432
|
+
out = torch.zeros(pred.shape[0], self.args.max_det, boxes.shape[-1] + 2 + extra_shape, **kwargs)
|
|
1472
1433
|
for i in range(bs):
|
|
1473
1434
|
box, cls, score, extra = boxes[i], classes[i], scores[i], extras[i]
|
|
1474
1435
|
mask = score > self.args.conf
|
|
1475
|
-
if self.is_tf:
|
|
1436
|
+
if self.is_tf or (self.args.format == "onnx" and self.obb):
|
|
1476
1437
|
# TFLite GatherND error if mask is empty
|
|
1477
1438
|
score *= mask
|
|
1478
1439
|
# Explicit length otherwise reshape error, hardcoded to `self.args.max_det * 5`
|
|
@@ -1480,27 +1441,28 @@ class NMSModel(torch.nn.Module):
|
|
|
1480
1441
|
box, score, cls, extra = box[mask], score[mask], cls[mask], extra[mask]
|
|
1481
1442
|
nmsbox = box.clone()
|
|
1482
1443
|
# `8` is the minimum value experimented to get correct NMS results for obb
|
|
1483
|
-
multiplier = 8 if self.obb else 1
|
|
1444
|
+
multiplier = (8 if self.obb else 1) / max(len(self.model.names), 1)
|
|
1484
1445
|
# Normalize boxes for NMS since large values for class offset causes issue with int8 quantization
|
|
1485
1446
|
if self.args.format == "tflite": # TFLite is already normalized
|
|
1486
1447
|
nmsbox *= multiplier
|
|
1487
1448
|
else:
|
|
1488
|
-
nmsbox = multiplier * nmsbox / torch.tensor(x.shape[2:], **kwargs).max()
|
|
1489
|
-
if not self.args.agnostic_nms: # class-
|
|
1449
|
+
nmsbox = multiplier * (nmsbox / torch.tensor(x.shape[2:], **kwargs).max())
|
|
1450
|
+
if not self.args.agnostic_nms: # class-wise NMS
|
|
1490
1451
|
end = 2 if self.obb else 4
|
|
1491
1452
|
# fully explicit expansion otherwise reshape error
|
|
1492
|
-
|
|
1493
|
-
cls_offset = cls.reshape(-1, 1).expand(nmsbox.shape[0], end)
|
|
1453
|
+
cls_offset = cls.view(cls.shape[0], 1).expand(cls.shape[0], end)
|
|
1494
1454
|
offbox = nmsbox[:, :end] + cls_offset * multiplier
|
|
1495
1455
|
nmsbox = torch.cat((offbox, nmsbox[:, end:]), dim=-1)
|
|
1496
1456
|
nms_fn = (
|
|
1497
1457
|
partial(
|
|
1498
|
-
|
|
1458
|
+
TorchNMS.fast_nms,
|
|
1499
1459
|
use_triu=not (
|
|
1500
1460
|
self.is_tf
|
|
1501
1461
|
or (self.args.opset or 14) < 14
|
|
1502
1462
|
or (self.args.format == "openvino" and self.args.int8) # OpenVINO int8 error with triu
|
|
1503
1463
|
),
|
|
1464
|
+
iou_func=batch_probiou,
|
|
1465
|
+
exit_early=False,
|
|
1504
1466
|
)
|
|
1505
1467
|
if self.obb
|
|
1506
1468
|
else nms
|