meaculpitt commited on
Commit
353ae45
·
verified ·
1 Parent(s): 853af23

deploy push for crime (deploy)

Browse files
Files changed (1) hide show
  1. miner.py +37 -202
miner.py CHANGED
@@ -1,25 +1,17 @@
1
- # build-marker: v3-ensemble-alfred-priority-all
2
- """SN44 crime detection miner — ENSEMBLE of alfred yolo26n + RF-DETR base.
3
 
4
- Composes two internal miners with different preprocess/inference pipelines:
5
- _AlfredMiner: yolo26n e2e ONNX, letterbox 1280 + /255, TTA (h-flip + conf boost)
6
- _RFDETRMiner: rfdetr base e2e ONNX, stretch 1288 + ImageNet normalize, no TTA
 
 
7
 
8
- Class routing (final union after per-class NMS@IoU=0.5, alfred wins conflicts):
9
- cls0 balaclava : BOTH (alfred priority on conflicts)
10
- cls1 hoodie : BOTH (alfred priority on conflicts)
11
- cls2 glove : BOTH (alfred priority on conflicts)
12
- cls3 bat : BOTH (alfred priority on conflicts)
13
- cls4 spray paint: BOTH (alfred priority on conflicts)
14
- cls5 graffiti : alfred only (RF-DETR can't read static walls)
15
 
16
- Conf threshold 0.52 is applied INSIDE each internal miner; the union is the
17
- already-thresholded boxes from each. This matches alfred's existing per-class
18
- calibration (TTA conf-boost happens against the 0.52 threshold).
19
-
20
- ONNX file names expected in path_hf_repo:
21
- weights.onnx - alfred yolo26n e2e [1,300,6] in input-pixel coords (1280)
22
- weights_rfdetr.onnx - RF-DETR base e2e [1,300,6] in input-pixel coords (1288)
23
  """
24
  import math
25
  from pathlib import Path
@@ -46,46 +38,57 @@ class TVFrameResult(BaseModel):
46
  keypoints: list[tuple[int, int]]
47
 
48
 
49
- _IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
50
- _IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
51
-
52
 
53
- # ============================================================ ALFRED PATH
54
- # Verbatim alfred-style pipeline (letterbox + TTA). Returns list[BoundingBox]
55
- # already conf-filtered at 0.52, geometry-filtered, NMS'd, cross-class deduped.
56
- class _AlfredMiner:
57
- def __init__(self, path_hf_repo: Path):
58
- self.path_hf_repo = path_hf_repo
59
  self.class_names = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"]
60
- self.cls_remap = np.arange(6, dtype=np.int32)
 
 
 
 
 
61
 
62
  sess_options = ort.SessionOptions()
63
  sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
64
  try:
65
  self.session = ort.InferenceSession(
66
- str(path_hf_repo / "weights.onnx"),
67
  sess_options=sess_options,
68
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
69
  )
70
  except Exception:
71
  self.session = ort.InferenceSession(
72
- str(path_hf_repo / "weights.onnx"),
73
  sess_options=sess_options,
74
  providers=["CPUExecutionProvider"],
75
  )
76
  self.input_name = self.session.get_inputs()[0].name
77
  self.output_names = [o.name for o in self.session.get_outputs()]
 
78
  self.input_h = 1280
79
  self.input_w = 1280
80
  self.conf_threshold = 0.52
81
  self.iou_thresh = 0.4
82
  self.cross_iou_thresh = 0.7
83
  self.max_det = 150
84
- self.use_tta = True
85
  self.min_box_area = 196
86
  self.min_side = 8
87
  self.max_aspect_ratio = 8.0
88
 
 
 
 
 
 
 
 
 
 
 
89
  def _letterbox(self, image):
90
  h, w = image.shape[:2]
91
  ratio = min(self.input_w / w, self.input_h / h)
@@ -162,21 +165,6 @@ class _AlfredMiner:
162
  kept = np.array(keep, dtype=np.intp)
