MTerryJack commited on
Commit
74ebbaf
·
verified ·
1 Parent(s): c9f0209

subnet_bridge: copy winning miner repo into library

Browse files
Files changed (4) hide show
  1. README.md +3 -3
  2. miner.py +37 -151
  3. readme.md +3 -3
  4. weights.onnx +1 -1
README.md CHANGED
@@ -10,9 +10,9 @@ tags:
10
  manako:
11
  source: winner_fetch
12
  manifest_element_name: manak0/Detect-fire
13
- winner_repo_id: navierstocks/fire-light
14
- winner_revision: 95133792375f1fd3e5f192d0494c3b02f770cdc4
15
- note: E=0.03088120 (map50=0.600000, size_mb=19.429295)
16
  ---
17
 
18
  ## YOLO26 ONNX detector
 
10
  manako:
11
  source: winner_fetch
12
  manifest_element_name: manak0/Detect-fire
13
+ winner_repo_id: navierstocks/disaster
14
+ winner_revision: 3a0b049b490e28f3d29f0328c2af1a7799217933
15
+ note: E=0.03088868 (map50=0.600000, size_mb=19.424589)
16
  ---
17
 
18
  ## YOLO26 ONNX detector
miner.py CHANGED
@@ -24,17 +24,17 @@ class TVFrameResult(BaseModel):
24
 
25
 
26
  class Miner:
27
- """ONNX Runtime miner. Soft per-class NMS + sanity filter + flip TTA."""
28
 
29
  class_names = ["fire", "smoke", "fire extinguisher"]
30
  input_size = 1280
31
- iou_thres = 0.3
32
- soft_sigma = 0.5
33
- min_side = 8.0
34
  min_box_area = 144.0
35
  max_aspect_ratio = 6.0
36
- max_det = 100
37
- _conf_thres_array = np.array([0.25, 0.15, 0.15], dtype=np.float32)
 
38
 
39
  def __init__(self, path_hf_repo: Path) -> None:
40
  model_path = path_hf_repo / "weights.onnx"
@@ -81,6 +81,7 @@ class Miner:
81
  self.input_width = self._safe_dim(self.input_shape[3], default=self.input_size)
82
 
83
  print(f"ONNX model loaded from: {model_path}")
 
84
  print(f"ONNX input: name={self.input_name}, shape={self.input_shape}")
