scorevision: push artifact
Browse files- chute_config.yml +1 -1
- miner.py +255 -7
chute_config.yml
CHANGED
|
@@ -8,7 +8,7 @@ Image:
|
|
| 8 |
NodeSelector:
|
| 9 |
gpu_count: 1
|
| 10 |
min_vram_gb_per_gpu: 16
|
| 11 |
-
max_hourly_price_per_gpu:
|
| 12 |
exclude:
|
| 13 |
- '5090'
|
| 14 |
- b200
|
|
|
|
| 8 |
NodeSelector:
|
| 9 |
gpu_count: 1
|
| 10 |
min_vram_gb_per_gpu: 16
|
| 11 |
+
max_hourly_price_per_gpu: 0.50
|
| 12 |
exclude:
|
| 13 |
- '5090'
|
| 14 |
- b200
|
miner.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
Score Vision SN44 β Unified miner v3.
|
| 3 |
-
|
| 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).
|
| 6 |
Person weights loaded from primary HF repo (template downloads automatically).
|
|
@@ -27,8 +27,10 @@ Pose model (pose_weights.onnx):
|
|
| 27 |
3. Box refinement: blend detected box with tight keypoint bbox for better fit.
|
| 28 |
Face detector (optional): if face_session loaded, face inside box β never suppress.
|
| 29 |
|
| 30 |
-
|
| 31 |
Vehicle eval uses cls_id 1-3. Person eval uses cls_id 0 only.
|
|
|
|
|
|
|
| 32 |
"""
|
| 33 |
|
| 34 |
import os
|
|
@@ -283,7 +285,7 @@ PER_TILE_OVERLAP = 0.20 # 20% overlap between tiles
|
|
| 283 |
PER_TILE_MIN_DIM_RATIO = 1.15 # tile when image dim > model_dim * this (~1104px for 960 model)
|
| 284 |
PER_TILE_CONF = 0.55 # raised from 0.40 to match PER_CONF_LOW
|
| 285 |
PER_NMS_IOU = 0.50 # NMS IoU for merging across passes (max-conf wins)
|
| 286 |
-
PER_MAX_DET =
|
| 287 |
|
| 288 |
# ββ Frame quality gating (Laplacian variance) βββββββββββββββββββββββββββββββ
|
| 289 |
PER_BLUR_THRESHOLD = 50.0 # Laplacian variance below this = severely blurry
|
|
@@ -354,6 +356,29 @@ ENABLE_PARALLEL = True
|
|
| 354 |
# ββ Secondary HF repo for vehicle weights βββββββββββββββββββββββββββββββββββ
|
| 355 |
VEHICLE_HF_REPO = "meaculpitt/ScoreVision-Vehicle"
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
|
| 359 |
"""Weighted Boxes Fusion (multi-class). Boxes in [0,1] normalized coords."""
|
|
@@ -624,6 +649,40 @@ class Miner:
|
|
| 624 |
self.plate_session = None
|
| 625 |
logger.info("[init] No plate model found, plate confirmation disabled")
|
| 626 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
# Pose cache β populated by _pose_filter_refine, read by vehicle parts
|
| 628 |
self._cached_pose_data = None
|
| 629 |
|
|
@@ -1886,10 +1945,12 @@ class Miner:
|
|
| 1886 |
_CHALLENGE_TYPE_MAP = {2: 'person', 12: 'vehicle'}
|
| 1887 |
|
| 1888 |
def _detect_element_hint(self) -> str:
|
| 1889 |
-
"""Detect whether this request is for person or
|
| 1890 |
|
| 1891 |
Reads challenge_type_id from the chute template predict() metadata
|
| 1892 |
-
via stack frame inspection. Returns 'person', 'vehicle', or 'both'.
|
|
|
|
|
|
|
| 1893 |
"""
|
| 1894 |
frame = None
|
| 1895 |
try:
|
|
@@ -1901,7 +1962,10 @@ class Miner:
|
|
| 1901 |
meta = frame.f_locals.get('metadata')
|
| 1902 |
if isinstance(meta, dict) and 'challenge_type_id' in meta:
|
| 1903 |
ct_id = meta['challenge_type_id']
|
| 1904 |
-
|
|
|
|
|
|
|
|
|
|
| 1905 |
except Exception:
|
| 1906 |
pass
|
| 1907 |
finally:
|
|
@@ -1922,6 +1986,9 @@ class Miner:
|
|
| 1922 |
# detections, large vehicles with conf < 0.55 get falsely suppressed.
|
| 1923 |
return self._infer_vehicle(image_bgr)
|
| 1924 |
|
|
|
|
|
|
|
|
|
|
| 1925 |
# Fallback: run both (original behavior)
|
| 1926 |
if ENABLE_PARALLEL:
|
| 1927 |
veh_future = self._executor.submit(self._infer_vehicle, image_bgr)
|
|
@@ -1938,6 +2005,187 @@ class Miner:
|
|
| 1938 |
|
| 1939 |
return vehicle_boxes + person_boxes
|
| 1940 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1941 |
# -- Replay buffer -------------------------------------------------------
|
| 1942 |
REPLAY_DIR = Path("/home/miner/replay_buffer")
|
| 1943 |
REPLAY_MAX = 100
|
|
|
|
| 1 |
"""
|
| 2 |
+
Score Vision SN44 β Unified miner v3.21 (2026-04-04). YOLO12s + TRT + bus fix + petrol.
|
| 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).
|
| 6 |
Person weights loaded from primary HF repo (template downloads automatically).
|
|
|
|
| 27 |
3. Box refinement: blend detected box with tight keypoint bbox for better fit.
|
| 28 |
Face detector (optional): if face_session loaded, face inside box β never suppress.
|
| 29 |
|
| 30 |
+
Vehicle + person models run on every image when hint='both'. All detections merged.
|
| 31 |
Vehicle eval uses cls_id 1-3. Person eval uses cls_id 0 only.
|
| 32 |
+
Petrol model runs only when challenge_type_id is unrecognized (not 2 or 12).
|
| 33 |
+
Petrol weights loaded from meaculpitt/ScoreVision-Petrol HF repo.
|
| 34 |
"""
|
| 35 |
|
| 36 |
import os
|
|
|
|
| 285 |
PER_TILE_MIN_DIM_RATIO = 1.15 # tile when image dim > model_dim * this (~1104px for 960 model)
|
| 286 |
PER_TILE_CONF = 0.55 # raised from 0.40 to match PER_CONF_LOW
|
| 287 |
PER_NMS_IOU = 0.50 # NMS IoU for merging across passes (max-conf wins)
|
| 288 |
+
PER_MAX_DET = 30 # hard cap on person detections per image (raised from 15: 17% of frames were hitting cap)
|
| 289 |
|
| 290 |
# ββ Frame quality gating (Laplacian variance) βββββββββββββββββββββββββββββββ
|
| 291 |
PER_BLUR_THRESHOLD = 50.0 # Laplacian variance below this = severely blurry
|
|
|
|
| 356 |
# ββ Secondary HF repo for vehicle weights βββββββββββββββββββββββββββββββββββ
|
| 357 |
VEHICLE_HF_REPO = "meaculpitt/ScoreVision-Vehicle"
|
| 358 |
|
| 359 |
+
# ββ Petrol config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 360 |
+
PETROL_HF_REPO = "meaculpitt/ScoreVision-Petrol"
|
| 361 |
+
PETROL_CONF = 0.25
|
| 362 |
+
PETROL_IOU = 0.45
|
| 363 |
+
# Class IDs (petrol model output β independent of person/vehicle cls_ids
|
| 364 |
+
# because element_hint routing ensures only one pipeline runs per challenge)
|
| 365 |
+
PETROL_CLS_HOSE = 0
|
| 366 |
+
PETROL_CLS_PUMP = 1
|
| 367 |
+
PETROL_CLS_PRICEBOARD = 2
|
| 368 |
+
PETROL_CLS_CANOPY = 3
|
| 369 |
+
# Geometric validation thresholds
|
| 370 |
+
PETROL_CANOPY_MIN_ASPECT = 0.8
|
| 371 |
+
PETROL_PUMP_MAX_ASPECT = 4.0
|
| 372 |
+
PETROL_PRICEBOARD_MAX_AREA_FRAC = 0.15
|
| 373 |
+
PETROL_HOSE_MIN_AREA_FRAC = 0.0005
|
| 374 |
+
PETROL_GEOM_PENALTY = 0.10
|
| 375 |
+
# Spatial co-occurrence
|
| 376 |
+
PETROL_COOCCUR_PUMP_CANOPY = 0.05
|
| 377 |
+
PETROL_COOCCUR_PUMP_HOSE = 0.08
|
| 378 |
+
PETROL_COOCCUR_CANOPY_HOSE = 0.05
|
| 379 |
+
PETROL_COOCCUR_SUPPRESS = 0.03
|
| 380 |
+
PETROL_COOCCUR_PROXIMITY = 0.5
|
| 381 |
+
|
| 382 |
|
| 383 |
def _wbf_multi(boxes_list, scores_list, labels_list, iou_thr=0.55, skip_thr=0.0001):
|
| 384 |
"""Weighted Boxes Fusion (multi-class). Boxes in [0,1] normalized coords."""
|
|
|
|
| 649 |
self.plate_session = None
|
| 650 |
logger.info("[init] No plate model found, plate confirmation disabled")
|
| 651 |
|
| 652 |
+
# Petrol model β download from dedicated HF repo
|
| 653 |
+
try:
|
| 654 |
+
from huggingface_hub import snapshot_download as _sd
|
| 655 |
+
petrol_path = Path(_sd(PETROL_HF_REPO))
|
| 656 |
+
petrol_weights = str(petrol_path / "weights.onnx")
|
| 657 |
+
logger.info(f"[init] Petrol weights from {PETROL_HF_REPO}")
|
| 658 |
+
except Exception as e:
|
| 659 |
+
logger.warning(f"[init] Petrol secondary repo failed ({e}), trying primary repo")
|
| 660 |
+
petrol_weights = str(path_hf_repo / "weights.onnx")
|
| 661 |
+
if not Path(petrol_weights).exists():
|
| 662 |
+
petrol_weights = None
|
| 663 |
+
logger.warning("[init] No petrol weights found β petrol inference disabled")
|
| 664 |
+
|
| 665 |
+
if petrol_weights and Path(petrol_weights).exists():
|
| 666 |
+
self.petrol_session = ort.InferenceSession(
|
| 667 |
+
petrol_weights,
|
| 668 |
+
providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
|
| 669 |
+
)
|
| 670 |
+
self.petrol_input_name = self.petrol_session.get_inputs()[0].name
|
| 671 |
+
petrol_shape = self.petrol_session.get_inputs()[0].shape
|
| 672 |
+
self.petrol_h = int(petrol_shape[2])
|
| 673 |
+
self.petrol_w = int(petrol_shape[3])
|
| 674 |
+
# Detect output format
|
| 675 |
+
petrol_out_shape = self.petrol_session.get_outputs()[0].shape
|
| 676 |
+
self._petrol_end2end = (
|
| 677 |
+
len(petrol_out_shape) == 3
|
| 678 |
+
and petrol_out_shape[2] == 6
|
| 679 |
+
and (petrol_out_shape[1] or 0) <= 1000
|
| 680 |
+
)
|
| 681 |
+
logger.info(f"[init] Petrol model loaded: {petrol_shape}, end2end={self._petrol_end2end}")
|
| 682 |
+
else:
|
| 683 |
+
self.petrol_session = None
|
| 684 |
+
self._petrol_end2end = False
|
| 685 |
+
|
| 686 |
# Pose cache β populated by _pose_filter_refine, read by vehicle parts
|
| 687 |
self._cached_pose_data = None
|
| 688 |
|
|
|
|
| 1945 |
_CHALLENGE_TYPE_MAP = {2: 'person', 12: 'vehicle'}
|
| 1946 |
|
| 1947 |
def _detect_element_hint(self) -> str:
|
| 1948 |
+
"""Detect whether this request is for person, vehicle, or petrol.
|
| 1949 |
|
| 1950 |
Reads challenge_type_id from the chute template predict() metadata
|
| 1951 |
+
via stack frame inspection. Returns 'person', 'vehicle', 'petrol', or 'both'.
|
| 1952 |
+
Any unrecognized challenge_type_id routes to petrol (the only other
|
| 1953 |
+
element on this chute).
|
| 1954 |
"""
|
| 1955 |
frame = None
|
| 1956 |
try:
|
|
|
|
| 1962 |
meta = frame.f_locals.get('metadata')
|
| 1963 |
if isinstance(meta, dict) and 'challenge_type_id' in meta:
|
| 1964 |
ct_id = meta['challenge_type_id']
|
| 1965 |
+
hint = self._CHALLENGE_TYPE_MAP.get(ct_id)
|
| 1966 |
+
if hint:
|
| 1967 |
+
return hint
|
| 1968 |
+
return 'petrol' if self.petrol_session else 'both'
|
| 1969 |
except Exception:
|
| 1970 |
pass
|
| 1971 |
finally:
|
|
|
|
| 1986 |
# detections, large vehicles with conf < 0.55 get falsely suppressed.
|
| 1987 |
return self._infer_vehicle(image_bgr)
|
| 1988 |
|
| 1989 |
+
if element_hint == 'petrol' and self.petrol_session:
|
| 1990 |
+
return self._infer_petrol(image_bgr)
|
| 1991 |
+
|
| 1992 |
# Fallback: run both (original behavior)
|
| 1993 |
if ENABLE_PARALLEL:
|
| 1994 |
veh_future = self._executor.submit(self._infer_vehicle, image_bgr)
|
|
|
|
| 2005 |
|
| 2006 |
return vehicle_boxes + person_boxes
|
| 2007 |
|
| 2008 |
+
# ββ Petrol inference pipeline βββββββββββββββββββββββββββββββββββββββββββ
|
| 2009 |
+
|
| 2010 |
+
def _petrol_preprocess(self, image_bgr: ndarray):
|
| 2011 |
+
"""Resize to model input, normalize to [0,1] float32 NCHW."""
|
| 2012 |
+
h, w = image_bgr.shape[:2]
|
| 2013 |
+
rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
|
| 2014 |
+
resized = cv2.resize(rgb, (self.petrol_w, self.petrol_h))
|
| 2015 |
+
x = resized.astype(np.float32) / 255.0
|
| 2016 |
+
x = np.transpose(x, (2, 0, 1))[None, ...]
|
| 2017 |
+
return x, (h, w)
|
| 2018 |
+
|
| 2019 |
+
def _petrol_decode_end2end(self, out, orig_h, orig_w):
|
| 2020 |
+
"""Decode end-to-end [1, N, 6] output: [x1,y1,x2,y2,conf,cls_id]."""
|
| 2021 |
+
pred = out[0]
|
| 2022 |
+
if pred.ndim != 2 or pred.shape[1] != 6:
|
| 2023 |
+
return []
|
| 2024 |
+
confs = pred[:, 4]
|
| 2025 |
+
keep = confs >= PETROL_CONF
|
| 2026 |
+
pred = pred[keep]
|
| 2027 |
+
if pred.shape[0] == 0:
|
| 2028 |
+
return []
|
| 2029 |
+
sx = orig_w / float(self.petrol_w)
|
| 2030 |
+
sy = orig_h / float(self.petrol_h)
|
| 2031 |
+
results = []
|
| 2032 |
+
for i in range(pred.shape[0]):
|
| 2033 |
+
results.append((
|
| 2034 |
+
pred[i, 0] * sx, pred[i, 1] * sy,
|
| 2035 |
+
pred[i, 2] * sx, pred[i, 3] * sy,
|
| 2036 |
+
float(pred[i, 4]), int(pred[i, 5]),
|
| 2037 |
+
))
|
| 2038 |
+
return results
|
| 2039 |
+
|
| 2040 |
+
def _petrol_decode_raw(self, out, orig_h, orig_w):
|
| 2041 |
+
"""Decode raw [1, 4+nc, N] output with NMS."""
|
| 2042 |
+
pred = out[0]
|
| 2043 |
+
if pred.ndim != 2:
|
| 2044 |
+
return []
|
| 2045 |
+
if pred.shape[0] < pred.shape[1]:
|
| 2046 |
+
pred = pred.T
|
| 2047 |
+
if pred.shape[1] < 5:
|
| 2048 |
+
return []
|
| 2049 |
+
boxes = pred[:, :4]
|
| 2050 |
+
cls_scores = pred[:, 4:]
|
| 2051 |
+
if cls_scores.shape[1] == 0:
|
| 2052 |
+
return []
|
| 2053 |
+
cls_ids = np.argmax(cls_scores, axis=1)
|
| 2054 |
+
confs = np.max(cls_scores, axis=1)
|
| 2055 |
+
keep = confs >= PETROL_CONF
|
| 2056 |
+
boxes, confs, cls_ids = boxes[keep], confs[keep], cls_ids[keep]
|
| 2057 |
+
if boxes.shape[0] == 0:
|
| 2058 |
+
return []
|
| 2059 |
+
sx = orig_w / float(self.petrol_w)
|
| 2060 |
+
sy = orig_h / float(self.petrol_h)
|
| 2061 |
+
dets = []
|
| 2062 |
+
for i in range(boxes.shape[0]):
|
| 2063 |
+
cx, cy, bw, bh = boxes[i].tolist()
|
| 2064 |
+
dets.append((
|
| 2065 |
+
(cx - bw / 2.0) * sx, (cy - bh / 2.0) * sy,
|
| 2066 |
+
(cx + bw / 2.0) * sx, (cy + bh / 2.0) * sy,
|
| 2067 |
+
float(confs[i]), int(cls_ids[i]),
|
| 2068 |
+
))
|
| 2069 |
+
# Simple NMS
|
| 2070 |
+
if not dets:
|
| 2071 |
+
return dets
|
| 2072 |
+
arr_b = np.array([[d[0], d[1], d[2], d[3]] for d in dets], dtype=np.float32)
|
| 2073 |
+
arr_s = np.array([d[4] for d in dets], dtype=np.float32)
|
| 2074 |
+
order = arr_s.argsort()[::-1]
|
| 2075 |
+
kept = []
|
| 2076 |
+
while order.size > 0:
|
| 2077 |
+
i = order[0]
|
| 2078 |
+
kept.append(i)
|
| 2079 |
+
xx1 = np.maximum(arr_b[i, 0], arr_b[order[1:], 0])
|
| 2080 |
+
yy1 = np.maximum(arr_b[i, 1], arr_b[order[1:], 1])
|
| 2081 |
+
xx2 = np.minimum(arr_b[i, 2], arr_b[order[1:], 2])
|
| 2082 |
+
yy2 = np.minimum(arr_b[i, 3], arr_b[order[1:], 3])
|
| 2083 |
+
inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
|
| 2084 |
+
area_i = (arr_b[i, 2] - arr_b[i, 0]) * (arr_b[i, 3] - arr_b[i, 1])
|
| 2085 |
+
area_r = (arr_b[order[1:], 2] - arr_b[order[1:], 0]) * (arr_b[order[1:], 3] - arr_b[order[1:], 1])
|
| 2086 |
+
iou = inter / np.maximum(area_i + area_r - inter, 1e-6)
|
| 2087 |
+
order = order[np.where(iou <= PETROL_IOU)[0] + 1]
|
| 2088 |
+
return [dets[idx] for idx in kept]
|
| 2089 |
+
|
| 2090 |
+
def _petrol_geometric_validate(self, dets, orig_h, orig_w):
|
| 2091 |
+
"""Per-class shape filters: aspect ratio + area checks."""
|
| 2092 |
+
img_area = max(orig_h * orig_w, 1)
|
| 2093 |
+
result = []
|
| 2094 |
+
for x1, y1, x2, y2, conf, cls_id in dets:
|
| 2095 |
+
bw = max(x2 - x1, 1)
|
| 2096 |
+
bh = max(y2 - y1, 1)
|
| 2097 |
+
aspect = bw / bh
|
| 2098 |
+
area_frac = (bw * bh) / img_area
|
| 2099 |
+
penalty = 0.0
|
| 2100 |
+
if cls_id == PETROL_CLS_CANOPY and aspect < PETROL_CANOPY_MIN_ASPECT:
|
| 2101 |
+
penalty = PETROL_GEOM_PENALTY
|
| 2102 |
+
elif cls_id == PETROL_CLS_PUMP and aspect > PETROL_PUMP_MAX_ASPECT:
|
| 2103 |
+
penalty = PETROL_GEOM_PENALTY
|
| 2104 |
+
elif cls_id == PETROL_CLS_PRICEBOARD and area_frac > PETROL_PRICEBOARD_MAX_AREA_FRAC:
|
| 2105 |
+
penalty = PETROL_GEOM_PENALTY
|
| 2106 |
+
elif cls_id == PETROL_CLS_HOSE and area_frac < PETROL_HOSE_MIN_AREA_FRAC:
|
| 2107 |
+
penalty = PETROL_GEOM_PENALTY
|
| 2108 |
+
new_conf = max(0.0, conf - penalty)
|
| 2109 |
+
if new_conf >= PETROL_CONF:
|
| 2110 |
+
result.append((x1, y1, x2, y2, new_conf, cls_id))
|
| 2111 |
+
return result
|
| 2112 |
+
|
| 2113 |
+
def _petrol_spatial_cooccurrence(self, dets, orig_h, orig_w):
|
| 2114 |
+
"""Proximity-based confidence adjustments for petrol objects."""
|
| 2115 |
+
if not dets:
|
| 2116 |
+
return dets
|
| 2117 |
+
n = len(dets)
|
| 2118 |
+
adjustments = [0.0] * n
|
| 2119 |
+
diag = math.sqrt(orig_h ** 2 + orig_w ** 2)
|
| 2120 |
+
prox = PETROL_COOCCUR_PROXIMITY * diag
|
| 2121 |
+
|
| 2122 |
+
centers = [((x1 + x2) / 2, (y1 + y2) / 2) for x1, y1, x2, y2, _, _ in dets]
|
| 2123 |
+
cls_map = {}
|
| 2124 |
+
for i, (_, _, _, _, _, cls_id) in enumerate(dets):
|
| 2125 |
+
cls_map.setdefault(cls_id, []).append(i)
|
| 2126 |
+
|
| 2127 |
+
def near(i, j):
|
| 2128 |
+
dx = centers[i][0] - centers[j][0]
|
| 2129 |
+
dy = centers[i][1] - centers[j][1]
|
| 2130 |
+
return math.sqrt(dx * dx + dy * dy) < prox
|
| 2131 |
+
|
| 2132 |
+
# Pump + Canopy boost
|
| 2133 |
+
for pi in cls_map.get(PETROL_CLS_PUMP, []):
|
| 2134 |
+
for ci in cls_map.get(PETROL_CLS_CANOPY, []):
|
| 2135 |
+
if near(pi, ci):
|
| 2136 |
+
adjustments[pi] = max(adjustments[pi], PETROL_COOCCUR_PUMP_CANOPY)
|
| 2137 |
+
adjustments[ci] = max(adjustments[ci], PETROL_COOCCUR_PUMP_CANOPY)
|
| 2138 |
+
# Pump + Hose boost
|
| 2139 |
+
for pi in cls_map.get(PETROL_CLS_PUMP, []):
|
| 2140 |
+
for hi in cls_map.get(PETROL_CLS_HOSE, []):
|
| 2141 |
+
if near(pi, hi):
|
| 2142 |
+
adjustments[hi] = max(adjustments[hi], PETROL_COOCCUR_PUMP_HOSE)
|
| 2143 |
+
# Canopy + Hose boost
|
| 2144 |
+
for ci in cls_map.get(PETROL_CLS_CANOPY, []):
|
| 2145 |
+
for hi in cls_map.get(PETROL_CLS_HOSE, []):
|
| 2146 |
+
if near(ci, hi):
|
| 2147 |
+
adjustments[hi] = max(adjustments[hi], PETROL_COOCCUR_CANOPY_HOSE)
|
| 2148 |
+
# Suppress isolated low-conf (not price boards)
|
| 2149 |
+
for i, (_, _, _, _, conf, cls_id) in enumerate(dets):
|
| 2150 |
+
if cls_id == PETROL_CLS_PRICEBOARD or conf > 0.60:
|
| 2151 |
+
continue
|
| 2152 |
+
if not any(near(i, j) for j in range(n) if j != i):
|
| 2153 |
+
adjustments[i] = min(adjustments[i], adjustments[i] - PETROL_COOCCUR_SUPPRESS)
|
| 2154 |
+
|
| 2155 |
+
result = []
|
| 2156 |
+
for i, (x1, y1, x2, y2, conf, cls_id) in enumerate(dets):
|
| 2157 |
+
new_conf = min(1.0, max(0.0, conf + adjustments[i]))
|
| 2158 |
+
if new_conf >= PETROL_CONF:
|
| 2159 |
+
result.append((x1, y1, x2, y2, new_conf, cls_id))
|
| 2160 |
+
return result
|
| 2161 |
+
|
| 2162 |
+
def _infer_petrol(self, image_bgr: ndarray) -> list[BoundingBox]:
|
| 2163 |
+
"""Full petrol inference pipeline: preprocess β forward β decode β validate β cooccurrence."""
|
| 2164 |
+
inp, (orig_h, orig_w) = self._petrol_preprocess(image_bgr)
|
| 2165 |
+
out = self.petrol_session.run(None, {self.petrol_input_name: inp})[0]
|
| 2166 |
+
|
| 2167 |
+
if self._petrol_end2end:
|
| 2168 |
+
dets = self._petrol_decode_end2end(out, orig_h, orig_w)
|
| 2169 |
+
else:
|
| 2170 |
+
dets = self._petrol_decode_raw(out, orig_h, orig_w)
|
| 2171 |
+
if not dets:
|
| 2172 |
+
return []
|
| 2173 |
+
|
| 2174 |
+
dets = self._petrol_geometric_validate(dets, orig_h, orig_w)
|
| 2175 |
+
dets = self._petrol_spatial_cooccurrence(dets, orig_h, orig_w)
|
| 2176 |
+
|
| 2177 |
+
out_boxes = []
|
| 2178 |
+
for x1, y1, x2, y2, conf, cls_id in dets:
|
| 2179 |
+
out_boxes.append(BoundingBox(
|
| 2180 |
+
x1=max(0, min(orig_w, math.floor(x1))),
|
| 2181 |
+
y1=max(0, min(orig_h, math.floor(y1))),
|
| 2182 |
+
x2=max(0, min(orig_w, math.ceil(x2))),
|
| 2183 |
+
y2=max(0, min(orig_h, math.ceil(y2))),
|
| 2184 |
+
cls_id=cls_id,
|
| 2185 |
+
conf=max(0.0, min(1.0, conf)),
|
| 2186 |
+
))
|
| 2187 |
+
return out_boxes
|
| 2188 |
+
|
| 2189 |
# -- Replay buffer -------------------------------------------------------
|
| 2190 |
REPLAY_DIR = Path("/home/miner/replay_buffer")
|
| 2191 |
REPLAY_MAX = 100
|