163
  return boxes[kept], scores[kept], cls_ids[kept]
164
 
165
- @staticmethod
166
- def _max_score_per_cluster(coords, scores, keep_idx, iou_thresh):
167
- if len(keep_idx) == 0: return np.array([], dtype=np.float32)
168
- out = np.empty(len(keep_idx), dtype=np.float32)
169
- for j, idx in enumerate(keep_idx):
170
- bi = coords[idx]
171
- xx1 = np.maximum(bi[0], coords[:, 0]); yy1 = np.maximum(bi[1], coords[:, 1])
172
- xx2 = np.minimum(bi[2], coords[:, 2]); yy2 = np.minimum(bi[3], coords[:, 3])
173
- inter = np.maximum(0.0, xx2-xx1) * np.maximum(0.0, yy2-yy1)
174
- ai = (bi[2]-bi[0])*(bi[3]-bi[1])
175
- aj = (coords[:, 2]-coords[:, 0]) * (coords[:, 3]-coords[:, 1])
176
- iou = inter / (ai + aj - inter + 1e-7)
177
- out[j] = float(np.max(scores[iou >= iou_thresh]))
178
- return out
179
-
180
  def _infer_single(self, image_bgr):
181
  inp, ratio, (dx, dy) = self._preprocess(image_bgr)
182
  out = self.session.run(self.output_names, {self.input_name: inp})[0]
@@ -200,29 +188,6 @@ class _AlfredMiner:
200
  boxes, confs, cls_ids = self._cross_class_dedup(boxes, confs, cls_ids, self.cross_iou_thresh)
201
  return self._to_boundingboxes(boxes, confs, cls_ids, ow, oh)
202
 
203
- def _infer_tta(self, image_bgr):
204
- boxes_orig = self._infer_single(image_bgr)
205
- h, w = image_bgr.shape[:2]
206
- flipped = cv2.flip(image_bgr, 1)
207
- boxes_flip_raw = self._infer_single(flipped)
208
- boxes_flip = [BoundingBox(x1=w-b.x2, y1=b.y1, x2=w-b.x1, y2=b.y2, cls_id=b.cls_id, conf=b.conf)
209
- for b in boxes_flip_raw]
210
- all_boxes = boxes_orig + boxes_flip
211
- if not all_boxes: return []
212
- coords = np.array([[b.x1, b.y1, b.x2, b.y2] for b in all_boxes], dtype=np.float32)
213
- scores = np.array([b.conf for b in all_boxes], dtype=np.float32)
214
- cls_ids = np.array([b.cls_id for b in all_boxes], dtype=np.int32)
215
- keep_idx = self._per_class_hard_nms(coords, scores, cls_ids, self.iou_thresh)
216
- if len(keep_idx) == 0: return []
217
- keep_idx = keep_idx[: self.max_det]
218
- boosted = self._max_score_per_cluster(coords, scores, keep_idx, self.iou_thresh)
219
- out = []
220
- for j, idx in enumerate(keep_idx):
221
- b = all_boxes[idx]
222
- out.append(BoundingBox(x1=b.x1, y1=b.y1, x2=b.x2, y2=b.y2, cls_id=b.cls_id,
223
- conf=max(0.0, min(1.0, float(boosted[j])))))
224
- return out
225
-
226
  def _to_boundingboxes(self, boxes, confs, cls_ids, orig_w, orig_h):
227
  out = []
228
  for i in range(len(boxes)):
@@ -241,143 +206,13 @@ class _AlfredMiner:
241
  conf=max(0.0, min(1.0, float(confs[i])))))
242
  return out
243
 
