coolroman commited on
Commit
20faeb4
·
verified ·
1 Parent(s): a939533

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. miner.py +117 -8
miner.py CHANGED
@@ -98,11 +98,24 @@ class Miner:
98
  self.input_height = self._safe_dim(self.input_shape[2], default=1280)
99
  self.input_width = self._safe_dim(self.input_shape[3], default=1280)
100
 
101
- self.conf_thres = 0.20
 
 
 
 
102
  self.iou_thres = 0.5
103
  self.cross_iou_thresh = 0.7
104
  self.max_det = 300
105
  self.use_tta = True
 
 
 
 
 
 
 
 
 
106
 
107
  # Sanity filter — reject obviously bad boxes
108
  self.min_box_area = 6 * 6
@@ -387,12 +400,40 @@ class Miner:
387
  return []
388
  return self._build_results(boxes, scores, cls_ids)
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  def _predict_tta(self, image: np.ndarray) -> list[BoundingBox]:
391
- """Hflip TTA: merge primary + flipped via per-class hard-NMS,
392
- then cross-class dedup, with consensus-confidence boost."""
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  ow = image.shape[1]
 
394
  b1, s1, c1 = self._forward(image)
395
 
 
396
  flipped = cv2.flip(image, 1)
397
  b2, s2, c2 = self._forward(flipped)
398
  if len(b2):
@@ -400,19 +441,87 @@ class Miner:
400
  x2f = ow - b2[:, 0]
401
  b2 = np.stack([x1f, b2[:, 1], x2f, b2[:, 3]], axis=1)
402
 
403
- if len(b1) == 0 and len(b2) == 0:
 
 
 
 
 
 
 
 
404
  return []
405
 
406
- boxes = np.concatenate([b1, b2], axis=0) if len(b2) else b1
407
- scores = np.concatenate([s1, s2], axis=0) if len(b2) else s1
408
- cls_ids = np.concatenate([c1, c2], axis=0) if len(b2) else c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  keep = self._per_class_hard_nms(boxes, scores, cls_ids, self.iou_thres)
411
  if len(keep) == 0:
412
  return []
413
  keep = keep[: self.max_det]
414
 
415
- # Consensus-confidence boost: cluster by IoU and take max score.
416
  boosted = self._max_score_per_cluster(boxes, scores, keep, self.iou_thres)
417
 
418
  boxes = boxes[keep]
 
98
  self.input_height = self._safe_dim(self.input_shape[2], default=1280)
99
  self.input_width = self._safe_dim(self.input_shape[3], default=1280)
100
 
101
+ # Tuned on local benchmark vs rival-proxy GT (5/2/2026):
102
+ # V3 = consensus filter + hflip TTA. Multi-scale, cross-class tighter,
103
+ # strict-consensus-across-multi-views all tested and either hurt or
104
+ # matched. V3 is the local optimum.
105
+ self.conf_thres = 0.40
106
  self.iou_thres = 0.5
107
  self.cross_iou_thresh = 0.7
108
  self.max_det = 300
109
  self.use_tta = True
110
+ # Consensus TTA — our edge. None of the top miners (5FBnd/5CiAr/5CtY4)
111
+ # do this; they keep all-view union and only boost cluster scores.
112
+ self.use_consensus_tta = True
113
+ self.consensus_iou = 0.5
114
+ self.require_strict_consensus = False
115
+ # Multi-scale tested + abandoned: it loosened consensus and hurt FP
116
+ # suppression more than it helped recall on rival-proxy GT.
117
+ self.use_multi_scale_tta = False
118
+ self.tta_scale = 0.85
119
 
120
  # Sanity filter — reject obviously bad boxes
121
  self.min_box_area = 6 * 6
 
400
  return []
401
  return self._build_results(boxes, scores, cls_ids)
402
 
403
+ def _forward_scaled(self, image: np.ndarray, scale: float):
404
+ """Forward pass on a scale-augmented image; transform boxes back to original coords."""
405
+ if scale == 1.0:
406
+ return self._forward(image)
407
+ h, w = image.shape[:2]
408
+ nh, nw = int(round(h * scale)), int(round(w * scale))
409
+ scaled = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_LINEAR)
410
+ b, s, c = self._forward(scaled)
411
+ if len(b):
412
+ b = b / scale # scale boxes back to original image coords
413
+ b = self._clip_boxes(b, (w, h))
414
+ return b, s, c
415
+
416
  def _predict_tta(self, image: np.ndarray) -> list[BoundingBox]:
417
+ """Multi-view TTA with consensus filter.
418
+
419
+ Views (configurable):
420
+ v1 = primary forward (1.0x)
421
+ v2 = horizontal flip
422
+ v3 = downscaled forward (tta_scale, e.g. 0.85x) — catches small objects
423
+
424
+ Consensus filter (use_consensus_tta=True):
425
+ A box from v1 is kept iff it is confirmed by ≥1 OTHER view (v2 or v3)
426
+ at IoU >= consensus_iou with same class. Score = max across confirming
427
+ views. None of the top miners do this — this is our edge.
428
+
429
+ Merge (use_consensus_tta=False, fallback): union all views, per-class
430
+ hard-NMS, max-score boost on clusters.
431
+ """
432
  ow = image.shape[1]
