AnnotateIt · Open the app · Models & datasets · Documentation

Unofficial ONNX conversion of EdgeCrafter ECDet-S

Single-file FP32 ONNX (opset 17) conversion of the official EdgeCrafter ECDet-S 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

Upstream project EdgeCrafter — Compact ViTs for Edge Dense Prediction via Task-Specialized Distillation
Source code commit b17f0f340af687e7adf2dff42a49e2eb8250ee20
Model config ecdetseg/configs/ecdet/ecdet_s.yml
Checkpoint ecdet_s.pth (39,938,420 bytes)
Checkpoint SHA-256 35340a45101d24a094becf8776aeaa327c9e6b37453e5e81dfd2923c134c4619
model.onnx SHA-256 e0919a2c617682d049617f97c67ea986d7c9d908220647eb3e26a55c4c24bf87 (40,492,596 bytes)
Upstream-reported metric COCO val2017 AP 51.7 (as published in the EdgeCrafter README; not re-measured for this conversion)

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

name images
dtype / layout float32, NCHW
shape [batch, 3, 640, 640] — batch is dynamic, height/width fixed

Preprocessing (must be done outside the model):

  1. Decode to RGB.
  2. Resize (stretch) directly to 640×640 — no letterbox, no aspect preservation.
  3. float32, divide by 255.
  4. Normalize: x = (x - mean) / std with mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225] (per channel, RGB order).
  5. 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

output shape meaning
pred_logits [batch, 300, 80] raw class logits (sigmoid/focal scheme; sigmoid is not applied inside the graph)
pred_boxes [batch, 300, 4] normalized cxcywh boxes, relative to the 640×640 input

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 size

DETR-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): all 7 cases pass strictly (random batch 1 and 2, four real images, real-image batch 2).
  • Semantic parity on 12 real COCO val2017 images with identical external postprocessing: detections match 1:1 (12/12 images, max box diff < 0.1 px).
  • 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_boxes are 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.