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.