Download miner.py from coolroman/ScoreVision: direct link, hf CLI and curl.
- Browser
- Download file 20.1 kB
-
https://huggingface.co/coolroman/ScoreVision/resolve/main/miner.py
- Command line
-
hf download hf://coolroman/ScoreVision/miner.py
-
curl -L -o miner.py https://huggingface.co/coolroman/ScoreVision/resolve/main/miner.py
20.1 kB
| """ScoreVision crime detector v27 — YOLOv11s trained on 4-voter consensus | |
| (new vmodel0, hermes_sv, pmodel_9, SAM3-refined; >=2 agree, IoU>=0.5). | |
| Per-class confidence + per-class rescue (bonus) logic, flip TTA, cross-class NMS.""" | |
| from pathlib import Path | |
| import math | |
| 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] | |
| keypoints: list[tuple[int, int]] | |
| class Miner: | |
| """YOLOv11s + per-class threshold + per-class rescue + flip TTA + cross-class dedup.""" | |
| # validator output order | |
| class_names = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"] | |
| input_size = 1280 | |
| iou_thres = 0.30 | |
| cross_iou_thresh = 0.80 | |
| max_aspect_ratio = 10.0 | |
| max_det = 150 | |
| _conf_thres_array = np.array( | |
| [0.30, 0.70, 0.70, 0.50, 0.50, 0.40], dtype=np.float32 | |
| ) | |
| _bonus_array = np.array( | |
| [0.20, 0.25, 0.20, 0.30, 0.20, 0.15], dtype=np.float32 | |
| ) | |
| _glove_cls_id = 2 | |
| _glove_spurious_max_conf = 0.04 | |
| _glove_spurious_aspect = 2.5 | |
| _glove_crowd_min_count = 5 | |
| _glove_crowd_max_conf_trigger = 0.6 | |
| _glove_crowd_keep_min_conf = 0.27 | |
| _glove_split_low_max = 0.26 | |
| _glove_split_low_min_count = 4 | |
| _glove_split_high_min_count = 2 | |
| _glove_pair_low_max = 0.1 | |
| _glove_pair_high_min = 0.4 | |
| _glove_pair_max_center_x_dist = 0.028 | |
| _glove_reject_r_min = 100 | |
| _glove_reject_r_max = 125 | |
| _glove_reject_g_min = 88 | |
| _glove_reject_g_max = 115 | |
| _glove_reject_b_min = 68 | |
| _glove_reject_b_max = 88 | |
| _glove_reject_min_pixel_frac = 0.04 | |
| _glove_reject_p90_percentile = 90.0 | |
| _spray_paint_cls_id = 4 | |
| _spray_orphan_max_conf = 0.3 | |
| _spray_glove_min_overlap = 0.3 | |
| def __init__(self, path_hf_repo: Path) -> None: | |
| model_path = path_hf_repo / "weights.onnx" | |
| print("ORT version:", ort.__version__) | |
| try: | |
| ort.preload_dlls() | |
| print("preload_dlls success") | |
| except Exception as e: | |
| print(f"preload_dlls failed: {e}") | |
| print("ORT providers:", ort.get_available_providers()) | |
| opts = ort.SessionOptions() | |
| opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL | |
| try: | |
| self.session = ort.InferenceSession( | |
| str(model_path), sess_options=opts, | |
| providers=["CUDAExecutionProvider", "CPUExecutionProvider"], | |
| ) | |
| print("CUDA session created") | |
| except Exception as e: | |
| print(f"CUDA failed, CPU fallback: {e}") | |
| self.session = ort.InferenceSession( | |
| str(model_path), sess_options=opts, providers=["CPUExecutionProvider"] | |
| ) | |
| print("session providers:", self.session.get_providers()) | |
| 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()] | |
| sh = self.session.get_inputs()[0].shape | |
| self.input_height = self._safe_dim(sh[2], self.input_size) | |
| self.input_width = self._safe_dim(sh[3], self.input_size) | |
| print(f"ONNX loaded: {model_path}") | |
| print( | |
| "per-class conf: " | |
| + ", ".join(f"{n}={t:.2f}" for n, t in zip(self.class_names, self._conf_thres_array.tolist())) | |
| ) | |
| print( | |
| "per-class rescue bonus: " | |
| + ", ".join(f"{n}={t:.2f}" for n, t in zip(self.class_names, self._bonus_array.tolist())) | |
| ) | |
| def _safe_dim(v, d): | |
| return v if isinstance(v, int) and v > 0 else d | |
| def _letterbox(self, image, new_shape, color=(114, 114, 114)): | |
| h, w = image.shape[:2] | |
| nw, nh = new_shape | |
| r = min(nw / w, nh / h) | |
| rw, rh = int(round(w * r)), int(round(h * r)) | |
| if (rw, rh) != (w, h): | |
| interp = cv2.INTER_CUBIC if r > 1.0 else cv2.INTER_LINEAR | |
| image = cv2.resize(image, (rw, rh), interpolation=interp) | |
| dw, dh = (nw - rw) / 2.0, (nh - rh) / 2.0 | |
| top, bot = int(round(dh - 0.1)), int(round(dh + 0.1)) | |
| lt, rt = int(round(dw - 0.1)), int(round(dw + 0.1)) | |
| padded = cv2.copyMakeBorder(image, top, bot, lt, rt, cv2.BORDER_CONSTANT, value=color) | |
| return padded, r, (dw, dh) | |
| def _preprocess(self, image): | |
| h, w = image.shape[:2] | |
| img, ratio, pad = self._letterbox(image, (self.input_width, self.input_height)) | |
| img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 | |
| img = np.transpose(img, (2, 0, 1))[None, ...] | |
| return np.ascontiguousarray(img, dtype=np.float32), ratio, pad, (w, h) | |
| def _clip(boxes, size): | |
| w, h = 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 | |
| def _hard_nms(boxes, scores, iou_thr): | |
| n = len(boxes) | |
| if n == 0: return np.array([], dtype=np.intp) | |
| order = np.argsort(-scores) | |
| keep = [] | |
| while len(order) > 0: | |
| i = int(order[0]); keep.append(i) | |
| if len(order) == 1: break | |
| 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) | |
| a_i = max(0.0, boxes[i, 2]-boxes[i, 0])*max(0.0, boxes[i, 3]-boxes[i, 1]) | |
| a_r = (np.maximum(0.0, boxes[rest, 2]-boxes[rest, 0]) | |
| * np.maximum(0.0, boxes[rest, 3]-boxes[rest, 1])) | |
| iou = inter / (a_i+a_r-inter+1e-7) | |
| order = rest[iou <= iou_thr] | |
| return np.array(keep, dtype=np.intp) | |
| def _per_class_hard_nms(self, boxes, scores, cls_ids, iou_thr): | |
| if len(boxes) == 0: return np.array([], dtype=np.intp) | |
| keep_all = [] | |
| for c in np.unique(cls_ids): | |
| mask = cls_ids == c | |
| idx = np.where(mask)[0] | |
| keep = self._hard_nms(boxes[mask], scores[mask], iou_thr) | |
| keep_all.extend(idx[keep].tolist()) | |
| keep_all.sort() | |
| return np.array(keep_all, dtype=np.intp) | |
| def _cross_class_dedup(self, boxes, scores, cls_ids, iou_thr): | |
| n = len(boxes) | |
| if n == 0: return np.array([], dtype=np.intp) | |
| order = np.argsort(-scores) | |
| keep = []; suppressed = np.zeros(n, dtype=bool) | |
| for i in order: | |
| if suppressed[i]: continue | |
| keep.append(int(i)) | |
| ix1 = np.maximum(boxes[i, 0], boxes[:, 0]) | |
| iy1 = np.maximum(boxes[i, 1], boxes[:, 1]) | |
| ix2 = np.minimum(boxes[i, 2], boxes[:, 2]) | |
| iy2 = np.minimum(boxes[i, 3], boxes[:, 3]) | |
| inter = np.maximum(0.0, ix2-ix1)*np.maximum(0.0, iy2-iy1) | |
| a_i = max(0.0, boxes[i, 2]-boxes[i, 0])*max(0.0, boxes[i, 3]-boxes[i, 1]) | |
| a_r = (np.maximum(0.0, boxes[:, 2]-boxes[:, 0]) | |
| * np.maximum(0.0, boxes[:, 3]-boxes[:, 1])) | |
| iou = inter / (a_i+a_r-inter+1e-7) | |
| iou[i] = 0.0 | |
| suppressed |= iou >= iou_thr | |
| return np.array(keep, dtype=np.intp) | |
| def _filter_sane(self, boxes, scores, cls_ids, orig_size): | |
| if len(boxes) == 0: return boxes, scores, cls_ids | |
| w, h = orig_size | |
| area_img = float(w * h) | |
| bw = np.maximum(0.0, boxes[:, 2]-boxes[:, 0]) | |
| bh = np.maximum(0.0, boxes[:, 3]-boxes[:, 1]) | |
| area = bw * bh | |
| ar = np.where((bw > 0) & (bh > 0), | |
| np.maximum(bw / np.maximum(bh, 1e-6), bh / np.maximum(bw, 1e-6)), | |
| np.inf) | |
| keep = (area <= 0.95 * area_img) & (ar <= self.max_aspect_ratio) | |
| return boxes[keep], scores[keep], cls_ids[keep] | |
| def _conf_filter_with_rescue(self, boxes, scores, cls_ids): | |
| """Per-class threshold + per-class rescue (admit top-1 if score >= thr - bonus | |
| and no box of that class passed the normal threshold).""" | |
| if len(scores) == 0: | |
| return np.zeros(0, dtype=bool) | |
| thr = self._conf_thres_array[cls_ids] | |
| keep = scores >= thr | |
| for c in np.unique(cls_ids): | |
| bonus = float(self._bonus_array[c]) | |
| if bonus <= 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] >= self._conf_thres_array[c] - bonus: | |
| keep[top] = True | |
| return keep | |
| def _decode(self, preds, ratio, pad, orig_size): | |
| if preds.ndim == 3 and preds.shape[0] == 1: | |
| preds = preds[0] | |
| if preds.ndim != 2 or preds.shape[1] < 6: | |
| return (np.empty((0, 4), dtype=np.float32), | |
| np.empty((0,), dtype=np.float32), | |
| np.empty((0,), dtype=np.int32)) | |
| boxes = preds[:, :4].astype(np.float32) | |
| scores = preds[:, 4].astype(np.float32) | |
| cls_ids = preds[:, 5].astype(np.int32) | |
| keep = (scores > 0) & (cls_ids >= 0) & (cls_ids < len(self.class_names)) | |
| boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep] | |
| if len(boxes) == 0: return boxes, scores, cls_ids | |
| # per-class threshold WITH rescue | |
| keep = self._conf_filter_with_rescue(boxes, scores, cls_ids) | |
| boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep] | |
| if len(boxes) == 0: return boxes, scores, cls_ids | |
| pad_w, pad_h = pad | |
| boxes[:, [0, 2]] -= pad_w | |
| boxes[:, [1, 3]] -= pad_h | |
| boxes /= ratio | |
| boxes = self._clip(boxes, orig_size) | |
| return boxes, scores, cls_ids | |
| def _is_spurious_glove(cls, cls_id, x1, y1, x2, y2, conf): | |
| if cls_id != cls._glove_cls_id: return False | |
| w, h = x2 - x1, y2 - y1 | |
| if w <= 0: return False | |
| return h > cls._glove_spurious_aspect * w and conf < cls._glove_spurious_max_conf | |
| def _box_intersection_area(a, b): | |
| ix1, iy1 = max(a.x1, b.x1), max(a.y1, b.y1) | |
| ix2, iy2 = min(a.x2, b.x2), min(a.y2, b.y2) | |
| return max(0.0, ix2 - ix1) * max(0.0, iy2 - iy1) | |
| def _spray_overlaps_glove(cls, spray, glove): | |
| inter = cls._box_intersection_area(spray, glove) | |
| area = max(0.0, float(spray.x2 - spray.x1) * float(spray.y2 - spray.y1)) | |
| if area <= 0.0: return False | |
| return inter / area >= cls._spray_glove_min_overlap | |
| def _is_glove_rejection_color(cls, cls_id, image, x1, y1, x2, y2): | |
| if cls_id != cls._glove_cls_id or image is None: return False | |
| H, W = image.shape[:2] | |
| xi1 = max(0, int(math.floor(x1))) | |
| yi1 = max(0, int(math.floor(y1))) | |
| xi2 = min(W, int(math.ceil(x2))) | |
| yi2 = min(H, int(math.ceil(y2))) | |
| if xi2 <= xi1 or yi2 <= yi1: return False | |
| crop = image[yi1:yi2, xi1:xi2] | |
| if crop.size == 0: return False | |
| b = crop[:, :, 0].astype(np.float32) | |
| g = crop[:, :, 1].astype(np.float32) | |
| r = crop[:, :, 2].astype(np.float32) | |
| in_range = ((r >= cls._glove_reject_r_min) & (r <= cls._glove_reject_r_max) | |
| & (g >= cls._glove_reject_g_min) & (g <= cls._glove_reject_g_max) | |
| & (b >= cls._glove_reject_b_min) & (b <= cls._glove_reject_b_max)) | |
| if float(in_range.mean()) >= cls._glove_reject_min_pixel_frac: | |
| return True | |
| p90_r = float(np.percentile(r, cls._glove_reject_p90_percentile)) | |
| p90_g = float(np.percentile(g, cls._glove_reject_p90_percentile)) | |
| p90_b = float(np.percentile(b, cls._glove_reject_p90_percentile)) | |
| return (cls._glove_reject_r_min <= p90_r <= cls._glove_reject_r_max | |
| and cls._glove_reject_g_min <= p90_g <= cls._glove_reject_g_max | |
| and cls._glove_reject_b_min <= p90_b <= cls._glove_reject_b_max) | |
| def _filter_glove_by_color(cls, results, image): | |
| if image is None: return results | |
| return [b for b in results | |
| if not cls._is_glove_rejection_color(b.cls_id, image, b.x1, b.y1, b.x2, b.y2)] | |
| def _glove_cx(b): return (b.x1 + b.x2) / 2.0 | |
| def _filter_paired_low_glove(cls, results, image_width): | |
| if image_width <= 0: return results | |
| gloves = [b for b in results if b.cls_id == cls._glove_cls_id] | |
| if len(gloves) < 2: return results | |
| remove = set() | |
| for i, a in enumerate(gloves): | |
| for b in gloves[i+1:]: | |
| if a.conf < cls._glove_pair_low_max and b.conf > cls._glove_pair_high_min: | |
| low, high = a, b | |
| elif b.conf < cls._glove_pair_low_max and a.conf > cls._glove_pair_high_min: | |
| low, high = b, a | |
| else: | |
| continue | |
| cx_dist = abs(cls._glove_cx(low) - cls._glove_cx(high)) / float(image_width) | |
| if cx_dist < cls._glove_pair_max_center_x_dist: | |
| remove.add(id(low)) | |
| return [b for b in results if id(b) not in remove] | |
| def _filter_weak_glove_with_strong_others(cls, results): | |
| others = [b for b in results if b.cls_id != cls._glove_cls_id] | |
| if not others: return results | |
| if not all(b.conf > cls._glove_pair_high_min for b in others): return results | |
| return [b for b in results | |
| if b.cls_id != cls._glove_cls_id or b.conf >= cls._glove_pair_low_max] | |
| def _filter_split_glove_confidence(cls, results): | |
| gloves = [b for b in results if b.cls_id == cls._glove_cls_id] | |
| low = [b for b in gloves if b.conf < cls._glove_split_low_max] | |
| high = [b for b in gloves if b.conf > cls._glove_split_low_max] | |
| if len(low) <= cls._glove_split_low_min_count: return results | |
| if len(high) < cls._glove_split_high_min_count: return results | |
| best_low = max(low, key=lambda b: b.conf) | |
| others = [b for b in results if b.cls_id != cls._glove_cls_id] | |
| return others + high + [best_low] | |
| def _filter_crowded_gloves(cls, results): | |
| gloves = [b for b in results if b.cls_id == cls._glove_cls_id] | |
| if len(gloves) <= cls._glove_crowd_min_count: return results | |
| if max(b.conf for b in gloves) <= cls._glove_crowd_max_conf_trigger: return results | |
| return [b for b in results | |
| if b.cls_id != cls._glove_cls_id or b.conf > cls._glove_crowd_keep_min_conf] | |
| def _filter_orphan_low_conf_spray(cls, results): | |
| gloves = [b for b in results if b.cls_id == cls._glove_cls_id] | |
| kept = [] | |
| for box in results: | |
| if box.cls_id != cls._spray_paint_cls_id or box.conf >= cls._spray_orphan_max_conf: | |
| kept.append(box); continue | |
| if any(cls._spray_overlaps_glove(box, g) for g in gloves): | |
| kept.append(box) | |
| return kept | |
| def _box_iou_pair(cls, a, b): | |
| inter = cls._box_intersection_area(a, b) | |
| aw = max(0.0, float(a.x2 - a.x1)) * max(0.0, float(a.y2 - a.y1)) | |
| bw = max(0.0, float(b.x2 - b.x1)) * max(0.0, float(b.y2 - b.y1)) | |
| union = aw + bw - inter | |
| if union <= 0: return 0.0 | |
| return inter / union | |
| def _aggressive_dedup_balaclava_graffiti(cls, results, iou_thr=0.10): | |
| """For balaclava (cls 0) and graffiti (cls 5), drop lower-conf box | |
| whenever any two boxes of the same class overlap > iou_thr. | |
| Rationale: one face = one balaclava; one tag = one graffiti box.""" | |
| target = {0, 5} | |
| others = [b for b in results if b.cls_id not in target] | |
| by_cls = {} | |
| for b in results: | |
| if b.cls_id in target: | |
| by_cls.setdefault(b.cls_id, []).append(b) | |
| kept = list(others) | |
| for cid, boxes in by_cls.items(): | |
| boxes_sorted = sorted(boxes, key=lambda b: -b.conf) | |
| survivors = [] | |
| for b in boxes_sorted: | |
| if all(cls._box_iou_pair(b, k) < iou_thr for k in survivors): | |
| survivors.append(b) | |
| kept.extend(survivors) | |
| return kept | |
| def _apply_rules(cls, results, image_width, image=None): | |
| results = cls._filter_glove_by_color(results, image) | |
| results = cls._filter_paired_low_glove(results, image_width) | |
| results = cls._filter_weak_glove_with_strong_others(results) | |
| results = cls._filter_split_glove_confidence(results) | |
| results = cls._filter_crowded_gloves(results) | |
| results = cls._filter_orphan_low_conf_spray(results) | |
| return cls._aggressive_dedup_balaclava_graffiti(results) | |
| def _predict_single(self, image): | |
| x, ratio, pad, orig_size = self._preprocess(image) | |
| out = self.session.run(self.output_names, {self.input_name: x})[0] | |
| return self._decode(out, ratio, pad, orig_size) | |
| def _predict_tta(self, image): | |
| b0, s0, c0 = self._predict_single(image) | |
| flipped = cv2.flip(image, 1) | |
| bf, sf, cf = self._predict_single(flipped) | |
| if len(bf): | |
| w = image.shape[1] | |
| bf2 = bf.copy() | |
| bf2[:, 0] = w - bf[:, 2] | |
| bf2[:, 2] = w - bf[:, 0] | |
| bf = bf2 | |
| boxes = np.concatenate([b0, bf], axis=0) if len(b0) or len(bf) else b0 | |
| scores = np.concatenate([s0, sf], axis=0) if len(b0) or len(bf) else s0 | |
| cls_ids = np.concatenate([c0, cf], axis=0) if len(b0) or len(bf) else c0 | |
| if len(boxes) == 0: return [] | |
| boxes, scores, cls_ids = self._filter_sane(boxes, scores, cls_ids, | |
| (image.shape[1], image.shape[0])) | |
| if len(boxes) == 0: return [] | |
| keep = self._per_class_hard_nms(boxes, scores, cls_ids, self.iou_thres) | |
| if len(keep) == 0: return [] | |
| boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep] | |
| keep = self._cross_class_dedup(boxes, scores, cls_ids, self.cross_iou_thresh) | |
| 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] | |
| results = [] | |
| for b, s, c in zip(boxes, scores, cls_ids): | |
| if b[2] <= b[0] or b[3] <= b[1]: continue | |
| x1, y1, x2, y2 = float(b[0]), float(b[1]), float(b[2]), float(b[3]) | |
| if self._is_spurious_glove(int(c), x1, y1, x2, y2, float(s)): continue | |
| results.append(BoundingBox( | |
| x1=int(math.floor(x1)), y1=int(math.floor(y1)), | |
| x2=int(math.ceil(x2)), y2=int(math.ceil(y2)), | |
| cls_id=int(c), conf=float(s), | |
| )) | |
| return self._apply_rules(results, image.shape[1], image=image) | |
| def predict_batch(self, batch_images, offset, n_keypoints): | |
| results = [] | |
| for j, img in enumerate(batch_images): | |
| try: | |
| boxes = self._predict_tta(img) | |
| except Exception as e: | |
| print(f"Inference failed for frame {offset + j}: {e}") | |
| boxes = [] | |
| results.append( | |
| TVFrameResult( | |
| frame_id=offset + j, boxes=boxes, | |
| keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))], | |
| ) | |
| ) | |
| return results | |