coolroman commited on
Commit
219fc19
·
verified ·
1 Parent(s): 1da2550

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. miner.py +110 -123
miner.py CHANGED
@@ -1,5 +1,6 @@
1
- """ScoreVision crime detector v24 — YOLOv11s trained on rival-consensus labels.
2
- Per-class conf thresholds tuned vs validator-PGT proxy; flip TTA; cross-class NMS."""
 
3
  from pathlib import Path
4
  import math
5
 
@@ -26,17 +27,23 @@ class TVFrameResult(BaseModel):
26
 
27
 
28
  class Miner:
29
- """ONNX Runtime miner with horizontal-flip TTA and per-class confidence."""
30
 
 
31
  class_names = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"]
32
  input_size = 1280
33
- iou_thres = 0.45
34
- cross_iou_thresh = 0.50
35
  max_aspect_ratio = 10.0
36
  max_det = 150
37
- # per-class thresholds — tuned on rival-consensus GT (proxy for validator PGT)
38
  _conf_thres_array = np.array(
39
- [0.30, 0.70, 0.60, 0.50, 0.40, 0.40], dtype=np.float32
 
 
 
 
 
40
  )
41
 
42
  def __init__(self, path_hf_repo: Path) -> None:
@@ -47,25 +54,22 @@ class Miner:
47
  print("preload_dlls success")
48
  except Exception as e:
49
  print(f"preload_dlls failed: {e}")
50
- print("ORT available providers:", ort.get_available_providers())
51
 
52
- sess_options = ort.SessionOptions()
53
- sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
54
  try:
55
  self.session = ort.InferenceSession(
56
- str(model_path),
57
- sess_options=sess_options,
58
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
59
  )
60
- print("Created ORT session with preferred CUDA provider list")
61
  except Exception as e:
62
- print(f"CUDA session creation failed, falling back to CPU: {e}")
63
  self.session = ort.InferenceSession(
64
- str(model_path),
65
- sess_options=sess_options,
66
- providers=["CPUExecutionProvider"],
67
  )
68
- print("ORT session providers:", self.session.get_providers())
69
 
70
  for inp in self.session.get_inputs():
71
  print("INPUT:", inp.name, inp.shape, inp.type)
@@ -74,50 +78,43 @@ class Miner:
74
 
75
  self.input_name = self.session.get_inputs()[0].name
76
  self.output_names = [o.name for o in self.session.get_outputs()]
77
- self.input_shape = self.session.get_inputs()[0].shape
78
- self.input_height = self._safe_dim(self.input_shape[2], self.input_size)
79
- self.input_width = self._safe_dim(self.input_shape[3], self.input_size)
80
- print(f"ONNX loaded: {model_path} input={self.input_shape}")
81
  print(
82
  "per-class conf: "
83
- + ", ".join(
84
- f"{n}={t:.3f}" for n, t in zip(self.class_names, self._conf_thres_array.tolist())
85
- )
 
 
86
  )
87
-
88
- def __repr__(self) -> str:
89
- return f"ONNXRuntime(providers={self.session.get_providers()})"
90
 
91
  @staticmethod
92
- def _safe_dim(value, default: int) -> int:
93
- return value if isinstance(value, int) and value > 0 else default
94
 
95
- def _letterbox(self, image: ndarray, new_shape: tuple[int, int],
96
- color=(114, 114, 114)
97
- ) -> tuple[ndarray, float, tuple[float, float]]:
98
  h, w = image.shape[:2]
99
- new_w, new_h = new_shape
100
- ratio = min(new_w / w, new_h / h)
101
- rw, rh = int(round(w * ratio)), int(round(h * ratio))
102
  if (rw, rh) != (w, h):
103
- interp = cv2.INTER_CUBIC if ratio > 1.0 else cv2.INTER_LINEAR
104
  image = cv2.resize(image, (rw, rh), interpolation=interp)
105
- dw, dh = (new_w - rw) / 2.0, (new_h - rh) / 2.0
106
  top, bot = int(round(dh - 0.1)), int(round(dh + 0.1))
107
  lt, rt = int(round(dw - 0.1)), int(round(dw + 0.1))
108
- padded = cv2.copyMakeBorder(
109
- image, top, bot, lt, rt, cv2.BORDER_CONSTANT, value=color
110
- )
111
- return padded, ratio, (dw, dh)
112
 
