Exportar para ONNX (e verificar a paridade)
Exporta um modelo ONNX portátil e confirma que corresponde ao PyTorch em caixas, confianças e mAP antes de disponibilizares qualquer coisa.
Uma exportação que altera as confianças em 0.1 não causa uma falha. Altera silenciosamente as deteções que ultrapassam o teu limiar conf em todos os dispositivos para os quais fazes a implantação, e ninguém repara até as contagens estarem erradas. O ONNX é o ponto de partida portátil para a implantação, e cinco minutos a verificar a paridade são o seguro mais barato deste curso.
Prático
Exportação#
from ultralytics import YOLO
model = YOLO("yolo26n.pt") # or runs/detect/pills_v1/weights/best.pt
model.export(format="onnx", dynamic=True) # grava yolo26n.onnx ao lado dos pesosO registo mostra o opset escolhido (opset 18 com onnx 1.23) e que o onnxslim simplificou o grafo. As opções que alteram o comportamento, no modo Export:
| Opção | Padrão | Quando alterar |
|---|---|---|
imgsz | 640 | A produção usa outro tamanho; uma exportação estática fica fixa nesse tamanho |
dynamic | False | O tamanho do lote ou a forma da imagem varia durante a inferência |
batch | 1 | Fazes batching durante a inferência (o lote máximo para uma exportação dinâmica) |
opset | mais recente suportado | Um runtime mais antigo rejeita o modelo; reduz este valor |
quantize | None (FP32) | 16 para FP16; 8 para INT8 com calibração data |
nms | None | True incorpora NMS no grafo; False seleciona o cabeçalho sem NMS do YOLO26 |
Com o valor predefinido nms=None, a exportação devolve previsões em bruto e o wrapper da Ultralytics executa NMS, tal como no PyTorch.
Verifica a paridade numa imagem conhecida#
Executa a mesma imagem nos dois modelos e compara tudo:
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")Ordenar ambos os conjuntos por confiança emparelha os mesmos objetos, por isso as duas últimas linhas comparam cada deteção com a sua correspondente. Agora executa-o duas vezes, uma como mostrado e outra sem dynamic=True:
| Exportação | Caixas | Mesmas classes | Diferença máxima de confiança | Diferença máxima das caixas |
|---|---|---|---|---|
dynamic=True | 5 vs 5 | sim | 0.0000 | 0.00 px |
| estático (predefinido) | 5 vs 5 | sim | 0.0977 | 10.11 px |
A exportação estática não está avariada. O PyTorch ajusta a imagem vertical bus.jpg à menor forma alinhada pela passada, 640×480 (altura × largura), enquanto um grafo estático de 640×640 tem de a preencher até formar um quadrado, com barras cinzentas. O preenchimento diferente produz previsões ligeiramente diferentes. Para comparar em condições equivalentes, exporta com dynamic=True ou executa o lado do PyTorch com rect=False para que preencha até ao quadrado completo, tal como a exportação estática (consulta forma fixa vs retângulo mínimo); com rect=False, a exportação estática também corresponde, com uma diferença de 0.0000.
Valida ambos os ficheiros#
Uma imagem deteta erros grosseiros; a validação deteta os mais subtis. Usa o mesmo batch para ambos, para que o pré-processamento corresponda:
yolo val model=yolo26n.pt data=coco8.yaml batch=1
yolo val model=yolo26n.onnx data=coco8.yaml batch=1Ambos indicam mAP50-95 0.654 neste caso. O COCO8 valida o pipeline, não o modelo; no teu próprio conjunto de validação, investiga qualquer diferença superior a 0.5 pontos. A verificação completa:
Chama diretamente o ONNX Runtime#
Os serviços de produção muitas vezes dispensam o wrapper da Ultralytics e chamam diretamente o motor de inferência. Nesse caso, o pré-processamento fica a teu cargo e tem de corresponder exatamente ao da Ultralytics. Com nms=False, a exportação do YOLO26 devolve as deteções finais, por isso não é necessário código NMS:
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]Um autocarro (classe 5) e quatro pessoas (classe 0), correspondendo ao YOLO(path)(img) até ao píxel. O autocarro tem uma pontuação de 0.93 neste caso e de 0.88 com a exportação predefinida, porque nms=False seleciona um cabeçalho diferente; compara ficheiros exportados com o mesmo valor de nms.
Armadilhas#
Sintoma: mesmas caixas, mas confianças com diferença de até 0.1, principalmente em imagens não quadradas. Solução: verifique a paridade com dynamic=True, ou com rect=False no lado do PyTorch, ou avalie a exportação estática pela mAP de validação.
Sintoma: seu próprio código ONNX Runtime encontra menos caixas ou caixas deslocadas em comparação com YOLO("model.onnx"). Solução: use letterbox (não estique), preencha com 114, converta BGR para RGB, divida por 255 e desfaça o preenchimento e o redimensionamento das caixas, exatamente como faz ort_raw.py.
Sintoma: todas as confianças são ligeiramente diferentes, mesmo com formatos correspondentes. Solução: compare uma exportação nms=False (sem NMS) somente com outra exportação nms=False. Com nms=True, confira também se conf e iou incorporados ao grafo correspondem aos que você usa em outros lugares.
Mostrar solução
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 pointsUm relatório de paridade para yolo26n em bus.jpg indica: diferença máxima de confiança na exportação estática de 0,0977 (formato letterbox, removido por rect=False), exportação dinâmica de 0,0000, diferença de mAP50-95 de 0,00 pontos. Usa a exportação dinâmica.