@trillboards/edge-sdk 0.2.89 → 0.2.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ """A coordinate space is MEASURED or REFUSED. It is never inferred from silence.
2
+
3
+ `session.py` takes the layout and the box units from the artifact's DECLARED
4
+ variant contract and never re-derives them from the tensor — that is deliberate,
5
+ because `[1, 300, 6]` and `[1, 6, 8400]` are both rank-3 with a 6 in them and a
6
+ reader that decides from shape alone gets one of them backwards with no
7
+ exception and a plausible table. The whole weight of that design sits on the
8
+ declaration being true.
9
+
10
+ `classify_box_units` is where the declaration is made, from one forward pass.
11
+ Its refusal band exists because the wrong pick is silent: LiteRT boxes in 0..1
12
+ and ONNX boxes in pixels agree on output shape, dtype, class count and every
13
+ score, and differ only by a factor of imgsz on four channels.
14
+
15
+ The hole these tests close: the guard was `box_abs_max is None`, so a
16
+ `box_abs_max` of exactly `0.0` fell through to `ratio = 0.0`, landed under
17
+ BOX_UNITS_NORMALIZED_RATIO_MAX, and returned a CONFIDENT `normalized` with
18
+ `scale_to_pixels=640` from zero observations. That is not hypothetical: a model
19
+ whose classification head is dead still emits box rows — `forklift_akdve_11n`
20
+ has healthy boxes (2.93-636 px) and a max class score of 0.0063 — so nothing is
21
+ admitted at threshold and `box_abs_max` never moves off its seed. The dead
22
+ artifact then published a coordinate-space claim no frame supports.
23
+
24
+ STDLIB ONLY, and that is why `box_units.py` is a separate module: the
25
+ `detector-service` CI lane runs `python3 -m unittest discover` with no pip
26
+ install, and `run_candidate.py` imports onnxruntime at module scope, so a test
27
+ importing it would error the whole lane during discovery rather than skip.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import pathlib
33
+ import sys
34
+ import unittest
35
+
36
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "bench"))
37
+
38
+ from box_units import ( # noqa: E402
39
+ BOX_UNITS_NORMALIZED_RATIO_MAX,
40
+ BOX_UNITS_PIXEL_RATIO_MIN,
41
+ classify_box_units,
42
+ )
43
+
44
+ IMGSZ = 640
45
+
46
+
47
+ class ZeroEvidenceIsRefused(unittest.TestCase):
48
+ def test_exactly_zero_refuses(self):
49
+ """The seed value must not read as a measurement."""
50
+ r = classify_box_units(0.0, IMGSZ)
51
+ self.assertIsNone(r["units"], "0.0 observed box extent was classified as a unit system")
52
+ self.assertIsNone(r["scale_to_pixels"])
53
+ self.assertIn("unmeasured", r["refusal"])
54
+
55
+ def test_negative_refuses(self):
56
+ """A negative max is impossible from `abs()`, so it means a bug upstream."""
57
+ self.assertIsNone(classify_box_units(-1.0, IMGSZ)["units"])
58
+
59
+ def test_none_still_refuses(self):
60
+ r = classify_box_units(None, IMGSZ)
61
+ self.assertIsNone(r["units"])
62
+ self.assertIn("unmeasured", r["refusal"])
63
+
64
+ def test_zero_imgsz_refuses(self):
65
+ self.assertIsNone(classify_box_units(639.9, 0)["units"])
66
+
67
+ def test_the_refusal_is_reachable_at_all(self):
68
+ """Guards the shape of the bug, not just this instance.
69
+
70
+ The refusal string named this exact case and could not be reached. A
71
+ message describing a state the code cannot produce is not a safeguard.
72
+ """
73
+ reasons = {
74
+ classify_box_units(v, IMGSZ)["refusal"]
75
+ for v in (None, 0.0, -0.0, -1.0)
76
+ }
77
+ self.assertEqual(len(reasons), 1, "every zero-evidence input must give the same reason")
78
+ self.assertTrue(all(r for r in reasons))
79
+
80
+
81
+ class RealMeasurementsStillClassify(unittest.TestCase):
82
+ """The fix must not widen the refusal into the measured bands."""
83
+
84
+ def test_a_normalised_head_is_still_normalised(self):
85
+ # 1/640 = 0.0016 — a normalised head's ratio, from the runbook.
86
+ r = classify_box_units(1.0, IMGSZ)
87
+ self.assertEqual(r["units"], "normalized")
88
+ self.assertEqual(r["scale_to_pixels"], float(IMGSZ))
89
+ self.assertIsNone(r["refusal"])
90
+
91
+ def test_smallest_credible_normalised_value_survives(self):
92
+ """The tightest case the fix could have swallowed.
93
+
94
+ A normalised box of 1e-6 of the frame is absurd but non-zero, and the
95
+ distinction the fix draws is between zero and non-zero, not between
96
+ small and large.
97
+ """
98
+ r = classify_box_units(1e-6, IMGSZ)
99
+ self.assertEqual(r["units"], "normalized")
100
+
101
+ def test_a_pixel_head_is_still_pixels(self):
102
+ r = classify_box_units(639.29, IMGSZ) # the shipped rfdetr_medium score card
103
+ self.assertEqual(r["units"], "pixels")
104
+ self.assertEqual(r["scale_to_pixels"], 1.0)
105
+
106
+ def test_the_dead_zone_still_refuses_with_its_ratio(self):
107
+ mid = (BOX_UNITS_NORMALIZED_RATIO_MAX + BOX_UNITS_PIXEL_RATIO_MIN) / 2
108
+ r = classify_box_units(mid * IMGSZ, IMGSZ)
109
+ self.assertIsNone(r["units"])
110
+ self.assertAlmostEqual(r["ratio"], mid, places=6)
111
+ self.assertIn("falls between", r["refusal"])
112
+
113
+ def test_band_edges_are_inclusive_as_documented(self):
114
+ self.assertEqual(classify_box_units(BOX_UNITS_PIXEL_RATIO_MIN * IMGSZ, IMGSZ)["units"], "pixels")
115
+ self.assertEqual(classify_box_units(BOX_UNITS_NORMALIZED_RATIO_MAX * IMGSZ, IMGSZ)["units"], "normalized")
116
+
117
+
118
+ if __name__ == "__main__":
119
+ unittest.main()
@@ -0,0 +1,559 @@
1
+ """The two YOLO decodes, against hand-built tensors. No model, no ORT session.
2
+
3
+ WHY THESE ASSERTIONS AND NOT "IT RETURNS SOMETHING"
4
+ --------------------------------------------------
5
+ Every failure this file is guarding against produces a complete, plausible table:
6
+
7
+ * a transposed anchor axis reads box channels as class scores;
8
+ * a second sigmoid squashes every confidence into 0.5..0.73;
9
+ * a missing `box_scale_to_pixels` draws every box 640x too small;
10
+ * suppression skipped or run twice changes the COUNT and nothing else;
11
+ * `end2end_n_6` decoded as `anchor_4_plus_nc` reads x1,y1,x2,y2,score,cls as
12
+ four box channels and two classes.
13
+
14
+ None of them raises, and none of them is visible in a shape or dtype check. So
15
+ each is asserted against a tensor whose right answer is known by construction,
16
+ and `bench/verify_detector_decode.py` is the second control: the same decode
17
+ against `ultralytics`' own NMS on real artifacts.
18
+
19
+ The whole file is skipped without numpy — the wire contract in `test_server.py`
20
+ needs no third-party package and is the half that must always run. It does NOT
21
+ need onnxruntime, deliberately: `decode.py` imports numpy and nothing else, so
22
+ the arithmetic is exercisable on any runner that can `import numpy`, which is
23
+ strictly more than the 24 cases in `test_session.py` reach.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import unittest
29
+
30
+ try:
31
+ import numpy as np
32
+
33
+ from trillboards_detector.decode import (
34
+ ANCHOR_4_PLUS_NC,
35
+ CLASS_OFFSET_STRIDE,
36
+ DECODABLE_LAYOUTS,
37
+ END2END_N_6,
38
+ MAX_DETECTIONS,
39
+ MAX_NMS_CANDIDATES,
40
+ NMS_IOU_THRESHOLD,
41
+ PREPROCESSING_BY_LAYOUT,
42
+ RFDETR_DETS_LABELS,
43
+ YOLO_DECODERS,
44
+ LetterboxGeometry,
45
+ box_iou_against,
46
+ decode_anchor_4_plus_nc,
47
+ decode_end2end_n_6,
48
+ greedy_nms,
49
+ )
50
+ from trillboards_detector.labels import (
51
+ BOX_UNITS,
52
+ BOX_UNITS_NORMALIZED,
53
+ BOX_UNITS_PIXELS,
54
+ OUTPUT_LAYOUTS,
55
+ VariantContract,
56
+ )
57
+
58
+ HAVE_NUMPY = True
59
+ except ImportError: # pragma: no cover - exercised only on a bare interpreter
60
+ HAVE_NUMPY = False
61
+
62
+
63
+ #: An identity mapping: no scaling, no padding, and a "frame" the size of the
64
+ #: model input. Model-input pixels and frame pixels then coincide, so a box's
65
+ #: expected value can be written down directly.
66
+ def _identity(size=640):
67
+ return LetterboxGeometry(1.0, 0.0, 0.0, size, size)
68
+
69
+
70
+ def _anchor(boxes_and_scores, num_classes, anchors=None):
71
+ """Build `[1, 4+nc, A]` from `[(cx, cy, w, h, {cls: score}), ...]`."""
72
+ rows = list(boxes_and_scores)
73
+ total = anchors or len(rows)
74
+ grid = np.zeros((1, 4 + num_classes, total), dtype=np.float32)
75
+ for i, (cx, cy, w, h, scores) in enumerate(rows):
76
+ grid[0, 0, i], grid[0, 1, i], grid[0, 2, i], grid[0, 3, i] = cx, cy, w, h
77
+ for cls, score in scores.items():
78
+ grid[0, 4 + cls, i] = score
79
+ return grid
80
+
81
+
82
+ def _end2end(rows):
83
+ """Build `[1, N, 6]` from `[(x1, y1, x2, y2, score, cls), ...]`."""
84
+ return np.asarray([list(rows)], dtype=np.float32)
85
+
86
+
87
+ def _anchor_decode(raw, geometry, min_confidence, box_scale_to_pixels=1.0):
88
+ """Decode a hand-built anchor tensor whose class width is right BY
89
+ CONSTRUCTION, so `expected_classes` is not the subject of the case.
90
+
91
+ Reading it off the tensor is exactly what production must never do, which is
92
+ why it happens HERE and nowhere in `decode.py`: the fixture author knows the
93
+ width because they wrote it. `TestAnchorClassWidthRefusal` is where a wrong
94
+ one is passed on purpose.
95
+ """
96
+ return decode_anchor_4_plus_nc(
97
+ raw, geometry, min_confidence,
98
+ box_scale_to_pixels=box_scale_to_pixels,
99
+ expected_classes=raw.shape[1] - 4,
100
+ )
101
+
102
+
103
+ def _end2end_decode(raw, geometry, min_confidence, box_scale_to_pixels=1.0, expected_classes=4):
104
+ return decode_end2end_n_6(
105
+ raw, geometry, min_confidence,
106
+ box_scale_to_pixels=box_scale_to_pixels,
107
+ expected_classes=expected_classes,
108
+ )
109
+
110
+
111
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
112
+ class TestDeclaredLayoutSet(unittest.TestCase):
113
+ """The two halves of one declaration, in two languages, must agree.
114
+
115
+ `OUTPUT_LAYOUTS` and `BOX_UNITS` are the box-side copies of the CHECK
116
+ constraints in
117
+ `trillboard-api/postgres/migrations/20260825032444_add_variant_coordinate_space.sql`
118
+ (`model_platform_variants_output_layout_ck`, `..._box_units_ck`). Nothing
119
+ mechanical can compare a Python frozenset to a SQL CHECK across two
120
+ languages, so the values are pinned here literally: a value the registry
121
+ accepts and this build drops publishes a variant nothing can decode, and a
122
+ value this build accepts and the registry rejects is a model that can never
123
+ ship. If the SQL set widens, this test is what has to be edited — and the
124
+ edit is where the new layout's decode gets written.
125
+ """
126
+
127
+ def test_output_layouts_match_the_migrations_check_constraint(self):
128
+ self.assertEqual(
129
+ OUTPUT_LAYOUTS,
130
+ frozenset({"anchor_4_plus_nc", "end2end_n_6", "rfdetr_dets_labels"}),
131
+ )
132
+
133
+ def test_box_units_match_the_migrations_check_constraint(self):
134
+ self.assertEqual(BOX_UNITS, frozenset({"pixels", "normalized"}))
135
+
136
+ def test_every_declarable_layout_has_a_decode(self):
137
+ """A layout with no decode is a model that loads and answers `[]`."""
138
+ self.assertEqual(DECODABLE_LAYOUTS, OUTPUT_LAYOUTS)
139
+ self.assertEqual(frozenset(YOLO_DECODERS), OUTPUT_LAYOUTS - {RFDETR_DETS_LABELS})
140
+
141
+ def test_every_layout_names_its_preprocessing(self):
142
+ self.assertEqual(frozenset(PREPROCESSING_BY_LAYOUT), OUTPUT_LAYOUTS)
143
+ # The two YOLO heads share one preprocessor and RF-DETR does not share it
144
+ # with either: that non-sharing is the fact worth pinning.
145
+ self.assertEqual(
146
+ PREPROCESSING_BY_LAYOUT[ANCHOR_4_PLUS_NC],
147
+ PREPROCESSING_BY_LAYOUT[END2END_N_6],
148
+ )
149
+ self.assertNotEqual(
150
+ PREPROCESSING_BY_LAYOUT[RFDETR_DETS_LABELS],
151
+ PREPROCESSING_BY_LAYOUT[ANCHOR_4_PLUS_NC],
152
+ )
153
+
154
+ def test_the_ultralytics_defaults_are_the_ones_a_score_assumes(self):
155
+ """`ultralytics/cfg/default.yaml`: iou 0.7, max_det 300, max_wh 7680."""
156
+ self.assertEqual(NMS_IOU_THRESHOLD, 0.7)
157
+ self.assertEqual(MAX_DETECTIONS, 300)
158
+ self.assertEqual(MAX_NMS_CANDIDATES, 30000)
159
+ self.assertEqual(CLASS_OFFSET_STRIDE, 7680)
160
+
161
+
162
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
163
+ class TestVariantContract(unittest.TestCase):
164
+ def test_an_unknown_layout_is_refused_at_declaration(self):
165
+ with self.assertRaisesRegex(ValueError, "not one of"):
166
+ VariantContract("yolo_v99_head", BOX_UNITS_PIXELS, 1.0)
167
+
168
+ def test_unknown_units_are_refused(self):
169
+ with self.assertRaisesRegex(ValueError, "box_units"):
170
+ VariantContract(ANCHOR_4_PLUS_NC, "0..640", 1.0)
171
+
172
+ def test_pixels_with_a_scale_that_is_not_one_is_refused(self):
173
+ """Mirrors `model_platform_variants_box_scale_ck`. A scale of 640 next to
174
+ `pixels` reads as a declaration and would multiply every box by 640."""
175
+ with self.assertRaisesRegex(ValueError, "already pixels"):
176
+ VariantContract(ANCHOR_4_PLUS_NC, BOX_UNITS_PIXELS, 640.0)
177
+
178
+ def test_normalized_without_a_scale_is_refused(self):
179
+ with self.assertRaisesRegex(ValueError, "positive box_scale_to_pixels"):
180
+ VariantContract(ANCHOR_4_PLUS_NC, BOX_UNITS_NORMALIZED, 0.0)
181
+
182
+ def test_normalized_with_a_scale_is_accepted(self):
183
+ contract = VariantContract(ANCHOR_4_PLUS_NC, BOX_UNITS_NORMALIZED, 640.0)
184
+ self.assertEqual(contract.box_scale_to_pixels, 640.0)
185
+
186
+
187
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
188
+ class TestIouAndSuppression(unittest.TestCase):
189
+ def test_iou_of_identical_boxes_is_one_and_of_disjoint_boxes_is_zero(self):
190
+ box = np.asarray([0.0, 0.0, 10.0, 10.0], dtype=np.float32)
191
+ others = np.asarray([[0.0, 0.0, 10.0, 10.0], [20.0, 20.0, 30.0, 30.0]], dtype=np.float32)
192
+ got = box_iou_against(box, others)
193
+ self.assertAlmostEqual(float(got[0]), 1.0, places=6)
194
+ self.assertAlmostEqual(float(got[1]), 0.0, places=6)
195
+
196
+ def test_half_overlap_is_one_third(self):
197
+ """Two 10x10 boxes sharing a 5x10 strip: 50 / (100 + 100 - 50)."""
198
+ box = np.asarray([0.0, 0.0, 10.0, 10.0], dtype=np.float32)
199
+ others = np.asarray([[5.0, 0.0, 15.0, 10.0]], dtype=np.float32)
200
+ self.assertAlmostEqual(float(box_iou_against(box, others)[0]), 1.0 / 3.0, places=6)
201
+
202
+ def test_a_zero_area_box_does_not_suppress_a_real_one(self):
203
+ box = np.asarray([5.0, 5.0, 5.0, 5.0], dtype=np.float32)
204
+ others = np.asarray([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32)
205
+ self.assertEqual(float(box_iou_against(box, others)[0]), 0.0)
206
+
207
+ def test_nms_keeps_the_best_and_drops_what_overlaps_it(self):
208
+ boxes = np.asarray(
209
+ [[0, 0, 10, 10], [1, 1, 11, 11], [50, 50, 60, 60]], dtype=np.float32
210
+ )
211
+ scores = np.asarray([0.9, 0.8, 0.7], dtype=np.float32)
212
+ self.assertEqual(list(greedy_nms(boxes, scores, 0.5)), [0, 2])
213
+
214
+ def test_nms_keeps_a_neighbour_below_the_threshold(self):
215
+ boxes = np.asarray([[0, 0, 10, 10], [5, 0, 15, 10]], dtype=np.float32)
216
+ scores = np.asarray([0.9, 0.8], dtype=np.float32)
217
+ # IoU is 1/3, under 0.7, so both survive at the shipped threshold.
218
+ self.assertEqual(list(greedy_nms(boxes, scores, NMS_IOU_THRESHOLD)), [0, 1])
219
+ self.assertEqual(list(greedy_nms(boxes, scores, 0.3)), [0])
220
+
221
+ def test_nms_of_nothing_is_an_empty_index_and_not_an_exception(self):
222
+ empty = np.zeros((0, 4), dtype=np.float32)
223
+ self.assertEqual(greedy_nms(empty, np.zeros((0,), dtype=np.float32), 0.7).shape, (0,))
224
+
225
+ def test_ties_break_by_first_index_and_not_by_reversal(self):
226
+ """`argsort(scores)[::-1]` reverses ties too, so the survivor flips."""
227
+ boxes = np.asarray([[0, 0, 10, 10], [0, 0, 10, 10]], dtype=np.float32)
228
+ scores = np.asarray([0.5, 0.5], dtype=np.float32)
229
+ self.assertEqual(list(greedy_nms(boxes, scores, 0.7)), [0])
230
+
231
+
232
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
233
+ class TestAnchorDecode(unittest.TestCase):
234
+ def test_cxcywh_becomes_frame_pixels(self):
235
+ raw = _anchor([(100.0, 200.0, 40.0, 60.0, {1: 0.9})], num_classes=3)
236
+ scores, classes, x0, y0, x1, y1 = _anchor_decode(
237
+ raw, _identity(), 0.25, 1.0
238
+ )
239
+ self.assertEqual(len(scores), 1)
240
+ self.assertAlmostEqual(float(scores[0]), 0.9, places=6)
241
+ self.assertEqual(int(classes[0]), 1)
242
+ self.assertAlmostEqual(float(x0[0]), 80.0, places=4)
243
+ self.assertAlmostEqual(float(y0[0]), 170.0, places=4)
244
+ self.assertAlmostEqual(float(x1[0]), 120.0, places=4)
245
+ self.assertAlmostEqual(float(y1[0]), 230.0, places=4)
246
+
247
+ def test_scores_are_taken_as_given_and_never_sigmoided_again(self):
248
+ """An Ultralytics detect head applies it inside the graph. A second one
249
+ turns 0.9 into 0.711 and leaves a table that still looks like
250
+ confidences."""
251
+ raw = _anchor([(10.0, 10.0, 4.0, 4.0, {0: 0.9})], num_classes=1)
252
+ scores, *_ = _anchor_decode(raw, _identity(), 0.25)
253
+ self.assertAlmostEqual(float(scores[0]), 0.9, places=6)
254
+
255
+ def test_the_best_class_wins_and_the_others_are_not_emitted(self):
256
+ """`multi_label` is False in Ultralytics' predict path, so one anchor
257
+ yields at most one detection however many classes clear the floor."""
258
+ raw = _anchor([(10.0, 10.0, 4.0, 4.0, {0: 0.4, 2: 0.8, 3: 0.6})], num_classes=4)
259
+ scores, classes, *_ = _anchor_decode(
260
+ raw, _identity(), 0.25, 1.0
261
+ )
262
+ self.assertEqual(len(scores), 1)
263
+ self.assertEqual(int(classes[0]), 2)
264
+
265
+ def test_normalized_boxes_are_scaled_to_pixels_by_the_declared_factor(self):
266
+ """THE 640x TRAP. Same tensor, same shape, same dtype, same scores."""
267
+ raw = _anchor([(0.5, 0.5, 0.1, 0.2, {0: 0.9})], num_classes=1)
268
+ _, _, x0, y0, x1, y1 = _anchor_decode(
269
+ raw, _identity(), 0.25, 640.0
270
+ )
271
+ self.assertAlmostEqual(float(x0[0]), 288.0, places=3)
272
+ self.assertAlmostEqual(float(y0[0]), 256.0, places=3)
273
+ self.assertAlmostEqual(float(x1[0]), 352.0, places=3)
274
+ self.assertAlmostEqual(float(y1[0]), 384.0, places=3)
275
+
276
+ def test_the_same_tensor_read_as_pixels_is_640x_too_small_and_never_raises(self):
277
+ """The negative control for the trap above, asserted rather than trusted:
278
+ a box 64 px wide becomes 0.1 px wide, with an identical score and class.
279
+ Nothing about the result says it is wrong, which is why the units are a
280
+ declaration and not an inference."""
281
+ raw = _anchor([(0.5, 0.5, 0.1, 0.2, {0: 0.9})], num_classes=1)
282
+ scores, classes, x0, _, x1, _ = _anchor_decode(
283
+ raw, _identity(), 0.25, 1.0
284
+ )
285
+ self.assertAlmostEqual(float(scores[0]), 0.9, places=6)
286
+ self.assertEqual(int(classes[0]), 0)
287
+ self.assertAlmostEqual(float(x1[0]) - float(x0[0]), 0.1, places=5)
288
+
289
+ def test_overlapping_boxes_of_one_class_are_suppressed(self):
290
+ raw = _anchor(
291
+ [
292
+ (100.0, 100.0, 50.0, 50.0, {0: 0.9}),
293
+ (102.0, 102.0, 50.0, 50.0, {0: 0.8}),
294
+ (400.0, 400.0, 50.0, 50.0, {0: 0.7}),
295
+ ],
296
+ num_classes=1,
297
+ )
298
+ scores, *_ = _anchor_decode(raw, _identity(), 0.25)
299
+ self.assertEqual([round(float(s), 3) for s in scores], [0.9, 0.7])
300
+
301
+ def test_suppression_never_crosses_classes(self):
302
+ """`agnostic=False`: two labels on the same object must both survive, or
303
+ a PPE model's `helmet` silently deletes its own `person`."""
304
+ raw = _anchor(
305
+ [
306
+ (100.0, 100.0, 50.0, 50.0, {0: 0.9}),
307
+ (100.0, 100.0, 50.0, 50.0, {1: 0.8}),
308
+ ],
309
+ num_classes=2,
310
+ )
311
+ scores, classes, *_ = _anchor_decode(
312
+ raw, _identity(), 0.25, 1.0
313
+ )
314
+ self.assertEqual(sorted(int(c) for c in classes), [0, 1])
315
+ self.assertEqual(len(scores), 2)
316
+
317
+ def test_the_confidence_floor_is_strict(self):
318
+ raw = _anchor([(10.0, 10.0, 4.0, 4.0, {0: 0.25})], num_classes=1)
319
+ scores, *_ = _anchor_decode(raw, _identity(), 0.25)
320
+ self.assertEqual(len(scores), 0)
321
+
322
+ def test_an_empty_frame_returns_empty_arrays_of_the_right_dtype(self):
323
+ raw = _anchor([(10.0, 10.0, 4.0, 4.0, {0: 0.01})], num_classes=1)
324
+ scores, classes, x0, y0, x1, y1 = _anchor_decode(
325
+ raw, _identity(), 0.25, 1.0
326
+ )
327
+ for arr in (scores, x0, y0, x1, y1):
328
+ self.assertEqual(arr.shape, (0,))
329
+ self.assertEqual(classes.dtype, np.int64)
330
+
331
+ def test_more_than_max_detections_are_truncated(self):
332
+ rows = [
333
+ (float(20 * i + 10), 10.0, 4.0, 4.0, {0: 0.9})
334
+ for i in range(MAX_DETECTIONS + 25)
335
+ ]
336
+ raw = _anchor(rows, num_classes=1)
337
+ scores, *_ = _anchor_decode(raw, _identity(), 0.25)
338
+ self.assertEqual(len(scores), MAX_DETECTIONS)
339
+
340
+ def test_letterbox_padding_is_removed_and_the_gain_undone(self):
341
+ """A 1200x900 frame at 640: gain 0.5333, 80 px of padding top and bottom.
342
+ A box centred in the letterboxed square is centred in the frame."""
343
+ gain = 640.0 / 1200.0
344
+ geometry = LetterboxGeometry(gain, 0.0, 80.0, 1200, 900)
345
+ raw = _anchor([(320.0, 320.0, 64.0, 64.0, {0: 0.9})], num_classes=1)
346
+ _, _, x0, y0, x1, y1 = _anchor_decode(
347
+ raw, geometry, 0.25, box_scale_to_pixels=1.0
348
+ )
349
+ self.assertAlmostEqual((float(x0[0]) + float(x1[0])) / 2.0, 600.0, places=3)
350
+ self.assertAlmostEqual((float(y0[0]) + float(y1[0])) / 2.0, 450.0, places=3)
351
+
352
+ def test_a_box_running_off_the_frame_is_clipped_to_it(self):
353
+ raw = _anchor([(0.0, 0.0, 100.0, 100.0, {0: 0.9})], num_classes=1)
354
+ _, _, x0, y0, x1, y1 = _anchor_decode(
355
+ raw, _identity(100), 0.25, 1.0
356
+ )
357
+ self.assertEqual((float(x0[0]), float(y0[0])), (0.0, 0.0))
358
+ self.assertAlmostEqual(float(x1[0]), 50.0, places=4)
359
+
360
+ def test_a_tensor_with_no_class_channels_is_refused(self):
361
+ with self.assertRaisesRegex(ValueError, "at least one class"):
362
+ decode_anchor_4_plus_nc(
363
+ np.zeros((1, 4, 10), dtype=np.float32), _identity(), 0.25,
364
+ box_scale_to_pixels=1.0, expected_classes=1,
365
+ )
366
+
367
+ def test_a_rank_two_tensor_is_refused_rather_than_reshaped(self):
368
+ with self.assertRaisesRegex(ValueError, ANCHOR_4_PLUS_NC):
369
+ decode_anchor_4_plus_nc(
370
+ np.zeros((5, 8400), dtype=np.float32), _identity(), 0.25,
371
+ box_scale_to_pixels=1.0, expected_classes=1,
372
+ )
373
+
374
+
375
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
376
+ class TestEnd2EndDecode(unittest.TestCase):
377
+ def test_rows_are_x1y1x2y2_score_cls_in_that_order(self):
378
+ raw = _end2end([(10.0, 20.0, 30.0, 40.0, 0.9, 2.0)])
379
+ scores, classes, x0, y0, x1, y1 = _end2end_decode(
380
+ raw, _identity(), 0.25, box_scale_to_pixels=1.0
381
+ )
382
+ self.assertAlmostEqual(float(scores[0]), 0.9, places=6)
383
+ self.assertEqual(int(classes[0]), 2)
384
+ self.assertEqual(
385
+ (float(x0[0]), float(y0[0]), float(x1[0]), float(y1[0])), (10.0, 20.0, 30.0, 40.0)
386
+ )
387
+
388
+ def test_already_suppressed_rows_are_not_suppressed_again(self):
389
+ """Two heavily overlapping rows are the model's own answer here. Running
390
+ NMS would delete one and produce a count nothing upstream ever measured."""
391
+ raw = _end2end(
392
+ [
393
+ (100.0, 100.0, 150.0, 150.0, 0.9, 0.0),
394
+ (101.0, 101.0, 151.0, 151.0, 0.8, 0.0),
395
+ ]
396
+ )
397
+ scores, *_ = _end2end_decode(raw, _identity(), 0.25)
398
+ self.assertEqual(len(scores), 2)
399
+
400
+ def test_the_row_order_is_preserved_and_not_re_ranked(self):
401
+ raw = _end2end(
402
+ [
403
+ (0.0, 0.0, 10.0, 10.0, 0.9, 0.0),
404
+ (20.0, 20.0, 30.0, 30.0, 0.4, 1.0),
405
+ (40.0, 40.0, 50.0, 50.0, 0.6, 2.0),
406
+ ]
407
+ )
408
+ scores, classes, *_ = _end2end_decode(raw, _identity(), 0.25)
409
+ self.assertEqual([int(c) for c in classes], [0, 1, 2])
410
+ self.assertEqual([round(float(s), 3) for s in scores], [0.9, 0.4, 0.6])
411
+
412
+ def test_the_confidence_floor_is_strict(self):
413
+ raw = _end2end([(0.0, 0.0, 10.0, 10.0, 0.25, 0.0)])
414
+ scores, *_ = _end2end_decode(raw, _identity(), 0.25)
415
+ self.assertEqual(len(scores), 0)
416
+
417
+ def test_normalized_rows_are_scaled_by_the_declared_factor(self):
418
+ raw = _end2end([(0.25, 0.25, 0.5, 0.5, 0.9, 0.0)])
419
+ _, _, x0, y0, x1, y1 = _end2end_decode(
420
+ raw, _identity(), 0.25, box_scale_to_pixels=640.0
421
+ )
422
+ self.assertEqual(
423
+ (float(x0[0]), float(y0[0]), float(x1[0]), float(y1[0])), (160.0, 160.0, 320.0, 320.0)
424
+ )
425
+
426
+ def test_a_padded_tail_of_zero_rows_is_dropped_by_the_floor(self):
427
+ """These graphs emit a fixed 300 rows and pad with zeros; a zero row has
428
+ score 0 and class 0, and would otherwise be a `person` at the origin."""
429
+ rows = [(10.0, 10.0, 20.0, 20.0, 0.9, 1.0)] + [(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)] * 299
430
+ scores, classes, *_ = _end2end_decode(
431
+ _end2end(rows), _identity(), 0.25, box_scale_to_pixels=1.0
432
+ )
433
+ self.assertEqual(len(scores), 1)
434
+ self.assertEqual(int(classes[0]), 1)
435
+
436
+ def test_a_tensor_whose_last_axis_is_not_six_is_refused(self):
437
+ with self.assertRaisesRegex(ValueError, END2END_N_6):
438
+ _end2end_decode(np.zeros((1, 300, 5), dtype=np.float32), _identity(), 0.25)
439
+
440
+ def test_an_anchor_tensor_handed_to_this_decode_is_refused_not_misread(self):
441
+ """`[1, 6, 8400]` and `[1, 300, 6]` are both rank-3 with a 6 in them.
442
+ This is the confusion the declared layout exists to make impossible, and
443
+ the refusal is what keeps a mis-declaration loud instead of plausible."""
444
+ with self.assertRaisesRegex(ValueError, END2END_N_6):
445
+ _end2end_decode(np.zeros((1, 6, 8400), dtype=np.float32), _identity(), 0.25)
446
+
447
+
448
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
449
+ class TestCrossLayoutRefusal(unittest.TestCase):
450
+ """Each decode handed the OTHER layout's tensor must refuse, both ways.
451
+
452
+ This is the failure the declared layout exists to prevent, checked at the
453
+ tensor rather than at the graph. `_read_graph_spec` refuses a mis-declared
454
+ artifact at LOAD from the shape the graph DECLARES; this refuses it from the
455
+ array ORT actually returned, which is a different claim the moment an axis is
456
+ dynamic. Neither check is redundant and neither alone is the whole guard.
457
+
458
+ A decode that "works" on the wrong layout is the shape that matters: there is
459
+ no exception, no NaN, no wrong dtype and no out-of-range confidence to notice
460
+ — one measurement of it produced a confident `cls_maxabs` of 632 from a box
461
+ coordinate read as a class score.
462
+ """
463
+
464
+ def test_an_end2end_tensor_sent_to_the_anchor_decode_is_refused(self):
465
+ """`[1, 300, 6]` arrives here as 300 - 4 = 296 "classes" over 6
466
+ "anchors" and decodes without raising into a meaningless table."""
467
+ raw = _end2end([(0.0, 0.0, 10.0, 10.0, 0.9, 1.0)] * 300)
468
+ with self.assertRaisesRegex(ValueError, r"296 class channels but this model's"):
469
+ decode_anchor_4_plus_nc(
470
+ raw, _identity(), 0.25, box_scale_to_pixels=1.0, expected_classes=2
471
+ )
472
+
473
+ def test_an_anchor_tensor_sent_to_the_end2end_decode_is_refused(self):
474
+ """`[1, 6, 8400]` has a 6 in it too — read as rows it would return six
475
+ detections whose `cls` column is a box coordinate."""
476
+ with self.assertRaisesRegex(ValueError, END2END_N_6):
477
+ _end2end_decode(np.zeros((1, 6, 8400), dtype=np.float32), _identity(), 0.25)
478
+
479
+ def test_the_anchor_refusal_names_the_other_layout(self):
480
+ """The message has to point at the fix. A bare shape mismatch sends the
481
+ reader to the artifact; naming `end2end_n_6` sends them to the row."""
482
+ raw = _end2end([(0.0, 0.0, 10.0, 10.0, 0.9, 1.0)] * 300)
483
+ with self.assertRaises(ValueError) as caught:
484
+ decode_anchor_4_plus_nc(
485
+ raw, _identity(), 0.25, box_scale_to_pixels=1.0, expected_classes=2
486
+ )
487
+ self.assertIn(END2END_N_6, str(caught.exception))
488
+
489
+ def test_a_correct_width_is_not_refused(self):
490
+ """The negative half of the control: the check must not fire on the
491
+ artifact it is meant to let through."""
492
+ raw = _anchor([(10.0, 10.0, 4.0, 4.0, {0: 0.9})], num_classes=2)
493
+ scores, *_ = decode_anchor_4_plus_nc(
494
+ raw, _identity(), 0.25, box_scale_to_pixels=1.0, expected_classes=2
495
+ )
496
+ self.assertEqual(len(scores), 1)
497
+
498
+
499
+ @unittest.skipUnless(HAVE_NUMPY, "numpy is not installed")
500
+ class TestNoClampBeforeSuppression(unittest.TestCase):
501
+ """A real head puts boxes off the frame; clipping early hides a layout error.
502
+
503
+ Measured on a committed artifact: box values reach -45.1 px. Clipping to the
504
+ frame before NMS moves a box's area, moves its IoU with its neighbours, and
505
+ changes which one survives — so the shipped answer would depend on a step
506
+ Ultralytics does not take, and an off-frame coordinate that should look wrong
507
+ would have been made to look sane first.
508
+ """
509
+
510
+ def test_a_box_outside_the_frame_still_participates_in_suppression(self):
511
+ # Two boxes overlapping at IoU 1.0, both hanging off the left edge. If
512
+ # either were clipped first they would stop being identical.
513
+ raw = _anchor(
514
+ [
515
+ (-20.0, 100.0, 100.0, 100.0, {0: 0.9}),
516
+ (-20.0, 100.0, 100.0, 100.0, {0: 0.8}),
517
+ ],
518
+ num_classes=1,
519
+ )
520
+ scores, _, x0, *_ = _anchor_decode(raw, _identity(), 0.25)
521
+ self.assertEqual(len(scores), 1)
522
+ self.assertAlmostEqual(float(scores[0]), 0.9, places=6)
523
+ # And the survivor IS clamped on the way out — the wire carries pixels of
524
+ # the submitted frame, and a negative one is not one.
525
+ self.assertEqual(float(x0[0]), 0.0)
526
+
527
+ def test_clipping_first_would_suppress_a_box_that_must_survive(self):
528
+ """The discriminating case, and it took a second attempt to build one.
529
+
530
+ Two boxes hanging off the left edge:
531
+
532
+ A xyxy [-100, 0, 100, 100] area 20,000
533
+ B xyxy [-300, 0, 100, 100] area 40,000
534
+
535
+ UNCLIPPED their IoU is 20,000 / 40,000 = 0.5, under the 0.7 threshold, so
536
+ both survive — which is what Ultralytics does and therefore what the
537
+ published number was measured on. CLIPPED to the frame first, both become
538
+ [0, 0, 100, 100], IoU 1.0, and the second is suppressed. One detection
539
+ disappears and nothing anywhere reports it.
540
+
541
+ The first fixture written for this did NOT discriminate: both boxes
542
+ clipped to ZERO width, `box_iou_against` scores a zero-area box 0, and
543
+ the mutant passed. A negative control that survives its own mutant is
544
+ worth nothing, so the geometry here is chosen for the property rather
545
+ than for looking off-frame.
546
+ """
547
+ raw = _anchor(
548
+ [
549
+ (0.0, 50.0, 200.0, 100.0, {0: 0.9}),
550
+ (-100.0, 50.0, 400.0, 100.0, {0: 0.8}),
551
+ ],
552
+ num_classes=1,
553
+ )
554
+ scores, *_ = _anchor_decode(raw, _identity(), 0.25)
555
+ self.assertEqual([round(float(v), 3) for v in scores], [0.9, 0.8])
556
+
557
+
558
+ if __name__ == "__main__":
559
+ unittest.main()