meaculpitt commited on
Commit
4a79163
·
verified ·
1 Parent(s): d0e9ded

v8 ep-10 YOLO26s weights + shape-dispatch miner.py + defensive guards

Browse files
Files changed (4) hide show
  1. README.md +53 -0
  2. miner.py +102 -4
  3. numberplate_weights.onnx +2 -2
  4. 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
- all_dets.append(
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:4c62bedf6d9d5ca79b6d663efb4fe105ed9785712603a488ee49c79e530179cc
3
- size 19770499
 
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