113
- def _preprocess(self, image: ndarray):
114
- orig_h, orig_w = image.shape[:2]
115
  img, ratio, pad = self._letterbox(image, (self.input_width, self.input_height))
116
- img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
117
- img = img.astype(np.float32) / 255.0
118
  img = np.transpose(img, (2, 0, 1))[None, ...]
119
- img = np.ascontiguousarray(img, dtype=np.float32)
120
- return img, ratio, pad, (orig_w, orig_h)
121
 
122
  @staticmethod
123
  def _clip(boxes, size):
@@ -131,33 +128,27 @@ class Miner:
131
  @staticmethod
132
  def _hard_nms(boxes, scores, iou_thr):
133
  n = len(boxes)
134
- if n == 0:
135
- return np.array([], dtype=np.intp)
136
  order = np.argsort(-scores)
137
  keep = []
138
  while len(order) > 0:
139
- i = int(order[0])
140
- keep.append(i)
141
- if len(order) == 1:
142
- break
143
  rest = order[1:]
144
  xx1 = np.maximum(boxes[i, 0], boxes[rest, 0])
145
  yy1 = np.maximum(boxes[i, 1], boxes[rest, 1])
146
  xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
147
  yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
148
- inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
149
- a_i = max(0.0, boxes[i, 2] - boxes[i, 0]) * max(0.0, boxes[i, 3] - boxes[i, 1])
150
- a_r = (
151
- np.maximum(0.0, boxes[rest, 2] - boxes[rest, 0])
152
- * np.maximum(0.0, boxes[rest, 3] - boxes[rest, 1])
153
- )
154
- iou = inter / (a_i + a_r - inter + 1e-7)
155
  order = rest[iou <= iou_thr]
156
  return np.array(keep, dtype=np.intp)
157
 
158
  def _per_class_hard_nms(self, boxes, scores, cls_ids, iou_thr):
159
- if len(boxes) == 0:
160
- return np.array([], dtype=np.intp)
161
  keep_all = []
162
  for c in np.unique(cls_ids):
163
  mask = cls_ids == c
@@ -169,68 +160,73 @@ class Miner:
169
 
170
  def _cross_class_dedup(self, boxes, scores, cls_ids, iou_thr):
171
  n = len(boxes)
172
- if n == 0:
173
- return np.array([], dtype=np.intp)
174
  order = np.argsort(-scores)
175
- keep = []
176
- suppressed = np.zeros(n, dtype=bool)
177
  for i in order:
178
- if suppressed[i]:
179
- continue
180
  keep.append(int(i))
181
  ix1 = np.maximum(boxes[i, 0], boxes[:, 0])
182
  iy1 = np.maximum(boxes[i, 1], boxes[:, 1])
183
  ix2 = np.minimum(boxes[i, 2], boxes[:, 2])
184
  iy2 = np.minimum(boxes[i, 3], boxes[:, 3])
185
- inter = np.maximum(0.0, ix2 - ix1) * np.maximum(0.0, iy2 - iy1)
186
- a_i = max(0.0, boxes[i, 2] - boxes[i, 0]) * max(0.0, boxes[i, 3] - boxes[i, 1])
187
- a_r = (
188
- np.maximum(0.0, boxes[:, 2] - boxes[:, 0])
189
- * np.maximum(0.0, boxes[:, 3] - boxes[:, 1])
190
- )
191
- iou = inter / (a_i + a_r - inter + 1e-7)
192
  iou[i] = 0.0
193
  suppressed |= iou >= iou_thr
194
  return np.array(keep, dtype=np.intp)
195
 
196
  def _filter_sane(self, boxes, scores, cls_ids, orig_size):
197
- if len(boxes) == 0:
198
- return boxes, scores, cls_ids
199
  w, h = orig_size
200
  area_img = float(w * h)
201
- bw = np.maximum(0.0, boxes[:, 2] - boxes[:, 0])
202
- bh = np.maximum(0.0, boxes[:, 3] - boxes[:, 1])
203
  area = bw * bh
204
- ar = np.where(
205
- (bw > 0) & (bh > 0),
206
- np.maximum(bw / np.maximum(bh, 1e-6), bh / np.maximum(bw, 1e-6)),
207
- np.inf,
208
- )
209
  keep = (area <= 0.95 * area_img) & (ar <= self.max_aspect_ratio)
