O modelo manda
Duas coisas sobre um .onnx estão dentro do arquivo: a resolução que ele
aceita na entrada e os nomes das classes que ele emite. Repetir esses números na
configuração é o que gera a dor de cabeça clássica — a configuração envelhece, o
arquivo é re-exportado, e os dois divergem sem ninguém perceber.
O SDK lê ambos do modelo. Esta página mostra como, e o que sobra para você configurar. 🚀
O problema: 640 contra 224
Um export de detecção do Ultralytics sai em 640×640. Um export de
classificação (-cls) sai em 224×224 — é o default da ferramenta. Se o seu
código assume 640 para os dois, o classificador morre no meio da inferência:
Got invalid dimensions for input: images for the following indices
index: 2 Got: 640 Expected: 224
Esse erro não tem como ser previsto de fora
O número vive no grafo. Nenhuma configuração, manifest ou constante ao lado
do arquivo é fonte da verdade sobre ele — só o próprio .onnx é.
A solução: perguntar ao grafo
Você não configura nada:
from ort_vision_sdk import Classifier
clf = Classifier("classify.onnx")
print(clf.input_size)
#> (224, 224)
O SDK leu o shape declarado pelo grafo ([1, 3, 224, 224]) e vai
pré-processar nessa resolução. Um detector no mesmo programa resolve o dele:
from ort_vision_sdk import Detector
det = Detector("detect.onnx")
print(det.input_size)
#> (640, 640)
Leia de volta o que rodou
task.input_size é a resolução que a inferência realmente usou — útil
para logs e telemetria, onde reportar o valor configurado esconde
exatamente o bug que você está caçando.
E se eu passar input_size?
O grafo ganha, e o SDK avisa:
clf = Classifier("classify.onnx", input_size=(640, 640))
#> UserWarning: The model declares a 224x224 input; ignoring the requested
#> 640x640, which ONNX Runtime would reject.
print(clf.input_size)
#> (224, 224)
Isso é de propósito. Um shape estático não é uma preferência: é a única coisa que o ONNX Runtime aceita. Obedecer você ali só trocaria um problema corrigível por uma execução que falha.
Quando input_size ainda importa
Um modelo exportado com eixos dinâmicos (dynamic=True no Ultralytics) declara
altura e largura como símbolos, e aí ele aceita várias resoluções. Nesse caso o
grafo não tem o que dizer, e o seu valor vale:
clf = Classifier("dynamic.onnx", input_size=(384, 384))
print(clf.input_size)
#> (384, 384)
Precedência, em uma linha
grafo estático → o que você passou → default da tarefa (224 para classificação, 640 para detecção/segmentação).
Rótulos que vêm do próprio modelo
O Ultralytics grava names nos metadados do .onnx — o mapa dict[int, str]
de id de classe para nome. Uma lista mantida à mão do lado pode ser reordenada
por acidente, e o efeito é o pior possível: nada falha, as predições
simplesmente trocam de classe.
Sem labels, o SDK usa o que o modelo declara:
from ort_vision_sdk import Detector
det = Detector("detect.onnx")
print(det.labels)
#> ('ocular-mucosa',)
Isso também conserta um tropeço antigo
Antes, um detector custom falhava sem labels explícito: o default era
o preset COCO de 80 nomes, que discordava da contagem de classes do modelo.
Agora ele resolve o próprio nome.
A precedência segue a mesma ideia:
# 1) O que você passa sempre ganha
det = Detector("detect.onnx", labels=["mucosa"])
# 2) Sem labels: os `names` do modelo
det = Detector("detect.onnx")
# 3) Modelo sem `names`: "class_0", "class_1", ...
# (o preset COCO só entra quando o modelo tem mesmo 80 classes)
Parsing seguro
O valor é lido com ast.literal_eval, então um metadado malformado — ou
hostil — é rejeitado, nunca executado. Um mapa que não seja um dict
de inteiros contíguos começando em zero é descartado inteiro, em vez de
aplicado pela metade.
Lendo os metadados você mesmo
O mapa completo está disponível na sessão:
from ort_vision_sdk import Classifier
clf = Classifier("classify.onnx")
print(clf.session.metadata["task"])
#> classify
print(clf.session.input_shape)
#> (1, 3, 224, 224)
E os helpers puros por trás de tudo isso são públicos, para quem monta o próprio pipeline:
from ort_vision_sdk import model_names, resolve_input_size, spatial_input_size
spatial_input_size((1, 3, 224, 224))
#> (224, 224)
spatial_input_size((1, 3, "h", "w"))
#> None
resolve_input_size(graph_shape=(1, 3, 224, 224), requested=None, fallback=(640, 640))
#> (224, 224)
model_names({"names": "{0: 'deworm', 1: 'not_deworm'}"})
#> {0: 'deworm', 1: 'not_deworm'}
Detalhes técnicos: backends que não leem metadados
Um backend que só repassa tensores para um runtime nativo (bridge Android,
onnxruntime-web via Pyodide) não consegue ler o mapa de metadados. Por
isso a capacidade é um protocolo separado, MetadataBackend, e não um
membro obrigatório de InferenceBackend: as tarefas consultam com
read_metadata() e simplesmente não recebem nada quando o backend não
oferece. Backends escritos antes disso continuam válidos.
Recapitulando
- A resolução de entrada vem do grafo;
input_sizeé fallback para modelos de eixo dinâmico. - Passar um tamanho que contradiz um grafo estático emite
UserWarninge é ignorado — o ORT rejeitaria mesmo. task.input_sizediz o que a inferência usou de verdade.- Sem
labels, os nomes vêm dos metadados do modelo (namesdo Ultralytics), caindo paraclass_<id>quando o modelo não os carrega — ou para o preset COCO quando o modelo realmente tem 80 classes. session.metadata,session.input_shapee os helpersspatial_input_size/resolve_input_size/model_namesestão públicos.
Veja também
- Guia Python — entradas aceitas, providers, inferência assíncrona.
- Web (browser) — o mesmo comportamento no SDK TypeScript.
- Backends de inferência — o protocolo
InferenceBackend.