scorevision: push artifact
Browse files
miner.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Score Vision SN44 — Unified miner v3.
|
| 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.
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
|
|
|
|
|
|
| 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
|
| 2172 |
-
"""
|
| 2173 |
h, w = image_bgr.shape[:2]
|
| 2174 |
-
|
| 2175 |
-
|
| 2176 |
-
|
| 2177 |
-
|
| 2178 |
-
|
| 2179 |
-
|
| 2180 |
-
|
| 2181 |
-
|
| 2182 |
-
|
| 2183 |
-
|
| 2184 |
-
|
| 2185 |
-
|
| 2186 |
-
|
| 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 |
-
|
| 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 |
-
|
| 2226 |
-
|
| 2227 |
-
|
| 2228 |
-
|
| 2229 |
-
))
|
| 2230 |
-
|
| 2231 |
-
|
| 2232 |
-
|
| 2233 |
-
|
| 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 |
-
|
| 2272 |
-
|
|
|
|
| 2273 |
if not dets:
|
| 2274 |
-
return
|
| 2275 |
-
|
| 2276 |
-
|
| 2277 |
-
|
| 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
|
| 2315 |
-
|
| 2316 |
-
|
| 2317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2318 |
return result
|
| 2319 |
|
| 2320 |
-
def
|
| 2321 |
-
"""
|
| 2322 |
-
|
| 2323 |
-
|
| 2324 |
-
|
| 2325 |
-
|
| 2326 |
-
|
| 2327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2328 |
|
| 2329 |
def _infer_petrol(self, image_bgr: ndarray) -> list[BoundingBox]:
|
| 2330 |
-
"""Petrol inference with flip TTA
|
| 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 |
-
#
|
| 2339 |
-
|
| 2340 |
|
| 2341 |
-
#
|
| 2342 |
flipped = cv2.flip(image_bgr, 1)
|
| 2343 |
-
|
| 2344 |
-
|
| 2345 |
-
|
| 2346 |
-
|
| 2347 |
-
|
| 2348 |
-
#
|
| 2349 |
-
|
| 2350 |
-
|
| 2351 |
-
|
| 2352 |
-
|
| 2353 |
-
|
| 2354 |
-
|
| 2355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2356 |
continue
|
| 2357 |
-
|
| 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
|
| 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
|