meaculpitt commited on
Commit
94efd3a
·
verified ·
1 Parent(s): 2020e13

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. miner.py +154 -198
miner.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Score Vision SN44 — Unified miner v3.24 (2026-04-07). Petrol v4 weights (960px, GDINO+Mapillary), petrol TTA consensus, hard geometric filter, provider logging. Vehicle: lazy FP32, 0-box trigger. Person: TTA consensus.
3
  Tri-model: vehicle (YOLO11m INT8 1280) + person (YOLO12s FP16 960 TRT) + petrol (end2end 640).
4
  Pose model: YOLOv8n-pose FP16 640 for false-positive filtering + keypoint box refinement.
5
  Vehicle weights loaded from secondary HF repo (meaculpitt/ScoreVision-Vehicle).
@@ -378,27 +378,18 @@ VEHICLE_HF_REPO = "meaculpitt/ScoreVision-Vehicle"
378
 
379
  # ── Petrol config ───────────────────────────────────────────────────────────
380
  PETROL_HF_REPO = "meaculpitt/ScoreVision-Petrol"
381
- PETROL_CONF = 0.30
382
- PETROL_CONF_ORIG = 0.35
383
- PETROL_TTA_MATCH_IOU = 0.50
384
- PETROL_IOU = 0.45
 
 
385
  # Class IDs (petrol model output — independent of person/vehicle cls_ids
386
  # because element_hint routing ensures only one pipeline runs per challenge)
387
  PETROL_CLS_HOSE = 0
388
  PETROL_CLS_PUMP = 1
389
  PETROL_CLS_PRICEBOARD = 2
390
  PETROL_CLS_CANOPY = 3
391
- # Geometric validation thresholds
392
- PETROL_CANOPY_MIN_ASPECT = 0.8
393
- PETROL_PUMP_MAX_ASPECT = 4.0
394
- PETROL_PRICEBOARD_MAX_AREA_FRAC = 0.15
395
- PETROL_HOSE_MIN_AREA_FRAC = 0.0005
396
- # Spatial co-occurrence
397
- PETROL_COOCCUR_PUMP_CANOPY = 0.05
398
- PETROL_COOCCUR_PUMP_HOSE = 0.08
399
- PETROL_COOCCUR_CANOPY_HOSE = 0.05
400
- PETROL_COOCCUR_SUPPRESS = 0.03
401
- PETROL_COOCCUR_PROXIMITY = 0.5
402
 
403
 
404
  def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
@@ -2166,229 +2157,194 @@ class Miner:
2166
 
2167
  return vehicle_boxes + person_boxes
2168
 
2169
- # ── Petrol inference pipeline ───────────────────────────────────────────
2170
 
2171
- def _petrol_preprocess(self, image_bgr: ndarray):
2172
- """Resize to model input, normalize to [0,1] float32 NCHW."""
2173
  h, w = image_bgr.shape[:2]
2174
- rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
2175
- resized = cv2.resize(rgb, (self.petrol_w, self.petrol_h))
2176
- x = resized.astype(np.float32) / 255.0
2177
- x = np.transpose(x, (2, 0, 1))[None, ...]
2178
- return x, (h, w)
2179
-
2180
- def _petrol_decode_end2end(self, out, orig_h, orig_w):
2181
- """Decode end-to-end [1, N, 6] output: [x1,y1,x2,y2,conf,cls_id]."""
2182
- pred = out[0]
2183
- if pred.ndim != 2 or pred.shape[1] != 6:
2184
- return []
2185
- confs = pred[:, 4]
2186
- keep = confs >= PETROL_CONF
2187
- pred = pred[keep]
2188
- if pred.shape[0] == 0:
2189
- return []
2190
- sx = orig_w / float(self.petrol_w)
2191
- sy = orig_h / float(self.petrol_h)
2192
- results = []
2193
- for i in range(pred.shape[0]):
2194
- results.append((
2195
- pred[i, 0] * sx, pred[i, 1] * sy,
2196
- pred[i, 2] * sx, pred[i, 3] * sy,
2197
- float(pred[i, 4]), int(pred[i, 5]),
2198
- ))
2199
- return results
2200
-
2201
- def _petrol_decode_raw(self, out, orig_h, orig_w):
2202
- """Decode raw [1, 4+nc, N] output with NMS."""
2203
  pred = out[0]