244
- def predict_one(self, image_bgr):
245
- return self._infer_tta(image_bgr) if self.use_tta else self._infer_single(image_bgr)
246
-
247
-
248
- # ============================================================ RFDETR PATH
249
- class _RFDETRMiner:
250
- def __init__(self, path_hf_repo: Path):
251
- sess_options = ort.SessionOptions()
252
- sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
253
- try:
254
- self.session = ort.InferenceSession(
255
- str(path_hf_repo / "weights_rfdetr.onnx"),
256
- sess_options=sess_options,
257
- providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
258
- )
259
- except Exception:
260
- self.session = ort.InferenceSession(
261
- str(path_hf_repo / "weights_rfdetr.onnx"),
262
- sess_options=sess_options,
263
- providers=["CPUExecutionProvider"],
264
- )
265
- self.input_name = self.session.get_inputs()[0].name
266
- self.output_names = [o.name for o in self.session.get_outputs()]
267
- self.input_h = 1288
268
- self.input_w = 1288
269
- self.conf_threshold = 0.52
270
- self.min_box_area = 196
271
- self.min_side = 8
272
- self.max_aspect_ratio = 8.0
273
-
274
- def predict_one(self, image_bgr):
275
- oh, ow = image_bgr.shape[:2]
276
- rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
277
- resized = cv2.resize(rgb, (self.input_w, self.input_h), interpolation=cv2.INTER_LINEAR)
278
- x = resized.astype(np.float32) / 255.0
279
- x = (x - _IMAGENET_MEAN) / _IMAGENET_STD
280
- x = np.ascontiguousarray(np.transpose(x, (2, 0, 1))[None, ...].astype(np.float32))
281
- out = self.session.run(self.output_names, {self.input_name: x})[0]
282
- if out.ndim == 3: out = out[0]
283
- confs = out[:, 4].astype(np.float32)
284
- keep = confs >= self.conf_threshold
285
- if not keep.any(): return []
286
- out = out[keep]
287
- boxes = out[:, :4].astype(np.float32).copy()
288
- confs = out[:, 4].astype(np.float32)
289
- cls_ids = out[:, 5].astype(np.int32)
290
- sx = ow / float(self.input_w); sy = oh / float(self.input_h)
291
- boxes[:, [0, 2]] *= sx; boxes[:, [1, 3]] *= sy
292
- boxes[:, [0, 2]] = np.clip(boxes[:, [0, 2]], 0, ow - 1)
293
- boxes[:, [1, 3]] = np.clip(boxes[:, [1, 3]], 0, oh - 1)
294
- out_boxes = []
295
- for i in range(len(boxes)):
296
- x1, y1, x2, y2 = boxes[i]
297
- ix1 = max(0, min(ow, math.floor(x1))); iy1 = max(0, min(oh, math.floor(y1)))
298
- ix2 = max(0, min(ow, math.ceil(x2))); iy2 = max(0, min(oh, math.ceil(y2)))
299
- if ix2 <= ix1 or iy2 <= iy1: continue
300
- bw, bh = ix2 - ix1, iy2 - iy1
301
- if bw * bh < self.min_box_area: continue
302
- if min(bw, bh) < self.min_side: continue
303
- ar = max(bw / max(bh, 1), bh / max(bw, 1))
304
- if ar > self.max_aspect_ratio: continue
305
- out_boxes.append(BoundingBox(x1=ix1, y1=iy1, x2=ix2, y2=iy2,
306
- cls_id=int(cls_ids[i]),
307
- conf=max(0.0, min(1.0, float(confs[i])))))
308
- return out_boxes
309
-
310
-
311
- # ============================================================ ENSEMBLE PUBLIC
312
- class Miner:
313
- """Public ensemble miner — chute calls predict_batch(...)."""
314
-
315
- def __init__(self, path_hf_repo) -> None:
316
- self.path_hf_repo = Path(path_hf_repo)
317
- self.class_names = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"]
318
- try:
319
- ort.preload_dlls()
320
- except Exception:
321
- pass
322
- self.alfred = _AlfredMiner(self.path_hf_repo)
323
- self.rfdetr = _RFDETRMiner(self.path_hf_repo)
324
- # v3 (2026-05-04): all classes go through alfred (was {0,1,5}).
325
- # cid 61709 post-mortem showed alfred returning correct gloves at
326
- # conf 0.79/0.89 that the prior {0,1,5} filter dropped, costing the
327
- # full validator score on that frame. RF-DETR remains additive on
328
- # cls 0..4; cls 5 (graffiti) stays alfred-only since RF-DETR can't
329
- # read static walls.
330
- self.alfred_classes = {0, 1, 2, 3, 4, 5}
331
- self.rfdetr_classes = {0, 1, 2, 3, 4}
332
- self.merge_iou = 0.5
333
- # Warmup
334
- warm = np.zeros((1280, 1280, 3), dtype=np.uint8)
335
- for _ in range(2):
336
- try: self.alfred.predict_one(warm)
337
- except Exception: break
338
- for _ in range(2):
339
- try: self.rfdetr.predict_one(warm)
340
- except Exception: break
341
-
342
- def __repr__(self):
343
- return (f"CrimeEnsembleMiner v3 alfred(yolo26n@1280, TTA) + "
344
- f"rfdetr(base@1288) conf>=0.52 merge_iou={self.merge_iou} "
345
- f"alfred_priority_all_classes")
346
-
347
- @staticmethod
348
- def _box_iou(a: BoundingBox, b: BoundingBox) -> float:
349
- xx1 = max(a.x1, b.x1); yy1 = max(a.y1, b.y1)
350
- xx2 = min(a.x2, b.x2); yy2 = min(a.y2, b.y2)
351
- inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
352
- ai = (a.x2 - a.x1) * (a.y2 - a.y1)
353
- bi = (b.x2 - b.x1) * (b.y2 - b.y1)
354
- return inter / (ai + bi - inter + 1e-7)
355
-
356
- def _merge(self, alfred_boxes: list, rfdetr_boxes: list) -> list:
357
- """Per-class union: alfred always kept; rfdetr kept ONLY if not overlapping
358
- an alfred same-class box at IoU >= merge_iou. cls 0..4 see both — alfred
359
- priority on conflicts; cls 5 is alfred-only (no rfdetr boxes there)."""
360
- kept = list(alfred_boxes)
361
- for rb in rfdetr_boxes:
362
- collide = False
363
- for ab in alfred_boxes:
364
- if ab.cls_id == rb.cls_id and self._box_iou(ab, rb) >= self.merge_iou:
365
- collide = True; break
366
- if not collide:
367
- kept.append(rb)
368
- return kept
369
-
370
  def predict_batch(self, batch_images, offset, n_keypoints):
