scorevision: push artifact
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"""
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
return []
|
| 405 |
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|