Nach ONNX exportieren (und Übereinstimmung überprüfen)
Exportiere ein portables ONNX-Modell und weise anschließend nach, dass es bei BBoxen, Konfidenzen und mAP mit PyTorch übereinstimmt, bevor du etwas bereitstellst.
Ein Export, bei dem sich Konfidenzen um 0.1 verschieben, stürzt nicht ab. Er verschiebt unbemerkt Erkennungen auf jedem Gerät, auf dem du ihn bereitstellst, über oder unter deinen conf-Schwellenwert. Das fällt erst auf, wenn die Zählwerte nicht stimmen. ONNX ist der portable Ausgangspunkt für die Bereitstellung. Fünf Minuten zur Überprüfung der Übereinstimmung sind die günstigste Absicherung in diesem Kurs.
Praxisnah
Export#
from ultralytics import YOLO
model = YOLO("yolo26n.pt") # oder runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True) # schreibt yolo26n.onnx neben die GewichteDas Protokoll zeigt die gewählte Opset-Version (opset 18 mit onnx 1.23) und dass onnxslim den Graphen vereinfacht hat. Diese Optionen aus dem Exportmodus ändern das Verhalten:
| Option | Standard | Ändern, wenn |
|---|---|---|
imgsz | 640 | Die Bereitstellung erfolgt mit einer anderen Größe; ein statischer Export ist auf die angegebene Größe festgelegt |
dynamic | False | Batchgröße oder Bildform variiert bei der Inferenz |
batch | 1 | Du verwendest Batches bei der Inferenz (maximale Batchgröße für einen dynamischen Export) |
opset | höchste unterstützte Version | Eine ältere Laufzeitumgebung lehnt das Modell ab; verwende eine niedrigere Version |
quantize | None (FP32) | 16 für FP16; 8 für INT8 mit Kalibrierungsdaten data |
nms | None | True bindet NMS in den Graphen ein; False wählt den NMS-freien Erkennungskopf von YOLO26 |
Mit dem Standardwert nms=None gibt der Export Rohvorhersagen zurück und der Ultralytics-Wrapper führt NMS aus – genau wie bei PyTorch.
Übereinstimmung anhand eines bekannten Bildes überprüfen#
Führe dasselbe Bild durch beide Modelle und vergleiche alles:
from ultralytics import YOLO
pt = YOLO("yolo26n.pt")
onnx = YOLO(pt.export(format="onnx", dynamic=True))
img = "https://ultralytics.com/images/bus.jpg"
a = pt(img, verbose=False)[0].boxes
b = onnx(img, verbose=False)[0].boxes
ia, ib = a.conf.argsort(descending=True), b.conf.argsort(descending=True)
print(f"boxes: PyTorch {len(a)} ONNX {len(b)}")
print(f"same classes: {a.cls[ia].tolist() == b.cls[ib].tolist()}")
print(f"max conf diff: {(a.conf[ia] - b.conf[ib]).abs().max():.4f}")
print(f"max xyxy diff: {(a.xyxy[ia] - b.xyxy[ib]).abs().max():.2f} px")Durch das Sortieren beider Ergebnismengen nach Konfidenz lassen sich dieselben Objekte einander zuordnen. Die letzten beiden Zeilen vergleichen daher jede Erkennung mit ihrem Gegenstück. Führe den Vergleich nun zweimal aus: einmal wie gezeigt und einmal ohne dynamic=True:
| Export | BBoxen | Gleiche Klassen | Max. Konfidenzabweichung | Max. BBox-Abweichung |
|---|---|---|---|---|
dynamic=True | 5 gegenüber 5 | ja | 0.0000 | 0.00 px |
| statisch (Standard) | 5 gegenüber 5 | ja | 0.0977 | 10.11 px |
Der statische Export ist nicht fehlerhaft. PyTorch passt das Hochformatbild bus.jpg an die kleinste stride-ausgerichtete Form an: 640×480 (Höhe × Breite). Ein statischer Graph mit 640×640 muss es dagegen mit grauen Balken auf ein Quadrat auffüllen. Durch die unterschiedliche Auffüllung weichen die Vorhersagen leicht ab. Für einen direkten Vergleich exportierst du entweder mit dynamic=True oder führst die PyTorch-Seite mit rect=False aus, damit sie wie beim statischen Export auf das vollständige Quadrat auffüllt (siehe feste Form gegenüber kleinstem Rechteck). Mit rect=False stimmt auch der statische Export bis auf 0.0000 überein.
Beide Dateien validieren#
Ein Bild deckt grobe Fehler auf; die Validierung findet subtile Abweichungen. Verwende für beide dasselbe batch, damit die Vorverarbeitung übereinstimmt:
yolo val model=yolo26n.pt data=coco8.yaml batch=1
yolo val model=yolo26n.onnx data=coco8.yaml batch=1Beide melden hier mAP50-95 0.654. COCO8 überprüft die Pipeline, nicht das Modell. Untersuche auf deinem eigenen Validierungsdatensatz jede Abweichung von mehr als 0.5 Punkten. Die gesamte Prüfung:
ONNX Runtime direkt aufrufen#
Produktionsdienste umgehen oft den Ultralytics-Wrapper und rufen die Inferenz-Engine direkt auf. Dann bist du für die Vorverarbeitung zuständig und musst sie exakt an Ultralytics anpassen. Mit nms=False gibt der YOLO26-Export die endgültigen Erkennungen zurück, sodass kein NMS-Code erforderlich ist:
import cv2
import numpy as np
import onnxruntime as ort
from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download
path = YOLO("yolo26n.pt").export(format="onnx", nms=False) # NMS-free head: output (1, 300, 6)
session = ort.InferenceSession(path, providers=["CPUExecutionProvider"]) # put CUDAExecutionProvider first on GPU
name = session.get_inputs()[0].name
# Preprocess like Ultralytics: letterbox to 640x640 with gray (114) padding, BGR->RGB, 0-1, NCHW
im = cv2.imread(safe_download("https://ultralytics.com/images/bus.jpg"))
h, w = im.shape[:2]
r = 640 / max(h, w)
nh, nw = round(h * r), round(w * r)
top, left = (640 - nh) // 2, (640 - nw) // 2
canvas = np.full((640, 640, 3), 114, np.uint8)
canvas[top : top + nh, left : left + nw] = cv2.resize(im, (nw, nh), interpolation=cv2.INTER_LINEAR)
x = canvas[..., ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255
(out,) = session.run(None, {name: x})
det = out[0][out[0][:, 4] > 0.25] # rows: x1, y1, x2, y2, score, class
det[:, [0, 2]] = ((det[:, [0, 2]] - left) / r).clip(0, w) # undo padding and scale
det[:, [1, 3]] = ((det[:, [1, 3]] - top) / r).clip(0, h)
for x1, y1, x2, y2, score, cls in det:
print(f"class {int(cls):2d} score {score:.2f} box [{x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f}]")class 5 score 0.93 box [6, 229, 807, 749]
class 0 score 0.92 box [47, 399, 237, 902]
class 0 score 0.90 box [223, 405, 345, 863]
class 0 score 0.86 box [669, 391, 810, 880]
class 0 score 0.52 box [0, 554, 65, 874]Ein Bus (Klasse 5) und vier Personen (Klasse 0); die BBoxen stimmen pixelgenau mit YOLO(path)(img) überein. Der Bus erreicht hier 0.93, beim Standardexport dagegen 0.88, weil nms=False einen anderen Erkennungskopf auswählt. Vergleiche Dateien, die mit demselben Wert für nms exportiert wurden.
Stolperfallen#
Symptom: Dieselben Boxen, aber Konfidenzwerte weichen um bis zu 0,1 ab, am stärksten bei nicht quadratischen Bildern. Lösung: Prüfe die Übereinstimmung mit dynamic=True oder auf der PyTorch-Seite mit rect=False, oder bewerte den statischen Export anhand der Validierungs-mAP.
Symptom: Dein eigener ONNX-Runtime-Code findet weniger oder verschobene Boxen als YOLO("model.onnx"). Lösung: Verwende Letterboxing (nicht strecken), fülle mit 114 auf, wandle BGR in RGB um, teile durch 255 und mache Auffüllung und Skalierung der Boxen rückgängig – genau wie ort_raw.py.
Symptom: Jeder Konfidenzwert ist leicht unterschiedlich, selbst bei übereinstimmenden Formen. Lösung: Vergleiche einen nms=False-Export (ohne NMS) nur mit einem anderen nms=False-Export. Bei nms=True prüfe außerdem, ob die in den Graphen eingebetteten Werte conf und iou mit den anderweitig verwendeten Werten übereinstimmen.
Lösung anzeigen
from ultralytics import YOLO
if __name__ == "__main__": # validation starts worker processes on macOS and Windows
weights = "yolo26n.pt" # or runs/detect/pills_v1/weights/best.pt
onnx_path = YOLO(weights).export(format="onnx", dynamic=True)
maps = {}
for w in (weights, onnx_path):
maps[w] = YOLO(w).val(data="coco8.yaml", batch=1, plots=False).box.map # or my_dataset/data.yaml
print(f"{w}: mAP50-95 = {maps[w]:.4f}")
print(f"gap = {abs(maps[weights] - maps[onnx_path]) * 100:.2f} points")yolo26n.pt: mAP50-95 = 0.6540
yolo26n.onnx: mAP50-95 = 0.6540
gap = 0.00 pointsEin Paritätsbericht für yolo26n auf bus.jpg lautet: maximale Konfidenzabweichung beim statischen Export 0.0977 (Letterbox-Form, durch rect=False entfernt), beim dynamischen Export 0.0000, mAP50-95-Abweichung 0.00 Punkte. Stelle den dynamischen Export bereit.