2204
  if pred.ndim != 2:
2205
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2206
  if pred.shape[0] < pred.shape[1]:
2207
  pred = pred.T
2208
  if pred.shape[1] < 5:
2209
  return []
2210
  boxes = pred[:, :4]
2211
  cls_scores = pred[:, 4:]
2212
- if cls_scores.shape[1] == 0:
2213
- return []
2214
  cls_ids = np.argmax(cls_scores, axis=1)
2215
  confs = np.max(cls_scores, axis=1)
2216
  keep = confs >= PETROL_CONF
2217
  boxes, confs, cls_ids = boxes[keep], confs[keep], cls_ids[keep]
2218
- if boxes.shape[0] == 0:
2219
- return []
2220
- sx = orig_w / float(self.petrol_w)
2221
- sy = orig_h / float(self.petrol_h)
2222
- dets = []
2223
  for i in range(boxes.shape[0]):
2224
  cx, cy, bw, bh = boxes[i].tolist()
2225
- dets.append((
2226
- (cx - bw / 2.0) * sx, (cy - bh / 2.0) * sy,
2227
- (cx + bw / 2.0) * sx, (cy + bh / 2.0) * sy,
2228
- float(confs[i]), int(cls_ids[i]),
2229
- ))
2230
- # Simple NMS
2231
- if not dets:
2232
- return dets
2233
- arr_b = np.array([[d[0], d[1], d[2], d[3]] for d in dets], dtype=np.float32)
2234
- arr_s = np.array([d[4] for d in dets], dtype=np.float32)
2235
- order = arr_s.argsort()[::-1]
2236
- kept = []
2237
- while order.size > 0:
2238
- i = order[0]
2239
- kept.append(i)
2240
- xx1 = np.maximum(arr_b[i, 0], arr_b[order[1:], 0])
2241
- yy1 = np.maximum(arr_b[i, 1], arr_b[order[1:], 1])
2242
- xx2 = np.minimum(arr_b[i, 2], arr_b[order[1:], 2])
2243
- yy2 = np.minimum(arr_b[i, 3], arr_b[order[1:], 3])
2244
- inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
2245
- area_i = (arr_b[i, 2] - arr_b[i, 0]) * (arr_b[i, 3] - arr_b[i, 1])
2246
- area_r = (arr_b[order[1:], 2] - arr_b[order[1:], 0]) * (arr_b[order[1:], 3] - arr_b[order[1:], 1])
2247
- iou = inter / np.maximum(area_i + area_r - inter, 1e-6)
2248
- order = order[np.where(iou <= PETROL_IOU)[0] + 1]
2249
- return [dets[idx] for idx in kept]
2250
-
2251
- def _petrol_geometric_validate(self, dets, orig_h, orig_w):
2252
- """Per-class hard shape filters. Reject detections that fail."""
2253
- img_area = max(orig_h * orig_w, 1)
2254
- result = []
2255
- for x1, y1, x2, y2, conf, cls_id in dets:
2256
- bw = max(x2 - x1, 1)
2257
- bh = max(y2 - y1, 1)
2258
- aspect = bw / bh
2259
- area_frac = (bw * bh) / img_area
2260
- if cls_id == PETROL_CLS_CANOPY and aspect < PETROL_CANOPY_MIN_ASPECT:
2261
- continue
2262
- if cls_id == PETROL_CLS_PUMP and (aspect > PETROL_PUMP_MAX_ASPECT or bh < 30):
2263
- continue
2264
- if cls_id == PETROL_CLS_PRICEBOARD and (area_frac > PETROL_PRICEBOARD_MAX_AREA_FRAC or (bw * bh) < 200):
2265
- continue
2266
- if cls_id == PETROL_CLS_HOSE and (bw * bh) < 400:
2267
- continue
2268
- result.append((x1, y1, x2, y2, conf, cls_id))
2269
- return result
2270
 
