ScoreVision / miner.py
liberoaphpollo's picture
scorevision: push artifact
ef847ca verified
Raw History Blame Contribute Delete
6.07 kB
"""SN44 TurboVision public-track miner for a raw YOLO head (the owner's element_trainer baselines: YOLO11n, detect or
seg, output [1, 4 + NC (+ mask coefficients), N]), decoded exactly as the owner's own baseline miner decodes it.
The validator hands predict_batch OpenCV BGR uint8 frames. Each frame is converted to RGB, resized (plain stretch, no
letterbox) to the model's input size, scaled to 0..1, and run on the CPU with 2 threads. Per anchor: the arg-max of the
NC class scores (mask coefficients ignored), kept when it reaches CONF; one class-agnostic greedy NMS at IOU over all
kept boxes; boxes mapped back by the stretch factors, floored / ceiled to whole pixels and clipped to the frame.
CLASS_MAP maps the model's classes to the element's `objects` indices (-1 drops a class; empty: the same order).
"""
import math
from pathlib import Path
import cv2
import numpy as np
import onnxruntime as ort
from numpy import ndarray
from pydantic import BaseModel
MODEL_FILE = "model.onnx"
NC = 5
CONF = 0.25
IOU = 0.45
THREADS = 2
CLASS_MAP = ()
WARMUP_RUNS = 3
class BoundingBox(BaseModel):
x1: int
y1: int
x2: int
y2: int
cls_id: int
conf: float
class Polygon(BaseModel):
cls_id: int
conf: float
points: list[tuple[int, int]]
class TVFrameResult(BaseModel):
frame_id: int
boxes: list[BoundingBox] | None = None
polygons: list[Polygon] | None = None
keypoints: list[tuple[int, int]] | None = None
def _as_bgr(img) -> ndarray:
if not isinstance(img, np.ndarray) or img.ndim not in (2, 3) or img.size == 0:
raise ValueError(f"unsupported frame: {type(img).__name__} {getattr(img, 'shape', None)}")
if img.dtype != np.uint8:
img = np.clip(np.nan_to_num(img), 0, 255).astype(np.uint8)
if img.ndim == 2 or img.shape[2] == 1:
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
if img.shape[2] == 4:
return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
return img
def _nms(boxes: ndarray, scores: ndarray, iou_thr: float) -> list[int]:
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(int(i))
rest = order[1:]
xx1 = np.maximum(boxes[i, 0], boxes[rest, 0])
yy1 = np.maximum(boxes[i, 1], boxes[rest, 1])
xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1])
area_r = (boxes[rest, 2] - boxes[rest, 0]) * (boxes[rest, 3] - boxes[rest, 1])
iou = inter / np.maximum(area_i + area_r - inter, 1e-6)
order = rest[np.where(iou <= iou_thr)[0]]
return keep
class Miner:
def __init__(self, path_hf_repo: Path) -> None:
so = ort.SessionOptions()
so.intra_op_num_threads = THREADS
so.inter_op_num_threads = 1
so.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
so.add_session_config_entry("session.intra_op.allow_spinning", "0")
cv2.setNumThreads(1)
self.session = ort.InferenceSession(str(Path(path_hf_repo) / MODEL_FILE), sess_options=so,
providers=["CPUExecutionProvider"])
inp = self.session.get_inputs()[0]
self.input_name = inp.name
self.in_h, self.in_w = int(inp.shape[2]), int(inp.shape[3])
dummy = np.zeros((1024, 1024, 3), np.uint8)
for _ in range(WARMUP_RUNS):
self._frame_boxes(dummy)
def __repr__(self) -> str:
return f"{MODEL_FILE} @ {self.in_w}x{self.in_h} stretch, CPU {THREADS} threads, conf {CONF}, agnostic NMS {IOU}"
def _frame_boxes(self, img) -> list[BoundingBox]:
img = _as_bgr(img)
h, w = img.shape[:2]
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
x = cv2.resize(rgb, (self.in_w, self.in_h)).astype(np.float32) / 255.0
x = np.ascontiguousarray(np.transpose(x, (2, 0, 1))[None])
pred = self.session.run(None, {self.input_name: x})[0][0]
if pred.shape[0] < pred.shape[1]:
pred = pred.T
scores = pred[:, 4:4 + NC]
cls = scores.argmax(1)
conf = scores.max(1)
m = np.isfinite(pred[:, :4]).all(1) & (conf >= CONF)
b, cls, conf = pred[m, :4], cls[m], conf[m]
if b.shape[0] == 0:
return []
sx, sy = w / float(self.in_w), h / float(self.in_h)
xyxy = np.stack([(b[:, 0] - b[:, 2] / 2) * sx, (b[:, 1] - b[:, 3] / 2) * sy,
(b[:, 0] + b[:, 2] / 2) * sx, (b[:, 1] + b[:, 3] / 2) * sy], 1)
boxes = []
for i in _nms(xyxy, conf, IOU):
c = int(cls[i])
if CLASS_MAP:
if c >= len(CLASS_MAP) or CLASS_MAP[c] < 0:
continue
c = CLASS_MAP[c]
x1, y1, x2, y2 = xyxy[i]
ix1, iy1 = max(0, min(w, math.floor(x1))), max(0, min(h, math.floor(y1)))
ix2, iy2 = max(0, min(w, math.ceil(x2))), max(0, min(h, math.ceil(y2)))
if ix2 - ix1 < 1 or iy2 - iy1 < 1:
continue
boxes.append(BoundingBox(x1=ix1, y1=iy1, x2=ix2, y2=iy2, cls_id=c, conf=float(max(0.0, min(1.0, conf[i])))))
return boxes
def predict_batch(self, batch_images: list[ndarray], offset: int, n_keypoints: int) -> list[TVFrameResult]:
results: list[TVFrameResult] = []
for i, img in enumerate(batch_images):
try:
boxes = self._frame_boxes(img)
except Exception as exc: # one bad frame must not fail the whole challenge
print(f"inference_failure frame {offset + i}: {type(exc).__name__}: {exc}")
boxes = []
results.append(TVFrameResult(frame_id=offset + i, boxes=boxes, polygons=[],
keypoints=[(0, 0)] * int(n_keypoints) if n_keypoints else []))
return results