433
+ # v1: primary
434
  b1, s1, c1 = self._forward(image)
435
 
436
+ # v2: hflip
437
  flipped = cv2.flip(image, 1)
438
  b2, s2, c2 = self._forward(flipped)
439
  if len(b2):
 
441
  x2f = ow - b2[:, 0]
442
  b2 = np.stack([x1f, b2[:, 1], x2f, b2[:, 3]], axis=1)
443
 
444
+ # v3: multi-scale (0.85x)
445
+ if self.use_multi_scale_tta:
446
+ b3, s3, c3 = self._forward_scaled(image, self.tta_scale)
447
+ else:
448
+ b3 = np.empty((0, 4), dtype=np.float32)
449
+ s3 = np.empty((0,), dtype=np.float32)
450
+ c3 = np.empty((0,), dtype=np.int32)
451
+
452
+ if len(b1) == 0 and len(b2) == 0 and len(b3) == 0:
453
  return []
454
 
455
+ if self.use_consensus_tta:
456
+ if len(b1) == 0:
457
+ return []
458
+ # Per-view best-IoU helper.
459
+ def best_iou_match(box, cls, vb, vc, vs):
460
+ if len(vb) == 0:
461
+ return 0.0, 0.0
462
+ same_cls = vc == cls
463
+ if not same_cls.any():
464
+ return 0.0, 0.0
465
+ xx1 = np.maximum(box[0], vb[:, 0])
466
+ yy1 = np.maximum(box[1], vb[:, 1])
467
+ xx2 = np.minimum(box[2], vb[:, 2])
468
+ yy2 = np.minimum(box[3], vb[:, 3])
469
+ inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
470
+ a_i = (box[2] - box[0]) * (box[3] - box[1])
471
+ a_j = (vb[:, 2] - vb[:, 0]) * (vb[:, 3] - vb[:, 1])
472
+ ious = inter / (a_i + a_j - inter + 1e-7)
473
+ ious = np.where(same_cls, ious, 0.0)
474
+ idx = int(ious.argmax())
475
+ return float(ious[idx]), float(vs[idx])
476
+
477
+ keep_b = []; keep_s = []; keep_c = []
478
+ for i in range(len(b1)):
479
+ iou_h, sc_h = best_iou_match(b1[i], c1[i], b2, c2, s2)
480
+ iou_m, sc_m = best_iou_match(b1[i], c1[i], b3, c3, s3) if len(b3) else (0.0, 0.0)
481
+ if self.require_strict_consensus and self.use_multi_scale_tta:
482
+ # Both hflip AND multi-scale must confirm.
483
+ if iou_h >= self.consensus_iou and iou_m >= self.consensus_iou:
484
+ keep_b.append(b1[i]); keep_c.append(c1[i])
485
+ keep_s.append(max(float(s1[i]), sc_h, sc_m))
486
+ else:
487
+ # ANY one other view confirming is enough.
488
+ if max(iou_h, iou_m) >= self.consensus_iou:
489
+ keep_b.append(b1[i]); keep_c.append(c1[i])
490
+ # Score = max across confirming views.
491
+ partners = [float(s1[i])]
492
+ if iou_h >= self.consensus_iou: partners.append(sc_h)
493
+ if iou_m >= self.consensus_iou: partners.append(sc_m)
494
+ keep_s.append(max(partners))
495
+ if not keep_b:
496
+ return []
497
+ boxes = np.asarray(keep_b, dtype=np.float32)
498
+ scores = np.asarray(keep_s, dtype=np.float32)
499
+ cls_ids = np.asarray(keep_c, dtype=np.int32)
500
+ keep = self._per_class_hard_nms(boxes, scores, cls_ids, self.iou_thres)
501
+ if len(keep) == 0:
502
+ return []
503
+ keep = keep[: self.max_det]
504
+ boxes = boxes[keep]
505
+ scores = scores[keep]
506
+ cls_ids = cls_ids[keep]
507
+ boxes, scores, cls_ids = self._cross_class_dedup(
508
+ boxes, scores, cls_ids, self.cross_iou_thresh
509
+ )
510
+ return self._build_results(boxes, scores, cls_ids)
511
+
512
+ # Merge mode (fallback): union all available views.
513
+ parts_b = [b1] + ([b2] if len(b2) else []) + ([b3] if len(b3) else [])
514
+ parts_s = [s1] + ([s2] if len(b2) else []) + ([s3] if len(b3) else [])
515
+ parts_c = [c1] + ([c2] if len(b2) else []) + ([c3] if len(b3) else [])
516
+ boxes = np.concatenate(parts_b, axis=0) if len(parts_b) > 1 else b1
517
+ scores = np.concatenate(parts_s, axis=0) if len(parts_s) > 1 else s1
518
+ cls_ids = np.concatenate(parts_c, axis=0) if len(parts_c) > 1 else c1
519
 
520
  keep = self._per_class_hard_nms(boxes, scores, cls_ids, self.iou_thres)
521
  if len(keep) == 0:
522
  return []
523
  keep = keep[: self.max_det]
524
 
 
525
  boosted = self._max_score_per_cluster(boxes, scores, keep, self.iou_thres)
526
 
527
  boxes = boxes[keep]