2271
- def _petrol_spatial_cooccurrence(self, dets, orig_h, orig_w):
2272
- """Proximity-based confidence adjustments for petrol objects."""
 
2273
  if not dets:
2274
- return dets
2275
- n = len(dets)
2276
- adjustments = [0.0] * n
2277
- diag = math.sqrt(orig_h ** 2 + orig_w ** 2)
2278
- prox = PETROL_COOCCUR_PROXIMITY * diag
2279
-
2280
- centers = [((x1 + x2) / 2, (y1 + y2) / 2) for x1, y1, x2, y2, _, _ in dets]
2281
- cls_map = {}
2282
- for i, (_, _, _, _, _, cls_id) in enumerate(dets):
2283
- cls_map.setdefault(cls_id, []).append(i)
2284
-
2285
- def near(i, j):
2286
- dx = centers[i][0] - centers[j][0]
2287
- dy = centers[i][1] - centers[j][1]
2288
- return math.sqrt(dx * dx + dy * dy) < prox
2289
-
2290
- # Pump + Canopy boost
2291
- for pi in cls_map.get(PETROL_CLS_PUMP, []):
2292
- for ci in cls_map.get(PETROL_CLS_CANOPY, []):
2293
- if near(pi, ci):
2294
- adjustments[pi] = max(adjustments[pi], PETROL_COOCCUR_PUMP_CANOPY)
2295
- adjustments[ci] = max(adjustments[ci], PETROL_COOCCUR_PUMP_CANOPY)
2296
- # Pump + Hose boost
2297
- for pi in cls_map.get(PETROL_CLS_PUMP, []):
2298
- for hi in cls_map.get(PETROL_CLS_HOSE, []):
2299
- if near(pi, hi):
2300
- adjustments[hi] = max(adjustments[hi], PETROL_COOCCUR_PUMP_HOSE)
2301
- # Canopy + Hose boost
2302
- for ci in cls_map.get(PETROL_CLS_CANOPY, []):
2303
- for hi in cls_map.get(PETROL_CLS_HOSE, []):
2304
- if near(ci, hi):
2305
- adjustments[hi] = max(adjustments[hi], PETROL_COOCCUR_CANOPY_HOSE)
2306
- # Suppress isolated low-conf (not price boards)
2307
- for i, (_, _, _, _, conf, cls_id) in enumerate(dets):
2308
- if cls_id == PETROL_CLS_PRICEBOARD or conf > 0.60:
2309
- continue
2310
- if not any(near(i, j) for j in range(n) if j != i):
2311
- adjustments[i] = min(adjustments[i], adjustments[i] - PETROL_COOCCUR_SUPPRESS)
2312
-
2313
  result = []
2314
- for i, (x1, y1, x2, y2, conf, cls_id) in enumerate(dets):
2315
- new_conf = min(1.0, max(0.0, conf + adjustments[i]))
2316
- if new_conf >= PETROL_CONF:
2317
- result.append((x1, y1, x2, y2, new_conf, cls_id))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2318
  return result
2319
 
2320
- def _petrol_run_pass(self, image_bgr):
2321
- """Single petrol inference pass: preprocess -> forward -> decode."""
2322
- inp, (orig_h, orig_w) = self._petrol_preprocess(image_bgr)
2323
- out = self.petrol_session.run(None, {self.petrol_input_name: inp})[0]
2324
- if self._petrol_end2end:
2325
- return self._petrol_decode_end2end(out, orig_h, orig_w), orig_h, orig_w
2326
- else:
2327
- return self._petrol_decode_raw(out, orig_h, orig_w), orig_h, orig_w
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2328
 
