flare325 commited on
Commit
4a132e1
·
verified ·
1 Parent(s): 3031fa2

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. miner.py +178 -0
miner.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TurboVision public-track miner for manak0/Detect-worksite-ppe-compliance-detection-probe-set.
2
+
3
+ Model: YOLO26n exported to ONNX (input 1x3x704x704, NMS embedded, output 1x300x6
4
+ rows of x1, y1, x2, y2, score, class in letterboxed pixel space).
5
+
6
+ cls_id in the response is the index into the element's manifest `objects` list.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ import cv2
12
+ import numpy as np
13
+ import onnxruntime as ort
14
+ from numpy import ndarray
15
+ from pydantic import BaseModel
16
+
17
+ MANIFEST_CLASSES = [
18
+ "safety helmet",
19
+ "high-visibility safety vest",
20
+ "protective gloves",
21
+ "protective eyewear",
22
+ ]
23
+
24
+ # Per-class confidence thresholds, indexed like MANIFEST_CLASSES.
25
+ CLASS_CONF = np.array([0.40, 0.40, 0.40, 0.30], dtype=np.float32)
26
+
27
+ MODEL_FILENAME = "weights.onnx"
28
+ LETTERBOX_FILL = 114
29
+
30
+
31
+ class BoundingBox(BaseModel):
32
+ x1: int
33
+ y1: int
34
+ x2: int
35
+ y2: int
36
+ cls_id: int
37
+ conf: float
38
+
39
+
40
+ class Polygon(BaseModel):
41
+ cls_id: int
42
+ conf: float
43
+ points: list[tuple[int, int]]
44
+
45
+
46
+ class TVFrameResult(BaseModel):
47
+ frame_id: int
48
+ boxes: list[BoundingBox] | None = None
49
+ polygons: list[Polygon] | None = None
50
+ keypoints: list[tuple[int, int]] | None = None
51
+
52
+
53
+ def _letterbox(image: ndarray, size: int) -> tuple[ndarray, float, int, int]:
54
+ """Ultralytics-style centred letterbox to a size x size canvas."""
55
+ height, width = image.shape[:2]
56
+ ratio = min(size / height, size / width)
57
+ new_w = int(round(width * ratio))
58
+ new_h = int(round(height * ratio))
59
+ if (new_w, new_h) != (width, height):
60
+ image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
61
+ pad_w = (size - new_w) / 2
62
+ pad_h = (size - new_h) / 2
63
+ top, bottom = int(round(pad_h - 0.1)), int(round(pad_h + 0.1))
64
+ left, right = int(round(pad_w - 0.1)), int(round(pad_w + 0.1))
65
+ canvas = cv2.copyMakeBorder(
66
+ image,
67
+ top,
68
+ bottom,
69
+ left,
70
+ right,
71
+ cv2.BORDER_CONSTANT,
72
+ value=(LETTERBOX_FILL, LETTERBOX_FILL, LETTERBOX_FILL),
73
+ )
74
+ return canvas, ratio, left, top
75
+
76
+
77
+ class Miner:
78
+ def __init__(self, path_hf_repo: Path) -> None:
79
+ model_path = Path(path_hf_repo) / MODEL_FILENAME
80
+ if not model_path.is_file():
81
+ raise FileNotFoundError(f"{MODEL_FILENAME} not found in {path_hf_repo}")
82
+
83
+ options = ort.SessionOptions()
84
+ options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
85
+ options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
86
+ options.intra_op_num_threads = 2
87
+ options.inter_op_num_threads = 1
88
+
89
+ providers = ["CPUExecutionProvider"]
90
+ if "CUDAExecutionProvider" in ort.get_available_providers():
91
+ providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
92
+ try:
93
+ self.session = ort.InferenceSession(
94
+ str(model_path), sess_options=options, providers=providers
95
+ )
96
+ except Exception as exc:
97
+ print(f"CUDA session failed, falling back to CPU: {exc}")
98
+ self.session = ort.InferenceSession(
99
+ str(model_path), sess_options=options, providers=["CPUExecutionProvider"]
100
+ )
101
+
102
+ model_input = self.session.get_inputs()[0]
103
+ self.input_name = model_input.name
104
+ self.input_size = int(model_input.shape[2])
105
+ self.output_name = self.session.get_outputs()[0].name
106
+ self._check_class_order()
107
+
108
+ warmup = np.zeros((self.input_size, self.input_size, 3), dtype=np.uint8)
109
+ for _ in range(2):
110
+ self._infer_one(warmup)
111
+ print(f"Miner ready: providers={self.session.get_providers()} input={self.input_size}")
112
+
113
+ def __repr__(self) -> str:
114
+ return (
115
+ f"PPE YOLO26n ONNX input={self.input_size} "
116
+ f"providers={self.session.get_providers()}"
117
+ )
118
+
119
+ def _check_class_order(self) -> None:
120
+ names = self.session.get_modelmeta().custom_metadata_map.get("names", "")
121
+ for idx, name in enumerate(MANIFEST_CLASSES):
122
+ if f"{idx}: '{name}'" not in names:
123
+ raise ValueError(f"ONNX class order does not match manifest: {names}")
124
+
125
+ def _infer_one(self, image: ndarray) -> list[BoundingBox]:
126
+ canvas, ratio, pad_x, pad_y = _letterbox(image, self.input_size)
127
+ rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
128
+ tensor = np.ascontiguousarray(rgb.transpose(2, 0, 1)[None], dtype=np.float32) / 255.0
129
+ raw = self.session.run([self.output_name], {self.input_name: tensor})[0][0]
130
+
131
+ scores = raw[:, 4]
132
+ cls_ids = raw[:, 5].astype(np.int64)
133
+ known = (cls_ids >= 0) & (cls_ids < len(MANIFEST_CLASSES))
134
+ keep = known & (scores >= CLASS_CONF[np.clip(cls_ids, 0, len(CLASS_CONF) - 1)])
135
+ if not keep.any():
136
+ return []
137
+
138
+ boxes = raw[keep, :4].astype(np.float64)
139
+ boxes[:, [0, 2]] = (boxes[:, [0, 2]] - pad_x) / ratio
140
+ boxes[:, [1, 3]] = (boxes[:, [1, 3]] - pad_y) / ratio
141
+ height, width = image.shape[:2]
142
+ boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, width)
143
+ boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, height)
144
+
145
+ results: list[BoundingBox] = []
146
+ for (x1, y1, x2, y2), score, cls_id in zip(boxes, scores[keep], cls_ids[keep]):
147
+ x1, y1, x2, y2 = (int(round(v)) for v in (x1, y1, x2, y2))
148
+ if x2 <= x1 or y2 <= y1:
149
+ continue
150
+ results.append(
151
+ BoundingBox(
152
+ x1=x1, y1=y1, x2=x2, y2=y2, cls_id=int(cls_id), conf=float(score)
153
+ )
154
+ )
155
+ return results
156
+
157
+ def predict_batch(
158
+ self,
159
+ batch_images: list[ndarray],
160
+ offset: int,
161
+ n_keypoints: int,
162
+ ) -> list[TVFrameResult]:
163
+ results: list[TVFrameResult] = []
164
+ for index, image in enumerate(batch_images):
165
+ try:
166
+ boxes = self._infer_one(image)
167
+ except Exception as exc:
168
+ print(f"Inference failed on frame {offset + index}: {exc}")
169
+ boxes = []
170
+ results.append(
171
+ TVFrameResult(
172
+ frame_id=offset + index,
173
+ boxes=boxes,
174
+ polygons=[],
175
+ keypoints=[(0, 0)] * max(0, int(n_keypoints)),
176
+ )
177
+ )
178
+ return results