MTerryJack commited on
Commit
5a70fbe
·
verified ·
1 Parent(s): a5ae721

subnet_bridge: copy winning miner repo into library

Browse files
README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - element_type:detect
4
+ - model:yolov11-nano
5
+ - object:person
6
+ manako:
7
+ description: Roboflow - generated by element_trainer service to detect person
8
+ source: element_trainer/800e961b-eb64-4380-880c-f1ed67abd563
9
+ prompt_hints: null
10
+ input_payload:
11
+ - name: frame
12
+ type: image
13
+ description: RGB frame
14
+ output_payload:
15
+ - name: detections
16
+ type: detections
17
+ description: List of detections
18
+ evaluation_score: null
19
+ last_benchmark:
20
+ type: synthetic_fixed
21
+ ran_at: '2026-03-06T02:20:51.927289Z'
22
+ result_path: benchmark/synthetic/1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb.json
23
+ ---
benchmark/synthetic/1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb.json ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "meta": {
3
+ "element_id": "manak0/Detect-Person",
4
+ "run_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
5
+ "benchmark_type": "synthetic_fixed",
6
+ "saved_at": "2026-03-06T02:20:51.927289Z",
7
+ "result_path": "benchmark/synthetic/1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb.json",
8
+ "split_ref": "private_final"
9
+ },
10
+ "results": {
11
+ "overall_iou": 0.6932410264986374,
12
+ "per_class_iou": {
13
+ "person": 0.6932410264986374
14
+ },
15
+ "map_50": 0.3755112726134544,
16
+ "map_50_90": 0.21786430796282266,
17
+ "precision": 0.8950531668978271,
18
+ "recall": 0.3885989562424729,
19
+ "per_class_map_50": {
20
+ "person": 0.3755112726134544
21
+ },
22
+ "per_class_map_50_90": {
23
+ "person": 0.21786430796282266
24
+ },
25
+ "per_class_precision": {
26
+ "person": 0.8950531668978271
27
+ },
28
+ "per_class_recall": {
29
+ "person": 0.3885989562424729
30
+ },
31
+ "matched_images": 165,
32
+ "gt_count": 4982,
33
+ "pred_count": 2163,
34
+ "classes": [
35
+ "person"
36
+ ],
37
+ "dataset_id": "800e961b-eb64-4380-880c-f1ed67abd563",
38
+ "dataset_version_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
39
+ "run_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
40
+ "split_ref": "private_final",
41
+ "selected_frame_count": 165,
42
+ "selected_frame_indices": [
43
+ 1,
44
+ 6,
45
+ 9,
46
+ 12,
47
+ 22,
48
+ 25,
49
+ 26,
50
+ 27,
51
+ 28,
52
+ 32,
53
+ 38,
54
+ 40,
55
+ 42,
56
+ 50,
57
+ 51,
58
+ 52,
59
+ 53,
60
+ 54,
61
+ 55,
62
+ 57,
63
+ 60,
64
+ 67,
65
+ 77,
66
+ 80,
67
+ 81,
68
+ 88,
69
+ 96,
70
+ 97,
71
+ 98,
72
+ 101,
73
+ 104,
74
+ 105,
75
+ 114,
76
+ 115,
77
+ 120,
78
+ 122,
79
+ 128,
80
+ 129,
81
+ 131,
82
+ 136,
83
+ 137,
84
+ 141,
85
+ 145,
86
+ 146,
87
+ 148,
88
+ 152,
89
+ 155,
90
+ 156,
91
+ 159,
92
+ 161,
93
+ 163,
94
+ 165,
95
+ 166,
96
+ 171,
97
+ 172,
98
+ 174,
99
+ 176,
100
+ 186,
101
+ 189,
102
+ 190,
103
+ 196,
104
+ 199,
105
+ 200,
106
+ 201,
107
+ 202,
108
+ 213,
109
+ 214,
110
+ 218,
111
+ 226,
112
+ 234,
113
+ 235,
114
+ 236,
115
+ 238,
116
+ 239,
117
+ 243,
118
+ 245,
119
+ 251,
120
+ 255,
121
+ 259,
122
+ 260,
123
+ 268,
124
+ 270,
125
+ 274,
126
+ 275,
127
+ 278,
128
+ 279,
129
+ 283,
130
+ 287,
131
+ 289,
132
+ 291,
133
+ 300,
134
+ 301,
135
+ 303,
136
+ 307,
137
+ 310,
138
+ 311,
139
+ 318,
140
+ 319,
141
+ 321,
142
+ 322,
143
+ 326,
144
+ 329,
145
+ 330,
146
+ 332,
147
+ 333,
148
+ 335,
149
+ 336,
150
+ 341,
151
+ 342,
152
+ 343,
153
+ 344,
154
+ 346,
155
+ 348,
156
+ 354,
157
+ 357,
158
+ 358,
159
+ 360,
160
+ 362,
161
+ 367,
162
+ 372,
163
+ 373,
164
+ 376,
165
+ 378,
166
+ 379,
167
+ 381,
168
+ 384,
169
+ 385,
170
+ 395,
171
+ 398,
172
+ 399,
173
+ 401,
174
+ 417,
175
+ 418,
176
+ 428,
177
+ 431,
178
+ 432,
179
+ 433,
180
+ 434,
181
+ 438,
182
+ 440,
183
+ 442,
184
+ 443,
185
+ 444,
186
+ 446,
187
+ 447,
188
+ 455,
189
+ 458,
190
+ 459,
191
+ 465,
192
+ 468,
193
+ 469,
194
+ 470,
195
+ 475,
196
+ 476,
197
+ 480,
198
+ 483,
199
+ 489,
200
+ 492,
201
+ 493,
202
+ 494,
203
+ 495,
204
+ 496,
205
+ 498,
206
+ 499,
207
+ 500
208
+ ],
209
+ "evaluated_version_ids": [
210
+ "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb"
211
+ ],
212
+ "skipped_missing_pairs": 0
213
+ }
214
+ }
benchmark/synthetic/index.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "run_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
4
+ "saved_at": "2026-03-06T02:20:51.927289Z",
5
+ "result_path": "benchmark/synthetic/1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb.json",
6
+ "overall_iou": 0.6932410264986374,
7
+ "map_50": 0.3755112726134544,
8
+ "map_50_90": 0.21786430796282266,
9
+ "precision": 0.8950531668978271,
10
+ "recall": 0.3885989562424729,
11
+ "matched_images": 165,
12
+ "gt_count": 4982,
13
+ "pred_count": 2163,
14
+ "split_ref": "private_final"
15
+ }
16
+ ]
benchmark/synthetic/latest.json ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "meta": {
3
+ "element_id": "manak0/Detect-Person",
4
+ "run_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
5
+ "benchmark_type": "synthetic_fixed",
6
+ "saved_at": "2026-03-06T02:20:51.927289Z",
7
+ "result_path": "benchmark/synthetic/1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb.json",
8
+ "split_ref": "private_final"
9
+ },
10
+ "results": {
11
+ "overall_iou": 0.6932410264986374,
12
+ "per_class_iou": {
13
+ "person": 0.6932410264986374
14
+ },
15
+ "map_50": 0.3755112726134544,
16
+ "map_50_90": 0.21786430796282266,
17
+ "precision": 0.8950531668978271,
18
+ "recall": 0.3885989562424729,
19
+ "per_class_map_50": {
20
+ "person": 0.3755112726134544
21
+ },
22
+ "per_class_map_50_90": {
23
+ "person": 0.21786430796282266
24
+ },
25
+ "per_class_precision": {
26
+ "person": 0.8950531668978271
27
+ },
28
+ "per_class_recall": {
29
+ "person": 0.3885989562424729
30
+ },
31
+ "matched_images": 165,
32
+ "gt_count": 4982,
33
+ "pred_count": 2163,
34
+ "classes": [
35
+ "person"
36
+ ],
37
+ "dataset_id": "800e961b-eb64-4380-880c-f1ed67abd563",
38
+ "dataset_version_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
39
+ "run_id": "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb",
40
+ "split_ref": "private_final",
41
+ "selected_frame_count": 165,
42
+ "selected_frame_indices": [
43
+ 1,
44
+ 6,
45
+ 9,
46
+ 12,
47
+ 22,
48
+ 25,
49
+ 26,
50
+ 27,
51
+ 28,
52
+ 32,
53
+ 38,
54
+ 40,
55
+ 42,
56
+ 50,
57
+ 51,
58
+ 52,
59
+ 53,
60
+ 54,
61
+ 55,
62
+ 57,
63
+ 60,
64
+ 67,
65
+ 77,
66
+ 80,
67
+ 81,
68
+ 88,
69
+ 96,
70
+ 97,
71
+ 98,
72
+ 101,
73
+ 104,
74
+ 105,
75
+ 114,
76
+ 115,
77
+ 120,
78
+ 122,
79
+ 128,
80
+ 129,
81
+ 131,
82
+ 136,
83
+ 137,
84
+ 141,
85
+ 145,
86
+ 146,
87
+ 148,
88
+ 152,
89
+ 155,
90
+ 156,
91
+ 159,
92
+ 161,
93
+ 163,
94
+ 165,
95
+ 166,
96
+ 171,
97
+ 172,
98
+ 174,
99
+ 176,
100
+ 186,
101
+ 189,
102
+ 190,
103
+ 196,
104
+ 199,
105
+ 200,
106
+ 201,
107
+ 202,
108
+ 213,
109
+ 214,
110
+ 218,
111
+ 226,
112
+ 234,
113
+ 235,
114
+ 236,
115
+ 238,
116
+ 239,
117
+ 243,
118
+ 245,
119
+ 251,
120
+ 255,
121
+ 259,
122
+ 260,
123
+ 268,
124
+ 270,
125
+ 274,
126
+ 275,
127
+ 278,
128
+ 279,
129
+ 283,
130
+ 287,
131
+ 289,
132
+ 291,
133
+ 300,
134
+ 301,
135
+ 303,
136
+ 307,
137
+ 310,
138
+ 311,
139
+ 318,
140
+ 319,
141
+ 321,
142
+ 322,
143
+ 326,
144
+ 329,
145
+ 330,
146
+ 332,
147
+ 333,
148
+ 335,
149
+ 336,
150
+ 341,
151
+ 342,
152
+ 343,
153
+ 344,
154
+ 346,
155
+ 348,
156
+ 354,
157
+ 357,
158
+ 358,
159
+ 360,
160
+ 362,
161
+ 367,
162
+ 372,
163
+ 373,
164
+ 376,
165
+ 378,
166
+ 379,
167
+ 381,
168
+ 384,
169
+ 385,
170
+ 395,
171
+ 398,
172
+ 399,
173
+ 401,
174
+ 417,
175
+ 418,
176
+ 428,
177
+ 431,
178
+ 432,
179
+ 433,
180
+ 434,
181
+ 438,
182
+ 440,
183
+ 442,
184
+ 443,
185
+ 444,
186
+ 446,
187
+ 447,
188
+ 455,
189
+ 458,
190
+ 459,
191
+ 465,
192
+ 468,
193
+ 469,
194
+ 470,
195
+ 475,
196
+ 476,
197
+ 480,
198
+ 483,
199
+ 489,
200
+ 492,
201
+ 493,
202
+ 494,
203
+ 495,
204
+ 496,
205
+ 498,
206
+ 499,
207
+ 500
208
+ ],
209
+ "evaluated_version_ids": [
210
+ "1ada5b1e-38b8-4bdc-967a-d8a27b0e6afb"
211
+ ],
212
+ "skipped_missing_pairs": 0
213
+ }
214
+ }
chute_config.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>=1.16' 'opencv-python>=4.7' 'pillow>=9.5' 'huggingface_hub>=0.19.4' 'pydantic>=2.0' 'pyyaml>=6.0' 'aiohttp>=3.9' 'torch<2.6'
6
+
7
+ NodeSelector:
8
+ gpu_count: 1
9
+ min_vram_gb_per_gpu: 24
10
+ min_memory_gb: 32
11
+ min_cpu_count: 32
12
+
13
+ Chute:
14
+ timeout_seconds: 900
15
+ concurrency: 4
16
+ max_instances: 5
17
+ scaling_threshold: 0.5
18
+ shutdown_after_seconds: 288000
class_names.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ person
main.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import cv2
10
+
11
+ from miner import Miner
12
+
13
+
14
+ CLASS_NAMES = ['person']
15
+ MODEL_TYPE = 'ultralytics-yolo'
16
+
17
+
18
+ def _to_dict(value: Any) -> dict[str, Any]:
19
+ if isinstance(value, dict):
20
+ return value
21
+ if hasattr(value, "model_dump") and callable(value.model_dump):
22
+ dumped = value.model_dump()
23
+ if isinstance(dumped, dict):
24
+ return dumped
25
+ if hasattr(value, "__dict__"):
26
+ return dict(value.__dict__)
27
+ return {}
28
+
29
+
30
+ def _extract_boxes(frame_result: Any) -> list[Any]:
31
+ frame = _to_dict(frame_result)
32
+ boxes = frame.get("boxes", [])
33
+ if isinstance(boxes, list):
34
+ return boxes
35
+ return []
36
+
37
+
38
+ def _to_detection(box: Any) -> dict[str, Any]:
39
+ payload = _to_dict(box)
40
+ cls_id = int(payload.get("cls_id", 0))
41
+ x1 = float(payload.get("x1", 0.0))
42
+ y1 = float(payload.get("y1", 0.0))
43
+ x2 = float(payload.get("x2", 0.0))
44
+ y2 = float(payload.get("y2", 0.0))
45
+ width = max(0.0, x2 - x1)
46
+ height = max(0.0, y2 - y1)
47
+ return {
48
+ "x": x1 + width / 2.0,
49
+ "y": y1 + height / 2.0,
50
+ "width": width,
51
+ "height": height,
52
+ "confidence": float(payload.get("conf", 0.0)),
53
+ "class_id": cls_id,
54
+ "class": CLASS_NAMES[cls_id] if 0 <= cls_id < len(CLASS_NAMES) else str(cls_id),
55
+ }
56
+
57
+
58
+ def load_model(onnx_path: str | None = None, data_dir: str | None = None):
59
+ del onnx_path
60
+ repo_dir = Path(data_dir) if data_dir else Path(__file__).resolve().parent
61
+ miner = Miner(repo_dir)
62
+ return {
63
+ "miner": miner,
64
+ "model_type": MODEL_TYPE,
65
+ "class_names": CLASS_NAMES,
66
+ }
67
+
68
+
69
+ def _candidate_keypoint_counts(miner: Any) -> list[int]:
70
+ counts: list[int] = [0]
71
+ for attr in ("n_keypoints", "num_keypoints", "keypoint_count", "num_joints"):
72
+ value = getattr(miner, attr, None)
73
+ if isinstance(value, int) and value > 0:
74
+ counts.append(value)
75
+ counts.append(32)
76
+
77
+ seen: set[int] = set()
78
+ ordered: list[int] = []
79
+ for count in counts:
80
+ if count in seen:
81
+ continue
82
+ seen.add(count)
83
+ ordered.append(count)
84
+ return ordered
85
+
86
+
87
+ def _predict_batch_with_fallbacks(miner: Any, image: Any) -> list[Any]:
88
+ errors: list[str] = []
89
+ for n_keypoints in _candidate_keypoint_counts(miner):
90
+ try:
91
+ return miner.predict_batch([image], offset=0, n_keypoints=n_keypoints)
92
+ except Exception as exc:
93
+ errors.append(f"n_keypoints={n_keypoints} -> {exc}")
94
+ continue
95
+ raise RuntimeError("predict_batch failed for all keypoint candidates: " + " | ".join(errors))
96
+
97
+
98
+ def run_model(model: Any, image: Any = None, onnx_path: str | None = None, data_dir: str | None = None):
99
+ del onnx_path
100
+ if image is None:
101
+ image = model
102
+ model = load_model(data_dir=data_dir)
103
+ miner = model["miner"]
104
+ results = _predict_batch_with_fallbacks(miner, image)
105
+ if not results:
106
+ return [[]]
107
+ frame_boxes = _extract_boxes(results[0])
108
+ detections = [_to_detection(box) for box in frame_boxes]
109
+ return [detections]
110
+
111
+
112
+ def main() -> None:
113
+ if len(sys.argv) < 2:
114
+ print("Usage: main.py <image_path>", file=sys.stderr)
115
+ raise SystemExit(1)
116
+ image_path = sys.argv[1]
117
+ image = cv2.imread(image_path, cv2.IMREAD_COLOR)
118
+ if image is None:
119
+ print(f"Could not read image: {image_path}", file=sys.stderr)
120
+ raise SystemExit(1)
121
+ data_dir = os.path.dirname(os.path.abspath(__file__))
122
+ model = load_model(data_dir=data_dir)
123
+ output = run_model(model, image)
124
+ print(json.dumps(output, indent=2))
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
miner.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import math
3
+
4
+ import cv2
5
+ import numpy as np
6
+ import onnxruntime as ort
7
+ from numpy import ndarray
8
+ from pydantic import BaseModel
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
+ """
28
+ Auto-generated by subnet_bridge from a Manako element repo.
29
+ This miner is intentionally self-contained for chute import restrictions.
30
+ """
31
+
32
+ def __init__(self, path_hf_repo: Path) -> None:
33
+ self.path_hf_repo = path_hf_repo
34
+ self.class_names = ['person']
35
+ self.session = ort.InferenceSession(
36
+ str(path_hf_repo / "weights.onnx"),
37
+ providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
38
+ )
39
+ self.input_name = self.session.get_inputs()[0].name
40
+ input_shape = self.session.get_inputs()[0].shape
41
+ # expected [N, C, H, W]
42
+ self.input_h = int(input_shape[2])
43
+ self.input_w = int(input_shape[3])
44
+ self.conf_threshold = 0.25
45
+ self.iou_threshold = 0.45
46
+
47
+ def __repr__(self) -> str:
48
+ return f"ONNX Miner session={type(self.session).__name__} classes={len(self.class_names)}"
49
+
50
+ def _preprocess(self, image_bgr: ndarray) -> tuple[np.ndarray, tuple[int, int]]:
51
+ h, w = image_bgr.shape[:2]
52
+ rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
53
+ resized = cv2.resize(rgb, (self.input_w, self.input_h))
54
+ x = resized.astype(np.float32) / 255.0
55
+ x = np.transpose(x, (2, 0, 1))[None, ...]
56
+ return x, (h, w)
57
+
58
+ def _normalize_predictions(self, raw: np.ndarray) -> np.ndarray:
59
+ # Common ultralytics export shapes:
60
+ # - [1, C, N] where C=4+num_classes
61
+ # - [1, N, C]
62
+ pred = raw[0]
63
+ if pred.ndim != 2:
64
+ raise ValueError(f"Unexpected prediction shape: {raw.shape}")
65
+ if pred.shape[0] < pred.shape[1]:
66
+ pred = pred.transpose(1, 0)
67
+ return pred
68
+
69
+ def _nms(self, dets: list[tuple[float, float, float, float, float, int]]) -> list[tuple[float, float, float, float, float, int]]:
70
+ if not dets:
71
+ return []
72
+
73
+ boxes = np.array([[d[0], d[1], d[2], d[3]] for d in dets], dtype=np.float32)
74
+ scores = np.array([d[4] for d in dets], dtype=np.float32)
75
+ order = scores.argsort()[::-1]
76
+ keep = []
77
+
78
+ while order.size > 0:
79
+ i = order[0]
80
+ keep.append(i)
81
+
82
+ xx1 = np.maximum(boxes[i, 0], boxes[order[1:], 0])
83
+ yy1 = np.maximum(boxes[i, 1], boxes[order[1:], 1])
84
+ xx2 = np.minimum(boxes[i, 2], boxes[order[1:], 2])
85
+ yy2 = np.minimum(boxes[i, 3], boxes[order[1:], 3])
86
+
87
+ w = np.maximum(0.0, xx2 - xx1)
88
+ h = np.maximum(0.0, yy2 - yy1)
89
+ inter = w * h
90
+
91
+ area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1])
92
+ area_rest = (boxes[order[1:], 2] - boxes[order[1:], 0]) * (boxes[order[1:], 3] - boxes[order[1:], 1])
93
+ union = np.maximum(area_i + area_rest - inter, 1e-6)
94
+ iou = inter / union
95
+
96
+ remaining = np.where(iou <= self.iou_threshold)[0]
97
+ order = order[remaining + 1]
98
+
99
+ return [dets[idx] for idx in keep]
100
+
101
+ def _infer_single(self, image_bgr: ndarray) -> list[BoundingBox]:
102
+ inp, (orig_h, orig_w) = self._preprocess(image_bgr)
103
+ out = self.session.run(None, {self.input_name: inp})[0]
104
+ pred = self._normalize_predictions(out)
105
+
106
+ if pred.shape[1] < 5:
107
+ return []
108
+
109
+ boxes = pred[:, :4]
110
+ cls_scores = pred[:, 4:]
111
+
112
+ if cls_scores.shape[1] == 0:
113
+ return []
114
+
115
+ cls_ids = np.argmax(cls_scores, axis=1)
116
+ confs = np.max(cls_scores, axis=1)
117
+ keep = confs >= self.conf_threshold
118
+
119
+ boxes = boxes[keep]
120
+ confs = confs[keep]
121
+ cls_ids = cls_ids[keep]
122
+
123
+ if boxes.shape[0] == 0:
124
+ return []
125
+
126
+ sx = orig_w / float(self.input_w)
127
+ sy = orig_h / float(self.input_h)
128
+
129
+ dets: list[tuple[float, float, float, float, float, int]] = []
130
+ for i in range(boxes.shape[0]):
131
+ cx, cy, bw, bh = boxes[i].tolist()
132
+ x1 = (cx - bw / 2.0) * sx
133
+ y1 = (cy - bh / 2.0) * sy
134
+ x2 = (cx + bw / 2.0) * sx
135
+ y2 = (cy + bh / 2.0) * sy
136
+ dets.append((x1, y1, x2, y2, float(confs[i]), int(cls_ids[i])))
137
+
138
+ dets = self._nms(dets)
139
+
140
+ out_boxes: list[BoundingBox] = []
141
+ for x1, y1, x2, y2, conf, cls_id in dets:
142
+ ix1 = max(0, min(orig_w, math.floor(x1)))
143
+ iy1 = max(0, min(orig_h, math.floor(y1)))
144
+ ix2 = max(0, min(orig_w, math.ceil(x2)))
145
+ iy2 = max(0, min(orig_h, math.ceil(y2)))
146
+ out_boxes.append(
147
+ BoundingBox(
148
+ x1=ix1,
149
+ y1=iy1,
150
+ x2=ix2,
151
+ y2=iy2,
152
+ cls_id=cls_id,
153
+ conf=max(0.0, min(1.0, conf)),
154
+ )
155
+ )
156
+ return out_boxes
157
+
158
+ def predict_batch(
159
+ self,
160
+ batch_images: list[ndarray],
161
+ offset: int,
162
+ n_keypoints: int,
163
+ ) -> list[TVFrameResult]:
164
+ results: list[TVFrameResult] = []
165
+ for idx, image in enumerate(batch_images):
166
+ boxes = self._infer_single(image)
167
+ keypoints = [(0, 0) for _ in range(max(0, int(n_keypoints)))]
168
+ results.append(
169
+ TVFrameResult(
170
+ frame_id=offset + idx,
171
+ boxes=boxes,
172
+ keypoints=keypoints,
173
+ )
174
+ )
175
+ return results
model_type.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "task_type": "object-detection",
3
+ "model_type": "yolov11-nano"
4
+ }
pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "miner-element-adapter"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.9"
5
+
6
+ dependencies = [
7
+ "numpy>=1.23",
8
+ "onnxruntime>=1.16",
9
+ "opencv-python>=4.7",
10
+ "pillow>=9.5",
11
+ "huggingface_hub>=0.19.4",
12
+ "pydantic>=2.0",
13
+ "pyyaml>=6.0",
14
+ "aiohttp>=3.9",
15
+ "torch<2.6",
16
+ ]
test_miner.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for Detect-Person miner.
4
+ Loads images from a folder, runs predict_batch, and saves result images with bounding boxes.
5
+ """
6
+ from pathlib import Path
7
+ import argparse
8
+ import sys
9
+
10
+ import cv2
11
+ import numpy as np
12
+
13
+ # Add parent dir so we can import miner
14
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+ from miner import Miner
16
+
17
+
18
+ IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
19
+ BOX_COLOR = (0, 255, 0) # Green (BGR)
20
+ LABEL_COLOR = (0, 255, 0) # Green
21
+ FONT_SCALE = 0.6
22
+ FONT_THICKNESS = 2
23
+ BOX_THICKNESS = 2
24
+
25
+
26
+ def list_images(folder: Path) -> list[Path]:
27
+ """Collect image paths from folder, sorted by name."""
28
+ paths = [p for p in folder.iterdir() if p.suffix.lower() in IMAGE_EXTENSIONS]
29
+ return sorted(paths)
30
+
31
+
32
+ def load_images(paths: list[Path]) -> tuple[list[np.ndarray], list[Path]]:
33
+ """Load images as BGR numpy arrays. Returns (images, successful_paths)."""
34
+ images = []
35
+ successful_paths = []
36
+ for p in paths:
37
+ img = cv2.imread(str(p))
38
+ if img is None:
39
+ print(f"Warning: Could not load {p}", file=sys.stderr)
40
+ continue
41
+ images.append(img)
42
+ successful_paths.append(p)
43
+ return images, successful_paths
44
+
45
+
46
+ def draw_detections(
47
+ image: np.ndarray,
48
+ boxes: list,
49
+ class_names: list[str],
50
+ ) -> np.ndarray:
51
+ """Draw bounding boxes and labels on image. Returns a copy."""
52
+ out = image.copy()
53
+ for box in boxes:
54
+ x1, y1 = box.x1, box.y1
55
+ x2, y2 = box.x2, box.y2
56
+ cls_name = class_names[box.cls_id] if box.cls_id < len(class_names) else str(box.cls_id)
57
+ label = f"{cls_name} {box.conf:.2f}"
58
+ cv2.rectangle(out, (x1, y1), (x2, y2), BOX_COLOR, BOX_THICKNESS)
59
+ (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, FONT_SCALE, FONT_THICKNESS)
60
+ cv2.rectangle(out, (x1, y1 - th - 8), (x1 + tw + 4, y1), BOX_COLOR, -1)
61
+ cv2.putText(
62
+ out, label,
63
+ (x1 + 2, y1 - 4),
64
+ cv2.FONT_HERSHEY_SIMPLEX,
65
+ FONT_SCALE,
66
+ (0, 0, 0),
67
+ FONT_THICKNESS,
68
+ )
69
+ return out
70
+
71
+
72
+ def main() -> None:
73
+ parser = argparse.ArgumentParser(
74
+ description="Test Detect-Person miner: run detection on images and save visualized results.",
75
+ )
76
+ parser.add_argument(
77
+ "images_dir",
78
+ type=Path,
79
+ help="Folder containing images to process",
80
+ )
81
+ parser.add_argument(
82
+ "--model-dir",
83
+ type=Path,
84
+ default=None,
85
+ help="Path to model directory (contains weights.onnx). Default: same as script",
86
+ )
87
+ parser.add_argument(
88
+ "--output",
89
+ "-o",
90
+ type=Path,
91
+ default=None,
92
+ help="Output folder for result images. Default: images_dir/results",
93
+ )
94
+ parser.add_argument(
95
+ "--batch-size",
96
+ type=int,
97
+ default=8,
98
+ help="Batch size for predict_batch (default: 8)",
99
+ )
100
+ parser.add_argument(
101
+ "--show",
102
+ action="store_true",
103
+ help="Display each result image in a window (requires display)",
104
+ )
105
+ args = parser.parse_args()
106
+
107
+ images_dir = args.images_dir
108
+ if not images_dir.is_dir():
109
+ print(f"Error: Not a directory: {images_dir}", file=sys.stderr)
110
+ sys.exit(1)
111
+
112
+ image_paths = list_images(images_dir)
113
+ if not image_paths:
114
+ print(f"Error: No images found in {images_dir}", file=sys.stderr)
115
+ sys.exit(1)
116
+
117
+ model_dir = args.model_dir or Path(__file__).resolve().parent
118
+ if not (model_dir / "weights.onnx").exists():
119
+ print(f"Error: weights.onnx not found in {model_dir}", file=sys.stderr)
120
+ sys.exit(1)
121
+
122
+ output_dir = args.output or (images_dir / "results")
123
+ output_dir.mkdir(parents=True, exist_ok=True)
124
+ print(f"Output: {output_dir}")
125
+
126
+ print("Loading Miner...")
127
+ miner = Miner(model_dir)
128
+ print(f" {miner}")
129
+
130
+ images, valid_paths = load_images(image_paths)
131
+ if len(images) != len(image_paths):
132
+ print(f"Loaded {len(images)} / {len(image_paths)} images (some failed)", file=sys.stderr)
133
+
134
+ total_detections = 0
135
+ for i in range(0, len(images), args.batch_size):
136
+ batch = images[i : i + args.batch_size]
137
+ batch_paths = valid_paths[i : i + args.batch_size]
138
+ offset = i
139
+ results = miner.predict_batch(batch, offset=offset, n_keypoints=0)
140
+
141
+ for j, (result, img_path) in enumerate(zip(results, batch_paths)):
142
+ vis = draw_detections(batch[j], result.boxes, miner.class_names)
143
+ total_detections += len(result.boxes)
144
+ out_path = output_dir / f"{img_path.stem}_det{img_path.suffix}"
145
+ cv2.imwrite(str(out_path), vis)
146
+ print(f" {img_path.name} -> {out_path.name} ({len(result.boxes)} detections)")
147
+
148
+ if args.show:
149
+ cv2.imshow("Detect-Person Result", vis)
150
+ key = cv2.waitKey(0)
151
+ if key == ord("q"):
152
+ args.show = False
153
+
154
+ if args.show:
155
+ cv2.destroyAllWindows()
156
+
157
+ print(f"\nDone. Total detections: {total_detections}. Results saved to {output_dir}")
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()
weights.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ca5959e96dc41038edd0fef0d0d3858acb9887e12f684f47d61ef45c108ca76b
3
+ size 22410189