meaculpitt commited on
Commit
493bb1d
Β·
verified Β·
1 Parent(s): 98e116b

scorevision: push artifact

Browse files
Files changed (4) hide show
  1. __pycache__/miner.cpython-312.pyc +0 -0
  2. miner.py +138 -22
  3. miner.py.bak +408 -0
  4. miner.py.bak_v1 +470 -0
__pycache__/miner.cpython-312.pyc ADDED
Binary file (29.6 kB). View file
 
miner.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
- Score Vision SN44 β€” Unified miner v1 (2026-03-27).
3
  Dual-model: vehicle (YOLO11s) + person (YOLO11s).
 
4
 
5
  Vehicle model (vehicle_weights.onnx):
6
  Trained classes: 0=car, 1=bus, 2=truck, 3=motorcycle
@@ -16,6 +17,8 @@ Vehicle eval uses cls_id 0-3. Person eval uses cls_id 0 only.
16
 
17
  from pathlib import Path
18
  import math
 
 
19
 
20
  import cv2
21
  import numpy as np
@@ -23,6 +26,13 @@ import onnxruntime as ort
23
  from numpy import ndarray
24
  from pydantic import BaseModel
25
 
 
 
 
 
 
 
 
26
  # ── Vehicle config ──────────────────────────────────────────────────────────
27
  VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
28
  VEH_NUM_CLASSES = 4
@@ -40,6 +50,10 @@ PER_WBF_IOU = 0.45
40
  # ── Shared ──────────────────────────────────────────────────────────────────
41
  WBF_SKIP_THR = 0.0001
42
 
 
 
 
 
43
 
44
  def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
45
  """Weighted Boxes Fusion (multi-class). Boxes in [0,1] normalized coords."""
@@ -194,8 +208,18 @@ class Miner:
194
  self.per_h = int(per_shape[2])
195
  self.per_w = int(per_shape[3])
196
 
 
 
 
 
 
 
 
 
 
 
197
  def __repr__(self) -> str:
198
- return "Unified Miner v1 β€” dual-model vehicle+person"
199
 
200
  # ── Vehicle preprocessing (letterbox) ───────────────────────────────────
201
 
@@ -259,19 +283,31 @@ class Miner:
259
  all_s.append(confs)
260
  all_l.append(out_cls)
261
 
262
- # Pass 1: original
263
- _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
264
- # Pass 2: hflip
265
- flipped = cv2.flip(image_bgr, 1)
266
- bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
267
- if len(bx):
268
- bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
269
- _collect(bx, sc, cl)
 
 
 
 
 
270
 
271
  if not all_b:
272
  return []
273
 
274
- fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
 
 
 
 
 
 
 
275
  if len(fb) == 0:
276
  return []
277
 
@@ -350,19 +386,28 @@ class Miner:
350
  all_b.append(norm)
351
  all_s.append(confs)
352
 
353
- # Pass 1: original
354
- _collect(*self._per_run_pass(image_bgr, PER_TTA_CONF))
355
- # Pass 2: hflip
356
- flipped = cv2.flip(image_bgr, 1)
357
- bx, sc = self._per_run_pass(flipped, PER_TTA_CONF)
358
- if len(bx):
359
- bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
360
- _collect(bx, sc)
 
 
 
 
361
 
362
  if not all_b:
363
  return []
364
 
365
- fb, fs = _wbf_single(all_b, all_s, iou_thr=PER_WBF_IOU, skip_thr=WBF_SKIP_THR)
 
 
 
 
 
366
  if len(fb) == 0:
367
  return []
368
 
@@ -388,21 +433,92 @@ class Miner:
388
  # ── Unified inference ───────────────────────────────────────────────────
389
 
390
  def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
391
- vehicle_boxes = self._infer_vehicle(image_bgr)
392
- person_boxes = self._infer_person(image_bgr)
 
 
 
 
 
 
 
393
  return vehicle_boxes + person_boxes
394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  def predict_batch(
396
  self,
397
  batch_images: list[ndarray],
398
  offset: int,
399
  n_keypoints: int,
400
  ) -> list[TVFrameResult]:
 
 
401
  results: list[TVFrameResult] = []
402
  for idx, image in enumerate(batch_images):
 
403
  boxes = self._infer_single(image)
 
 
 
404
  keypoints = [(0, 0) for _ in range(max(0, int(n_keypoints)))]
405
  results.append(TVFrameResult(
406
  frame_id=offset + idx, boxes=boxes, keypoints=keypoints,
407
  ))
 
 
 
 
 
 
 
 
 
 
 
 
408
  return results
 