210
  return boxes[keep], scores[keep], cls_ids[keep]
211
 
212
- def _decode(self, preds: ndarray, ratio: float, pad: tuple[float, float],
213
- orig_size: tuple[int, int]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  if preds.ndim == 3 and preds.shape[0] == 1:
215
  preds = preds[0]
216
  if preds.ndim != 2 or preds.shape[1] < 6:
217
- return (
218
- np.empty((0, 4), dtype=np.float32),
219
- np.empty((0,), dtype=np.float32),
220
- np.empty((0,), dtype=np.int32),
221
- )
222
  boxes = preds[:, :4].astype(np.float32)
223
  scores = preds[:, 4].astype(np.float32)
224
  cls_ids = preds[:, 5].astype(np.int32)
225
  keep = (scores > 0) & (cls_ids >= 0) & (cls_ids < len(self.class_names))
226
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
227
- if len(boxes) == 0:
228
- return boxes, scores, cls_ids
229
- thr = self._conf_thres_array[cls_ids]
230
- keep = scores >= thr
231
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
232
- if len(boxes) == 0:
233
- return boxes, scores, cls_ids
234
  pad_w, pad_h = pad
235
  boxes[:, [0, 2]] -= pad_w
236
  boxes[:, [1, 3]] -= pad_h
@@ -238,12 +234,12 @@ class Miner:
238
  boxes = self._clip(boxes, orig_size)
239
  return boxes, scores, cls_ids
240
 
241
- def _predict_single(self, image: ndarray):
242
  x, ratio, pad, orig_size = self._preprocess(image)
243
  out = self.session.run(self.output_names, {self.input_name: x})[0]
244
  return self._decode(out, ratio, pad, orig_size)
245
 
246
- def _predict_tta(self, image: ndarray) -> list[BoundingBox]:
247
  b0, s0, c0 = self._predict_single(image)
248
  flipped = cv2.flip(image, 1)
249
  bf, sf, cf = self._predict_single(flipped)
@@ -256,48 +252,39 @@ class Miner:
256
  boxes = np.concatenate([b0, bf], axis=0) if len(b0) or len(bf) else b0
257
  scores = np.concatenate([s0, sf], axis=0) if len(b0) or len(bf) else s0
258
  cls_ids = np.concatenate([c0, cf], axis=0) if len(b0) or len(bf) else c0
259
- if len(boxes) == 0:
260
- return []
261
- boxes, scores, cls_ids = self._filter_sane(
262
- boxes, scores, cls_ids, (image.shape[1], image.shape[0])
263
- )
264
- if len(boxes) == 0:
265
- return []
266
  keep = self._per_class_hard_nms(boxes, scores, cls_ids, self.iou_thres)
267
- if len(keep) == 0:
268
- return []
269
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
270
  keep = self._cross_class_dedup(boxes, scores, cls_ids, self.cross_iou_thresh)
271
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
272
  if len(scores) > self.max_det:
273
- top = np.argsort(-scores)[: self.max_det]
274
  boxes, scores, cls_ids = boxes[top], scores[top], cls_ids[top]
275
  return [
276
  BoundingBox(
277
- x1=int(math.floor(b[0])),
278
- y1=int(math.floor(b[1])),
279
- x2=int(math.ceil(b[2])),
280
- y2=int(math.ceil(b[3])),
281
- cls_id=int(c),
282
- conf=float(s),
283
  )
284
  for b, s, c in zip(boxes, scores, cls_ids)
285
  if b[2] > b[0] and b[3] > b[1]
286
  ]
287
 
288
- def predict_batch(self, batch_images: list[ndarray], offset: int,
289
- n_keypoints: int) -> list[TVFrameResult]:
290
- results: list[TVFrameResult] = []
291
- for j, image in enumerate(batch_images):
292
  try:
293
- boxes = self._predict_tta(image)
294
  except Exception as e:
295
  print(f"Inference failed for frame {offset + j}: {e}")
296
  boxes = []
297
  results.append(
298
  TVFrameResult(
299
- frame_id=offset + j,
300
- boxes=boxes,
301
  keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))],
302
  )
303
  )
 
1
+ """ScoreVision crime detector v27 — YOLOv11s trained on 4-voter consensus
2
+ (new vmodel0, hermes_sv, pmodel_9, SAM3-refined; >=2 agree, IoU>=0.5).
3
+ Per-class confidence + per-class rescue (bonus) logic, flip TTA, cross-class NMS."""
4
  from pathlib import Path