371
  results = []
372
  for idx, image in enumerate(batch_images):
373
- a_all = self.alfred.predict_one(image)
374
- r_all = self.rfdetr.predict_one(image)
375
- a_keep = [b for b in a_all if b.cls_id in self.alfred_classes]
376
- r_keep = [b for b in r_all if b.cls_id in self.rfdetr_classes]
377
- merged = self._merge(a_keep, r_keep)
378
  results.append(TVFrameResult(
379
  frame_id=offset + idx,
380
- boxes=merged,
381
  keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))],
382
  ))
383
  return results
 
1
+ # build-marker: v5-alfred-only-no-tta
2
+ """SN44 crime detection miner — ALFRED ONLY, no TTA, no RF-DETR.
3
 
4
+ v5 (2026-05-04): drops the RF-DETR branch entirely. Component benchmarks showed
5
+ RF-DETR was ~10× slower than alfred (8.2s vs 0.8s on CPU) and contributed zero
6
+ observed scoring credit on cid 61709 (alfred alone returned the same 3 correct
7
+ boxes that the alfred-competitor used to earn 0.8). Goal: get under the 5s
8
+ validator gate with comfortable margin (target p95 < 2000ms e2e).
9
 
10
+ Single ONNX file expected in path_hf_repo:
11
+ weights.onnx alfred yolo26n e2e [1,300,6] in input-pixel coords (1280)
 
 
 
 
 
12
 
13
+ Conf threshold 0.52, NMS IoU 0.4, min_box_area 196 unchanged from v3/v4.
14
+ All 6 classes routed through alfred (identity remap).
 
 
 
 
 
15
  """
