meaculpitt commited on
Commit
128fb23
·
verified ·
1 Parent(s): 52a0bb5

scorevision: push artifact

Browse files
Files changed (7) hide show
  1. README.md +10 -0
  2. chute_config.yml +2 -8
  3. class_names.txt +5 -0
  4. miner.py +175 -97
  5. model_type.json +1 -0
  6. person_weights.onnx +2 -2
  7. 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: 900
19
  concurrency: 4
20
  max_instances: 5
21
  scaling_threshold: 0.5
22
- shutdown_after_seconds: 288000
 
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
- VEH_CONF_PER_CLASS = {0: 0.15, 1: 0.30, 2: 0.20, 3: 0.15}
167
- VEH_CONF_DEFAULT = 0.35
168
  VEH_TTA_CONF = 0.10
169
- VEH_WBF_IOU = 0.40
 
 
 
 
 
 
 
170
 
171
  # ── Person config (TTA consensus) ───────────────────────────────────────────
172
- PER_CONF_LOW = 0.32
173
- PER_CONF_HIGH = 0.55
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 = 8.0
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
- _collect(*self._veh_run_pass(image_bgr, VEH_TTA_CONF))
441
  flipped = cv2.flip(image_bgr, 1)
442
- bx, sc, cl = self._veh_run_pass(flipped, VEH_TTA_CONF)
443
- if len(bx):
444
- bx[:, 0], bx[:, 2] = ow - bx[:, 2], ow - bx[:, 0]
445
- _collect(bx, sc, cl)
 
 
 
 
 
 
446
  else:
447
- bx, confs, cls_ids = self._veh_run_pass(image_bgr, VEH_TTA_CONF)
448
- _collect(bx, confs, cls_ids)
449
 
450
- if not all_b:
451
  return []
452
 
453
- fb, fs, fl = _wbf_multi(all_b, all_s, all_l, iou_thr=VEH_WBF_IOU, skip_thr=WBF_SKIP_THR)
 
454
 
455
- if len(fb) == 0:
456
- return []
 
457
 
458
- fb[:, [0, 2]] *= ow
459
- fb[:, [1, 3]] *= oh
460
 
461
- keep = np.array([
462
- fs[i] >= VEH_CONF_PER_CLASS.get(int(fl[i]), VEH_CONF_DEFAULT)
463
- for i in range(len(fs))
464
- ])
465
  if not keep.any():
466
  return []
467
- fb, fs, fl = fb[keep], fs[keep], fl[keep]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
  out = []
470
- for i in range(len(fb)):
471
- b = fb[i]
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(fl[i]),
478
- conf=max(0.0, min(1.0, float(fs[i]))),
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
- img_r = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
 
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[:, 2] = (boxes[:, 2] - pl) / ratio
519
- boxes[:, 1] = (boxes[:, 1] - pt) / ratio
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
- for i in range(len(boxes_orig)):
568
- kept_b.append(boxes_orig[i])
569
- kept_s.append(confs_orig[i])
570
-
571
- if has_flip:
572
- for i in range(len(boxes_flip)):
573
- if confs_flip[i] < PER_CONF_HIGH:
574
- continue
575
- if len(boxes_orig) > 0:
576
- xx1 = np.maximum(boxes_flip[i, 0], boxes_orig[:, 0])
577
- yy1 = np.maximum(boxes_flip[i, 1], boxes_orig[:, 1])
578
- xx2 = np.minimum(boxes_flip[i, 2], boxes_orig[:, 2])
579
- yy2 = np.minimum(boxes_flip[i, 3], boxes_orig[:, 3])
580
- inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
581
- a1 = (boxes_flip[i, 2] - boxes_flip[i, 0]) * (boxes_flip[i, 3] - boxes_flip[i, 1])
582
- a2 = (boxes_orig[:, 2] - boxes_orig[:, 0]) * (boxes_orig[:, 3] - boxes_orig[:, 1])
583
- iou = inter / (a1 + a2 - inter + 1e-9)
584
- if np.max(iou) >= 0.3:
585
- continue
586
- kept_b.append(boxes_flip[i])
587
- kept_s.append(confs_flip[i])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(fb)):
610
- bw = fb[i, 2] - fb[i, 0]
611
- bh = fb[i, 3] - fb[i, 1]
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
- fb = fb[sane]
626
- fs = fs[sane]
627
 
628
  out = []
629
- for i in range(len(fb)):
630
- b = fb[i]
631
  out.append(BoundingBox(
632
- x1=max(0, min(ow, math.floor(b[0]))),
633
- y1=max(0, min(oh, math.floor(b[1]))),
634
- x2=max(0, min(ow, math.ceil(b[2]))),
635
- y2=max(0, min(oh, math.ceil(b[3]))),
636
  cls_id=0,
637
- conf=max(0.0, min(1.0, float(fs[i]))),
638
  ))
639
  return out
640
 
@@ -726,4 +804,4 @@ class Miner:
726
  ).start()
727
 
728
  return results
729
- # Miner v3.2 — letterbox person preprocessing + YOLO26s 960px 20260331
 
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:b23ca9c0a7efd6613753b66033d7c6637b338a79547562687d5e58ba5fb92d7c
3
- size 19258183
 
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
+ ]