meaculpitt commited on
Commit
6b9d0d6
Β·
verified Β·
1 Parent(s): 3e48203

scorevision: push artifact

Browse files
Files changed (2) hide show
  1. chute_config.yml +1 -1
  2. 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: 2.0
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.20 (2026-04-04). YOLO12s + TRT + bus fix.
3
- Dual-model: vehicle (YOLO11m INT8 1280, 1-pass) + person (YOLO12s FP16 960 end2end, TRT).
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
- Both vehicle + person models run on every image. All detections merged.
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 = 15 # hard cap on person detections per image
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 vehicle.
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
- return self._CHALLENGE_TYPE_MAP.get(ct_id, 'both')
 
 
 
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