scorevision: push artifact
Browse files- README.md +10 -0
- chute_config.yml +2 -8
- class_names.txt +5 -0
- miner.py +175 -97
- model_type.json +1 -0
- person_weights.onnx +2 -2
- pyproject.toml +10 -0
README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ScoreVision SN44 Unified Miner
|
| 2 |
+
|
| 3 |
+
Dual-model approach: vehicle (YOLO11s) + person (YOLO11s).
|
| 4 |
+
Runs both models on every image and merges all detections.
|
| 5 |
+
|
| 6 |
+
## Classes
|
| 7 |
+
- cls_id 0: bus (vehicle eval) / person (person eval)
|
| 8 |
+
- cls_id 1: car
|
| 9 |
+
- cls_id 2: truck
|
| 10 |
+
- cls_id 3: motorcycle
|
chute_config.yml
CHANGED
|
@@ -7,16 +7,10 @@ Image:
|
|
| 7 |
NodeSelector:
|
| 8 |
gpu_count: 1
|
| 9 |
min_vram_gb_per_gpu: 16
|
| 10 |
-
max_hourly_price_per_gpu: 0.50
|
| 11 |
-
exclude:
|
| 12 |
-
- "5090"
|
| 13 |
-
- b200
|
| 14 |
-
- h200
|
| 15 |
-
- mi300x
|
| 16 |
|
| 17 |
Chute:
|
| 18 |
-
timeout_seconds:
|
| 19 |
concurrency: 4
|
| 20 |
max_instances: 5
|
| 21 |
scaling_threshold: 0.5
|
| 22 |
-
shutdown_after_seconds:
|
|
|
|
| 7 |
NodeSelector:
|
| 8 |
gpu_count: 1
|
| 9 |
min_vram_gb_per_gpu: 16
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
Chute:
|
| 12 |
+
timeout_seconds: 300
|
| 13 |
concurrency: 4
|
| 14 |
max_instances: 5
|
| 15 |
scaling_threshold: 0.5
|
| 16 |
+
shutdown_after_seconds: 600
|
class_names.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
bus
|
| 2 |
+
car
|
| 3 |
+
truck
|
| 4 |
+
motorcycle
|
| 5 |
+
person
|
miner.py
CHANGED
|
@@ -163,22 +163,27 @@ logger = logging.getLogger(__name__)
|
|
| 163 |
# ── Vehicle config ──────────────────────────────────────────────────────────
|
| 164 |
VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
|
| 165 |
VEH_NUM_CLASSES = 4
|
| 166 |
-
|
| 167 |
-
VEH_CONF_DEFAULT = 0.35
|
| 168 |
VEH_TTA_CONF = 0.10
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
# ── Person config (TTA consensus) ───────────────────────────────────────────
|
| 172 |
-
PER_CONF_LOW = 0.
|
| 173 |
-
PER_CONF_HIGH = 0.
|
| 174 |
PER_CONSENSUS_IOU = 0.50
|
| 175 |
-
PER_WBF_IOU = 0.40
|
| 176 |
PER_RTF_BUDGET = 8.0
|
| 177 |
|
| 178 |
# ── Person box sanity filters ──────────────────────────────────────────────
|
| 179 |
PER_MIN_WH = 8
|
| 180 |
PER_MIN_AREA = 14 * 14
|
| 181 |
-
PER_MAX_ASPECT =
|
| 182 |
PER_MAX_AREA_RATIO = 0.80
|
| 183 |
|
| 184 |
# ── Shared ──────────────────────────────────────────────────────────────────
|
|
@@ -309,6 +314,48 @@ def _wbf_single(boxes_list, scores_list, iou_thr=0.45, skip_thr=0.0001):
|
|
| 309 |
return np.array(fused_b), np.array(fused_s)
|
| 310 |
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
class BoundingBox(BaseModel):
|
| 313 |
x1: int
|
| 314 |
y1: int
|
|
@@ -422,60 +469,78 @@ class Miner:
|
|
| 422 |
|
| 423 |
def _infer_vehicle(self, image_bgr):
|
| 424 |
oh, ow = image_bgr.shape[:2]
|
| 425 |
-
all_b, all_s, all_l = [], [], []
|
| 426 |
-
|
| 427 |
-
def _collect(boxes, confs, cls_ids):
|
| 428 |
-
if len(boxes) == 0:
|
| 429 |
-
return
|
| 430 |
-
out_cls = np.array([VEH_MODEL_TO_OUT[int(c)] for c in cls_ids])
|
| 431 |
-
norm = boxes.copy()
|
| 432 |
-
norm[:, [0, 2]] /= ow
|
| 433 |
-
norm[:, [1, 3]] /= oh
|
| 434 |
-
norm = np.clip(norm, 0, 1)
|
| 435 |
-
all_b.append(norm)
|
| 436 |
-
all_s.append(confs)
|
| 437 |
-
all_l.append(out_cls)
|
| 438 |
|
| 439 |
if ENABLE_TTA:
|
| 440 |
-
|
| 441 |
flipped = cv2.flip(image_bgr, 1)
|
| 442 |
-
|
| 443 |
-
if len(
|
| 444 |
-
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
else:
|
| 447 |
-
|
| 448 |
-
_collect(bx, confs, cls_ids)
|
| 449 |
|
| 450 |
-
if
|
| 451 |
return []
|
| 452 |
|
| 453 |
-
|
|
|
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
|
|
|
| 457 |
|
| 458 |
-
|
| 459 |
-
|
| 460 |
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
for i in range(len(fs))
|
| 464 |
-
])
|
| 465 |
if not keep.any():
|
| 466 |
return []
|
| 467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
|
| 469 |
out = []
|
| 470 |
-
for i in range(len(
|
| 471 |
-
b =
|
| 472 |
out.append(BoundingBox(
|
| 473 |
x1=max(0, min(ow, math.floor(b[0]))),
|
| 474 |
y1=max(0, min(oh, math.floor(b[1]))),
|
| 475 |
x2=max(0, min(ow, math.ceil(b[2]))),
|
| 476 |
y2=max(0, min(oh, math.ceil(b[3]))),
|
| 477 |
-
cls_id=int(
|
| 478 |
-
conf=max(0.0, min(1.0, float(
|
| 479 |
))
|
| 480 |
return out
|
| 481 |
|
|
@@ -485,7 +550,8 @@ class Miner:
|
|
| 485 |
h, w = img.shape[:2]
|
| 486 |
r = min(self.per_h / h, self.per_w / w)
|
| 487 |
nw, nh = int(round(w * r)), int(round(h * r))
|
| 488 |
-
|
|
|
|
| 489 |
dw, dh = self.per_w - nw, self.per_h - nh
|
| 490 |
pl, pt = dw // 2, dh // 2
|
| 491 |
img_p = cv2.copyMakeBorder(
|
|
@@ -514,10 +580,10 @@ class Miner:
|
|
| 514 |
boxes, confs = pred[keep, :4], confs[keep]
|
| 515 |
if len(boxes) == 0:
|
| 516 |
return np.empty((0, 4)), np.empty(0)
|
| 517 |
-
boxes[:, 0] = (boxes[:, 0] - pl) / ratio
|
| 518 |
-
boxes[:,
|
| 519 |
-
boxes[:,
|
| 520 |
-
boxes[:, 3] = (boxes[:, 3] - pt) / ratio
|
| 521 |
boxes = np.clip(boxes, 0, [[ow, oh, ow, oh]])
|
| 522 |
return boxes, confs
|
| 523 |
|
|
@@ -533,10 +599,10 @@ class Miner:
|
|
| 533 |
if len(boxes) == 0:
|
| 534 |
return np.empty((0, 4)), np.empty(0)
|
| 535 |
cx, cy, bw, bh = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
|
| 536 |
-
x1 = np.clip((cx - bw / 2 - pl) / ratio, 0, ow)
|
| 537 |
-
y1 = np.clip((cy - bh / 2 - pt) / ratio, 0, oh)
|
| 538 |
-
x2 = np.clip((cx + bw / 2 - pl) / ratio, 0, ow)
|
| 539 |
-
y2 = np.clip((cy + bh / 2 - pt) / ratio, 0, oh)
|
| 540 |
return np.stack([x1, y1, x2, y2], axis=1), confs
|
| 541 |
|
| 542 |
def _per_run_pass(self, image_bgr, conf_thresh):
|
|
@@ -564,51 +630,63 @@ class Miner:
|
|
| 564 |
|
| 565 |
kept_b, kept_s = [], []
|
| 566 |
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
if not kept_b:
|
| 590 |
return []
|
| 591 |
|
| 592 |
kept_b = np.array(kept_b)
|
| 593 |
kept_s = np.array(kept_s)
|
| 594 |
-
norm = kept_b.copy()
|
| 595 |
-
norm[:, [0, 2]] /= ow
|
| 596 |
-
norm[:, [1, 3]] /= oh
|
| 597 |
-
norm = np.clip(norm, 0, 1)
|
| 598 |
-
|
| 599 |
-
fb, fs = _wbf_single([norm], [kept_s], iou_thr=PER_WBF_IOU, skip_thr=WBF_SKIP_THR)
|
| 600 |
-
|
| 601 |
-
if len(fb) == 0:
|
| 602 |
-
return []
|
| 603 |
-
|
| 604 |
-
fb[:, [0, 2]] *= ow
|
| 605 |
-
fb[:, [1, 3]] *= oh
|
| 606 |
|
|
|
|
| 607 |
img_area = float(oh * ow)
|
| 608 |
sane = []
|
| 609 |
-
for i in range(len(
|
| 610 |
-
bw =
|
| 611 |
-
bh =
|
| 612 |
if bw < PER_MIN_WH or bh < PER_MIN_WH:
|
| 613 |
continue
|
| 614 |
area = bw * bh
|
|
@@ -622,19 +700,19 @@ class Miner:
|
|
| 622 |
|
| 623 |
if not sane:
|
| 624 |
return []
|
| 625 |
-
|
| 626 |
-
|
| 627 |
|
| 628 |
out = []
|
| 629 |
-
for i in range(len(
|
| 630 |
-
b =
|
| 631 |
out.append(BoundingBox(
|
| 632 |
-
x1=max(0, min(ow,
|
| 633 |
-
y1=max(0, min(oh,
|
| 634 |
-
x2=max(0, min(ow,
|
| 635 |
-
y2=max(0, min(oh,
|
| 636 |
cls_id=0,
|
| 637 |
-
conf=max(0.0, min(1.0,
|
| 638 |
))
|
| 639 |
return out
|
| 640 |
|
|
@@ -726,4 +804,4 @@ class Miner:
|
|
| 726 |
).start()
|
| 727 |
|
| 728 |
return results
|
| 729 |
-
# Miner v3.
|
|
|
|
| 163 |
# ── Vehicle config ──────────────────────────────────────────────────────────
|
| 164 |
VEH_MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
|
| 165 |
VEH_NUM_CLASSES = 4
|
| 166 |
+
VEH_CONF_THRES = 0.35
|
|
|
|
| 167 |
VEH_TTA_CONF = 0.10
|
| 168 |
+
VEH_NMS_IOU = 0.50
|
| 169 |
+
|
| 170 |
+
# ── Vehicle box sanity filters ─────────────────────────────────────────────
|
| 171 |
+
VEH_MIN_WH = 8
|
| 172 |
+
VEH_MIN_AREA = 196
|
| 173 |
+
VEH_MAX_ASPECT = 8.0
|
| 174 |
+
VEH_MAX_AREA_RATIO = 0.95
|
| 175 |
+
VEH_MAX_DET = 150
|
| 176 |
|
| 177 |
# ── Person config (TTA consensus) ───────────────────────────────────────────
|
| 178 |
+
PER_CONF_LOW = 0.45
|
| 179 |
+
PER_CONF_HIGH = 0.58
|
| 180 |
PER_CONSENSUS_IOU = 0.50
|
|
|
|
| 181 |
PER_RTF_BUDGET = 8.0
|
| 182 |
|
| 183 |
# ── Person box sanity filters ──────────────────────────────────────────────
|
| 184 |
PER_MIN_WH = 8
|
| 185 |
PER_MIN_AREA = 14 * 14
|
| 186 |
+
PER_MAX_ASPECT = 6.0
|
| 187 |
PER_MAX_AREA_RATIO = 0.80
|
| 188 |
|
| 189 |
# ── Shared ──────────────────────────────────────────────────────────────────
|
|
|
|
| 314 |
return np.array(fused_b), np.array(fused_s)
|
| 315 |
|
| 316 |
|
| 317 |
+
def _nms_per_class_boost(boxes, scores, labels, iou_thr=0.50):
|
| 318 |
+
"""Per-class hard NMS with max-score cluster boosting.
|
| 319 |
+
Surviving box keeps its coordinates but gets the max confidence
|
| 320 |
+
among all boxes in its overlap cluster."""
|
| 321 |
+
if len(boxes) == 0:
|
| 322 |
+
return np.empty((0, 4)), np.empty(0), np.empty(0, dtype=int)
|
| 323 |
+
|
| 324 |
+
out_b, out_s, out_l = [], [], []
|
| 325 |
+
for cls in np.unique(labels):
|
| 326 |
+
m = labels == cls
|
| 327 |
+
cb, cs = boxes[m], scores[m]
|
| 328 |
+
order = cs.argsort()[::-1]
|
| 329 |
+
cb, cs = cb[order], cs[order]
|
| 330 |
+
|
| 331 |
+
suppressed = set()
|
| 332 |
+
for i in range(len(cb)):
|
| 333 |
+
if i in suppressed:
|
| 334 |
+
continue
|
| 335 |
+
max_score = float(cs[i])
|
| 336 |
+
for j in range(i + 1, len(cb)):
|
| 337 |
+
if j in suppressed:
|
| 338 |
+
continue
|
| 339 |
+
xx1 = max(cb[i, 0], cb[j, 0])
|
| 340 |
+
yy1 = max(cb[i, 1], cb[j, 1])
|
| 341 |
+
xx2 = min(cb[i, 2], cb[j, 2])
|
| 342 |
+
yy2 = min(cb[i, 3], cb[j, 3])
|
| 343 |
+
inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
|
| 344 |
+
a1 = (cb[i, 2] - cb[i, 0]) * (cb[i, 3] - cb[i, 1])
|
| 345 |
+
a2 = (cb[j, 2] - cb[j, 0]) * (cb[j, 3] - cb[j, 1])
|
| 346 |
+
iou = inter / (a1 + a2 - inter + 1e-9)
|
| 347 |
+
if iou >= iou_thr:
|
| 348 |
+
max_score = max(max_score, float(cs[j]))
|
| 349 |
+
suppressed.add(j)
|
| 350 |
+
out_b.append(cb[i])
|
| 351 |
+
out_s.append(max_score)
|
| 352 |
+
out_l.append(cls)
|
| 353 |
+
|
| 354 |
+
if not out_b:
|
| 355 |
+
return np.empty((0, 4)), np.empty(0), np.empty(0, dtype=int)
|
| 356 |
+
return np.array(out_b), np.array(out_s), np.array(out_l, dtype=int)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
class BoundingBox(BaseModel):
|
| 360 |
x1: int
|
| 361 |
y1: int
|
|
|
|
| 469 |
|
| 470 |
def _infer_vehicle(self, image_bgr):
|
| 471 |
oh, ow = image_bgr.shape[:2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
|
| 473 |
if ENABLE_TTA:
|
| 474 |
+
boxes1, confs1, cls1 = self._veh_run_pass(image_bgr, VEH_TTA_CONF)
|
| 475 |
flipped = cv2.flip(image_bgr, 1)
|
| 476 |
+
boxes2, confs2, cls2 = self._veh_run_pass(flipped, VEH_TTA_CONF)
|
| 477 |
+
if len(boxes2):
|
| 478 |
+
boxes2[:, 0], boxes2[:, 2] = ow - boxes2[:, 2], ow - boxes2[:, 0]
|
| 479 |
+
parts = [(b, s, c) for b, s, c in
|
| 480 |
+
[(boxes1, confs1, cls1), (boxes2, confs2, cls2)] if len(b)]
|
| 481 |
+
if not parts:
|
| 482 |
+
return []
|
| 483 |
+
boxes = np.concatenate([p[0] for p in parts])
|
| 484 |
+
confs = np.concatenate([p[1] for p in parts])
|
| 485 |
+
cls_ids = np.concatenate([p[2] for p in parts])
|
| 486 |
else:
|
| 487 |
+
boxes, confs, cls_ids = self._veh_run_pass(image_bgr, VEH_CONF_THRES)
|
|
|
|
| 488 |
|
| 489 |
+
if len(boxes) == 0:
|
| 490 |
return []
|
| 491 |
|
| 492 |
+
# Remap model classes to output classes
|
| 493 |
+
out_cls = np.array([VEH_MODEL_TO_OUT[int(c)] for c in cls_ids])
|
| 494 |
|
| 495 |
+
# Per-class hard NMS with max-score cluster boosting
|
| 496 |
+
boxes, confs, out_cls = _nms_per_class_boost(
|
| 497 |
+
boxes, confs, out_cls, iou_thr=VEH_NMS_IOU)
|
| 498 |
|
| 499 |
+
if len(boxes) == 0:
|
| 500 |
+
return []
|
| 501 |
|
| 502 |
+
# Global confidence filter (needed after TTA where lower threshold was used)
|
| 503 |
+
keep = confs >= VEH_CONF_THRES
|
|
|
|
|
|
|
| 504 |
if not keep.any():
|
| 505 |
return []
|
| 506 |
+
boxes, confs, out_cls = boxes[keep], confs[keep], out_cls[keep]
|
| 507 |
+
|
| 508 |
+
# Sanity filters
|
| 509 |
+
img_area = float(oh * ow)
|
| 510 |
+
sane = []
|
| 511 |
+
for i in range(len(boxes)):
|
| 512 |
+
bw = boxes[i, 2] - boxes[i, 0]
|
| 513 |
+
bh = boxes[i, 3] - boxes[i, 1]
|
| 514 |
+
if bw < VEH_MIN_WH or bh < VEH_MIN_WH:
|
| 515 |
+
continue
|
| 516 |
+
area = bw * bh
|
| 517 |
+
if area < VEH_MIN_AREA:
|
| 518 |
+
continue
|
| 519 |
+
if max(bw, bh) / max(min(bw, bh), 1e-6) > VEH_MAX_ASPECT:
|
| 520 |
+
continue
|
| 521 |
+
if area / img_area > VEH_MAX_AREA_RATIO:
|
| 522 |
+
continue
|
| 523 |
+
sane.append(i)
|
| 524 |
+
|
| 525 |
+
if not sane:
|
| 526 |
+
return []
|
| 527 |
+
boxes, confs, out_cls = boxes[sane], confs[sane], out_cls[sane]
|
| 528 |
+
|
| 529 |
+
# Limit max detections
|
| 530 |
+
if len(boxes) > VEH_MAX_DET:
|
| 531 |
+
top_k = np.argsort(confs)[::-1][:VEH_MAX_DET]
|
| 532 |
+
boxes, confs, out_cls = boxes[top_k], confs[top_k], out_cls[top_k]
|
| 533 |
|
| 534 |
out = []
|
| 535 |
+
for i in range(len(boxes)):
|
| 536 |
+
b = boxes[i]
|
| 537 |
out.append(BoundingBox(
|
| 538 |
x1=max(0, min(ow, math.floor(b[0]))),
|
| 539 |
y1=max(0, min(oh, math.floor(b[1]))),
|
| 540 |
x2=max(0, min(ow, math.ceil(b[2]))),
|
| 541 |
y2=max(0, min(oh, math.ceil(b[3]))),
|
| 542 |
+
cls_id=int(out_cls[i]),
|
| 543 |
+
conf=max(0.0, min(1.0, float(confs[i]))),
|
| 544 |
))
|
| 545 |
return out
|
| 546 |
|
|
|
|
| 550 |
h, w = img.shape[:2]
|
| 551 |
r = min(self.per_h / h, self.per_w / w)
|
| 552 |
nw, nh = int(round(w * r)), int(round(h * r))
|
| 553 |
+
interp = cv2.INTER_CUBIC if r > 1.0 else cv2.INTER_LINEAR
|
| 554 |
+
img_r = cv2.resize(img, (nw, nh), interpolation=interp)
|
| 555 |
dw, dh = self.per_w - nw, self.per_h - nh
|
| 556 |
pl, pt = dw // 2, dh // 2
|
| 557 |
img_p = cv2.copyMakeBorder(
|
|
|
|
| 580 |
boxes, confs = pred[keep, :4], confs[keep]
|
| 581 |
if len(boxes) == 0:
|
| 582 |
return np.empty((0, 4)), np.empty(0)
|
| 583 |
+
boxes[:, 0] = np.floor((boxes[:, 0] - pl) / ratio)
|
| 584 |
+
boxes[:, 1] = np.floor((boxes[:, 1] - pt) / ratio)
|
| 585 |
+
boxes[:, 2] = np.ceil((boxes[:, 2] - pl) / ratio)
|
| 586 |
+
boxes[:, 3] = np.ceil((boxes[:, 3] - pt) / ratio)
|
| 587 |
boxes = np.clip(boxes, 0, [[ow, oh, ow, oh]])
|
| 588 |
return boxes, confs
|
| 589 |
|
|
|
|
| 599 |
if len(boxes) == 0:
|
| 600 |
return np.empty((0, 4)), np.empty(0)
|
| 601 |
cx, cy, bw, bh = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
|
| 602 |
+
x1 = np.clip(np.floor((cx - bw / 2 - pl) / ratio), 0, ow)
|
| 603 |
+
y1 = np.clip(np.floor((cy - bh / 2 - pt) / ratio), 0, oh)
|
| 604 |
+
x2 = np.clip(np.ceil((cx + bw / 2 - pl) / ratio), 0, ow)
|
| 605 |
+
y2 = np.clip(np.ceil((cy + bh / 2 - pt) / ratio), 0, oh)
|
| 606 |
return np.stack([x1, y1, x2, y2], axis=1), confs
|
| 607 |
|
| 608 |
def _per_run_pass(self, image_bgr, conf_thresh):
|
|
|
|
| 630 |
|
| 631 |
kept_b, kept_s = [], []
|
| 632 |
|
| 633 |
+
if not has_flip or len(boxes_flip) == 0:
|
| 634 |
+
# No flip available — keep all original boxes
|
| 635 |
+
for i in range(len(boxes_orig)):
|
| 636 |
+
kept_b.append(boxes_orig[i])
|
| 637 |
+
kept_s.append(float(confs_orig[i]))
|
| 638 |
+
else:
|
| 639 |
+
# Consensus TTA merge
|
| 640 |
+
used_flip = set()
|
| 641 |
+
|
| 642 |
+
for i in range(len(boxes_orig)):
|
| 643 |
+
# Compute IoU with all flip boxes
|
| 644 |
+
xx1 = np.maximum(boxes_orig[i, 0], boxes_flip[:, 0])
|
| 645 |
+
yy1 = np.maximum(boxes_orig[i, 1], boxes_flip[:, 1])
|
| 646 |
+
xx2 = np.minimum(boxes_orig[i, 2], boxes_flip[:, 2])
|
| 647 |
+
yy2 = np.minimum(boxes_orig[i, 3], boxes_flip[:, 3])
|
| 648 |
+
inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
|
| 649 |
+
a1 = (boxes_orig[i, 2] - boxes_orig[i, 0]) * (boxes_orig[i, 3] - boxes_orig[i, 1])
|
| 650 |
+
a2 = (boxes_flip[:, 2] - boxes_flip[:, 0]) * (boxes_flip[:, 3] - boxes_flip[:, 1])
|
| 651 |
+
iou = inter / (a1 + a2 - inter + 1e-9)
|
| 652 |
+
best_j = int(np.argmax(iou))
|
| 653 |
+
best_iou = float(iou[best_j])
|
| 654 |
+
|
| 655 |
+
if confs_orig[i] >= PER_CONF_HIGH:
|
| 656 |
+
# High-conf original: auto-accept, boost with flip match
|
| 657 |
+
score = float(confs_orig[i])
|
| 658 |
+
if best_iou >= PER_CONSENSUS_IOU:
|
| 659 |
+
score = max(score, float(confs_flip[best_j]))
|
| 660 |
+
used_flip.add(best_j)
|
| 661 |
+
kept_b.append(boxes_orig[i])
|
| 662 |
+
kept_s.append(score)
|
| 663 |
+
else:
|
| 664 |
+
# Low-conf original: need confirmation from flip view
|
| 665 |
+
if best_iou >= PER_CONSENSUS_IOU:
|
| 666 |
+
score = max(float(confs_orig[i]), float(confs_flip[best_j]))
|
| 667 |
+
used_flip.add(best_j)
|
| 668 |
+
kept_b.append(boxes_orig[i])
|
| 669 |
+
kept_s.append(score)
|
| 670 |
+
# else: unconfirmed low-conf → drop
|
| 671 |
+
|
| 672 |
+
# Add unmatched high-conf flip boxes
|
| 673 |
+
for j in range(len(boxes_flip)):
|
| 674 |
+
if j not in used_flip and confs_flip[j] >= PER_CONF_HIGH:
|
| 675 |
+
kept_b.append(boxes_flip[j])
|
| 676 |
+
kept_s.append(float(confs_flip[j]))
|
| 677 |
|
| 678 |
if not kept_b:
|
| 679 |
return []
|
| 680 |
|
| 681 |
kept_b = np.array(kept_b)
|
| 682 |
kept_s = np.array(kept_s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
+
# Sanity filters (consensus merge handles dedup — no WBF needed)
|
| 685 |
img_area = float(oh * ow)
|
| 686 |
sane = []
|
| 687 |
+
for i in range(len(kept_b)):
|
| 688 |
+
bw = kept_b[i, 2] - kept_b[i, 0]
|
| 689 |
+
bh = kept_b[i, 3] - kept_b[i, 1]
|
| 690 |
if bw < PER_MIN_WH or bh < PER_MIN_WH:
|
| 691 |
continue
|
| 692 |
area = bw * bh
|
|
|
|
| 700 |
|
| 701 |
if not sane:
|
| 702 |
return []
|
| 703 |
+
kept_b = kept_b[sane]
|
| 704 |
+
kept_s = kept_s[sane]
|
| 705 |
|
| 706 |
out = []
|
| 707 |
+
for i in range(len(kept_b)):
|
| 708 |
+
b = kept_b[i]
|
| 709 |
out.append(BoundingBox(
|
| 710 |
+
x1=max(0, min(ow, int(b[0]))),
|
| 711 |
+
y1=max(0, min(oh, int(b[1]))),
|
| 712 |
+
x2=max(0, min(ow, int(b[2]))),
|
| 713 |
+
y2=max(0, min(oh, int(b[3]))),
|
| 714 |
cls_id=0,
|
| 715 |
+
conf=max(0.0, min(1.0, kept_s[i])),
|
| 716 |
))
|
| 717 |
return out
|
| 718 |
|
|
|
|
| 804 |
).start()
|
| 805 |
|
| 806 |
return results
|
| 807 |
+
# Miner v3.4 — consensus TTA, per-class NMS boost, vehicle sanity filters, floor/ceil decode 20260401
|
model_type.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"task_type": "object-detection", "model_type": "yolov11-small-dual", "deploy": "2026-03-27T09:00Z"}
|
person_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:aa6c46dca9dc995b8a641674e8f760b80d9af067bf2874e16a6addfd77a387bf
|
| 3 |
+
size 10042378
|
pyproject.toml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "onnx-runner-detection"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
requires-python = ">=3.9"
|
| 5 |
+
dependencies = [
|
| 6 |
+
"numpy>=1.23",
|
| 7 |
+
"onnxruntime>=1.16",
|
| 8 |
+
"opencv-python>=4.7",
|
| 9 |
+
"pillow>=9.5",
|
| 10 |
+
]
|