@trillboards/edge-sdk 0.2.91 → 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,71 @@
|
|
|
1
|
+
"""What a box coordinate MEANS, decided from a forward pass — STDLIB ONLY.
|
|
2
|
+
|
|
3
|
+
Split out of `run_candidate.py` so the `detector-service` CI lane can reach it.
|
|
4
|
+
That lane runs `python3 -m unittest discover` with NO pip install, and
|
|
5
|
+
`run_candidate.py` imports `trillboards_detector.session` and therefore
|
|
6
|
+
onnxruntime at module scope — so a test importing it does not skip, it errors
|
|
7
|
+
the whole lane during discovery. The decision lives here in stdlib; the forward
|
|
8
|
+
pass that produces `box_abs_max` stays there.
|
|
9
|
+
|
|
10
|
+
Same split, and the same reason, as `bench/twin_parity_contract.py`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
#: LiteRT boxes in 0..1, agreeing to 0.00266 px after multiplying by 640, while
|
|
17
|
+
#: their CLASS channels agree to 1.2e-07. `queue_supermarket`, the one end2end
|
|
18
|
+
#: twin, agrees on both sides — end2end heads emit pixels everywhere.
|
|
19
|
+
#:
|
|
20
|
+
#: Every check a publish gate would otherwise run PASSES on that pair: same
|
|
21
|
+
#: output shape, same dtype, loads, runs, class scores identical. An Android
|
|
22
|
+
#: post-processor reusing the ONNX decode draws every box 640x too small and
|
|
23
|
+
#: nothing reports an error. So the units are MEASURED from a forward pass, per
|
|
24
|
+
#: artifact, and recorded — never carried across from the sibling format.
|
|
25
|
+
#:
|
|
26
|
+
#: The discriminator is a ratio, max|box channel| / imgsz, and it is decisive by
|
|
27
|
+
#: two and a half orders of magnitude. Measured over 119 ONNX artifacts the ratio
|
|
28
|
+
#: spans 0.988..1.167; a normalised head sits at ~1/640 = 0.0016. The dead zone
|
|
29
|
+
#: below is EMPTY across every artifact in the tree, and an artifact landing in it
|
|
30
|
+
#: is refused rather than guessed at — that is the whole point of the field.
|
|
31
|
+
BOX_UNITS_PIXEL_RATIO_MIN = 0.2
|
|
32
|
+
BOX_UNITS_NORMALIZED_RATIO_MAX = 0.02
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def classify_box_units(box_abs_max: float, imgsz: int) -> dict:
|
|
36
|
+
"""pixels, normalized, or REFUSE — never a guess.
|
|
37
|
+
|
|
38
|
+
Returns the verdict plus the evidence, because a bare enum in a score card is
|
|
39
|
+
a claim and the ratio is what makes it checkable.
|
|
40
|
+
"""
|
|
41
|
+
# `<= 0.0` MATTERS AS MUCH AS `is None`, and it is the case that actually
|
|
42
|
+
# happens. A model whose classification head is dead still emits box rows —
|
|
43
|
+
# `forklift_akdve_11n` has healthy boxes (2.93-636 px) and a max class score
|
|
44
|
+
# of 0.0063 — so nothing is admitted at threshold, `box_abs_max` stays at its
|
|
45
|
+
# 0.0 seed, and the ratio 0.0/imgsz lands under BOX_UNITS_NORMALIZED_RATIO_MAX.
|
|
46
|
+
# The old predicate then returned a CONFIDENT `normalized` with
|
|
47
|
+
# scale_to_pixels=640 from zero observations, and that declaration is exactly
|
|
48
|
+
# what `session.py` now trusts absolutely: the layout and units are DECLARED
|
|
49
|
+
# by the variant contract and never re-derived from the tensor. So a dead
|
|
50
|
+
# artifact published a coordinate-space claim no frame supports.
|
|
51
|
+
#
|
|
52
|
+
# The refusal string below already described this case. It just could not be
|
|
53
|
+
# reached, which is the whole shape of the defect this function exists to
|
|
54
|
+
# prevent — a check that runs, passes, and measures nothing.
|
|
55
|
+
if imgsz <= 0 or box_abs_max is None or box_abs_max <= 0.0:
|
|
56
|
+
return {"units": None, "ratio": None, "scale_to_pixels": None,
|
|
57
|
+
"refusal": "no box channels were observed, so the coordinate space is unmeasured"}
|
|
58
|
+
ratio = box_abs_max / imgsz
|
|
59
|
+
if ratio >= BOX_UNITS_PIXEL_RATIO_MIN:
|
|
60
|
+
return {"units": "pixels", "ratio": ratio, "scale_to_pixels": 1.0, "refusal": None}
|
|
61
|
+
if ratio <= BOX_UNITS_NORMALIZED_RATIO_MAX:
|
|
62
|
+
return {"units": "normalized", "ratio": ratio, "scale_to_pixels": float(imgsz), "refusal": None}
|
|
63
|
+
return {
|
|
64
|
+
"units": None, "ratio": ratio, "scale_to_pixels": None,
|
|
65
|
+
"refusal": (
|
|
66
|
+
f"max|box|/imgsz = {ratio:.4f} falls between {BOX_UNITS_NORMALIZED_RATIO_MAX} and "
|
|
67
|
+
f"{BOX_UNITS_PIXEL_RATIO_MIN}, where neither pixels nor normalised is credible. "
|
|
68
|
+
"Measured over 119 artifacts this band is empty; refusing rather than picking one, "
|
|
69
|
+
"because the wrong pick is silent and scales every box by 640."
|
|
70
|
+
),
|
|
71
|
+
}
|
|
@@ -82,7 +82,13 @@ import time
|
|
|
82
82
|
from pathlib import Path
|
|
83
83
|
|
|
84
84
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # noqa: E402
|
|
85
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent)) # noqa: E402 (bench siblings)
|
|
85
86
|
|
|
87
|
+
from box_units import ( # noqa: E402 (sibling module, stdlib-only on purpose)
|
|
88
|
+
BOX_UNITS_NORMALIZED_RATIO_MAX,
|
|
89
|
+
BOX_UNITS_PIXEL_RATIO_MIN,
|
|
90
|
+
classify_box_units,
|
|
91
|
+
)
|
|
86
92
|
from trillboards_detector.errors import ModelContractError # noqa: E402
|
|
87
93
|
from trillboards_detector.labels import ( # noqa: E402
|
|
88
94
|
BOX_UNITS_PIXELS,
|
|
@@ -107,23 +113,7 @@ SCHEMA = "detector-candidate-run/2"
|
|
|
107
113
|
#: Ultralytics' TFLite export path wraps a `task=detect`, `end2end=False` model in
|
|
108
114
|
#: `_NormalizeCoords`, dividing the four box channels by (W, H). Measured here on
|
|
109
115
|
#: all 17 committed LiteRT twins: 16 of 17 have ONNX boxes in pixels 0..640 and
|
|
110
|
-
|
|
111
|
-
#: their CLASS channels agree to 1.2e-07. `queue_supermarket`, the one end2end
|
|
112
|
-
#: twin, agrees on both sides — end2end heads emit pixels everywhere.
|
|
113
|
-
#:
|
|
114
|
-
#: Every check a publish gate would otherwise run PASSES on that pair: same
|
|
115
|
-
#: output shape, same dtype, loads, runs, class scores identical. An Android
|
|
116
|
-
#: post-processor reusing the ONNX decode draws every box 640x too small and
|
|
117
|
-
#: nothing reports an error. So the units are MEASURED from a forward pass, per
|
|
118
|
-
#: artifact, and recorded — never carried across from the sibling format.
|
|
119
|
-
#:
|
|
120
|
-
#: The discriminator is a ratio, max|box channel| / imgsz, and it is decisive by
|
|
121
|
-
#: two and a half orders of magnitude. Measured over 119 ONNX artifacts the ratio
|
|
122
|
-
#: spans 0.988..1.167; a normalised head sits at ~1/640 = 0.0016. The dead zone
|
|
123
|
-
#: below is EMPTY across every artifact in the tree, and an artifact landing in it
|
|
124
|
-
#: is refused rather than guessed at — that is the whole point of the field.
|
|
125
|
-
BOX_UNITS_PIXEL_RATIO_MIN = 0.2
|
|
126
|
-
BOX_UNITS_NORMALIZED_RATIO_MAX = 0.02
|
|
116
|
+
|
|
127
117
|
|
|
128
118
|
#: Warm-up forward passes before any timing is kept. ORT's first run pays for
|
|
129
119
|
#: arena allocation and kernel selection; folding that into a p50 over 134 frames
|
|
@@ -271,31 +261,6 @@ def classify_output_layout(output_shapes: "list[list]") -> str:
|
|
|
271
261
|
return f"unknown_rank_{len(shape)}"
|
|
272
262
|
|
|
273
263
|
|
|
274
|
-
def classify_box_units(box_abs_max: float, imgsz: int) -> dict:
|
|
275
|
-
"""pixels, normalized, or REFUSE — never a guess.
|
|
276
|
-
|
|
277
|
-
Returns the verdict plus the evidence, because a bare enum in a score card is
|
|
278
|
-
a claim and the ratio is what makes it checkable.
|
|
279
|
-
"""
|
|
280
|
-
if imgsz <= 0 or box_abs_max is None:
|
|
281
|
-
return {"units": None, "ratio": None, "scale_to_pixels": None,
|
|
282
|
-
"refusal": "no box channels were observed, so the coordinate space is unmeasured"}
|
|
283
|
-
ratio = box_abs_max / imgsz
|
|
284
|
-
if ratio >= BOX_UNITS_PIXEL_RATIO_MIN:
|
|
285
|
-
return {"units": "pixels", "ratio": ratio, "scale_to_pixels": 1.0, "refusal": None}
|
|
286
|
-
if ratio <= BOX_UNITS_NORMALIZED_RATIO_MAX:
|
|
287
|
-
return {"units": "normalized", "ratio": ratio, "scale_to_pixels": float(imgsz), "refusal": None}
|
|
288
|
-
return {
|
|
289
|
-
"units": None, "ratio": ratio, "scale_to_pixels": None,
|
|
290
|
-
"refusal": (
|
|
291
|
-
f"max|box|/imgsz = {ratio:.4f} falls between {BOX_UNITS_NORMALIZED_RATIO_MAX} and "
|
|
292
|
-
f"{BOX_UNITS_PIXEL_RATIO_MIN}, where neither pixels nor normalised is credible. "
|
|
293
|
-
"Measured over 119 artifacts this band is empty; refusing rather than picking one, "
|
|
294
|
-
"because the wrong pick is silent and scales every box by 640."
|
|
295
|
-
),
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
264
|
def letterbox(image, width: int, height: int):
|
|
300
265
|
"""Ultralytics' resize: scale to fit, pad the remainder 114-grey, centred.
|
|
301
266
|
|
|
@@ -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()
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.92';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
package/dist/diagnose.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface DiagnosticReport {
|
|
|
31
31
|
skipped: number;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
declare const SDK_VERSION = "0.2.
|
|
34
|
+
declare const SDK_VERSION = "0.2.92";
|
|
35
35
|
declare function checkNodeVersion(): CheckResult;
|
|
36
36
|
declare function checkPlatform(): CheckResult;
|
|
37
37
|
declare function checkMemory(): CheckResult;
|
package/dist/diagnose.js
CHANGED
|
@@ -63,7 +63,7 @@ const localNetworkAccess_1 = require("./localNetworkAccess");
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
// SDK version — mirrors package.json; kept in sync by CI
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
const SDK_VERSION = '0.2.
|
|
66
|
+
const SDK_VERSION = '0.2.92';
|
|
67
67
|
exports._SDK_VERSION = SDK_VERSION;
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
// ANSI helpers (no external deps)
|