trackforge 0.5.0__cp315-cp315-win_amd64.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.
- trackforge/__init__.py +6 -0
- trackforge/py.typed +0 -0
- trackforge/trackforge.cp315-win_amd64.pyd +0 -0
- trackforge/trackforge.pyi +315 -0
- trackforge/ultralytics.py +83 -0
- trackforge-0.5.0.dist-info/METADATA +622 -0
- trackforge-0.5.0.dist-info/RECORD +10 -0
- trackforge-0.5.0.dist-info/WHEEL +4 -0
- trackforge-0.5.0.dist-info/licenses/LICENSE +21 -0
- trackforge-0.5.0.dist-info/sboms/trackforge.cyclonedx.json +1336 -0
trackforge/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .trackforge import * # noqa: F403
|
|
2
|
+
from .ultralytics import from_ultralytics, track_ultralytics
|
|
3
|
+
|
|
4
|
+
__doc__ = trackforge.__doc__ # noqa: F405
|
|
5
|
+
if hasattr(trackforge, "__all__"): # noqa: F405
|
|
6
|
+
__all__ = [*trackforge.__all__, "from_ultralytics", "track_ultralytics"] # noqa: F405
|
trackforge/py.typed
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"BOTSORT",
|
|
5
|
+
"BYTETRACK",
|
|
6
|
+
"DEEPOCSORT",
|
|
7
|
+
"DEEPSORT",
|
|
8
|
+
"OCSORT",
|
|
9
|
+
"SORT",
|
|
10
|
+
"TRACKTRACK",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
class BYTETRACK:
|
|
14
|
+
"""
|
|
15
|
+
ByteTrack tracker implementation.
|
|
16
|
+
|
|
17
|
+
**Usage Example:**
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from trackforge import BYTETRACK
|
|
21
|
+
|
|
22
|
+
tracker = BYTETRACK(
|
|
23
|
+
track_thresh=0.5,
|
|
24
|
+
track_buffer=30,
|
|
25
|
+
match_thresh=0.8,
|
|
26
|
+
det_thresh=0.6,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
detections = [
|
|
30
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
31
|
+
([200.0, 200.0, 60.0, 120.0], 0.85, 0),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
tracks = tracker.update(detections)
|
|
35
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
36
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
track_thresh: float = 0.5,
|
|
43
|
+
track_buffer: int = 30,
|
|
44
|
+
match_thresh: float = 0.8,
|
|
45
|
+
det_thresh: float = 0.6,
|
|
46
|
+
second_match_thresh: float = 0.5,
|
|
47
|
+
) -> None: ...
|
|
48
|
+
def update(
|
|
49
|
+
self, output_results: List[Tuple[List[float], float, int]]
|
|
50
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
51
|
+
|
|
52
|
+
class SORT:
|
|
53
|
+
"""
|
|
54
|
+
SORT (Simple Online and Realtime Tracking) tracker implementation.
|
|
55
|
+
|
|
56
|
+
**Usage Example:**
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from trackforge import SORT
|
|
60
|
+
|
|
61
|
+
tracker = SORT(max_age=1, min_hits=3, iou_threshold=0.3)
|
|
62
|
+
|
|
63
|
+
detections = [
|
|
64
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
tracks = tracker.update(detections)
|
|
68
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
69
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
70
|
+
```
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
max_age: int = 1,
|
|
76
|
+
min_hits: int = 3,
|
|
77
|
+
iou_threshold: float = 0.3,
|
|
78
|
+
) -> None: ...
|
|
79
|
+
def update(
|
|
80
|
+
self, detections: List[Tuple[List[float], float, int]]
|
|
81
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
82
|
+
|
|
83
|
+
class OCSORT:
|
|
84
|
+
"""
|
|
85
|
+
OC-SORT (Observation-Centric SORT) tracker implementation.
|
|
86
|
+
|
|
87
|
+
**Usage Example:**
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from trackforge import OCSORT
|
|
91
|
+
|
|
92
|
+
tracker = OCSORT(
|
|
93
|
+
max_age=30,
|
|
94
|
+
min_hits=3,
|
|
95
|
+
iou_threshold=0.3,
|
|
96
|
+
delta_t=3,
|
|
97
|
+
inertia=0.2,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
detections = [
|
|
101
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
tracks = tracker.update(detections)
|
|
105
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
106
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
107
|
+
```
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
max_age: int = 30,
|
|
113
|
+
min_hits: int = 3,
|
|
114
|
+
iou_threshold: float = 0.3,
|
|
115
|
+
delta_t: int = 3,
|
|
116
|
+
inertia: float = 0.2,
|
|
117
|
+
) -> None: ...
|
|
118
|
+
def update(
|
|
119
|
+
self, detections: List[Tuple[List[float], float, int]]
|
|
120
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
121
|
+
|
|
122
|
+
class DEEPSORT:
|
|
123
|
+
"""
|
|
124
|
+
DeepSORT tracker implementation with appearance feature matching.
|
|
125
|
+
|
|
126
|
+
**Usage Example:**
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from trackforge import DEEPSORT
|
|
130
|
+
import numpy as np
|
|
131
|
+
|
|
132
|
+
tracker = DEEPSORT(
|
|
133
|
+
max_age=70,
|
|
134
|
+
n_init=3,
|
|
135
|
+
max_iou_distance=0.7,
|
|
136
|
+
max_cosine_distance=0.2,
|
|
137
|
+
nn_budget=100,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
detections = [
|
|
141
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
142
|
+
]
|
|
143
|
+
embeddings = [np.random.rand(128).tolist()]
|
|
144
|
+
|
|
145
|
+
tracks = tracker.update(detections, embeddings)
|
|
146
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
147
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
148
|
+
```
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
max_age: int = 70,
|
|
154
|
+
n_init: int = 3,
|
|
155
|
+
max_iou_distance: float = 0.7,
|
|
156
|
+
max_cosine_distance: float = 0.2,
|
|
157
|
+
nn_budget: int = 100,
|
|
158
|
+
) -> None: ...
|
|
159
|
+
def update(
|
|
160
|
+
self,
|
|
161
|
+
detections: List[Tuple[List[float], float, int]],
|
|
162
|
+
embeddings: List[List[float]],
|
|
163
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
164
|
+
|
|
165
|
+
class DEEPOCSORT:
|
|
166
|
+
"""
|
|
167
|
+
Deep OC-SORT tracker: OC-SORT motion with appearance association.
|
|
168
|
+
|
|
169
|
+
Blends a cosine distance to a per-track feature gallery into the OC-SORT
|
|
170
|
+
motion cost. Pass embeddings for appearance-aware tracking, or omit them to
|
|
171
|
+
track on motion only.
|
|
172
|
+
|
|
173
|
+
**Usage Example:**
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from trackforge import DEEPOCSORT
|
|
177
|
+
|
|
178
|
+
tracker = DEEPOCSORT(
|
|
179
|
+
max_age=30,
|
|
180
|
+
min_hits=3,
|
|
181
|
+
iou_threshold=0.3,
|
|
182
|
+
delta_t=3,
|
|
183
|
+
inertia=0.2,
|
|
184
|
+
appearance_weight=0.5,
|
|
185
|
+
max_cosine_distance=0.2,
|
|
186
|
+
nn_budget=100,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
detections = [
|
|
190
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
191
|
+
]
|
|
192
|
+
embeddings = [[0.1, 0.2, 0.3]]
|
|
193
|
+
|
|
194
|
+
tracks = tracker.update(detections, embeddings)
|
|
195
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
196
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
197
|
+
```
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
max_age: int = 30,
|
|
203
|
+
min_hits: int = 3,
|
|
204
|
+
iou_threshold: float = 0.3,
|
|
205
|
+
delta_t: int = 3,
|
|
206
|
+
inertia: float = 0.2,
|
|
207
|
+
appearance_weight: float = 0.5,
|
|
208
|
+
max_cosine_distance: float = 0.2,
|
|
209
|
+
nn_budget: int = 100,
|
|
210
|
+
) -> None: ...
|
|
211
|
+
def update(
|
|
212
|
+
self,
|
|
213
|
+
detections: List[Tuple[List[float], float, int]],
|
|
214
|
+
embeddings: List[List[float]] = ...,
|
|
215
|
+
camera_motion: Optional[List[float]] = ...,
|
|
216
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
217
|
+
|
|
218
|
+
class BOTSORT:
|
|
219
|
+
"""
|
|
220
|
+
BoT-SORT tracker: ByteTrack with camera motion and appearance fusion.
|
|
221
|
+
|
|
222
|
+
Adds camera motion compensation and an appearance-fused first association stage
|
|
223
|
+
on top of ByteTrack's two-stage cascade. Pass embeddings for appearance-aware
|
|
224
|
+
tracking, or omit them to track on motion only.
|
|
225
|
+
|
|
226
|
+
**Usage Example:**
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from trackforge import BOTSORT
|
|
230
|
+
|
|
231
|
+
tracker = BOTSORT(
|
|
232
|
+
track_thresh=0.5,
|
|
233
|
+
track_buffer=30,
|
|
234
|
+
match_thresh=0.8,
|
|
235
|
+
det_thresh=0.6,
|
|
236
|
+
proximity_thresh=0.5,
|
|
237
|
+
appearance_thresh=0.25,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
detections = [
|
|
241
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
242
|
+
]
|
|
243
|
+
embeddings = [[0.1, 0.2, 0.3]]
|
|
244
|
+
|
|
245
|
+
tracks = tracker.update(detections, embeddings)
|
|
246
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
247
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
248
|
+
```
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(
|
|
252
|
+
self,
|
|
253
|
+
track_thresh: float = 0.5,
|
|
254
|
+
track_buffer: int = 30,
|
|
255
|
+
match_thresh: float = 0.8,
|
|
256
|
+
det_thresh: float = 0.6,
|
|
257
|
+
second_match_thresh: float = 0.5,
|
|
258
|
+
proximity_thresh: float = 0.5,
|
|
259
|
+
appearance_thresh: float = 0.25,
|
|
260
|
+
) -> None: ...
|
|
261
|
+
def update(
|
|
262
|
+
self,
|
|
263
|
+
detections: List[Tuple[List[float], float, int]],
|
|
264
|
+
embeddings: List[List[float]] = ...,
|
|
265
|
+
camera_motion: Optional[List[float]] = ...,
|
|
266
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
267
|
+
|
|
268
|
+
class TRACKTRACK:
|
|
269
|
+
"""
|
|
270
|
+
TrackTrack tracker: track-perspective association with track-aware initialization.
|
|
271
|
+
|
|
272
|
+
Each track picks its own best detection and a pair matches only when the choice
|
|
273
|
+
is mutual, repeating with a cost gate that tightens each round. New tracks start
|
|
274
|
+
only from confident detections that do not overlap existing tracks. Pass
|
|
275
|
+
embeddings for appearance-aware tracking, or omit them to track on motion only.
|
|
276
|
+
|
|
277
|
+
**Usage Example:**
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from trackforge import TRACKTRACK
|
|
281
|
+
|
|
282
|
+
tracker = TRACKTRACK(
|
|
283
|
+
det_thresh=0.6,
|
|
284
|
+
match_thresh=0.7,
|
|
285
|
+
track_buffer=30,
|
|
286
|
+
min_hits=3,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
detections = [
|
|
290
|
+
([100.0, 100.0, 50.0, 100.0], 0.9, 0),
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
tracks = tracker.update(detections)
|
|
294
|
+
for track_id, box, score, class_id, det_ind in tracks:
|
|
295
|
+
print(f"Track ID: {track_id}, Box: {box}")
|
|
296
|
+
```
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(
|
|
300
|
+
self,
|
|
301
|
+
det_thresh: float = 0.6,
|
|
302
|
+
match_thresh: float = 0.7,
|
|
303
|
+
track_buffer: int = 30,
|
|
304
|
+
min_hits: int = 3,
|
|
305
|
+
init_thresh: float = 0.7,
|
|
306
|
+
tai_thresh: float = 0.55,
|
|
307
|
+
penalty_low: float = 0.2,
|
|
308
|
+
reduce_step: float = 0.05,
|
|
309
|
+
) -> None: ...
|
|
310
|
+
def update(
|
|
311
|
+
self,
|
|
312
|
+
detections: List[Tuple[List[float], float, int]],
|
|
313
|
+
embeddings: List[List[float]] = ...,
|
|
314
|
+
camera_motion: Optional[List[float]] = ...,
|
|
315
|
+
) -> List[Tuple[int, List[float], float, int, Optional[int]]]: ...
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Helpers for feeding Ultralytics YOLO results into trackforge trackers.
|
|
2
|
+
|
|
3
|
+
They only read the result objects, so ``ultralytics`` stays an optional dependency
|
|
4
|
+
and is never imported here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple
|
|
10
|
+
|
|
11
|
+
Detection = Tuple[List[float], float, int]
|
|
12
|
+
Embedder = Callable[[Any, List[List[float]]], Sequence[Sequence[float]]]
|
|
13
|
+
|
|
14
|
+
__all__ = ["from_ultralytics", "track_ultralytics"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def from_ultralytics(result: Any) -> List[Detection]:
|
|
18
|
+
"""Convert one Ultralytics result into trackforge detections.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
result: A ``Results`` object from ``model.predict(...)`` or ``model(...)``, or
|
|
22
|
+
the one-element list those calls return for a single image.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A list of ``(tlwh, score, class_id)`` detections, in the same order as
|
|
26
|
+
``result.boxes``, so a track's ``det_ind`` indexes into ``result.boxes``.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If a list holding more than one result is passed.
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(result, (list, tuple)):
|
|
32
|
+
if len(result) != 1:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"expected the result of a single image, got {len(result)} results"
|
|
35
|
+
)
|
|
36
|
+
result = result[0]
|
|
37
|
+
|
|
38
|
+
boxes = getattr(result, "boxes", None)
|
|
39
|
+
if boxes is None or len(boxes) == 0:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
([x1, y1, x2 - x1, y2 - y1], float(score), int(cls))
|
|
44
|
+
for (x1, y1, x2, y2), score, cls in zip(
|
|
45
|
+
boxes.xyxy.tolist(), boxes.conf.tolist(), boxes.cls.tolist()
|
|
46
|
+
)
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def track_ultralytics(
|
|
51
|
+
model: Any,
|
|
52
|
+
source: Any,
|
|
53
|
+
tracker: Any,
|
|
54
|
+
embedder: Optional[Embedder] = None,
|
|
55
|
+
**predict_kwargs: Any,
|
|
56
|
+
) -> Iterator[Tuple[Any, list]]:
|
|
57
|
+
"""Run an Ultralytics model over a source and track every frame.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
model: A loaded ``ultralytics.YOLO`` (or RT-DETR) model.
|
|
61
|
+
source: Anything ``model.predict`` accepts: a video path, webcam index,
|
|
62
|
+
stream URL, image folder, and so on.
|
|
63
|
+
tracker: A trackforge tracker such as ``trackforge.BYTETRACK()``.
|
|
64
|
+
embedder: Optional ``embedder(frame, tlwh_boxes) -> embeddings`` callable for
|
|
65
|
+
appearance trackers. ``DEEPSORT`` needs one; ``DEEPOCSORT``, ``BOTSORT``
|
|
66
|
+
and ``TRACKTRACK`` use it when given.
|
|
67
|
+
**predict_kwargs: Passed to ``model.predict``, for example ``classes=[0]``
|
|
68
|
+
or ``conf=0.3``.
|
|
69
|
+
|
|
70
|
+
Yields:
|
|
71
|
+
``(result, tracks)`` per frame, where ``tracks`` is the tracker output and
|
|
72
|
+
``result.orig_img`` is the BGR frame.
|
|
73
|
+
"""
|
|
74
|
+
predict_kwargs.setdefault("stream", True)
|
|
75
|
+
predict_kwargs.setdefault("verbose", False)
|
|
76
|
+
for result in model.predict(source, **predict_kwargs):
|
|
77
|
+
detections = from_ultralytics(result)
|
|
78
|
+
if embedder is None:
|
|
79
|
+
tracks = tracker.update(detections)
|
|
80
|
+
else:
|
|
81
|
+
boxes = [d[0] for d in detections]
|
|
82
|
+
tracks = tracker.update(detections, embedder(result.orig_img, boxes))
|
|
83
|
+
yield result, tracks
|