1
  """
2
+ Score Vision SN44 β€” Unified miner v2 (2026-03-28).
3
  Dual-model: vehicle (YOLO11s) + person (YOLO11s).
4
+ Optimized for latency: parallel threading + configurable TTA.
5
 
6
  Vehicle model (vehicle_weights.onnx):
7
  Trained classes: 0=car, 1=bus, 2=truck, 3=motorcycle
 
17
 
18
  from pathlib import Path
19
  import math
20
+ import time
21
+ import logging
22
 
23
  import cv2
24
  import numpy as np
 
26
  from numpy import ndarray
27
  from pydantic import BaseModel
28
 
29
+ import json
30
+ import threading
31
+ from datetime import datetime, timezone
32
+ from concurrent.futures import ThreadPoolExecutor, as_completed
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
  # ── Vehicle config ──────────────────────────────────────────────────────────
37
  VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
38
  VEH_NUM_CLASSES = 4
 
50
  # ── Shared ──────────────────────────────────────────────────────────────────
51
  WBF_SKIP_THR = 0.0001
52
 
53
+ # ── Speed config ────────────────────────────────────────────────────────────
54
+ ENABLE_TTA = False # Set True to re-enable 2-pass TTA (doubles inference time)
55
+ ENABLE_PARALLEL = True # Run vehicle + person in parallel threads
56
+
57
 
58
  def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
59
  """Weighted Boxes Fusion (multi-class). Boxes in [0,1] normalized coords."""
 
208
  self.per_h = int(per_shape[2])
209
  self.per_w = int(per_shape[3])
210
 
211
+ # Thread pool for parallel inference
212
+ self._executor = ThreadPoolExecutor(max_workers=2)
213
+
214
+ # Log provider info
215
+ veh_prov = self.veh_session.get_providers()
216
+ per_prov = self.per_session.get_providers()
217
+ logger.info(f"Vehicle ORT providers: {veh_prov}")
218
+ logger.info(f"Person ORT providers: {per_prov}")
219
+ logger.info(f"TTA={ENABLE_TTA} PARALLEL={ENABLE_PARALLEL}")
220
+
221
  def __repr__(self) -> str:
222
+ return "Unified Miner v2 β€” dual-model vehicle+person (parallel, TTA-configurable)"
223
 
224
  # ── Vehicle preprocessing (letterbox) ───────────────────────────────────
225
 
 
283
  all_s.append(confs)
284
  all_l.append(out_cls)
285
 
286
+ if ENABLE_TTA:
287
+ # Pass 1: original
288
+ _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
289
+ # Pass 2: hflip
290
+ flipped = cv2.flip(image_bgr, 1)
291
+ bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
292
+ if len(bx):
293
+ bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
294
+ _collect(bx, sc, cl)
295
+ else:
296
+ # Single pass β€” use per-class conf thresholds directly
297
+ bx, confs, cls_ids = self._veh_run_pass(image_bgr, VEH_TTA_CONF)
298
+ _collect(bx, confs, cls_ids)
299
 
300
  if not all_b:
301
  return []
302
 
303
+ if ENABLE_TTA:
304
+ fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
305
+ else:
306
+ # No WBF needed for single pass, just concatenate
307
+ fb = np.concatenate(all_b, axis=0)
308
+ fs = np.concatenate(all_s, axis=0)
309
+ fl = np.concatenate(all_l, axis=0)
310
+
311
  if len(fb) == 0:
312
  return []
313
 
 
386
  all_b.append(norm)
387
  all_s.append(confs)
388
 
389
+ if ENABLE_TTA:
390
+ # Pass 1: original
391
+ _collect(*self._per_run_pass(image_bgr, PER_TTA_CONF))
392
+ # Pass 2: hflip
393
+ flipped = cv2.flip(image_bgr, 1)
394
+ bx, sc = self._per_run_pass(flipped, PER_TTA_CONF)
395
+ if len(bx):
396
+ bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
397
+ _collect(bx, sc)
398
+ else:
399
+ # Single pass
400
+ _collect(*self._per_run_pass(image_bgr, PER_CONF))
401
 
402
  if not all_b:
403
  return []
404
 
405
+ if ENABLE_TTA:
406
+ fb, fs = _wbf_single(all_b, all_s, iou_thr=PER_WBF_IOU, skip_thr=WBF_SKIP_THR)
407
+ else:
408
+ fb = np.concatenate(all_b, axis=0)
409
+ fs = np.concatenate(all_s, axis=0)
410
+
411
  if len(fb) == 0:
412
  return []
413
 
 
433
  # ── Unified inference ───────────────────────────────────────────────────
434
 
435
  def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
436
+ if ENABLE_PARALLEL:
437
+ # Run both models in parallel threads
438
+ veh_future = self._executor.submit(self._infer_vehicle, image_bgr)
439
+ per_future = self._executor.submit(self._infer_person, image_bgr)
440
+ vehicle_boxes = veh_future.result()
441
+ person_boxes = per_future.result()
442
+ else:
443
+ vehicle_boxes = self._infer_vehicle(image_bgr)
444
+ person_boxes = self._infer_person(image_bgr)
445
  return vehicle_boxes + person_boxes
446
 
447
+ # -- Replay buffer -------------------------------------------------------
448
+ REPLAY_DIR = Path("/home/miner/replay_buffer")
449
+ REPLAY_MAX = 100
450
+
451
+ def _replay_save(self, batch_images, results):
452
+ """Save validator query images + our predictions to replay buffer (background)."""
453
+ try:
454
+ ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
455
+ query_dir = self.REPLAY_DIR / ts
456
+ query_dir.mkdir(parents=True, exist_ok=True)
457
+
458
+ for i, img in enumerate(batch_images):
459
+ cv2.imwrite(str(query_dir / f"img_{i:03d}.jpg"), img,
460
+ [cv2.IMWRITE_JPEG_QUALITY, 95])
461
+
462
+ preds = []
463
+ for r in results:
464
+ preds.append({
465
+ "frame_id": r.frame_id,
466
+ "boxes": [b.model_dump() for b in r.boxes],
467
+ })
468
+ meta = {
469
+ "timestamp": ts,
470
+ "num_images": len(batch_images),
471
+ "image_shapes": [list(img.shape) for img in batch_images],
472
+ "predictions": preds,
473
+ }
474
+ (query_dir / "meta.json").write_text(json.dumps(meta, indent=2))
475
+ self._replay_prune()
476
+ except Exception:
477
+ pass
478
+
479
+ def _replay_prune(self):
480
+ """Keep only the most recent REPLAY_MAX queries."""
481
+ try:
482
+ dirs = sorted(
483
+ [d for d in self.REPLAY_DIR.iterdir() if d.is_dir()],
484
+ key=lambda d: d.name,
485
+ )
486
+ if len(dirs) > self.REPLAY_MAX:
487
+ import shutil
488
+ for old in dirs[: len(dirs) - self.REPLAY_MAX]:
489
+ shutil.rmtree(old, ignore_errors=True)
490
+ except Exception:
491
+ pass
492
+
493
  def predict_batch(
494
  self,
495
  batch_images: list[ndarray],
496
  offset: int,
497
  n_keypoints: int,
498
  ) -> list[TVFrameResult]:
499
+ t_start = time.perf_counter()
500
+
501
  results: list[TVFrameResult] = []
502
  for idx, image in enumerate(batch_images):
503
+ t_img = time.perf_counter()
504
  boxes = self._infer_single(image)
505
+ dt_img = (time.perf_counter() - t_img) * 1000
506
+ logger.info(f"[miner] image {idx}: {len(boxes)} boxes in {dt_img:.0f}ms "
507
+ f"(shape={image.shape}, TTA={ENABLE_TTA}, PAR={ENABLE_PARALLEL})")
508
  keypoints = [(0, 0) for _ in range(max(0, int(n_keypoints)))]
509
  results.append(TVFrameResult(
510
  frame_id=offset + idx, boxes=boxes, keypoints=keypoints,
511
  ))
512
+
513
+ dt_total = (time.perf_counter() - t_start) * 1000
514
+ logger.info(f"[miner] predict_batch: {len(batch_images)} images, "
515
+ f"{sum(len(r.boxes) for r in results)} total boxes, {dt_total:.0f}ms")
516
+
517
+ # Save to replay buffer (background thread)
518
+ threading.Thread(
519
+ target=self._replay_save,
520
+ args=(batch_images, results),
521
+ daemon=True,
522
+ ).start()
523
+
524
  return results
miner.py.bak ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Score Vision SN44 β€” Unified miner v1 (2026-03-27).
3
+ Dual-model: vehicle (YOLO11s) + person (YOLO11s).
4
+
5
+ Vehicle model (vehicle_weights.onnx):
6
+ Trained classes: 0=car, 1=bus, 2=truck, 3=motorcycle
7
+ Remapped to manifest: 0=bus, 1=car, 2=truck, 3=motorcycle
8
+
9
+ Person model (person_weights.onnx):
10
+ Single class: 0=person
11
+
12
+ Both models run on every image. All detections merged.
13
+ cls_id 0 is shared: "bus" for vehicle eval, "person" for person eval.
14
+ Vehicle eval uses cls_id 0-3. Person eval uses cls_id 0 only.
15
+ """
16
+
17
+ from pathlib import Path
18
+ import math
19
+
20
+ import cv2
21
+ import numpy as np
22
+ import onnxruntime as ort
23
+ from numpy import ndarray
24
+ from pydantic import BaseModel
25
+
26
+ # ── Vehicle config ──────────────────────────────────────────────────────────
27
+ VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
28
+ VEH_NUM_CLASSES = 4
29
+ VEH_IMG_SIZE = 1280
30
+ VEH_CONF_PER_CLASS = {0: 0.33, 1: 0.50, 2: 0.40, 3: 0.36}
31
+ VEH_CONF_DEFAULT = 0.35
32
+ VEH_TTA_CONF = 0.25
33
+ VEH_WBF_IOU = 0.55
34
+
35
+ # ── Person config ───────────────────────────────────────────────────────────
36
+ PER_CONF = 0.35
37
+ PER_TTA_CONF = 0.25
38
+ PER_WBF_IOU = 0.45
39
+
40
+ # ── Shared ──────────────────────────────────────────────────────────────────
41
+ WBF_SKIP_THR = 0.0001
42
+
43
+
44
+ def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
45
+ """Weighted Boxes Fusion (multi-class). Boxes in [0,1] normalized coords."""
46
+ if not boxes_list:
47
+ return np.empty((0, 4)), np.empty(0), np.empty(0)
48
+
49
+ all_b, all_s, all_l = [], [], []
50
+ for bx, sc, lb in zip(boxes_list, scores_list, labels_list):
51
+ for i in range(len(bx)):
52
+ if sc[i] < skip_thr:
53
+ continue
54
+ all_b.append(bx[i])
55
+ all_s.append(sc[i])
56
+ all_l.append(int(lb[i]))
57
+
58
+ if not all_b:
59
+ return np.empty((0, 4)), np.empty(0), np.empty(0)
60
+
61
+ all_b = np.array(all_b)
62
+ all_s = np.array(all_s)
63
+ all_l = np.array(all_l, dtype=int)
64
+
65
+ fused_b, fused_s, fused_l = [], [], []
66
+ for cls in np.unique(all_l):
67
+ m = all_l == cls
68
+ cb, cs = all_b[m], all_s[m]
69
+ order = cs.argsort()[::-1]
70
+ cb, cs = cb[order], cs[order]
71
+
72
+ clusters, cboxes = [], []
73
+ for i in range(len(cb)):
74
+ matched, best_iou = -1, iou_thr
75
+ for ci, cbox in enumerate(cboxes):
76
+ xx1 = max(cb[i, 0], cbox[0])
77
+ yy1 = max(cb[i, 1], cbox[1])
78
+ xx2 = min(cb[i, 2], cbox[2])
79
+ yy2 = min(cb[i, 3], cbox[3])
80
+ inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
81
+ a1 = (cb[i, 2] - cb[i, 0]) * (cb[i, 3] - cb[i, 1])
82
+ a2 = (cbox[2] - cbox[0]) * (cbox[3] - cbox[1])
83
+ iou = inter / (a1 + a2 - inter + 1e-9)
84
+ if iou > best_iou:
85
+ best_iou = iou
86
+ matched = ci
87
+ if matched >= 0:
88
+ clusters[matched].append(i)
89
+ idxs = clusters[matched]
90
+ w = cs[idxs]
91
+ cboxes[matched] = (cb[idxs] * w[:, None]).sum(0) / w.sum()
92
+ else:
93
+ clusters.append([i])
94
+ cboxes.append(cb[i].copy())
95
+
96
+ for ci, idxs in enumerate(clusters):
97
+ fused_b.append(cboxes[ci])
98
+ fused_s.append(cs[idxs].mean())
99
+ fused_l.append(cls)
100
+
101
+ if not fused_b:
102
+ return np.empty((0, 4)), np.empty(0), np.empty(0)
103
+ return np.array(fused_b), np.array(fused_s), np.array(fused_l)
104
+
105
+
106
+ def _wbf_single(boxes_list, scores_list, iou_thr=0.45, skip_thr=0.0001):
107
+ """Weighted Boxes Fusion (single-class). Boxes in [0,1] normalized coords."""
108
+ if not boxes_list:
109
+ return np.empty((0, 4)), np.empty(0)
110
+
111
+ all_b, all_s = [], []
112
+ for bx, sc in zip(boxes_list, scores_list):
113
+ for i in range(len(bx)):
114
+ if sc[i] < skip_thr:
115
+ continue
116
+ all_b.append(bx[i])
117
+ all_s.append(sc[i])
118
+
119
+ if not all_b:
120
+ return np.empty((0, 4)), np.empty(0)
121
+
122
+ all_b = np.array(all_b)
123
+ all_s = np.array(all_s)
124
+ order = all_s.argsort()[::-1]
125
+ all_b, all_s = all_b[order], all_s[order]
126
+
127
+ clusters, cboxes = [], []
128
+ for i in range(len(all_b)):
129
+ matched, best_iou = -1, iou_thr
130
+ for ci, cbox in enumerate(cboxes):
131
+ xx1 = max(all_b[i, 0], cbox[0])
132
+ yy1 = max(all_b[i, 1], cbox[1])
133
+ xx2 = min(all_b[i, 2], cbox[2])
134
+ yy2 = min(all_b[i, 3], cbox[3])
135
+ inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
136
+ a1 = (all_b[i, 2] - all_b[i, 0]) * (all_b[i, 3] - all_b[i, 1])
137
+ a2 = (cbox[2] - cbox[0]) * (cbox[3] - cbox[1])
138
+ iou = inter / (a1 + a2 - inter + 1e-9)
139
+ if iou > best_iou:
140
+ best_iou = iou
141
+ matched = ci
142
+ if matched >= 0:
143
+ clusters[matched].append(i)
144
+ idxs = clusters[matched]
145
+ w = all_s[idxs]
146
+ cboxes[matched] = (all_b[idxs] * w[:, None]).sum(0) / w.sum()
147
+ else:
148
+ clusters.append([i])
149
+ cboxes.append(all_b[i].copy())
150
+
151
+ fused_b, fused_s = [], []
152
+ for ci, idxs in enumerate(clusters):
153
+ fused_b.append(cboxes[ci])
154
+ fused_s.append(all_s[idxs].mean())
155
+
156
+ if not fused_b:
157
+ return np.empty((0, 4)), np.empty(0)
158
+ return np.array(fused_b), np.array(fused_s)
159
+
160
+
161
+ class BoundingBox(BaseModel):
162
+ x1: int
163
+ y1: int
164
+ x2: int
165
+ y2: int
166
+ cls_id: int
167
+ conf: float
168
+
169
+
170
+ class TVFrameResult(BaseModel):
171
+ frame_id: int
172
+ boxes: list[BoundingBox]
173
+ keypoints: list[tuple[int, int]]
174
+
175
+
176
+ class Miner:
177
+ def __init__(self, path_hf_repo: Path) -> None:
178
+ self.path_hf_repo = path_hf_repo
179
+
180
+ # Vehicle model (YOLO11s, 4 classes)
181
+ self.veh_session = ort.InferenceSession(
182
+ str(path_hf_repo / "vehicle_weights.onnx"),
183
+ providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
184
+ )
185
+ self.veh_input_name = self.veh_session.get_inputs()[0].name
186
+
187
+ # Person model (YOLO11s, 1 class)
188
+ self.per_session = ort.InferenceSession(
189
+ str(path_hf_repo / "person_weights.onnx"),
190
+ providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
191
+ )
192
+ self.per_input_name = self.per_session.get_inputs()[0].name
193
+ per_shape = self.per_session.get_inputs()[0].shape
194
+ self.per_h = int(per_shape[2])
195
+ self.per_w = int(per_shape[3])
196
+
197
+ def __repr__(self) -> str:
198
+ return "Unified Miner v1 β€” dual-model vehicle+person"
199
+
200
+ # ── Vehicle preprocessing (letterbox) ───────────────────────────────────
201
+
202
+ def _veh_letterbox(self, img):
203
+ h, w = img.shape[:2]
204
+ r = min(VEH_IMG_SIZE / h, VEH_IMG_SIZE / w)
205
+ nw, nh = int(round(w * r)), int(round(h * r))
206
+ img_r = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
207
+ dw, dh = VEH_IMG_SIZE - nw, VEH_IMG_SIZE - nh
208
+ pl, pt = dw // 2, dh // 2
209
+ img_p = cv2.copyMakeBorder(
210
+ img_r, pt, dh - pt, pl, dw - pl,
211
+ cv2.BORDER_CONSTANT, value=(114, 114, 114),
212
+ )
213
+ return img_p, r, pl, pt
214
+
215
+ def _veh_preprocess(self, image_bgr):
216
+ img_p, ratio, pl, pt = self._veh_letterbox(image_bgr)
217
+ rgb = cv2.cvtColor(img_p, cv2.COLOR_BGR2RGB)
218
+ inp = rgb.astype(np.float32) / 255.0
219
+ inp = np.ascontiguousarray(inp.transpose(2, 0, 1)[np.newaxis])
220
+ return inp, ratio, pl, pt
221
+
222
+ def _veh_decode(self, raw, ratio, pl, pt, ow, oh, conf_thresh):
223
+ pred = raw[0]
224
+ if pred.shape[0] < pred.shape[1]:
225
+ pred = pred.T
226
+ cls_scores = pred[:, 4:]
227
+ cls_ids = np.argmax(cls_scores, axis=1)
228
+ confs = np.max(cls_scores, axis=1)
229
+ mask = confs >= conf_thresh
230
+ if not mask.any():
231
+ return np.empty((0, 4)), np.empty(0), np.empty(0, dtype=int)
232
+ bx, confs, cls_ids = pred[mask, :4], confs[mask], cls_ids[mask]
233
+ cx, cy, bw, bh = bx[:, 0], bx[:, 1], bx[:, 2], bx[:, 3]
234
+ x1 = np.clip((cx - bw / 2 - pl) / ratio, 0, ow)
235
+ y1 = np.clip((cy - bh / 2 - pt) / ratio, 0, oh)
236
+ x2 = np.clip((cx + bw / 2 - pl) / ratio, 0, ow)
237
+ y2 = np.clip((cy + bh / 2 - pt) / ratio, 0, oh)
238
+ return np.stack([x1, y1, x2, y2], axis=1), confs, cls_ids
239
+
240
+ def _veh_run_pass(self, image_bgr, conf_thresh):
241
+ oh, ow = image_bgr.shape[:2]
242
+ inp, ratio, pl, pt = self._veh_preprocess(image_bgr)
243
+ raw = self.veh_session.run(None, {self.veh_input_name: inp})[0]
244
+ return self._veh_decode(raw, ratio, pl, pt, ow, oh, conf_thresh)
245
+
246
+ def _infer_vehicle(self, image_bgr):
247
+ oh, ow = image_bgr.shape[:2]
248
+ all_b, all_s, all_l = [], [], []
249
+
250
+ def _collect(boxes, confs, cls_ids):
251
+ if len(boxes) == 0:
252
+ return
253
+ out_cls = np.array([VEH_MODEL_TO_OUT[int(c)] for c in cls_ids])
254
+ norm = boxes.copy()
255
+ norm[:, [0, 2]] /= ow
256
+ norm[:, [1, 3]] /= oh
257
+ norm = np.clip(norm, 0, 1)
258
+ all_b.append(norm)
259
+ all_s.append(confs)
260
+ all_l.append(out_cls)
261
+
262
+ # Pass 1: original
263
+ _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
264
+ # Pass 2: hflip
265
+ flipped = cv2.flip(image_bgr, 1)
266
+ bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
267
+ if len(bx):
268
+ bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
269
+ _collect(bx, sc, cl)
270
+
271
+ if not all_b:
272
+ return []
273
+
274
+ fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
275
+ if len(fb) == 0:
276
+ return []
277
+
278
+ fb[:, [0, 2]] *= ow
279
+ fb[:, [1, 3]] *= oh
280
+
281
+ keep = np.array([
282
+ fs[i] >= VEH_CONF_PER_CLASS.get(int(fl[i]), VEH_CONF_DEFAULT)
283
+ for i in range(len(fs))
284
+ ])
285
+ if not keep.any():
286
+ return []
287
+ fb, fs, fl = fb[keep], fs[keep], fl[keep]
288
+
289
+ out = []
290
+ for i in range(len(fb)):
291
+ b = fb[i]
292
+ out.append(BoundingBox(
293
+ x1=max(0, min(ow, math.floor(b[0]))),
294
+ y1=max(0, min(oh, math.floor(b[1]))),
295
+ x2=max(0, min(ow, math.ceil(b[2]))),
296
+ y2=max(0, min(oh, math.ceil(b[3]))),
297
+ cls_id=int(fl[i]),
298
+ conf=max(0.0, min(1.0, float(fs[i]))),
299
+ ))
300
+ return out
301
+
302
+ # ── Person preprocessing (stretch resize) ──────────────────────────────
303
+
304
+ def _per_preprocess(self, image_bgr):
305
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
306
+ resized = cv2.resize(rgb, (self.per_w, self.per_h))
307
+ x = resized.astype(np.float32) / 255.0
308
+ x = np.transpose(x, (2, 0, 1))[None, ...]
309
+ return x
310
+
311
+ def _per_decode(self, raw, oh, ow, conf_thresh):
312
+ pred = raw[0]
313
+ if pred.ndim != 2:
314
+ return np.empty((0, 4)), np.empty(0)
315
+ if pred.shape[0] < pred.shape[1]:
316
+ pred = pred.T
317
+ if pred.shape[1] < 5:
318
+ return np.empty((0, 4)), np.empty(0)
319
+ cls_scores = pred[:, 4:]
320
+ confs = np.max(cls_scores, axis=1)
321
+ keep = confs >= conf_thresh
322
+ boxes, confs = pred[keep, :4], confs[keep]
323
+ if len(boxes) == 0:
324
+ return np.empty((0, 4)), np.empty(0)
325
+ sx, sy = ow / float(self.per_w), oh / float(self.per_h)
326
+ cx, cy, bw, bh = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
327
+ x1 = np.clip((cx - bw / 2) * sx, 0, ow)
328
+ y1 = np.clip((cy - bh / 2) * sy, 0, oh)
329
+ x2 = np.clip((cx + bw / 2) * sx, 0, ow)
330
+ y2 = np.clip((cy + bh / 2) * sy, 0, oh)
331
+ return np.stack([x1, y1, x2, y2], axis=1), confs
332
+
333
+ def _per_run_pass(self, image_bgr, conf_thresh):
334
+ oh, ow = image_bgr.shape[:2]
335
+ inp = self._per_preprocess(image_bgr)
336
+ raw = self.per_session.run(None, {self.per_input_name: inp})[0]
337
+ return self._per_decode(raw, oh, ow, conf_thresh)
338
+
339
+ def _infer_person(self, image_bgr):
340
+ oh, ow = image_bgr.shape[:2]
341
+ all_b, all_s = [], []
342
+
343
+ def _collect(boxes, confs):
344
+ if len(boxes) == 0:
345
+ return
346
+ norm = boxes.copy()
347
+ norm[:, [0, 2]] /= ow
348
+ norm[:, [1, 3]] /= oh
349
+ norm = np.clip(norm, 0, 1)
350
+ all_b.append(norm)
351
+ all_s.append(confs)
352
+
353
+ # Pass 1: original
354
+ _collect(*self._per_run_pass(image_bgr, PER_TTA_CONF))
355
+ # Pass 2: hflip
356
+ flipped = cv2.flip(image_bgr, 1)
357
+ bx, sc = self._per_run_pass(flipped, PER_TTA_CONF)
358
+ if len(bx):
359
+ bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
360
+ _collect(bx, sc)
361
+
362
+ if not all_b:
363
+ return []
364
+
365
+ fb, fs = _wbf_single(all_b, all_s, iou_thr=PER_WBF_IOU, skip_thr=WBF_SKIP_THR)
366
+ if len(fb) == 0:
367
+ return []
368
+
369
+ fb[:, [0, 2]] *= ow
370
+ fb[:, [1, 3]] *= oh
371
+
372
+ keep = fs >= PER_CONF
373
+ fb, fs = fb[keep], fs[keep]
374
+
375
+ out = []
376
+ for i in range(len(fb)):
377
+ b = fb[i]
378
+ out.append(BoundingBox(
379
+ x1=max(0, min(ow, math.floor(b[0]))),
380
+ y1=max(0, min(oh, math.floor(b[1]))),
381
+ x2=max(0, min(ow, math.ceil(b[2]))),
382
+ y2=max(0, min(oh, math.ceil(b[3]))),
383
+ cls_id=0,
384
+ conf=max(0.0, min(1.0, float(fs[i]))),
385
+ ))
386
+ return out
387
+
388
+ # ── Unified inference ───────────────────────────────────────────────────
389
+
390
+ def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
391
+ vehicle_boxes = self._infer_vehicle(image_bgr)
392
+ person_boxes = self._infer_person(image_bgr)
393
+ return vehicle_boxes + person_boxes
394
+
395
+ def predict_batch(
396
+ self,
397
+ batch_images: list[ndarray],
398
+ offset: int,
399
+ n_keypoints: int,
400
+ ) -> list[TVFrameResult]:
401
+ results: list[TVFrameResult] = []
402
+ for idx, image in enumerate(batch_images):
403
+ boxes = self._infer_single(image)
404
+ keypoints = [(0, 0) for _ in range(max(0, int(n_keypoints)))]
405
+ results.append(TVFrameResult(
406
+ frame_id=offset + idx, boxes=boxes, keypoints=keypoints,
407
+ ))
408
+ return results
miner.py.bak_v1 ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Score Vision SN44 β€” Unified miner v1 (2026-03-27).
3
+ Dual-model: vehicle (YOLO11s) + person (YOLO11s).
4
+
5
+ Vehicle model (vehicle_weights.onnx):
6
+ Trained classes: 0=car, 1=bus, 2=truck, 3=motorcycle
7
+ Remapped to manifest: 0=bus, 1=car, 2=truck, 3=motorcycle
8
+
9
+ Person model (person_weights.onnx):
10
+ Single class: 0=person
11
+
12
+ Both models run on every image. All detections merged.
13
+ cls_id 0 is shared: "bus" for vehicle eval, "person" for person eval.
14
+ Vehicle eval uses cls_id 0-3. Person eval uses cls_id 0 only.
15
+ """
16
+
17
+ from pathlib import Path
18
+ import math
19
+
20
+ import cv2
21
+ import numpy as np
22
+ import onnxruntime as ort
23
+ from numpy import ndarray
24
+ from pydantic import BaseModel
25
+
26
+ import json
27
+ import threading
28
+ from datetime import datetime, timezone
29
+
30
+ # ── Vehicle config ──────────────────────────────────────────────────────────
31
+ VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
32
+ VEH_NUM_CLASSES = 4
33
+ VEH_IMG_SIZE = 1280
34
+ VEH_CONF_PER_CLASS = {0: 0.33, 1: 0.50, 2: 0.40, 3: 0.36}
35
+ VEH_CONF_DEFAULT = 0.35
36
+ VEH_TTA_CONF = 0.25
37
+ VEH_WBF_IOU = 0.55
38
+
39
+ # ── Person config ───────────────────────────────────────────────────────────
40
+ PER_CONF = 0.35
41
+ PER_TTA_CONF = 0.25
42
+ PER_WBF_IOU = 0.45
43
+
44
+ # ── Shared ──────────────────────────────────────────────────────────────────
45
+ WBF_SKIP_THR = 0.0001
46
+
47
+
48
+ def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
49
+ """Weighted Boxes Fusion (multi-class). Boxes in [0,1] normalized coords."""
50
+ if not boxes_list:
51
+ return np.empty((0, 4)), np.empty(0), np.empty(0)
52
+
53
+ all_b, all_s, all_l = [], [], []
54
+ for bx, sc, lb in zip(boxes_list, scores_list, labels_list):
55
+ for i in range(len(bx)):
56
+ if sc[i] < skip_thr:
57
+ continue
58
+ all_b.append(bx[i])
59
+ all_s.append(sc[i])
60
+ all_l.append(int(lb[i]))
61
+
62
+ if not all_b:
63
+ return np.empty((0, 4)), np.empty(0), np.empty(0)
64
+
65
+ all_b = np.array(all_b)
66
+ all_s = np.array(all_s)
67
+ all_l = np.array(all_l, dtype=int)
68
+
69
+ fused_b, fused_s, fused_l = [], [], []
70
+ for cls in np.unique(all_l):
71
+ m = all_l == cls
72
+ cb, cs = all_b[m], all_s[m]
73
+ order = cs.argsort()[::-1]
74
+ cb, cs = cb[order], cs[order]
75
+
76
+ clusters, cboxes = [], []
77
+ for i in range(len(cb)):
78
+ matched, best_iou = -1, iou_thr
79
+ for ci, cbox in enumerate(cboxes):
80
+ xx1 = max(cb[i, 0], cbox[0])
81
+ yy1 = max(cb[i, 1], cbox[1])
82
+ xx2 = min(cb[i, 2], cbox[2])
83
+ yy2 = min(cb[i, 3], cbox[3])
84
+ inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
85
+ a1 = (cb[i, 2] - cb[i, 0]) * (cb[i, 3] - cb[i, 1])
86
+ a2 = (cbox[2] - cbox[0]) * (cbox[3] - cbox[1])
87
+ iou = inter / (a1 + a2 - inter + 1e-9)
88
+ if iou > best_iou:
89
+ best_iou = iou
90
+ matched = ci
91
+ if matched >= 0:
92
+ clusters[matched].append(i)
93
+ idxs = clusters[matched]
94
+ w = cs[idxs]
95
+ cboxes[matched] = (cb[idxs] * w[:, None]).sum(0) / w.sum()
96
+ else:
97
+ clusters.append([i])
98
+ cboxes.append(cb[i].copy())
99
+
100
+ for ci, idxs in enumerate(clusters):
101
+ fused_b.append(cboxes[ci])
102
+ fused_s.append(cs[idxs].mean())
103
+ fused_l.append(cls)
104
+
105
+ if not fused_b:
106
+ return np.empty((0, 4)), np.empty(0), np.empty(0)
107
+ return np.array(fused_b), np.array(fused_s), np.array(fused_l)
108
+
109
+
110
+ def _wbf_single(boxes_list, scores_list, iou_thr=0.45, skip_thr=0.0001):
111
+ """Weighted Boxes Fusion (single-class). Boxes in [0,1] normalized coords."""
112
+ if not boxes_list:
113
+ return np.empty((0, 4)), np.empty(0)
114
+
115
+ all_b, all_s = [], []
116
+ for bx, sc in zip(boxes_list, scores_list):
117
+ for i in range(len(bx)):
118
+ if sc[i] < skip_thr:
119
+ continue
120
+ all_b.append(bx[i])
121
+ all_s.append(sc[i])
122
+
123
+ if not all_b:
124
+ return np.empty((0, 4)), np.empty(0)
125
+
126
+ all_b = np.array(all_b)
127
+ all_s = np.array(all_s)
128
+ order = all_s.argsort()[::-1]
129
+ all_b, all_s = all_b[order], all_s[order]
130
+
131
+ clusters, cboxes = [], []
132
+ for i in range(len(all_b)):
133
+ matched, best_iou = -1, iou_thr
134
+ for ci, cbox in enumerate(cboxes):
135
+ xx1 = max(all_b[i, 0], cbox[0])
136
+ yy1 = max(all_b[i, 1], cbox[1])
137
+ xx2 = min(all_b[i, 2], cbox[2])
138
+ yy2 = min(all_b[i, 3], cbox[3])
139
+ inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
140
+ a1 = (all_b[i, 2] - all_b[i, 0]) * (all_b[i, 3] - all_b[i, 1])
141
+ a2 = (cbox[2] - cbox[0]) * (cbox[3] - cbox[1])
142
+ iou = inter / (a1 + a2 - inter + 1e-9)
143
+ if iou > best_iou:
144
+ best_iou = iou
145
+ matched = ci
146
+ if matched >= 0:
147
+ clusters[matched].append(i)
148
+ idxs = clusters[matched]
149
+ w = all_s[idxs]
150
+ cboxes[matched] = (all_b[idxs] * w[:, None]).sum(0) / w.sum()
151
+ else:
152
+ clusters.append([i])
153
+ cboxes.append(all_b[i].copy())
154
+
155
+ fused_b, fused_s = [], []
156
+ for ci, idxs in enumerate(clusters):
157
+ fused_b.append(cboxes[ci])
158
+ fused_s.append(all_s[idxs].mean())
159
+
160
+ if not fused_b:
161
+ return np.empty((0, 4)), np.empty(0)
162
+ return np.array(fused_b), np.array(fused_s)
163
+
164
+
165
+ class BoundingBox(BaseModel):
166
+ x1: int
167
+ y1: int
168
+ x2: int
169
+ y2: int
170
+ cls_id: int
171
+ conf: float
172
+
173
+
174
+ class TVFrameResult(BaseModel):
175
+ frame_id: int
176
+ boxes: list[BoundingBox]
177
+ keypoints: list[tuple[int, int]]
178
+
179
+
180
+ class Miner:
181
+ def __init__(self, path_hf_repo: Path) -> None:
182
+ self.path_hf_repo = path_hf_repo
183
+
184
+ # Vehicle model (YOLO11s, 4 classes)
185
+ self.veh_session = ort.InferenceSession(
186
+ str(path_hf_repo / "vehicle_weights.onnx"),
187
+ providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
188
+ )
189
+ self.veh_input_name = self.veh_session.get_inputs()[0].name
190
+
191
+ # Person model (YOLO11s, 1 class)
192
+ self.per_session = ort.InferenceSession(
193
+ str(path_hf_repo / "person_weights.onnx"),
194
+ providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
195
+ )
196
+ self.per_input_name = self.per_session.get_inputs()[0].name
197
+ per_shape = self.per_session.get_inputs()[0].shape
198
+ self.per_h = int(per_shape[2])
199
+ self.per_w = int(per_shape[3])
200
+
201
+ def __repr__(self) -> str:
202
+ return "Unified Miner v1 β€” dual-model vehicle+person"
203
+
204
+ # ── Vehicle preprocessing (letterbox) ───────────────────────────────────
205
+
206
+ def _veh_letterbox(self, img):
207
+ h, w = img.shape[:2]
208
+ r = min(VEH_IMG_SIZE / h, VEH_IMG_SIZE / w)
209
+ nw, nh = int(round(w * r)), int(round(h * r))
210
+ img_r = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
211
+ dw, dh = VEH_IMG_SIZE - nw, VEH_IMG_SIZE - nh
212
+ pl, pt = dw // 2, dh // 2
213
+ img_p = cv2.copyMakeBorder(
214
+ img_r, pt, dh - pt, pl, dw - pl,
215
+ cv2.BORDER_CONSTANT, value=(114, 114, 114),
216
+ )
217
+ return img_p, r, pl, pt
218
+
219
+ def _veh_preprocess(self, image_bgr):
220
+ img_p, ratio, pl, pt = self._veh_letterbox(image_bgr)
221
+ rgb = cv2.cvtColor(img_p, cv2.COLOR_BGR2RGB)
222
+ inp = rgb.astype(np.float32) / 255.0
223
+ inp = np.ascontiguousarray(inp.transpose(2, 0, 1)[np.newaxis])
224
+ return inp, ratio, pl, pt
225
+
226
+ def _veh_decode(self, raw, ratio, pl, pt, ow, oh, conf_thresh):
227
+ pred = raw[0]
228
+ if pred.shape[0] < pred.shape[1]:
229
+ pred = pred.T
230
+ cls_scores = pred[:, 4:]
231
+ cls_ids = np.argmax(cls_scores, axis=1)
232
+ confs = np.max(cls_scores, axis=1)
233
+ mask = confs >= conf_thresh
234
+ if not mask.any():
235
+ return np.empty((0, 4)), np.empty(0), np.empty(0, dtype=int)
236
+ bx, confs, cls_ids = pred[mask, :4], confs[mask], cls_ids[mask]
237
+ cx, cy, bw, bh = bx[:, 0], bx[:, 1], bx[:, 2], bx[:, 3]
238
+ x1 = np.clip((cx - bw / 2 - pl) / ratio, 0, ow)
239
+ y1 = np.clip((cy - bh / 2 - pt) / ratio, 0, oh)
240
+ x2 = np.clip((cx + bw / 2 - pl) / ratio, 0, ow)
241
+ y2 = np.clip((cy + bh / 2 - pt) / ratio, 0, oh)
242
+ return np.stack([x1, y1, x2, y2], axis=1), confs, cls_ids
243
+
244
+ def _veh_run_pass(self, image_bgr, conf_thresh):
245
+ oh, ow = image_bgr.shape[:2]
246
+ inp, ratio, pl, pt = self._veh_preprocess(image_bgr)
247
+ raw = self.veh_session.run(None, {self.veh_input_name: inp})[0]
248
+ return self._veh_decode(raw, ratio, pl, pt, ow, oh, conf_thresh)
249
+
250
+ def _infer_vehicle(self, image_bgr):
251
+ oh, ow = image_bgr.shape[:2]
252
+ all_b, all_s, all_l = [], [], []
253
+
254
+ def _collect(boxes, confs, cls_ids):
255
+ if len(boxes) == 0:
256
+ return
257
+ out_cls = np.array([VEH_MODEL_TO_OUT[int(c)] for c in cls_ids])
258
+ norm = boxes.copy()
259
+ norm[:, [0, 2]] /= ow
260
+ norm[:, [1, 3]] /= oh
261
+ norm = np.clip(norm, 0, 1)
262
+ all_b.append(norm)
263
+ all_s.append(confs)
264
+ all_l.append(out_cls)
265
+
266
+ # Pass 1: original
267
+ _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
268
+ # Pass 2: hflip
269
+ flipped = cv2.flip(image_bgr, 1)
270
+ bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
271
+ if len(bx):
272
+ bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
273
+ _collect(bx, sc, cl)
274
+
275
+ if not all_b:
276
+ return []
277
+
278
+ fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
279
+ if len(fb) == 0:
280
+ return []
281
+
282
+ fb[:, [0, 2]] *= ow
283
+ fb[:, [1, 3]] *= oh
284
+
285
+ keep = np.array([
286
+ fs[i] >= VEH_CONF_PER_CLASS.get(int(fl[i]), VEH_CONF_DEFAULT)
287
+ for i in range(len(fs))
288
+ ])
289
+ if not keep.any():
290
+ return []
291
+ fb, fs, fl = fb[keep], fs[keep], fl[keep]
292
+
293
+ out = []
294
+ for i in range(len(fb)):
295
+ b = fb[i]
296
+ out.append(BoundingBox(
297
+ x1=max(0, min(ow, math.floor(b[0]))),
298
+ y1=max(0, min(oh, math.floor(b[1]))),
299
+ x2=max(0, min(ow, math.ceil(b[2]))),
300
+ y2=max(0, min(oh, math.ceil(b[3]))),
301
+ cls_id=int(fl[i]),
302
+ conf=max(0.0, min(1.0, float(fs[i]))),
303
+ ))
304
+ return out
305
+
306
+ # ── Person preprocessing (stretch resize) ──────────────────────────────
307
+
308
+ def _per_preprocess(self, image_bgr):
309
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
310
+ resized = cv2.resize(rgb, (self.per_w, self.per_h))
311
+ x = resized.astype(np.float32) / 255.0
312
+ x = np.transpose(x, (2, 0, 1))[None, ...]
313
+ return x
314
+
315
+ def _per_decode(self, raw, oh, ow, conf_thresh):
316
+ pred = raw[0]
317
+ if pred.ndim != 2:
318
+ return np.empty((0, 4)), np.empty(0)
319
+ if pred.shape[0] < pred.shape[1]:
320
+ pred = pred.T
321
+ if pred.shape[1] < 5:
322
+ return np.empty((0, 4)), np.empty(0)
323
+ cls_scores = pred[:, 4:]
324
+ confs = np.max(cls_scores, axis=1)
325
+ keep = confs >= conf_thresh
326
+ boxes, confs = pred[keep, :4], confs[keep]
327
+ if len(boxes) == 0:
328
+ return np.empty((0, 4)), np.empty(0)
329
+ sx, sy = ow / float(self.per_w), oh / float(self.per_h)
330
+ cx, cy, bw, bh = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
331
+ x1 = np.clip((cx - bw / 2) * sx, 0, ow)
332
+ y1 = np.clip((cy - bh / 2) * sy, 0, oh)
333
+ x2 = np.clip((cx + bw / 2) * sx, 0, ow)
334
+ y2 = np.clip((cy + bh / 2) * sy, 0, oh)
335
+ return np.stack([x1, y1, x2, y2], axis=1), confs
336
+
337
+ def _per_run_pass(self, image_bgr, conf_thresh):
338
+ oh, ow = image_bgr.shape[:2]
339
+ inp = self._per_preprocess(image_bgr)
340
+ raw = self.per_session.run(None, {self.per_input_name: inp})[0]
341
+ return self._per_decode(raw, oh, ow, conf_thresh)
342
+
343
+ def _infer_person(self, image_bgr):
344
+ oh, ow = image_bgr.shape[:2]
345
+ all_b, all_s = [], []
346
+
347
+ def _collect(boxes, confs):
348
+ if len(boxes) == 0:
349
+ return
350
+ norm = boxes.copy()
351
+ norm[:, [0, 2]] /= ow
352
+ norm[:, [1, 3]] /= oh
353
+ norm = np.clip(norm, 0, 1)
354
+ all_b.append(norm)
355
+ all_s.append(confs)
356
+
357
+ # Pass 1: original
358
+ _collect(*self._per_run_pass(image_bgr, PER_TTA_CONF))
359
+ # Pass 2: hflip
360
+ flipped = cv2.flip(image_bgr, 1)
361
+ bx, sc = self._per_run_pass(flipped, PER_TTA_CONF)
362
+ if len(bx):
363
+ bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
364
+ _collect(bx, sc)
365
+
366
+ if not all_b:
367
+ return []
368
+
369
+ fb, fs = _wbf_single(all_b, all_s, iou_thr=PER_WBF_IOU, skip_thr=WBF_SKIP_THR)
370
+ if len(fb) == 0:
371
+ return []
372
+
373
+ fb[:, [0, 2]] *= ow
374
+ fb[:, [1, 3]] *= oh
375
+
376
+ keep = fs >= PER_CONF
377
+ fb, fs = fb[keep], fs[keep]
378
+
379
+ out = []
380
+ for i in range(len(fb)):
381
+ b = fb[i]
382
+ out.append(BoundingBox(
383
+ x1=max(0, min(ow, math.floor(b[0]))),
384
+ y1=max(0, min(oh, math.floor(b[1]))),
385
+ x2=max(0, min(ow, math.ceil(b[2]))),
386
+ y2=max(0, min(oh, math.ceil(b[3]))),
387
+ cls_id=0,
388
+ conf=max(0.0, min(1.0, float(fs[i]))),
389
+ ))
390
+ return out
391
+
392
+ # ── Unified inference ───────────────────────────────────────────────────
393
+
394
+ def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
395
+ vehicle_boxes = self._infer_vehicle(image_bgr)
396
+ person_boxes = self._infer_person(image_bgr)
397
+ return vehicle_boxes + person_boxes
398
+
399
+
400
+ # -- Replay buffer -------------------------------------------------------
401
+ REPLAY_DIR = Path("/home/miner/replay_buffer")
402
+ REPLAY_MAX = 100
403
+
404
+ def _replay_save(self, batch_images, results):
405
+ """Save validator query images + our predictions to replay buffer (background)."""
406
+ try:
407
+ ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f")
408
+ query_dir = self.REPLAY_DIR / ts
409
+ query_dir.mkdir(parents=True, exist_ok=True)
410
+
411
+ # Save each image as JPEG
412
+ for i, img in enumerate(batch_images):
413
+ cv2.imwrite(str(query_dir / f"img_{i:03d}.jpg"), img,
414
+ [cv2.IMWRITE_JPEG_QUALITY, 95])
415
+
416
+ # Save predictions as JSON
417
+ preds = []
418
+ for r in results:
419
+ preds.append({
420
+ "frame_id": r.frame_id,
421
+ "boxes": [b.model_dump() for b in r.boxes],
422
+ })
423
+ meta = {
424
+ "timestamp": ts,
425
+ "num_images": len(batch_images),
426
+ "image_shapes": [list(img.shape) for img in batch_images],
427
+ "predictions": preds,
428
+ }
429
+ (query_dir / "meta.json").write_text(json.dumps(meta, indent=2))
430
+
431
+ # Prune old entries
432
+ self._replay_prune()
433
+ except Exception:
434
+ pass # never break inference
435
+
436
+ def _replay_prune(self):
437
+ """Keep only the most recent REPLAY_MAX queries."""
438
+ try:
439
+ dirs = sorted(
440
+ [d for d in self.REPLAY_DIR.iterdir() if d.is_dir()],
441
+ key=lambda d: d.name,
442
+ )
443
+ if len(dirs) > self.REPLAY_MAX:
444
+ import shutil
445
+ for old in dirs[: len(dirs) - self.REPLAY_MAX]:
446
+ shutil.rmtree(old, ignore_errors=True)
447
+ except Exception:
448
+ pass
449
+
450
+ def predict_batch(
451
+ self,
452
+ batch_images: list[ndarray],
453
+ offset: int,
454
+ n_keypoints: int,
455
+ ) -> list[TVFrameResult]:
456
+ results: list[TVFrameResult] = []
457
+ for idx, image in enumerate(batch_images):
458
+ boxes = self._infer_single(image)
459
+ keypoints = [(0, 0) for _ in range(max(0, int(n_keypoints)))]
460
+ results.append(TVFrameResult(
461
+ frame_id=offset + idx, boxes=boxes, keypoints=keypoints,
462
+ ))
463
+ # Save to replay buffer (background thread -- no latency impact)
464
+ threading.Thread(
465
+ target=self._replay_save,
466
+ args=(batch_images, results),
467
+ daemon=True,
468
+ ).start()
469
+
470
+ return results