@camstack/addon-pipeline 1.2.103 → 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.
- package/dist/{addon-utils-CLc6yHCN.js → addon-utils-DRbRzrDy.js} +1 -1
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/detection-pipeline/index.js +844 -110
- package/dist/detection-pipeline/index.mjs +842 -108
- package/dist/{dist-Ccmt3fGJ.mjs → dist-D9sltFoR.mjs} +230 -6
- package/dist/{dist-C11WuNUP.js → dist-gLAVhbvO.js} +247 -5
- package/dist/{event-loop-stall-monitor-OJrOMeuu.mjs → event-loop-stall-monitor-BS_lPX6H.mjs} +82 -88
- package/dist/{event-loop-stall-monitor-Cq_NeC4o.js → event-loop-stall-monitor-BUhYY39J.js} +82 -88
- package/dist/{lazy-sharp-RxUs6on_.js → lazy-sharp-OiAUva0g.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +8 -6
- package/dist/pipeline-runner/index.mjs +6 -4
- package/dist/{process-memory-DOjQ3MgC.js → process-memory-DY4RHcTj.js} +1 -1
- package/dist/{process-memory-D0zDmXLI.mjs → process-memory-Dw7-9jCI.mjs} +1 -1
- package/dist/recorder/index.js +660 -130
- package/dist/recorder/index.mjs +659 -129
- package/dist/remote-restream-BeHi78PZ.mjs +25 -0
- package/dist/remote-restream-CO36Sr30.js +36 -0
- package/dist/restream-intent-B4BXZra7.mjs +72 -0
- package/dist/{remote-restream-BYbAsgUf.js → restream-intent-Cv9x3jmu.js} +30 -30
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- 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
- 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
- package/dist/stream-broker/{hostInit-Da9wVA2r.mjs → hostInit-aZY81RnZ.mjs} +3 -3
- package/dist/stream-broker/index.js +598 -82
- package/dist/stream-broker/index.mjs +597 -81
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DDpliBIW.mjs → worker-protocol-Bz7-sMZC.mjs} +1 -1
- package/dist/{worker-protocol-BePduZVV.js → worker-protocol-D_0q_4Le.js} +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +193 -1
- package/python/postprocessors/__init__.py +4 -0
- package/python/postprocessors/ctc.py +52 -0
- package/python/postprocessors/plate_slots.py +180 -0
- package/python/postprocessors/test_ctc.py +39 -0
- package/python/postprocessors/test_plate_slots.py +217 -0
- package/python/postprocessors/test_yolonas.py +83 -0
- package/python/postprocessors/yolonas.py +67 -0
- package/python/test_inference_pool_backpressure.py +6 -2
- package/python/test_inference_pool_coreml_cache.py +12 -4
- package/python/test_inference_pool_device_selection.py +12 -4
- package/python/test_inference_pool_layout.py +16 -5
- package/python/test_inference_pool_memstats.py +6 -2
- package/python/test_inference_pool_ov_ppp.py +12 -9
- package/python/test_inference_pool_preprocess.py +57 -0
- package/python/test_inference_pool_static_batch.py +426 -0
- package/dist/remote-restream-Ci7RXNGb.mjs +0 -66
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|