detect-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
detect/__init__.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ detect
3
+ ======
4
+ detect/
5
+ __init__.py
6
+ core/
7
+ schema.py # Detection types + det-v1 helpers
8
+ run.py # detect_video core logic (no CLI)
9
+ artifacts.py # artifact saving control, result object
10
+ viz.py # drawing helpers (optional dependency on cv2)
11
+ backends/
12
+ __init__.py # plugin registry
13
+ ultralytics/
14
+ __init__.py
15
+ detectors.py # bbox/pose/seg wrappers
16
+ export.py # ultralytics exporter adapter
17
+ registry.json # ultralytics-specific model keys (optional)
18
+ registry/
19
+ registry.py # merge registries + resolve_weights_path
20
+ default.json # your current registry (or split by backend)
21
+ cli/
22
+ detect_video.py # argparse → calls core.run.detect_video()
23
+ export_model.py # argparse → calls backend exporter
24
+
25
+ A modular object-detection framework centered around a stable JSON output schema
26
+ (det-v1), with pluggable model backends (Ultralytics YOLO today; others later).
27
+
28
+ Public API (stable-ish):
29
+ - Detection schema types and helpers (core.schema)
30
+ - Detection runner (core.run.detect_video)
31
+ - Artifact control + result object (core.artifacts)
32
+ - Backend registry + detector factory (backends)
33
+ """
34
+
35
+ from .core.schema import (
36
+ SCHEMA_VERSION,
37
+ Detection,
38
+ FrameRecord,
39
+ VideoMeta,
40
+ DetectorConfig,
41
+ BaseDetector,
42
+ select_device,
43
+ parse_classes,
44
+ frame_file_name,
45
+ )
46
+
47
+ from .core.artifacts import (
48
+ ArtifactOptions,
49
+ DetectResult,
50
+ )
51
+
52
+ from .core.run import (
53
+ detect_video,
54
+ )
55
+
56
+ from .backends import (
57
+ create_detector,
58
+ available_detectors,
59
+ available_models,
60
+ )
61
+
62
+ __all__ = [
63
+ # Schema / types
64
+ "SCHEMA_VERSION",
65
+ "Detection",
66
+ "FrameRecord",
67
+ "VideoMeta",
68
+ "DetectorConfig",
69
+ "BaseDetector",
70
+ "select_device",
71
+ "parse_classes",
72
+ "frame_file_name",
73
+ # Running + outputs
74
+ "ArtifactOptions",
75
+ "DetectResult",
76
+ "detect_video",
77
+ # Backends
78
+ "create_detector",
79
+ "available_detectors",
80
+ "available_models",
81
+ ]
@@ -0,0 +1,141 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ detect.backends
5
+ ---------------
6
+
7
+ Backend/plugin registry.
8
+
9
+ Today we ship an Ultralytics backend with three detectors:
10
+ - yolo_bbox
11
+ - yolo_pose
12
+ - yolo_seg
13
+
14
+ This module provides:
15
+ - register_detector() for future backends
16
+ - create_detector() as the main factory
17
+ - available_detectors() listing
18
+ - available_models() combining registry + installed models
19
+
20
+ Design notes:
21
+ - Detector keys are currently simple (e.g. "yolo_bbox") for backward compatibility.
22
+ - Internally, we store (backend, name) so we can support future keys like "onnxrt:foo".
23
+ """
24
+
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Callable, Dict, List, Optional, Tuple, Type, Union
28
+
29
+ from ..core.schema import BaseDetector
30
+ from ..registry.registry import (
31
+ list_installed_models,
32
+ list_registered_models,
33
+ resolve_weights_path,
34
+ )
35
+
36
+ # Import and register built-in backend(s)
37
+ from .ultralytics.detectors import ( # noqa: E402
38
+ YOLOBBoxDetector,
39
+ YOLOPoseDetector,
40
+ YOLOSegDetector,
41
+ )
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class DetectorSpec:
46
+ backend: str
47
+ name: str
48
+ cls: Type[BaseDetector]
49
+
50
+
51
+ # Keyed by detector "public name" (e.g. "yolo_bbox") -> spec
52
+ _DETECTORS: Dict[str, DetectorSpec] = {}
53
+
54
+
55
+ def register_detector(*, name: str, backend: str, cls: Type[BaseDetector]) -> None:
56
+ key = (name or "").strip().lower()
57
+ if not key:
58
+ raise ValueError("Detector name must be non-empty.")
59
+ if key in _DETECTORS:
60
+ raise ValueError(f"Detector '{key}' already registered.")
61
+ _DETECTORS[key] = DetectorSpec(backend=backend, name=key, cls=cls)
62
+
63
+
64
+ def available_detectors() -> List[str]:
65
+ return sorted(_DETECTORS.keys())
66
+
67
+
68
+ def available_models(
69
+ *,
70
+ detector: Optional[str] = None,
71
+ backend: Optional[str] = None,
72
+ models_dir: Union[str, Path] = "models",
73
+ ) -> dict:
74
+ """
75
+ Return a dict: {"registered": {...}, "installed": {...}}.
76
+
77
+ - registered models are from the JSON registry files (optionally filtered).
78
+ - installed models are local model files found under models_dir.
79
+ """
80
+ det_key = detector.strip().lower() if detector else None
81
+ return {
82
+ "registered": list_registered_models(detector=det_key, backend=backend),
83
+ "installed": list_installed_models(models_dir=models_dir),
84
+ }
85
+
86
+
87
+ def create_detector(
88
+ *,
89
+ name: str,
90
+ weights: Union[str, Path],
91
+ conf: float = 0.25,
92
+ classes: Optional[List[int]] = None,
93
+ imgsz: int = 640,
94
+ device: str = "auto",
95
+ half: bool = False,
96
+ models_dir: Union[str, Path] = "models",
97
+ allow_download: bool = True,
98
+ ) -> BaseDetector:
99
+ """
100
+ Create a detector instance.
101
+
102
+ - Resolves weights from local path / URL / registry key into a local file.
103
+ - Instantiates the registered detector class.
104
+ """
105
+ key = (name or "").strip().lower()
106
+ if key not in _DETECTORS:
107
+ raise ValueError(f"Unknown detector '{name}'. Available: {', '.join(available_detectors())}")
108
+
109
+ spec = _DETECTORS[key]
110
+
111
+ weights_path = resolve_weights_path(
112
+ weights,
113
+ models_dir=models_dir,
114
+ detector=key,
115
+ backend=spec.backend,
116
+ allow_download=allow_download,
117
+ )
118
+ return spec.cls(
119
+ weights=weights_path,
120
+ conf=conf,
121
+ classes=classes,
122
+ imgsz=imgsz,
123
+ device=device,
124
+ half=half,
125
+ )
126
+
127
+
128
+ # -------------------------
129
+ # Built-in detector wiring
130
+ # -------------------------
131
+ register_detector(name="yolo_bbox", backend="ultralytics", cls=YOLOBBoxDetector)
132
+ register_detector(name="yolo_pose", backend="ultralytics", cls=YOLOPoseDetector)
133
+ register_detector(name="yolo_seg", backend="ultralytics", cls=YOLOSegDetector)
134
+
135
+
136
+ __all__ = [
137
+ "register_detector",
138
+ "available_detectors",
139
+ "available_models",
140
+ "create_detector",
141
+ ]
@@ -0,0 +1,14 @@
1
+ """
2
+ detect.backends.ultralytics
3
+ ---------------------------
4
+
5
+ Ultralytics backend integration.
6
+
7
+ Provides:
8
+ - YOLO bbox/pose/seg detectors
9
+ - (later) exporter adapter
10
+
11
+ This backend assumes `ultralytics` is installed.
12
+ """
13
+
14
+ __all__ = []
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ detect.backends.ultralytics.detectors
5
+ ------------------------------------
6
+
7
+ Ultralytics YOLO detector wrappers for:
8
+ - bounding boxes (yolo_bbox)
9
+ - pose (yolo_pose)
10
+ - segmentation (yolo_seg)
11
+
12
+ All implement BaseDetector.process_frame(frame_bgr) and return canonical Detection dicts
13
+ (without det_ind; runner assigns it).
14
+
15
+ This file intentionally mirrors your previous per-file detectors but consolidates them.
16
+ """
17
+
18
+ from pathlib import Path
19
+ from typing import List, Optional, Union
20
+
21
+ import numpy as np
22
+
23
+ from ...core.schema import BaseDetector, Detection
24
+
25
+ try:
26
+ from ultralytics import YOLO # type: ignore
27
+ except Exception as e: # pragma: no cover
28
+ YOLO = None # type: ignore
29
+ _ULTRALYTICS_IMPORT_ERROR = e
30
+
31
+ try:
32
+ import torch # type: ignore
33
+ except Exception: # pragma: no cover
34
+ torch = None # type: ignore
35
+
36
+
37
+ def _ultralytics_device_arg(device: str):
38
+ """
39
+ Convert our device string to what Ultralytics expects.
40
+
41
+ Ultralytics expects:
42
+ - "cpu", "mps", or GPU indices like "0" / "0,1"
43
+ It does NOT accept "auto".
44
+ """
45
+ d = (device or "").strip().lower()
46
+
47
+ if d in ("", "auto"):
48
+ if torch is not None:
49
+ # Prefer Apple MPS when available
50
+ if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
51
+ return "mps"
52
+ # Prefer CUDA gpu index "0" when available
53
+ if torch.cuda.is_available() and torch.cuda.device_count() > 0:
54
+ return "0"
55
+ return "cpu"
56
+
57
+ # Normalize common CUDA spellings
58
+ if d == "cuda":
59
+ return "0"
60
+ if d.startswith("cuda:"):
61
+ # "cuda:0" -> "0"
62
+ return d.split(":", 1)[1].strip() or "0"
63
+
64
+ # passthrough: "cpu", "mps", "0", "0,1", etc.
65
+ return d
66
+
67
+ class _UltralyticsBase(BaseDetector):
68
+ backend: str = "ultralytics"
69
+
70
+ def __init__(
71
+ self,
72
+ *,
73
+ weights: Union[str, Path],
74
+ conf: float = 0.25,
75
+ classes: Optional[List[int]] = None,
76
+ imgsz: int = 640,
77
+ device: str = "auto",
78
+ half: bool = False,
79
+ ) -> None:
80
+ if YOLO is None: # pragma: no cover
81
+ raise ImportError(
82
+ "ultralytics is required but failed to import"
83
+ ) from _ULTRALYTICS_IMPORT_ERROR
84
+
85
+ super().__init__(
86
+ weights=weights,
87
+ conf=conf,
88
+ classes=classes,
89
+ imgsz=imgsz,
90
+ device=device,
91
+ half=half,
92
+ )
93
+ self.model = YOLO(str(self.weights))
94
+
95
+ # Class names map (list or dict depending on Ultralytics version)
96
+ self.names = None
97
+ try:
98
+ self.names = getattr(self.model, "names", None) or getattr(self.model.model, "names", None)
99
+ except Exception:
100
+ self.names = None
101
+
102
+ def _class_name(self, cls_id: int) -> Optional[str]:
103
+ names = self.names
104
+ if names is None:
105
+ return None
106
+ try:
107
+ if isinstance(names, dict):
108
+ return names.get(int(cls_id))
109
+ if isinstance(names, (list, tuple)):
110
+ if 0 <= int(cls_id) < len(names):
111
+ return str(names[int(cls_id)])
112
+ return None
113
+ except Exception:
114
+ return None
115
+
116
+ def _predict(self, frame_bgr: np.ndarray):
117
+ # Ultralytics can take numpy BGR directly; it handles resize/letterbox internally.
118
+ dev = _ultralytics_device_arg(self.device_str)
119
+ return self.model.predict(
120
+ source=frame_bgr,
121
+ conf=self.conf,
122
+ imgsz=self.imgsz,
123
+ classes=self.classes,
124
+ device=dev,
125
+ half=self.half,
126
+ verbose=False,
127
+ )
128
+
129
+ def warmup(self) -> None:
130
+ h = w = max(32, int(self.imgsz))
131
+ dummy = np.zeros((h, w, 3), dtype=np.uint8)
132
+ try:
133
+ dev = _ultralytics_device_arg(self.device_str)
134
+ _ = self.model.predict(
135
+ source=dummy,
136
+ conf=0.01,
137
+ imgsz=self.imgsz,
138
+ device=dev,
139
+ half=self.half,
140
+ verbose=False,
141
+ )
142
+ except Exception:
143
+ pass
144
+
145
+
146
+ class YOLOBBoxDetector(_UltralyticsBase):
147
+ """Ultralytics YOLO bounding-box detector."""
148
+
149
+ # Keep this name stable for JSON readability
150
+ def __init__(self, **kwargs) -> None:
151
+ super().__init__(**kwargs)
152
+
153
+ def process_frame(self, frame_bgr: np.ndarray) -> List[Detection]:
154
+ results = self._predict(frame_bgr)
155
+ r = results[0]
156
+ detections: List[Detection] = []
157
+
158
+ if getattr(r, "boxes", None) is None or len(r.boxes) == 0:
159
+ return detections
160
+
161
+ boxes_xyxy = r.boxes.xyxy.cpu().numpy().astype(float)
162
+ scores = r.boxes.conf.cpu().numpy().astype(float)
163
+ cls_ids = r.boxes.cls.cpu().numpy().astype(int)
164
+
165
+ for i in range(boxes_xyxy.shape[0]):
166
+ x1, y1, x2, y2 = boxes_xyxy[i].tolist()
167
+ score = float(scores[i])
168
+ cid = int(cls_ids[i])
169
+ det: Detection = {
170
+ "bbox": [x1, y1, x2, y2],
171
+ "score": score,
172
+ "class_id": cid,
173
+ }
174
+ cname = self._class_name(cid)
175
+ if cname is not None:
176
+ det["class_name"] = cname
177
+ detections.append(det)
178
+ return detections
179
+
180
+
181
+ class YOLOPoseDetector(_UltralyticsBase):
182
+ """Ultralytics YOLO pose model wrapper (boxes + keypoints)."""
183
+
184
+ def __init__(self, **kwargs) -> None:
185
+ super().__init__(**kwargs)
186
+
187
+ def process_frame(self, frame_bgr: np.ndarray) -> List[Detection]:
188
+ results = self._predict(frame_bgr)
189
+ r = results[0]
190
+ detections: List[Detection] = []
191
+
192
+ if getattr(r, "boxes", None) is None or len(r.boxes) == 0:
193
+ return detections
194
+
195
+ boxes_xyxy = r.boxes.xyxy.cpu().numpy().astype(float)
196
+ scores = r.boxes.conf.cpu().numpy().astype(float)
197
+ cls_ids = r.boxes.cls.cpu().numpy().astype(int)
198
+
199
+ # Keypoints tensor: (N, K, 3) with (x, y, score)
200
+ kpts = None
201
+ if getattr(r, "keypoints", None) is not None:
202
+ try:
203
+ kpts = r.keypoints.data.cpu().numpy().astype(float)
204
+ except Exception:
205
+ kpts = None
206
+
207
+ for i in range(boxes_xyxy.shape[0]):
208
+ x1, y1, x2, y2 = boxes_xyxy[i].tolist()
209
+ score = float(scores[i])
210
+ cid = int(cls_ids[i])
211
+ det: Detection = {
212
+ "bbox": [x1, y1, x2, y2],
213
+ "score": score,
214
+ "class_id": cid,
215
+ }
216
+ cname = self._class_name(cid)
217
+ if cname is not None:
218
+ det["class_name"] = cname
219
+
220
+ if kpts is not None and i < kpts.shape[0]:
221
+ kp_list = kpts[i].tolist()
222
+ det["keypoints"] = [[float(a), float(b), float(c)] for a, b, c in kp_list]
223
+
224
+ detections.append(det)
225
+
226
+ return detections
227
+
228
+
229
+ class YOLOSegDetector(_UltralyticsBase):
230
+ """Ultralytics YOLO segmentation model wrapper (boxes + polygon segments)."""
231
+
232
+ def __init__(self, **kwargs) -> None:
233
+ super().__init__(**kwargs)
234
+
235
+ def process_frame(self, frame_bgr: np.ndarray) -> List[Detection]:
236
+ results = self._predict(frame_bgr)
237
+ r = results[0]
238
+ detections: List[Detection] = []
239
+
240
+ if getattr(r, "boxes", None) is None or len(r.boxes) == 0:
241
+ return detections
242
+
243
+ boxes_xyxy = r.boxes.xyxy.cpu().numpy().astype(float)
244
+ scores = r.boxes.conf.cpu().numpy().astype(float)
245
+ cls_ids = r.boxes.cls.cpu().numpy().astype(int)
246
+
247
+ # r.masks.xy is typically list-like per instance
248
+ mask_polys = None
249
+ if getattr(r, "masks", None) is not None:
250
+ try:
251
+ mask_polys = r.masks.xy
252
+ except Exception:
253
+ mask_polys = None
254
+
255
+ for i in range(boxes_xyxy.shape[0]):
256
+ x1, y1, x2, y2 = boxes_xyxy[i].tolist()
257
+ score = float(scores[i])
258
+ cid = int(cls_ids[i])
259
+ det: Detection = {
260
+ "bbox": [x1, y1, x2, y2],
261
+ "score": score,
262
+ "class_id": cid,
263
+ }
264
+ cname = self._class_name(cid)
265
+ if cname is not None:
266
+ det["class_name"] = cname
267
+
268
+ segs: List[List[List[float]]] = []
269
+ if mask_polys is not None and i < len(mask_polys):
270
+ polys_i = mask_polys[i]
271
+ # Ultralytics may return a list of polygons per instance
272
+ if isinstance(polys_i, (list, tuple)):
273
+ for poly in polys_i:
274
+ if poly is None:
275
+ continue
276
+ arr = np.asarray(poly, dtype=float)
277
+ if arr.ndim == 2 and arr.shape[1] == 2:
278
+ segs.append(arr.tolist())
279
+ else:
280
+ arr = np.asarray(polys_i, dtype=float)
281
+ if arr.ndim == 2 and arr.shape[1] == 2:
282
+ segs.append(arr.tolist())
283
+
284
+ if segs:
285
+ det["segments"] = segs
286
+
287
+ detections.append(det)
288
+
289
+ return detections
290
+
291
+
292
+ __all__ = [
293
+ "YOLOBBoxDetector",
294
+ "YOLOPoseDetector",
295
+ "YOLOSegDetector",
296
+ ]