ArthurSword commited on
Commit
1bb7f50
·
verified ·
1 Parent(s): 68817a2

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. miner.py +214 -0
miner.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from pathlib import Path
5
+
6
+ import cv2
7
+ import numpy as np
8
+ import onnxruntime as ort
9
+ from numpy import ndarray
10
+ from pydantic import BaseModel
11
+
12
+ PERSON_CLS_ID = 0
13
+
14
+ class BoundingBox(BaseModel):
15
+ x1: int
16
+ y1: int
17
+ x2: int
18
+ y2: int
19
+ cls_id: int
20
+ conf: float
21
+
22
+ class TVFrameResult(BaseModel):
23
+ frame_id: int
24
+ boxes: list[BoundingBox]
25
+ keypoints: list[tuple[int, int]]
26
+
27
+ class Miner:
28
+ BOX_FORMAT = "xyxy"
29
+
30
+ CONF_THRESHOLD = 0.56
31
+ NMS_IOU = 0.45
32
+ MAX_DET = 50
33
+
34
+ # Scores are rounded to this many decimals before anything is compared or
35
+ # sorted. The compliance checker reruns this repo on its own machine and
36
+ # holds the boxes against the ones the chute returned, so an ordering that a
37
+ # difference in the sixth digit can flip turns arithmetic noise into a
38
+ # missing or an extra box. Four decimals is far coarser than any such
39
+ # difference and far finer than the gaps that decide real detections.
40
+ SCORE_DECIMALS = 4
41
+ PRE_NMS_TOPK = 1000
42
+
43
+ MIN_SIDE = 3.0
44
+ MAX_ASPECT_RATIO = 10.0
45
+ MAX_BOX_AREA_RATIO = 0.95
46
+
47
+ def __init__(self, path_hf_repo: Path) -> None:
48
+ cv2.setNumThreads(1)
49
+ options = ort.SessionOptions()
50
+ options.intra_op_num_threads = 2
51
+ options.inter_op_num_threads = 1
52
+ options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
53
+ options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
54
+ model_path = Path(path_hf_repo) / "weights.onnx"
55
+ self.session = ort.InferenceSession(
56
+ str(model_path), sess_options=options,
57
+ providers=["CPUExecutionProvider"],
58
+ )
59
+
60
+ model_input = self.session.get_inputs()[0]
61
+ self.input_name = model_input.name
62
+ self.input_height = int(model_input.shape[2])
63
+ self.input_width = int(model_input.shape[3])
64
+ self._warmup()
65
+
66
+ def _warmup(self, iters: int = 3) -> None:
67
+ dummy = np.zeros((720, 1280, 3), dtype=np.uint8)
68
+ for _ in range(max(0, iters)):
69
+ self._predict_single(dummy)
70
+
71
+ def _letterbox(self, image: ndarray) -> tuple[ndarray, float, tuple[float, float]]:
72
+ h0, w0 = image.shape[:2]
73
+ ratio = min(self.input_width / w0, self.input_height / h0)
74
+ new_w, new_h = int(round(w0 * ratio)), int(round(h0 * ratio))
75
+ if (new_w, new_h) != (w0, h0):
76
+ image = cv2.resize(
77
+ image, (new_w, new_h),
78
+ interpolation=cv2.INTER_CUBIC if ratio > 1.0 else cv2.INTER_LINEAR,
79
+ )
80
+ pad_w = (self.input_width - new_w) / 2.0
81
+ pad_h = (self.input_height - new_h) / 2.0
82
+ padded = cv2.copyMakeBorder(
83
+ image,
84
+ int(round(pad_h - 0.1)), int(round(pad_h + 0.1)),
85
+ int(round(pad_w - 0.1)), int(round(pad_w + 0.1)),
86
+ cv2.BORDER_CONSTANT, value=(114, 114, 114),
87
+ )
88
+ return padded, ratio, (pad_w, pad_h)
89
+
90
+ def _preprocess(self, image: ndarray) -> tuple[ndarray, float, tuple[float, float]]:
91
+ padded, ratio, pad = self._letterbox(image)
92
+ blob = cv2.dnn.blobFromImage(padded, scalefactor=1 / 255.0, swapRB=True)
93
+ return blob, ratio, pad
94
+
95
+ def _candidates(self, preds: ndarray) -> tuple[ndarray, ndarray]:
96
+ anchors = preds[0].T
97
+ boxes = anchors[:, :4].astype(np.float32)
98
+ if self.BOX_FORMAT == "xywh":
99
+ half_w, half_h = boxes[:, 2] / 2.0, boxes[:, 3] / 2.0
100
+ boxes = np.stack([boxes[:, 0] - half_w, boxes[:, 1] - half_h,
101
+ boxes[:, 0] + half_w, boxes[:, 1] + half_h], axis=1)
102
+ return boxes, anchors[:, 4].astype(np.float32)
103
+
104
+ def _rank(self, boxes: ndarray, scores: ndarray) -> ndarray:
105
+ """Best first, by a key two machines cannot disagree on.
106
+
107
+ The quantised score decides, and where it ties the box's own top-left
108
+ corner does, so the result never depends on the order the candidates
109
+ arrived in or on a difference in the last float digits.
110
+ """
111
+ q = np.round(scores.astype(np.float64), self.SCORE_DECIMALS)
112
+ return np.lexsort((np.round(boxes[:, 1], 3), np.round(boxes[:, 0], 3), -q))
113
+
114
+ def _screen(self, boxes: ndarray, scores: ndarray) -> tuple[ndarray, ndarray]:
115
+ keep = np.round(scores.astype(np.float64), self.SCORE_DECIMALS) >= self.CONF_THRESHOLD
116
+ boxes, scores = boxes[keep], scores[keep]
117
+ if boxes.shape[0] > self.PRE_NMS_TOPK:
118
+ order = self._rank(boxes, scores)[: self.PRE_NMS_TOPK]
119
+ boxes, scores = boxes[order], scores[order]
120
+ return boxes, scores
121
+
122
+ def _nms(self, boxes: ndarray, scores: ndarray, threshold: float) -> ndarray:
123
+ x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
124
+ areas = np.maximum(x2 - x1, 0.0) * np.maximum(y2 - y1, 0.0)
125
+ order = self._rank(boxes, scores)
126
+ keep: list[int] = []
127
+ while order.size > 0:
128
+ best = int(order[0])
129
+ keep.append(best)
130
+ if order.size == 1:
131
+ break
132
+ rest = order[1:]
133
+ xx1 = np.maximum(x1[best], x1[rest])
134
+ yy1 = np.maximum(y1[best], y1[rest])
135
+ xx2 = np.minimum(x2[best], x2[rest])
136
+ yy2 = np.minimum(y2[best], y2[rest])
137
+ inter = np.clip(xx2 - xx1, 0.0, None) * np.clip(yy2 - yy1, 0.0, None)
138
+ iou = inter / np.maximum(areas[best] + areas[rest] - inter, 1e-9)
139
+ order = rest[iou < threshold]
140
+ return np.array(keep, dtype=np.int64)
141
+
142
+ def _suppress(
143
+ self, boxes: ndarray, scores: ndarray, orig_size: tuple[int, int]
144
+ ) -> tuple[ndarray, ndarray]:
145
+ w, h = orig_size
146
+ widths = boxes[:, 2] - boxes[:, 0]
147
+ heights = boxes[:, 3] - boxes[:, 1]
148
+ aspect = np.maximum(widths / np.maximum(heights, 1e-6),
149
+ heights / np.maximum(widths, 1e-6))
150
+ sane = (widths >= self.MIN_SIDE) & (heights >= self.MIN_SIDE)
151
+ sane &= widths * heights <= self.MAX_BOX_AREA_RATIO * float(w * h)
152
+ sane &= aspect <= self.MAX_ASPECT_RATIO
153
+ boxes, scores = boxes[sane], scores[sane]
154
+ order = self._rank(boxes, scores)[: self.MAX_DET]
155
+ return boxes[order], scores[order]
156
+
157
+ @staticmethod
158
+ def _clip_boxes(boxes: ndarray, image_size: tuple[int, int]) -> ndarray:
159
+ w, h = image_size
160
+ boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, w - 1)
161
+ boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, h - 1)
162
+ return boxes
163
+
164
+ def _decode(
165
+ self,
166
+ preds: ndarray,
167
+ ratio: float,
168
+ pad: tuple[float, float],
169
+ orig_size: tuple[int, int],
170
+ ) -> list[BoundingBox]:
171
+ boxes, scores = self._candidates(preds)
172
+ boxes, scores = self._screen(boxes, scores)
173
+ keep = self._nms(boxes, scores, self.NMS_IOU)
174
+ boxes, scores = boxes[keep], scores[keep]
175
+
176
+ pad_w, pad_h = pad
177
+ boxes[:, [0, 2]] = (boxes[:, [0, 2]] - pad_w) / ratio
178
+ boxes[:, [1, 3]] = (boxes[:, [1, 3]] - pad_h) / ratio
179
+ boxes = self._clip_boxes(boxes, orig_size)
180
+
181
+ boxes, scores = self._suppress(boxes, scores, orig_size)
182
+ return [
183
+ BoundingBox(
184
+ x1=int(math.floor(float(b[0]))), y1=int(math.floor(float(b[1]))),
185
+ x2=int(math.ceil(float(b[2]))), y2=int(math.ceil(float(b[3]))),
186
+ cls_id=PERSON_CLS_ID, conf=float(c),
187
+ )
188
+ for b, c in zip(boxes, scores)
189
+ if b[2] > b[0] and b[3] > b[1]
190
+ ]
191
+
192
+ def _predict_single(self, image: ndarray) -> list[BoundingBox]:
193
+ blob, ratio, pad = self._preprocess(image)
194
+ preds = self.session.run(None, {self.input_name: blob})[0]
195
+ return self._decode(preds, ratio, pad, (image.shape[1], image.shape[0]))
196
+
197
+ def predict_batch(
198
+ self,
199
+ batch_images: list[ndarray],
200
+ offset: int,
201
+ n_keypoints: int,
202
+ ) -> list[TVFrameResult]:
203
+ _ = n_keypoints
204
+ results: list[TVFrameResult] = []
205
+ for index, image in enumerate(batch_images):
206
+ try:
207
+ boxes = self._predict_single(image)
208
+ except Exception as exc:
209
+ print(f"⚠️ Inference failed for frame {offset + index}: {exc}")
210
+ boxes = []
211
+ results.append(
212
+ TVFrameResult(frame_id=offset + index, boxes=boxes, keypoints=[])
213
+ )
214
+ return results