meaculpitt commited on
Commit
66e4efc
·
verified ·
1 Parent(s): 8f5b9a3

DetectPerson v5: WBF fix (avg contributing) + conf=0.30

Browse files
Files changed (1) hide show
  1. miner.py +102 -126
miner.py CHANGED
@@ -1,12 +1,7 @@
1
  """
2
- Score Vision SN44 — VehicleDetect miner v6 (2026-03-26).
3
- TTA (3-pass) + inline WBF. Per-class NMS. Letterbox preprocessing.
4
-
5
- Model: YOLO11s ONNX, 4 classes trained as:
6
- 0 = car, 1 = bus, 2 = truck, 3 = motorcycle
7
-
8
- Official submission order (remapped in MODEL_TO_OUT):
9
- 0 = bus, 1 = car, 2 = truck, 3 = motorcycle
10
  """
11
 
12
  from pathlib import Path
@@ -18,12 +13,7 @@ import onnxruntime as ort
18
  from numpy import ndarray
19
  from pydantic import BaseModel
20
 
21
- MODEL_TO_OUT: dict[int, int] = {0: 1, 1: 0, 2: 2, 3: 3}
22
- OUT_NAMES = ["bus", "car", "truck", "motorcycle"]
23
- NUM_CLASSES = 4
24
-
25
- IMG_SIZE = 1280
26
- CONF_THRESH = 0.35
27
  TTA_CONF_THRESH = 0.25
28
  IOU_THRESH = 0.45
29
  WBF_IOU_THR = 0.55
@@ -32,80 +22,68 @@ TTA_SCALE = 1.2
32
 
33
 
34
  def _wbf(boxes_list: list[np.ndarray], scores_list: list[np.ndarray],
35
- labels_list: list[np.ndarray], iou_thr: float = 0.55,
36
- skip_box_thr: float = 0.0001) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
37
- """Weighted Boxes Fusion (inline, no external dep). Boxes in [0,1] normalized coords."""
38
  if not boxes_list:
39
- return np.empty((0, 4)), np.empty(0), np.empty(0)
40
 
41
- # Collect all boxes with model index
42
- all_boxes, all_scores, all_labels = [], [], []
43
- for model_idx, (bx, sc, lb) in enumerate(zip(boxes_list, scores_list, labels_list)):
44
  for i in range(len(bx)):
45
  if sc[i] < skip_box_thr:
46
  continue
47
  all_boxes.append(bx[i])
48
  all_scores.append(sc[i])
49
- all_labels.append(int(lb[i]))
50
 
51
  if not all_boxes:
52
- return np.empty((0, 4)), np.empty(0), np.empty(0)
53
 
54
  all_boxes = np.array(all_boxes)
55
  all_scores = np.array(all_scores)
56
- all_labels = np.array(all_labels, dtype=int)
57
-
58
  n_models = len(boxes_list)
59
- fused_boxes, fused_scores, fused_labels = [], [], []
60
-
61
- for cls in np.unique(all_labels):
62
- cls_mask = all_labels == cls
63
- cls_boxes = all_boxes[cls_mask]
64
- cls_scores = all_scores[cls_mask]
65
-
66
- order = cls_scores.argsort()[::-1]
67
- cls_boxes = cls_boxes[order]
68
- cls_scores = cls_scores[order]
69
-
70
- clusters: list[list[int]] = []
71
- cluster_boxes: list[np.ndarray] = []
72
-
73
- for i in range(len(cls_boxes)):
74
- matched = -1
75
- best_iou = iou_thr
76
- for c_idx, c_box in enumerate(cluster_boxes):
77
- xx1 = max(cls_boxes[i, 0], c_box[0])
78
- yy1 = max(cls_boxes[i, 1], c_box[1])
79
- xx2 = min(cls_boxes[i, 2], c_box[2])
80
- yy2 = min(cls_boxes[i, 3], c_box[3])
81
- inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
82
- a1 = (cls_boxes[i, 2] - cls_boxes[i, 0]) * (cls_boxes[i, 3] - cls_boxes[i, 1])
83
- a2 = (c_box[2] - c_box[0]) * (c_box[3] - c_box[1])
84
- iou = inter / (a1 + a2 - inter + 1e-9)
85
- if iou > best_iou:
86
- best_iou = iou
87
- matched = c_idx
88
- if matched >= 0:
89
- clusters[matched].append(i)
90
- # Update cluster box as weighted average
91
- idxs = clusters[matched]
92
- weights = cls_scores[idxs]
93
- w_sum = weights.sum()
94
- cluster_boxes[matched] = (cls_boxes[idxs] * weights[:, None]).sum(0) / w_sum
95
- else:
96
- clusters.append([i])
97
- cluster_boxes.append(cls_boxes[i].copy())
98
-
99
- for c_idx, idxs in enumerate(clusters):
100
- weights = cls_scores[idxs]
101
- score = weights.mean() # avg of contributing passes, not all n_models
102
- fused_boxes.append(cluster_boxes[c_idx])
103
- fused_scores.append(score)
104
- fused_labels.append(cls)
105
 