2329
  def _infer_petrol(self, image_bgr: ndarray) -> list[BoundingBox]:
2330
- """Petrol inference with flip TTA consensus merging."""
2331
  if not hasattr(self, '_petrol_providers_logged'):
2332
  provs = self.petrol_session.get_providers()
2333
  logger.warning(f"[petrol] First inference — active providers: {provs}")
2334
  self._petrol_providers_logged = True
2335
 
2336
  oh, ow = image_bgr.shape[:2]
 
2337
 
2338
- # Original pass
2339
- dets_orig, _, _ = self._petrol_run_pass(image_bgr)
2340
 
2341
- # Flip pass
2342
  flipped = cv2.flip(image_bgr, 1)
2343
- dets_flip, _, _ = self._petrol_run_pass(flipped)
2344
- # Mirror flip boxes back
2345
- dets_flip = [(ow - x2, y1, ow - x1, y2, conf, cls_id)
2346
- for x1, y1, x2, y2, conf, cls_id in dets_flip]
2347
-
2348
- # Consensus merge: match across views
2349
- matched_orig = set()
2350
- matched_flip = set()
2351
- merged = []
2352
-
2353
- for i, (ox1, oy1, ox2, oy2, oc, ocls) in enumerate(dets_orig):
2354
- for j, (fx1, fy1, fx2, fy2, fc, fcls) in enumerate(dets_flip):
2355
- if ocls != fcls or j in matched_flip:
 
 
 
 
 
 
 
 
 
 
 
 
 
2356
  continue
2357
- ix1, iy1 = max(ox1, fx1), max(oy1, fy1)
2358
- ix2, iy2 = min(ox2, fx2), min(oy2, fy2)
2359
- inter = max(0, ix2 - ix1) * max(0, iy2 - iy1)
2360
- oa = (ox2 - ox1) * (oy2 - oy1)
2361
- fa = (fx2 - fx1) * (fy2 - fy1)
2362
- iou = inter / max(oa + fa - inter, 1e-6)
2363
- if iou >= PETROL_TTA_MATCH_IOU:
2364
- conf = max(oc, fc)
2365
- if conf >= PETROL_CONF:
2366
- merged.append((ox1, oy1, ox2, oy2, conf, ocls))
2367
- matched_orig.add(i)
2368
- matched_flip.add(j)
2369
- break
2370
-
2371
- # Original-only: keep at stricter threshold
2372
- for i, det in enumerate(dets_orig):
2373
- if i not in matched_orig and det[4] >= PETROL_CONF_ORIG:
2374
- merged.append(det)
2375
-
2376
- # Flip-only: discard
2377
-
2378
- if not merged:
2379
- return []
2380
-
2381
- merged = self._petrol_geometric_validate(merged, oh, ow)
2382
- merged = self._petrol_spatial_cooccurrence(merged, oh, ow)
2383
 
2384
  out_boxes = []
2385
- for x1, y1, x2, y2, conf, cls_id in merged:
2386
  out_boxes.append(BoundingBox(
2387
  x1=max(0, min(ow, math.floor(x1))),
2388
  y1=max(0, min(oh, math.floor(y1))),
2389
  x2=max(0, min(ow, math.ceil(x2))),
2390
  y2=max(0, min(oh, math.ceil(y2))),
2391
- cls_id=cls_id,
2392
  conf=max(0.0, min(1.0, conf)),
2393
  ))
2394
  return out_boxes
 
