test / miner.py
Phoenix-95107's picture
scorevision: push artifact
ce65e38 verified
Raw History Blame Contribute Delete
29.1 kB
import os
import math
from pathlib import Path
import cv2
import numpy as np
import onnxruntime as ort
from numpy import ndarray
from pydantic import BaseModel
class BoundingBox(BaseModel):
x1: int
y1: int
x2: int
y2: int
cls_id: int
conf: float
class TVFrameResult(BaseModel):
frame_id: int
boxes: list[BoundingBox]
polygons: list | None = None
keypoints: list[tuple[int, int]]
class Miner:
def __init__(self, path_hf_repo: Path) -> None:
cv2.setNumThreads(1)
model_path = self._resolve_model_path(path_hf_repo)
self.class_names = ['car', 'bus', 'truck', 'motorcycle']
self._model_class_order = ['car', 'bus', 'truck', 'motorcycle']
print('ORT version:', ort.__version__)
print('ORT available providers:', ort.get_available_providers())
self._model_path = str(model_path)
self.session = self._open_session(self._model_path)
print('ORT session providers:', self.session.get_providers())
self.sink_name = 'tuktuk'
self.sink_margin = 0.05
self.sink_index = None
self.vehicle_cols = None
model_class_order = self._read_model_class_order()
if model_class_order is None:
model_class_order = list(self._model_class_order)
print(f'cls order: no usable ONNX metadata, FALLBACK {model_class_order}')
else:
print(f'cls order: from ONNX metadata {model_class_order}')
if self.sink_name in model_class_order:
self.sink_index = model_class_order.index(self.sink_name)
self.vehicle_cols = np.array([model_class_order.index(n) for n in self.class_names], dtype=np.int32)
print(f'cls order: drop {self.sink_name} col {self.sink_index} when score + {self.sink_margin} >= vehicle')
self.cls_remap = np.full(len(model_class_order), -1, dtype=np.int32)
for i, name in enumerate(model_class_order):
if name in self.class_names:
self.cls_remap[i] = self.class_names.index(name)
for inp in self.session.get_inputs():
print('INPUT:', inp.name, inp.shape, inp.type)
for out in self.session.get_outputs():
print('OUTPUT:', out.name, out.shape, out.type)
self.input_name = self.session.get_inputs()[0].name
self.output_names = [o.name for o in self.session.get_outputs()]
self.input_shape = self.session.get_inputs()[0].shape
input_type = self.session.get_inputs()[0].type
self.np_dtype = np.float16 if 'float16' in input_type else np.float32
print(f'ONNX input dtype: {input_type} -> numpy {self.np_dtype}')
self.input_height = self._safe_dim(self.input_shape[2], default=704)
self.input_width = self._safe_dim(self.input_shape[3], default=704)
self._iou_thres_array = np.array([0.37, 0.4, 0.38, 0.35], dtype=np.float32)
self.cross_iou_thresh = 0.7
self.max_det = 300
self.pre_nms_topk = 1000
self.use_tta = False
self.use_cluster_boost = True
self.use_soft_nms = False
self.soft_nms_sigma = 0.5
self._conf_thres_array = np.array([0.35, 0.2, 0.5, 0.45], dtype=np.float32)
self._extra_conf_thres_array = np.array([0.25, 0.2, 0.2, 0.2], dtype=np.float32)
self._bonus_array = np.array([0, 0, 0.15, 0], dtype=np.float32)
self._min_box_area_array = np.array([64, 12, 12, 12], dtype=np.float32)
self.min_side = 3
self.max_aspect_ratio = 20
self.max_area_frac = 0.95
self.square_pad_frac = 0
self.dual_label_pair = ('bus', 'truck')
self.dual_label_below = 0
self._avg_iou = float(np.mean(self._iou_thres_array))
print(f'ONNX model loaded from: {model_path}')
print(f'ONNX input: name={self.input_name}, shape={self.input_shape}')
self._warmup()
def _session_options(self) -> ort.SessionOptions:
options = ort.SessionOptions()
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
options.intra_op_num_threads = 2
options.inter_op_num_threads = 1
options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
return options
def _open_session(self, model_path: str) -> ort.InferenceSession:
# CPU is the default. onnxruntime-gpu session creation can abort the
# process on a library mismatch, which leaves the chute unhealthy and
# the scorer records a zero with no predictions.
use_cuda = os.environ.get('MINER_USE_CUDA', '') == '1' and os.environ.get('MINER_FORCE_CPU', '') != '1'
if use_cuda and 'CUDAExecutionProvider' in ort.get_available_providers():
try:
session = ort.InferenceSession(model_path, sess_options=self._session_options(), providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
self._probe_session(session)
print('ORT session: CUDA probe ok')
return session
except Exception as e:
print(f'CUDA session unusable, using CPU: {e}')
session = ort.InferenceSession(model_path, sess_options=self._session_options(), providers=['CPUExecutionProvider'])
self._probe_session(session)
print('ORT session: CPU probe ok')
return session
@staticmethod
def _probe_session(session: ort.InferenceSession) -> None:
inp = session.get_inputs()[0]
dims = list(inp.shape)
if len(dims) != 4:
shape = [1, 3, 704, 704]
else:
shape = []
for axis, dim in enumerate(dims):
if isinstance(dim, int) and dim > 0:
shape.append(dim)
elif axis == 1:
shape.append(3)
elif axis == 0:
shape.append(1)
else:
shape.append(704)
dtype = np.float16 if 'float16' in inp.type else np.float32
session.run(None, {inp.name: np.zeros(shape, dtype=dtype)})
def _run(self, tensor: np.ndarray) -> list:
try:
return self.session.run(self.output_names, {self.input_name: tensor})
except Exception as e:
if self.session.get_providers() == ['CPUExecutionProvider']:
raise
print(f'inference on {self.session.get_providers()} failed, switching to CPU: {e}')
self.session = self._open_session_cpu()
return self.session.run(self.output_names, {self.input_name: tensor})
def _open_session_cpu(self) -> ort.InferenceSession:
session = ort.InferenceSession(self._model_path, sess_options=self._session_options(), providers=['CPUExecutionProvider'])
self._probe_session(session)
return session
def _warmup(self, iters: int=1) -> None:
dummy = np.zeros((1024, 1024, 3), dtype=np.uint8)
for _ in range(max(1, iters)):
self.predict_batch(batch_images=[dummy], offset=0, n_keypoints=0)
print(f'warmup: {max(1, iters)} dummy predict_batch call(s) done')
def __repr__(self) -> str:
return f'PetrolMiner(classes={len(self.class_names)}, providers={self.session.get_providers()})'
@staticmethod
def _safe_dim(value, default: int) -> int:
return value if isinstance(value, int) and value > 0 else default
@staticmethod
def _resolve_model_path(repo: Path) -> Path:
for name in ('weights.onnx', 'weights_int8.onnx'):
p = repo / name
if p.exists():
if name != 'weights.onnx':
print(f'model: weights.onnx not found, using {name}')
return p
candidates = sorted(repo.glob('*.onnx'))
if candidates:
print(f'model: using {candidates[0].name}')
return candidates[0]
return repo / 'weights.onnx'
def _read_model_class_order(self) -> list[str] | None:
try:
import ast
meta = self.session.get_modelmeta().custom_metadata_map
names = ast.literal_eval(meta['names'])
order = [str(names[i]) for i in sorted(names)] if isinstance(names, dict) else [str(n) for n in names]
except Exception as e:
print(f'cls order: could not read ONNX names metadata ({e})')
return None
lower = [n.lower() for n in order]
vehicles = [n for n in lower if n != self.sink_name]
if sorted(vehicles) != sorted(self.class_names):
print(f'cls order: ONNX names {order} != expected {self.class_names}; ignoring')
return None
return lower
def _letterbox(self, image: ndarray, new_shape: tuple[int, int], color=(114, 114, 114)) -> tuple[ndarray, float, tuple[float, float]]:
h, w = image.shape[:2]
new_w, new_h = new_shape
ratio = min(new_w / w, new_h / h)
rw, rh = (int(round(w * ratio)), int(round(h * ratio)))
if (rw, rh) != (w, h):
interp = cv2.INTER_CUBIC if ratio > 1.0 else cv2.INTER_LINEAR
image = cv2.resize(image, (rw, rh), interpolation=interp)
dw = (new_w - rw) / 2.0
dh = (new_h - rh) / 2.0
left, right = (int(round(dw - 0.1)), int(round(dw + 0.1)))
top, bottom = (int(round(dh - 0.1)), int(round(dh + 0.1)))
padded = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
if padded.shape[0] != new_h or padded.shape[1] != new_w:
padded = cv2.resize(padded, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
return (padded, ratio, (dw, dh))
def _preprocess(self, image: ndarray) -> tuple[np.ndarray, dict]:
orig_h, orig_w = image.shape[:2]
extra_left = extra_right = 0
if self.square_pad_frac > 0.0 and orig_w == orig_h:
target_w = int(orig_w * (1.0 + self.square_pad_frac))
if target_w > orig_w:
total = target_w - orig_w
extra_left = total // 2
extra_right = total - extra_left
image = cv2.copyMakeBorder(image, 0, 0, extra_left, extra_right, cv2.BORDER_CONSTANT, value=(114, 114, 114))
img, ratio, pad = self._letterbox(image, (self.input_width, self.input_height))
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32) / 255.0
img = np.transpose(img, (2, 0, 1))[None, ...]
img = np.ascontiguousarray(img, dtype=self.np_dtype)
return (img, {'ratio': ratio, 'pad': pad, 'orig_size': (orig_w, orig_h), 'extra_left': extra_left, 'extra_right': extra_right})
@staticmethod
def _clip_boxes(boxes: np.ndarray, image_size: tuple[int, int]) -> np.ndarray:
w, h = image_size
boxes[:, 0] = np.clip(boxes[:, 0], 0, w - 1)
boxes[:, 1] = np.clip(boxes[:, 1], 0, h - 1)
boxes[:, 2] = np.clip(boxes[:, 2], 0, w - 1)
boxes[:, 3] = np.clip(boxes[:, 3], 0, h - 1)
return boxes
@staticmethod
def _xywh_to_xyxy(boxes: np.ndarray) -> np.ndarray:
out = np.empty_like(boxes)
out[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0
out[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0
out[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0
out[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0
return out
@staticmethod
def _hard_nms(boxes: np.ndarray, scores: np.ndarray, iou_thresh: float) -> np.ndarray:
n = len(boxes)
if n == 0:
return np.array([], dtype=np.intp)
x1, y1, x2, y2 = (boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3])
areas = np.maximum(0.0, x2 - x1) * np.maximum(0.0, y2 - y1)
order = np.argsort(-scores)
keep = []
while order.size > 0:
i = int(order[0])
keep.append(i)
if order.size == 1:
break
rest = order[1:]
xx1 = np.maximum(x1[i], x1[rest])
yy1 = np.maximum(y1[i], y1[rest])
xx2 = np.minimum(x2[i], x2[rest])
yy2 = np.minimum(y2[i], y2[rest])
inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
iou = inter / (areas[i] + areas[rest] - inter + 1e-07)
order = rest[iou <= iou_thresh]
return np.array(keep, dtype=np.intp)
@staticmethod
def _soft_nms(boxes: np.ndarray, scores: np.ndarray, sigma: float) -> tuple[np.ndarray, np.ndarray]:
n = len(boxes)
if n == 0:
return (np.array([], dtype=np.intp), np.array([], dtype=np.float32))
b = boxes.astype(np.float32, copy=True)
sc = scores.astype(np.float32, copy=True)
order = np.arange(n)
for i in range(n):
mp = i + int(np.argmax(sc[i:]))
b[[i, mp]] = b[[mp, i]]
sc[[i, mp]] = sc[[mp, i]]
order[[i, mp]] = order[[mp, i]]
if i + 1 >= n:
break
xx1 = np.maximum(b[i, 0], b[i + 1:, 0])
yy1 = np.maximum(b[i, 1], b[i + 1:, 1])
xx2 = np.minimum(b[i, 2], b[i + 1:, 2])
yy2 = np.minimum(b[i, 3], b[i + 1:, 3])
inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
ai = max(0.0, float((b[i, 2] - b[i, 0]) * (b[i, 3] - b[i, 1])))
aj = np.maximum(0.0, b[i + 1:, 2] - b[i + 1:, 0]) * np.maximum(0.0, b[i + 1:, 3] - b[i + 1:, 1])
iou = inter / (ai + aj - inter + 1e-07)
sc[i + 1:] *= np.exp(-iou ** 2 / sigma)
return (order, sc)
def _per_class_soft_nms(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
keep_idx: list[int] = []
keep_sc: list[float] = []
for c in np.unique(cls_ids):
idx = np.where(cls_ids == c)[0]
order, dec = self._soft_nms(boxes[idx], scores[idx], self.soft_nms_sigma)
thr = float(self._conf_thres_array[c])
for k, s_dec in zip(order, dec):
if s_dec >= thr:
keep_idx.append(int(idx[k]))
keep_sc.append(float(s_dec))
return (np.array(keep_idx, dtype=np.intp), np.array(keep_sc, dtype=np.float32))
def _per_class_hard_nms(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray) -> np.ndarray:
if len(boxes) == 0:
return np.array([], dtype=np.intp)
all_keep: list[int] = []
for c in np.unique(cls_ids):
idx = np.where(cls_ids == c)[0]
cls_iou = float(self._iou_thres_array[c])
keep = self._hard_nms(boxes[idx], scores[idx], cls_iou)
all_keep.extend(idx[keep].tolist())
all_keep.sort()
return np.array(all_keep, dtype=np.intp)
@staticmethod
def _max_score_per_cluster(post_boxes: np.ndarray, post_cls: np.ndarray, full_boxes: np.ndarray, full_scores: np.ndarray, full_cls: np.ndarray, iou_thresh: float) -> np.ndarray:
n = len(post_boxes)
if n == 0:
return np.empty(0, dtype=np.float32)
m = len(full_boxes)
if m == 0:
return np.zeros(n, dtype=np.float32)
pa = np.maximum(0.0, post_boxes[:, 2] - post_boxes[:, 0]) * np.maximum(0.0, post_boxes[:, 3] - post_boxes[:, 1])
fa = np.maximum(0.0, full_boxes[:, 2] - full_boxes[:, 0]) * np.maximum(0.0, full_boxes[:, 3] - full_boxes[:, 1])
xx1 = np.maximum(post_boxes[:, 0][:, None], full_boxes[:, 0][None, :])
yy1 = np.maximum(post_boxes[:, 1][:, None], full_boxes[:, 1][None, :])
xx2 = np.minimum(post_boxes[:, 2][:, None], full_boxes[:, 2][None, :])
yy2 = np.minimum(post_boxes[:, 3][:, None], full_boxes[:, 3][None, :])
inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
iou = inter / (pa[:, None] + fa[None, :] - inter + 1e-07)
mask = (iou >= iou_thresh) & (post_cls[:, None] == full_cls[None, :])
tiled = np.where(mask, full_scores[None, :], -np.inf)
out = tiled.max(axis=1)
out[~np.isfinite(out)] = 0.0
return out.astype(np.float32)
def _conf_filter_mask(self, scores: np.ndarray, cls_ids: np.ndarray, extra_left: int) -> np.ndarray:
if len(scores) == 0:
return np.zeros(0, dtype=bool)
thr_arr = self._extra_conf_thres_array if extra_left > 0 else self._conf_thres_array
keep = scores >= thr_arr[cls_ids]
for c in np.unique(cls_ids):
b = float(self._bonus_array[c])
if b <= 0.0:
continue
cm = cls_ids == c
if keep[cm].any():
continue
idx = np.where(cm)[0]
top = int(idx[int(np.argmax(scores[idx]))])
if scores[top] >= float(self._conf_thres_array[c]) - b:
keep[top] = True
return keep
def _filter_sane_boxes(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray, orig_size: tuple[int, int]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
if len(boxes) == 0:
return (boxes, scores, cls_ids)
orig_w, orig_h = orig_size
image_area = float(orig_w * orig_h)
bw = np.maximum(0.0, boxes[:, 2] - boxes[:, 0])
bh = np.maximum(0.0, boxes[:, 3] - boxes[:, 1])
area = bw * bh
ar = np.maximum(bw / np.maximum(bh, 1e-06), bh / np.maximum(bw, 1e-06))
keep = (bw >= self.min_side) & (bh >= self.min_side) & (area >= self._min_box_area_array[cls_ids]) & (area <= self.max_area_frac * image_area) & (ar <= self.max_aspect_ratio)
return (boxes[keep], scores[keep], cls_ids[keep])
def _cross_class_dedup_op(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray, iou_thresh: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
n = len(boxes)
if n <= 1:
return (boxes, scores, cls_ids)
boxes = np.asarray(boxes, dtype=np.float32)
scores = np.asarray(scores, dtype=np.float32)
cls_ids = np.asarray(cls_ids, dtype=np.int32)
areas = np.maximum(0.0, boxes[:, 2] - boxes[:, 0]) * np.maximum(0.0, boxes[:, 3] - boxes[:, 1])
margins = scores - self._conf_thres_array[cls_ids]
order = np.lexsort((-areas, -margins))
suppressed = np.zeros(n, dtype=bool)
keep: list[int] = []
for i in order:
if suppressed[i]:
continue
keep.append(int(i))
bi = boxes[i]
xx1 = np.maximum(bi[0], boxes[:, 0])
yy1 = np.maximum(bi[1], boxes[:, 1])
xx2 = np.minimum(bi[2], boxes[:, 2])
yy2 = np.minimum(bi[3], boxes[:, 3])
inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
a_i = max(1e-07, float((bi[2] - bi[0]) * (bi[3] - bi[1])))
iou = inter / (a_i + areas - inter + 1e-07)
dup = iou > iou_thresh
dup[i] = False
suppressed |= dup
keep_idx = np.array(keep, dtype=np.intp)
return (boxes[keep_idx], scores[keep_idx], cls_ids[keep_idx])
def _apply_dual_label(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
if self.dual_label_below <= 0.0 or len(boxes) == 0:
return (boxes, scores, cls_ids)
a = self.class_names.index(self.dual_label_pair[0])
b = self.class_names.index(self.dual_label_pair[1])
sib = np.full(len(self.class_names), -1, dtype=np.int32)
sib[a], sib[b] = (b, a)
mask = (sib[cls_ids] >= 0) & (scores < self.dual_label_below)
if not mask.any():
return (boxes, scores, cls_ids)
boxes = np.concatenate([boxes, boxes[mask]], axis=0)
scores = np.concatenate([scores, scores[mask]], axis=0)
cls_ids = np.concatenate([cls_ids, sib[cls_ids[mask]]], axis=0)
return (boxes, scores, cls_ids)
def _per_view_pipeline(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray, orig_size: tuple[int, int]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
boxes, scores, cls_ids = self._filter_sane_boxes(boxes, scores, cls_ids, orig_size)
if len(boxes) == 0:
return (boxes, scores, cls_ids)
if len(scores) > self.pre_nms_topk:
top = np.argpartition(-scores, self.pre_nms_topk)[:self.pre_nms_topk]
boxes, scores, cls_ids = (boxes[top], scores[top], cls_ids[top])
if len(boxes) > 1:
if self.use_soft_nms:
keep, scores = self._per_class_soft_nms(boxes, scores, cls_ids)
boxes, cls_ids = (boxes[keep], cls_ids[keep])
else:
keep = self._per_class_hard_nms(boxes, scores, cls_ids)
boxes, scores, cls_ids = (boxes[keep], scores[keep], cls_ids[keep])
if len(scores) > self.max_det:
top = np.argsort(-scores)[:self.max_det]
boxes, scores, cls_ids = (boxes[top], scores[top], cls_ids[top])
if len(boxes) > 1:
boxes, scores, cls_ids = self._cross_class_dedup_op(boxes, scores, cls_ids, self.cross_iou_thresh)
return (boxes, scores, cls_ids)
def _unmap_and_finish(self, boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray, meta: dict) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
pad_w, pad_h = meta['pad']
ratio = meta['ratio']
orig_w, orig_h = meta['orig_size']
extra_left, extra_right = (meta['extra_left'], meta['extra_right'])
boxes[:, [0, 2]] -= pad_w
boxes[:, [1, 3]] -= pad_h
boxes /= ratio
if extra_left:
boxes[:, [0, 2]] -= extra_left
if extra_left or extra_right:
cx = (boxes[:, 0] + boxes[:, 2]) * 0.5
inside = (cx >= 0) & (cx <= orig_w)
boxes, scores, cls_ids = (boxes[inside], scores[inside], cls_ids[inside])
if len(boxes) == 0:
return (boxes, scores, cls_ids)
boxes = self._clip_boxes(boxes, (orig_w, orig_h))
return self._per_view_pipeline(boxes, scores, cls_ids, (orig_w, orig_h))
def _decode_final_dets(self, preds: np.ndarray, meta: dict) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
empty = (np.empty((0, 4), np.float32), np.empty(0, np.float32), np.empty(0, np.int32))
if preds.ndim == 3 and preds.shape[0] == 1:
preds = preds[0]
if preds.ndim != 2 or preds.shape[1] < 6:
raise ValueError(f'Unexpected final-det output shape: {preds.shape}')
boxes = preds[:, :4].astype(np.float32)
scores = preds[:, 4].astype(np.float32)
raw_cls = preds[:, 5].astype(np.int32)
raw_cls = np.clip(raw_cls, 0, len(self.cls_remap) - 1)
cls_ids = self.cls_remap[raw_cls]
known = cls_ids >= 0
boxes, scores, cls_ids = (boxes[known], scores[known], cls_ids[known])
keep = self._conf_filter_mask(scores, cls_ids, meta['extra_left'])
boxes, scores, cls_ids = (boxes[keep], scores[keep], cls_ids[keep])
if len(boxes) == 0:
return empty
return self._unmap_and_finish(boxes, scores, cls_ids, meta)
def _decode_raw_yolo(self, preds: np.ndarray, meta: dict) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
empty = (np.empty((0, 4), np.float32), np.empty(0, np.float32), np.empty(0, np.int32))
if preds.ndim != 3 or preds.shape[0] != 1:
raise ValueError(f'Unexpected raw output shape: {preds.shape}')
preds = preds[0]
if preds.shape[0] <= 16 and preds.shape[1] > preds.shape[0]:
preds = preds.T
if preds.ndim != 2 or preds.shape[1] < 5:
raise ValueError(f'Unexpected normalized raw shape: {preds.shape}')
boxes_xywh = preds[:, :4].astype(np.float32)
cls_part = preds[:, 4:].astype(np.float32)
if cls_part.shape[1] == 1:
scores = cls_part[:, 0]
cls_ids = np.zeros(len(scores), dtype=np.int32)
elif self.vehicle_cols is not None and cls_part.shape[1] > int(self.vehicle_cols.max()):
veh = cls_part[:, self.vehicle_cols]
cls_ids = np.argmax(veh, axis=1).astype(np.int32)
scores = veh[np.arange(len(veh)), cls_ids]
if self.sink_index is not None and self.sink_index < cls_part.shape[1]:
keep_veh = cls_part[:, self.sink_index] + self.sink_margin < scores
boxes_xywh, scores, cls_ids = (boxes_xywh[keep_veh], scores[keep_veh], cls_ids[keep_veh])
else:
cls_ids = np.argmax(cls_part, axis=1).astype(np.int32)
scores = cls_part[np.arange(len(cls_part)), cls_ids]
cls_ids = self.cls_remap[np.clip(cls_ids, 0, len(self.cls_remap) - 1)]
keep_known = cls_ids >= 0
boxes_xywh, scores, cls_ids = (boxes_xywh[keep_known], scores[keep_known], cls_ids[keep_known])
keep = self._conf_filter_mask(scores, cls_ids, meta['extra_left'])
boxes_xywh, scores, cls_ids = (boxes_xywh[keep], scores[keep], cls_ids[keep])
if len(boxes_xywh) == 0:
return empty
boxes = self._xywh_to_xyxy(boxes_xywh)
return self._unmap_and_finish(boxes, scores, cls_ids, meta)
def _decode(self, output: np.ndarray, meta: dict) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
if output.ndim == 2 and output.shape[1] >= 6:
return self._decode_final_dets(output, meta)
if output.ndim == 3 and output.shape[0] == 1 and (output.shape[2] == 6):
return self._decode_final_dets(output, meta)
return self._decode_raw_yolo(output, meta)
def _predict_single_arrays(self, image: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray, dict]:
if not isinstance(image, np.ndarray) or image.ndim != 3 or image.shape[2] != 3:
raise ValueError(f"Expected HWC BGR image, got {type(image)} {getattr(image, 'shape', None)}")
if image.dtype != np.uint8:
image = image.astype(np.uint8)
inp, meta = self._preprocess(image)
outputs = self._run(inp)
boxes, scores, cls_ids = self._decode(outputs[0], meta)
return (boxes, scores, cls_ids, meta)
@staticmethod
def _to_boxes(boxes: np.ndarray, scores: np.ndarray, cls_ids: np.ndarray, orig_size: tuple[int, int]) -> list[BoundingBox]:
results: list[BoundingBox] = []
orig_w, orig_h = orig_size
for box, conf, cls_id in zip(boxes, scores, cls_ids):
x1, y1, x2, y2 = box.tolist()
if x2 <= x1 or y2 <= y1:
continue
results.append(BoundingBox(x1=max(0, min(orig_w, int(math.floor(x1)))), y1=max(0, min(orig_h, int(math.floor(y1)))), x2=max(0, min(orig_w, int(math.ceil(x2)))), y2=max(0, min(orig_h, int(math.ceil(y2)))), cls_id=int(cls_id), conf=float(max(0.0, min(1.0, conf)))))
return results
def _infer_single(self, image: np.ndarray) -> list[BoundingBox]:
boxes, scores, cls_ids, meta = self._predict_single_arrays(image)
if len(boxes) == 0:
return []
if self.use_cluster_boost and len(boxes) > 1:
scores = self._max_score_per_cluster(boxes, cls_ids, boxes, scores, cls_ids, self._avg_iou)
boxes, scores, cls_ids = self._apply_dual_label(boxes, scores, cls_ids)
return self._to_boxes(boxes, scores, cls_ids, meta['orig_size'])
def _infer_tta(self, image: np.ndarray) -> list[BoundingBox]:
b0, s0, c0, meta = self._predict_single_arrays(image)
w = image.shape[1]
bf, sf, cf, _ = self._predict_single_arrays(cv2.flip(image, 1))
if len(bf):
bf = bf.copy()
bf[:, [0, 2]] = w - bf[:, [2, 0]]
coords = np.concatenate([b0, bf], axis=0) if len(bf) else b0
scores = np.concatenate([s0, sf], axis=0) if len(sf) else s0
cls_ids = np.concatenate([c0, cf], axis=0) if len(cf) else c0
if len(coords) == 0:
return []
keep = self._per_class_hard_nms(coords, scores, cls_ids)
if len(keep) == 0:
return []
if len(keep) > self.max_det:
keep = keep[np.argsort(-scores[keep])[:self.max_det]]
boosted = self._max_score_per_cluster(coords[keep], cls_ids[keep], coords, scores, cls_ids, self._avg_iou)
kb, kc = (coords[keep], cls_ids[keep])
if len(kb) > 1:
kb, boosted, kc = self._cross_class_dedup_op(kb, boosted, kc, self.cross_iou_thresh)
kb, boosted, kc = self._apply_dual_label(kb, boosted, kc)
return self._to_boxes(kb, boosted, kc, meta['orig_size'])
def predict_batch(self, batch_images: list[ndarray], offset: int, n_keypoints: int) -> list[TVFrameResult]:
results: list[TVFrameResult] = []
for idx, image in enumerate(batch_images):
boxes = self._infer_tta(image) if self.use_tta else self._infer_single(image)
keypoints = [(0, 0) for _ in range(max(0, int(n_keypoints)))]
results.append(TVFrameResult(frame_id=offset + idx, boxes=boxes, polygons=[], keypoints=keypoints))
return results