85
  print("per-class conf: " + ", ".join(
86
  f"{n}={t:.3f}" for n, t in zip(self.class_names,
@@ -173,81 +174,9 @@ class Miner:
173
  order = rest[iou <= iou_thresh]
174
  return np.array(keep, dtype=np.intp)
175
 
176
- @staticmethod
177
- def _soft_nms(boxes: np.ndarray, scores: np.ndarray,
178
- sigma: float, score_thresh: float = 0.001
179
- ) -> tuple[np.ndarray, np.ndarray]:
180
- n = len(boxes)
181
- if n == 0:
182
- return np.array([], dtype=np.intp), np.array([], dtype=np.float32)
183
- boxes = boxes.astype(np.float32, copy=True)
184
- scores = scores.astype(np.float32, copy=True)
185
- order = np.arange(n)
186
- for i in range(n):
187
- max_pos = i + int(np.argmax(scores[i:]))
188
- boxes[[i, max_pos]] = boxes[[max_pos, i]]
189
- scores[[i, max_pos]] = scores[[max_pos, i]]
190
- order[[i, max_pos]] = order[[max_pos, i]]
191
- if i + 1 >= n:
192
- break
193
- xx1 = np.maximum(boxes[i, 0], boxes[i + 1:, 0])
194
- yy1 = np.maximum(boxes[i, 1], boxes[i + 1:, 1])
195
- xx2 = np.minimum(boxes[i, 2], boxes[i + 1:, 2])
196
- yy2 = np.minimum(boxes[i, 3], boxes[i + 1:, 3])
197
- inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
198
- a_i = max(0.0, float((boxes[i, 2] - boxes[i, 0]) *
199
- (boxes[i, 3] - boxes[i, 1])))
200
- a_j = (np.maximum(0.0, boxes[i + 1:, 2] - boxes[i + 1:, 0]) *
201
- np.maximum(0.0, boxes[i + 1:, 3] - boxes[i + 1:, 1]))
202
- iou = inter / (a_i + a_j - inter + 1e-7)
203
- scores[i + 1:] *= np.exp(-(iou ** 2) / sigma)
204
- mask = scores > score_thresh
205
- return order[mask], scores[mask]
206
-
207
- def _per_class_hard_nms(self, boxes: np.ndarray, scores: np.ndarray,
208
- cls_ids: np.ndarray, iou_thresh: float
209
- ) -> np.ndarray:
210
- if len(boxes) == 0:
211
- return np.array([], dtype=np.intp)
212
- all_keep: list[int] = []
213
- for c in np.unique(cls_ids):
214
- mask = cls_ids == c
215
- indices = np.where(mask)[0]
216
- keep = self._hard_nms(boxes[mask], scores[mask], iou_thresh)
217
- all_keep.extend(indices[keep].tolist())
218
- all_keep.sort()
219
- return np.array(all_keep, dtype=np.intp)
220
-
221
- def _per_class_soft_nms(self, boxes: np.ndarray, scores: np.ndarray,
222
- cls_ids: np.ndarray
223
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
224
- if len(boxes) == 0:
225
- return boxes, scores, cls_ids
226
- out_b: list = []
227
- out_s: list = []
228
- out_c: list = []
229
- for c in np.unique(cls_ids):
230
- mask = cls_ids == c
231
- sub_b = boxes[mask]
232
- sub_s = scores[mask]
233
- sub_c = cls_ids[mask]
234
- idx, decayed = self._soft_nms(sub_b, sub_s, self.soft_sigma)
235
- if len(idx) == 0:
236
- continue
237
- out_b.append(sub_b[idx])
238
- out_s.append(decayed)
239
- out_c.append(sub_c[idx])
240
- if not out_b:
241
- return (np.empty((0, 4), dtype=np.float32),
242
- np.empty((0,), dtype=np.float32),
243
- np.empty((0,), dtype=cls_ids.dtype))
244
- return (np.concatenate(out_b, axis=0),
245
- np.concatenate(out_s, axis=0),
246
- np.concatenate(out_c, axis=0))
247
-
248
- def _filter_sane_boxes(self, boxes: np.ndarray, scores: np.ndarray,
249
- cls_ids: np.ndarray, orig_size: tuple[int, int]
250
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
251
  if len(boxes) == 0:
252
  return boxes, scores, cls_ids
253
  orig_w, orig_h = orig_size
@@ -268,40 +197,39 @@ class Miner:
268
  )
269
  return boxes[keep], scores[keep], cls_ids[keep]
270
 
271
- @staticmethod
272
- def _max_score_per_cluster(post_boxes: np.ndarray,
273
- full_boxes: np.ndarray,
274
- full_scores: np.ndarray,
275
- iou_thresh: float) -> np.ndarray:
276
- n = len(post_boxes)
277
- if n == 0:
278
- return np.empty(0, dtype=np.float32)
279
- full_areas = (np.maximum(0.0, full_boxes[:, 2] - full_boxes[:, 0]) *
280
- np.maximum(0.0, full_boxes[:, 3] - full_boxes[:, 1]))
281
- out = np.empty(n, dtype=np.float32)
282
- for i in range(n):
283
- bi = post_boxes[i]
284
- xx1 = np.maximum(bi[0], full_boxes[:, 0])
285
- yy1 = np.maximum(bi[1], full_boxes[:, 1])
286
- xx2 = np.minimum(bi[2], full_boxes[:, 2])
287
- yy2 = np.minimum(bi[3], full_boxes[:, 3])
288
- inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
289
- a_i = max(0.0, float((bi[2] - bi[0]) * (bi[3] - bi[1])))
290
- iou = inter / (a_i + full_areas - inter + 1e-7)
291
- cluster = iou >= iou_thresh
292
- out[i] = float(np.max(full_scores[cluster])) if np.any(cluster) else 0.0
293
- return out
294
 
295
  def _per_view_pipeline(self, boxes: np.ndarray, scores: np.ndarray,
296
  cls_ids: np.ndarray, orig_size: tuple[int, int]
297
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
298
- boxes, scores, cls_ids = self._filter_sane_boxes(
299
  boxes, scores, cls_ids, orig_size
300
  )
301
  if len(boxes) == 0:
302
  return boxes, scores, cls_ids
303
  if len(boxes) > 1:
304
- boxes, scores, cls_ids = self._per_class_soft_nms(boxes, scores, cls_ids)
 
305
  if len(scores) > self.max_det:
306
  top = np.argsort(-scores)[: self.max_det]
307
  boxes, scores, cls_ids = boxes[top], scores[top], cls_ids[top]
@@ -319,7 +247,7 @@ class Miner:
319
  scores = preds[:, 4].astype(np.float32)
320
  cls_ids = preds[:, 5].astype(np.int32)
321
 
322
- keep = scores >= self._conf_thres_array[cls_ids]
323
  boxes = boxes[keep]
324
  scores = scores[keep]
325
  cls_ids = cls_ids[keep]
@@ -357,7 +285,7 @@ class Miner:
357
  cls_ids = np.argmax(cls_part, axis=1).astype(np.int32)
358
  scores = cls_part[np.arange(len(cls_part)), cls_ids]
359
 
360
- keep = scores >= self._conf_thres_array[cls_ids]
361
  boxes_xywh = boxes_xywh[keep]
362
  scores = scores[keep]
363
  cls_ids = cls_ids[keep]
@@ -427,54 +355,12 @@ class Miner:
427
  outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
428
  return self._postprocess(outputs[0], ratio, pad, orig_size)
429
 
430
- def _predict_tta(self, image: np.ndarray) -> list[BoundingBox]:
431
- boxes_orig = self._predict_single(image)
432
- flipped = cv2.flip(image, 1)
433
- boxes_flip = self._predict_single(flipped)
434
- w = image.shape[1]
435
- boxes_flip = [
436
- BoundingBox(
437
- x1=w - b.x2, y1=b.y1, x2=w - b.x1, y2=b.y2,
438
- cls_id=b.cls_id, conf=b.conf,
439
- )
440
- for b in boxes_flip
441
- ]
442
- all_boxes = boxes_orig + boxes_flip
443
- if not all_boxes:
444
- return []
445
-
446
- coords = np.array(
447
- [[b.x1, b.y1, b.x2, b.y2] for b in all_boxes], dtype=np.float32
448
- )
449
- scores = np.array([b.conf for b in all_boxes], dtype=np.float32)
450
- cls_ids = np.array([b.cls_id for b in all_boxes], dtype=np.int32)
451
-
452
- hard_keep = self._per_class_hard_nms(coords, scores, cls_ids, self.iou_thres)
453
- if len(hard_keep) == 0:
454
- return []
455
- hard_keep = hard_keep[: self.max_det]
456
- boosted = self._max_score_per_cluster(
457
- coords[hard_keep], coords, scores, self.iou_thres
458
- )
459
-
460
- return [
461
- BoundingBox(
462
- x1=all_boxes[i].x1,
463
- y1=all_boxes[i].y1,
464
- x2=all_boxes[i].x2,
465
- y2=all_boxes[i].y2,
466
- cls_id=all_boxes[i].cls_id,
467
- conf=float(boosted[j]),
468
- )
469
- for j, i in enumerate(hard_keep)
470
- ]
471
-
472
  def predict_batch(self, batch_images: list[ndarray], offset: int,
473
  n_keypoints: int) -> list[TVFrameResult]:
474
  results: list[TVFrameResult] = []
475
  for frame_number_in_batch, image in enumerate(batch_images):
476
  try:
477
- boxes = self._predict_tta(image)
478
  except Exception as e:
479
  print(f"Inference failed for frame {offset + frame_number_in_batch}: {e}")
480
  boxes = []
 
24
 
25
 
26
  class Miner:
27
+ """ONNX Runtime miner. Hard global NMS + sanity filter + per-class conf rescue."""
28
 
29
  class_names = ["fire", "smoke", "fire extinguisher"]
30
  input_size = 1280
31
+ iou_thres = 0.4
32
+ min_side = 12.0
 
33
  min_box_area = 144.0
34
  max_aspect_ratio = 6.0
35
+ max_det = 150
36
+ _conf_thres_array = np.array([0.25, 0.4, 0.2], dtype=np.float32)
37
+ _bonus_array = np.array([0.15, 0.35, 0.15], dtype=np.float32)
38
 
39
  def __init__(self, path_hf_repo: Path) -> None:
40
  model_path = path_hf_repo / "weights.onnx"
 
81
  self.input_width = self._safe_dim(self.input_shape[3], default=self.input_size)
82
 
83
  print(f"ONNX model loaded from: {model_path}")
84
+ print(f"ONNX providers: {self.session.get_providers()}")
85
  print(f"ONNX input: name={self.input_name}, shape={self.input_shape}")
86
  print("per-class conf: " + ", ".join(
87
  f"{n}={t:.3f}" for n, t in zip(self.class_names,
 
174
  order = rest[iou <= iou_thresh]
175
  return np.array(keep, dtype=np.intp)
176
 
177
+ def _filter_sane_boxes_op(self, boxes: np.ndarray, scores: np.ndarray,
178
+ cls_ids: np.ndarray, orig_size: tuple[int, int]
179
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  if len(boxes) == 0:
181
  return boxes, scores, cls_ids
182
  orig_w, orig_h = orig_size
 
197
  )
198
  return boxes[keep], scores[keep], cls_ids[keep]
199
 
200
+ def _conf_filter_mask(self, scores: np.ndarray,
201
+ cls_ids: np.ndarray) -> np.ndarray:
202
+ """Boolean keep-mask: score >= per-class threshold, with a per-class
203
+ rescue — if a class has zero boxes passing, admit its top-1 candidate
204
+ when its score >= (per-class threshold - per-class bonus)."""
205
+ if len(scores) == 0:
206
+ return np.zeros(0, dtype=bool)
207
+ thr = self._conf_thres_array[cls_ids]
208
+ keep = scores >= thr
209
+ for c in np.unique(cls_ids):
210
+ b = float(self._bonus_array[c])
211
+ if b <= 0.0:
212
+ continue
213
+ cm = cls_ids == c
214
+ if keep[cm].any():
215
+ continue
216
+ idx = np.where(cm)[0]
217
+ top = int(idx[int(np.argmax(scores[idx]))])
218
+ if scores[top] >= self._conf_thres_array[c] - b:
219
+ keep[top] = True
220
+ return keep
 
 
221
 
222
  def _per_view_pipeline(self, boxes: np.ndarray, scores: np.ndarray,
223
  cls_ids: np.ndarray, orig_size: tuple[int, int]
224
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
225
+ boxes, scores, cls_ids = self._filter_sane_boxes_op(
226
  boxes, scores, cls_ids, orig_size
227
  )
228
  if len(boxes) == 0:
229
  return boxes, scores, cls_ids
230
  if len(boxes) > 1:
231
+ keep = self._hard_nms(boxes, scores, self.iou_thres)
232
+ boxes, scores, cls_ids = boxes[keep], scores[keep], cls_ids[keep]
233
  if len(scores) > self.max_det:
234
  top = np.argsort(-scores)[: self.max_det]
235
  boxes, scores, cls_ids = boxes[top], scores[top], cls_ids[top]
 
247
  scores = preds[:, 4].astype(np.float32)
248
  cls_ids = preds[:, 5].astype(np.int32)
249
 
250
+ keep = self._conf_filter_mask(scores, cls_ids)
251
  boxes = boxes[keep]
252
  scores = scores[keep]
253
  cls_ids = cls_ids[keep]
 
285
  cls_ids = np.argmax(cls_part, axis=1).astype(np.int32)
286
  scores = cls_part[np.arange(len(cls_part)), cls_ids]
287
 
288
+ keep = self._conf_filter_mask(scores, cls_ids)
289
  boxes_xywh = boxes_xywh[keep]
290
  scores = scores[keep]
291
  cls_ids = cls_ids[keep]
 
355
  outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
356
  return self._postprocess(outputs[0], ratio, pad, orig_size)
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  def predict_batch(self, batch_images: list[ndarray], offset: int,
359
  n_keypoints: int) -> list[TVFrameResult]:
360
  results: list[TVFrameResult] = []
361
  for frame_number_in_batch, image in enumerate(batch_images):
362
  try:
363
+ boxes = self._predict_single(image)
364
  except Exception as e:
365
  print(f"Inference failed for frame {offset + frame_number_in_batch}: {e}")
366
  boxes = []
readme.md CHANGED
@@ -10,9 +10,9 @@ tags:
10
  manako:
11
  source: winner_fetch
12
  manifest_element_name: manak0/Detect-fire
13
- winner_repo_id: navierstocks/fire-light
14
- winner_revision: 95133792375f1fd3e5f192d0494c3b02f770cdc4
15
- note: E=0.03088120 (map50=0.600000, size_mb=19.429295)
16
  ---
17
 
18
  ## YOLO26 ONNX detector
 
10
  manako:
11
  source: winner_fetch
12
  manifest_element_name: manak0/Detect-fire
13
+ winner_repo_id: navierstocks/disaster
14
+ winner_revision: 3a0b049b490e28f3d29f0328c2af1a7799217933
15
+ note: E=0.03088868 (map50=0.600000, size_mb=19.424589)
16
  ---
17
 
18
  ## YOLO26 ONNX detector
weights.onnx CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:40ec65251e308d8240c59ea7704956fc44823e750067e433f287aec71e8939ac
3
  size 19407447
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2cbc3051c706b96f99e0223ca078af3e8fd69d40ee4ca659c6310b6abe2c87a7
3
  size 19407447