1
  """
2
+ Score Vision SN44 — Unified miner v3.25 (2026-04-07). Petrol SAHI tiling (512px/50% overlap + full-image + flip TTA, per-class NMS merge). Per-class conf: hose/pump 0.25, pboard/canopy 0.35. Vehicle: lazy FP32, 0-box trigger. Person: TTA consensus.
3
  Tri-model: vehicle (YOLO11m INT8 1280) + person (YOLO12s FP16 960 TRT) + petrol (end2end 640).
4
  Pose model: YOLOv8n-pose FP16 640 for false-positive filtering + keypoint box refinement.
5
  Vehicle weights loaded from secondary HF repo (meaculpitt/ScoreVision-Vehicle).
 
378
 
379
  # ── Petrol config ───────────────────────────────────────────────────────────
380
  PETROL_HF_REPO = "meaculpitt/ScoreVision-Petrol"
381
+ PETROL_CONF = 0.25 # base conf for hose + pump
382
+ PETROL_CONF_HIGH = 0.35 # stricter conf for price_board + canopy (reduce tile FPs)
383
+ PETROL_NMS_IOU = 0.45
384
+ # SAHI tiling
385
+ PETROL_TILE_SIZE = 512
386
+ PETROL_TILE_OVERLAP = 0.5
387
  # Class IDs (petrol model output — independent of person/vehicle cls_ids
388
  # because element_hint routing ensures only one pipeline runs per challenge)
389
  PETROL_CLS_HOSE = 0
390
  PETROL_CLS_PUMP = 1
391
  PETROL_CLS_PRICEBOARD = 2
392
  PETROL_CLS_CANOPY = 3
 
 
 
 
 
 
 
 
 
 
 
393
 
394
 
395
  def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
 
2157
 
2158
  return vehicle_boxes + person_boxes
2159
 
2160
+ # ── Petrol inference pipeline (SAHI tiling) ──────────────────────────────
2161
 
2162
+ def _petrol_letterbox(self, image_bgr):
2163
+ """Letterbox resize to model input, return blob + undo params."""
2164
  h, w = image_bgr.shape[:2]
2165
+ scale = min(self.petrol_h / h, self.petrol_w / w)
2166
+ nh, nw = int(h * scale), int(w * scale)
2167
+ resized = cv2.resize(image_bgr, (nw, nh))
2168
+ canvas = np.full((self.petrol_h, self.petrol_w, 3), 114, dtype=np.uint8)
2169
+ dy, dx = (self.petrol_h - nh) // 2, (self.petrol_w - nw) // 2
2170
+ canvas[dy:dy + nh, dx:dx + nw] = resized
2171
+ rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
2172
+ blob = rgb.astype(np.float32) / 255.0
2173
+ blob = np.transpose(blob, (2, 0, 1))[None, ...]
2174
+ return blob, scale, dx, dy
2175
+
2176
+ def _petrol_decode(self, out, orig_h, orig_w, scale, dx, dy):
2177
+ """Decode end2end [1,N,6] or raw [1,C,N] output with letterbox undo."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2178
  pred = out[0]
2179
  if pred.ndim != 2:
2180
  return []
2181
+ # End-to-end: [N, 6] = x1,y1,x2,y2,conf,cls
2182
+ if pred.shape[1] == 6:
2183
+ results = []
2184
+ for i in range(pred.shape[0]):
2185
+ conf = float(pred[i, 4])
2186
+ if conf < PETROL_CONF:
2187
+ continue
2188
+ x1 = (float(pred[i, 0]) - dx) / scale
2189
+ y1 = (float(pred[i, 1]) - dy) / scale
2190
+ x2 = (float(pred[i, 2]) - dx) / scale
2191
+ y2 = (float(pred[i, 3]) - dy) / scale
2192
+ cls_id = int(pred[i, 5])
2193
+ x1, y1 = max(0.0, x1), max(0.0, y1)
2194
+ x2, y2 = min(float(orig_w), x2), min(float(orig_h), y2)
2195
+ if x2 > x1 + 2 and y2 > y1 + 2:
2196
+ results.append([x1, y1, x2, y2, conf, cls_id])
2197
+ return results
2198
+ # Raw: [C, N] → transpose to [N, C], decode cx,cy,w,h + class scores
2199
  if pred.shape[0] < pred.shape[1]:
2200
  pred = pred.T
2201
  if pred.shape[1] < 5:
