exporter.py 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229
  1. # Ultralytics YOLO 🚀, AGPL-3.0 license
  2. """
  3. Export a YOLOv8 PyTorch model to other formats. TensorFlow exports authored by https://github.com/zldrobit
  4. Format | `format=argument` | Model
  5. --- | --- | ---
  6. PyTorch | - | yolov8n.pt
  7. TorchScript | `torchscript` | yolov8n.torchscript
  8. ONNX | `onnx` | yolov8n.onnx
  9. OpenVINO | `openvino` | yolov8n_openvino_model/
  10. TensorRT | `engine` | yolov8n.engine
  11. CoreML | `coreml` | yolov8n.mlpackage
  12. TensorFlow SavedModel | `saved_model` | yolov8n_saved_model/
  13. TensorFlow GraphDef | `pb` | yolov8n.pb
  14. TensorFlow Lite | `tflite` | yolov8n.tflite
  15. TensorFlow Edge TPU | `edgetpu` | yolov8n_edgetpu.tflite
  16. TensorFlow.js | `tfjs` | yolov8n_web_model/
  17. PaddlePaddle | `paddle` | yolov8n_paddle_model/
  18. NCNN | `ncnn` | yolov8n_ncnn_model/
  19. Requirements:
  20. $ pip install "ultralytics[export]"
  21. Python:
  22. from ultralytics import YOLO
  23. model = YOLO('yolov8n.pt')
  24. results = model.export(format='onnx')
  25. CLI:
  26. $ yolo mode=export model=yolov8n.pt format=onnx
  27. Inference:
  28. $ yolo predict model=yolov8n.pt # PyTorch
  29. yolov8n.torchscript # TorchScript
  30. yolov8n.onnx # ONNX Runtime or OpenCV DNN with dnn=True
  31. yolov8n_openvino_model # OpenVINO
  32. yolov8n.engine # TensorRT
  33. yolov8n.mlpackage # CoreML (macOS-only)
  34. yolov8n_saved_model # TensorFlow SavedModel
  35. yolov8n.pb # TensorFlow GraphDef
  36. yolov8n.tflite # TensorFlow Lite
  37. yolov8n_edgetpu.tflite # TensorFlow Edge TPU
  38. yolov8n_paddle_model # PaddlePaddle
  39. yolov8n_ncnn_model # NCNN
  40. TensorFlow.js:
  41. $ cd .. && git clone https://github.com/zldrobit/tfjs-yolov5-example.git && cd tfjs-yolov5-example
  42. $ npm install
  43. $ ln -s ../../yolov5/yolov8n_web_model public/yolov8n_web_model
  44. $ npm start
  45. """
  46. import gc
  47. import json
  48. import os
  49. import shutil
  50. import subprocess
  51. import time
  52. import warnings
  53. from copy import deepcopy
  54. from datetime import datetime
  55. from pathlib import Path
  56. import numpy as np
  57. import torch
  58. from ultralytics.cfg import TASK2DATA, get_cfg
  59. from ultralytics.data import build_dataloader
  60. from ultralytics.data.dataset import YOLODataset
  61. from ultralytics.data.utils import check_cls_dataset, check_det_dataset
  62. from ultralytics.nn.autobackend import check_class_names, default_class_names
  63. from ultralytics.nn.modules import C2f, Detect, RTDETRDecoder
  64. from ultralytics.nn.tasks import DetectionModel, SegmentationModel, WorldModel
  65. from ultralytics.utils import (
  66. ARM64,
  67. DEFAULT_CFG,
  68. IS_JETSON,
  69. LINUX,
  70. LOGGER,
  71. MACOS,
  72. PYTHON_VERSION,
  73. ROOT,
  74. WINDOWS,
  75. __version__,
  76. callbacks,
  77. colorstr,
  78. get_default_args,
  79. yaml_save,
  80. )
  81. from ultralytics.utils.checks import check_imgsz, check_is_path_safe, check_requirements, check_version
  82. from ultralytics.utils.downloads import attempt_download_asset, get_github_assets, safe_download
  83. from ultralytics.utils.files import file_size, spaces_in_path
  84. from ultralytics.utils.ops import Profile
  85. from ultralytics.utils.torch_utils import TORCH_1_13, get_latest_opset, select_device, smart_inference_mode
  86. def export_formats():
  87. """YOLOv8 export formats."""
  88. import pandas # scope for faster 'import ultralytics'
  89. x = [
  90. ["PyTorch", "-", ".pt", True, True],
  91. ["TorchScript", "torchscript", ".torchscript", True, True],
  92. ["ONNX", "onnx", ".onnx", True, True],
  93. ["OpenVINO", "openvino", "_openvino_model", True, False],
  94. ["TensorRT", "engine", ".engine", False, True],
  95. ["CoreML", "coreml", ".mlpackage", True, False],
  96. ["TensorFlow SavedModel", "saved_model", "_saved_model", True, True],
  97. ["TensorFlow GraphDef", "pb", ".pb", True, True],
  98. ["TensorFlow Lite", "tflite", ".tflite", True, False],
  99. ["TensorFlow Edge TPU", "edgetpu", "_edgetpu.tflite", True, False],
  100. ["TensorFlow.js", "tfjs", "_web_model", True, False],
  101. ["PaddlePaddle", "paddle", "_paddle_model", True, True],
  102. ["NCNN", "ncnn", "_ncnn_model", True, True],
  103. ]
  104. return pandas.DataFrame(x, columns=["Format", "Argument", "Suffix", "CPU", "GPU"])
  105. def gd_outputs(gd):
  106. """TensorFlow GraphDef model output node names."""
  107. name_list, input_list = [], []
  108. for node in gd.node: # tensorflow.core.framework.node_def_pb2.NodeDef
  109. name_list.append(node.name)
  110. input_list.extend(node.input)
  111. return sorted(f"{x}:0" for x in list(set(name_list) - set(input_list)) if not x.startswith("NoOp"))
  112. def try_export(inner_func):
  113. """YOLOv8 export decorator, i.e. @try_export."""
  114. inner_args = get_default_args(inner_func)
  115. def outer_func(*args, **kwargs):
  116. """Export a model."""
  117. prefix = inner_args["prefix"]
  118. try:
  119. with Profile() as dt:
  120. f, model = inner_func(*args, **kwargs)
  121. LOGGER.info(f"{prefix} export success ✅ {dt.t:.1f}s, saved as '{f}' ({file_size(f):.1f} MB)")
  122. return f, model
  123. except Exception as e:
  124. LOGGER.info(f"{prefix} export failure ❌ {dt.t:.1f}s: {e}")
  125. raise e
  126. return outer_func
  127. class Exporter:
  128. """
  129. A class for exporting a model.
  130. Attributes:
  131. args (SimpleNamespace): Configuration for the exporter.
  132. callbacks (list, optional): List of callback functions. Defaults to None.
  133. """
  134. def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
  135. """
  136. Initializes the Exporter class.
  137. Args:
  138. cfg (str, optional): Path to a configuration file. Defaults to DEFAULT_CFG.
  139. overrides (dict, optional): Configuration overrides. Defaults to None.
  140. _callbacks (dict, optional): Dictionary of callback functions. Defaults to None.
  141. """
  142. self.args = get_cfg(cfg, overrides)
  143. if self.args.format.lower() in {"coreml", "mlmodel"}: # fix attempt for protobuf<3.20.x errors
  144. os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" # must run before TensorBoard callback
  145. self.callbacks = _callbacks or callbacks.get_default_callbacks()
  146. callbacks.add_integration_callbacks(self)
  147. @smart_inference_mode()
  148. def __call__(self, model=None) -> str:
  149. """Returns list of exported files/dirs after running callbacks."""
  150. self.run_callbacks("on_export_start")
  151. t = time.time()
  152. fmt = self.args.format.lower() # to lowercase
  153. if fmt in {"tensorrt", "trt"}: # 'engine' aliases
  154. fmt = "engine"
  155. if fmt in {"mlmodel", "mlpackage", "mlprogram", "apple", "ios", "coreml"}: # 'coreml' aliases
  156. fmt = "coreml"
  157. fmts = tuple(export_formats()["Argument"][1:]) # available export formats
  158. flags = [x == fmt for x in fmts]
  159. if sum(flags) != 1:
  160. raise ValueError(f"Invalid export format='{fmt}'. Valid formats are {fmts}")
  161. jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle, ncnn = flags # export booleans
  162. is_tf_format = any((saved_model, pb, tflite, edgetpu, tfjs))
  163. # Device
  164. if fmt == "engine" and self.args.device is None:
  165. LOGGER.warning("WARNING ⚠️ TensorRT requires GPU export, automatically assigning device=0")
  166. self.args.device = "0"
  167. self.device = select_device("cpu" if self.args.device is None else self.args.device)
  168. # Checks
  169. if not hasattr(model, "names"):
  170. model.names = default_class_names()
  171. model.names = check_class_names(model.names)
  172. if self.args.half and self.args.int8:
  173. LOGGER.warning("WARNING ⚠️ half=True and int8=True are mutually exclusive, setting half=False.")
  174. self.args.half = False
  175. if self.args.half and onnx and self.device.type == "cpu":
  176. LOGGER.warning("WARNING ⚠️ half=True only compatible with GPU export, i.e. use device=0")
  177. self.args.half = False
  178. assert not self.args.dynamic, "half=True not compatible with dynamic=True, i.e. use only one."
  179. self.imgsz = check_imgsz(self.args.imgsz, stride=model.stride, min_dim=2) # check image size
  180. if self.args.int8 and (engine or xml):
  181. self.args.dynamic = True # enforce dynamic to export TensorRT INT8; ensures ONNX is dynamic
  182. if self.args.optimize:
  183. assert not ncnn, "optimize=True not compatible with format='ncnn', i.e. use optimize=False"
  184. assert self.device.type == "cpu", "optimize=True not compatible with cuda devices, i.e. use device='cpu'"
  185. if edgetpu:
  186. if not LINUX:
  187. raise SystemError("Edge TPU export only supported on Linux. See https://coral.ai/docs/edgetpu/compiler")
  188. elif self.args.batch != 1: # see github.com/ultralytics/ultralytics/pull/13420
  189. LOGGER.warning("WARNING ⚠️ Edge TPU export requires batch size 1, setting batch=1.")
  190. self.args.batch = 1
  191. if isinstance(model, WorldModel):
  192. LOGGER.warning(
  193. "WARNING ⚠️ YOLOWorld (original version) export is not supported to any format.\n"
  194. "WARNING ⚠️ YOLOWorldv2 models (i.e. 'yolov8s-worldv2.pt') only support export to "
  195. "(torchscript, onnx, openvino, engine, coreml) formats. "
  196. "See https://docs.ultralytics.com/models/yolo-world for details."
  197. )
  198. if self.args.int8 and not self.args.data:
  199. self.args.data = DEFAULT_CFG.data or TASK2DATA[getattr(model, "task", "detect")] # assign default data
  200. LOGGER.warning(
  201. "WARNING ⚠️ INT8 export requires a missing 'data' arg for calibration. "
  202. f"Using default 'data={self.args.data}'."
  203. )
  204. # Input
  205. im = torch.zeros(self.args.batch, 3, *self.imgsz).to(self.device)
  206. file = Path(
  207. getattr(model, "pt_path", None) or getattr(model, "yaml_file", None) or model.yaml.get("yaml_file", "")
  208. )
  209. if file.suffix in {".yaml", ".yml"}:
  210. file = Path(file.name)
  211. # Update model
  212. model = deepcopy(model).to(self.device)
  213. for p in model.parameters():
  214. p.requires_grad = False
  215. model.eval()
  216. model.float()
  217. model = model.fuse()
  218. for m in model.modules():
  219. if isinstance(m, (Detect, RTDETRDecoder)): # includes all Detect subclasses like Segment, Pose, OBB
  220. m.dynamic = self.args.dynamic
  221. m.export = True
  222. m.format = self.args.format
  223. elif isinstance(m, C2f) and not is_tf_format:
  224. # EdgeTPU does not support FlexSplitV while split provides cleaner ONNX graph
  225. m.forward = m.forward_split
  226. y = None
  227. for _ in range(2):
  228. y = model(im) # dry runs
  229. if self.args.half and onnx and self.device.type != "cpu":
  230. im, model = im.half(), model.half() # to FP16
  231. # Filter warnings
  232. warnings.filterwarnings("ignore", category=torch.jit.TracerWarning) # suppress TracerWarning
  233. warnings.filterwarnings("ignore", category=UserWarning) # suppress shape prim::Constant missing ONNX warning
  234. warnings.filterwarnings("ignore", category=DeprecationWarning) # suppress CoreML np.bool deprecation warning
  235. # Assign
  236. self.im = im
  237. self.model = model
  238. self.file = file
  239. self.output_shape = (
  240. tuple(y.shape)
  241. if isinstance(y, torch.Tensor)
  242. else tuple(tuple(x.shape if isinstance(x, torch.Tensor) else []) for x in y)
  243. )
  244. self.pretty_name = Path(self.model.yaml.get("yaml_file", self.file)).stem.replace("yolo", "YOLO")
  245. data = model.args["data"] if hasattr(model, "args") and isinstance(model.args, dict) else ""
  246. description = f'Ultralytics {self.pretty_name} model {f"trained on {data}" if data else ""}'
  247. self.metadata = {
  248. "description": description,
  249. "author": "Ultralytics",
  250. "date": datetime.now().isoformat(),
  251. "version": __version__,
  252. "license": "AGPL-3.0 License (https://ultralytics.com/license)",
  253. "docs": "https://docs.ultralytics.com",
  254. "stride": int(max(model.stride)),
  255. "task": model.task,
  256. "batch": self.args.batch,
  257. "imgsz": self.imgsz,
  258. "names": model.names,
  259. } # model metadata
  260. if model.task == "pose":
  261. self.metadata["kpt_shape"] = model.model[-1].kpt_shape
  262. LOGGER.info(
  263. f"\n{colorstr('PyTorch:')} starting from '{file}' with input shape {tuple(im.shape)} BCHW and "
  264. f'output shape(s) {self.output_shape} ({file_size(file):.1f} MB)'
  265. )
  266. # Exports
  267. f = [""] * len(fmts) # exported filenames
  268. if jit or ncnn: # TorchScript
  269. f[0], _ = self.export_torchscript()
  270. if engine: # TensorRT required before ONNX
  271. f[1], _ = self.export_engine()
  272. if onnx: # ONNX
  273. f[2], _ = self.export_onnx()
  274. if xml: # OpenVINO
  275. f[3], _ = self.export_openvino()
  276. if coreml: # CoreML
  277. f[4], _ = self.export_coreml()
  278. if is_tf_format: # TensorFlow formats
  279. self.args.int8 |= edgetpu
  280. f[5], keras_model = self.export_saved_model()
  281. if pb or tfjs: # pb prerequisite to tfjs
  282. f[6], _ = self.export_pb(keras_model=keras_model)
  283. if tflite:
  284. f[7], _ = self.export_tflite(keras_model=keras_model, nms=False, agnostic_nms=self.args.agnostic_nms)
  285. if edgetpu:
  286. f[8], _ = self.export_edgetpu(tflite_model=Path(f[5]) / f"{self.file.stem}_full_integer_quant.tflite")
  287. if tfjs:
  288. f[9], _ = self.export_tfjs()
  289. if paddle: # PaddlePaddle
  290. f[10], _ = self.export_paddle()
  291. if ncnn: # NCNN
  292. f[11], _ = self.export_ncnn()
  293. # Finish
  294. f = [str(x) for x in f if x] # filter out '' and None
  295. if any(f):
  296. f = str(Path(f[-1]))
  297. square = self.imgsz[0] == self.imgsz[1]
  298. s = (
  299. ""
  300. if square
  301. else f"WARNING ⚠️ non-PyTorch val requires square images, 'imgsz={self.imgsz}' will not "
  302. f"work. Use export 'imgsz={max(self.imgsz)}' if val is required."
  303. )
  304. imgsz = self.imgsz[0] if square else str(self.imgsz)[1:-1].replace(" ", "")
  305. predict_data = f"data={data}" if model.task == "segment" and fmt == "pb" else ""
  306. q = "int8" if self.args.int8 else "half" if self.args.half else "" # quantization
  307. LOGGER.info(
  308. f'\nExport complete ({time.time() - t:.1f}s)'
  309. f"\nResults saved to {colorstr('bold', file.parent.resolve())}"
  310. f'\nPredict: yolo predict task={model.task} model={f} imgsz={imgsz} {q} {predict_data}'
  311. f'\nValidate: yolo val task={model.task} model={f} imgsz={imgsz} data={data} {q} {s}'
  312. f'\nVisualize: https://netron.app'
  313. )
  314. self.run_callbacks("on_export_end")
  315. return f # return list of exported files/dirs
  316. def get_int8_calibration_dataloader(self, prefix=""):
  317. """Build and return a dataloader suitable for calibration of INT8 models."""
  318. LOGGER.info(f"{prefix} collecting INT8 calibration images from 'data={self.args.data}'")
  319. data = (check_cls_dataset if self.model.task == "classify" else check_det_dataset)(self.args.data)
  320. dataset = YOLODataset(
  321. data[self.args.split or "val"],
  322. data=data,
  323. task=self.model.task,
  324. imgsz=self.imgsz[0],
  325. augment=False,
  326. batch_size=self.args.batch * 2, # NOTE TensorRT INT8 calibration should use 2x batch size
  327. )
  328. n = len(dataset)
  329. if n < 300:
  330. LOGGER.warning(f"{prefix} WARNING ⚠️ >300 images recommended for INT8 calibration, found {n} images.")
  331. return build_dataloader(dataset, batch=self.args.batch * 2, workers=0) # required for batch loading
  332. @try_export
  333. def export_torchscript(self, prefix=colorstr("TorchScript:")):
  334. """YOLOv8 TorchScript model export."""
  335. LOGGER.info(f"\n{prefix} starting export with torch {torch.__version__}...")
  336. f = self.file.with_suffix(".torchscript")
  337. ts = torch.jit.trace(self.model, self.im, strict=False)
  338. extra_files = {"config.txt": json.dumps(self.metadata)} # torch._C.ExtraFilesMap()
  339. if self.args.optimize: # https://pytorch.org/tutorials/recipes/mobile_interpreter.html
  340. LOGGER.info(f"{prefix} optimizing for mobile...")
  341. from torch.utils.mobile_optimizer import optimize_for_mobile
  342. optimize_for_mobile(ts)._save_for_lite_interpreter(str(f), _extra_files=extra_files)
  343. else:
  344. ts.save(str(f), _extra_files=extra_files)
  345. return f, None
  346. @try_export
  347. def export_onnx(self, prefix=colorstr("ONNX:")):
  348. """YOLOv8 ONNX export."""
  349. requirements = ["onnx>=1.12.0"]
  350. if self.args.simplify:
  351. requirements += ["onnxslim>=0.1.31", "onnxruntime" + ("-gpu" if torch.cuda.is_available() else "")]
  352. check_requirements(requirements)
  353. import onnx # noqa
  354. opset_version = self.args.opset or get_latest_opset()
  355. LOGGER.info(f"\n{prefix} starting export with onnx {onnx.__version__} opset {opset_version}...")
  356. f = str(self.file.with_suffix(".onnx"))
  357. output_names = ["output0", "output1"] if isinstance(self.model, SegmentationModel) else ["output0"]
  358. dynamic = self.args.dynamic
  359. if dynamic:
  360. dynamic = {"images": {0: "batch", 2: "height", 3: "width"}} # shape(1,3,640,640)
  361. if isinstance(self.model, SegmentationModel):
  362. dynamic["output0"] = {0: "batch", 2: "anchors"} # shape(1, 116, 8400)
  363. dynamic["output1"] = {0: "batch", 2: "mask_height", 3: "mask_width"} # shape(1,32,160,160)
  364. elif isinstance(self.model, DetectionModel):
  365. dynamic["output0"] = {0: "batch", 2: "anchors"} # shape(1, 84, 8400)
  366. torch.onnx.export(
  367. self.model.cpu() if dynamic else self.model, # dynamic=True only compatible with cpu
  368. self.im.cpu() if dynamic else self.im,
  369. f,
  370. verbose=False,
  371. opset_version=opset_version,
  372. do_constant_folding=True, # WARNING: DNN inference with torch>=1.12 may require do_constant_folding=False
  373. input_names=["images"],
  374. output_names=output_names,
  375. dynamic_axes=dynamic or None,
  376. )
  377. # Checks
  378. model_onnx = onnx.load(f) # load onnx model
  379. # onnx.checker.check_model(model_onnx) # check onnx model
  380. # Simplify
  381. if self.args.simplify:
  382. try:
  383. import onnxslim
  384. LOGGER.info(f"{prefix} slimming with onnxslim {onnxslim.__version__}...")
  385. model_onnx = onnxslim.slim(model_onnx)
  386. # ONNX Simplifier (deprecated as must be compiled with 'cmake' in aarch64 and Conda CI environments)
  387. # import onnxsim
  388. # model_onnx, check = onnxsim.simplify(model_onnx)
  389. # assert check, "Simplified ONNX model could not be validated"
  390. except Exception as e:
  391. LOGGER.warning(f"{prefix} simplifier failure: {e}")
  392. # Metadata
  393. for k, v in self.metadata.items():
  394. meta = model_onnx.metadata_props.add()
  395. meta.key, meta.value = k, str(v)
  396. onnx.save(model_onnx, f)
  397. return f, model_onnx
  398. @try_export
  399. def export_openvino(self, prefix=colorstr("OpenVINO:")):
  400. """YOLOv8 OpenVINO export."""
  401. check_requirements(f'openvino{"<=2024.0.0" if ARM64 else ">=2024.0.0"}') # fix OpenVINO issue on ARM64
  402. import openvino as ov
  403. LOGGER.info(f"\n{prefix} starting export with openvino {ov.__version__}...")
  404. assert TORCH_1_13, f"OpenVINO export requires torch>=1.13.0 but torch=={torch.__version__} is installed"
  405. ov_model = ov.convert_model(
  406. self.model.cpu(),
  407. input=None if self.args.dynamic else [self.im.shape],
  408. example_input=self.im,
  409. )
  410. def serialize(ov_model, file):
  411. """Set RT info, serialize and save metadata YAML."""
  412. ov_model.set_rt_info("YOLOv8", ["model_info", "model_type"])
  413. ov_model.set_rt_info(True, ["model_info", "reverse_input_channels"])
  414. ov_model.set_rt_info(114, ["model_info", "pad_value"])
  415. ov_model.set_rt_info([255.0], ["model_info", "scale_values"])
  416. ov_model.set_rt_info(self.args.iou, ["model_info", "iou_threshold"])
  417. ov_model.set_rt_info([v.replace(" ", "_") for v in self.model.names.values()], ["model_info", "labels"])
  418. if self.model.task != "classify":
  419. ov_model.set_rt_info("fit_to_window_letterbox", ["model_info", "resize_type"])
  420. ov.runtime.save_model(ov_model, file, compress_to_fp16=self.args.half)
  421. yaml_save(Path(file).parent / "metadata.yaml", self.metadata) # add metadata.yaml
  422. if self.args.int8:
  423. fq = str(self.file).replace(self.file.suffix, f"_int8_openvino_model{os.sep}")
  424. fq_ov = str(Path(fq) / self.file.with_suffix(".xml").name)
  425. check_requirements("nncf>=2.8.0")
  426. import nncf
  427. def transform_fn(data_item) -> np.ndarray:
  428. """Quantization transform function."""
  429. data_item: torch.Tensor = data_item["img"] if isinstance(data_item, dict) else data_item
  430. assert data_item.dtype == torch.uint8, "Input image must be uint8 for the quantization preprocessing"
  431. im = data_item.numpy().astype(np.float32) / 255.0 # uint8 to fp16/32 and 0 - 255 to 0.0 - 1.0
  432. return np.expand_dims(im, 0) if im.ndim == 3 else im
  433. # Generate calibration data for integer quantization
  434. ignored_scope = None
  435. if isinstance(self.model.model[-1], Detect):
  436. # Includes all Detect subclasses like Segment, Pose, OBB, WorldDetect
  437. head_module_name = ".".join(list(self.model.named_modules())[-1][0].split(".")[:2])
  438. ignored_scope = nncf.IgnoredScope( # ignore operations
  439. patterns=[
  440. f".*{head_module_name}/.*/Add",
  441. f".*{head_module_name}/.*/Sub*",
  442. f".*{head_module_name}/.*/Mul*",
  443. f".*{head_module_name}/.*/Div*",
  444. f".*{head_module_name}\\.dfl.*",
  445. ],
  446. types=["Sigmoid"],
  447. )
  448. quantized_ov_model = nncf.quantize(
  449. model=ov_model,
  450. calibration_dataset=nncf.Dataset(self.get_int8_calibration_dataloader(prefix), transform_fn),
  451. preset=nncf.QuantizationPreset.MIXED,
  452. ignored_scope=ignored_scope,
  453. )
  454. serialize(quantized_ov_model, fq_ov)
  455. return fq, None
  456. f = str(self.file).replace(self.file.suffix, f"_openvino_model{os.sep}")
  457. f_ov = str(Path(f) / self.file.with_suffix(".xml").name)
  458. serialize(ov_model, f_ov)
  459. return f, None
  460. @try_export
  461. def export_paddle(self, prefix=colorstr("PaddlePaddle:")):
  462. """YOLOv8 Paddle export."""
  463. check_requirements(("paddlepaddle", "x2paddle"))
  464. import x2paddle # noqa
  465. from x2paddle.convert import pytorch2paddle # noqa
  466. LOGGER.info(f"\n{prefix} starting export with X2Paddle {x2paddle.__version__}...")
  467. f = str(self.file).replace(self.file.suffix, f"_paddle_model{os.sep}")
  468. pytorch2paddle(module=self.model, save_dir=f, jit_type="trace", input_examples=[self.im]) # export
  469. yaml_save(Path(f) / "metadata.yaml", self.metadata) # add metadata.yaml
  470. return f, None
  471. @try_export
  472. def export_ncnn(self, prefix=colorstr("NCNN:")):
  473. """
  474. YOLOv8 NCNN export using PNNX https://github.com/pnnx/pnnx.
  475. """
  476. check_requirements("ncnn")
  477. import ncnn # noqa
  478. LOGGER.info(f"\n{prefix} starting export with NCNN {ncnn.__version__}...")
  479. f = Path(str(self.file).replace(self.file.suffix, f"_ncnn_model{os.sep}"))
  480. f_ts = self.file.with_suffix(".torchscript")
  481. name = Path("pnnx.exe" if WINDOWS else "pnnx") # PNNX filename
  482. pnnx = name if name.is_file() else (ROOT / name)
  483. if not pnnx.is_file():
  484. LOGGER.warning(
  485. f"{prefix} WARNING ⚠️ PNNX not found. Attempting to download binary file from "
  486. "https://github.com/pnnx/pnnx/.\nNote PNNX Binary file must be placed in current working directory "
  487. f"or in {ROOT}. See PNNX repo for full installation instructions."
  488. )
  489. system = "macos" if MACOS else "windows" if WINDOWS else "linux-aarch64" if ARM64 else "linux"
  490. try:
  491. release, assets = get_github_assets(repo="pnnx/pnnx")
  492. asset = [x for x in assets if f"{system}.zip" in x][0]
  493. assert isinstance(asset, str), "Unable to retrieve PNNX repo assets" # i.e. pnnx-20240410-macos.zip
  494. LOGGER.info(f"{prefix} successfully found latest PNNX asset file {asset}")
  495. except Exception as e:
  496. release = "20240410"
  497. asset = f"pnnx-{release}-{system}.zip"
  498. LOGGER.warning(f"{prefix} WARNING ⚠️ PNNX GitHub assets not found: {e}, using default {asset}")
  499. unzip_dir = safe_download(f"https://github.com/pnnx/pnnx/releases/download/{release}/{asset}", delete=True)
  500. if check_is_path_safe(Path.cwd(), unzip_dir): # avoid path traversal security vulnerability
  501. (unzip_dir / name).rename(pnnx) # move binary to ROOT
  502. pnnx.chmod(0o777) # set read, write, and execute permissions for everyone
  503. shutil.rmtree(unzip_dir) # delete unzip dir
  504. ncnn_args = [
  505. f'ncnnparam={f / "model.ncnn.param"}',
  506. f'ncnnbin={f / "model.ncnn.bin"}',
  507. f'ncnnpy={f / "model_ncnn.py"}',
  508. ]
  509. pnnx_args = [
  510. f'pnnxparam={f / "model.pnnx.param"}',
  511. f'pnnxbin={f / "model.pnnx.bin"}',
  512. f'pnnxpy={f / "model_pnnx.py"}',
  513. f'pnnxonnx={f / "model.pnnx.onnx"}',
  514. ]
  515. cmd = [
  516. str(pnnx),
  517. str(f_ts),
  518. *ncnn_args,
  519. *pnnx_args,
  520. f"fp16={int(self.args.half)}",
  521. f"device={self.device.type}",
  522. f'inputshape="{[self.args.batch, 3, *self.imgsz]}"',
  523. ]
  524. f.mkdir(exist_ok=True) # make ncnn_model directory
  525. LOGGER.info(f"{prefix} running '{' '.join(cmd)}'")
  526. subprocess.run(cmd, check=True)
  527. # Remove debug files
  528. pnnx_files = [x.split("=")[-1] for x in pnnx_args]
  529. for f_debug in ("debug.bin", "debug.param", "debug2.bin", "debug2.param", *pnnx_files):
  530. Path(f_debug).unlink(missing_ok=True)
  531. yaml_save(f / "metadata.yaml", self.metadata) # add metadata.yaml
  532. return str(f), None
  533. @try_export
  534. def export_coreml(self, prefix=colorstr("CoreML:")):
  535. """YOLOv8 CoreML export."""
  536. mlmodel = self.args.format.lower() == "mlmodel" # legacy *.mlmodel export format requested
  537. check_requirements("coremltools>=6.0,<=6.2" if mlmodel else "coremltools>=7.0")
  538. import coremltools as ct # noqa
  539. LOGGER.info(f"\n{prefix} starting export with coremltools {ct.__version__}...")
  540. assert not WINDOWS, "CoreML export is not supported on Windows, please run on macOS or Linux."
  541. assert self.args.batch == 1, "CoreML batch sizes > 1 are not supported. Please retry at 'batch=1'."
  542. f = self.file.with_suffix(".mlmodel" if mlmodel else ".mlpackage")
  543. if f.is_dir():
  544. shutil.rmtree(f)
  545. bias = [0.0, 0.0, 0.0]
  546. scale = 1 / 255
  547. classifier_config = None
  548. if self.model.task == "classify":
  549. classifier_config = ct.ClassifierConfig(list(self.model.names.values())) if self.args.nms else None
  550. model = self.model
  551. elif self.model.task == "detect":
  552. model = IOSDetectModel(self.model, self.im) if self.args.nms else self.model
  553. else:
  554. if self.args.nms:
  555. LOGGER.warning(f"{prefix} WARNING ⚠️ 'nms=True' is only available for Detect models like 'yolov8n.pt'.")
  556. # TODO CoreML Segment and Pose model pipelining
  557. model = self.model
  558. ts = torch.jit.trace(model.eval(), self.im, strict=False) # TorchScript model
  559. ct_model = ct.convert(
  560. ts,
  561. inputs=[ct.ImageType("image", shape=self.im.shape, scale=scale, bias=bias)],
  562. classifier_config=classifier_config,
  563. convert_to="neuralnetwork" if mlmodel else "mlprogram",
  564. )
  565. bits, mode = (8, "kmeans") if self.args.int8 else (16, "linear") if self.args.half else (32, None)
  566. if bits < 32:
  567. if "kmeans" in mode:
  568. check_requirements("scikit-learn") # scikit-learn package required for k-means quantization
  569. if mlmodel:
  570. ct_model = ct.models.neural_network.quantization_utils.quantize_weights(ct_model, bits, mode)
  571. elif bits == 8: # mlprogram already quantized to FP16
  572. import coremltools.optimize.coreml as cto
  573. op_config = cto.OpPalettizerConfig(mode="kmeans", nbits=bits, weight_threshold=512)
  574. config = cto.OptimizationConfig(global_config=op_config)
  575. ct_model = cto.palettize_weights(ct_model, config=config)
  576. if self.args.nms and self.model.task == "detect":
  577. if mlmodel:
  578. # coremltools<=6.2 NMS export requires Python<3.11
  579. check_version(PYTHON_VERSION, "<3.11", name="Python ", hard=True)
  580. weights_dir = None
  581. else:
  582. ct_model.save(str(f)) # save otherwise weights_dir does not exist
  583. weights_dir = str(f / "Data/com.apple.CoreML/weights")
  584. ct_model = self._pipeline_coreml(ct_model, weights_dir=weights_dir)
  585. m = self.metadata # metadata dict
  586. ct_model.short_description = m.pop("description")
  587. ct_model.author = m.pop("author")
  588. ct_model.license = m.pop("license")
  589. ct_model.version = m.pop("version")
  590. ct_model.user_defined_metadata.update({k: str(v) for k, v in m.items()})
  591. try:
  592. ct_model.save(str(f)) # save *.mlpackage
  593. except Exception as e:
  594. LOGGER.warning(
  595. f"{prefix} WARNING ⚠️ CoreML export to *.mlpackage failed ({e}), reverting to *.mlmodel export. "
  596. f"Known coremltools Python 3.11 and Windows bugs https://github.com/apple/coremltools/issues/1928."
  597. )
  598. f = f.with_suffix(".mlmodel")
  599. ct_model.save(str(f))
  600. return f, ct_model
  601. @try_export
  602. def export_engine(self, prefix=colorstr("TensorRT:")):
  603. """YOLOv8 TensorRT export https://developer.nvidia.com/tensorrt."""
  604. assert self.im.device.type != "cpu", "export running on CPU but must be on GPU, i.e. use 'device=0'"
  605. # self.args.simplify = True
  606. f_onnx, _ = self.export_onnx() # run before TRT import https://github.com/ultralytics/ultralytics/issues/7016
  607. try:
  608. import tensorrt as trt # noqa
  609. except ImportError:
  610. if LINUX:
  611. check_requirements("tensorrt>7.0.0,<=10.1.0")
  612. import tensorrt as trt # noqa
  613. check_version(trt.__version__, ">=7.0.0", hard=True)
  614. check_version(trt.__version__, "<=10.1.0", msg="https://github.com/ultralytics/ultralytics/pull/14239")
  615. # Setup and checks
  616. LOGGER.info(f"\n{prefix} starting export with TensorRT {trt.__version__}...")
  617. is_trt10 = int(trt.__version__.split(".")[0]) >= 10 # is TensorRT >= 10
  618. assert Path(f_onnx).exists(), f"failed to export ONNX file: {f_onnx}"
  619. f = self.file.with_suffix(".engine") # TensorRT engine file
  620. logger = trt.Logger(trt.Logger.INFO)
  621. if self.args.verbose:
  622. logger.min_severity = trt.Logger.Severity.VERBOSE
  623. # Engine builder
  624. builder = trt.Builder(logger)
  625. config = builder.create_builder_config()
  626. workspace = int(self.args.workspace * (1 << 30))
  627. if is_trt10:
  628. config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace)
  629. else: # TensorRT versions 7, 8
  630. config.max_workspace_size = workspace
  631. flag = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
  632. network = builder.create_network(flag)
  633. half = builder.platform_has_fast_fp16 and self.args.half
  634. int8 = builder.platform_has_fast_int8 and self.args.int8
  635. # Read ONNX file
  636. parser = trt.OnnxParser(network, logger)
  637. if not parser.parse_from_file(f_onnx):
  638. raise RuntimeError(f"failed to load ONNX file: {f_onnx}")
  639. # Network inputs
  640. inputs = [network.get_input(i) for i in range(network.num_inputs)]
  641. outputs = [network.get_output(i) for i in range(network.num_outputs)]
  642. for inp in inputs:
  643. LOGGER.info(f'{prefix} input "{inp.name}" with shape{inp.shape} {inp.dtype}')
  644. for out in outputs:
  645. LOGGER.info(f'{prefix} output "{out.name}" with shape{out.shape} {out.dtype}')
  646. if self.args.dynamic:
  647. shape = self.im.shape
  648. if shape[0] <= 1:
  649. LOGGER.warning(f"{prefix} WARNING ⚠️ 'dynamic=True' model requires max batch size, i.e. 'batch=16'")
  650. profile = builder.create_optimization_profile()
  651. min_shape = (1, shape[1], 32, 32) # minimum input shape
  652. max_shape = (*shape[:2], *(max(1, self.args.workspace) * d for d in shape[2:])) # max input shape
  653. for inp in inputs:
  654. profile.set_shape(inp.name, min=min_shape, opt=shape, max=max_shape)
  655. config.add_optimization_profile(profile)
  656. LOGGER.info(f"{prefix} building {'INT8' if int8 else 'FP' + ('16' if half else '32')} engine as {f}")
  657. if int8:
  658. config.set_flag(trt.BuilderFlag.INT8)
  659. config.set_calibration_profile(profile)
  660. config.profiling_verbosity = trt.ProfilingVerbosity.DETAILED
  661. class EngineCalibrator(trt.IInt8Calibrator):
  662. def __init__(
  663. self,
  664. dataset, # ultralytics.data.build.InfiniteDataLoader
  665. batch: int,
  666. cache: str = "",
  667. ) -> None:
  668. trt.IInt8Calibrator.__init__(self)
  669. self.dataset = dataset
  670. self.data_iter = iter(dataset)
  671. self.algo = trt.CalibrationAlgoType.ENTROPY_CALIBRATION_2
  672. self.batch = batch
  673. self.cache = Path(cache)
  674. def get_algorithm(self) -> trt.CalibrationAlgoType:
  675. """Get the calibration algorithm to use."""
  676. return self.algo
  677. def get_batch_size(self) -> int:
  678. """Get the batch size to use for calibration."""
  679. return self.batch or 1
  680. def get_batch(self, names) -> list:
  681. """Get the next batch to use for calibration, as a list of device memory pointers."""
  682. try:
  683. im0s = next(self.data_iter)["img"] / 255.0
  684. im0s = im0s.to("cuda") if im0s.device.type == "cpu" else im0s
  685. return [int(im0s.data_ptr())]
  686. except StopIteration:
  687. # Return [] or None, signal to TensorRT there is no calibration data remaining
  688. return None
  689. def read_calibration_cache(self) -> bytes:
  690. """Use existing cache instead of calibrating again, otherwise, implicitly return None."""
  691. if self.cache.exists() and self.cache.suffix == ".cache":
  692. return self.cache.read_bytes()
  693. def write_calibration_cache(self, cache) -> None:
  694. """Write calibration cache to disk."""
  695. _ = self.cache.write_bytes(cache)
  696. # Load dataset w/ builder (for batching) and calibrate
  697. config.int8_calibrator = EngineCalibrator(
  698. dataset=self.get_int8_calibration_dataloader(prefix),
  699. batch=2 * self.args.batch,
  700. cache=str(self.file.with_suffix(".cache")),
  701. )
  702. elif half:
  703. config.set_flag(trt.BuilderFlag.FP16)
  704. # Free CUDA memory
  705. del self.model
  706. gc.collect()
  707. torch.cuda.empty_cache()
  708. # Write file
  709. build = builder.build_serialized_network if is_trt10 else builder.build_engine
  710. with build(network, config) as engine, open(f, "wb") as t:
  711. # Metadata
  712. meta = json.dumps(self.metadata)
  713. t.write(len(meta).to_bytes(4, byteorder="little", signed=True))
  714. t.write(meta.encode())
  715. # Model
  716. t.write(engine if is_trt10 else engine.serialize())
  717. return f, None
  718. @try_export
  719. def export_saved_model(self, prefix=colorstr("TensorFlow SavedModel:")):
  720. """YOLOv8 TensorFlow SavedModel export."""
  721. cuda = torch.cuda.is_available()
  722. try:
  723. import tensorflow as tf # noqa
  724. except ImportError:
  725. suffix = "-macos" if MACOS else "-aarch64" if ARM64 else "" if cuda else "-cpu"
  726. version = ">=2.0.0"
  727. check_requirements(f"tensorflow{suffix}{version}")
  728. import tensorflow as tf # noqa
  729. check_requirements(
  730. (
  731. "keras", # required by 'onnx2tf' package
  732. "tf_keras", # required by 'onnx2tf' package
  733. "sng4onnx>=1.0.1", # required by 'onnx2tf' package
  734. "onnx_graphsurgeon>=0.3.26", # required by 'onnx2tf' package
  735. "onnx>=1.12.0",
  736. "onnx2tf>1.17.5,<=1.22.3",
  737. "onnxslim>=0.1.31",
  738. "tflite_support<=0.4.3" if IS_JETSON else "tflite_support", # fix ImportError 'GLIBCXX_3.4.29'
  739. "flatbuffers>=23.5.26,<100", # update old 'flatbuffers' included inside tensorflow package
  740. "onnxruntime-gpu" if cuda else "onnxruntime",
  741. ),
  742. cmds="--extra-index-url https://pypi.ngc.nvidia.com", # onnx_graphsurgeon only on NVIDIA
  743. )
  744. LOGGER.info(f"\n{prefix} starting export with tensorflow {tf.__version__}...")
  745. check_version(
  746. tf.__version__,
  747. ">=2.0.0",
  748. name="tensorflow",
  749. verbose=True,
  750. msg="https://github.com/ultralytics/ultralytics/issues/5161",
  751. )
  752. import onnx2tf
  753. f = Path(str(self.file).replace(self.file.suffix, "_saved_model"))
  754. if f.is_dir():
  755. shutil.rmtree(f) # delete output folder
  756. # Pre-download calibration file to fix https://github.com/PINTO0309/onnx2tf/issues/545
  757. onnx2tf_file = Path("calibration_image_sample_data_20x128x128x3_float32.npy")
  758. if not onnx2tf_file.exists():
  759. attempt_download_asset(f"{onnx2tf_file}.zip", unzip=True, delete=True)
  760. # Export to ONNX
  761. self.args.simplify = True
  762. f_onnx, _ = self.export_onnx()
  763. # Export to TF
  764. np_data = None
  765. if self.args.int8:
  766. tmp_file = f / "tmp_tflite_int8_calibration_images.npy" # int8 calibration images file
  767. verbosity = "info"
  768. if self.args.data:
  769. f.mkdir()
  770. images = [batch["img"].permute(0, 2, 3, 1) for batch in self.get_int8_calibration_dataloader(prefix)]
  771. images = torch.cat(images, 0).float()
  772. # mean = images.view(-1, 3).mean(0) # imagenet mean [123.675, 116.28, 103.53]
  773. # std = images.view(-1, 3).std(0) # imagenet std [58.395, 57.12, 57.375]
  774. np.save(str(tmp_file), images.numpy().astype(np.float32)) # BHWC
  775. np_data = [["images", tmp_file, [[[[0, 0, 0]]]], [[[[255, 255, 255]]]]]]
  776. else:
  777. verbosity = "error"
  778. LOGGER.info(f"{prefix} starting TFLite export with onnx2tf {onnx2tf.__version__}...")
  779. onnx2tf.convert(
  780. input_onnx_file_path=f_onnx,
  781. output_folder_path=str(f),
  782. not_use_onnxsim=True,
  783. verbosity=verbosity,
  784. output_integer_quantized_tflite=self.args.int8,
  785. quant_type="per-tensor", # "per-tensor" (faster) or "per-channel" (slower but more accurate)
  786. custom_input_op_name_np_data_path=np_data,
  787. )
  788. yaml_save(f / "metadata.yaml", self.metadata) # add metadata.yaml
  789. # Remove/rename TFLite models
  790. if self.args.int8:
  791. tmp_file.unlink(missing_ok=True)
  792. for file in f.rglob("*_dynamic_range_quant.tflite"):
  793. file.rename(file.with_name(file.stem.replace("_dynamic_range_quant", "_int8") + file.suffix))
  794. for file in f.rglob("*_integer_quant_with_int16_act.tflite"):
  795. file.unlink() # delete extra fp16 activation TFLite files
  796. # Add TFLite metadata
  797. for file in f.rglob("*.tflite"):
  798. f.unlink() if "quant_with_int16_act.tflite" in str(f) else self._add_tflite_metadata(file)
  799. return str(f), tf.saved_model.load(f, tags=None, options=None) # load saved_model as Keras model
  800. @try_export
  801. def export_pb(self, keras_model, prefix=colorstr("TensorFlow GraphDef:")):
  802. """YOLOv8 TensorFlow GraphDef *.pb export https://github.com/leimao/Frozen_Graph_TensorFlow."""
  803. import tensorflow as tf # noqa
  804. from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 # noqa
  805. LOGGER.info(f"\n{prefix} starting export with tensorflow {tf.__version__}...")
  806. f = self.file.with_suffix(".pb")
  807. m = tf.function(lambda x: keras_model(x)) # full model
  808. m = m.get_concrete_function(tf.TensorSpec(keras_model.inputs[0].shape, keras_model.inputs[0].dtype))
  809. frozen_func = convert_variables_to_constants_v2(m)
  810. frozen_func.graph.as_graph_def()
  811. tf.io.write_graph(graph_or_graph_def=frozen_func.graph, logdir=str(f.parent), name=f.name, as_text=False)
  812. return f, None
  813. @try_export
  814. def export_tflite(self, keras_model, nms, agnostic_nms, prefix=colorstr("TensorFlow Lite:")):
  815. """YOLOv8 TensorFlow Lite export."""
  816. # BUG https://github.com/ultralytics/ultralytics/issues/13436
  817. import tensorflow as tf # noqa
  818. LOGGER.info(f"\n{prefix} starting export with tensorflow {tf.__version__}...")
  819. saved_model = Path(str(self.file).replace(self.file.suffix, "_saved_model"))
  820. if self.args.int8:
  821. f = saved_model / f"{self.file.stem}_int8.tflite" # fp32 in/out
  822. elif self.args.half:
  823. f = saved_model / f"{self.file.stem}_float16.tflite" # fp32 in/out
  824. else:
  825. f = saved_model / f"{self.file.stem}_float32.tflite"
  826. return str(f), None
  827. @try_export
  828. def export_edgetpu(self, tflite_model="", prefix=colorstr("Edge TPU:")):
  829. """YOLOv8 Edge TPU export https://coral.ai/docs/edgetpu/models-intro/."""
  830. LOGGER.warning(f"{prefix} WARNING ⚠️ Edge TPU known bug https://github.com/ultralytics/ultralytics/issues/1185")
  831. cmd = "edgetpu_compiler --version"
  832. help_url = "https://coral.ai/docs/edgetpu/compiler/"
  833. assert LINUX, f"export only supported on Linux. See {help_url}"
  834. if subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True).returncode != 0:
  835. LOGGER.info(f"\n{prefix} export requires Edge TPU compiler. Attempting install from {help_url}")
  836. sudo = subprocess.run("sudo --version >/dev/null", shell=True).returncode == 0 # sudo installed on system
  837. for c in (
  838. "curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -",
  839. 'echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | '
  840. "sudo tee /etc/apt/sources.list.d/coral-edgetpu.list",
  841. "sudo apt-get update",
  842. "sudo apt-get install edgetpu-compiler",
  843. ):
  844. subprocess.run(c if sudo else c.replace("sudo ", ""), shell=True, check=True)
  845. ver = subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1]
  846. LOGGER.info(f"\n{prefix} starting export with Edge TPU compiler {ver}...")
  847. f = str(tflite_model).replace(".tflite", "_edgetpu.tflite") # Edge TPU model
  848. cmd = f'edgetpu_compiler -s -d -k 10 --out_dir "{Path(f).parent}" "{tflite_model}"'
  849. LOGGER.info(f"{prefix} running '{cmd}'")
  850. subprocess.run(cmd, shell=True)
  851. self._add_tflite_metadata(f)
  852. return f, None
  853. @try_export
  854. def export_tfjs(self, prefix=colorstr("TensorFlow.js:")):
  855. """YOLOv8 TensorFlow.js export."""
  856. check_requirements("tensorflowjs")
  857. if ARM64:
  858. # Fix error: `np.object` was a deprecated alias for the builtin `object` when exporting to TF.js on ARM64
  859. check_requirements("numpy==1.23.5")
  860. import tensorflow as tf
  861. import tensorflowjs as tfjs # noqa
  862. LOGGER.info(f"\n{prefix} starting export with tensorflowjs {tfjs.__version__}...")
  863. f = str(self.file).replace(self.file.suffix, "_web_model") # js dir
  864. f_pb = str(self.file.with_suffix(".pb")) # *.pb path
  865. gd = tf.Graph().as_graph_def() # TF GraphDef
  866. with open(f_pb, "rb") as file:
  867. gd.ParseFromString(file.read())
  868. outputs = ",".join(gd_outputs(gd))
  869. LOGGER.info(f"\n{prefix} output node names: {outputs}")
  870. quantization = "--quantize_float16" if self.args.half else "--quantize_uint8" if self.args.int8 else ""
  871. with spaces_in_path(f_pb) as fpb_, spaces_in_path(f) as f_: # exporter can not handle spaces in path
  872. cmd = (
  873. "tensorflowjs_converter "
  874. f'--input_format=tf_frozen_model {quantization} --output_node_names={outputs} "{fpb_}" "{f_}"'
  875. )
  876. LOGGER.info(f"{prefix} running '{cmd}'")
  877. subprocess.run(cmd, shell=True)
  878. if " " in f:
  879. LOGGER.warning(f"{prefix} WARNING ⚠️ your model may not work correctly with spaces in path '{f}'.")
  880. # f_json = Path(f) / 'model.json' # *.json path
  881. # with open(f_json, 'w') as j: # sort JSON Identity_* in ascending order
  882. # subst = re.sub(
  883. # r'{"outputs": {"Identity.?.?": {"name": "Identity.?.?"}, '
  884. # r'"Identity.?.?": {"name": "Identity.?.?"}, '
  885. # r'"Identity.?.?": {"name": "Identity.?.?"}, '
  886. # r'"Identity.?.?": {"name": "Identity.?.?"}}}',
  887. # r'{"outputs": {"Identity": {"name": "Identity"}, '
  888. # r'"Identity_1": {"name": "Identity_1"}, '
  889. # r'"Identity_2": {"name": "Identity_2"}, '
  890. # r'"Identity_3": {"name": "Identity_3"}}}',
  891. # f_json.read_text(),
  892. # )
  893. # j.write(subst)
  894. yaml_save(Path(f) / "metadata.yaml", self.metadata) # add metadata.yaml
  895. return f, None
  896. def _add_tflite_metadata(self, file):
  897. """Add metadata to *.tflite models per https://www.tensorflow.org/lite/models/convert/metadata."""
  898. import flatbuffers
  899. if ARM64:
  900. from tflite_support import metadata # noqa
  901. from tflite_support import metadata_schema_py_generated as schema # noqa
  902. else:
  903. # TFLite Support bug https://github.com/tensorflow/tflite-support/issues/954#issuecomment-2108570845
  904. from tensorflow_lite_support.metadata import metadata_schema_py_generated as schema # noqa
  905. from tensorflow_lite_support.metadata.python import metadata # noqa
  906. # Create model info
  907. model_meta = schema.ModelMetadataT()
  908. model_meta.name = self.metadata["description"]
  909. model_meta.version = self.metadata["version"]
  910. model_meta.author = self.metadata["author"]
  911. model_meta.license = self.metadata["license"]
  912. # Label file
  913. tmp_file = Path(file).parent / "temp_meta.txt"
  914. with open(tmp_file, "w") as f:
  915. f.write(str(self.metadata))
  916. label_file = schema.AssociatedFileT()
  917. label_file.name = tmp_file.name
  918. label_file.type = schema.AssociatedFileType.TENSOR_AXIS_LABELS
  919. # Create input info
  920. input_meta = schema.TensorMetadataT()
  921. input_meta.name = "image"
  922. input_meta.description = "Input image to be detected."
  923. input_meta.content = schema.ContentT()
  924. input_meta.content.contentProperties = schema.ImagePropertiesT()
  925. input_meta.content.contentProperties.colorSpace = schema.ColorSpaceType.RGB
  926. input_meta.content.contentPropertiesType = schema.ContentProperties.ImageProperties
  927. # Create output info
  928. output1 = schema.TensorMetadataT()
  929. output1.name = "output"
  930. output1.description = "Coordinates of detected objects, class labels, and confidence score"
  931. output1.associatedFiles = [label_file]
  932. if self.model.task == "segment":
  933. output2 = schema.TensorMetadataT()
  934. output2.name = "output"
  935. output2.description = "Mask protos"
  936. output2.associatedFiles = [label_file]
  937. # Create subgraph info
  938. subgraph = schema.SubGraphMetadataT()
  939. subgraph.inputTensorMetadata = [input_meta]
  940. subgraph.outputTensorMetadata = [output1, output2] if self.model.task == "segment" else [output1]
  941. model_meta.subgraphMetadata = [subgraph]
  942. b = flatbuffers.Builder(0)
  943. b.Finish(model_meta.Pack(b), metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER)
  944. metadata_buf = b.Output()
  945. populator = metadata.MetadataPopulator.with_model_file(str(file))
  946. populator.load_metadata_buffer(metadata_buf)
  947. populator.load_associated_files([str(tmp_file)])
  948. populator.populate()
  949. tmp_file.unlink()
  950. def _pipeline_coreml(self, model, weights_dir=None, prefix=colorstr("CoreML Pipeline:")):
  951. """YOLOv8 CoreML pipeline."""
  952. import coremltools as ct # noqa
  953. LOGGER.info(f"{prefix} starting pipeline with coremltools {ct.__version__}...")
  954. _, _, h, w = list(self.im.shape) # BCHW
  955. # Output shapes
  956. spec = model.get_spec()
  957. out0, out1 = iter(spec.description.output)
  958. if MACOS:
  959. from PIL import Image
  960. img = Image.new("RGB", (w, h)) # w=192, h=320
  961. out = model.predict({"image": img})
  962. out0_shape = out[out0.name].shape # (3780, 80)
  963. out1_shape = out[out1.name].shape # (3780, 4)
  964. else: # linux and windows can not run model.predict(), get sizes from PyTorch model output y
  965. out0_shape = self.output_shape[2], self.output_shape[1] - 4 # (3780, 80)
  966. out1_shape = self.output_shape[2], 4 # (3780, 4)
  967. # Checks
  968. names = self.metadata["names"]
  969. nx, ny = spec.description.input[0].type.imageType.width, spec.description.input[0].type.imageType.height
  970. _, nc = out0_shape # number of anchors, number of classes
  971. # _, nc = out0.type.multiArrayType.shape
  972. assert len(names) == nc, f"{len(names)} names found for nc={nc}" # check
  973. # Define output shapes (missing)
  974. out0.type.multiArrayType.shape[:] = out0_shape # (3780, 80)
  975. out1.type.multiArrayType.shape[:] = out1_shape # (3780, 4)
  976. # spec.neuralNetwork.preprocessing[0].featureName = '0'
  977. # Flexible input shapes
  978. # from coremltools.models.neural_network import flexible_shape_utils
  979. # s = [] # shapes
  980. # s.append(flexible_shape_utils.NeuralNetworkImageSize(320, 192))
  981. # s.append(flexible_shape_utils.NeuralNetworkImageSize(640, 384)) # (height, width)
  982. # flexible_shape_utils.add_enumerated_image_sizes(spec, feature_name='image', sizes=s)
  983. # r = flexible_shape_utils.NeuralNetworkImageSizeRange() # shape ranges
  984. # r.add_height_range((192, 640))
  985. # r.add_width_range((192, 640))
  986. # flexible_shape_utils.update_image_size_range(spec, feature_name='image', size_range=r)
  987. # Print
  988. # print(spec.description)
  989. # Model from spec
  990. model = ct.models.MLModel(spec, weights_dir=weights_dir)
  991. # 3. Create NMS protobuf
  992. nms_spec = ct.proto.Model_pb2.Model()
  993. nms_spec.specificationVersion = 5
  994. for i in range(2):
  995. decoder_output = model._spec.description.output[i].SerializeToString()
  996. nms_spec.description.input.add()
  997. nms_spec.description.input[i].ParseFromString(decoder_output)
  998. nms_spec.description.output.add()
  999. nms_spec.description.output[i].ParseFromString(decoder_output)
  1000. nms_spec.description.output[0].name = "confidence"
  1001. nms_spec.description.output[1].name = "coordinates"
  1002. output_sizes = [nc, 4]
  1003. for i in range(2):
  1004. ma_type = nms_spec.description.output[i].type.multiArrayType
  1005. ma_type.shapeRange.sizeRanges.add()
  1006. ma_type.shapeRange.sizeRanges[0].lowerBound = 0
  1007. ma_type.shapeRange.sizeRanges[0].upperBound = -1
  1008. ma_type.shapeRange.sizeRanges.add()
  1009. ma_type.shapeRange.sizeRanges[1].lowerBound = output_sizes[i]
  1010. ma_type.shapeRange.sizeRanges[1].upperBound = output_sizes[i]
  1011. del ma_type.shape[:]
  1012. nms = nms_spec.nonMaximumSuppression
  1013. nms.confidenceInputFeatureName = out0.name # 1x507x80
  1014. nms.coordinatesInputFeatureName = out1.name # 1x507x4
  1015. nms.confidenceOutputFeatureName = "confidence"
  1016. nms.coordinatesOutputFeatureName = "coordinates"
  1017. nms.iouThresholdInputFeatureName = "iouThreshold"
  1018. nms.confidenceThresholdInputFeatureName = "confidenceThreshold"
  1019. nms.iouThreshold = 0.45
  1020. nms.confidenceThreshold = 0.25
  1021. nms.pickTop.perClass = True
  1022. nms.stringClassLabels.vector.extend(names.values())
  1023. nms_model = ct.models.MLModel(nms_spec)
  1024. # 4. Pipeline models together
  1025. pipeline = ct.models.pipeline.Pipeline(
  1026. input_features=[
  1027. ("image", ct.models.datatypes.Array(3, ny, nx)),
  1028. ("iouThreshold", ct.models.datatypes.Double()),
  1029. ("confidenceThreshold", ct.models.datatypes.Double()),
  1030. ],
  1031. output_features=["confidence", "coordinates"],
  1032. )
  1033. pipeline.add_model(model)
  1034. pipeline.add_model(nms_model)
  1035. # Correct datatypes
  1036. pipeline.spec.description.input[0].ParseFromString(model._spec.description.input[0].SerializeToString())
  1037. pipeline.spec.description.output[0].ParseFromString(nms_model._spec.description.output[0].SerializeToString())
  1038. pipeline.spec.description.output[1].ParseFromString(nms_model._spec.description.output[1].SerializeToString())
  1039. # Update metadata
  1040. pipeline.spec.specificationVersion = 5
  1041. pipeline.spec.description.metadata.userDefined.update(
  1042. {"IoU threshold": str(nms.iouThreshold), "Confidence threshold": str(nms.confidenceThreshold)}
  1043. )
  1044. # Save the model
  1045. model = ct.models.MLModel(pipeline.spec, weights_dir=weights_dir)
  1046. model.input_description["image"] = "Input image"
  1047. model.input_description["iouThreshold"] = f"(optional) IoU threshold override (default: {nms.iouThreshold})"
  1048. model.input_description["confidenceThreshold"] = (
  1049. f"(optional) Confidence threshold override (default: {nms.confidenceThreshold})"
  1050. )
  1051. model.output_description["confidence"] = 'Boxes × Class confidence (see user-defined metadata "classes")'
  1052. model.output_description["coordinates"] = "Boxes × [x, y, width, height] (relative to image size)"
  1053. LOGGER.info(f"{prefix} pipeline success")
  1054. return model
  1055. def add_callback(self, event: str, callback):
  1056. """Appends the given callback."""
  1057. self.callbacks[event].append(callback)
  1058. def run_callbacks(self, event: str):
  1059. """Execute all callbacks for a given event."""
  1060. for callback in self.callbacks.get(event, []):
  1061. callback(self)
  1062. class IOSDetectModel(torch.nn.Module):
  1063. """Wrap an Ultralytics YOLO model for Apple iOS CoreML export."""
  1064. def __init__(self, model, im):
  1065. """Initialize the IOSDetectModel class with a YOLO model and example image."""
  1066. super().__init__()
  1067. _, _, h, w = im.shape # batch, channel, height, width
  1068. self.model = model
  1069. self.nc = len(model.names) # number of classes
  1070. if w == h:
  1071. self.normalize = 1.0 / w # scalar
  1072. else:
  1073. self.normalize = torch.tensor([1.0 / w, 1.0 / h, 1.0 / w, 1.0 / h]) # broadcast (slower, smaller)
  1074. def forward(self, x):
  1075. """Normalize predictions of object detection model with input size-dependent factors."""
  1076. xywh, cls = self.model(x)[0].transpose(0, 1).split((4, self.nc), 1)
  1077. return cls, xywh * self.normalize # confidence (3780, 80), coordinates (3780, 4)