gloriforge commited on
Commit
187dd75
Β·
verified Β·
1 Parent(s): de651d3

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +91 -91
  2. config.yml +14 -26
  3. miner.py +247 -206
  4. objdetect.pt +3 -0
  5. player.pt +3 -0
README.md CHANGED
@@ -1,92 +1,92 @@
1
- # πŸš€ Example Chute for Turbovision πŸͺ‚
2
-
3
- This repository demonstrates how to deploy a **Chute** via the **Turbovision CLI**, hosted on **Hugging Face Hub**.
4
- It serves as a minimal example showcasing the required structure and workflow for integrating machine learning models, preprocessing, and orchestration into a reproducible Chute environment.
5
-
6
- ## Repository Structure
7
- The following two files **must be present** (in their current locations) for a successful deployment β€” their content can be modified as needed:
8
-
9
- | File | Purpose |
10
- |------|----------|
11
- | `miner.py` | Defines the ML model type(s), orchestration, and all pre/postprocessing logic. |
12
- | `config.yml` | Specifies machine configuration (e.g., GPU type, memory, environment variables). |
13
-
14
- Other files β€” e.g., model weights, utility scripts, or dependencies β€” are **optional** and can be included as needed for your model. Note: Any required assets must be defined or contained **within this repo**, which is fully open-source, since all network-related operations (downloading challenge data, weights, etc.) are disabled **inside the Chute**
15
-
16
- ## Overview
17
-
18
- Below is a high-level diagram showing the interaction between Huggingface, Chutes and Turbovision:
19
-
20
- ![](../images/miner.png)
21
-
22
- ## Local Testing
23
- After editing the `config.yml` and `miner.py` and saving it into your Huggingface Repo, you will want to test it works locally.
24
-
25
- 1. Copy the file `scorevision/chute_tmeplate/turbovision_chute.py.j2` as a python file called `my_chute.py` and fill in the missing variables:
26
- ```python
27
- HF_REPO_NAME = "{{ huggingface_repository_name }}"
28
- HF_REPO_REVISION = "{{ huggingface_repository_revision }}"
29
- CHUTES_USERNAME = "{{ chute_username }}"
30
- CHUTE_NAME = "{{ chute_name }}"
31
- ```
32
-
33
- 2. Run the following command to build the chute locally (Caution: there are known issues with the docker location when running this on a mac)
34
- ```bash
35
- chutes build my_chute:chute --local --public
36
- ```
37
-
38
- 3. Run the name of the docker image just built (i.e. `CHUTE_NAME`) and enter it
39
- ```bash
40
- docker run -p 8000:8000 -e CHUTES_EXECUTION_CONTEXT=REMOTE -it <image-name> /bin/bash
41
- ```
42
-
43
- 4. Run the file from within the container
44
- ```bash
45
- chutes run my_chute:chute --dev --debug
46
- ```
47
-
48
- 5. In another terminal, test the local endpoints to ensure there are no bugs
49
- ```bash
50
- curl -X POST http://localhost:8000/health -d '{}'
51
- curl -X POST http://localhost:8000/predict -d '{"url": "https://scoredata.me/2025_03_14/35ae7a/h1_0f2ca0.mp4","meta": {}}'
52
- ```
53
-
54
- ## Live Testing
55
- 1. If you have any chute with the same name (ie from a previous deployment), ensure you delete that first (or you will get an error when trying to build).
56
- ```bash
57
- chutes chutes list
58
- ```
59
- Take note of the chute id that you wish to delete (if any)
60
- ```bash
61
- chutes chutes delete <chute-id>
62
- ```
63
-
64
- You should also delete its associated image
65
- ```bash
66
- chutes images list
67
- ```
68
- Take note of the chute image id
69
- ```bash
70
- chutes images delete <chute-image-id>
71
- ```
72
-
73
- 2. Use Turbovision's CLI to build, deploy and commit on-chain (Note: you can skip the on-chain commit using `--no-commit`. You can also specify a past huggingface revision to point to using `--revision` and/or the local files you want to upload to your huggingface repo using `--model-path`)
74
- ```bash
75
- sv -vv push
76
- ```
77
-
78
- 3. When completed, warm up the chute (if its cold 🧊). (You can confirm its status using `chutes chutes list` or `chutes chutes get <chute-id>` if you already know its id). Note: Warming up can sometimes take a while but if the chute runs without errors (should be if you've tested locally first) and there are sufficient nodes (i.e. machines) available matching the `config.yml` you specified, the chute should become hot πŸ”₯!
79
- ```bash
80
- chutes warmup <chute-id>
81
- ```
82
-
83
- 4. Test the chute's endpoints
84
- ```bash
85
- curl -X POST https://<YOUR-CHUTE-SLUG>.chutes.ai/health -d '{}' -H "Authorization: Bearer $CHUTES_API_KEY"
86
- curl -X POST https://<YOUR-CHUTE-SLUG>.chutes.ai/predict -d '{"url": "https://scoredata.me/2025_03_14/35ae7a/h1_0f2ca0.mp4","meta": {}}' -H "Authorization: Bearer $CHUTES_API_KEY"
87
- ```
88
-
89
- 5. Test what your chute would get on a validator (this also applies any validation/integrity checks which may fail if you did not use the Turbovision CLI above to deploy the chute)
90
- ```bash
91
- sv -vv run-once
92
  ```
 
1
+ # πŸš€ Example Chute for Turbovision πŸͺ‚
2
+
3
+ This repository demonstrates how to deploy a **Chute** via the **Turbovision CLI**, hosted on **Hugging Face Hub**.
4
+ It serves as a minimal example showcasing the required structure and workflow for integrating machine learning models, preprocessing, and orchestration into a reproducible Chute environment.
5
+
6
+ ## Repository Structure
7
+ The following two files **must be present** (in their current locations) for a successful deployment β€” their content can be modified as needed:
8
+
9
+ | File | Purpose |
10
+ |------|----------|
11
+ | `miner.py` | Defines the ML model type(s), orchestration, and all pre/postprocessing logic. |
12
+ | `config.yml` | Specifies machine configuration (e.g., GPU type, memory, environment variables). |
13
+
14
+ Other files β€” e.g., model weights, utility scripts, or dependencies β€” are **optional** and can be included as needed for your model. Note: Any required assets must be defined or contained **within this repo**, which is fully open-source, since all network-related operations (downloading challenge data, weights, etc.) are disabled **inside the Chute**
15
+
16
+ ## Overview
17
+
18
+ Below is a high-level diagram showing the interaction between Huggingface, Chutes and Turbovision:
19
+
20
+ ![](../images/miner.png)
21
+
22
+ ## Local Testing
23
+ After editing the `config.yml` and `miner.py` and saving it into your Huggingface Repo, you will want to test it works locally.
24
+
25
+ 1. Copy the file `scorevision/chute_tmeplate/turbovision_chute.py.j2` as a python file called `my_chute.py` and fill in the missing variables:
26
+ ```python
27
+ HF_REPO_NAME = "{{ huggingface_repository_name }}"
28
+ HF_REPO_REVISION = "{{ huggingface_repository_revision }}"
29
+ CHUTES_USERNAME = "{{ chute_username }}"
30
+ CHUTE_NAME = "{{ chute_name }}"
31
+ ```
32
+
33
+ 2. Run the following command to build the chute locally (Caution: there are known issues with the docker location when running this on a mac)
34
+ ```bash
35
+ chutes build my_chute:chute --local --public
36
+ ```
37
+
38
+ 3. Run the name of the docker image just built (i.e. `CHUTE_NAME`) and enter it
39
+ ```bash
40
+ docker run -p 8000:8000 -e CHUTES_EXECUTION_CONTEXT=REMOTE -it <image-name> /bin/bash
41
+ ```
42
+
43
+ 4. Run the file from within the container
44
+ ```bash
45
+ chutes run my_chute:chute --dev --debug
46
+ ```
47
+
48
+ 5. In another terminal, test the local endpoints to ensure there are no bugs
49
+ ```bash
50
+ curl -X POST http://localhost:8000/health -d '{}'
51
+ curl -X POST http://localhost:8000/predict -d '{"url": "https://scoredata.me/2025_03_14/35ae7a/h1_0f2ca0.mp4","meta": {}}'
52
+ ```
53
+
54
+ ## Live Testing
55
+ 1. If you have any chute with the same name (ie from a previous deployment), ensure you delete that first (or you will get an error when trying to build).
56
+ ```bash
57
+ chutes chutes list
58
+ ```
59
+ Take note of the chute id that you wish to delete (if any)
60
+ ```bash
61
+ chutes chutes delete <chute-id>
62
+ ```
63
+
64
+ You should also delete its associated image
65
+ ```bash
66
+ chutes images list
67
+ ```
68
+ Take note of the chute image id
69
+ ```bash
70
+ chutes images delete <chute-image-id>
71
+ ```
72
+
73
+ 2. Use Turbovision's CLI to build, deploy and commit on-chain (Note: you can skip the on-chain commit using `--no-commit`. You can also specify a past huggingface revision to point to using `--revision` and/or the local files you want to upload to your huggingface repo using `--model-path`)
74
+ ```bash
75
+ sv -vv push
76
+ ```
77
+
78
+ 3. When completed, warm up the chute (if its cold 🧊). (You can confirm its status using `chutes chutes list` or `chutes chutes get <chute-id>` if you already know its id). Note: Warming up can sometimes take a while but if the chute runs without errors (should be if you've tested locally first) and there are sufficient nodes (i.e. machines) available matching the `config.yml` you specified, the chute should become hot πŸ”₯!
79
+ ```bash
80
+ chutes warmup <chute-id>
81
+ ```
82
+
83
+ 4. Test the chute's endpoints
84
+ ```bash
85
+ curl -X POST https://<YOUR-CHUTE-SLUG>.chutes.ai/health -d '{}' -H "Authorization: Bearer $CHUTES_API_KEY"
86
+ curl -X POST https://<YOUR-CHUTE-SLUG>.chutes.ai/predict -d '{"url": "https://scoredata.me/2025_03_14/35ae7a/h1_0f2ca0.mp4","meta": {}}' -H "Authorization: Bearer $CHUTES_API_KEY"
87
+ ```
88
+
89
+ 5. Test what your chute would get on a validator (this also applies any validation/integrity checks which may fail if you did not use the Turbovision CLI above to deploy the chute)
90
+ ```bash
91
+ sv -vv run-once
92
  ```
config.yml CHANGED
@@ -2,40 +2,28 @@ Image:
2
  from_base: parachutes/python:3.12
3
  run_command:
4
  - pip install --upgrade setuptools wheel
5
- - pip install huggingface_hub==0.19.4 requests opencv-python-headless pydantic onnxruntime onnxruntime-gpu scikit-learn tensorflow torch-tensorrt==2.7 torch==2.7.1 torchvision==0.22.1 pyyaml
6
-
7
  set_workdir: /app
8
 
 
9
  NodeSelector:
10
  gpu_count: 1
11
- min_vram_gb_per_gpu: 24
12
- # include:
13
- # - a100
14
- # - a100_40gb
15
- # - "3090"
16
- # - a40
17
- # - a6000
18
- exclude:
19
- - h100
20
  - a100
 
 
 
21
  - l40s
22
- - mi300x
23
  - b200
24
  - h200
25
  - h20
26
- - h800
27
- - h100_sxm
28
- - h100_nvl
29
- - a100_sxm
30
- - a100_40gb_sxm
31
- - a100_40gb
32
- - l40
33
- - pro_6000
34
- - a6000_ada
35
- - '5090'
36
 
37
  Chute:
38
- timeout_seconds: 300
39
- concurrency: 4 # Reduced concurrency to limit memory usage
40
- max_instances: 5 # Reduced max instances to limit memory usage
41
- scaling_threshold: 0.5 # Higher threshold to reduce scaling frequency
 
2
  from_base: parachutes/python:3.12
3
  run_command:
4
  - pip install --upgrade setuptools wheel
5
+ - pip install "ultralytics==8.3.222" "opencv-python-headless" "numpy" "pydantic"
6
+ - pip install "tensorflow" "torch==2.7.1" "torchvision==0.22.1" "torch-tensorrt==2.7"
7
  set_workdir: /app
8
 
9
+
10
  NodeSelector:
11
  gpu_count: 1
12
+ min_vram_gb_per_gpu: 16
13
+ include:
 
 
 
 
 
 
 
14
  - a100
15
+ - "3090"
16
+ - a40
17
+ - a6000
18
  - l40s
19
+ exclude:
20
  - b200
21
  - h200
22
  - h20
23
+ - mi300x
 
 
 
 
 
 
 
 
 
24
 
25
  Chute:
26
+ timeout_seconds: 900
27
+ concurrency: 4
28
+ max_instances: 5
29
+ scaling_threshold: 0.5
miner.py CHANGED
@@ -1,47 +1,43 @@
1
  from pathlib import Path
 
 
 
2
 
3
  from numpy import ndarray
4
  import numpy as np
5
  from pydantic import BaseModel
6
- import sys, os
 
7
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
8
 
9
- os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
10
  os.environ["OMP_NUM_THREADS"] = "16"
11
  os.environ["TF_NUM_INTRAOP_THREADS"] = "16"
12
  os.environ["TF_NUM_INTEROP_THREADS"] = "2"
13
- os.environ['CUDA_LAUNCH_BLOCKING'] = '0'
14
- # Suppress ONNX Runtime warnings
15
- os.environ['ORT_LOGGING_LEVEL'] = '3'
16
 
17
  import logging
18
- logging.getLogger('tensorflow').setLevel(logging.ERROR)
19
-
20
  import tensorflow as tf
 
 
 
 
 
 
 
 
 
21
  tf.config.threading.set_intra_op_parallelism_threads(16)
22
  tf.config.threading.set_inter_op_parallelism_threads(2)
23
- os.environ['TF_ENABLE_ONEDNN_OPTS'] = '0'
24
  tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
25
- tf.get_logger().setLevel('ERROR')
26
  tf.autograph.set_verbosity(0)
27
-
28
- from tensorflow.keras import mixed_precision
29
- mixed_precision.set_global_policy('mixed_float16')
30
  tf.config.optimizer.set_jit(True)
31
-
32
- import torch._dynamo
33
  torch._dynamo.config.suppress_errors = True
34
- import onnxruntime as ort
35
- import gc
36
-
37
- import torch
38
- import torch_tensorrt
39
- import torchvision.transforms as T
40
- import yaml
41
- import cv2
42
 
43
- from player import player_detection_result
44
- from pitch import process_batch_input, get_cls_net, get_cls_net_l
45
 
46
  class BoundingBox(BaseModel):
47
  x1: int
@@ -54,57 +50,21 @@ class BoundingBox(BaseModel):
54
 
55
  class TVFrameResult(BaseModel):
56
  frame_id: int
57
- boxes: list[BoundingBox]
58
- keypoints: list[tuple[int, int]]
59
 
60
- class Miner:
61
- """
62
- This class is responsible for:
63
- - Loading ML models.
64
- - Running batched predictions on images.
65
- - Parsing ML model outputs into structured results (TVFrameResult).
66
 
67
- This class can be modified, but it must have the following to be compatible with the chute:
68
- - be named `Miner`
69
- - have a `predict_batch` function with the inputs and outputs specified
70
- - be stored in a file called `miner.py` which lives in the root of the HFHub repo
71
- """
72
 
73
  def __init__(self, path_hf_repo: Path) -> None:
74
- """
75
- Loads all ML models from the repository.
76
- -----(Adjust as needed)----
77
-
78
- Args:
79
- path_hf_repo (Path):
80
- Path to the downloaded HuggingFace Hub repository
81
-
82
- Returns:
83
- None
84
- """
85
- global torch
86
- device = 'cuda' if torch.cuda.is_available() else 'cpu'
87
-
88
- providers = [
89
- 'CUDAExecutionProvider',
90
- 'CPUExecutionProvider'
91
-
92
- ]
93
- # providers = [ 'CPUExecutionProvider']
94
- model_path = path_hf_repo / "object-detection.onnx"
95
- session = ort.InferenceSession(model_path, providers=providers)
96
- input_name = session.get_inputs()[0].name
97
- height = width = 640
98
- dummy = np.zeros((1, 3, height, width), dtype=np.float32)
99
- session.run(None, {input_name: dummy})
100
- model = session
101
- self.bbox_model = model
102
- print(f"βœ… BBox Model Loaded")
103
-
104
- self.kp_threshold = 0.1
105
- # self.lp_threshold = 0.7
106
-
107
- model_kp_path = path_hf_repo / 'SV_kp.engine'
108
  model_kp = torch_tensorrt.load(model_kp_path)
109
 
110
  @torch.inference_mode()
@@ -114,114 +74,214 @@ class Miner:
114
  return output
115
 
116
  run_inference(model_kp, torch.randn(8, 3, 540, 960, device=device, dtype=torch.float32))
117
- # model_kp_path = path_hf_repo / 'SV_kp'
118
- # model_lp_path = path_hf_repo / 'SV_lines'
119
- # config_kp_path = path_hf_repo / 'hrnetv2_w48.yaml'
120
- # config_lp_path = path_hf_repo / 'hrnetv2_w48_l.yaml'
121
- # cfg_kp = yaml.safe_load(open(config_kp_path, 'r'))
122
- # cfg_lp = yaml.safe_load(open(config_lp_path, 'r'))
123
-
124
- # loaded_state_kp = torch.load(model_kp_path, map_location=device)
125
- # model_kp = get_cls_net(cfg_kp)
126
- # model_kp.load_state_dict(loaded_state_kp)
127
- # model_kp.to(device)
128
- # model_kp.eval()
129
-
130
- # loaded_state_lp = torch.load(model_lp_path, map_location=device)
131
- # model_lp = get_cls_net_l(cfg_lp)
132
- # model_lp.load_state_dict(loaded_state_lp)
133
- # model_lp.to(device)
134
- # model_lp.eval()
135
-
136
- # self.transform = T.Resize((540, 960))
137
-
138
  self.keypoints_model = model_kp
139
- # self.lines_model = model_lp
140
-
141
- # print("πŸ”₯ Warming up compiled models...")
142
- # self._warmup_models(device)
143
-
144
- # Increase batch sizes for better GPU utilization
145
- self.player_batch_size = 16 # Increased from 32
146
- self.pitch_batch_size = 8 # Increased from 32
147
- print(f"βœ… Keypoints Model Loaded")
148
 
149
  def __repr__(self) -> str:
150
- return f"BBox Model: {type(self.bbox_model).__name__}\nKeypoints Model: {type(self.keypoints_model).__name__}"
151
-
152
- def predict_batch(
153
- self,
154
- batch_images: list[ndarray],
155
- offset: int,
156
- n_keypoints: int,
157
- ) -> list[TVFrameResult]:
158
- player_batch_size = min(self.player_batch_size, len(batch_images))
159
- bboxes: dict[int, list[BoundingBox]] = {}
160
- while True:
161
- try:
162
- gc.collect()
163
- if torch.cuda.is_available():
164
- tf.keras.backend.clear_session()
165
- torch.cuda.empty_cache()
166
- torch.cuda.synchronize()
167
-
168
- bbox_model_results, _, _, _ = player_detection_result(batch_images, player_batch_size, self.bbox_model)
169
- if bbox_model_results is not None and len(bbox_model_results) > 0:
170
- for frame_number_in_batch, detections in enumerate(bbox_model_results):
171
- # Ensure frame_number_in_batch is within batch_images bounds
172
- if frame_number_in_batch >= len(batch_images):
173
- print(f"⚠️ Warning: bbox_model_results has more frames ({len(bbox_model_results)}) than batch_images ({len(batch_images)}). Skipping extra frames.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  break
175
-
176
- boxes = []
177
- if detections is not None and isinstance(detections, (list, tuple)):
178
- for detection in detections:
179
- try:
180
- # Detection format from player.py: {"id": int, "bbox": [x1, y1, x2, y2], "class_id": int}
181
- if isinstance(detection, dict):
182
- x1, y1, x2, y2 = detection.get("bbox", [0, 0, 0, 0])
183
- cls_id = detection.get("class_id", 0)
184
- conf = detection.get("conf", 0.0)
185
- else:
186
- # Handle tuple/array format: (box, score, cls)
187
- if len(detection) >= 3:
188
- x1, y1, x2, y2 = detection[0] if hasattr(detection[0], '__iter__') else [0, 0, 0, 0]
189
- conf = detection[1] if len(detection) > 1 else 0.0
190
- cls_id = detection[2] if len(detection) > 2 else 0
191
- else:
192
- continue
193
-
194
- boxes.append(
195
- BoundingBox(
196
- x1=int(x1),
197
- y1=int(y1),
198
- x2=int(x2),
199
- y2=int(y2),
200
- cls_id=int(cls_id),
201
- conf=float(conf),
202
- )
203
- )
204
- except (KeyError, TypeError, ValueError, IndexError) as det_err:
205
- print(f"⚠️ Warning: Could not parse detection: {det_err}")
206
- continue
207
- bboxes[offset + frame_number_in_batch] = boxes
208
- print("βœ… BBoxes predicted")
209
- break
210
- except RuntimeError as e:
211
- print(self.player_batch_size)
212
- if 'out of memory' in str(e):
213
- if self.player_batch_size == 1:
214
- break
215
- self.player_batch_size = self.player_batch_size // 2 if self.player_batch_size > 1 else 1
216
- player_batch_size = min(self.player_batch_size, len(batch_images))
217
  else:
218
- break
219
- except Exception as e:
220
- print(f"❌ Error during bbox prediction: {e}")
221
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  pitch_batch_size = min(self.pitch_batch_size, len(batch_images))
224
- keypoints: dict[int, list[tuple[int, int]]] = {}
225
  while True:
226
  try:
227
  gc.collect()
@@ -229,28 +289,21 @@ class Miner:
229
  tf.keras.backend.clear_session()
230
  torch.cuda.empty_cache()
231
  torch.cuda.synchronize()
232
- # Removed expensive memory clearing operations for speed
233
  keypoints_result = process_batch_input(
234
  batch_images,
235
  self.keypoints_model,
236
  self.kp_threshold,
237
- 'cuda' if torch.cuda.is_available() else 'cpu',
238
- batch_size=pitch_batch_size
239
  )
240
-
241
  if keypoints_result is not None and len(keypoints_result) > 0:
242
  for frame_number_in_batch, kp_dict in enumerate(keypoints_result):
243
- # Ensure frame_number_in_batch is within batch_images bounds
244
  if frame_number_in_batch >= len(batch_images):
245
- print(f"⚠️ Warning: keypoints_result has more frames ({len(keypoints_result)}) than batch_images ({len(batch_images)}). Skipping extra frames.")
246
  break
247
-
248
- frame_keypoints: list[tuple[int, int]] = []
249
-
250
- # Get image dimensions for conversion from normalized to pixel coordinates
251
  try:
252
  height, width = batch_images[frame_number_in_batch].shape[:2]
253
-
254
  if kp_dict is not None and isinstance(kp_dict, dict):
255
  for idx in range(32):
256
  x, y = 0, 0
@@ -258,31 +311,24 @@ class Miner:
258
  if kp_idx in kp_dict:
259
  try:
260
  kp_data = kp_dict[kp_idx]
261
- if isinstance(kp_data, dict) and 'x' in kp_data and 'y' in kp_data:
262
- # Convert normalized coordinates to pixel coordinates
263
- x = int(kp_data['x'] * width)
264
- y = int(kp_data['y'] * height)
265
- except (KeyError, TypeError, ValueError) as kp_err:
266
- print(f"⚠️ Warning: Could not parse keypoint {kp_idx}: {kp_err}")
267
  frame_keypoints.append((x, y))
268
- except (IndexError, ValueError, AttributeError) as img_err:
269
- print(f"⚠️ Warning: Could not process frame {frame_number_in_batch}: {img_err}")
270
- # Create default keypoints if processing fails
271
  frame_keypoints = [(0, 0)] * 32
272
-
273
- # Pad or truncate to match expected number of keypoints
274
  if len(frame_keypoints) < n_keypoints:
275
  frame_keypoints.extend([(0, 0)] * (n_keypoints - len(frame_keypoints)))
276
  else:
277
  frame_keypoints = frame_keypoints[:n_keypoints]
278
-
279
  keypoints[offset + frame_number_in_batch] = frame_keypoints
280
-
281
  print("βœ… Keypoints predicted")
282
  break
283
  except RuntimeError as e:
284
  print(self.pitch_batch_size)
285
- if 'out of memory' in str(e):
286
  if self.pitch_batch_size == 1:
287
  break
288
  self.pitch_batch_size = self.pitch_batch_size // 2 if self.pitch_batch_size > 1 else 1
@@ -293,13 +339,10 @@ class Miner:
293
  print(f"❌ Error during keypoints prediction: {e}")
294
  break
295
 
296
- # Combine results
297
- results: list[TVFrameResult] = []
298
- for i, frame_number in enumerate(range(offset, offset + len(batch_images))):
299
  frame_boxes = bboxes.get(frame_number, [])
300
  frame_keypoints = keypoints.get(frame_number, [(0, 0) for _ in range(n_keypoints)])
301
-
302
- # Create result object
303
  result = TVFrameResult(
304
  frame_id=frame_number,
305
  boxes=frame_boxes,
@@ -307,12 +350,10 @@ class Miner:
307
  )
308
  results.append(result)
309
 
310
- print("βœ… Combined results as TVFrameResult")
311
-
312
  gc.collect()
313
  if torch.cuda.is_available():
314
  tf.keras.backend.clear_session()
315
  torch.cuda.empty_cache()
316
  torch.cuda.synchronize()
317
-
318
- return results
 
1
  from pathlib import Path
2
+ from typing import List, Tuple, Dict
3
+ import sys
4
+ import os
5
 
6
  from numpy import ndarray
7
  import numpy as np
8
  from pydantic import BaseModel
9
+ import cv2
10
+
11
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
12
 
13
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
14
  os.environ["OMP_NUM_THREADS"] = "16"
15
  os.environ["TF_NUM_INTRAOP_THREADS"] = "16"
16
  os.environ["TF_NUM_INTEROP_THREADS"] = "2"
17
+ os.environ["CUDA_LAUNCH_BLOCKING"] = "0"
18
+ os.environ["ORT_LOGGING_LEVEL"] = "3"
19
+ os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
20
 
21
  import logging
 
 
22
  import tensorflow as tf
23
+ from tensorflow.keras import mixed_precision
24
+ import torch._dynamo
25
+ import torch
26
+ import torch_tensorrt
27
+ import gc
28
+ from ultralytics import YOLO
29
+ from pitch import process_batch_input
30
+
31
+ logging.getLogger("tensorflow").setLevel(logging.ERROR)
32
  tf.config.threading.set_intra_op_parallelism_threads(16)
33
  tf.config.threading.set_inter_op_parallelism_threads(2)
 
34
  tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR)
35
+ tf.get_logger().setLevel("ERROR")
36
  tf.autograph.set_verbosity(0)
37
+ mixed_precision.set_global_policy("mixed_float16")
 
 
38
  tf.config.optimizer.set_jit(True)
 
 
39
  torch._dynamo.config.suppress_errors = True
 
 
 
 
 
 
 
 
40
 
 
 
41
 
42
  class BoundingBox(BaseModel):
43
  x1: int
 
50
 
51
  class TVFrameResult(BaseModel):
52
  frame_id: int
53
+ boxes: List[BoundingBox]
54
+ keypoints: List[Tuple[int, int]]
55
 
 
 
 
 
 
 
56
 
57
+ class Miner:
58
+ QUASI_TOTAL_IOA: float = 0.90
59
+ SMALL_CONTAINED_IOA: float = 0.85
60
+ SMALL_RATIO_MAX: float = 0.50
61
+ SINGLE_PLAYER_HUE_PIVOT: float = 90.0
62
 
63
  def __init__(self, path_hf_repo: Path) -> None:
64
+ self.bbox_model = YOLO(path_hf_repo / "player.pt")
65
+ print(" BBox Model (objdetect.pt) Loaded")
66
+ device = "cuda" if torch.cuda.is_available() else "cpu"
67
+ model_kp_path = path_hf_repo / "SV_kp.engine"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  model_kp = torch_tensorrt.load(model_kp_path)
69
 
70
  @torch.inference_mode()
 
74
  return output
75
 
76
  run_inference(model_kp, torch.randn(8, 3, 540, 960, device=device, dtype=torch.float32))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  self.keypoints_model = model_kp
78
+ self.kp_threshold = 0.1
79
+ self.pitch_batch_size = 8
80
+ print("βœ… Keypoints Model Loaded")
 
 
 
 
 
 
81
 
82
  def __repr__(self) -> str:
83
+ return (
84
+ f"BBox Model: {type(self.bbox_model).__name__}\n"
85
+ f"Keypoints Model: {type(self.keypoints_model).__name__}"
86
+ )
87
+
88
+ @staticmethod
89
+ def _clip_box_to_image(x1: int, y1: int, x2: int, y2: int, w: int, h: int) -> Tuple[int, int, int, int]:
90
+ x1 = max(0, min(int(x1), w - 1))
91
+ y1 = max(0, min(int(y1), h - 1))
92
+ x2 = max(0, min(int(x2), w - 1))
93
+ y2 = max(0, min(int(y2), h - 1))
94
+ if x2 <= x1:
95
+ x2 = min(w - 1, x1 + 1)
96
+ if y2 <= y1:
97
+ y2 = min(h - 1, y1 + 1)
98
+ return x1, y1, x2, y2
99
+
100
+ @staticmethod
101
+ def _area(bb: BoundingBox) -> int:
102
+ return max(0, bb.x2 - bb.x1) * max(0, bb.y2 - bb.y1)
103
+
104
+ @staticmethod
105
+ def _intersect_area(a: BoundingBox, b: BoundingBox) -> int:
106
+ ix1 = max(a.x1, b.x1)
107
+ iy1 = max(a.y1, b.y1)
108
+ ix2 = min(a.x2, b.x2)
109
+ iy2 = min(a.y2, b.y2)
110
+ if ix2 <= ix1 or iy2 <= iy1:
111
+ return 0
112
+ return (ix2 - ix1) * (iy2 - iy1)
113
+
114
+ @staticmethod
115
+ def _center(bb: BoundingBox) -> Tuple[float, float]:
116
+ return (0.5 * (bb.x1 + bb.x2), 0.5 * (bb.y1 + bb.y2))
117
+
118
+ @staticmethod
119
+ def _mean_hs(img_bgr: np.ndarray) -> Tuple[float, float]:
120
+ hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
121
+ return float(np.mean(hsv[:, :, 0])), float(np.mean(hsv[:, :, 1]))
122
+
123
+ def _hs_feature_from_roi(self, img_bgr: np.ndarray, box: BoundingBox) -> np.ndarray:
124
+ H, W = img_bgr.shape[:2]
125
+ x1, y1, x2, y2 = self._clip_box_to_image(box.x1, box.y1, box.x2, box.y2, W, H)
126
+ roi = img_bgr[y1:y2, x1:x2]
127
+ if roi.size == 0:
128
+ return np.array([0.0, 0.0], dtype=np.float32)
129
+ hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
130
+ lower_green = np.array([35, 60, 60], dtype=np.uint8)
131
+ upper_green = np.array([85, 255, 255], dtype=np.uint8)
132
+ green_mask = cv2.inRange(hsv, lower_green, upper_green)
133
+ non_green_mask = cv2.bitwise_not(green_mask)
134
+ num_non_green = int(np.count_nonzero(non_green_mask))
135
+ total = hsv.shape[0] * hsv.shape[1]
136
+ if num_non_green > max(50, total // 20):
137
+ h_vals = hsv[:, :, 0][non_green_mask > 0]
138
+ s_vals = hsv[:, :, 1][non_green_mask > 0]
139
+ h_mean = float(np.mean(h_vals)) if h_vals.size else 0.0
140
+ s_mean = float(np.mean(s_vals)) if s_vals.size else 0.0
141
+ else:
142
+ h_mean, s_mean = self._mean_hs(roi)
143
+ return np.array([h_mean, s_mean], dtype=np.float32)
144
+
145
+ def _ioa(self, a: BoundingBox, b: BoundingBox) -> float:
146
+ inter = self._intersect_area(a, b)
147
+ aa = self._area(a)
148
+ if aa <= 0:
149
+ return 0.0
150
+ return inter / aa
151
+
152
+ def suppress_quasi_total_containment(self, boxes: List[BoundingBox]) -> List[BoundingBox]:
153
+ if len(boxes) <= 1:
154
+ return boxes
155
+ keep = [True] * len(boxes)
156
+ for i in range(len(boxes)):
157
+ if not keep[i]:
158
+ continue
159
+ for j in range(len(boxes)):
160
+ if i == j or not keep[j]:
161
+ continue
162
+ ioa_i_in_j = self._ioa(boxes[i], boxes[j])
163
+ if ioa_i_in_j >= self.QUASI_TOTAL_IOA:
164
+ keep[i] = False
165
+ break
166
+ return [bb for bb, k in zip(boxes, keep) if k]
167
+
168
+ def suppress_small_contained(self, boxes: List[BoundingBox]) -> List[BoundingBox]:
169
+ if len(boxes) <= 1:
170
+ return boxes
171
+ keep = [True] * len(boxes)
172
+ areas = [self._area(bb) for bb in boxes]
173
+ for i in range(len(boxes)):
174
+ if not keep[i]:
175
+ continue
176
+ for j in range(len(boxes)):
177
+ if i == j or not keep[j]:
178
+ continue
179
+ ai, aj = areas[i], areas[j]
180
+ if ai == 0 or aj == 0:
181
+ continue
182
+ if ai <= aj:
183
+ ratio = ai / aj
184
+ if ratio <= self.SMALL_RATIO_MAX:
185
+ ioa_i_in_j = self._ioa(boxes[i], boxes[j])
186
+ if ioa_i_in_j >= self.SMALL_CONTAINED_IOA:
187
+ keep[i] = False
188
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  else:
190
+ ratio = aj / ai
191
+ if ratio <= self.SMALL_RATIO_MAX:
192
+ ioa_j_in_i = self._ioa(boxes[j], boxes[i])
193
+ if ioa_j_in_i >= self.SMALL_CONTAINED_IOA:
194
+ keep[j] = False
195
+ return [bb for bb, k in zip(boxes, keep) if k]
196
+
197
+ def _assign_players_two_clusters(self, features: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
198
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
199
+ _, labels, centers = cv2.kmeans(
200
+ np.float32(features),
201
+ K=2,
202
+ bestLabels=None,
203
+ criteria=criteria,
204
+ attempts=5,
205
+ flags=cv2.KMEANS_PP_CENTERS,
206
+ )
207
+ return labels.reshape(-1), centers
208
+
209
+ def _reclass_extra_goalkeepers(self, img_bgr: np.ndarray, boxes: List[BoundingBox], cluster_centers: np.ndarray | None) -> None:
210
+ gk_idxs = [i for i, bb in enumerate(boxes) if int(bb.cls_id) == 1]
211
+ if len(gk_idxs) <= 1:
212
+ return
213
+ gk_idxs_sorted = sorted(gk_idxs, key=lambda i: boxes[i].conf, reverse=True)
214
+ keep_gk_idx = gk_idxs_sorted[0]
215
+ to_reclass = gk_idxs_sorted[1:]
216
+ for gki in to_reclass:
217
+ hs_gk = self._hs_feature_from_roi(img_bgr, boxes[gki])
218
+ if cluster_centers is not None:
219
+ d0 = float(np.linalg.norm(hs_gk - cluster_centers[0]))
220
+ d1 = float(np.linalg.norm(hs_gk - cluster_centers[1]))
221
+ assign_cls = 6 if d0 <= d1 else 7
222
+ else:
223
+ assign_cls = 6 if float(hs_gk[0]) < self.SINGLE_PLAYER_HUE_PIVOT else 7
224
+ boxes[gki].cls_id = int(assign_cls)
225
+
226
+ def predict_batch(self, batch_images: List[ndarray], offset: int, n_keypoints: int) -> List[TVFrameResult]:
227
+ bboxes: Dict[int, List[BoundingBox]] = {}
228
+ bbox_model_results = self.bbox_model.predict(batch_images)
229
+ if bbox_model_results is not None:
230
+ for frame_idx_in_batch, detection in enumerate(bbox_model_results):
231
+ if not hasattr(detection, "boxes") or detection.boxes is None:
232
+ continue
233
+ boxes: List[BoundingBox] = []
234
+ for box in detection.boxes.data:
235
+ x1, y1, x2, y2, conf, cls_id = box.tolist()
236
+ if cls_id == 3:
237
+ cls_id = 2
238
+ elif cls_id == 2:
239
+ cls_id = 3
240
+ boxes.append(
241
+ BoundingBox(
242
+ x1=int(x1),
243
+ y1=int(y1),
244
+ x2=int(x2),
245
+ y2=int(y2),
246
+ cls_id=int(cls_id),
247
+ conf=float(conf),
248
+ )
249
+ )
250
+ footballs = [bb for bb in boxes if int(bb.cls_id) == 0]
251
+ if len(footballs) > 1:
252
+ best_ball = max(footballs, key=lambda b: b.conf)
253
+ boxes = [bb for bb in boxes if int(bb.cls_id) != 0]
254
+ boxes.append(best_ball)
255
+ boxes = self.suppress_quasi_total_containment(boxes)
256
+ boxes = self.suppress_small_contained(boxes)
257
+ img_bgr = batch_images[frame_idx_in_batch]
258
+ player_indices: List[int] = []
259
+ player_feats: List[np.ndarray] = []
260
+ for i, bb in enumerate(boxes):
261
+ if int(bb.cls_id) == 2:
262
+ hs = self._hs_feature_from_roi(img_bgr, bb)
263
+ player_indices.append(i)
264
+ player_feats.append(hs)
265
+ cluster_centers = None
266
+ n_players = len(player_feats)
267
+ if n_players >= 2:
268
+ feats = np.vstack(player_feats)
269
+ labels, centers = self._assign_players_two_clusters(feats)
270
+ order = np.argsort(centers[:, 0])
271
+ centers = centers[order]
272
+ remap = {old_idx: new_idx for new_idx, old_idx in enumerate(order)}
273
+ labels = np.vectorize(remap.get)(labels)
274
+ cluster_centers = centers
275
+ for idx_in_list, lbl in zip(player_indices, labels):
276
+ boxes[idx_in_list].cls_id = 6 if int(lbl) == 0 else 7
277
+ elif n_players == 1:
278
+ hue, _ = player_feats[0]
279
+ boxes[player_indices[0]].cls_id = 6 if float(hue) < self.SINGLE_PLAYER_HUE_PIVOT else 7
280
+ self._reclass_extra_goalkeepers(img_bgr, boxes, cluster_centers)
281
+ bboxes[offset + frame_idx_in_batch] = boxes
282
 
283
  pitch_batch_size = min(self.pitch_batch_size, len(batch_images))
284
+ keypoints: Dict[int, List[Tuple[int, int]]] = {}
285
  while True:
286
  try:
287
  gc.collect()
 
289
  tf.keras.backend.clear_session()
290
  torch.cuda.empty_cache()
291
  torch.cuda.synchronize()
292
+ device_str = "cuda" if torch.cuda.is_available() else "cpu"
293
  keypoints_result = process_batch_input(
294
  batch_images,
295
  self.keypoints_model,
296
  self.kp_threshold,
297
+ device_str,
298
+ batch_size=pitch_batch_size,
299
  )
 
300
  if keypoints_result is not None and len(keypoints_result) > 0:
301
  for frame_number_in_batch, kp_dict in enumerate(keypoints_result):
 
302
  if frame_number_in_batch >= len(batch_images):
 
303
  break
304
+ frame_keypoints: List[Tuple[int, int]] = []
 
 
 
305
  try:
306
  height, width = batch_images[frame_number_in_batch].shape[:2]
 
307
  if kp_dict is not None and isinstance(kp_dict, dict):
308
  for idx in range(32):
309
  x, y = 0, 0
 
311
  if kp_idx in kp_dict:
312
  try:
313
  kp_data = kp_dict[kp_idx]
314
+ if isinstance(kp_data, dict) and "x" in kp_data and "y" in kp_data:
315
+ x = int(kp_data["x"] * width)
316
+ y = int(kp_data["y"] * height)
317
+ except (KeyError, TypeError, ValueError):
318
+ pass
 
319
  frame_keypoints.append((x, y))
320
+ except (IndexError, ValueError, AttributeError):
 
 
321
  frame_keypoints = [(0, 0)] * 32
 
 
322
  if len(frame_keypoints) < n_keypoints:
323
  frame_keypoints.extend([(0, 0)] * (n_keypoints - len(frame_keypoints)))
324
  else:
325
  frame_keypoints = frame_keypoints[:n_keypoints]
 
326
  keypoints[offset + frame_number_in_batch] = frame_keypoints
 
327
  print("βœ… Keypoints predicted")
328
  break
329
  except RuntimeError as e:
330
  print(self.pitch_batch_size)
331
+ if "out of memory" in str(e):
332
  if self.pitch_batch_size == 1:
333
  break
334
  self.pitch_batch_size = self.pitch_batch_size // 2 if self.pitch_batch_size > 1 else 1
 
339
  print(f"❌ Error during keypoints prediction: {e}")
340
  break
341
 
342
+ results: List[TVFrameResult] = []
343
+ for frame_number in range(offset, offset + len(batch_images)):
 
344
  frame_boxes = bboxes.get(frame_number, [])
345
  frame_keypoints = keypoints.get(frame_number, [(0, 0) for _ in range(n_keypoints)])
 
 
346
  result = TVFrameResult(
347
  frame_id=frame_number,
348
  boxes=frame_boxes,
 
350
  )
351
  results.append(result)
352
 
 
 
353
  gc.collect()
354
  if torch.cuda.is_available():
355
  tf.keras.backend.clear_session()
356
  torch.cuda.empty_cache()
357
  torch.cuda.synchronize()
358
+
359
+ 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
player.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ce9fc31f61e6f156f786077abb8eef36b0836bda1ef07d1d0ba82d43ae0ecd0b
3
+ size 22540152