scorevision: push artifact
Browse files
miner.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SN44 public-track miner (our own). HF repo root must contain: miner.py, weights.onnx, sn44.json, chute_config.yml.
|
| 2 |
+
|
| 3 |
+
- Images arrive as BGR uint8 (cv2.imdecode in the chute template).
|
| 4 |
+
- Output cls_id = index into the element's manifest `objects` list (e.g. vehicle: car,bus,truck,motorcycle,van).
|
| 5 |
+
Our ONNX may have fewer classes (no 'van') -> `cls_map` in sn44.json maps onnx class -> manifest index.
|
| 6 |
+
- CPU onnxruntime with 2 threads, deterministic (the conformity checker re-runs us and compares boxes by IoU).
|
| 7 |
+
sn44.json: {"imgsz":1024, "conf": 0.30, "nms_iou": 0.6, "max_det": 300, "cls_map": [0], "threads": 2}
|
| 8 |
+
"""
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import cv2
|
| 13 |
+
import numpy as np
|
| 14 |
+
import onnxruntime as ort
|
| 15 |
+
from numpy import ndarray
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class BoundingBox(BaseModel):
|
| 20 |
+
x1: int
|
| 21 |
+
y1: int
|
| 22 |
+
x2: int
|
| 23 |
+
y2: int
|
| 24 |
+
cls_id: int
|
| 25 |
+
conf: float
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class TVFrameResult(BaseModel):
|
| 29 |
+
frame_id: int
|
| 30 |
+
boxes: list[BoundingBox] | None = None
|
| 31 |
+
keypoints: list[tuple[int, int]] | None = None
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class Miner:
|
| 35 |
+
def __init__(self, path_hf_repo: Path) -> None:
|
| 36 |
+
repo = Path(path_hf_repo)
|
| 37 |
+
cfg = json.loads((repo / "sn44.json").read_text())
|
| 38 |
+
self.size = int(cfg.get("imgsz", 1024))
|
| 39 |
+
self.conf = float(cfg.get("conf", 0.3))
|
| 40 |
+
self.conf_cls = {int(k): float(v) for k, v in (cfg.get("conf_per_class") or {}).items()}
|
| 41 |
+
self.iou = float(cfg.get("nms_iou", 0.6))
|
| 42 |
+
self.max_det = int(cfg.get("max_det", 300))
|
| 43 |
+
self.cls_map = list(cfg.get("cls_map", [0]))
|
| 44 |
+
so = ort.SessionOptions()
|
| 45 |
+
so.intra_op_num_threads = int(cfg.get("threads", 2)); so.inter_op_num_threads = 1
|
| 46 |
+
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 47 |
+
self.sess = ort.InferenceSession(str(repo / "weights.onnx"), sess_options=so, providers=["CPUExecutionProvider"])
|
| 48 |
+
self.inp = self.sess.get_inputs()[0].name
|
| 49 |
+
|
| 50 |
+
def __repr__(self) -> str:
|
| 51 |
+
return f"SN44Miner(size={self.size}, conf={self.conf}, classes={len(self.cls_map)})"
|
| 52 |
+
|
| 53 |
+
def _letterbox(self, img):
|
| 54 |
+
h, w = img.shape[:2]
|
| 55 |
+
r = min(self.size / h, self.size / w)
|
| 56 |
+
nh, nw = int(round(h * r)), int(round(w * r))
|
| 57 |
+
top, left = (self.size - nh) // 2, (self.size - nw) // 2
|
| 58 |
+
canvas = np.full((self.size, self.size, 3), 114, np.uint8)
|
| 59 |
+
canvas[top:top + nh, left:left + nw] = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
|
| 60 |
+
x = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).transpose(2, 0, 1)[None].astype(np.float32) / 255.0
|
| 61 |
+
return x, r, left, top
|
| 62 |
+
|
| 63 |
+
def _detect(self, img):
|
| 64 |
+
h, w = img.shape[:2]
|
| 65 |
+
x, r, left, top = self._letterbox(img)
|
| 66 |
+
y = self.sess.run(None, {self.inp: x})[0][0] # ultralytics: (4+nc, N) cx,cy,w,h,scores
|
| 67 |
+
if y.shape[0] > y.shape[1]:
|
| 68 |
+
y = y.T
|
| 69 |
+
scores = y[4:]
|
| 70 |
+
cls = scores.argmax(0); conf = scores.max(0)
|
| 71 |
+
keep = conf >= min([self.conf] + list(self.conf_cls.values()))
|
| 72 |
+
if not keep.any():
|
| 73 |
+
return []
|
| 74 |
+
b = y[:4, keep].T; cls = cls[keep]; conf = conf[keep]
|
| 75 |
+
xyxy = np.stack([b[:, 0] - b[:, 2] / 2, b[:, 1] - b[:, 3] / 2, b[:, 0] + b[:, 2] / 2, b[:, 1] + b[:, 3] / 2], 1)
|
| 76 |
+
xyxy[:, [0, 2]] = ((xyxy[:, [0, 2]] - left) / r).clip(0, w)
|
| 77 |
+
xyxy[:, [1, 3]] = ((xyxy[:, [1, 3]] - top) / r).clip(0, h)
|
| 78 |
+
out = []
|
| 79 |
+
for c in np.unique(cls): # class-wise NMS
|
| 80 |
+
m = cls == c
|
| 81 |
+
bb = xyxy[m]; cc = conf[m]
|
| 82 |
+
idx = cv2.dnn.NMSBoxes([[float(a), float(b_), float(c_ - a), float(d - b_)] for a, b_, c_, d in bb],
|
| 83 |
+
cc.astype(np.float32).tolist(), 0.0, self.iou)
|
| 84 |
+
if int(c) >= len(self.cls_map) or self.cls_map[int(c)] < 0: # -1 = drop this onnx class
|
| 85 |
+
continue
|
| 86 |
+
for i in np.asarray(idx, dtype=int).reshape(-1):
|
| 87 |
+
if cc[i] < self.conf_cls.get(int(c), self.conf):
|
| 88 |
+
continue
|
| 89 |
+
x1, y1, x2, y2 = bb[i]
|
| 90 |
+
out.append(BoundingBox(x1=int(round(x1)), y1=int(round(y1)), x2=int(round(x2)), y2=int(round(y2)),
|
| 91 |
+
cls_id=int(self.cls_map[int(c)]), conf=float(round(float(cc[i]), 5))))
|
| 92 |
+
out.sort(key=lambda b: -b.conf)
|
| 93 |
+
return out[: self.max_det]
|
| 94 |
+
|
| 95 |
+
def predict_batch(self, batch_images: list[ndarray], offset: int, n_keypoints: int) -> list[TVFrameResult]:
|
| 96 |
+
return [TVFrameResult(frame_id=offset + i, boxes=self._detect(img), keypoints=[(0, 0)] * n_keypoints)
|
| 97 |
+
for i, img in enumerate(batch_images)]
|