reneeice commited on
Commit
0dd89fc
·
verified ·
1 Parent(s): dc9b2db

Add files using upload-large-folder tool

Browse files
Files changed (4) hide show
  1. config.yml +21 -0
  2. keypointdetect.pt +3 -0
  3. miner.py +296 -0
  4. objdetect.pt +3 -0
config.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Image:
2
+ from_base: parachutes/python:3.12
3
+ run_command:
4
+ - pip install --upgrade setuptools wheel
5
+ - pip install ultralytics==8.3.222 torch==2.9.0 opencv-python-headless numpy pydantic
6
+ set_workdir: /app
7
+
8
+ NodeSelector:
9
+ gpu_count: 1
10
+ min_vram_gb_per_gpu: 16
11
+ exclude:
12
+ - "5090"
13
+ - b200
14
+ - h200
15
+ - mi300x
16
+
17
+ Chute:
18
+ timeout_seconds: 300
19
+ concurrency: 4
20
+ max_instances: 5
21
+ scaling_threshold: 0.5
keypointdetect.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6dd10dba85895c92760cdb5a99c5cfca899c68f361a66c5448f38a187280ee1f
3
+ size 6849672
miner.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from typing import List, Tuple, Dict, Optional
3
+
4
+ from ultralytics import YOLO
5
+ from numpy import ndarray
6
+ from pydantic import BaseModel
7
+ import numpy as np
8
+ import cv2
9
+
10
+
11
+ class BoundingBox(BaseModel):
12
+ x1: int
13
+ y1: int
14
+ x2: int
15
+ y2: int
16
+ cls_id: int
17
+ conf: float
18
+
19
+
20
+ class TVFrameResult(BaseModel):
21
+ frame_id: int
22
+ boxes: List[BoundingBox]
23
+ keypoints: List[Tuple[int, int]]
24
+
25
+
26
+ class Miner:
27
+ QUASI_TOTAL_IOA: float = 0.90
28
+ SMALL_CONTAINED_IOA: float = 0.85
29
+ SMALL_RATIO_MAX: float = 0.50
30
+ SINGLE_PLAYER_HUE_PIVOT: float = 90.0
31
+ CORNER_INDICES = {0, 5, 24, 29}
32
+
33
+ def __init__(self, path_hf_repo: Path) -> None:
34
+ self.bbox_model = YOLO(path_hf_repo / "objdetect.pt")
35
+ print("BBox Model (objdetect.pt) Loaded")
36
+ self.keypoints_model = YOLO(path_hf_repo / "keypointdetect.pt")
37
+ print("Keypoints Model (keypointdetect.pt) Loaded")
38
+
39
+ def __repr__(self) -> str:
40
+ return (
41
+ f"BBox Model: {type(self.bbox_model).__name__}\n"
42
+ f"Keypoints Model: {type(self.keypoints_model).__name__}"
43
+ )
44
+
45
+ @staticmethod
46
+ def _clip_box_to_image(x1: int, y1: int, x2: int, y2: int, w: int, h: int) -> Tuple[int, int, int, int]:
47
+ x1 = max(0, min(int(x1), w - 1))
48
+ y1 = max(0, min(int(y1), h - 1))
49
+ x2 = max(0, min(int(x2), w - 1))
50
+ y2 = max(0, min(int(y2), h - 1))
51
+ if x2 <= x1:
52
+ x2 = min(w - 1, x1 + 1)
53
+ if y2 <= y1:
54
+ y2 = min(h - 1, y1 + 1)
55
+ return x1, y1, x2, y2
56
+
57
+ @staticmethod
58
+ def _area(bb: BoundingBox) -> int:
59
+ return max(0, bb.x2 - bb.x1) * max(0, bb.y2 - bb.y1)
60
+
61
+ @staticmethod
62
+ def _intersect_area(a: BoundingBox, b: BoundingBox) -> int:
63
+ ix1 = max(a.x1, b.x1)
64
+ iy1 = max(a.y1, b.y1)
65
+ ix2 = min(a.x2, b.x2)
66
+ iy2 = min(a.y2, b.y2)
67
+ if ix2 <= ix1 or iy2 <= iy1:
68
+ return 0
69
+ return (ix2 - ix1) * (iy2 - iy1)
70
+
71
+ @staticmethod
72
+ def _center(bb: BoundingBox) -> Tuple[float, float]:
73
+ return (0.5 * (bb.x1 + bb.x2), 0.5 * (bb.y1 + bb.y2))
74
+
75
+ @staticmethod
76
+ def _mean_hs(img_bgr: np.ndarray) -> Tuple[float, float]:
77
+ hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
78
+ return float(np.mean(hsv[:, :, 0])), float(np.mean(hsv[:, :, 1]))
79
+
80
+ def _hs_feature_from_roi(self, img_bgr: np.ndarray, box: BoundingBox) -> np.ndarray:
81
+ H, W = img_bgr.shape[:2]
82
+ x1, y1, x2, y2 = self._clip_box_to_image(box.x1, box.y1, box.x2, box.y2, W, H)
83
+ roi = img_bgr[y1:y2, x1:x2]
84
+ if roi.size == 0:
85
+ return np.array([0.0, 0.0], dtype=np.float32)
86
+ hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
87
+ lower_green = np.array([35, 60, 60], dtype=np.uint8)
88
+ upper_green = np.array([85, 255, 255], dtype=np.uint8)
89
+ green_mask = cv2.inRange(hsv, lower_green, upper_green)
90
+ non_green_mask = cv2.bitwise_not(green_mask)
91
+ num_non_green = int(np.count_nonzero(non_green_mask))
92
+ total = hsv.shape[0] * hsv.shape[1]
93
+ if num_non_green > max(50, total // 20):
94
+ h_vals = hsv[:, :, 0][non_green_mask > 0]
95
+ s_vals = hsv[:, :, 1][non_green_mask > 0]
96
+ h_mean = float(np.mean(h_vals)) if h_vals.size else 0.0
97
+ s_mean = float(np.mean(s_vals)) if s_vals.size else 0.0
98
+ else:
99
+ h_mean, s_mean = self._mean_hs(roi)
100
+ return np.array([h_mean, s_mean], dtype=np.float32)
101
+
102
+ def _ioa(self, a: BoundingBox, b: BoundingBox) -> float:
103
+ inter = self._intersect_area(a, b)
104
+ aa = self._area(a)
105
+ if aa <= 0:
106
+ return 0.0
107
+ return inter / aa
108
+
109
+ def suppress_quasi_total_containment(self, boxes: List[BoundingBox]) -> List[BoundingBox]:
110
+ if len(boxes) <= 1:
111
+ return boxes
112
+ keep = [True] * len(boxes)
113
+ for i in range(len(boxes)):
114
+ if not keep[i]:
115
+ continue
116
+ for j in range(len(boxes)):
117
+ if i == j or not keep[j]:
118
+ continue
119
+ ioa_i_in_j = self._ioa(boxes[i], boxes[j])
120
+ if ioa_i_in_j >= self.QUASI_TOTAL_IOA:
121
+ keep[i] = False
122
+ break
123
+ return [bb for bb, k in zip(boxes, keep) if k]
124
+
125
+ def suppress_small_contained(self, boxes: List[BoundingBox]) -> List[BoundingBox]:
126
+ if len(boxes) <= 1:
127
+ return boxes
128
+ keep = [True] * len(boxes)
129
+ areas = [self._area(bb) for bb in boxes]
130
+ for i in range(len(boxes)):
131
+ if not keep[i]:
132
+ continue
133
+ for j in range(len(boxes)):
134
+ if i == j or not keep[j]:
135
+ continue
136
+ ai, aj = areas[i], areas[j]
137
+ if ai == 0 or aj == 0:
138
+ continue
139
+ if ai <= aj:
140
+ ratio = ai / aj
141
+ if ratio <= self.SMALL_RATIO_MAX:
142
+ ioa_i_in_j = self._ioa(boxes[i], boxes[j])
143
+ if ioa_i_in_j >= self.SMALL_CONTAINED_IOA:
144
+ keep[i] = False
145
+ break
146
+ else:
147
+ ratio = aj / ai
148
+ if ratio <= self.SMALL_RATIO_MAX:
149
+ ioa_j_in_i = self._ioa(boxes[j], boxes[i])
150
+ if ioa_j_in_i >= self.SMALL_CONTAINED_IOA:
151
+ keep[j] = False
152
+ return [bb for bb, k in zip(boxes, keep) if k]
153
+
154
+ def _assign_players_two_clusters(self, features: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
155
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
156
+ _, labels, centers = cv2.kmeans(
157
+ np.float32(features),
158
+ K=2,
159
+ bestLabels=None,
160
+ criteria=criteria,
161
+ attempts=5,
162
+ flags=cv2.KMEANS_PP_CENTERS,
163
+ )
164
+ return labels.reshape(-1), centers
165
+
166
+ def _reclass_extra_goalkeepers(
167
+ self,
168
+ img_bgr: np.ndarray,
169
+ boxes: List[BoundingBox],
170
+ cluster_centers: Optional[np.ndarray],
171
+ ) -> None:
172
+ gk_idxs = [i for i, bb in enumerate(boxes) if int(bb.cls_id) == 1]
173
+ if len(gk_idxs) <= 1:
174
+ return
175
+ gk_idxs_sorted = sorted(gk_idxs, key=lambda i: boxes[i].conf, reverse=True)
176
+ keep_gk_idx = gk_idxs_sorted[0]
177
+ to_reclass = gk_idxs_sorted[1:]
178
+ for gki in to_reclass:
179
+ hs_gk = self._hs_feature_from_roi(img_bgr, boxes[gki])
180
+ if cluster_centers is not None:
181
+ d0 = float(np.linalg.norm(hs_gk - cluster_centers[0]))
182
+ d1 = float(np.linalg.norm(hs_gk - cluster_centers[1]))
183
+ assign_cls = 6 if d0 <= d1 else 7
184
+ else:
185
+ assign_cls = 6 if float(hs_gk[0]) < self.SINGLE_PLAYER_HUE_PIVOT else 7
186
+ boxes[gki].cls_id = int(assign_cls)
187
+
188
+ def predict_batch(
189
+ self,
190
+ batch_images: List[ndarray],
191
+ offset: int,
192
+ n_keypoints: int,
193
+ task_type: Optional[str] = None,
194
+ ) -> List[TVFrameResult]:
195
+ process_objects = task_type is None or task_type == "object"
196
+ process_keypoints = task_type is None or task_type == "keypoint"
197
+ bboxes: Dict[int, List[BoundingBox]] = {}
198
+ if process_objects:
199
+ bbox_model_results = self.bbox_model.predict(batch_images)
200
+ else:
201
+ bbox_model_results = None
202
+ if bbox_model_results is not None:
203
+ for frame_idx_in_batch, detection in enumerate(bbox_model_results):
204
+ if not hasattr(detection, "boxes") or detection.boxes is None:
205
+ continue
206
+ boxes: List[BoundingBox] = []
207
+ for box in detection.boxes.data:
208
+ x1, y1, x2, y2, conf, cls_id = box.tolist()
209
+ boxes.append(
210
+ BoundingBox(
211
+ x1=int(x1),
212
+ y1=int(y1),
213
+ x2=int(x2),
214
+ y2=int(y2),
215
+ cls_id=int(cls_id),
216
+ conf=float(conf),
217
+ )
218
+ )
219
+ footballs = [bb for bb in boxes if int(bb.cls_id) == 0]
220
+ if len(footballs) > 1:
221
+ best_ball = max(footballs, key=lambda b: b.conf)
222
+ boxes = [bb for bb in boxes if int(bb.cls_id) != 0]
223
+ boxes.append(best_ball)
224
+ boxes = self.suppress_quasi_total_containment(boxes)
225
+ boxes = self.suppress_small_contained(boxes)
226
+ img_bgr = batch_images[frame_idx_in_batch]
227
+ player_indices: List[int] = []
228
+ player_feats: List[np.ndarray] = []
229
+ for i, bb in enumerate(boxes):
230
+ if int(bb.cls_id) == 2:
231
+ hs = self._hs_feature_from_roi(img_bgr, bb)
232
+ player_indices.append(i)
233
+ player_feats.append(hs)
234
+ cluster_centers: Optional[np.ndarray] = None
235
+ n_players = len(player_feats)
236
+ if n_players >= 2:
237
+ feats = np.vstack(player_feats)
238
+ labels, centers = self._assign_players_two_clusters(feats)
239
+ order = np.argsort(centers[:, 0])
240
+ centers = centers[order]
241
+ remap = {old_idx: new_idx for new_idx, old_idx in enumerate(order)}
242
+ labels = np.vectorize(remap.get)(labels)
243
+ cluster_centers = centers
244
+ for idx_in_list, lbl in zip(player_indices, labels):
245
+ boxes[idx_in_list].cls_id = 6 if int(lbl) == 0 else 7
246
+ elif n_players == 1:
247
+ hue, _ = player_feats[0]
248
+ boxes[player_indices[0]].cls_id = 6 if float(hue) < self.SINGLE_PLAYER_HUE_PIVOT else 7
249
+ self._reclass_extra_goalkeepers(img_bgr, boxes, cluster_centers)
250
+ bboxes[offset + frame_idx_in_batch] = boxes
251
+ keypoints: Dict[int, List[Tuple[int, int]]] = {}
252
+ if process_keypoints:
253
+ keypoints_model_results = self.keypoints_model.predict(batch_images)
254
+ else:
255
+ keypoints_model_results = None
256
+ if keypoints_model_results is not None:
257
+ for frame_idx_in_batch, detection in enumerate(keypoints_model_results):
258
+ if not hasattr(detection, "keypoints") or detection.keypoints is None:
259
+ continue
260
+ frame_keypoints_with_conf: List[Tuple[int, int, float]] = []
261
+ for i, part_points in enumerate(detection.keypoints.data):
262
+ for k_id, (x, y, _) in enumerate(part_points):
263
+ confidence = float(detection.keypoints.conf[i][k_id])
264
+ frame_keypoints_with_conf.append((int(x), int(y), confidence))
265
+ if len(frame_keypoints_with_conf) < n_keypoints:
266
+ frame_keypoints_with_conf.extend(
267
+ [(0, 0, 0.0)] * (n_keypoints - len(frame_keypoints_with_conf))
268
+ )
269
+ else:
270
+ frame_keypoints_with_conf = frame_keypoints_with_conf[:n_keypoints]
271
+ filtered_keypoints: List[Tuple[int, int]] = []
272
+ for idx, (x, y, confidence) in enumerate(frame_keypoints_with_conf):
273
+ if idx in self.CORNER_INDICES:
274
+ if confidence < 0.3:
275
+ filtered_keypoints.append((0, 0))
276
+ else:
277
+ filtered_keypoints.append((int(x), int(y)))
278
+ else:
279
+ if confidence < 0.5:
280
+ filtered_keypoints.append((0, 0))
281
+ else:
282
+ filtered_keypoints.append((int(x), int(y)))
283
+ keypoints[offset + frame_idx_in_batch] = filtered_keypoints
284
+ results: List[TVFrameResult] = []
285
+ for frame_number in range(offset, offset + len(batch_images)):
286
+ results.append(
287
+ TVFrameResult(
288
+ frame_id=frame_number,
289
+ boxes=bboxes.get(frame_number, []),
290
+ keypoints=keypoints.get(
291
+ frame_number,
292
+ [(0, 0) for _ in range(n_keypoints)],
293
+ ),
294
+ )
295
+ )
296
+ return results
objdetect.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8bbacfcb38e38b1b8816788e9e6e845160533719a0b87b693d58b932380d0d28
3
+ size 152961687