Upload folder using huggingface_hub
Browse files- miner.py +42 -83
- weights.onnx +2 -2
miner.py
CHANGED
|
@@ -25,44 +25,40 @@ class TVFrameResult(BaseModel):
|
|
| 25 |
|
| 26 |
class Miner:
|
| 27 |
"""
|
| 28 |
-
YOLO ONNX miner for
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
Pipeline per frame: preprocess -> ONNX -> decode ->
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
-
Speed
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
matrix, so cost stays flat as the number of detected objects grows
|
| 40 |
-
instead of scaling like a Python loop.
|
| 41 |
-
- `_hard_nms` precomputes box areas once; `pre_nms_topk` bounds NMS cost
|
| 42 |
-
on pathologically crowded frames.
|
| 43 |
"""
|
| 44 |
|
| 45 |
-
class_names = ['
|
| 46 |
-
input_size =
|
| 47 |
-
cross_iou_thresh = 0.
|
| 48 |
max_det = 300
|
| 49 |
-
# NMS is O(n^2)
|
| 50 |
-
#
|
| 51 |
pre_nms_topk = 1000
|
| 52 |
#overlap_suppress_threshold = 0.85
|
| 53 |
|
| 54 |
-
# Per-class confidence
|
| 55 |
-
_conf_thres_array = np.array([0.
|
| 56 |
-
_extra_conf_thres_array = np.array([0.32, 0.3, 0.36, 0.3], dtype=np.float32)
|
| 57 |
|
| 58 |
-
# Per-class IoU
|
| 59 |
-
_iou_thres_array = np.array([0.
|
| 60 |
|
| 61 |
# Per-class rescue bonus
|
| 62 |
-
_bonus_array = np.array([0.2
|
| 63 |
|
| 64 |
-
# Per-class minimum box area
|
| 65 |
-
_min_box_area_array = np.array([
|
| 66 |
|
| 67 |
def __init__(self, path_hf_repo: Path) -> None:
|
| 68 |
self.path_hf_repo = path_hf_repo
|
|
@@ -87,7 +83,6 @@ class Miner:
|
|
| 87 |
)
|
| 88 |
print("Created ORT session with preferred CUDA provider list")
|
| 89 |
print("ORT session providers:", self.session.get_providers())
|
| 90 |
-
# If CUDAExecutionProvider is NOT listed above, you are running on CPU.
|
| 91 |
|
| 92 |
self.input_name = self.session.get_inputs()[0].name
|
| 93 |
input_shape = self.session.get_inputs()[0].shape
|
|
@@ -95,9 +90,9 @@ class Miner:
|
|
| 95 |
self.input_h = self._safe_dim(input_shape[2], default=self.input_size)
|
| 96 |
self.input_w = self._safe_dim(input_shape[3], default=self.input_size)
|
| 97 |
|
| 98 |
-
# Same-class cluster score boost
|
| 99 |
-
#
|
| 100 |
-
#
|
| 101 |
self.use_cluster_boost = True
|
| 102 |
self._avg_iou = float(np.mean(self._iou_thres_array))
|
| 103 |
|
|
@@ -113,7 +108,7 @@ class Miner:
|
|
| 113 |
print(f"warmup skipped: {e}")
|
| 114 |
|
| 115 |
def __repr__(self) -> str:
|
| 116 |
-
return f"
|
| 117 |
|
| 118 |
@staticmethod
|
| 119 |
def _safe_dim(value, default: int) -> int:
|
|
@@ -150,30 +145,19 @@ class Miner:
|
|
| 150 |
)
|
| 151 |
return out, r, pad_w, pad_h
|
| 152 |
|
| 153 |
-
def _preprocess(self, image_bgr: np.ndarray,
|
| 154 |
-
allow_pad: bool = True) -> tuple[np.ndarray, dict]:
|
| 155 |
orig_h, orig_w = image_bgr.shape[:2]
|
| 156 |
-
extra_left = 0
|
| 157 |
-
extra_right = 0
|
| 158 |
-
if allow_pad and orig_w == orig_h: # only pad when allowed
|
| 159 |
-
target_w = int(orig_w * 1.05)
|
| 160 |
-
if target_w > orig_w:
|
| 161 |
-
total_extra = target_w - orig_w
|
| 162 |
-
extra_left = total_extra // 2
|
| 163 |
-
extra_right = total_extra - extra_left
|
| 164 |
-
image_bgr = cv2.copyMakeBorder(
|
| 165 |
-
image_bgr, 0, 0, extra_left, extra_right,
|
| 166 |
-
cv2.BORDER_CONSTANT, value=(114, 114, 114),
|
| 167 |
-
)
|
| 168 |
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
| 169 |
img, ratio, pad_w, pad_h = self._letterbox(rgb, (self.input_w, self.input_h))
|
| 170 |
x = img.astype(np.float32) / 255.0
|
| 171 |
x = np.transpose(x, (2, 0, 1))[None, ...]
|
| 172 |
x = np.ascontiguousarray(x)
|
| 173 |
return x, {
|
| 174 |
-
"orig_h": orig_h,
|
| 175 |
-
"
|
| 176 |
-
"
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
# βββ Vectorized box operations βββββββββββββββββββββββββββββββ
|
|
@@ -314,15 +298,11 @@ class Miner:
|
|
| 314 |
return out.astype(np.float32)
|
| 315 |
|
| 316 |
def _conf_filter_mask(self, scores: np.ndarray,
|
| 317 |
-
cls_ids: np.ndarray
|
| 318 |
"""Per-class threshold with rescue bonus for missed classes."""
|
| 319 |
if len(scores) == 0:
|
| 320 |
return np.zeros(0, dtype=bool)
|
| 321 |
-
thr =
|
| 322 |
-
if extra_left > 0:
|
| 323 |
-
thr = self._extra_conf_thres_array[cls_ids]
|
| 324 |
-
else:
|
| 325 |
-
thr = self._conf_thres_array[cls_ids]
|
| 326 |
keep = scores >= thr
|
| 327 |
for c in np.unique(cls_ids):
|
| 328 |
b = float(self._bonus_array[c])
|
|
@@ -365,11 +345,10 @@ class Miner:
|
|
| 365 |
|
| 366 |
def _decode_yolo_output(self, preds: np.ndarray, ratio: float,
|
| 367 |
pad: tuple[float, float],
|
| 368 |
-
orig_size: tuple[int, int]
|
| 369 |
-
extra: tuple[int, int] = (0, 0)
|
| 370 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 371 |
-
"""Decode -
|
| 372 |
-
|
| 373 |
empty = (np.empty((0, 4), np.float32), np.empty(0, np.float32),
|
| 374 |
np.empty(0, np.int32))
|
| 375 |
if preds.ndim == 3 and preds.shape[0] == 1:
|
|
@@ -388,33 +367,15 @@ class Miner:
|
|
| 388 |
if len(boxes) == 0:
|
| 389 |
return empty
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
keep = self._conf_filter_mask(scores, cls_ids, extra_left)
|
| 394 |
boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
|
| 395 |
if len(boxes) == 0:
|
| 396 |
return empty
|
| 397 |
|
| 398 |
-
# 1) undo letterbox -> coords in the PADDED (widened) image
|
| 399 |
pad_w, pad_h = pad
|
| 400 |
boxes[:, [0, 2]] -= pad_w
|
| 401 |
boxes[:, [1, 3]] -= pad_h
|
| 402 |
boxes /= ratio
|
| 403 |
-
|
| 404 |
-
# 2) undo left/right pre-padding -> original-image coords
|
| 405 |
-
if extra_left:
|
| 406 |
-
boxes[:, [0, 2]] -= extra_left
|
| 407 |
-
|
| 408 |
-
# 2b) drop boxes whose CENTER falls in the black padding bars
|
| 409 |
-
if extra_left or _extra_right:
|
| 410 |
-
orig_w, orig_h = orig_size
|
| 411 |
-
cx = (boxes[:, 0] + boxes[:, 2]) * 0.5
|
| 412 |
-
inside = (cx >= 0) & (cx <= orig_w)
|
| 413 |
-
boxes, scores, cls_ids = boxes[inside], scores[inside], cls_ids[inside]
|
| 414 |
-
if len(boxes) == 0:
|
| 415 |
-
return empty
|
| 416 |
-
|
| 417 |
-
# 3) clip to ORIGINAL image bounds
|
| 418 |
boxes = self._clip_boxes(boxes, orig_size)
|
| 419 |
|
| 420 |
return self._per_view_pipeline(boxes, scores, cls_ids, orig_size)
|
|
@@ -441,9 +402,9 @@ class Miner:
|
|
| 441 |
)
|
| 442 |
return results
|
| 443 |
|
| 444 |
-
# βββ Inference ββββββββββββββββββββββββββ
|
| 445 |
|
| 446 |
-
def _predict_single(self, image_bgr: np.ndarray
|
| 447 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 448 |
"""One forward pass -> decoded (boxes, scores, cls_ids) in original coords."""
|
| 449 |
if image_bgr is None or not isinstance(image_bgr, np.ndarray):
|
|
@@ -451,22 +412,20 @@ class Miner:
|
|
| 451 |
if image_bgr.dtype != np.uint8:
|
| 452 |
image_bgr = image_bgr.astype(np.uint8)
|
| 453 |
|
| 454 |
-
inp, meta = self._preprocess(image_bgr
|
| 455 |
outputs = self.session.run(None, {self.input_name: inp})
|
| 456 |
|
| 457 |
ratio = float(meta["ratio"])
|
| 458 |
pad = (float(meta["pad_w"]), float(meta["pad_h"]))
|
| 459 |
orig_size = (int(meta["orig_w"]), int(meta["orig_h"]))
|
| 460 |
-
extra = (int(meta["extra_left"]), int(meta["extra_right"]))
|
| 461 |
|
| 462 |
-
return self._decode_yolo_output(outputs[0], ratio, pad, orig_size
|
| 463 |
|
| 464 |
def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
|
| 465 |
-
"""Single-view inference (no TTA)."""
|
| 466 |
orig_h, orig_w = image_bgr.shape[:2]
|
| 467 |
orig_size = (orig_w, orig_h)
|
| 468 |
|
| 469 |
-
boxes, scores, cls_ids = self._predict_single(image_bgr
|
| 470 |
if len(boxes) == 0:
|
| 471 |
return []
|
| 472 |
|
|
|
|
| 25 |
|
| 26 |
class Miner:
|
| 27 |
"""
|
| 28 |
+
YOLO (NMS-free) ONNX miner for road-sign detection. One forward pass per
|
| 29 |
+
frame β no TTA.
|
| 30 |
|
| 31 |
+
Class: single 'road sign'.
|
| 32 |
|
| 33 |
+
Pipeline per frame: preprocess -> ONNX -> decode -> conf threshold (+ rescue
|
| 34 |
+
bonus) -> un-letterbox -> sanity filter -> per-class NMS -> cross-class dedup
|
| 35 |
+
-> same-class cluster score boost -> results.
|
| 36 |
|
| 37 |
+
Speed: the detection pipeline runs exactly ONCE per frame (the old redundant
|
| 38 |
+
second NMS/dedup pass is gone), and the cluster boost is a single vectorized
|
| 39 |
+
IoU matrix, so cost stays flat as the number of detected objects grows.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"""
|
| 41 |
|
| 42 |
+
class_names = ['road sign']
|
| 43 |
+
input_size = 1536
|
| 44 |
+
cross_iou_thresh = 0.8
|
| 45 |
max_det = 300
|
| 46 |
+
# NMS is O(n^2); if a frame yields a huge candidate list, keep the top-K by
|
| 47 |
+
# score before NMS. Set high enough never to touch real detections.
|
| 48 |
pre_nms_topk = 1000
|
| 49 |
#overlap_suppress_threshold = 0.85
|
| 50 |
|
| 51 |
+
# Per-class confidence threshold
|
| 52 |
+
_conf_thres_array = np.array([0.32], dtype=np.float32)
|
|
|
|
| 53 |
|
| 54 |
+
# Per-class IoU threshold for same-class NMS
|
| 55 |
+
_iou_thres_array = np.array([0.8], dtype=np.float32)
|
| 56 |
|
| 57 |
# Per-class rescue bonus
|
| 58 |
+
_bonus_array = np.array([0.2], dtype=np.float32)
|
| 59 |
|
| 60 |
+
# Per-class minimum box area (index 0 = road sign)
|
| 61 |
+
_min_box_area_array = np.array([9.0], dtype=np.float32)
|
| 62 |
|
| 63 |
def __init__(self, path_hf_repo: Path) -> None:
|
| 64 |
self.path_hf_repo = path_hf_repo
|
|
|
|
| 83 |
)
|
| 84 |
print("Created ORT session with preferred CUDA provider list")
|
| 85 |
print("ORT session providers:", self.session.get_providers())
|
|
|
|
| 86 |
|
| 87 |
self.input_name = self.session.get_inputs()[0].name
|
| 88 |
input_shape = self.session.get_inputs()[0].shape
|
|
|
|
| 90 |
self.input_h = self._safe_dim(input_shape[2], default=self.input_size)
|
| 91 |
self.input_w = self._safe_dim(input_shape[3], default=self.input_size)
|
| 92 |
|
| 93 |
+
# Same-class cluster score boost (raises overlapping same-class survivors
|
| 94 |
+
# to their cluster max). Part of the current tuned behaviour; for a single
|
| 95 |
+
# class it is nearly a no-op. Set False to disable.
|
| 96 |
self.use_cluster_boost = True
|
| 97 |
self._avg_iou = float(np.mean(self._iou_thres_array))
|
| 98 |
|
|
|
|
| 108 |
print(f"warmup skipped: {e}")
|
| 109 |
|
| 110 |
def __repr__(self) -> str:
|
| 111 |
+
return f"road sign Miner classes={len(self.class_names)}"
|
| 112 |
|
| 113 |
@staticmethod
|
| 114 |
def _safe_dim(value, default: int) -> int:
|
|
|
|
| 145 |
)
|
| 146 |
return out, r, pad_w, pad_h
|
| 147 |
|
| 148 |
+
def _preprocess(self, image_bgr: ndarray) -> tuple[np.ndarray, dict]:
|
|
|
|
| 149 |
orig_h, orig_w = image_bgr.shape[:2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
| 151 |
img, ratio, pad_w, pad_h = self._letterbox(rgb, (self.input_w, self.input_h))
|
| 152 |
x = img.astype(np.float32) / 255.0
|
| 153 |
x = np.transpose(x, (2, 0, 1))[None, ...]
|
| 154 |
x = np.ascontiguousarray(x)
|
| 155 |
return x, {
|
| 156 |
+
"orig_h": orig_h,
|
| 157 |
+
"orig_w": orig_w,
|
| 158 |
+
"ratio": ratio,
|
| 159 |
+
"pad_w": pad_w,
|
| 160 |
+
"pad_h": pad_h,
|
| 161 |
}
|
| 162 |
|
| 163 |
# βββ Vectorized box operations βββββββββββββββββββββββββββββββ
|
|
|
|
| 298 |
return out.astype(np.float32)
|
| 299 |
|
| 300 |
def _conf_filter_mask(self, scores: np.ndarray,
|
| 301 |
+
cls_ids: np.ndarray) -> np.ndarray:
|
| 302 |
"""Per-class threshold with rescue bonus for missed classes."""
|
| 303 |
if len(scores) == 0:
|
| 304 |
return np.zeros(0, dtype=bool)
|
| 305 |
+
thr = self._conf_thres_array[cls_ids]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
keep = scores >= thr
|
| 307 |
for c in np.unique(cls_ids):
|
| 308 |
b = float(self._bonus_array[c])
|
|
|
|
| 345 |
|
| 346 |
def _decode_yolo_output(self, preds: np.ndarray, ratio: float,
|
| 347 |
pad: tuple[float, float],
|
| 348 |
+
orig_size: tuple[int, int]
|
|
|
|
| 349 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 350 |
+
"""Decode NMS-free output [1,N,6]=(x1,y1,x2,y2,conf,cls) -> conf filter ->
|
| 351 |
+
un-letterbox -> sanity+NMS+dedup. Returns arrays in ORIGINAL coords."""
|
| 352 |
empty = (np.empty((0, 4), np.float32), np.empty(0, np.float32),
|
| 353 |
np.empty(0, np.int32))
|
| 354 |
if preds.ndim == 3 and preds.shape[0] == 1:
|
|
|
|
| 367 |
if len(boxes) == 0:
|
| 368 |
return empty
|
| 369 |
|
| 370 |
+
keep = self._conf_filter_mask(scores, cls_ids)
|
|
|
|
|
|
|
| 371 |
boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
|
| 372 |
if len(boxes) == 0:
|
| 373 |
return empty
|
| 374 |
|
|
|
|
| 375 |
pad_w, pad_h = pad
|
| 376 |
boxes[:, [0, 2]] -= pad_w
|
| 377 |
boxes[:, [1, 3]] -= pad_h
|
| 378 |
boxes /= ratio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
boxes = self._clip_boxes(boxes, orig_size)
|
| 380 |
|
| 381 |
return self._per_view_pipeline(boxes, scores, cls_ids, orig_size)
|
|
|
|
| 402 |
)
|
| 403 |
return results
|
| 404 |
|
| 405 |
+
# βββ Inference (single view, no TTA) ββββββββββββββββββββββββββ
|
| 406 |
|
| 407 |
+
def _predict_single(self, image_bgr: np.ndarray
|
| 408 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 409 |
"""One forward pass -> decoded (boxes, scores, cls_ids) in original coords."""
|
| 410 |
if image_bgr is None or not isinstance(image_bgr, np.ndarray):
|
|
|
|
| 412 |
if image_bgr.dtype != np.uint8:
|
| 413 |
image_bgr = image_bgr.astype(np.uint8)
|
| 414 |
|
| 415 |
+
inp, meta = self._preprocess(image_bgr)
|
| 416 |
outputs = self.session.run(None, {self.input_name: inp})
|
| 417 |
|
| 418 |
ratio = float(meta["ratio"])
|
| 419 |
pad = (float(meta["pad_w"]), float(meta["pad_h"]))
|
| 420 |
orig_size = (int(meta["orig_w"]), int(meta["orig_h"]))
|
|
|
|
| 421 |
|
| 422 |
+
return self._decode_yolo_output(outputs[0], ratio, pad, orig_size)
|
| 423 |
|
| 424 |
def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
|
|
|
|
| 425 |
orig_h, orig_w = image_bgr.shape[:2]
|
| 426 |
orig_size = (orig_w, orig_h)
|
| 427 |
|
| 428 |
+
boxes, scores, cls_ids = self._predict_single(image_bgr)
|
| 429 |
if len(boxes) == 0:
|
| 430 |
return []
|
| 431 |
|
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:9ad0e8fc7e38bd4c5f5c8f4cfece75dcc2fa6a41c2cf71e4cba9672c87b98687
|
| 3 |
+
size 9757809
|