5
  import math
6
 
 
27
 
28
 
29
  class Miner:
30
+ """YOLOv11s + per-class threshold + per-class rescue + flip TTA + cross-class dedup."""
31
 
32
+ # validator output order
33
  class_names = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"]
34
  input_size = 1280
35
+ iou_thres = 0.40
36
+ cross_iou_thresh = 0.70
37
  max_aspect_ratio = 10.0
38
  max_det = 150
39
+ # tuned on consensus-GT, healthy distribution: [balaclava, hoodie, glove, bat, spray, graffiti]
40
  _conf_thres_array = np.array(
41
+ [0.60, 0.70, 0.70, 0.80, 0.50, 0.20], dtype=np.float32
42
+ )
43
+ # per-class rescue gap: if a class has no box above threshold, admit top-1
44
+ # if its score >= (threshold - bonus). Inspired by new-vmodel0's _bonus_array.
45
+ _bonus_array = np.array(
46
+ [0.20, 0.25, 0.20, 0.30, 0.20, 0.15], dtype=np.float32
47
  )
48
 
49
  def __init__(self, path_hf_repo: Path) -> None:
 
54
  print("preload_dlls success")
55
  except Exception as e:
56
  print(f"preload_dlls failed: {e}")
57
+ print("ORT providers:", ort.get_available_providers())
58
 
59
+ opts = ort.SessionOptions()
60
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
61
  try:
62
  self.session = ort.InferenceSession(
63
+ str(model_path), sess_options=opts,
 
64
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
65
  )
66
+ print("CUDA session created")
67
  except Exception as e:
68
+ print(f"CUDA failed, CPU fallback: {e}")
69
  self.session = ort.InferenceSession(
70
+ str(model_path), sess_options=opts, providers=["CPUExecutionProvider"]
 
 
71
  )
72
+ print("session providers:", self.session.get_providers())
73
 
74
  for inp in self.session.get_inputs():
75
  print("INPUT:", inp.name, inp.shape, inp.type)
 
78
 
79
  self.input_name = self.session.get_inputs()[0].name
80
  self.output_names = [o.name for o in self.session.get_outputs()]
81
+ sh = self.session.get_inputs()[0].shape
82
+ self.input_height = self._safe_dim(sh[2], self.input_size)
83
+ self.input_width = self._safe_dim(sh[3], self.input_size)
84
+ print(f"ONNX loaded: {model_path}")
85
  print(
86
  "per-class conf: "
87
+ + ", ".join(f"{n}={t:.2f}" for n, t in zip(self.class_names, self._conf_thres_array.tolist()))
88
+ )
89
+ print(
90
+ "per-class rescue bonus: "
91
+ + ", ".join(f"{n}={t:.2f}" for n, t in zip(self.class_names, self._bonus_array.tolist()))
92
  )
 
 
 
93
 
94
  @staticmethod
95
+ def _safe_dim(v, d):
96
+ return v if isinstance(v, int) and v > 0 else d
97
 
98
+ def _letterbox(self, image, new_shape, color=(114, 114, 114)):
 
 
99
  h, w = image.shape[:2]
100
+ nw, nh = new_shape
101
+ r = min(nw / w, nh / h)
102
+ rw, rh = int(round(w * r)), int(round(h * r))
103
  if (rw, rh) != (w, h):
104
+ interp = cv2.INTER_CUBIC if r > 1.0 else cv2.INTER_LINEAR
105
  image = cv2.resize(image, (rw, rh), interpolation=interp)
106
+ dw, dh = (nw - rw) / 2.0, (nh - rh) / 2.0
107
  top, bot = int(round(dh - 0.1)), int(round(dh + 0.1))
108
  lt, rt = int(round(dw - 0.1)), int(round(dw + 0.1))
109
+ padded = cv2.copyMakeBorder(image, top, bot, lt, rt, cv2.BORDER_CONSTANT, value=color)
110
+ return padded, r, (dw, dh)
 
 
111
 
112
+ def _preprocess(self, image):
113
+ h, w = image.shape[:2]
114
  img, ratio, pad = self._letterbox(image, (self.input_width, self.input_height))
115
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
 
116
  img = np.transpose(img, (2, 0, 1))[None, ...]