2202
  return []
2203
  boxes = pred[:, :4]
2204
  cls_scores = pred[:, 4:]
 
 
2205
  cls_ids = np.argmax(cls_scores, axis=1)
2206
  confs = np.max(cls_scores, axis=1)
2207
  keep = confs >= PETROL_CONF
2208
  boxes, confs, cls_ids = boxes[keep], confs[keep], cls_ids[keep]
2209
+ results = []
 
 
 
 
2210
  for i in range(boxes.shape[0]):
2211
  cx, cy, bw, bh = boxes[i].tolist()
2212
+ x1 = ((cx - bw / 2.0) - dx) / scale
2213
+ y1 = ((cy - bh / 2.0) - dy) / scale
2214
+ x2 = ((cx + bw / 2.0) - dx) / scale
2215
+ y2 = ((cy + bh / 2.0) - dy) / scale
2216
+ x1, y1 = max(0.0, x1), max(0.0, y1)
2217
+ x2, y2 = min(float(orig_w), x2), min(float(orig_h), y2)
2218
+ if x2 > x1 + 2 and y2 > y1 + 2:
2219
+ results.append([x1, y1, x2, y2, float(confs[i]), int(cls_ids[i])])
2220
+ return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2221
 
2222
+ @staticmethod
2223
+ def _petrol_nms(dets, iou_thresh=PETROL_NMS_IOU):
2224
+ """Per-class NMS on list of [x1,y1,x2,y2,conf,cls_id]."""
2225
  if not dets:
2226
+ return []
2227
+ by_cls = {}
2228
+ for d in dets:
2229
+ by_cls.setdefault(int(d[5]), []).append(d)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2230
  result = []
2231
+ for cls_dets in by_cls.values():
2232
+ arr = np.array(cls_dets, dtype=np.float32)
2233
+ boxes, scores = arr[:, :4], arr[:, 4]
2234
+ order = scores.argsort()[::-1]
2235
+ kept = []
2236
+ while order.size > 0:
2237
+ i = order[0]
2238
+ kept.append(i)
2239
+ if order.size == 1:
2240
+ break
2241
+ xx1 = np.maximum(boxes[i, 0], boxes[order[1:], 0])
2242
+ yy1 = np.maximum(boxes[i, 1], boxes[order[1:], 1])
2243
+ xx2 = np.minimum(boxes[i, 2], boxes[order[1:], 2])
2244
+ yy2 = np.minimum(boxes[i, 3], boxes[order[1:], 3])
2245
+ inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
2246
+ area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1])
2247
+ area_r = (boxes[order[1:], 2] - boxes[order[1:], 0]) * (boxes[order[1:], 3] - boxes[order[1:], 1])
2248
+ iou = inter / np.maximum(area_i + area_r - inter, 1e-6)
2249
+ order = order[np.where(iou <= iou_thresh)[0] + 1]
2250
+ for k in kept:
2251
+ result.append(cls_dets[k])
2252
  return result
2253
 
2254
+ def _petrol_run_on_crop(self, crop_bgr):
2255
+ """Run petrol model on a single crop, return dets in crop coords."""
2256
+ ch, cw = crop_bgr.shape[:2]
2257
+ blob, scale, dx, dy = self._petrol_letterbox(crop_bgr)
2258
+ out = self.petrol_session.run(None, {self.petrol_input_name: blob})[0]
2259
+ return self._petrol_decode(out, ch, cw, scale, dx, dy)
2260
+
2261
+ @staticmethod
2262
+ def _petrol_tiles(img_h, img_w, tile_size, overlap):
2263
+ """Generate tile coordinates (y1, x1, y2, x2)."""
2264
+ stride = max(1, int(tile_size * (1.0 - overlap)))
2265
+ tiles = []
2266
+ y = 0
2267
+ while True:
2268
+ x = 0
2269
+ while True:
2270
+ y1, x1 = y, x
2271
+ y2 = min(y + tile_size, img_h)
2272
+ x2 = min(x + tile_size, img_w)
2273
+ # Snap small trailing tiles to image edge
2274
+ if y2 - y1 < tile_size * 0.4:
2275
+ y1 = max(0, img_h - tile_size)
2276
+ y2 = img_h
2277
+ if x2 - x1 < tile_size * 0.4:
2278
+ x1 = max(0, img_w - tile_size)
2279
+ x2 = img_w
2280
+ tiles.append((y1, x1, y2, x2))
2281
+ x += stride
2282
+ if x >= img_w:
2283
+ break
2284
+ y += stride
2285
+ if y >= img_h:
2286
+ break
2287
+ # Deduplicate
2288
+ seen = set()
2289
+ unique = []
2290
+ for t in tiles:
2291
+ if t not in seen:
2292
+ seen.add(t)
2293
+ unique.append(t)
2294
+ return unique
2295
 