16
  import math
17
  from pathlib import Path
 
38
  keypoints: list[tuple[int, int]]
39
 
40
 
41
+ class Miner:
42
+ """Public miner chute calls predict_batch(...). v5 is alfred-only,
43
+ single forward pass, no TTA, no RF-DETR."""
44
 
45
+ def __init__(self, path_hf_repo) -> None:
46
+ self.path_hf_repo = Path(path_hf_repo)
 
 
 
 
47
  self.class_names = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"]
48
+ self.cls_remap = np.arange(6, dtype=np.int32) # identity remap, all classes
49
+
50
+ try:
51
+ ort.preload_dlls()
52
+ except Exception:
53
+ pass
54
 
55
  sess_options = ort.SessionOptions()
56
  sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
57
  try:
58
  self.session = ort.InferenceSession(
59
+ str(self.path_hf_repo / "weights.onnx"),
60
  sess_options=sess_options,
61
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
62
  )
63
  except Exception:
64
  self.session = ort.InferenceSession(
65
+ str(self.path_hf_repo / "weights.onnx"),
66
  sess_options=sess_options,
67
  providers=["CPUExecutionProvider"],
68
  )
69
  self.input_name = self.session.get_inputs()[0].name
70
  self.output_names = [o.name for o in self.session.get_outputs()]
71
+
72
  self.input_h = 1280
73
  self.input_w = 1280
74
  self.conf_threshold = 0.52
75
  self.iou_thresh = 0.4
76
  self.cross_iou_thresh = 0.7
77
  self.max_det = 150
 
78
  self.min_box_area = 196
79
  self.min_side = 8
80
  self.max_aspect_ratio = 8.0
81
 
82
+ # Warmup
83
+ warm = np.zeros((1280, 1280, 3), dtype=np.uint8)
84
+ for _ in range(2):
85
+ try: self._infer_single(warm)
86
+ except Exception: break
87
+
88
+ def __repr__(self):
89
+ return (f"CrimeMiner v5 alfred-only(yolo26n@1280, NO TTA) "
90
+ f"conf>=0.52 iou={self.iou_thresh} min_area={self.min_box_area}")
91
+
92
  def _letterbox(self, image):
93
  h, w = image.shape[:2]
94
  ratio = min(self.input_w / w, self.input_h / h)
 
165
  kept = np.array(keep, dtype=np.intp)
166
  return boxes[kept], scores[kept], cls_ids[kept]
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  def _infer_single(self, image_bgr):
169
  inp, ratio, (dx, dy) = self._preprocess(image_bgr)
170
  out = self.session.run(self.output_names, {self.input_name: inp})[0]
 
188
  boxes, confs, cls_ids = self._cross_class_dedup(boxes, confs, cls_ids, self.cross_iou_thresh)
189
  return self._to_boundingboxes(boxes, confs, cls_ids, ow, oh)
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  def _to_boundingboxes(self, boxes, confs, cls_ids, orig_w, orig_h):
192
  out = []
193
  for i in range(len(boxes)):
 
206
  conf=max(0.0, min(1.0, float(confs[i])))))
207
  return out
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  def predict_batch(self, batch_images, offset, n_keypoints):
210
  results = []
211
  for idx, image in enumerate(batch_images):
212
+ boxes = self._infer_single(image)
 
 
 
 
213
  results.append(TVFrameResult(
214
  frame_id=offset + idx,
215
+ boxes=boxes,
216
  keypoints=[(0, 0) for _ in range(max(0, int(n_keypoints)))],
217
  ))
218
  return results