fitleech commited on
Commit
01b2bd8
·
verified ·
1 Parent(s): fc50ffd

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +3 -0
  2. chute_config.yml +20 -0
  3. class_names.txt +4 -0
  4. miner.py +554 -0
  5. petrol.onnx +3 -0
README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ---
2
+ license: mit
3
+ ---
chute_config.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Image:
2
+ from_base: parachutes/python:3.12
3
+ run_command:
4
+ - pip install --upgrade setuptools wheel
5
+ - pip install 'numpy>=1.23' 'onnxruntime-gpu[cuda,cudnn]>=1.16' 'opencv-python>=4.7' 'pillow>=9.5' 'huggingface_hub>=0.19.4' 'pydantic>=2.0' 'pyyaml>=6.0' 'aiohttp>=3.9'
6
+ - pip install torch torchvision
7
+ set_workdir: /app
8
+
9
+ NodeSelector:
10
+ gpu_count: 1
11
+ include:
12
+ - pro_6000
13
+
14
+ Chute:
15
+ tee: true
16
+ timeout_seconds: 900
17
+ concurrency: 4
18
+ max_instances: 5
19
+ scaling_threshold: 0.5
20
+ shutdown_after_seconds: 288000
class_names.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ petrol hose
2
+ petrol pump
3
+ price board
4
+ roof canopy
miner.py ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from pathlib import Path
3
+ import math
4
+
5
+ import cv2
6
+ import numpy as np
7
+ import onnxruntime as ort
8
+ from numpy import ndarray
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class BoundingBox(BaseModel):
13
+ x1: int
14
+ y1: int
15
+ x2: int
16
+ y2: int
17
+ cls_id: int
18
+ conf: float
19
+
20
+
21
+ class TVFrameResult(BaseModel):
22
+ frame_id: int
23
+ boxes: list[BoundingBox]
24
+ keypoints: list[tuple[int, int]]
25
+
26
+
27
+ class Miner:
28
+ """ONNX-backed petrol-tracking miner with canopy union-merge post-process."""
29
+
30
+ CANOPY_CLS = 3
31
+
32
+ def __init__(self, path_hf_repo: Path) -> None:
33
+ model_path = path_hf_repo / "petrol.onnx"
34
+
35
+ # Class order as exported from the training pt: must match model.names
36
+ self.class_names = ["petrol hose", "petrol pump", "price board", "roof canopy"]
37
+
38
+ print("ORT version:", ort.__version__)
39
+
40
+ try:
41
+ ort.preload_dlls()
42
+ print("✅ onnxruntime.preload_dlls() success")
43
+ except Exception as e:
44
+ print(f"⚠️ preload_dlls failed: {e}")
45
+
46
+ print("ORT available providers BEFORE session:", ort.get_available_providers())
47
+
48
+ sess_options = ort.SessionOptions()
49
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
50
+
51
+ try:
52
+ self.session = ort.InferenceSession(
53
+ str(model_path),
54
+ sess_options=sess_options,
55
+ providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
56
+ )
57
+ print("✅ Created ORT session with preferred CUDA provider list")
58
+ except Exception as e:
59
+ print(f"⚠️ CUDA session creation failed, falling back to CPU: {e}")
60
+ self.session = ort.InferenceSession(
61
+ str(model_path),
62
+ sess_options=sess_options,
63
+ providers=["CPUExecutionProvider"],
64
+ )
65
+
66
+ print("ORT session providers:", self.session.get_providers())
67
+
68
+ for inp in self.session.get_inputs():
69
+ print("INPUT:", inp.name, inp.shape, inp.type)
70
+
71
+ for out in self.session.get_outputs():
72
+ print("OUTPUT:", out.name, out.shape, out.type)
73
+
74
+ self.input_name = self.session.get_inputs()[0].name
75
+ self.output_names = [output.name for output in self.session.get_outputs()]
76
+ self.input_shape = self.session.get_inputs()[0].shape
77
+
78
+ self.input_height = self._safe_dim(self.input_shape[2], default=640)
79
+ self.input_width = self._safe_dim(self.input_shape[3], default=640)
80
+
81
+ # Thresholds
82
+ self.conf_thres = 0.38
83
+ self.iou_thres = 0.50
84
+ self.max_det = 300
85
+
86
+ # Canopy union-merge: same-class IoU above this triggers a union merge
87
+ # for class 3 only (roof canopy). Set to 0 to disable.
88
+ self.canopy_merge_iou = 0.30
89
+
90
+ print(f"✅ Petrol ONNX model loaded from: {model_path}")
91
+ print(f"✅ ONNX providers: {self.session.get_providers()}")
92
+ print(f"✅ ONNX input: name={self.input_name}, shape={self.input_shape}")
93
+ print(f"✅ Canopy merge IoU: {self.canopy_merge_iou}")
94
+
95
+ def __repr__(self) -> str:
96
+ return (
97
+ f"Petrol ONNXRuntime(session={type(self.session).__name__}, "
98
+ f"providers={self.session.get_providers()})"
99
+ )
100
+
101
+ @staticmethod
102
+ def _safe_dim(value, default: int) -> int:
103
+ return value if isinstance(value, int) and value > 0 else default
104
+
105
+ def _letterbox(
106
+ self,
107
+ image: ndarray,
108
+ new_shape: tuple[int, int],
109
+ color=(114, 114, 114),
110
+ ) -> tuple[ndarray, float, tuple[float, float]]:
111
+ h, w = image.shape[:2]
112
+ new_w, new_h = new_shape
113
+
114
+ ratio = min(new_w / w, new_h / h)
115
+ resized_w = int(round(w * ratio))
116
+ resized_h = int(round(h * ratio))
117
+
118
+ if (resized_w, resized_h) != (w, h):
119
+ interp = cv2.INTER_CUBIC if ratio > 1.0 else cv2.INTER_LINEAR
120
+ image = cv2.resize(image, (resized_w, resized_h), interpolation=interp)
121
+
122
+ dw = new_w - resized_w
123
+ dh = new_h - resized_h
124
+ dw /= 2.0
125
+ dh /= 2.0
126
+
127
+ left = int(round(dw - 0.1))
128
+ right = int(round(dw + 0.1))
129
+ top = int(round(dh - 0.1))
130
+ bottom = int(round(dh + 0.1))
131
+
132
+ padded = cv2.copyMakeBorder(
133
+ image,
134
+ top,
135
+ bottom,
136
+ left,
137
+ right,
138
+ borderType=cv2.BORDER_CONSTANT,
139
+ value=color,
140
+ )
141
+ return padded, ratio, (dw, dh)
142
+
143
+ def _preprocess(
144
+ self, image: ndarray
145
+ ) -> tuple[np.ndarray, float, tuple[float, float], tuple[int, int]]:
146
+ orig_h, orig_w = image.shape[:2]
147
+
148
+ img, ratio, pad = self._letterbox(
149
+ image, (self.input_width, self.input_height)
150
+ )
151
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
152
+ img = img.astype(np.float32) / 255.0
153
+ img = np.transpose(img, (2, 0, 1))[None, ...]
154
+ img = np.ascontiguousarray(img, dtype=np.float32)
155
+
156
+ return img, ratio, pad, (orig_w, orig_h)
157
+
158
+ @staticmethod
159
+ def _clip_boxes(boxes: np.ndarray, image_size: tuple[int, int]) -> np.ndarray:
160
+ w, h = image_size
161
+ boxes[:, 0] = np.clip(boxes[:, 0], 0, w - 1)
162
+ boxes[:, 1] = np.clip(boxes[:, 1], 0, h - 1)
163
+ boxes[:, 2] = np.clip(boxes[:, 2], 0, w - 1)
164
+ boxes[:, 3] = np.clip(boxes[:, 3], 0, h - 1)
165
+ return boxes
166
+
167
+ @staticmethod
168
+ def _xywh_to_xyxy(boxes: np.ndarray) -> np.ndarray:
169
+ out = np.empty_like(boxes)
170
+ out[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0
171
+ out[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0
172
+ out[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0
173
+ out[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0
174
+ return out
175
+
176
+ @staticmethod
177
+ def _hard_nms(
178
+ boxes: np.ndarray,
179
+ scores: np.ndarray,
180
+ iou_thresh: float,
181
+ ) -> np.ndarray:
182
+ if len(boxes) == 0:
183
+ return np.array([], dtype=np.intp)
184
+
185
+ boxes = np.asarray(boxes, dtype=np.float32)
186
+ scores = np.asarray(scores, dtype=np.float32)
187
+ order = np.argsort(scores)[::-1]
188
+ keep = []
189
+
190
+ while len(order) > 0:
191
+ i = order[0]
192
+ keep.append(i)
193
+ if len(order) == 1:
194
+ break
195
+
196
+ rest = order[1:]
197
+
198
+ xx1 = np.maximum(boxes[i, 0], boxes[rest, 0])
199
+ yy1 = np.maximum(boxes[i, 1], boxes[rest, 1])
200
+ xx2 = np.minimum(boxes[i, 2], boxes[rest, 2])
201
+ yy2 = np.minimum(boxes[i, 3], boxes[rest, 3])
202
+
203
+ inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1)
204
+
205
+ area_i = max(0.0, (boxes[i, 2] - boxes[i, 0])) * max(0.0, (boxes[i, 3] - boxes[i, 1]))
206
+ area_r = np.maximum(0.0, boxes[rest, 2] - boxes[rest, 0]) * np.maximum(0.0, boxes[rest, 3] - boxes[rest, 1])
207
+
208
+ iou = inter / (area_i + area_r - inter + 1e-7)
209
+ order = rest[iou <= iou_thresh]
210
+
211
+ return np.array(keep, dtype=np.intp)
212
+
213
+ @classmethod
214
+ def _nms_per_class(
215
+ cls,
216
+ boxes: np.ndarray,
217
+ scores: np.ndarray,
218
+ cls_ids: np.ndarray,
219
+ iou_thresh: float,
220
+ max_det: int,
221
+ ) -> np.ndarray:
222
+ if len(boxes) == 0:
223
+ return np.array([], dtype=np.intp)
224
+ keep_all: list[int] = []
225
+ for c in np.unique(cls_ids):
226
+ idxs = np.nonzero(cls_ids == c)[0]
227
+ if len(idxs) == 0:
228
+ continue
229
+ local_keep = cls._hard_nms(boxes[idxs], scores[idxs], iou_thresh)
230
+ keep_all.extend(idxs[local_keep].tolist())
231
+ keep_all_arr = np.array(keep_all, dtype=np.intp)
232
+ order = np.argsort(scores[keep_all_arr])[::-1]
233
+ return keep_all_arr[order[:max_det]]
234
+
235
+ @staticmethod
236
+ def _pairwise_iou(boxes: np.ndarray) -> np.ndarray:
237
+ """N×N IoU matrix for an [N,4] xyxy array."""
238
+ n = len(boxes)
239
+ if n == 0:
240
+ return np.zeros((0, 0), dtype=np.float32)
241
+ x1 = boxes[:, 0]; y1 = boxes[:, 1]
242
+ x2 = boxes[:, 2]; y2 = boxes[:, 3]
243
+ area = np.maximum(0.0, x2 - x1) * np.maximum(0.0, y2 - y1)
244
+
245
+ ix1 = np.maximum(x1[:, None], x1[None, :])
246
+ iy1 = np.maximum(y1[:, None], y1[None, :])
247
+ ix2 = np.minimum(x2[:, None], x2[None, :])
248
+ iy2 = np.minimum(y2[:, None], y2[None, :])
249
+ iw = np.maximum(0.0, ix2 - ix1)
250
+ ih = np.maximum(0.0, iy2 - iy1)
251
+ inter = iw * ih
252
+ union = area[:, None] + area[None, :] - inter
253
+ with np.errstate(divide="ignore", invalid="ignore"):
254
+ iou = np.where(union > 0, inter / union, 0.0)
255
+ np.fill_diagonal(iou, 0.0)
256
+ return iou.astype(np.float32)
257
+
258
+ def _union_merge_class(
259
+ self,
260
+ boxes: np.ndarray,
261
+ scores: np.ndarray,
262
+ cls_ids: np.ndarray,
263
+ target_cls: int,
264
+ merge_iou: float,
265
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
266
+ """Greedy union-merge for one class.
267
+
268
+ For boxes whose cls == target_cls, repeatedly fuse pairs whose IoU
269
+ exceeds `merge_iou`: replace them with the bounding-rectangle union
270
+ (max conf). Other classes are passed through unchanged.
271
+ """
272
+ if merge_iou <= 0 or len(boxes) == 0:
273
+ return boxes, scores, cls_ids
274
+
275
+ mask = cls_ids == target_cls
276
+ if mask.sum() < 2:
277
+ return boxes, scores, cls_ids
278
+
279
+ tgt_boxes = boxes[mask].astype(np.float32).copy()
280
+ tgt_scores = scores[mask].astype(np.float32).copy()
281
+
282
+ # Greedy merge: highest-conf box anchors each round; absorb all
283
+ # others above the IoU threshold; repeat until stable.
284
+ changed = True
285
+ while changed and len(tgt_boxes) > 1:
286
+ changed = False
287
+ order = np.argsort(tgt_scores)[::-1]
288
+ tgt_boxes = tgt_boxes[order]
289
+ tgt_scores = tgt_scores[order]
290
+
291
+ iou = self._pairwise_iou(tgt_boxes)
292
+ consumed = np.zeros(len(tgt_boxes), dtype=bool)
293
+ new_boxes: list[np.ndarray] = []
294
+ new_scores: list[float] = []
295
+ for i in range(len(tgt_boxes)):
296
+ if consumed[i]:
297
+ continue
298
+ cur = tgt_boxes[i].copy()
299
+ cur_s = float(tgt_scores[i])
300
+ for j in range(i + 1, len(tgt_boxes)):
301
+ if consumed[j]:
302
+ continue
303
+ if iou[i, j] > merge_iou:
304
+ cur = np.array([
305
+ min(cur[0], tgt_boxes[j, 0]),
306
+ min(cur[1], tgt_boxes[j, 1]),
307
+ max(cur[2], tgt_boxes[j, 2]),
308
+ max(cur[3], tgt_boxes[j, 3]),
309
+ ], dtype=np.float32)
310
+ cur_s = max(cur_s, float(tgt_scores[j]))
311
+ consumed[j] = True
312
+ changed = True
313
+ new_boxes.append(cur)
314
+ new_scores.append(cur_s)
315
+ tgt_boxes = np.stack(new_boxes, axis=0)
316
+ tgt_scores = np.array(new_scores, dtype=np.float32)
317
+
318
+ # Stitch results back together with non-target classes
319
+ other_boxes = boxes[~mask]
320
+ other_scores = scores[~mask]
321
+ other_cls = cls_ids[~mask]
322
+
323
+ merged_cls = np.full(len(tgt_boxes), target_cls, dtype=cls_ids.dtype)
324
+ out_boxes = np.concatenate([other_boxes, tgt_boxes], axis=0)
325
+ out_scores = np.concatenate([other_scores, tgt_scores], axis=0)
326
+ out_cls = np.concatenate([other_cls, merged_cls], axis=0)
327
+ return out_boxes, out_scores, out_cls
328
+
329
+ def _decode_yolov8(
330
+ self,
331
+ preds: np.ndarray,
332
+ ratio: float,
333
+ pad: tuple[float, float],
334
+ orig_size: tuple[int, int],
335
+ ) -> list[BoundingBox]:
336
+ """
337
+ Decode a raw YOLOv8-style ONNX detection output.
338
+
339
+ Expected shape: [1, 4 + nc, num_boxes] (no objectness channel).
340
+ Some exporters emit [1, num_boxes, 4 + nc]; both are handled.
341
+ """
342
+ if preds.ndim != 3 or preds.shape[0] != 1:
343
+ raise ValueError(f"Unexpected ONNX output shape: {preds.shape}")
344
+
345
+ preds = preds[0]
346
+
347
+ # Normalize to [N, C] where C = 4 + nc
348
+ nc = len(self.class_names)
349
+ expected_c = 4 + nc
350
+ if preds.shape[0] == expected_c:
351
+ preds = preds.T
352
+ elif preds.shape[1] != expected_c:
353
+ # Fall back: treat smaller dim as channels
354
+ if preds.shape[0] < preds.shape[1]:
355
+ preds = preds.T
356
+
357
+ if preds.ndim != 2 or preds.shape[1] < 5:
358
+ raise ValueError(f"Unexpected normalized output shape: {preds.shape}")
359
+
360
+ boxes_xywh = preds[:, :4].astype(np.float32)
361
+ class_probs = preds[:, 4:].astype(np.float32)
362
+
363
+ cls_ids = np.argmax(class_probs, axis=1).astype(np.int32)
364
+ scores = class_probs[np.arange(len(class_probs)), cls_ids]
365
+
366
+ keep = scores >= self.conf_thres
367
+ boxes_xywh = boxes_xywh[keep]
368
+ scores = scores[keep]
369
+ cls_ids = cls_ids[keep]
370
+
371
+ if len(boxes_xywh) == 0:
372
+ return []
373
+
374
+ boxes = self._xywh_to_xyxy(boxes_xywh)
375
+
376
+ pad_w, pad_h = pad
377
+ orig_w, orig_h = orig_size
378
+
379
+ boxes[:, [0, 2]] -= pad_w
380
+ boxes[:, [1, 3]] -= pad_h
381
+ boxes /= ratio
382
+ boxes = self._clip_boxes(boxes, (orig_w, orig_h))
383
+
384
+ keep_idx = self._nms_per_class(
385
+ boxes, scores, cls_ids, self.iou_thres, self.max_det
386
+ )
387
+
388
+ boxes = boxes[keep_idx]
389
+ scores = scores[keep_idx]
390
+ cls_ids = cls_ids[keep_idx]
391
+
392
+ # Class-3 union-merge: rejoin half-canopy splits into one box.
393
+ boxes, scores, cls_ids = self._union_merge_class(
394
+ boxes, scores, cls_ids,
395
+ target_cls=self.CANOPY_CLS,
396
+ merge_iou=self.canopy_merge_iou,
397
+ )
398
+
399
+ return [
400
+ BoundingBox(
401
+ x1=int(math.floor(box[0])),
402
+ y1=int(math.floor(box[1])),
403
+ x2=int(math.ceil(box[2])),
404
+ y2=int(math.ceil(box[3])),
405
+ cls_id=int(cls_id),
406
+ conf=float(conf),
407
+ )
408
+ for box, conf, cls_id in zip(boxes, scores, cls_ids)
409
+ if box[2] > box[0] and box[3] > box[1]
410
+ ]
411
+
412
+ def _predict_single(self, image: np.ndarray) -> list[BoundingBox]:
413
+ if image is None:
414
+ raise ValueError("Input image is None")
415
+ if not isinstance(image, np.ndarray):
416
+ raise TypeError(f"Input is not numpy array: {type(image)}")
417
+ if image.ndim != 3:
418
+ raise ValueError(f"Expected HWC image, got shape={image.shape}")
419
+ if image.shape[0] <= 0 or image.shape[1] <= 0:
420
+ raise ValueError(f"Invalid image shape={image.shape}")
421
+ if image.shape[2] != 3:
422
+ raise ValueError(f"Expected 3 channels, got shape={image.shape}")
423
+
424
+ if image.dtype != np.uint8:
425
+ image = image.astype(np.uint8)
426
+
427
+ input_tensor, ratio, pad, orig_size = self._preprocess(image)
428
+
429
+ expected_shape = (1, 3, self.input_height, self.input_width)
430
+ if input_tensor.shape != expected_shape:
431
+ raise ValueError(
432
+ f"Bad input tensor shape={input_tensor.shape}, expected={expected_shape}"
433
+ )
434
+
435
+ outputs = self.session.run(self.output_names, {self.input_name: input_tensor})
436
+ det_output = outputs[0]
437
+ return self._decode_yolov8(det_output, ratio, pad, orig_size)
438
+
439
+ def predict_batch(
440
+ self,
441
+ batch_images: list[ndarray],
442
+ offset: int,
443
+ n_keypoints: int,
444
+ ) -> list[TVFrameResult]:
445
+ """
446
+ Miner prediction for a batch of images using ONNX Runtime.
447
+
448
+ The petrol detector is a plain object-detection model (no pose),
449
+ so keypoints are returned as `n_keypoints` padding entries of (0, 0)
450
+ to keep the TVFrameResult schema stable across challenge types.
451
+ """
452
+ results: list[TVFrameResult] = []
453
+ n_kp = max(0, int(n_keypoints))
454
+
455
+ for frame_number_in_batch, image in enumerate(batch_images):
456
+ frame_idx = offset + frame_number_in_batch
457
+ try:
458
+ boxes = self._predict_single(image)
459
+ except Exception as e:
460
+ print(f"⚠️ Inference failed for frame {frame_idx}: {e}")
461
+ boxes = []
462
+
463
+ results.append(
464
+ TVFrameResult(
465
+ frame_id=frame_idx,
466
+ boxes=boxes,
467
+ keypoints=[(0, 0) for _ in range(n_kp)],
468
+ )
469
+ )
470
+
471
+ print("✅ Petrol ONNX predictions complete")
472
+ return results
473
+
474
+
475
+ def main() -> None:
476
+ """Example runner — same CLI as miner.py for direct A/B comparison."""
477
+ import sys
478
+
479
+ repo_path = Path(__file__).parent
480
+ print(f"Loading miner_v2 from: {repo_path}")
481
+ miner = Miner(path_hf_repo=repo_path)
482
+ print(repr(miner))
483
+
484
+ batch_images: list[np.ndarray] = []
485
+
486
+ if len(sys.argv) > 1:
487
+ for image_path in sys.argv[1:]:
488
+ image = cv2.imread(image_path)
489
+ if image is None:
490
+ raise ValueError(f"Cannot read image: {image_path}")
491
+ batch_images.append(image)
492
+ print(f"Loaded {len(batch_images)} image(s)")
493
+ else:
494
+ batch_images = [np.zeros((640, 640, 3), dtype=np.uint8)]
495
+ print("No image provided — running on a single blank dummy frame")
496
+
497
+ results = miner.predict_batch(
498
+ batch_images=batch_images,
499
+ offset=0,
500
+ n_keypoints=32,
501
+ )
502
+
503
+ output_dir = repo_path / "predictions_v2"
504
+ output_dir.mkdir(exist_ok=True)
505
+
506
+ class_names = {i: n for i, n in enumerate(miner.class_names)}
507
+
508
+ def color_for_class(cls_id: int) -> tuple[int, int, int]:
509
+ hue = (cls_id * 47) % 180
510
+ hsv = np.uint8([[[hue, 220, 255]]])
511
+ bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)[0, 0]
512
+ return int(bgr[0]), int(bgr[1]), int(bgr[2])
513
+
514
+ for image, r in zip(batch_images, results):
515
+ print(
516
+ f"frame={r.frame_id} "
517
+ f"boxes={len(r.boxes)} "
518
+ f"keypoints={len(r.keypoints)}"
519
+ )
520
+
521
+ vis = image.copy()
522
+ for box in r.boxes:
523
+ name = class_names.get(box.cls_id, str(box.cls_id))
524
+ color = color_for_class(box.cls_id)
525
+ print(
526
+ f" box cls={box.cls_id}({name}) conf={box.conf:.2f} "
527
+ f"[{box.x1},{box.y1},{box.x2},{box.y2}]"
528
+ )
529
+ cv2.rectangle(vis, (box.x1, box.y1), (box.x2, box.y2), color, 2)
530
+ label = f"{name} {box.conf:.2f}"
531
+ (tw, th), baseline = cv2.getTextSize(
532
+ label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1
533
+ )
534
+ top = max(box.y1 - th - baseline, 0)
535
+ cv2.rectangle(
536
+ vis, (box.x1, top), (box.x1 + tw, top + th + baseline), color, -1
537
+ )
538
+ cv2.putText(
539
+ vis, label, (box.x1, top + th),
540
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv2.LINE_AA,
541
+ )
542
+
543
+ for x, y in r.keypoints:
544
+ if x == 0 and y == 0:
545
+ continue
546
+ cv2.circle(vis, (x, y), 3, (0, 0, 255), -1)
547
+
548
+ out_path = output_dir / f"frame_{r.frame_id:04d}.jpg"
549
+ cv2.imwrite(str(out_path), vis)
550
+ print(f" saved: {out_path}")
551
+
552
+
553
+ if __name__ == "__main__":
554
+ main()
petrol.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb8ff9dbe935b06f64e6049b0604c2c871386b633b36ef9d320d0e02e5f35c36
3
+ size 22664875