106
  if not fused_boxes:
107
- return np.empty((0, 4)), np.empty(0), np.empty(0)
108
- return np.array(fused_boxes), np.array(fused_scores), np.array(fused_labels)
109
 
110
 
111
  class BoundingBox(BaseModel):
@@ -126,113 +104,112 @@ class TVFrameResult(BaseModel):
126
  class Miner:
127
  def __init__(self, path_hf_repo: Path) -> None:
128
  self.path_hf_repo = path_hf_repo
 
129
  self.session = ort.InferenceSession(
130
  str(path_hf_repo / "weights.onnx"),
131
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
132
  )
133
  self.input_name = self.session.get_inputs()[0].name
 
 
 
134
  self.conf_threshold = CONF_THRESH
135
  self.tta_conf_threshold = TTA_CONF_THRESH
136
  self.iou_threshold = IOU_THRESH
137
 
138
  def __repr__(self) -> str:
139
- return f"VehicleDetect Miner v5 TTA+WBF session={type(self.session).__name__}"
140
-
141
- def _letterbox(self, img: ndarray) -> tuple[np.ndarray, float, int, int]:
142
- h, w = img.shape[:2]
143
- r = min(IMG_SIZE / h, IMG_SIZE / w)
144
- new_w, new_h = int(round(w * r)), int(round(h * r))
145
- img_r = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
146
- dw, dh = IMG_SIZE - new_w, IMG_SIZE - new_h
147
- pad_l, pad_t = dw // 2, dh // 2
148
- img_p = cv2.copyMakeBorder(
149
- img_r, pad_t, dh - pad_t, pad_l, dw - pad_l,
150
- cv2.BORDER_CONSTANT, value=(114, 114, 114),
151
- )
152
- return img_p, r, pad_l, pad_t
153
-
154
- def _preprocess(self, image_bgr: ndarray) -> tuple[np.ndarray, float, int, int]:
155
- img_p, ratio, pad_l, pad_t = self._letterbox(image_bgr)
156
- img_rgb = cv2.cvtColor(img_p, cv2.COLOR_BGR2RGB)
157
- inp = img_rgb.astype(np.float32) / 255.0
158
- inp = np.ascontiguousarray(inp.transpose(2, 0, 1)[np.newaxis])
159
- return inp, ratio, pad_l, pad_t
160
-
161
- def _decode_raw(self, raw: np.ndarray, ratio: float, pad_l: int, pad_t: int,
162
- orig_w: int, orig_h: int, conf_thresh: float | None = None
163
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
164
  pred = raw[0]
 
 
165
  if pred.shape[0] < pred.shape[1]:
166
- pred = pred.T
167
- bboxes_cx = pred[:, :4]
 
 
 
168
  cls_scores = pred[:, 4:]
169
- cls_ids = np.argmax(cls_scores, axis=1)
 
 
170
  confs = np.max(cls_scores, axis=1)
171
  thresh = conf_thresh if conf_thresh is not None else self.conf_threshold
172
- mask = confs >= thresh
173
- if not mask.any():
174
- return np.empty((0, 4)), np.empty(0), np.empty(0, dtype=int)
175
- bboxes_cx, confs, cls_ids = bboxes_cx[mask], confs[mask], cls_ids[mask]
176
- cx, cy, bw, bh = bboxes_cx[:, 0], bboxes_cx[:, 1], bboxes_cx[:, 2], bboxes_cx[:, 3]
177
- x1 = np.clip((cx - bw / 2 - pad_l) / ratio, 0, orig_w)
178
- y1 = np.clip((cy - bh / 2 - pad_t) / ratio, 0, orig_h)
179
- x2 = np.clip((cx + bw / 2 - pad_l) / ratio, 0, orig_w)
180
- y2 = np.clip((cy + bh / 2 - pad_t) / ratio, 0, orig_h)
181
- return np.stack([x1, y1, x2, y2], axis=1), confs, cls_ids
 
 
 
182
 
183
  def _run_single_pass(self, image_bgr: ndarray, conf_thresh: float | None = None
184
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
185
  orig_h, orig_w = image_bgr.shape[:2]
186
- inp, ratio, pad_l, pad_t = self._preprocess(image_bgr)
187
  raw = self.session.run(None, {self.input_name: inp})[0]
188
- return self._decode_raw(raw, ratio, pad_l, pad_t, orig_w, orig_h, conf_thresh)
189
 
190
  def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
191
  orig_h, orig_w = image_bgr.shape[:2]
192
 
193
- all_boxes, all_scores, all_labels = [], [], []
194
 
195
- def _collect(boxes, confs, cls_ids):
196
  if len(boxes) == 0:
197
  return
198
- out_cls = np.array([MODEL_TO_OUT[int(c)] for c in cls_ids])
199
  norm = boxes.copy()
200
  norm[:, [0, 2]] /= orig_w
201
  norm[:, [1, 3]] /= orig_h
202
  norm = np.clip(norm, 0, 1)
203
  all_boxes.append(norm)
204
  all_scores.append(confs)
205
- all_labels.append(out_cls)
206
 
207
  # Pass 1: original (low threshold for TTA)
208
  _collect(*self._run_single_pass(image_bgr, self.tta_conf_threshold))
209
 
210
  # Pass 2: horizontal flip
211
  flipped = cv2.flip(image_bgr, 1)
212
- boxes_f, confs_f, cls_f = self._run_single_pass(flipped, self.tta_conf_threshold)
213
  if len(boxes_f):
214
  boxes_f[:, 0], boxes_f[:, 2] = orig_w - boxes_f[:, 2], orig_w - boxes_f[:, 0]
215
- _collect(boxes_f, confs_f, cls_f)
216
 
217
  # Pass 3: 1.2x scale center crop
218
  sh, sw = int(orig_h * TTA_SCALE), int(orig_w * TTA_SCALE)
219
  scaled = cv2.resize(image_bgr, (sw, sh), interpolation=cv2.INTER_LINEAR)
220
  yo, xo = (sh - orig_h) // 2, (sw - orig_w) // 2
221
  cropped = scaled[yo:yo + orig_h, xo:xo + orig_w]
222
- boxes_s, confs_s, cls_s = self._run_single_pass(cropped, self.tta_conf_threshold)
223
  if len(boxes_s):
224
  boxes_s[:, 0] = (boxes_s[:, 0] + xo) / TTA_SCALE
225
  boxes_s[:, 1] = (boxes_s[:, 1] + yo) / TTA_SCALE
226
  boxes_s[:, 2] = (boxes_s[:, 2] + xo) / TTA_SCALE
227
  boxes_s[:, 3] = (boxes_s[:, 3] + yo) / TTA_SCALE
228
  boxes_s = np.clip(boxes_s, 0, [[orig_w, orig_h, orig_w, orig_h]])
229
- _collect(boxes_s, confs_s, cls_s)
230
 
231
  if not all_boxes:
232
  return []
233
 
234
- fused_boxes, fused_scores, fused_labels = _wbf(
235
- all_boxes, all_scores, all_labels,
236
  iou_thr=WBF_IOU_THR, skip_box_thr=WBF_SKIP_THR,
237
  )
238
  if len(fused_boxes) == 0:
@@ -246,7 +223,6 @@ class Miner:
246
  keep = fused_scores >= self.conf_threshold
247
  fused_boxes = fused_boxes[keep]
248
  fused_scores = fused_scores[keep]
249
- fused_labels = fused_labels[keep]
250
 
251
  out: list[BoundingBox] = []
252
  for i in range(len(fused_boxes)):
@@ -256,7 +232,7 @@ class Miner:
256
  y1=max(0, min(orig_h, math.floor(b[1]))),
257
  x2=max(0, min(orig_w, math.ceil(b[2]))),
258
  y2=max(0, min(orig_h, math.ceil(b[3]))),
259
- cls_id=int(fused_labels[i]),
260
  conf=max(0.0, min(1.0, float(fused_scores[i]))),
261
  ))
