v3 weights (yolo11s, 204k merged plate dataset, val mAP50=0.944)
Browse files
miner.py
CHANGED
|
@@ -155,18 +155,26 @@ class Miner:
|
|
| 155 |
self._onnx_declared_h = _maybe_int(input_shape[2], None)
|
| 156 |
self._onnx_declared_w = _maybe_int(input_shape[3], None)
|
| 157 |
|
| 158 |
-
# Pre-NMS confidence threshold.
|
| 159 |
-
#
|
| 160 |
-
#
|
| 161 |
-
#
|
| 162 |
-
#
|
| 163 |
-
|
|
|
|
| 164 |
# Soft-NMS hyperparameters (Gaussian variant).
|
| 165 |
self.soft_nms_sigma = 0.5
|
| 166 |
-
# Final score floor after Soft-NMS decay.
|
| 167 |
-
#
|
| 168 |
-
#
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
# GPU warmup — force ORT / CUDA / cuDNN kernel compilation and pull
|
| 172 |
# the 4090 out of low-power idle state so the first real validator
|
|
@@ -351,23 +359,12 @@ class Miner:
|
|
| 351 |
dets.append((xa, ya, xb, yb, float(confs[i]), int(cls_ids[i])))
|
| 352 |
return dets
|
| 353 |
|
| 354 |
-
def
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
(top/bottom only) missed — especially the 5-7 px plates in
|
| 361 |
-
image 6 that need vertical AND horizontal magnification.
|
| 362 |
-
|
| 363 |
-
Overlap is ~10% on each axis to avoid seam misses. All tile
|
| 364 |
-
detections are merged via Soft-NMS.
|
| 365 |
-
|
| 366 |
-
Measured on the 7 starter frames vs TB-2:
|
| 367 |
-
mAP@50 0.406 -> 0.489
|
| 368 |
-
recall 0.433 -> 0.500
|
| 369 |
-
wall p95 55 ms -> 98 ms (budget 10 s)
|
| 370 |
-
"""
|
| 371 |
orig_h, orig_w = image_bgr.shape[:2]
|
| 372 |
OVERLAP_X = 70 # ~10% of 1408/2
|
| 373 |
OVERLAP_Y = 38 # ~10% of 768/2
|
|
@@ -380,10 +377,43 @@ class Miner:
|
|
| 380 |
(0, max(0, my - OVERLAP_Y), min(orig_w, mx + OVERLAP_X), orig_h), # BL
|
| 381 |
(max(0, mx - OVERLAP_X), max(0, my - OVERLAP_Y), orig_w, orig_h), # BR
|
| 382 |
]
|
| 383 |
-
|
| 384 |
-
all_dets = []
|
| 385 |
for x0, y0, x1, y1 in tiles:
|
| 386 |
all_dets.extend(self._infer_tile(image_bgr, x0, y0, x1, y1))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
dets = self._soft_nms(all_dets)
|
| 389 |
|
|
|
|
| 155 |
self._onnx_declared_h = _maybe_int(input_shape[2], None)
|
| 156 |
self._onnx_declared_w = _maybe_int(input_shape[3], None)
|
| 157 |
|
| 158 |
+
# Pre-NMS confidence threshold. Top-of-leaderboard miners (Cargile,
|
| 159 |
+
# alfred8995) run 0.16-0.21 with a LOW post-NMS floor (0.01) and TTA.
|
| 160 |
+
# Bench on 12 archived validator tasks (12-task consensus set) at
|
| 161 |
+
# conf=0.16, score_threshold=0.01 with our v3 ONNX: HIGH recall
|
| 162 |
+
# 57% -> 66% (+3 plates) at the cost of +2 singletons. Net positive
|
| 163 |
+
# given 0.6 weight on map50 vs 0.4 on false_positive in composite.
|
| 164 |
+
self.conf_threshold = 0.16
|
| 165 |
# Soft-NMS hyperparameters (Gaussian variant).
|
| 166 |
self.soft_nms_sigma = 0.5
|
| 167 |
+
# Final score floor after Soft-NMS decay. Was 0.20 — raised threshold
|
| 168 |
+
# killed decayed real plates (e.g. plate adjacent to a higher-conf
|
| 169 |
+
# detection gets decayed below 0.20 and dropped). Matches competitor
|
| 170 |
+
# 0.01 floor; Soft-NMS still prevents wild duplicates via decay.
|
| 171 |
+
self.score_threshold = 0.01
|
| 172 |
+
|
| 173 |
+
# Horizontal-flip TTA. Doubles inference cost (~101ms -> ~200ms at
|
| 174 |
+
# batch=1) but we have ~10s budget per-frame, massive headroom. Both
|
| 175 |
+
# top miners (Cargile, alfred8995) use TTA — the extra view helps
|
| 176 |
+
# catch plates the model is directionally biased against.
|
| 177 |
+
self.use_tta = True
|
| 178 |
|
| 179 |
# GPU warmup — force ORT / CUDA / cuDNN kernel compilation and pull
|
| 180 |
# the 4090 out of low-power idle state so the first real validator
|
|
|
|
| 359 |
dets.append((xa, ya, xb, yb, float(confs[i]), int(cls_ids[i])))
|
| 360 |
return dets
|
| 361 |
|
| 362 |
+
def _quad4_raw_dets(
|
| 363 |
+
self,
|
| 364 |
+
image_bgr: ndarray,
|
| 365 |
+
) -> list[tuple[float, float, float, float, float, int]]:
|
| 366 |
+
"""Run the quad-4 tile pipeline and return RAW (pre-Soft-NMS)
|
| 367 |
+
detections in original-image coordinates."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
orig_h, orig_w = image_bgr.shape[:2]
|
| 369 |
OVERLAP_X = 70 # ~10% of 1408/2
|
| 370 |
OVERLAP_Y = 38 # ~10% of 768/2
|
|
|
|
| 377 |
(0, max(0, my - OVERLAP_Y), min(orig_w, mx + OVERLAP_X), orig_h), # BL
|
| 378 |
(max(0, mx - OVERLAP_X), max(0, my - OVERLAP_Y), orig_w, orig_h), # BR
|
| 379 |
]
|
| 380 |
+
all_dets: list[tuple[float, float, float, float, float, int]] = []
|
|
|
|
| 381 |
for x0, y0, x1, y1 in tiles:
|
| 382 |
all_dets.extend(self._infer_tile(image_bgr, x0, y0, x1, y1))
|
| 383 |
+
return all_dets
|
| 384 |
+
|
| 385 |
+
def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
|
| 386 |
+
"""Quad-4 (2x2 quadrant) SAHI inference with optional horizontal-flip TTA.
|
| 387 |
+
|
| 388 |
+
Splits the frame into four overlapping quadrants, each
|
| 389 |
+
anisotropically resized to ``(input_h, input_w)`` for ~2x
|
| 390 |
+
magnification in both axes. Overlap is ~10% on each axis.
|
| 391 |
+
All tile detections are merged via Soft-NMS.
|
| 392 |
+
|
| 393 |
+
With ``self.use_tta=True``: additionally runs the same quad-4 pass
|
| 394 |
+
on a horizontally flipped copy and un-flips the x-coordinates back
|
| 395 |
+
into original space. Soft-NMS then merges across both views,
|
| 396 |
+
preferring the higher-confidence one for any paired detection.
|
| 397 |
+
|
| 398 |
+
Measured (quad-4 without TTA) on 7 starter frames vs TB-2:
|
| 399 |
+
mAP@50 0.406 -> 0.489
|
| 400 |
+
recall 0.433 -> 0.500
|
| 401 |
+
wall p95 55 ms -> 98 ms
|
| 402 |
+
|
| 403 |
+
TTA roughly doubles inference cost (budget: 10 s).
|
| 404 |
+
"""
|
| 405 |
+
orig_h, orig_w = image_bgr.shape[:2]
|
| 406 |
+
|
| 407 |
+
all_dets = self._quad4_raw_dets(image_bgr)
|
| 408 |
+
|
| 409 |
+
if self.use_tta:
|
| 410 |
+
flipped = cv2.flip(image_bgr, 1) # horizontal flip (mirror)
|
| 411 |
+
flip_dets = self._quad4_raw_dets(flipped)
|
| 412 |
+
# Un-flip x-coordinates: x_orig = W - x_flipped
|
| 413 |
+
for x1f, y1, x2f, y2, conf, cls_id in flip_dets:
|
| 414 |
+
all_dets.append(
|
| 415 |
+
(orig_w - x2f, y1, orig_w - x1f, y2, conf, cls_id)
|
| 416 |
+
)
|
| 417 |
|
| 418 |
dets = self._soft_nms(all_dets)
|
| 419 |
|