File size: 10,598 Bytes
3bdfea4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | ---
license: agpl-3.0
library_name: onnx
pipeline_tag: object-detection
tags:
- waste
- recycling
- smart-bin-recognition
- yolo11
- onnx
- int8
base_model:
- Ultralytics/YOLO11
datasets:
- arudaev/smart-bin-detect
---
# Smart Bin Recognition β validator and identifier
Two ONNX graphs behind a web app that answers *"what is this bin, and what goes
in it?"* from a phone camera. They run **server-side**; nothing here is
downloaded to a device.
**One of them may ship. The other may not, and the service refuses to start on
it.** Both are published because an artefact that failed its gate is evidence,
not waste, and hiding it would make the next person repeat the measurement.
| | **validator** | **identifier** |
|---|---|---|
| question | *is there a bin?* | *which bin?* |
| file | `v1/validator-v1.onnx` | `v1/identifier-v1.onnx` |
| architecture | YOLO11n detection | YOLO11s-cls |
| input | 448 Γ 448, NCHW, static batch 1 | 320 Γ 320, NCHW, **dynamic batch** |
| weights | int8 (U8S8, per-channel) | int8 (U8S8, per-channel) |
| size | 3.15 MB | 5.81 MB |
| **`may_ship`** | **`false`** | **`true`** |
**Every number below carries the split it was measured on and the hardware it
was measured on.** A figure without both is not quotable, here or in the project
that produced it.
---
## The class list, and its order
**Read the sidecar, not this table.** `classes` in each `*.json` is the
authoritative order and the service reads it from there. The order **is** the
ONNX output index; reordering it silently invalidates every deployed model, and
the project pins it with a test.
**validator** β 1 class:
| index | id |
|---:|---|
| 0 | `bin` |
**identifier** β 3 classes, **alphabetical**, which is the order the training
framework emitted and therefore the order the head produces. It is *not* the
order the taxonomy lists them in:
| index | id | what it is |
|---:|---|---|
| 0 | `igloo` | a glass/bottle bank |
| 1 | `wheelie_large` | a 660β1100 L four-wheeled container |
| 2 | `wheelie_small` | a 120β240 L two-wheeled household bin |
These are **form factors β shapes, not meanings**. The model never predicts a
waste stream. Meaning comes from a per-jurisdiction region pack (a JSON file)
that maps *(form factor, colour, region)* to a stream, so adding a country is a
data change rather than a retrain.
---
## The identifier β passes every gate
| gate | budget | measured | split / hardware |
|---|---:|---:|---|
| int8 accuracy cost | β€ 0.02 top-1 | **0.0000** | `test`, Kaggle T4 training run |
| median latency | β€ 25 ms / crop | **9.921 ms** | GCE `n2-standard-4`, 2 of 4 vCPU pinned, Intel Cascade Lake, `europe-west3-a`, onnxruntime 1.29.0, `representative: true` |
| p95 latency | β | 11.409 ms | same |
| hardware named | required | yes | same |
`gate_result: {"failures": [], "unmeasured": [], "may_ship": true}`.
### Its evidence is thin, and the card says so
**`test` top-1 is 1.0000 on 47 crops.**
| class | crops in `test` | capture clusters in `test` |
|---|---:|---:|
| `wheelie_small` | 25 | 9 |
| `wheelie_large` | 19 | 8 |
| **`igloo`** | **3** | **2** |
- 47 correct out of 47 is **not** certainty. By the rule of three the 95 % lower
bound is **0.936**.
- **`igloo`'s contribution is three crops from two scenes.** Its per-class number
is noisy and must never be quoted clean.
- **The better estimate is 0.9834** β out-of-fold accuracy over all 403 crops
under `GroupKFold` on capture cluster, against a 0.6823 majority-class
baseline. A random split of these crops measures memorisation: 403 crops come
from 100 capture clusters, and the largest holds 18 photographs of one bin.
**The task is close to saturated**, so this measurement can distinguish *"int8
and fp32 differ by at most one crop"* from *"they differ by two or more"* and not
much finer. The gate passed honestly; it did not pass by a margin the data can
resolve.
---
## The validator β trained, real, and it cannot ship
It is not a failed training run. It is a working detector that **int8
quantisation destroys**.
| gate | budget | measured | split / hardware |
|---|---:|---:|---|
| **int8 accuracy cost** | β€ 0.02 mAP@0.5 | **0.727** β | `test` |
| median latency | β€ 50 ms | 18.252 ms β
| GCE `n2-standard-4`, Cascade Lake, 2 vCPU pinned, `representative: true` |
| p95 latency | β | 21.345 ms | same |
| | value | split |
|---|---:|---|
| mAP@0.5, PyTorch fp32 reference | **0.7524** | `test` |
| mAP@0.5, the int8 graph published here | **0.025** | `test` |
| specificity on background frames | 0.9793 | 2 662 hard negatives |
`gate_result: {"failures": ["int8 quantisation cost 0.727 map50 (max 0.02)"], "may_ship": false}`.
**The service loads the validator unconditionally and refuses to start on an
artefact whose sidecar says `may_ship: false`. So nothing is deployed, and that
is correct rather than pending.**
### What is known about why
- **Quantising the detection head is what collapses it.** Leaving `/model.23/`
in fp32 takes the graph from 0.015 to **0.7481** on `val` β a fifty-fold
recovery β for about +5.7 ms and +1.2 MB on an x86 proxy.
- **It does not follow that nothing else matters.** That graph is still quantised
everywhere else and still loses **0.0252** against a 0.02 budget. It misses by
**0.0052**, and the residual is unattributed.
- The three remedies onnxruntime names for this failure mode β S8S8,
`reduce_range`, U8U8 β all stay at collapse, as does per-tensor. This is *not*
the x86 saturation case it resembles.
- **Post-training int8 over the whole graph is not viable for this
architecture.** Any future YOLO11 detection export here starts from
`exclude_head=True`.
### fp32 is the open route, and it was measured
Measured 2026-08-22, both formats on **one** Cascade Lake instance with the arms
alternated:
| | int8 | fp32 |
|---|---:|---:|
| validator p50 | 17.921 ms | **24.605 ms** |
| against a 50 ms budget | passes | **passes** |
| concurrent scanners @ 1 bin | 5 | **4** |
| accuracy cost | β0.727 mAP | 0.0 |
**An fp32 validator meets the latency budget with 25 ms to spare and costs one
concurrent scanner.** The ship gate currently refuses unquantised artefacts
outright, and its stated reason β *"it will not meet the latency budget"* β is
false on this architecture at this input size. Splitting that gate into
per-format profiles is staged for the maintainer's decision and is **not**
merged. **No fp32 artefact is published in this repository.**
---
## Serving contract
Both graphs, from the sidecars:
- **Normalisation:** scale `1/255`, mean `[0,0,0]`, std `[1,1,1]`. No ImageNet
statistics.
- **Input name:** `images`. Layout `NCHW`.
- **NMS is not in the graph.** Apply it yourself: `iou 0.45`, `score 0.35`.
- **The identifier accepts a batch > 1 and the validator does not.** A frame can
hold several bins β a bank of six is a normal input, not an edge case β so the
crops go through one call. The validator sees exactly one frame, every time.
Read `imgsz`, `classes`, `normalisation` and `nms` **from the sidecar** rather
than from this card. The service does, which is why a model swap needs no code
change.
---
## Coverage gap β six of ten form factors have no data at all
The taxonomy defines ten form factors. The identifier was trained on **three**.
| form factor | training crops | status |
|---|---:|---|
| `wheelie_small` | 247 | trained |
| `wheelie_large` | 115 | trained |
| `igloo` | 40 | trained |
| `street_basket` | **1** | **dropped** β one crop in one capture cluster cannot be split across train/val/test, so it can be neither trained nor evaluated |
| `underground`, `textile_bank`, `sack`, `crate`, `wall_unit`, `container_bank` | **0** | **no data** |
**Those six keep their ids.** An id with no training data is a coverage gap, not
a deletion. Everything the identifier has never seen resolves to `unknown`, which
is a designed state with a real interface, and the honest answer.
**Open Images cannot close this gap.** A frozen survey of 384 boxes found it to
be a street-litter corpus β 35 % `street_basket`, and **zero** `underground`,
`textile_bank` or `wall_unit`.
## There is no geographic holdout
Every training frame comes from **one city in one week** (Deggendorf, Bavaria),
plus Open Images frames whose `region_id` is `unknown` because the source does
not record where a photograph was taken. **No split in this data answers "does it
work in another city."** The targets `min_recall_heldout_city` and
`min_formfactor_acc_heldout_city` are reported as **unmeasurable**, not as
passed.
Do not quote the predecessor project's 95.2 % mAP as a baseline for these models.
It was measured on a random split of one week's photographs in one city.
---
## What this must not be used for
- **Do not use the validator as published.** It scores 0.025 mAP@0.5. It is here
as the evidence behind a `may_ship: false`, not as a detector.
- **Do not derive disposal advice from these models alone.** They output a shape.
Being confidently wrong about what goes in which bin is this product's worst
failure mode, and the mapping from shape to rule lives in a per-region pack
that is versioned, sourced and separately reviewed.
- **Do not deploy this outside Deggendorf and expect the numbers above.** They
are in-distribution numbers on one city, and the card says so twice because it
is the most likely way to be wrong with them.
- **Do not treat a colour attribute as measured.** As of 2026-08-22 colour
agreement against hand-labelled ground truth is **0.5625** on bin bodies, and
lid colour is **not measured at all** β an upper-band sampler scored 0.1966 and
was deliberately left unwired. Any rule matching on `lid_color` is unreachable.
- **Do not fine-tune on user-contributed images without human label review.**
High-agreement machine labels may auto-accept over a public corpus; never over
a frame a user contributed, and never for a form factor that has no data yet.
## Provenance
Trained on [`arudaev/smart-bin-detect`](https://huggingface.co/datasets/arudaev/smart-bin-detect)
at revision `8666aa23` β 18 954 frames: 370 legacy, 1 110 Open Images bins,
17 474 background. The identifier additionally uses a private crop pool whose
403 crops were **all** adjudicated by a human, blind, in a single pass; against
that pass the pool's shipped streamβshape proposals were wrong on **116 of
403 (28.8 %)**.
Splits are **group-aware on capture cluster**, so two photographs of the same
bin can never straddle a split.
|