2296
  def _infer_petrol(self, image_bgr: ndarray) -> list[BoundingBox]:
2297
+ """Petrol inference with SAHI tiling + flip TTA, merged via NMS."""
2298
  if not hasattr(self, '_petrol_providers_logged'):
2299
  provs = self.petrol_session.get_providers()
2300
  logger.warning(f"[petrol] First inference — active providers: {provs}")
2301
  self._petrol_providers_logged = True
2302
 
2303
  oh, ow = image_bgr.shape[:2]
2304
+ all_dets = []
2305
 
2306
+ # 1. Full-image pass (letterbox)
2307
+ all_dets.extend(self._petrol_run_on_crop(image_bgr))
2308
 
2309
+ # 2. Full-image flip TTA
2310
  flipped = cv2.flip(image_bgr, 1)
2311
+ flip_dets = self._petrol_run_on_crop(flipped)
2312
+ for d in flip_dets:
2313
+ d[0], d[2] = ow - d[2], ow - d[0] # mirror x coords
2314
+ all_dets.extend(flip_dets)
2315
+
2316
+ # 3. SAHI tile passes
2317
+ tiles = self._petrol_tiles(oh, ow, PETROL_TILE_SIZE, PETROL_TILE_OVERLAP)
2318
+ for ty1, tx1, ty2, tx2 in tiles:
2319
+ crop = image_bgr[ty1:ty2, tx1:tx2]
2320
+ tile_dets = self._petrol_run_on_crop(crop)
2321
+ for d in tile_dets:
2322
+ d[0] += tx1; d[1] += ty1 # offset to full-image coords
2323
+ d[2] += tx1; d[3] += ty1
2324
+ all_dets.extend(tile_dets)
2325
+
2326
+ # 4. Per-class NMS merge
2327
+ merged = self._petrol_nms(all_dets)
2328
+
2329
+ # 5. Apply per-class confidence thresholds
2330
+ # Hose + pump: PETROL_CONF (0.25) — already filtered in decode
2331
+ # Price board + canopy: PETROL_CONF_HIGH (0.35) — stricter to reduce tile FPs
2332
+ filtered = []
2333
+ for d in merged:
2334
+ cls_id = int(d[5])
2335
+ if cls_id in (PETROL_CLS_PRICEBOARD, PETROL_CLS_CANOPY):
2336
+ if d[4] < PETROL_CONF_HIGH:
2337
  continue
2338
+ filtered.append(d)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2339
 
2340
  out_boxes = []
2341
+ for x1, y1, x2, y2, conf, cls_id in filtered:
2342
  out_boxes.append(BoundingBox(
2343
  x1=max(0, min(ow, math.floor(x1))),
2344
  y1=max(0, min(oh, math.floor(y1))),
2345
  x2=max(0, min(ow, math.ceil(x2))),
2346
  y2=max(0, min(oh, math.ceil(y2))),
2347
+ cls_id=int(cls_id),
2348
  conf=max(0.0, min(1.0, conf)),
2349
  ))
2350
  return out_boxes