262
  return out
 
1
  """
2
+ Score Vision SN44 — DetectPerson miner v5 (2026-03-26).
3
+ TTA (3-pass) + inline WBF. Stretch resize preprocessing.
4
+ Single class: person (cls_id=0).
 
 
 
 
 
5
  """
6
 
7
  from pathlib import Path
 
13
  from numpy import ndarray
14
  from pydantic import BaseModel
15
 
16
+ CONF_THRESH = 0.30
 
 
 
 
 
17
  TTA_CONF_THRESH = 0.25
18
  IOU_THRESH = 0.45
19
  WBF_IOU_THR = 0.55
 
22
 
23
 
24
  def _wbf(boxes_list: list[np.ndarray], scores_list: list[np.ndarray],
25
+ iou_thr: float = 0.55, skip_box_thr: float = 0.0001
26
+ ) -> tuple[np.ndarray, np.ndarray]:
27
+ """Weighted Boxes Fusion for single-class detection. Boxes in [0,1] normalized coords."""
28
  if not boxes_list:
29
+ return np.empty((0, 4)), np.empty(0)
30
 
31
+ all_boxes, all_scores = [], []
32
+ for bx, sc in zip(boxes_list, scores_list):
 
33
  for i in range(len(bx)):
34
  if sc[i] < skip_box_thr:
