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.
Files changed (215) hide show
  1. {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/METADATA +41 -34
  2. dgenerate_ultralytics_headless-8.3.224.dist-info/RECORD +285 -0
  3. {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/WHEEL +1 -1
  4. tests/__init__.py +7 -6
  5. tests/conftest.py +15 -39
  6. tests/test_cli.py +17 -17
  7. tests/test_cuda.py +17 -8
  8. tests/test_engine.py +36 -10
  9. tests/test_exports.py +98 -37
  10. tests/test_integrations.py +12 -15
  11. tests/test_python.py +126 -82
  12. tests/test_solutions.py +319 -135
  13. ultralytics/__init__.py +27 -9
  14. ultralytics/cfg/__init__.py +83 -87
  15. ultralytics/cfg/datasets/Argoverse.yaml +4 -4
  16. ultralytics/cfg/datasets/DOTAv1.5.yaml +2 -2
  17. ultralytics/cfg/datasets/DOTAv1.yaml +2 -2
  18. ultralytics/cfg/datasets/GlobalWheat2020.yaml +2 -2
  19. ultralytics/cfg/datasets/HomeObjects-3K.yaml +4 -5
  20. ultralytics/cfg/datasets/ImageNet.yaml +3 -3
  21. ultralytics/cfg/datasets/Objects365.yaml +24 -20
  22. ultralytics/cfg/datasets/SKU-110K.yaml +9 -9
  23. ultralytics/cfg/datasets/VOC.yaml +10 -13
  24. ultralytics/cfg/datasets/VisDrone.yaml +43 -33
  25. ultralytics/cfg/datasets/african-wildlife.yaml +5 -5
  26. ultralytics/cfg/datasets/brain-tumor.yaml +4 -5
  27. ultralytics/cfg/datasets/carparts-seg.yaml +5 -5
  28. ultralytics/cfg/datasets/coco-pose.yaml +26 -4
  29. ultralytics/cfg/datasets/coco.yaml +4 -4
  30. ultralytics/cfg/datasets/coco128-seg.yaml +2 -2
  31. ultralytics/cfg/datasets/coco128.yaml +2 -2
  32. ultralytics/cfg/datasets/coco8-grayscale.yaml +103 -0
  33. ultralytics/cfg/datasets/coco8-multispectral.yaml +2 -2
  34. ultralytics/cfg/datasets/coco8-pose.yaml +23 -2
  35. ultralytics/cfg/datasets/coco8-seg.yaml +2 -2
  36. ultralytics/cfg/datasets/coco8.yaml +2 -2
  37. ultralytics/cfg/datasets/construction-ppe.yaml +32 -0
  38. ultralytics/cfg/datasets/crack-seg.yaml +5 -5
  39. ultralytics/cfg/datasets/dog-pose.yaml +32 -4
  40. ultralytics/cfg/datasets/dota8-multispectral.yaml +2 -2
  41. ultralytics/cfg/datasets/dota8.yaml +2 -2
  42. ultralytics/cfg/datasets/hand-keypoints.yaml +29 -4
  43. ultralytics/cfg/datasets/lvis.yaml +9 -9
  44. ultralytics/cfg/datasets/medical-pills.yaml +4 -5
  45. ultralytics/cfg/datasets/open-images-v7.yaml +7 -10
  46. ultralytics/cfg/datasets/package-seg.yaml +5 -5
  47. ultralytics/cfg/datasets/signature.yaml +4 -4
  48. ultralytics/cfg/datasets/tiger-pose.yaml +20 -4
  49. ultralytics/cfg/datasets/xView.yaml +5 -5
  50. ultralytics/cfg/default.yaml +96 -93
  51. ultralytics/cfg/trackers/botsort.yaml +16 -17
  52. ultralytics/cfg/trackers/bytetrack.yaml +9 -11
  53. ultralytics/data/__init__.py +4 -4
  54. ultralytics/data/annotator.py +12 -12
  55. ultralytics/data/augment.py +531 -564
  56. ultralytics/data/base.py +76 -81
  57. ultralytics/data/build.py +206 -42
  58. ultralytics/data/converter.py +179 -78
  59. ultralytics/data/dataset.py +121 -121
  60. ultralytics/data/loaders.py +114 -91
  61. ultralytics/data/split.py +28 -15
  62. ultralytics/data/split_dota.py +67 -48
  63. ultralytics/data/utils.py +110 -89
  64. ultralytics/engine/exporter.py +422 -460
  65. ultralytics/engine/model.py +224 -252
  66. ultralytics/engine/predictor.py +94 -89
  67. ultralytics/engine/results.py +345 -595
  68. ultralytics/engine/trainer.py +231 -134
  69. ultralytics/engine/tuner.py +279 -73
  70. ultralytics/engine/validator.py +53 -46
  71. ultralytics/hub/__init__.py +26 -28
  72. ultralytics/hub/auth.py +30 -16
  73. ultralytics/hub/google/__init__.py +34 -36
  74. ultralytics/hub/session.py +53 -77
  75. ultralytics/hub/utils.py +23 -109
  76. ultralytics/models/__init__.py +1 -1
  77. ultralytics/models/fastsam/__init__.py +1 -1
  78. ultralytics/models/fastsam/model.py +36 -18
  79. ultralytics/models/fastsam/predict.py +33 -44
  80. ultralytics/models/fastsam/utils.py +4 -5
  81. ultralytics/models/fastsam/val.py +12 -14
  82. ultralytics/models/nas/__init__.py +1 -1
  83. ultralytics/models/nas/model.py +16 -20
  84. ultralytics/models/nas/predict.py +12 -14
  85. ultralytics/models/nas/val.py +4 -5
  86. ultralytics/models/rtdetr/__init__.py +1 -1
  87. ultralytics/models/rtdetr/model.py +9 -9
  88. ultralytics/models/rtdetr/predict.py +22 -17
  89. ultralytics/models/rtdetr/train.py +20 -16
  90. ultralytics/models/rtdetr/val.py +79 -59
  91. ultralytics/models/sam/__init__.py +8 -2
  92. ultralytics/models/sam/amg.py +53 -38
  93. ultralytics/models/sam/build.py +29 -31
  94. ultralytics/models/sam/model.py +33 -38
  95. ultralytics/models/sam/modules/blocks.py +159 -182
  96. ultralytics/models/sam/modules/decoders.py +38 -47
  97. ultralytics/models/sam/modules/encoders.py +114 -133
  98. ultralytics/models/sam/modules/memory_attention.py +38 -31
  99. ultralytics/models/sam/modules/sam.py +114 -93
  100. ultralytics/models/sam/modules/tiny_encoder.py +268 -291
  101. ultralytics/models/sam/modules/transformer.py +59 -66
  102. ultralytics/models/sam/modules/utils.py +55 -72
  103. ultralytics/models/sam/predict.py +745 -341
  104. ultralytics/models/utils/loss.py +118 -107
  105. ultralytics/models/utils/ops.py +118 -71
  106. ultralytics/models/yolo/__init__.py +1 -1
  107. ultralytics/models/yolo/classify/predict.py +28 -26
  108. ultralytics/models/yolo/classify/train.py +50 -81
  109. ultralytics/models/yolo/classify/val.py +68 -61
  110. ultralytics/models/yolo/detect/predict.py +12 -15
  111. ultralytics/models/yolo/detect/train.py +56 -46
  112. ultralytics/models/yolo/detect/val.py +279 -223
  113. ultralytics/models/yolo/model.py +167 -86
  114. ultralytics/models/yolo/obb/predict.py +7 -11
  115. ultralytics/models/yolo/obb/train.py +23 -25
  116. ultralytics/models/yolo/obb/val.py +107 -99
  117. ultralytics/models/yolo/pose/__init__.py +1 -1
  118. ultralytics/models/yolo/pose/predict.py +12 -14
  119. ultralytics/models/yolo/pose/train.py +31 -69
  120. ultralytics/models/yolo/pose/val.py +119 -254
  121. ultralytics/models/yolo/segment/predict.py +21 -25
  122. ultralytics/models/yolo/segment/train.py +12 -66
  123. ultralytics/models/yolo/segment/val.py +126 -305
  124. ultralytics/models/yolo/world/train.py +53 -45
  125. ultralytics/models/yolo/world/train_world.py +51 -32
  126. ultralytics/models/yolo/yoloe/__init__.py +7 -7
  127. ultralytics/models/yolo/yoloe/predict.py +30 -37
  128. ultralytics/models/yolo/yoloe/train.py +89 -71
  129. ultralytics/models/yolo/yoloe/train_seg.py +15 -17
  130. ultralytics/models/yolo/yoloe/val.py +56 -41
  131. ultralytics/nn/__init__.py +9 -11
  132. ultralytics/nn/autobackend.py +179 -107
  133. ultralytics/nn/modules/__init__.py +67 -67
  134. ultralytics/nn/modules/activation.py +8 -7
  135. ultralytics/nn/modules/block.py +302 -323
  136. ultralytics/nn/modules/conv.py +61 -104
  137. ultralytics/nn/modules/head.py +488 -186
  138. ultralytics/nn/modules/transformer.py +183 -123
  139. ultralytics/nn/modules/utils.py +15 -20
  140. ultralytics/nn/tasks.py +327 -203
  141. ultralytics/nn/text_model.py +81 -65
  142. ultralytics/py.typed +1 -0
  143. ultralytics/solutions/__init__.py +12 -12
  144. ultralytics/solutions/ai_gym.py +19 -27
  145. ultralytics/solutions/analytics.py +36 -26
  146. ultralytics/solutions/config.py +29 -28
  147. ultralytics/solutions/distance_calculation.py +23 -24
  148. ultralytics/solutions/heatmap.py +17 -19
  149. ultralytics/solutions/instance_segmentation.py +21 -19
  150. ultralytics/solutions/object_blurrer.py +16 -17
  151. ultralytics/solutions/object_counter.py +48 -53
  152. ultralytics/solutions/object_cropper.py +22 -16
  153. ultralytics/solutions/parking_management.py +61 -58
  154. ultralytics/solutions/queue_management.py +19 -19
  155. ultralytics/solutions/region_counter.py +63 -50
  156. ultralytics/solutions/security_alarm.py +22 -25
  157. ultralytics/solutions/similarity_search.py +107 -60
  158. ultralytics/solutions/solutions.py +343 -262
  159. ultralytics/solutions/speed_estimation.py +35 -31
  160. ultralytics/solutions/streamlit_inference.py +104 -40
  161. ultralytics/solutions/templates/similarity-search.html +31 -24
  162. ultralytics/solutions/trackzone.py +24 -24
  163. ultralytics/solutions/vision_eye.py +11 -12
  164. ultralytics/trackers/__init__.py +1 -1
  165. ultralytics/trackers/basetrack.py +18 -27
  166. ultralytics/trackers/bot_sort.py +48 -39
  167. ultralytics/trackers/byte_tracker.py +94 -94
  168. ultralytics/trackers/track.py +7 -16
  169. ultralytics/trackers/utils/gmc.py +37 -69
  170. ultralytics/trackers/utils/kalman_filter.py +68 -76
  171. ultralytics/trackers/utils/matching.py +13 -17
  172. ultralytics/utils/__init__.py +251 -275
  173. ultralytics/utils/autobatch.py +19 -7
  174. ultralytics/utils/autodevice.py +68 -38
  175. ultralytics/utils/benchmarks.py +169 -130
  176. ultralytics/utils/callbacks/base.py +12 -13
  177. ultralytics/utils/callbacks/clearml.py +14 -15
  178. ultralytics/utils/callbacks/comet.py +139 -66
  179. ultralytics/utils/callbacks/dvc.py +19 -27
  180. ultralytics/utils/callbacks/hub.py +8 -6
  181. ultralytics/utils/callbacks/mlflow.py +6 -10
  182. ultralytics/utils/callbacks/neptune.py +11 -19
  183. ultralytics/utils/callbacks/platform.py +73 -0
  184. ultralytics/utils/callbacks/raytune.py +3 -4
  185. ultralytics/utils/callbacks/tensorboard.py +9 -12
  186. ultralytics/utils/callbacks/wb.py +33 -30
  187. ultralytics/utils/checks.py +163 -114
  188. ultralytics/utils/cpu.py +89 -0
  189. ultralytics/utils/dist.py +24 -20
  190. ultralytics/utils/downloads.py +176 -146
  191. ultralytics/utils/errors.py +11 -13
  192. ultralytics/utils/events.py +113 -0
  193. ultralytics/utils/export/__init__.py +7 -0
  194. ultralytics/utils/{export.py → export/engine.py} +81 -63
  195. ultralytics/utils/export/imx.py +294 -0
  196. ultralytics/utils/export/tensorflow.py +217 -0
  197. ultralytics/utils/files.py +33 -36
  198. ultralytics/utils/git.py +137 -0
  199. ultralytics/utils/instance.py +105 -120
  200. ultralytics/utils/logger.py +404 -0
  201. ultralytics/utils/loss.py +99 -61
  202. ultralytics/utils/metrics.py +649 -478
  203. ultralytics/utils/nms.py +337 -0
  204. ultralytics/utils/ops.py +263 -451
  205. ultralytics/utils/patches.py +70 -31
  206. ultralytics/utils/plotting.py +253 -223
  207. ultralytics/utils/tal.py +48 -61
  208. ultralytics/utils/torch_utils.py +244 -251
  209. ultralytics/utils/tqdm.py +438 -0
  210. ultralytics/utils/triton.py +22 -23
  211. ultralytics/utils/tuner.py +11 -10
  212. dgenerate_ultralytics_headless-8.3.137.dist-info/RECORD +0 -272
  213. {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/entry_points.txt +0 -0
  214. {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/licenses/LICENSE +0 -0
  215. {dgenerate_ultralytics_headless-8.3.137.dist-info → dgenerate_ultralytics_headless-8.3.224.dist-info}/top_level.txt +0 -0
@@ -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 attempt_download_asset, get_github_assets, safe_download
107
- from ultralytics.utils.export import export_engine, export_onnx
108
- from ultralytics.utils.files import file_size, spaces_in_path
109
- from ultralytics.utils.ops import Profile, nms_rotated
110
- from ultralytics.utils.torch_utils import TORCH_1_13, get_cpu_info, get_latest_opset, select_device
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, model = inner_func(*args, **kwargs)
193
- LOGGER.info(f"{prefix} export success {dt.t:.1f}s, saved as '{f}' ({file_size(f):.1f} MB)")
194
- return f, model
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
- A class for exporting a model.
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 (list, optional): List of callback functions.
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
- raise ValueError(f"Invalid export format='{fmt}'. Valid formats are {fmts}")
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
- (jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle, mnn, ncnn, imx, rknn) = (
269
- flags # export booleans
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 fmt == "engine" and self.args.device is None:
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 fmt == "engine" and "dla" in str(self.args.device): # convert int/list to str first
280
- dla = self.args.device.split(":")[-1]
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 != "detect":
296
- raise ValueError("IMX export only supported for detection models.")
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 (tflite and ARM64 and LINUX), "TFLite export with NMS unsupported on ARM64 Linux"
328
- if getattr(model, "end2end", False):
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 "intel" in get_cpu_info().lower():
433
+ if is_intel():
358
434
  LOGGER.info(
359
- "💡 ProTip: Export to OpenVINO format for best performance on Intel CPUs."
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.torch_utils import FXModel
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 = FXModel(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 or ncnn: # TorchScript
457
- f[0], _ = self.export_torchscript()
531
+ if jit: # TorchScript
532
+ f[0] = self.export_torchscript()
458
533
  if engine: # TensorRT required before ONNX
459
- f[1], _ = self.export_engine(dla=dla)
460
- if onnx: # ONNX
461
- f[2], _ = self.export_onnx()
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], _ = self.export_openvino()
538
+ f[3] = self.export_openvino()
464
539
  if coreml: # CoreML
465
- f[4], _ = self.export_coreml()
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], _ = self.export_pb(keras_model=keras_model)
545
+ f[6] = self.export_pb(keras_model=keras_model)
471
546
  if tflite:
472
- f[7], _ = self.export_tflite()
547
+ f[7] = self.export_tflite()
473
548
  if edgetpu:
474
- f[8], _ = self.export_edgetpu(tflite_model=Path(f[5]) / f"{self.file.stem}_full_integer_quant.tflite")
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], _ = self.export_tfjs()
551
+ f[9] = self.export_tfjs()
477
552
  if paddle: # PaddlePaddle
478
- f[10], _ = self.export_paddle()
553
+ f[10] = self.export_paddle()
479
554
  if mnn: # MNN
480
- f[11], _ = self.export_mnn()
555
+ f[11] = self.export_mnn()
481
556
  if ncnn: # NCNN
482
- f[12], _ = self.export_ncnn()
557
+ f[12] = self.export_ncnn()
483
558
  if imx:
484
- f[13], _ = self.export_imx()
559
+ f[13] = self.export_imx()
485
560
  if rknn:
486
- f[14], _ = self.export_rknn()
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 fmt == "pb" else ""
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 model export."""
541
- LOGGER.info(f"\n{prefix} starting export with torch {torch.__version__}...")
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, None
628
+ return f
554
629
 
555
630
  @try_export
556
631
  def export_onnx(self, prefix=colorstr("ONNX:")):
557
- """YOLO ONNX export."""
558
- requirements = ["onnx>=1.12.0,<1.18.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.53", "onnxruntime" + ("-gpu" if torch.cuda.is_available() else "")]
635
+ requirements += ["onnxslim>=0.1.71", "onnxruntime" + ("-gpu" if torch.cuda.is_available() else "")]
561
636
  check_requirements(requirements)
562
- import onnx # noqa
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 isinstance(self.model, SegmentationModel) else ["output0"]
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 = opset_version # for NMSModel
657
+ self.args.opset = opset # for NMSModel
580
658
 
581
659
  with arange_patch(self.args):
582
- export_onnx(
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=opset_version,
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, model_onnx
695
+ return f
613
696
 
614
697
  @try_export
615
698
  def export_openvino(self, prefix=colorstr("OpenVINO:")):
616
- """YOLO OpenVINO export."""
617
- if MACOS:
618
- msg = "OpenVINO error in macOS>=15.4 https://github.com/openvinotoolkit/openvino/issues/30023"
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 TORCH_1_13, f"OpenVINO export requires torch>=1.13.0 but torch=={torch.__version__} is installed"
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, None
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, None
770
+ return f
690
771
 
691
772
  @try_export
692
773
  def export_paddle(self, prefix=colorstr("PaddlePaddle:")):
693
- """YOLO Paddle export."""
774
+ """Export YOLO model to PaddlePaddle format."""
694
775
  assert not IS_JETSON, "Jetson Paddle exports not supported yet"
695
- check_requirements(("paddlepaddle-gpu" if torch.cuda.is_available() else "paddlepaddle>=3.0.0", "x2paddle"))
696
- import x2paddle # noqa
697
- from x2paddle.convert import pytorch2paddle # noqa
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, None
794
+ return f
705
795
 
706
796
  @try_export
707
797
  def export_mnn(self, prefix=colorstr("MNN:")):
708
- """YOLO MNN export using MNN https://github.com/alibaba/MNN."""
709
- f_onnx, _ = self.export_onnx() # get onnx model first
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 # noqa
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, None
819
+ return f
730
820
 
731
821
  @try_export
732
822
  def export_ncnn(self, prefix=colorstr("NCNN:")):
733
- """YOLO NCNN export using PNNX https://github.com/pnnx/pnnx."""
734
- check_requirements("ncnn")
735
- import ncnn # noqa
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
- f_ts = self.file.with_suffix(".torchscript")
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 = [x for x in assets if f"{system}.zip" in x][0]
753
- assert isinstance(asset, str), "Unable to retrieve PNNX repo assets" # i.e. pnnx-20240410-macos.zip
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 = "20240410"
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(f_ts),
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.split("=")[-1] for x in pnnx_args]
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), None
887
+ return str(f)
798
888
 
799
889
  @try_export
800
890
  def export_coreml(self, prefix=colorstr("CoreML:")):
801
- """YOLO CoreML export."""
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 # noqa
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 self.args.batch == 1, "CoreML batch sizes > 1 are not supported. Please retry at 'batch=1'."
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=[ct.ImageType("image", shape=self.im.shape, scale=scale, bias=bias)], # expects ct.TensorType
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, ct_model
981
+ return f
877
982
 
878
983
  @try_export
879
984
  def export_engine(self, dla=None, prefix=colorstr("TensorRT:")):
880
- """YOLO TensorRT export https://developer.nvidia.com/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, _ = self.export_onnx() # run before TRT import https://github.com/ultralytics/ultralytics/issues/7016
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 # noqa
990
+ import tensorrt as trt
886
991
  except ImportError:
887
992
  if LINUX:
888
- check_requirements("tensorrt>7.0.0,!=10.1.0")
889
- import tensorrt as trt # noqa
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
- export_engine(
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, None
1018
+ return f
913
1019
 
914
1020
  @try_export
915
1021
  def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):
916
- """YOLO TensorFlow SavedModel export."""
1022
+ """Export YOLO model to TensorFlow SavedModel format."""
917
1023
  cuda = torch.cuda.is_available()
918
1024
  try:
919
- import tensorflow as tf # noqa
1025
+ import tensorflow as tf
920
1026
  except ImportError:
921
- check_requirements("tensorflow>=2.0.0")
922
- import tensorflow as tf # noqa
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,<1.18.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.53",
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
- # Pre-download calibration file to fix https://github.com/PINTO0309/onnx2tf/issues/545
951
- onnx2tf_file = Path("calibration_image_sample_data_20x128x128x3_float32.npy")
952
- if not onnx2tf_file.exists():
953
- attempt_download_asset(f"{onnx2tf_file}.zip", unzip=True, delete=True)
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, _ = self.export_onnx()
958
-
959
- # Export to TF
960
- np_data = None
961
- if self.args.int8:
962
- tmp_file = f / "tmp_tflite_int8_calibration_images.npy" # int8 calibration images file
963
- if self.args.data:
964
- f.mkdir()
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
- f.unlink() if "quant_with_int16_act.tflite" in str(f) else self._add_tflite_metadata(file)
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 export https://github.com/leimao/Frozen-Graph-TensorFlow."""
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
- m = tf.function(lambda x: keras_model(x)) # full model
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 export."""
1097
+ """Export YOLO model to TensorFlow Lite format."""
1022
1098
  # BUG https://github.com/ultralytics/ultralytics/issues/13436
1023
- import tensorflow as tf # noqa
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), None
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 export https://coral.ai/docs/edgetpu/models-intro/."""
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, None
1166
+ return f
1069
1167
 
1070
1168
  @try_export
1071
1169
  def export_tfjs(self, prefix=colorstr("TensorFlow.js:")):
1072
- """YOLO TensorFlow.js export."""
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, None
1178
+ return f
1102
1179
 
1103
1180
  @try_export
1104
1181
  def export_rknn(self, prefix=colorstr("RKNN:")):
1105
- """YOLO RKNN model export."""
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, _ = self.export_onnx()
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, None
1205
+ return export_path
1129
1206
 
1130
1207
  @try_export
1131
1208
  def export_imx(self, prefix=colorstr("IMX:")):
1132
- """YOLO IMX export."""
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(("model-compression-toolkit>=2.3.0", "sony-custom-layers>=0.3.0", "edge-mdt-tpc>=1.1.0"))
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
- def representative_dataset_gen(dataloader=self.get_int8_calibration_dataloader(prefix)):
1161
- for batch in dataloader:
1162
- img = batch["img"]
1163
- img = img / 255.0
1164
- yield [img]
1165
-
1166
- tpc = get_target_platform_capabilities(tpc_version="4.0", device_type="imx500")
1167
-
1168
- bit_cfg = mct.core.BitWidthConfig()
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
- """YOLO CoreML pipeline."""
1293
- import coremltools as ct # noqa
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
- out0, out1 = iter(spec.description.output)
1301
- if MACOS:
1302
- from PIL import Image
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
- _, nc = out0_shape # number of anchors, number of classes
1316
- assert len(names) == nc, f"{len(names)} names found for nc={nc}" # check
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
- # 3. Create NMS protobuf
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(2):
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
- nms_spec.description.output[0].name = "confidence"
1336
- nms_spec.description.output[1].name = "coordinates"
1283
+ output_names = ["confidence", "coordinates"]
1284
+ for i, name in enumerate(output_names):
1285
+ nms_spec.description.output[i].name = name
1337
1286
 
1338
- output_sizes = [nc, 4]
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 = output_sizes[i]
1346
- ma_type.shapeRange.sizeRanges[1].upperBound = output_sizes[i]
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 = out0.name # 1x507x80
1351
- nms.coordinatesInputFeatureName = out1.name # 1x507x4
1352
- nms.confidenceOutputFeatureName = "confidence"
1353
- nms.coordinatesOutputFeatureName = "coordinates"
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
- # 4. Pipeline models together
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=["confidence", "coordinates"],
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
- """Appends the given callback."""
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([1.0 / w, 1.0 / h, 1.0 / w, 1.0 / h]) # broadcast (slower, smaller)
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
- return cls, xywh * self.normalize # confidence (3780, 80), coordinates (3780, 4)
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.module): The model to wrap with NMS postprocessing.
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 of detections after NMS.
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(bs, self.args.max_det, boxes.shape[-1] + 2 + extra_shape, **kwargs)
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-specific NMS
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
- # large max_wh causes issues when quantizing
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
- nms_rotated,
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