117
+ return np.ascontiguousarray(img, dtype=np.float32), ratio, pad, (w, h)
 
118
 
119
  @staticmethod
120
  def _clip(boxes, size):
 
128
  @staticmethod
129
  def _hard_nms(boxes, scores, iou_thr):
130
  n = len(boxes)
131
+ if n == 0: return np.array([], dtype=np.intp)
 
132
  order = np.argsort(-scores)
133
  keep = []
134
  while len(order) > 0:
135
+ i = int(order[0]); keep.append(i)
136
+ if len(order) == 1: break
 
 
137
  rest = order[1:]
138
  xx1 = np.maximum(boxes[i, 0], boxes[rest, 0])
139
  yy1 = np.maximum(boxes[i, 1], boxes[rest, 1])
140
  xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
141
  yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
142
+ inter = np.maximum(0.0, xx2-xx1)*np.maximum(0.0, yy2-yy1)
143
+ a_i = max(0.0, boxes[i, 2]-boxes[i, 0])*max(0.0, boxes[i, 3]-boxes[i, 1])
144
+ a_r = (np.maximum(0.0, boxes[rest, 2]-boxes[rest, 0])
145
+ * np.maximum(0.0, boxes[rest, 3]-boxes[rest, 1]))
146
+ iou = inter / (a_i+a_r-inter+1e-7)
 
 
147
  order = rest[iou <= iou_thr]
148
  return np.array(keep, dtype=np.intp)
149
 
150
  def _per_class_hard_nms(self, boxes, scores, cls_ids, iou_thr):
151
+ if len(boxes) == 0: return np.array([], dtype=np.intp)
 
152
  keep_all = []
153
  for c in np.unique(cls_ids):
154
  mask = cls_ids == c
 
160
 
161
  def _cross_class_dedup(self, boxes, scores, cls_ids, iou_thr):
162
  n = len(boxes)
163
+ if n == 0: return np.array([], dtype=np.intp)
 
164
  order = np.argsort(-scores)
165
+ keep = []; suppressed = np.zeros(n, dtype=bool)
 
166
  for i in order:
167
+ if suppressed[i]: continue
 
168
  keep.append(int(i))
169
  ix1 = np.maximum(boxes[i, 0], boxes[:, 0])
170
  iy1 = np.maximum(boxes[i, 1], boxes[:, 1])
171
  ix2 = np.minimum(boxes[i, 2], boxes[:, 2])
172
  iy2 = np.minimum(boxes[i, 3], boxes[:, 3])
173
+ inter = np.maximum(0.0, ix2-ix1)*np.maximum(0.0, iy2-iy1)
174
+ a_i = max(0.0, boxes[i, 2]-boxes[i, 0])*max(0.0, boxes[i, 3]-boxes[i, 1])
175
+ a_r = (np.maximum(0.0, boxes[:, 2]-boxes[:, 0])
176
+ * np.maximum(0.0, boxes[:, 3]-boxes[:, 1]))
177
+ iou = inter / (a_i+a_r-inter+1e-7)
 
 
178
  iou[i] = 0.0
179
  suppressed |= iou >= iou_thr
180
  return np.array(keep, dtype=np.intp)
181
 
182
  def _filter_sane(self, boxes, scores, cls_ids, orig_size):
183
+ if len(boxes) == 0: return boxes, scores, cls_ids
 
184
  w, h = orig_size
185
  area_img = float(w * h)
186
+ bw = np.maximum(0.0, boxes[:, 2]-boxes[:, 0])
187
+ bh = np.maximum(0.0, boxes[:, 3]-boxes[:, 1])
188
  area = bw * bh
189
+ ar = np.where((bw > 0) & (bh > 0),
190
+ np.maximum(bw / np.maximum(bh, 1e-6), bh / np.maximum(bw, 1e-6)),
191
+ np.inf)
 
 
192
  keep = (area <= 0.95 * area_img) & (ar <= self.max_aspect_ratio)
193
  return boxes[keep], scores[keep], cls_ids[keep]
194
 
