@trillboards/edge-sdk 0.2.85 → 0.2.91
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/deploy/detector/README.md +76 -7
- package/deploy/detector/bench/count_quantization.py +174 -0
- package/deploy/detector/bench/quantization-control.json +52 -0
- package/deploy/detector/bench/run_candidate.py +993 -0
- package/deploy/detector/bench/verify_detector_decode.py +574 -0
- package/deploy/detector/bench/verify_yolo_decode.py +135 -0
- package/deploy/detector/tests/test_decode.py +559 -0
- package/deploy/detector/tests/test_loader.py +10 -1
- package/deploy/detector/tests/test_registry.py +42 -4
- package/deploy/detector/tests/test_server.py +9 -1
- package/deploy/detector/tests/test_session.py +402 -1
- package/deploy/detector/trillboards_detector/decode.py +373 -0
- package/deploy/detector/trillboards_detector/labels.py +139 -0
- package/deploy/detector/trillboards_detector/loader.py +15 -1
- package/deploy/detector/trillboards_detector/registry.py +9 -0
- package/deploy/detector/trillboards_detector/session.py +233 -26
- package/dist/EdgeAgent.js +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/sensingCatchRate.d.ts +69 -5
- package/dist/sensingCatchRate.d.ts.map +1 -1
- package/dist/sensingCatchRate.js +41 -2
- package/dist/sensingCatchRate.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""verify_yolo_decode — is `run_candidate.py`'s YOLO decode the same instrument
|
|
3
|
+
Ultralytics is?
|
|
4
|
+
|
|
5
|
+
This is `verify_against_reference.py` one level up. That file checks the RF-DETR
|
|
6
|
+
ONNX export against the `rfdetr` package; this one checks the harness's OWN
|
|
7
|
+
decode of an Ultralytics (1, 4+C, 8400) head against `ultralytics` itself, on the
|
|
8
|
+
same pixels. It exists because §5 rule 5 of `cctv-detector-tier.md` is that every
|
|
9
|
+
wrong number in that day's work came from the instrument rather than the
|
|
10
|
+
reasoning, and the YOLO decode is the newest, least-exercised code in the scoring
|
|
11
|
+
path. A decode that transposes an axis, or applies a sigmoid twice, or reads the
|
|
12
|
+
box channels as class scores, produces a complete and entirely plausible table.
|
|
13
|
+
|
|
14
|
+
WHAT IS COMPARED, AND WHY NOT BOXES. The harness scores PRESENCE — the maximum
|
|
15
|
+
confidence per class per frame — because there is no box-level ground truth
|
|
16
|
+
committed anywhere in this repo. So the agreement that matters is on that
|
|
17
|
+
quantity, not on box coordinates the harness never reads. Comparing something the
|
|
18
|
+
scorer does not use would prove the export is fine and leave the actual scoring
|
|
19
|
+
path unchecked.
|
|
20
|
+
|
|
21
|
+
THE REFERENCE IS THE SAME ONNX FILE, LOADED BY ULTRALYTICS — not the `.pt` it
|
|
22
|
+
was exported from, and that choice cost a wrong run to find. Ultralytics'
|
|
23
|
+
predictor letterboxes with `auto = same_shapes and args.rect and format == "pt"`,
|
|
24
|
+
so a `.pt` reference on our 640x480 frames pads to **640x480** while the ONNX
|
|
25
|
+
graph has a fixed **640x640** input. The two arms then disagree by up to 0.25
|
|
26
|
+
confidence in BOTH directions and it looks like a decode bug. It is §5 rule 1
|
|
27
|
+
again: an arm at one imgsz compared against an arm at another is not a
|
|
28
|
+
comparison. Loading the ONNX makes `format != "pt"`, so `auto=False` and both
|
|
29
|
+
sides letterbox to the graph's own input size; `rect=False` is passed as well so
|
|
30
|
+
the geometry does not depend on that default. Pass `--reference <best.pt>`
|
|
31
|
+
explicitly to compare against the checkpoint, and expect it to disagree for this
|
|
32
|
+
reason rather than for a real one.
|
|
33
|
+
|
|
34
|
+
That leaves exactly one variable — our preprocessing and decode against theirs,
|
|
35
|
+
on identical bytes and identical geometry — which is what this file is for.
|
|
36
|
+
|
|
37
|
+
TOLERANCE. Pillow BILINEAR and Ultralytics' cv2 INTER_LINEAR agree closely and
|
|
38
|
+
not bit-for-bit, so a small confidence tolerance is expected and a large one is a
|
|
39
|
+
decode bug. The DECISION tolerance is zero: at the threshold, both sides must
|
|
40
|
+
agree on whether the class fires, on every frame.
|
|
41
|
+
|
|
42
|
+
python3 bench/verify_yolo_decode.py --onnx best.onnx \
|
|
43
|
+
--frames-dir /tmp/eval134 --limit 20 [--threshold 0.25] [--conf-tol 0.05]
|
|
44
|
+
|
|
45
|
+
Needs `ultralytics` — a documented manual run, never CI.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import argparse
|
|
51
|
+
import sys
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
|
|
54
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # noqa: E402
|
|
55
|
+
|
|
56
|
+
from run_candidate import HarnessYoloRunner, Frame, introspect_in_subprocess # noqa: E402
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main() -> int:
|
|
60
|
+
parser = argparse.ArgumentParser()
|
|
61
|
+
parser.add_argument("--onnx", required=True)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--reference", default=None,
|
|
64
|
+
help="what ultralytics should load as the reference. Defaults to --onnx, which is the "
|
|
65
|
+
"only setting that holds input geometry constant; see the file header.",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument("--frames-dir", required=True)
|
|
68
|
+
parser.add_argument("--limit", type=int, default=20)
|
|
69
|
+
parser.add_argument("--threshold", type=float, default=0.25)
|
|
70
|
+
parser.add_argument("--conf-tol", type=float, default=0.05)
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
from ultralytics import YOLO # noqa: PLC0415 - heavy, and only this file needs it
|
|
74
|
+
|
|
75
|
+
introspection = introspect_in_subprocess(Path(args.onnx))
|
|
76
|
+
names = {int(k): v for k, v in introspection["names"].items()}
|
|
77
|
+
ours = HarnessYoloRunner(Path(args.onnx), names)
|
|
78
|
+
reference = YOLO(args.reference or args.onnx)
|
|
79
|
+
|
|
80
|
+
frame_paths = sorted(Path(args.frames_dir).glob("*.jpg"))[: args.limit]
|
|
81
|
+
if not frame_paths:
|
|
82
|
+
raise SystemExit(f"no frames in {args.frames_dir}")
|
|
83
|
+
|
|
84
|
+
problems: "list[str]" = []
|
|
85
|
+
decisions = 0
|
|
86
|
+
for path in frame_paths:
|
|
87
|
+
frame = Frame(path)
|
|
88
|
+
# `HarnessYoloRunner.run` returns (per_class, infer_ms, box_max); the
|
|
89
|
+
# third arrived with the box-units verdict and this call still unpacked
|
|
90
|
+
# two, so this file could not execute at all.
|
|
91
|
+
mine, _, _ = ours.run(frame, 0.0)
|
|
92
|
+
# imgsz is pinned to the graph's own input size and verbose is off; a
|
|
93
|
+
# default imgsz here would compare an arm at 640 against one at 1280,
|
|
94
|
+
# which §5 rule 1 says is not a comparison.
|
|
95
|
+
result = reference.predict(
|
|
96
|
+
str(path), imgsz=ours.width, conf=0.001, rect=False, verbose=False, device="cpu"
|
|
97
|
+
)[0]
|
|
98
|
+
theirs: "dict[str, float]" = {}
|
|
99
|
+
for cls, conf in zip(result.boxes.cls.tolist(), result.boxes.conf.tolist()):
|
|
100
|
+
name = names[int(cls)]
|
|
101
|
+
theirs[name] = max(theirs.get(name, 0.0), float(conf))
|
|
102
|
+
|
|
103
|
+
for name in names.values():
|
|
104
|
+
a = mine.get(name, [0.0])[0] if name in mine else 0.0
|
|
105
|
+
b = theirs.get(name, 0.0)
|
|
106
|
+
decisions += 1
|
|
107
|
+
if (a >= args.threshold) != (b >= args.threshold):
|
|
108
|
+
problems.append(
|
|
109
|
+
f"{path.name} {name}: DECISION differs at {args.threshold} — ours {a:.4f}, reference {b:.4f}"
|
|
110
|
+
)
|
|
111
|
+
elif abs(a - b) > args.conf_tol and max(a, b) >= args.threshold:
|
|
112
|
+
problems.append(
|
|
113
|
+
f"{path.name} {name}: confidence {a:.4f} vs {b:.4f} (tol {args.conf_tol})"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
print(
|
|
117
|
+
f"reference {Path(args.reference or args.onnx).name} frames {len(frame_paths)} "
|
|
118
|
+
f"classes {len(names)} imgsz {ours.width}x{ours.height} presence decisions compared {decisions}"
|
|
119
|
+
)
|
|
120
|
+
if problems:
|
|
121
|
+
print("DISAGREEMENT — the decode in run_candidate.py is not the reference instrument:")
|
|
122
|
+
for p in problems[:40]:
|
|
123
|
+
print(f" - {p}")
|
|
124
|
+
if len(problems) > 40:
|
|
125
|
+
print(f" ... and {len(problems) - 40} more")
|
|
126
|
+
return 1
|
|
127
|
+
print(
|
|
128
|
+
f"AGREE: every presence decision matches at threshold {args.threshold}, and every "
|
|
129
|
+
f"firing confidence is within {args.conf_tol}."
|
|
130
|
+
)
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
raise SystemExit(main())
|