rfdetr-s-litter
The release record for the detector currently in the request path, rendered. Every field is read off the artifact, its sidecar, the exporter, or the deploy — not off a description of any of them.
| Version | v2.0.1-fp32 |
| Version grammar | v<MAJOR>.<MINOR>.<PATCH>-<precision>, precision one of fp32 · fp16 · int8. Rejected at export if it does not match. |
| Architecture | RF-DETR-Small (rfdetr 1.9.0), fine-tuned |
| Served key | litter-detector/v2.0.1-fp32/rfdetr-s-litter.onnx |
| Consumers | two Lambdas — inference-on-upload and cleanup-diff — both pinned to that same key |
| Sidecar key | derived, not configured: the loader rewrites the .onnx suffix to .meta.json |
| Artifact size | 120,039,167 bytes (120.0 MB) |
| Exported | 2026-08-02T15:57:45Z |
| Export target | rfdetr-s-512-fp32 in export.precision.TARGETS — fp32 compute, fp32 IO, opset 17, 512×512, no size ceiling. It is the source row of the target matrix, not a conversion of anything. |
| Sibling targets | three more descriptors over these same bytes: rfdetr-s-512-fp16 (what the exporter's default path writes — ORT refuses to load it), rfdetr-s-512-fp16-repaired, and rfdetr-s-512-int8-dynamic. Same source, same three calls, one dict entry apart. |
| Serving target | onnxruntime-node ^1.27.0 · cpu · aws-lambda-arm64-nodejs20 |
| Registry record | 31486ca5ebe0 — v2.0.1-fp32, role: reference, measuredOnTarget: false |
Precision is encoded in the version string and in the S3 prefix, and nowhere else.
The serving contract has no precision field — see the pipeline
page for why that is a gap rather than a detail. The two target rows above are also
the two senses of the word in this codebase: what was built, and where it runs.
The evidence on this record was gathered under Python onnxruntime
1.29.0, not under the runtime named on the serving row.
| Exporter | export.export_rfdetr → RFDETRSmall.export(format="onnx", opset_version=17, shape=(512, 512), batch_size=1) |
| fp32 path | the graph as the torch exporter writes it. No post-pass. This is what serves. |
| fp16 mechanism | a post-pass over the finished graph: onnxconverter_common.float16.convert_float_to_float16(model, keep_io_types=True) |
| int8 | refused by the exporter — "INT8 export requires a calibration pass; not wired for V2." |
| Compression at rest | .onnx.gz written alongside, gzip level 9, mtime pinned to 0 so identical bytes produce identical archives |
The two exporters do not mean the same thing by "fp16"
This repository has two exporters that both accept a -fp16 version string,
and they produce artifacts with different IO boundaries by different mechanisms:
| Exporter | Mechanism | Graph IO dtype |
|---|---|---|
export_yolov8 | precision suffix mapped to half=True and handed to the torch exporter — the cast happens during export | float16 in, float16 out |
export_rfdetr | convert_float_to_float16 applied after export, with keep_io_types=True | float32 in, float32 out |
Both write fp16 into the version string. A consumer reading only the version
string cannot tell whether it must feed float16 or float32, and the loader does not read
the graph to find out. That is a real defect in the naming scheme, not a stylistic
difference between two scripts — it is stated here because the card is where it would
have to be caught.
| ONNX opset | 17 (default domain only) · IR version 8 |
| Input | input · float32 · [1, 3, 512, 512] NCHW · batch fixed at 1 |
| Preprocess | squash resize to 512×512 (no letterbox), /255, then per-channel ImageNet mean/std. Not baked into the graph — the consumer must apply it. |
Output dets | float32 · [1, 300, 4] · one row per query |
Output labels | float32 · [1, 300, 44] · raw logits, no sigmoid in the graph |
| Coordinates | cx, cy, w, h, normalized to [0, 1] of the model input. Because the resize is a squash and not a letterbox, those are also normalized to the original image — decode converts to x_min, y_min, x_max, y_max and clamps to [0, 1]. |
| Decode | per-query argmax over the named columns → sigmoid of the winner → threshold → top-k 100. No NMS: DETR's set-based training already yields one query per object. |
The 44th column
classCount is 43 and the logits tensor has 44 columns. RF-DETR builds its
class head with one extra background column, so columns 0–42 are the canonical classes
and column 43 is background. Decode iterates 0 <= c < 43 and ignores the
trailing column; it raises if the channel count is neither classCount nor
classCount + 1. That check catches an arity mismatch. It cannot catch a
permutation, which is why the ordering is enforced upstream instead.
Column i of the logits tensor must be canonical class i. Nothing at serving time can verify that — a permuted head produces confident, well-formed, silently mislabelled output — so it is enforced where the artifact is produced.
| Canonical order | the 43 leaf classes from the training data.yaml, alphabetical: alcohol.bottle … softdrinks.straw |
| Enforced at | export time. _check_class_alignment compares the checkpoint's training_config.json class_names against that list and raises on any difference, including a reorder: "Logit column i would not be canonical class i." |
| Published in | the sidecar's classes array, in that same order — which is what the backend uses to name a class index |
| Escape hatches | --skip-class-check disables the comparison; and if training_config.json is absent next to the checkpoint the exporter logs a warning and proceeds on trust. Both are honest holes in an otherwise hard gate. |
Fetched alongside the graph on cold start and parsed into the backend's
ModelMeta. These are the fields the artifact actually carries:
| Field | Value | Consumed by |
|---|---|---|
version | v2.0.1-fp32 | session cache key — a changed version invalidates the warm session |
modelName | rfdetr-s-litter | artifact filename stem |
architecture | rfdetr | routes decode to the DETR path. Absent means yolo — every sidecar written before the field existed is a YOLO export. |
inputShape | [1, 3, 512, 512] | preprocess target size |
classes | 43 strings, canonical order | index → label at decode |
classCount | 43 | arity check against the logits tensor |
normalization | mean .485/.456/.406 std .229/.224/.225 | preprocess. Absent means ImageNet defaults. |
confThresholdRecommended | 0.4 | the operating point — see § 06 |
iouThresholdRecommended | 0.45 | nothing. The DETR path runs no NMS. It is carried only so the sidecar satisfies the ModelMeta schema shared with the YOLO exporter. |
trainingMetrics | map50 0.2153 map5095 0.1823 hardNegativeFpr 0.0244 hazardsMeanRecall 0.0 | nothing at run time — provenance only |
artifactSizeBytes | 120,039,167 | nothing at run time — provenance only |
trainedAt / exportedAt | 2026-08-02 | nothing at run time — provenance only |
There is no precision field. There is no field for a parity result, a
fixture set, or a runtime the artifact was measured on. The right-hand column is
deliberately blunt about which fields do nothing: four of twelve are provenance a
human reads, one is inert, and that is worth seeing on the record rather than
discovering during an incident.
| Threshold | 0.4 (sigmoid of the winning logit, per query) |
| Top-k | 100 detections after threshold |
| Upstream default | RF-DETR's own predict() uses 0.5 |
0.4 is a deliberate move below the upstream default, and the reason is a property of the product rather than of the model: every suggestion this detector produces is surfaced to a contributor who confirms or rejects it before it becomes data. A missed detection costs more than an extra suggestion — the extra suggestion is one tap to dismiss, the miss is a piece of litter that never enters the record. So the threshold is biased toward recall.
DETR confidences are not comparable to YOLO's. There is no NMS score-shaping here, so a number that means "loose" on the YOLO path does not mean "loose" on this one. 0.4 is not transferable to a model with a different decode.
The numbers below are the ones published on /benchmarks, measured on a shared internal validation split (826 images / 1,004 boxes). They are reproduced rather than restated so the release record and the benchmark page cannot drift apart:
| Metric | v1 · YOLO11n | v2 · RF-DETR-S (this artifact) |
|---|---|---|
| mAP@0.5, internal val (43 classes) | 0.074 | 0.215 (~3×) |
| Hard-negative false-positive rate | 0.049 | 0.024 (halved) |
| Hazard classes (needles, batteries) recall | 0.0 | 0.0 — unsolved, stated plainly |
| Training cost | — | 8.06 h · one RTX 4070 · 1.613 kWh |
Internal validation only. The 3× bundles an architecture change and a data change together, so it is a relative signal and not a headline accuracy claim.
Status: this artifact is the reference. fp32 is what serves, and no converted artifact has been promoted past it. Two conversions have been built and measured against it; both were held. The measurement and the reasoning are in the build log: What a conversion gate is for.
All rows: 200 real photographs, ONNX Runtime 1.29.0, CPU execution provider, decode mirroring the backend exactly, at the sidecar's 0.4.
| Artifact | Size | p50 vs fp32 | Detections @0.4 | Disagreements @0.4 |
|---|---|---|---|---|
| fp32 (serving) | 120.0 MB | reference | 202 | — |
| fp16 | 63.1 MB | 1.89× slower | 203 | 19 threshold · 6 class · 10 of 200 images |
| int8 dynamic | 36.0 MB | 1.85× faster | 202 — exactly equal | 112 threshold (56 each way) · 19 class · 50 of 200 images |
The int8 row is the one to read twice. It is 30% of the size, 1.85× faster, and returns exactly the same number of detections as the reference — and a quarter of the corpus comes back different. The counts match because 56 losses and 56 gains happen to be the same size, not because the artifacts agree. A release check that compared detection counts would have called that perfect parity.
What those flips cost, measured against human labels
Parity says the decisions changed. It does not say the model got worse, and that gap has since been closed from outside the harness: the same 200 fixtures labelled blind from the pixels — four reviewers, no model output, no boxes, per-image presence of a coarse 10-category rollup. Presence-level ground truth, not mAP, and not comparable to § 07.
| Image-level litter recall @ 0.4 | fp32 | int8 dynamic |
|---|---|---|
| All 200 images — 156 contain litter, 44 are clean | 52.6% (82/156) | 53.2% (83/156) |
| Excluding non-scene images | 52.9% | 53.5% |
| Excluding non-scene and crew-bag images | 46.9% (38/81) | 50.6% (41/81) |
| High-confidence labels only | 59.2% (61/103) | 57.3% (59/103) |
| False positives on genuinely clean scenes | 9.1% (4/44) | 9.1% — the same four images |
One to three images out of 81 to 156, pointing both ways depending on subset: on a set this size none of it is distinguishable from noise. So the 112 flips are instability, not degradation — an argument for putting a flip count in front of a person rather than wiring it to an automatic reject, and not an argument that int8 is better. The 202-against-202 result stands unchanged: a count-based check would still have reported perfect parity on a model whose decisions moved.
Reading those fixtures by hand also found that roughly six of the 200 are not photographs of scenes at all — video-game screenshots, a cartoon flyer, a marketing graphic, a phone screenshot of a web search. No parity harness can surface that: both artifacts agree happily on a screenshot. Status is unchanged by any of this. fp32 is still the reference, and nothing has been promoted.
- Hazard classes are at zero recall. Needles and batteries are in the 43-class space and the model does not find them. Nothing on this card should be read as hazard coverage.
- Labels are weaker than boxes. An earlier blind grading of this detector's lineage found ~95% of boxes on real litter and ~36% carrying the right label, with the material composition inverted relative to ground truth. See what a litter detector actually sees.
- Counting is not usable. Presence is. Quantity is not.
- Presence recall is about half. Blind human labels over the 200
conversion fixtures put image-level litter recall at 52.6% — this artifact returns
nothing at all on roughly half the images a person says contain litter. Per
category it recovers
bottlesat 49%,bags_film_wrappersat 30% andpaperat 2%.glass_brokenappeared in none of the 200 images andsmokingin one, so both are unmeasured, not good. Presence only — no boxes were drawn, so this is not a localization number and not comparable to the mAP above. - No Pi 5 number is published for this model. The edge benchmarks on /benchmarks are a stock COCO YOLOv8n proving the board can sustain inference; nothing on that page describes this artifact.
- The class-order gate can be bypassed by
--skip-class-check, and it degrades to a warning when the checkpoint ships withouttraining_config.json.
What these numbers do not license
- The eval numbers are internal validation only. They do not license a comparison against any published litter benchmark, and they are a relative signal between v1 and v2, not an accuracy claim.
- The parity numbers describe these two artifacts, this runtime, this execution provider, this fixture set. They say nothing about a GPU, an accelerator, the arm64 Lambda host that actually serves, or a different input distribution.
- Flips are still not accuracy — and the accuracy has now been measured. A query moving from 0.41 to 0.39 counts as a disagreement and might be the better answer. Blind presence-level labels on the fixture set say the 112 flips cost nothing measurable: 52.6% against 53.2% image-level recall, with an identical false-positive rate on clean scenes. That licenses "no measurable difference", not "int8 is better" — every gap is inside the noise of this fixture set.
- The ground truth behind those recall numbers is one reviewer per image — 200 fixtures split into four independently-labelled shards — so there is no inter-rater agreement figure, and where a reviewer was unsure of a category that image was excluded from scoring rather than resolved either way.
- The fixture set is 200 cleanup photographs from three subreddits. It describes Reddit, not the uploads this model sees in service.
- Nothing here separates "int8 arithmetic is lossy for this architecture" from "this converter quantizes this graph badly". Those have different fixes.
- Latency was measured on desktop CPU. The fp32 reference's own p50 wandered between 284 and 330 ms across runs — about 16% — so only within-run ratios are quoted.
This page is the registry record for one artifact. The edges it travels — what each stage is allowed to change, and who owns the contract when it does — are drawn once on how a model ships here.