v8 ep-10 YOLO26s weights + shape-dispatch miner.py + defensive guards
Browse files- README.md +53 -0
- miner.py +102 -4
- numberplate_weights.onnx +2 -2
- yolo26n.pt +3 -0
README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-4.0
|
| 3 |
+
tags:
|
| 4 |
+
- element_type:detect
|
| 5 |
+
- model:yolov11-small
|
| 6 |
+
- object:numberplate
|
| 7 |
+
- subnet:sn44
|
| 8 |
+
- element:manak0/Detect-number-plates-1-0
|
| 9 |
+
manako:
|
| 10 |
+
element_id: manak0/Detect-number-plates-1-0
|
| 11 |
+
description: License plate / number plate detector for SN44 element manak0/Detect-number-plates-1-0
|
| 12 |
+
source: meaculpitt/ScoreVision
|
| 13 |
+
prompt_hints: null
|
| 14 |
+
input_payload:
|
| 15 |
+
- name: frame
|
| 16 |
+
type: image
|
| 17 |
+
description: RGB frame (validator native resolution 1408x768)
|
| 18 |
+
output_payload:
|
| 19 |
+
- name: detections
|
| 20 |
+
type: detections
|
| 21 |
+
description: List of license plate bounding boxes (single class "numberplate", absolute pixel xyxy)
|
| 22 |
+
evaluation_score: null
|
| 23 |
+
last_benchmark: null
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
# meaculpitt/ScoreVision-Numberplate
|
| 27 |
+
|
| 28 |
+
Single-element chute for the SN44 element
|
| 29 |
+
**`manak0/Detect-number-plates-1-0`** (activates 2026-04-12T12:02 UTC).
|
| 30 |
+
|
| 31 |
+
- Architecture: YOLO11s exported to ONNX (≤30 MB hard cap)
|
| 32 |
+
- Input: 1408×768 RGB frames (the validator's native resolution)
|
| 33 |
+
- Output: bounding boxes around license plates (single class
|
| 34 |
+
`numberplate`, absolute-pixel xyxy coordinates)
|
| 35 |
+
- Inference: ONNX Runtime CUDA EP, Gaussian Soft-NMS (sigma=0.5)
|
| 36 |
+
for plate-dense scenes
|
| 37 |
+
- Latency target: p95 ≤ 50 ms end-to-end
|
| 38 |
+
|
| 39 |
+
Trained on a 344k-image combined corpus of:
|
| 40 |
+
- Roboflow license-plate-recognition-rxg4e v4
|
| 41 |
+
- Roboflow vehicle-registration-plates-trudk v2
|
| 42 |
+
- Roboflow car-license-fj1kd v4
|
| 43 |
+
- helpme-8ixem/anpr-iuzao (real CCTV ANPR footage)
|
| 44 |
+
- CCPD2019 (Chinese City Parking Dataset, ~200k overhead surveillance frames)
|
| 45 |
+
|
| 46 |
+
## Sibling repos in this org
|
| 47 |
+
|
| 48 |
+
- [`meaculpitt/ScoreVision-Vehicle`](https://huggingface.co/meaculpitt/ScoreVision-Vehicle) — vehicle (manak0/Detect-detect-vehicle), served by the `insect` chute
|
| 49 |
+
- [`meaculpitt/ScoreVision-Petrol`](https://huggingface.co/meaculpitt/ScoreVision-Petrol) — petrol station components
|
| 50 |
+
|
| 51 |
+
This repo (`ScoreVision-Numberplate`) is intentionally a separate
|
| 52 |
+
single-element submission deployed under its own chute slug, not part
|
| 53 |
+
of the unified `insect` chute.
|
miner.py
CHANGED
|
@@ -176,6 +176,17 @@ class Miner:
|
|
| 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
|
| 181 |
# frame doesn't pay a ~20 ms DVFS spin-up tax. SCOREVISION_WARMUP_CALLS
|
|
@@ -327,6 +338,32 @@ class Miner:
|
|
| 327 |
rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
|
| 328 |
x = np.transpose(rgb.astype(np.float32) / 255.0, (2, 0, 1))[None, ...]
|
| 329 |
out = self.session.run(None, {self.input_name: x})[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
pred = self._normalize_predictions(out)
|
| 331 |
|
| 332 |
if pred.shape[1] < 5:
|
|
@@ -346,9 +383,6 @@ class Miner:
|
|
| 346 |
if boxes_m.shape[0] == 0:
|
| 347 |
return []
|
| 348 |
|
| 349 |
-
# Model-space (input_w x input_h) -> crop-space -> original image
|
| 350 |
-
sx = cw / self.input_w
|
| 351 |
-
sy = ch / self.input_h
|
| 352 |
dets: list[tuple[float, float, float, float, float, int]] = []
|
| 353 |
for i in range(boxes_m.shape[0]):
|
| 354 |
cx, cy, bw, bh = boxes_m[i].tolist()
|
|
@@ -451,24 +485,57 @@ class Miner:
|
|
| 451 |
|
| 452 |
all_dets = self._quad4_raw_dets(image_bgr)
|
| 453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
if self.use_tta:
|
| 455 |
flipped = cv2.flip(image_bgr, 1) # horizontal flip (mirror)
|
| 456 |
flip_dets = self._quad4_raw_dets(flipped)
|
| 457 |
# Un-flip x-coordinates: x_orig = W - x_flipped
|
| 458 |
for x1f, y1, x2f, y2, conf, cls_id in flip_dets:
|
| 459 |
-
|
| 460 |
(orig_w - x2f, y1, orig_w - x1f, y2, conf, cls_id)
|
| 461 |
)
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
# TTA-aware cluster-dedup: collapse near-duplicate detections of the
|
| 464 |
# same plate (e.g. original + unflipped TTA view) BEFORE Soft-NMS,
|
| 465 |
# which would otherwise decay but not kill the lower-conf copy at
|
| 466 |
# our low score_threshold=0.01. Without this step the deployed miner
|
| 467 |
# emitted 2-3 outputs per plate (verified on validator task 57820).
|
|
|
|
| 468 |
all_dets = self._cluster_dedup(all_dets, iou_thresh=0.3)
|
| 469 |
|
| 470 |
dets = self._soft_nms(all_dets)
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
out_boxes: list[BoundingBox] = []
|
| 473 |
for x1, y1, x2, y2, conf, cls_id in dets:
|
| 474 |
ix1 = max(0, min(orig_w, math.floor(x1)))
|
|
@@ -497,6 +564,37 @@ class Miner:
|
|
| 497 |
conf=max(0.0, min(1.0, conf)),
|
| 498 |
)
|
| 499 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
return out_boxes
|
| 501 |
|
| 502 |
# ---------------------------------------------------------------- entry
|
|
|
|
| 176 |
# catch plates the model is directionally biased against.
|
| 177 |
self.use_tta = True
|
| 178 |
|
| 179 |
+
# Dual-threshold TTA verification gate (hermes-style, seen in the
|
| 180 |
+
# hermestech00/numberplate0 HF repo). Final-output gate:
|
| 181 |
+
# - conf >= conf_high → pass unconditionally
|
| 182 |
+
# - conf in [conf_threshold, conf_high) → must have a flip-view
|
| 183 |
+
# match with IoU >= tta_match_iou
|
| 184 |
+
# to survive
|
| 185 |
+
# Uses TTA as a cross-view VERIFIER, not just a recall booster.
|
| 186 |
+
# Skips when use_tta=False.
|
| 187 |
+
self.conf_high = 0.90
|
| 188 |
+
self.tta_match_iou = 0.01
|
| 189 |
+
|
| 190 |
# GPU warmup — force ORT / CUDA / cuDNN kernel compilation and pull
|
| 191 |
# the 4090 out of low-power idle state so the first real validator
|
| 192 |
# frame doesn't pay a ~20 ms DVFS spin-up tax. SCOREVISION_WARMUP_CALLS
|
|
|
|
| 338 |
rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)
|
| 339 |
x = np.transpose(rgb.astype(np.float32) / 255.0, (2, 0, 1))[None, ...]
|
| 340 |
out = self.session.run(None, {self.input_name: x})[0]
|
| 341 |
+
|
| 342 |
+
# Scale factors from model-input space -> crop -> original image coords.
|
| 343 |
+
sx = cw / self.input_w
|
| 344 |
+
sy = ch / self.input_h
|
| 345 |
+
|
| 346 |
+
# Shape-dispatch: detect end2end export format (YOLO26 family: [1, N, 6]
|
| 347 |
+
# with N<=300, per-row [x1, y1, x2, y2, conf, cls_id] already NMS'd) vs
|
| 348 |
+
# raw YOLO11/v8 export ([1, C, anchors] or [1, anchors, C] with cx/cy/w/h
|
| 349 |
+
# + per-class scores, pre-NMS).
|
| 350 |
+
if out.ndim == 3 and out.shape[-1] == 6:
|
| 351 |
+
rows = out[0] # [N, 6]
|
| 352 |
+
confs_all = rows[:, 4]
|
| 353 |
+
keep = confs_all >= self.conf_threshold
|
| 354 |
+
rows = rows[keep]
|
| 355 |
+
if rows.shape[0] == 0:
|
| 356 |
+
return []
|
| 357 |
+
dets_e2e: list[tuple[float, float, float, float, float, int]] = []
|
| 358 |
+
for i in range(rows.shape[0]):
|
| 359 |
+
x1m, y1m, x2m, y2m, conf, cls_id = rows[i].tolist()
|
| 360 |
+
xa = x1m * sx + x0
|
| 361 |
+
ya = y1m * sy + y0
|
| 362 |
+
xb = x2m * sx + x0
|
| 363 |
+
yb = y2m * sy + y0
|
| 364 |
+
dets_e2e.append((xa, ya, xb, yb, float(conf), int(cls_id)))
|
| 365 |
+
return dets_e2e
|
| 366 |
+
|
| 367 |
pred = self._normalize_predictions(out)
|
| 368 |
|
| 369 |
if pred.shape[1] < 5:
|
|
|
|
| 383 |
if boxes_m.shape[0] == 0:
|
| 384 |
return []
|
| 385 |
|
|
|
|
|
|
|
|
|
|
| 386 |
dets: list[tuple[float, float, float, float, float, int]] = []
|
| 387 |
for i in range(boxes_m.shape[0]):
|
| 388 |
cx, cy, bw, bh = boxes_m[i].tolist()
|
|
|
|
| 485 |
|
| 486 |
all_dets = self._quad4_raw_dets(image_bgr)
|
| 487 |
|
| 488 |
+
# Adaptive conf fallback: if the quad-4 pass produced nothing, retry
|
| 489 |
+
# once at a lower pre-NMS threshold. Rescues archived floor-drops
|
| 490 |
+
# where the model had plate signal but nothing crossed 0.18 anywhere
|
| 491 |
+
# in the 4 tiles (observed on validator tasks 57803/57836/57848).
|
| 492 |
+
if not all_dets:
|
| 493 |
+
_orig_conf = self.conf_threshold
|
| 494 |
+
try:
|
| 495 |
+
self.conf_threshold = 0.10
|
| 496 |
+
all_dets = self._quad4_raw_dets(image_bgr)
|
| 497 |
+
finally:
|
| 498 |
+
self.conf_threshold = _orig_conf
|
| 499 |
+
if all_dets:
|
| 500 |
+
_cuda_log.warning(
|
| 501 |
+
"adaptive conf fallback rescued %d raw dets at conf=0.10",
|
| 502 |
+
len(all_dets),
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
# Keep flipped-view detections SEPARATE from original, so we can use
|
| 506 |
+
# them as a cross-view verifier (hermes-style gate) later — not just
|
| 507 |
+
# merge them into all_dets as a recall booster.
|
| 508 |
+
flip_dets_unflipped: list[tuple] = []
|
| 509 |
if self.use_tta:
|
| 510 |
flipped = cv2.flip(image_bgr, 1) # horizontal flip (mirror)
|
| 511 |
flip_dets = self._quad4_raw_dets(flipped)
|
| 512 |
# Un-flip x-coordinates: x_orig = W - x_flipped
|
| 513 |
for x1f, y1, x2f, y2, conf, cls_id in flip_dets:
|
| 514 |
+
flip_dets_unflipped.append(
|
| 515 |
(orig_w - x2f, y1, orig_w - x1f, y2, conf, cls_id)
|
| 516 |
)
|
| 517 |
+
# Still merge flip into all_dets so dedup + NMS sees both views
|
| 518 |
+
# (preserves existing TTA recall behaviour).
|
| 519 |
+
all_dets.extend(flip_dets_unflipped)
|
| 520 |
|
| 521 |
# TTA-aware cluster-dedup: collapse near-duplicate detections of the
|
| 522 |
# same plate (e.g. original + unflipped TTA view) BEFORE Soft-NMS,
|
| 523 |
# which would otherwise decay but not kill the lower-conf copy at
|
| 524 |
# our low score_threshold=0.01. Without this step the deployed miner
|
| 525 |
# emitted 2-3 outputs per plate (verified on validator task 57820).
|
| 526 |
+
pre_nms_count = len(all_dets)
|
| 527 |
all_dets = self._cluster_dedup(all_dets, iou_thresh=0.3)
|
| 528 |
|
| 529 |
dets = self._soft_nms(all_dets)
|
| 530 |
|
| 531 |
+
# (Dual-threshold TTA gate tried here and reverted 2026-04-21: on our
|
| 532 |
+
# YOLO11s ONNX the gate cost −0.037 map50-proxy to save only +0.023 FP,
|
| 533 |
+
# net −0.013 composite on 20 post-jump archive tasks. Pattern is the
|
| 534 |
+
# right one for hermes's YOLO26s (higher recall, more conf >=0.90 boxes)
|
| 535 |
+
# but hurts YOLO11s. Keep self.conf_high + self.tta_match_iou params in
|
| 536 |
+
# __init__ in case v7/v8 training closes the recall gap and makes the
|
| 537 |
+
# gate net-positive — can re-add this block then.)
|
| 538 |
+
|
| 539 |
out_boxes: list[BoundingBox] = []
|
| 540 |
for x1, y1, x2, y2, conf, cls_id in dets:
|
| 541 |
ix1 = max(0, min(orig_w, math.floor(x1)))
|
|
|
|
| 564 |
conf=max(0.0, min(1.0, conf)),
|
| 565 |
)
|
| 566 |
)
|
| 567 |
+
|
| 568 |
+
# Silent-empty-submission guard: if the pipeline found raw detections
|
| 569 |
+
# but every one was filtered to nothing, bypass F1a/F1b and emit the
|
| 570 |
+
# post-NMS detections above score_threshold. Accepts a potential FP
|
| 571 |
+
# over a guaranteed zero — which scored 0.000-0.010 on validator
|
| 572 |
+
# tasks 57803/57836/57848 even though the model had clear plate
|
| 573 |
+
# signal in the tiles.
|
| 574 |
+
if pre_nms_count > 0 and not out_boxes:
|
| 575 |
+
_cuda_log.warning(
|
| 576 |
+
"empty-submission guard: %d raw dets → 0 filtered; emitting raw",
|
| 577 |
+
pre_nms_count,
|
| 578 |
+
)
|
| 579 |
+
for x1, y1, x2, y2, conf, cls_id in dets:
|
| 580 |
+
if conf < self.score_threshold:
|
| 581 |
+
continue
|
| 582 |
+
ix1 = max(0, min(orig_w, math.floor(x1)))
|
| 583 |
+
iy1 = max(0, min(orig_h, math.floor(y1)))
|
| 584 |
+
ix2 = max(0, min(orig_w, math.ceil(x2)))
|
| 585 |
+
iy2 = max(0, min(orig_h, math.ceil(y2)))
|
| 586 |
+
if ix2 <= ix1 or iy2 <= iy1:
|
| 587 |
+
continue
|
| 588 |
+
out_boxes.append(
|
| 589 |
+
BoundingBox(
|
| 590 |
+
x1=ix1,
|
| 591 |
+
y1=iy1,
|
| 592 |
+
x2=ix2,
|
| 593 |
+
y2=iy2,
|
| 594 |
+
cls_id=cls_id,
|
| 595 |
+
conf=max(0.0, min(1.0, conf)),
|
| 596 |
+
)
|
| 597 |
+
)
|
| 598 |
return out_boxes
|
| 599 |
|
| 600 |
# ---------------------------------------------------------------- entry
|
numberplate_weights.onnx
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:75e91a8ee137d2f52eef752527a3b3ff1159c5b4417c5b40f205af090fb0ea00
|
| 3 |
+
size 19579923
|
yolo26n.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9b09cc8bf347f0fc8a5f7657480587f25db09b34bf33b0652110fb03a8ad4fef
|
| 3 |
+
size 5544453
|