@camstack/addon-pipeline 1.2.102 → 1.2.104

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.
Files changed (52) hide show
  1. package/dist/{addon-utils-CLc6yHCN.js → addon-utils-DRbRzrDy.js} +1 -1
  2. package/dist/audio-analyzer/index.js +3 -3
  3. package/dist/audio-analyzer/index.mjs +2 -2
  4. package/dist/detection-pipeline/index.js +844 -110
  5. package/dist/detection-pipeline/index.mjs +842 -108
  6. package/dist/{dist-Ccmt3fGJ.mjs → dist-D9sltFoR.mjs} +230 -6
  7. package/dist/{dist-C11WuNUP.js → dist-gLAVhbvO.js} +247 -5
  8. package/dist/{event-loop-stall-monitor-OJrOMeuu.mjs → event-loop-stall-monitor-BS_lPX6H.mjs} +82 -88
  9. package/dist/{event-loop-stall-monitor-Cq_NeC4o.js → event-loop-stall-monitor-BUhYY39J.js} +82 -88
  10. package/dist/{lazy-sharp-RxUs6on_.js → lazy-sharp-OiAUva0g.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +8 -6
  14. package/dist/pipeline-runner/index.mjs +6 -4
  15. package/dist/{process-memory-DOjQ3MgC.js → process-memory-DY4RHcTj.js} +1 -1
  16. package/dist/{process-memory-D0zDmXLI.mjs → process-memory-Dw7-9jCI.mjs} +1 -1
  17. package/dist/recorder/index.js +660 -130
  18. package/dist/recorder/index.mjs +659 -129
  19. package/dist/remote-restream-BeHi78PZ.mjs +25 -0
  20. package/dist/remote-restream-CO36Sr30.js +36 -0
  21. package/dist/restream-intent-B4BXZra7.mjs +72 -0
  22. package/dist/{remote-restream-BYbAsgUf.js → restream-intent-Cv9x3jmu.js} +30 -30
  23. package/dist/session-decode/decode-worker-child.js +2 -2
  24. package/dist/session-decode/decode-worker-child.mjs +1 -1
  25. package/dist/stream-broker/_stub.js +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BdgcF1lL.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZaqZ17-x.mjs} +3 -3
  27. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BnhMnnKV.mjs +26 -0
  28. package/dist/stream-broker/{hostInit-Da9wVA2r.mjs → hostInit-aZY81RnZ.mjs} +3 -3
  29. package/dist/stream-broker/index.js +628 -92
  30. package/dist/stream-broker/index.mjs +627 -91
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/dist/{worker-protocol-DDpliBIW.mjs → worker-protocol-Bz7-sMZC.mjs} +1 -1
  33. package/dist/{worker-protocol-BePduZVV.js → worker-protocol-D_0q_4Le.js} +1 -1
  34. package/package.json +1 -1
  35. package/python/inference_pool.py +193 -1
  36. package/python/postprocessors/__init__.py +4 -0
  37. package/python/postprocessors/ctc.py +52 -0
  38. package/python/postprocessors/plate_slots.py +180 -0
  39. package/python/postprocessors/test_ctc.py +39 -0
  40. package/python/postprocessors/test_plate_slots.py +217 -0
  41. package/python/postprocessors/test_yolonas.py +83 -0
  42. package/python/postprocessors/yolonas.py +67 -0
  43. package/python/test_inference_pool_backpressure.py +6 -2
  44. package/python/test_inference_pool_coreml_cache.py +12 -4
  45. package/python/test_inference_pool_device_selection.py +12 -4
  46. package/python/test_inference_pool_layout.py +16 -5
  47. package/python/test_inference_pool_memstats.py +6 -2
  48. package/python/test_inference_pool_ov_ppp.py +12 -9
  49. package/python/test_inference_pool_preprocess.py +57 -0
  50. package/python/test_inference_pool_static_batch.py +426 -0
  51. package/dist/remote-restream-Ci7RXNGb.mjs +0 -66
  52. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DY31bUCj.mjs +0 -26