35
  continue
36
  all_boxes.append(bx[i])
37
  all_scores.append(sc[i])
 
38
 
39
  if not all_boxes:
40
+ return np.empty((0, 4)), np.empty(0)
41
 
42
  all_boxes = np.array(all_boxes)
43
  all_scores = np.array(all_scores)
 
 
44
  n_models = len(boxes_list)
45
+
46
+ order = all_scores.argsort()[::-1]
47
+ all_boxes = all_boxes[order]
48
+ all_scores = all_scores[order]
49
+
50
+ clusters: list[list[int]] = []
51
+ cluster_boxes: list[np.ndarray] = []
52
+
53
+ for i in range(len(all_boxes)):
54
+ matched = -1
55
+ best_iou = iou_thr
56
+ for c_idx, c_box in enumerate(cluster_boxes):
57
+ xx1 = max(all_boxes[i, 0], c_box[0])
58
+ yy1 = max(all_boxes[i, 1], c_box[1])
59
+ xx2 = min(all_boxes[i, 2], c_box[2])
60
+ yy2 = min(all_boxes[i, 3], c_box[3])
61
+ inter = max(0, xx2 - xx1) * max(0, yy2 - yy1)
62
+ a1 = (all_boxes[i, 2] - all_boxes[i, 0]) * (all_boxes[i, 3] - all_boxes[i, 1])
63
+ a2 = (c_box[2] - c_box[0]) * (c_box[3] - c_box[1])
64
+ iou = inter / (a1 + a2 - inter + 1e-9)
65
+ if iou > best_iou:
66
+ best_iou = iou
67
+ matched = c_idx
68
+ if matched >= 0:
69
+ clusters[matched].append(i)
70
+ idxs = clusters[matched]
71
+ weights = all_scores[idxs]
72
+ w_sum = weights.sum()
73
+ cluster_boxes[matched] = (all_boxes[idxs] * weights[:, None]).sum(0) / w_sum
74
+ else:
75
+ clusters.append([i])
76
+ cluster_boxes.append(all_boxes[i].copy())
77
+
78
+ fused_boxes, fused_scores = [], []
79
+ for c_idx, idxs in enumerate(clusters):
80
+ weights = all_scores[idxs]
81
+ fused_boxes.append(cluster_boxes[c_idx])
82
+ fused_scores.append(weights.mean()) # avg of contributing passes
 
 
 
 
 
 
 
 
83
 
