AnnotateIt/edgecrafter-ecdet-m-onnx
<!-- annotateit-brand:start --> <p><a href="https://huggingface.co/AnnotateIt"><img src="https://huggingface.co/spaces/AnnotateIt/README/resolve/d399a89b9d5dcf41afbe7cf29bf99c9028bc51e2/assets/annotateit-logo.png" width="48" height="48" alt="AnnotateIt"></a></p>
[AnnotateIt](https://annotateit.ai/) · [Open the app](https://app.annotateit.ai/) · [Models & datasets](https://huggingface.co/AnnotateIt) · [Documentation](https://annotateit.ai/docs/) <!-- annotateit-brand:end -->
Unofficial ONNX conversion of EdgeCrafter ECDet-M
Single-file FP32 ONNX (opset 17) conversion of the official EdgeCrafter ECDet-M object-detection checkpoint, exporting the raw model outputs (no NMS, no sigmoid, no top-k, no resizing — all pre/postprocessing stays outside the graph).
This is an unofficial conversion. It is a modification of the original EdgeCrafter release (PyTorch → ONNX, plus embedded metadata and validation artifacts) and is not endorsed by the EdgeCrafter authors.
Provenance
The checkpoint was loaded on CPU with strict=True (0 missing / 0 unexpected keys) following the official export flow, switched to deploy/eval mode, and traced with the TorchScript ONNX exporter. See export/export_raw_onnx.py for the exact, reproducible procedure (repeated runs produce a byte-identical file).
Input contract
Preprocessing (must be done outside the model):
- Decode to RGB.
- Resize (stretch) directly to 640×640 — no letterbox, no aspect preservation.
float32, divide by 255.- Normalize:
x = (x - mean) / stdwithmean = [0.485, 0.456, 0.406],std = [0.229, 0.224, 0.225](per channel, RGB order). - HWC → CHW, add batch dimension.
This mirrors the official evaluation pipeline (configs/ecdet/ecdet.yml val transforms: Resize [640,640] → ConvertPILImage(float32, scale=True) → Normalize(mean, std)).
Output contract
300 object queries, 80 contiguous COCO2017 classes. There is no orig_target_sizes input and no labels/scores postprocessing — apply your own.
Required external postprocessing:
probs = 1 / (1 + np.exp(-pred_logits)) # sigmoid
scores = probs.max(-1); labels = probs.argmax(-1)
keep = scores >= threshold # e.g. 0.4 (or use top-k)
cx, cy, w, h = boxes[keep].T # normalized cxcywh
xyxy = np.stack([cx-w/2, cy-h/2, cx+w/2, cy+h/2], -1).clip(0, 1)
xyxy *= [orig_w, orig_h, orig_w, orig_h] # scale to the ORIGINAL image sizeDETR-style set prediction — NMS is not required.
Class mapping
Contiguous COCO2017 ids 0..79 (0=person, 1=bicycle, 2=car, …, 79=toothbrush), taken from the official EdgeCrafter dataset code (engine/data/dataset/coco_dataset.py, mscoco_category2name). The full mapping is embedded in the ONNX metadata key names and duplicated in config.json. Identical for ECDet-S and ECDet-M.
ONNX Runtime example
import json, numpy as np, onnxruntime as ort
from PIL import Image
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
names = json.loads(sess.get_modelmeta().custom_metadata_map["names"])
img = Image.open("photo.jpg").convert("RGB")
ow, oh = img.size
x = np.asarray(img.resize((640, 640), Image.BILINEAR), np.float32) / 255.0
x = (x - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
x = x.transpose(2, 0, 1)[None].astype(np.float32)
logits, boxes = sess.run(None, {"images": x})
probs = 1 / (1 + np.exp(-logits[0]))
scores, labels = probs.max(-1), probs.argmax(-1)
for i in np.where(scores >= 0.4)[0]:
cx, cy, w, h = boxes[0, i]
box = [(cx-w/2)*ow, (cy-h/2)*oh, (cx+w/2)*ow, (cy+h/2)*oh]
print(names[str(labels[i])], round(float(scores[i]), 3), [round(v, 1) for v in box])Validation summary
Full machine-readable results: validation-report.json (status: passed). Highlights:
onnx.checker(full) + strict shape inference pass; single file, no external data, no custom operator domains; opset 17.- ONNX Runtime CPU: batch 1 and batch 2 verified; batched rows are bit-identical to single-image runs (dynamic batch works).
- PyTorch vs ONNX Runtime parity (
rtol=1e-3,atol=1e-4,np.testing.assert_allclose): 6 of 7 cases pass strictly. On one real image two queries with encoder scores tying within ~1e-6 appear in swapped order between PyTorch and ONNX Runtime; after row matching, all 300 query rows satisfy the strict tolerance (documented in detail in the report — this is float-tie reordering inside the model's internal TopK, not an export defect). - Semantic parity on 12 real COCO val2017 images with identical external postprocessing: detections match 1:1 (12/12 images).
- Re-running the export produces a byte-identical model.onnx.
Limitations
- Some low-confidence raw queries may produce box coordinates outside
[0,1]. Apply confidence filtering and clip coordinates before scaling to the original image. - Fixed 640×640 input; only the batch dimension is dynamic.
- FP32 only (no FP16/INT8 variants in this release).
pred_logits/pred_boxesare raw — you must apply sigmoid, thresholding and coordinate scaling yourself (see above).- The internal encoder TopK orders the 300 queries by score; queries whose scores tie within float precision may appear in a different order across runtimes. Treat the output as an unordered set.
- COCO AP was not re-measured for the ONNX model; the AP figure above is the upstream-reported PyTorch number.
- The trace fixes the ViT spatial resolution; use exactly 3×640×640 inputs.
License and attribution
Released under the Apache License 2.0 (see LICENSE), the same license as the upstream project. Original work: © 2026 The EdgeCrafter Authors (Intellindust AI Lab), with components derived from D-FINE and RT-DETR — see NOTICE. The ONNX conversion is a modification of the original model; this repository is not endorsed by the EdgeCrafter developers.