195
+ def _conf_filter_with_rescue(self, boxes, scores, cls_ids):
196
+ """Per-class threshold + per-class rescue (admit top-1 if score >= thr - bonus
197
+ and no box of that class passed the normal threshold)."""
198
+ if len(scores) == 0:
199
+ return np.zeros(0, dtype=bool)
200
+ thr = self._conf_thres_array[cls_ids]
201
+ keep = scores >= thr
202
+ for c in np.unique(cls_ids):
203
+ bonus = float(self._bonus_array[c])
204
+ if bonus <= 0.0: continue
205
+ cm = cls_ids == c
206
+ if keep[cm].any(): continue
207
+ idx = np.where(cm)[0]
208
+ top = int(idx[int(np.argmax(scores[idx]))])
209
+ if scores[top] >= self._conf_thres_array[c] - bonus:
210
+ keep[top] = True
211
+ return keep
212
+
213
+ def _decode(self, preds, ratio, pad, orig_size):
214
  if preds.ndim == 3 and preds.shape[0] == 1:
215
  preds = preds[0]
216
  if preds.ndim != 2 or preds.shape[1] < 6:
217
+ return (np.empty((0, 4), dtype=np.float32),
218
+ np.empty((0,), dtype=np.float32),
219
+ np.empty((0,), dtype=np.int32))
 
 
220
  boxes = preds[:, :4].astype(np.float32)
221
  scores = preds[:, 4].astype(np.float32)
222
  cls_ids = preds[:, 5].astype(np.int32)
223
  keep = (scores > 0) & (cls_ids >= 0) & (cls_ids < len(self.class_names))
224
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
225
+ if len(boxes) == 0: return boxes, scores, cls_ids
226
+ # per-class threshold WITH rescue
227
+ keep = self._conf_filter_with_rescue(boxes, scores, cls_ids)
 
228
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
229
+ if len(boxes) == 0: return boxes, scores, cls_ids
 
230
  pad_w, pad_h = pad
231
  boxes[:, [0, 2]] -= pad_w
232
  boxes[:, [1, 3]] -= pad_h
 
234
  boxes = self._clip(boxes, orig_size)
235
  return boxes, scores, cls_ids
236
 
237
+ def _predict_single(self, image):
238
  x, ratio, pad, orig_size = self._preprocess(image)
239
  out = self.session.run(self.output_names, {self.input_name: x})[0]
240
  return self._decode(out, ratio, pad, orig_size)
241
 
242
+ def _predict_tta(self, image):
243
  b0, s0, c0 = self._predict_single(image)
244
  flipped = cv2.flip(image, 1)
245
  bf, sf, cf = self._predict_single(flipped)
 
252
  boxes = np.concatenate([b0, bf], axis=0) if len(b0) or len(bf) else b0
253
  scores = np.concatenate([s0, sf], axis=0) if len(b0) or len(bf) else s0
254
  cls_ids = np.concatenate([c0, cf], axis=0) if len(b0) or len(bf) else c0
255
+ if len(boxes) == 0: return []
256
+ boxes, scores, cls_ids = self._filter_sane(boxes, scores, cls_ids,
257
+ (image.shape[1], image.shape[0]))
258
+ if len(boxes) == 0: return []
 
 
 
259
  keep = self._per_class_hard_nms(boxes, scores, cls_ids, self.iou_thres)
260
+ if len(keep) == 0: return []
 
261
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
262
  keep = self._cross_class_dedup(boxes, scores, cls_ids, self.cross_iou_thresh)
263
  boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
264
  if len(scores) > self.max_det:
265
+ top = np.argsort(-scores)[:self.max_det]
266
  boxes, scores, cls_ids = boxes[top], scores[top], cls_ids[top]
267
  return [
268
  BoundingBox(
269
+ x1=int(math.floor(b[0])), y1=int(math.floor(b[1])),
270
+ x2=int(math.ceil(b[2])), y2=int(math.ceil(b[3])),
271
+ cls_id=int(c), conf=float(s),
 
 
 
272
  )
273
  for b, s, c in zip(boxes, scores, cls_ids)
274
  if b[2] > b[0] and b[3] > b[1]
275
  ]
276
 
277
+ def predict_batch(self, batch_images, offset, n_keypoints):
278
+ results = []
279
+ for j, img in enumerate(batch_images):
 
280
  try:
281
+ boxes = self._predict_tta(img)
282
  except Exception as e:
283
  print(f"Inference failed for frame {offset + j}: {e}")
284
  boxes = []
285
  results.append(
286
  TVFrameResult(
287
+ frame_id=offset + j, boxes=boxes,
 
288
  keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))],
289
  )
290
  )