84
  if not fused_boxes:
85
+ return np.empty((0, 4)), np.empty(0)
86
+ return np.array(fused_boxes), np.array(fused_scores)
87
 
88
 
89
  class BoundingBox(BaseModel):
 
104
  class Miner:
105
  def __init__(self, path_hf_repo: Path) -> None:
106
  self.path_hf_repo = path_hf_repo
107
+ self.class_names = ['person']
108
  self.session = ort.InferenceSession(
109
  str(path_hf_repo / "weights.onnx"),
110
  providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
111
  )
112
  self.input_name = self.session.get_inputs()[0].name
113
+ input_shape = self.session.get_inputs()[0].shape
114
+ self.input_h = int(input_shape[2])
115
+ self.input_w = int(input_shape[3])
116
  self.conf_threshold = CONF_THRESH
117
  self.tta_conf_threshold = TTA_CONF_THRESH
118
  self.iou_threshold = IOU_THRESH
119
 
120
  def __repr__(self) -> str:
121
+ return f"DetectPerson Miner v4 TTA+WBF session={type(self.session).__name__}"
122
+
123
+ def _preprocess(self, image_bgr: ndarray) -> tuple[np.ndarray, tuple[int, int]]:
124
+ h, w = image_bgr.shape[:2]
125
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
126
+ resized = cv2.resize(rgb, (self.input_w, self.input_h))
127
+ x = resized.astype(np.float32) / 255.0
128
+ x = np.transpose(x, (2, 0, 1))[None, ...]
129
+ return x, (h, w)
130
+
131
+ def _decode_raw(self, raw: np.ndarray, orig_h: int, orig_w: int,
132
+ conf_thresh: float | None = None) -> tuple[np.ndarray, np.ndarray]:
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  pred = raw[0]
134
+ if pred.ndim != 2:
135
+ return np.empty((0, 4)), np.empty(0)
136
  if pred.shape[0] < pred.shape[1]:
137
+ pred = pred.transpose(1, 0)
138
+ if pred.shape[1] < 5:
139
+ return np.empty((0, 4)), np.empty(0)
140
+
141
+ boxes = pred[:, :4]
142
  cls_scores = pred[:, 4:]
143
+ if cls_scores.shape[1] == 0:
144
+ return np.empty((0, 4)), np.empty(0)
145
+
146
  confs = np.max(cls_scores, axis=1)
147
  thresh = conf_thresh if conf_thresh is not None else self.conf_threshold
148
+ keep = confs >= thresh
149
+ boxes, confs = boxes[keep], confs[keep]
150
+ if boxes.shape[0] == 0:
151
+ return np.empty((0, 4)), np.empty(0)
152
+
153
+ sx = orig_w / float(self.input_w)
154
+ sy = orig_h / float(self.input_h)
155
+ cx, cy, bw, bh = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
156
+ x1 = np.clip((cx - bw / 2) * sx, 0, orig_w)
157
+ y1 = np.clip((cy - bh / 2) * sy, 0, orig_h)
158
+ x2 = np.clip((cx + bw / 2) * sx, 0, orig_w)
159
+ y2 = np.clip((cy + bh / 2) * sy, 0, orig_h)
160
+ return np.stack([x1, y1, x2, y2], axis=1), confs
161
 