@@ -0,0 +1,217 @@
1
+ """Tests for the fixed-slot plate-OCR postprocessor (`cct-s-v2-global`).
2
+
3
+ The properties that make this reader safe are DIFFERENT from the CTC one's, and
4
+ that difference is the whole point of the file:
5
+
6
+ 1. **A fixed-slot reader is never silent on its own.** CTC collapses toward the
7
+ blank on noise, so "the string is too short" was a usable plausibility gate.
8
+ This model emits `max_plate_slots` softmax heads on EVERY input, so a wall,
9
+ a bumper and an OSD band all decode to some 7-character string. The gate
10
+ here is therefore a per-character CONFIDENCE FLOOR plus the region grammar —
11
+ `minTextLength` would pass literally everything.
12
+
13
+ 2. **The floor is on the WEAKEST character, not the mean.** A plate misread in
14
+ one position still averages ~0.9; it is the single bad slot that gives it
15
+ away. Measured on the 39-crop cam-617 set: every correct read has min-char
16
+ probability >= 0.75 or is silenced, and no junk ROI clears it on any engine.
17
+
18
+ 3. **Trailing pad is the length signal; interior pad is a refusal.** `_` is a
19
+ real class the model picks when a slot has no glyph. Trailing pads are
20
+ stripped (a 6-glyph plate in 10 slots); a pad in the MIDDLE means the model
21
+ declined a position it thinks exists, and that read is never a plate.
22
+ """
23
+ import numpy as np
24
+ import pytest
25
+
26
+ # Package-qualified, unlike the flat `from ctc import` of the sibling suites:
27
+ # `plate_slots` imports the shared plate grammar from `ctc`, so it has to be
28
+ # loaded AS a package member or the relative import has no parent. Same
29
+ # invocation as every other python suite here — `python -m pytest` from this
30
+ # directory, which puts both this directory and its parent on sys.path.
31
+ from postprocessors.plate_slots import (
32
+ DEFAULT_MIN_CHAR_PROBABILITY,
33
+ PLATE_ALPHABET,
34
+ PLATE_PAD_CHAR,
35
+ PLATE_SLOTS,
36
+ postprocess_plate_slots,
37
+ )
38
+
39
+ VOCAB = len(PLATE_ALPHABET)
40
+
41
+
42
+ def _slots(text: str, prob: float = 0.99, per_slot: "dict[int, float] | None" = None):
43
+ """Build a ``[1, SLOTS, VOCAB]`` softmax whose per-slot argmax spells `text`.
44
+
45
+ Slots beyond `text` take the pad class. `per_slot` overrides the winning
46
+ probability at individual positions so a single weak character can be
47
+ tested without weakening the rest.
48
+ """
49
+ padded = text.ljust(PLATE_SLOTS, PLATE_PAD_CHAR)[:PLATE_SLOTS]
50
+ out = np.zeros((PLATE_SLOTS, VOCAB), dtype=np.float32)
51
+ for slot, char in enumerate(padded):
52
+ winner = PLATE_ALPHABET.index(char)
53
+ p = (per_slot or {}).get(slot, prob)
54
+ out[slot] = (1.0 - p) / (VOCAB - 1)
55
+ out[slot, winner] = p
56
+ # A softmax over 37 classes cannot put its argmax below ~1/37, so a
57
+ # fixture asking for less would silently spell a DIFFERENT string and
58
+ # the test would be measuring nothing.
59
+ assert int(np.argmax(out[slot])) == winner, f"slot {slot}: p={p} does not win"
60
+ return out[np.newaxis]
61
+
62
+
63
+ def _read(text: str, config: "dict | None" = None, **kwargs) -> dict:
64
+ cfg = {"plateRegion": "DE"}
65
+ cfg.update(config or {})
66
+ return postprocess_plate_slots({"plate": _slots(text, **kwargs)}, cfg, 128, 64, 1.0, (0, 0))
67
+
68
+
69
+ # --- decode ------------------------------------------------------------------
70
+
71
+
72
+ def test_alphabet_matches_the_published_plate_config():
73
+ """The published `cct_s_v2_global_plate_config.yaml` contract. A drift here
74
+ shifts every class index and the model reads a different alphabet."""
75
+ assert PLATE_ALPHABET == "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_"
76
+ assert PLATE_PAD_CHAR == "_"
77
+ assert PLATE_SLOTS == 10
78
+ assert VOCAB == 37
79
+
80
+
81
+ def test_decodes_the_per_slot_argmax():
82
+ assert _read("DNRO309")["text"] == "DNRO309"
83
+
84
+
85
+ def test_strips_trailing_pad_only():
86
+ assert _read("DNLL78")["text"] == "DNLL78"
87
+
88
+
89
+ def test_interior_pad_is_refused():
90
+ """`DN_RO309` is the model declining slot 2, not a plate with an underscore."""
91
+ assert _read("DN_RO309")["text"] == ""
92
+
93
+
94
+ def test_all_pad_reads_empty():
95
+ assert _read("")["text"] == ""
96
+
97
+
98
+ def test_confidence_is_the_mean_over_kept_characters_only():
99
+ """The pad slots the model is trivially sure about must not inflate it."""
100
+ result = _read("DNLL78", prob=0.8)
101
+ assert result["confidence"] == pytest.approx(0.8, abs=1e-3)
102
+
103
+
104
+ def test_reads_the_plate_output_by_name_not_by_position():
105
+ """The graph emits `plate` AND `region`; dict order is not a contract."""
106
+ predictions = {"region": np.zeros((1, 66), np.float32), "plate": _slots("DNRO309")}
107
+ result = postprocess_plate_slots(predictions, {"plateRegion": "DE"}, 128, 64, 1.0, (0, 0))
108
+ assert result["text"] == "DNRO309"
109
+
110
+
111
+ def test_flat_output_is_reshaped():
112
+ """A [1, slots*vocab] export decodes identically to the [1, slots, vocab] one."""
113
+ flat = _slots("DNRO309").reshape(1, PLATE_SLOTS * VOCAB)
114
+ result = postprocess_plate_slots({"plate": flat}, {"plateRegion": "DE"}, 128, 64, 1.0, (0, 0))
115
+ assert result["text"] == "DNRO309"
116
+
117
+
118
+ def test_wrong_sized_output_is_refused_not_guessed():
119
+ with pytest.raises(ValueError):
120
+ postprocess_plate_slots(
121
+ {"plate": np.zeros((1, 9, VOCAB), np.float32)},
122
+ {"plateRegion": "DE"},
123
+ 128,
124
+ 64,
125
+ 1.0,
126
+ (0, 0),
127
+ )
128
+
129
+
130
+ # --- the confidence gate -----------------------------------------------------
131
+
132
+
133
+ def test_default_floor_is_the_measured_one():
134
+ assert DEFAULT_MIN_CHAR_PROBABILITY == 0.75
135
+
136
+
137
+ def test_one_weak_character_silences_the_whole_read():
138
+ """The failure this gate exists for: six confident glyphs and one guess."""
139
+ result = _read("DNRO309", per_slot={3: 0.42})
140
+ assert result["text"] == ""
141
+
142
+
143
+ def test_a_read_whose_weakest_character_clears_the_floor_survives():
144
+ result = _read("DNRO309", per_slot={3: 0.81})
145
+ assert result["text"] == "DNRO309"
146
+
147
+
148
+ def test_the_mean_cannot_rescue_a_weak_character():
149
+ """Six slots at 1.0 and one at 0.3 average 0.9 — the gate must still refuse."""
150
+ result = _read("DNRO309", prob=1.0, per_slot={0: 0.3})
151
+ assert result["text"] == ""
152
+
153
+
154
+ def test_floor_is_configurable():
155
+ result = _read("DNRO309", config={"minCharProbability": 0.4}, per_slot={3: 0.42})
156
+ assert result["text"] == "DNRO309"
157
+
158
+
159
+ def test_floor_of_zero_disables_the_gate():
160
+ result = _read("DNRO309", config={"minCharProbability": 0.0}, per_slot={3: 0.05})
161
+ assert result["text"] == "DNRO309"
162
+
163
+
164
+ def test_pad_slot_confidence_is_not_part_of_the_floor():
165
+ """Only the emitted glyphs are gated; a hesitant trailing pad is irrelevant."""
166
+ weak_pad = {slot: 0.3 for slot in range(6, PLATE_SLOTS)}
167
+ result = _read("DNLL78", per_slot=weak_pad)
168
+ assert result["text"] == "DNLL78"
169
+
170
+
171
+ # --- the grammar -------------------------------------------------------------
172
+
173
+
174
+ def test_non_conforming_read_is_silenced_when_a_region_is_active():
175
+ """Unlike the CTC path, a read that fails the grammar is DROPPED, not kept
176
+ with formatValid False: this reader's alphabet cannot produce the
177
+ 'obviously junk' shapes that made an annotated-but-kept read useful."""
178
+ assert _read("3JU511X")["text"] == ""
179
+
180
+
181
+ def test_conforming_read_is_marked_valid():
182
+ result = _read("DNRO309")
183
+ assert result["formatValid"] is True
184
+
185
+
186
+ def test_region_off_returns_the_raw_read():
187
+ result = _read("3JU511X", config={"plateRegion": "off"})
188
+ assert result["text"] == "3JU511X"
189
+ assert result["formatValid"] is False
190
+
191
+
192
+ def test_absent_region_defaults_to_off():
193
+ """A bare call (no config plumbing) must not silently apply a German
194
+ grammar to a plate photographed in another country."""
195
+ result = postprocess_plate_slots({"plate": _slots("3JU511X")}, {}, 128, 64, 1.0, (0, 0))
196
+ assert result["text"] == "3JU511X"
197
+
198
+
199
+ def test_unknown_region_does_not_crash_and_does_not_gate():
200
+ result = _read("3JU511X", config={"plateRegion": "ZZ"})
201
+ assert result["text"] == "3JU511X"
202
+ assert result["formatValid"] is False
203
+
204
+
205
+ def test_grammar_accepts_the_electric_suffix():
206
+ assert _read("DNRO309E")["text"] == "DNRO309E"
207
+
208
+
209
+ def test_kind_is_text():
210
+ assert _read("DNRO309")["kind"] == "text"
211
+
212
+
213
+ def test_silenced_read_still_reports_its_confidence():
214
+ """A dropped read must remain diagnosable in the logs."""
215
+ result = _read("3JU511X")
216
+ assert result["text"] == ""
217
+ assert result["confidence"] > 0
@@ -0,0 +1,83 @@
1
+ """Tests for the YOLO-NAS (Frigate-style flat NMS export) postprocessor.
2
+
3
+ Frigate's YOLO-NAS ONNX exports (super-gradients ``model.export()`` with fused
4
+ batched NMS, the format Frigate+ ships) emit ONE flat output tensor of shape
5
+ ``[N, 7]`` where each row is ``(batch_index, x1, y1, x2, y2, score, class_id)``
6
+ in INPUT pixel space, post-NMS. Empty GPU slots carry a negative class id.
7
+ """
8
+ import numpy as np
9
+
10
+ from postprocessors import POSTPROCESSORS
11
+ from postprocessors.yolonas import postprocess_yolonas
12
+
13
+
14
+ FRIGATE_LABELS = ["person", "car", "dog"]
15
+
16
+
17
+ def _run(output, config=None, orig_w=1280, orig_h=720, scale=0.25, pad=(0, 70)):
18
+ cfg = {"labels": FRIGATE_LABELS, "confidence": 0.3}
19
+ if config:
20
+ cfg.update(config)
21
+ return postprocess_yolonas({"output0": output}, cfg, orig_w, orig_h, scale, pad)
22
+
23
+
24
+ def test_registered_in_dispatch_table():
25
+ assert POSTPROCESSORS["yolonas"] is postprocess_yolonas
26
+
27
+
28
+ def test_decodes_flat_rows_and_undoes_letterbox():
29
+ # One person box at input-space (100, 120) → (200, 220), score 0.9.
30
+ out = np.array(
31
+ [
32
+ [0, 100.0, 120.0, 200.0, 220.0, 0.9, 0],
33
+ ],
34
+ dtype=np.float32,
35
+ )
36
+ result = _run(out)
37
+ assert result["kind"] == "detections"
38
+ dets = result["detections"]
39
+ assert len(dets) == 1
40
+ det = dets[0]
41
+ assert det["class"] == "person"
42
+ assert det["score"] == 0.9
43
+ # Undo letterbox: (x - pad_x) / scale, (y - pad_y) / scale
44
+ x1, y1, x2, y2 = det["bbox"]
45
+ assert x1 == 400.0 # (100 - 0) / 0.25
46
+ assert y1 == 200.0 # (120 - 70) / 0.25
47
+ assert x2 == 800.0
48
+ assert y2 == 600.0
49
+
50
+
51
+ def test_drops_below_confidence_and_negative_class():
52
+ out = np.array(
53
+ [
54
+ [0, 10, 10, 20, 20, 0.2, 0], # below floor
55
+ [0, 10, 10, 20, 20, 0.9, -1], # empty slot (negative class)
56
+ [0, 10, 10, 20, 20, 0.8, 1], # kept
57
+ ],
58
+ dtype=np.float32,
59
+ )
60
+ dets = _run(out)["detections"]
61
+ assert len(dets) == 1
62
+ assert dets[0]["class"] == "car"
63
+
64
+
65
+ def test_accepts_batched_3d_shape():
66
+ out = np.array([[[0, 10, 10, 20, 20, 0.8, 2]]], dtype=np.float32)
67
+ dets = _run(out)["detections"]
68
+ assert len(dets) == 1
69
+ assert dets[0]["class"] == "dog"
70
+
71
+
72
+ def test_out_of_range_class_uses_index_string():
73
+ out = np.array([[0, 10, 10, 20, 20, 0.8, 7]], dtype=np.float32)
74
+ dets = _run(out)["detections"]
75
+ assert dets[0]["class"] == "7"
76
+
77
+
78
+ def test_clamps_to_original_frame():
79
+ out = np.array([[0, -50, 0, 5000, 5000, 0.9, 0]], dtype=np.float32)
80
+ dets = _run(out)["detections"]
81
+ x1, y1, x2, y2 = dets[0]["bbox"]
82
+ assert x1 >= 0 and y1 >= 0
83
+ assert x2 <= 1280 and y2 <= 720
@@ -0,0 +1,67 @@
1
+ """YOLO-NAS postprocessor — Frigate-style flat batched-NMS ONNX export.
2
+
3
+ Frigate+ ``yolonas`` models (and any super-gradients ``model.export()`` with
4
+ fused NMS in FLAT format) emit ONE output tensor of shape ``[N, 7]`` where each
5
+ row is ``(batch_index, x1, y1, x2, y2, score, class_id)``:
6
+
7
+ - coordinates are in INPUT pixel space (0..inputSize), post-NMS,
8
+ - GPU exports pad unused slots with a NEGATIVE class id (skip them),
9
+ - class ids index the model's own label map (Frigate+ label set or COCO-80).
10
+
11
+ Output matches the yolo postprocessor:
12
+ ``{"kind": "detections", "detections": [{"class", "score", "bbox": [x1,y1,x2,y2]}]}``
13
+ with pixel bboxes re-projected to ORIGINAL frame coordinates (letterbox undone
14
+ via the pool-provided ``scale`` + ``pad``).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import numpy as np
19
+
20
+ from .yolo import COCO_80
21
+
22
+
23
+ def postprocess_yolonas(
24
+ predictions: dict,
25
+ config: dict,
26
+ orig_w: int,
27
+ orig_h: int,
28
+ scale: float,
29
+ pad: tuple[int, int],
30
+ ) -> dict:
31
+ conf_threshold = float(config.get("confidence", 0))
32
+ labels = config.get("labels", COCO_80)
33
+
34
+ output = np.asarray(list(predictions.values())[0])
35
+ # Accept [1, N, 7] batched exports by squeezing the batch axis.
36
+ if output.ndim == 3:
37
+ output = output.reshape(-1, output.shape[-1])
38
+ if output.ndim != 2 or output.shape[-1] != 7:
39
+ raise ValueError(
40
+ f"yolonas postprocessor expects a flat [N, 7] output, got shape {output.shape}"
41
+ )
42
+
43
+ detections = []
44
+ for row in output:
45
+ _batch_idx, x1, y1, x2, y2, score, class_id = row
46
+ score = float(score)
47
+ class_id = int(class_id)
48
+ if class_id < 0:
49
+ continue
50
+ if score < conf_threshold:
51
+ continue
52
+
53
+ ox1 = max(0.0, min(float(orig_w), (float(x1) - pad[0]) / scale))
54
+ oy1 = max(0.0, min(float(orig_h), (float(y1) - pad[1]) / scale))
55
+ ox2 = max(0.0, min(float(orig_w), (float(x2) - pad[0]) / scale))
56
+ oy2 = max(0.0, min(float(orig_h), (float(y2) - pad[1]) / scale))
57
+
58
+ label = labels[class_id] if class_id < len(labels) else str(class_id)
59
+ detections.append(
60
+ {
61
+ "class": label,
62
+ "score": round(score, 4),
63
+ "bbox": [round(ox1, 1), round(oy1, 1), round(ox2, 1), round(oy2, 1)],
64
+ }
65
+ )
66
+
67
+ return {"kind": "detections", "detections": detections}
@@ -16,7 +16,9 @@ import unittest
16
16
  # Stub the heavy third-party imports BEFORE importing inference_pool.
17
17
  # ---------------------------------------------------------------------------
18
18
 
19
- if "numpy" not in sys.modules:
19
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
20
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
21
+ except ImportError:
20
22
  # The stub must satisfy inference_pool's MODULE-LEVEL numpy usage
21
23
  # (_IMAGENET_MEAN/_IMAGENET_STD = np.array([...], dtype=np.float32));
22
24
  # a bare ModuleType broke this harness when those constants were added.
@@ -25,7 +27,9 @@ if "numpy" not in sys.modules:
25
27
  _np.array = lambda values, dtype=None: values
26
28
  sys.modules["numpy"] = _np
27
29
 
28
- if "PIL" not in sys.modules:
30
+ try: # real PIL when present, for the same reason
31
+ import PIL.Image # noqa: F401
32
+ except ImportError:
29
33
  _pil = types.ModuleType("PIL")
30
34
  _pil_image = types.ModuleType("PIL.Image")
31
35
  _pil.Image = _pil_image
@@ -29,10 +29,18 @@ import unittest
29
29
  # Stub the heavy third-party imports BEFORE importing inference_pool.
30
30
  # ---------------------------------------------------------------------------
31
31
 
32
- if "numpy" not in sys.modules:
33
- sys.modules["numpy"] = types.ModuleType("numpy")
34
-
35
- if "PIL" not in sys.modules:
32
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
33
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
34
+ except ImportError:
35
+ _np = types.ModuleType("numpy")
36
+ # `inference_pool` builds `_IMAGENET_MEAN = np.array(...)` at import time.
37
+ _np.array = lambda seq, dtype=None: list(seq) # noqa: E731
38
+ _np.float32 = "float32"
39
+ sys.modules["numpy"] = _np
40
+
41
+ try: # real PIL when present, for the same reason
42
+ import PIL.Image # noqa: F401
43
+ except ImportError:
36
44
  _pil = types.ModuleType("PIL")
37
45
  _pil_image = types.ModuleType("PIL.Image")
38
46
  _pil.Image = _pil_image
@@ -21,10 +21,18 @@ import unittest
21
21
  # Stub the heavy third-party imports BEFORE importing inference_pool.
22
22
  # ---------------------------------------------------------------------------
23
23
 
24
- if "numpy" not in sys.modules:
25
- sys.modules["numpy"] = types.ModuleType("numpy")
26
-
27
- if "PIL" not in sys.modules:
24
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
25
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
26
+ except ImportError:
27
+ _np = types.ModuleType("numpy")
28
+ # `inference_pool` builds `_IMAGENET_MEAN = np.array(...)` at import time.
29
+ _np.array = lambda seq, dtype=None: list(seq) # noqa: E731
30
+ _np.float32 = "float32"
31
+ sys.modules["numpy"] = _np
32
+
33
+ try: # real PIL when present, for the same reason
34
+ import PIL.Image # noqa: F401
35
+ except ImportError:
28
36
  _pil = types.ModuleType("PIL")
29
37
  _pil_image = types.ModuleType("PIL.Image")
30
38
  _pil.Image = _pil_image
@@ -12,11 +12,22 @@ in a minimal sandbox.
12
12
  import sys
13
13
  import types
14
14
 
15
- for _mod in ("numpy", "PIL", "PIL.Image"):
16
- if _mod not in sys.modules:
17
- sys.modules[_mod] = types.ModuleType(_mod)
18
- if not hasattr(sys.modules["PIL"], "Image"):
19
- sys.modules["PIL"].Image = types.ModuleType("PIL.Image")
15
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
16
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
17
+ except ImportError:
18
+ _np = types.ModuleType("numpy")
19
+ # `inference_pool` builds `_IMAGENET_MEAN = np.array(...)` at import time.
20
+ _np.array = lambda seq, dtype=None: list(seq) # noqa: E731
21
+ _np.float32 = "float32"
22
+ sys.modules["numpy"] = _np
23
+ try: # real PIL when present, for the same reason
24
+ import PIL.Image # noqa: F401
25
+ except ImportError:
26
+ _pil = types.ModuleType("PIL")
27
+ _pil_image = types.ModuleType("PIL.Image")
28
+ _pil.Image = _pil_image
29
+ sys.modules["PIL"] = _pil
30
+ sys.modules["PIL.Image"] = _pil_image
20
31
 
21
32
  from inference_pool import _is_channels_last, _resolve_input_dims # noqa: E402
22
33
 
@@ -17,7 +17,9 @@ import unittest
17
17
  # Stub the heavy third-party imports BEFORE importing inference_pool.
18
18
  # ---------------------------------------------------------------------------
19
19
 
20
- if "numpy" not in sys.modules:
20
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
21
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
22
+ except ImportError:
21
23
  # The stub must satisfy inference_pool's MODULE-LEVEL numpy usage
22
24
  # (_IMAGENET_MEAN/_IMAGENET_STD = np.array([...], dtype=np.float32));
23
25
  # a bare ModuleType broke this harness when those constants were added.
@@ -26,7 +28,9 @@ if "numpy" not in sys.modules:
26
28
  _np.array = lambda values, dtype=None: values
27
29
  sys.modules["numpy"] = _np
28
30
 
29
- if "PIL" not in sys.modules:
31
+ try: # real PIL when present, for the same reason
32
+ import PIL.Image # noqa: F401
33
+ except ImportError:
30
34
  _pil = types.ModuleType("PIL")
31
35
  _pil_image = types.ModuleType("PIL.Image")
32
36
  _pil.Image = _pil_image
@@ -13,19 +13,22 @@ import sys
13
13
  import types
14
14
  import unittest
15
15
 
16
- if "numpy" not in sys.modules:
16
+ try: # real numpy when the sandbox has it — a blind stub POISONS the whole
17
+ import numpy # noqa: F401 # pytest session for every sibling that needs it
18
+ except ImportError:
17
19
  _np = types.ModuleType("numpy")
18
- # inference_pool builds `_IMAGENET_MEAN = np.array(...)` at import time; the
19
- # PPP plan decision under test needs no real numpy, so a passthrough stub
20
- # keeps this pure test runnable in a numpy-less sandbox.
20
+ # `inference_pool` builds `_IMAGENET_MEAN = np.array(...)` at import time.
21
21
  _np.array = lambda seq, dtype=None: list(seq) # noqa: E731
22
22
  _np.float32 = "float32"
23
23
  sys.modules["numpy"] = _np
24
- for _mod in ("PIL", "PIL.Image"):
25
- if _mod not in sys.modules:
26
- sys.modules[_mod] = types.ModuleType(_mod)
27
- if not hasattr(sys.modules["PIL"], "Image"):
28
- sys.modules["PIL"].Image = types.ModuleType("PIL.Image")
24
+ try: # real PIL when present, for the same reason
25
+ import PIL.Image # noqa: F401
26
+ except ImportError:
27
+ _pil = types.ModuleType("PIL")
28
+ _pil_image = types.ModuleType("PIL.Image")
29
+ _pil.Image = _pil_image
30
+ sys.modules["PIL"] = _pil
31
+ sys.modules["PIL.Image"] = _pil_image
29
32
  if "postprocessors" not in sys.modules:
30
33
  _pp = types.ModuleType("postprocessors")
31
34
  _pp.POSTPROCESSORS = {}
@@ -155,3 +155,60 @@ def test_untagged_frame_never_caches() -> None:
155
155
  assert inference_pool._bench_cache_hits == 0
156
156
  assert inference_pool._bench_cache_misses == 0
157
157
  assert len(inference_pool._bench_preprocess_cache) == 0
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # uint8-input ONNX models (super-gradients / Frigate YOLO-NAS exports bake the
162
+ # /255 preprocessing INTO the graph and declare a uint8 input tensor)
163
+ # ---------------------------------------------------------------------------
164
+
165
+
166
+ def test_onnx_uint8_input_is_fed_raw_uint8_nchw() -> None:
167
+ inference_pool._runtime = "onnxruntime"
168
+ cfg = {
169
+ "inputSize": 320,
170
+ "_input_name": "input",
171
+ "_input_shape": [1, 3, 320, 320],
172
+ "_input_dtype": "uint8",
173
+ "preprocessMode": "letterbox",
174
+ }
175
+ img = Image.new("RGB", (640, 360), (200, 100, 50))
176
+ input_dict, scale, pad = _preprocess(img, cfg)
177
+ tensor = input_dict["input"]
178
+ assert tensor.dtype == np.uint8
179
+ assert tensor.shape == (1, 3, 320, 320)
180
+ # Letterbox math still reported for the postprocess re-projection.
181
+ assert scale == pytest.approx(0.5)
182
+ assert pad[1] > 0 # vertical padding for a 16:9 frame in a square input
183
+
184
+
185
+ def test_onnx_uint8_input_respects_nhwc_declared_shape() -> None:
186
+ inference_pool._runtime = "onnxruntime"
187
+ cfg = {
188
+ "inputSize": 320,
189
+ "_input_name": "input",
190
+ "_input_shape": [1, 320, 320, 3],
191
+ "_input_dtype": "uint8",
192
+ "preprocessMode": "letterbox",
193
+ }
194
+ img = Image.new("RGB", (320, 320), (10, 20, 30))
195
+ input_dict, _scale, _pad = _preprocess(img, cfg)
196
+ tensor = input_dict["input"]
197
+ assert tensor.dtype == np.uint8
198
+ assert tensor.shape == (1, 320, 320, 3)
199
+
200
+
201
+ def test_onnx_float_input_unchanged_by_dtype_feature() -> None:
202
+ # No `_input_dtype` → the historical float32 [0,1] path, byte-identical.
203
+ inference_pool._runtime = "onnxruntime"
204
+ cfg = {
205
+ "inputSize": 320,
206
+ "_input_name": "images",
207
+ "_input_shape": [1, 3, 320, 320],
208
+ "preprocessMode": "letterbox",
209
+ }
210
+ img = Image.new("RGB", (320, 320), (255, 255, 255))
211
+ input_dict, _scale, _pad = _preprocess(img, cfg)
212
+ tensor = input_dict["images"]
213
+ assert tensor.dtype == np.float32
214
+ assert float(tensor.max()) <= 1.0