162
  def _run_single_pass(self, image_bgr: ndarray, conf_thresh: float | None = None
163
+ ) -> tuple[np.ndarray, np.ndarray]:
164
  orig_h, orig_w = image_bgr.shape[:2]
165
+ inp, _ = self._preprocess(image_bgr)
166
  raw = self.session.run(None, {self.input_name: inp})[0]
167
+ return self._decode_raw(raw, orig_h, orig_w, conf_thresh)
168
 
169
  def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
170
  orig_h, orig_w = image_bgr.shape[:2]
171
 
172
+ all_boxes, all_scores = [], []
173
 
174
+ def _collect(boxes, confs):
175
  if len(boxes) == 0:
176
  return
 
177
  norm = boxes.copy()
178
  norm[:, [0, 2]] /= orig_w
179
  norm[:, [1, 3]] /= orig_h
180
  norm = np.clip(norm, 0, 1)
181
  all_boxes.append(norm)
182
  all_scores.append(confs)
 
183
 
184
  # Pass 1: original (low threshold for TTA)
185
  _collect(*self._run_single_pass(image_bgr, self.tta_conf_threshold))
186
 
187
  # Pass 2: horizontal flip
188
  flipped = cv2.flip(image_bgr, 1)
189
+ boxes_f, confs_f = self._run_single_pass(flipped, self.tta_conf_threshold)
190
  if len(boxes_f):
191
  boxes_f[:, 0], boxes_f[:, 2] = orig_w - boxes_f[:, 2], orig_w - boxes_f[:, 0]
192
+ _collect(boxes_f, confs_f)
193
 
194
  # Pass 3: 1.2x scale center crop
195
  sh, sw = int(orig_h * TTA_SCALE), int(orig_w * TTA_SCALE)
196
  scaled = cv2.resize(image_bgr, (sw, sh), interpolation=cv2.INTER_LINEAR)
197
  yo, xo = (sh - orig_h) // 2, (sw - orig_w) // 2
198
  cropped = scaled[yo:yo + orig_h, xo:xo + orig_w]
199
+ boxes_s, confs_s = self._run_single_pass(cropped, self.tta_conf_threshold)
200
  if len(boxes_s):
201
  boxes_s[:, 0] = (boxes_s[:, 0] + xo) / TTA_SCALE
202
  boxes_s[:, 1] = (boxes_s[:, 1] + yo) / TTA_SCALE
203
  boxes_s[:, 2] = (boxes_s[:, 2] + xo) / TTA_SCALE
204
  boxes_s[:, 3] = (boxes_s[:, 3] + yo) / TTA_SCALE
205
  boxes_s = np.clip(boxes_s, 0, [[orig_w, orig_h, orig_w, orig_h]])
206
+ _collect(boxes_s, confs_s)
207
 
208
  if not all_boxes:
209
  return []
210
 
211
+ fused_boxes, fused_scores = _wbf(
212
+ all_boxes, all_scores,
213
  iou_thr=WBF_IOU_THR, skip_box_thr=WBF_SKIP_THR,
214
  )
215
  if len(fused_boxes) == 0:
 
223
  keep = fused_scores >= self.conf_threshold
224
  fused_boxes = fused_boxes[keep]
225
  fused_scores = fused_scores[keep]
 
226
 
227
  out: list[BoundingBox] = []
228
  for i in range(len(fused_boxes)):
 
232
  y1=max(0, min(orig_h, math.floor(b[1]))),
233
  x2=max(0, min(orig_w, math.ceil(b[2]))),
234
  y2=max(0, min(orig_h, math.ceil(b[3]))),
235
+ cls_id=0,
236
  conf=max(0.0, min(1.0, float(fused_scores[i]))),
237
  ))
238
  return out