superiorvision 0.30.0.dev0__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.
- superiorvision/__init__.py +19 -0
- superiorvision-0.30.0.dev0.dist-info/METADATA +398 -0
- superiorvision-0.30.0.dev0.dist-info/RECORD +96 -0
- superiorvision-0.30.0.dev0.dist-info/WHEEL +5 -0
- superiorvision-0.30.0.dev0.dist-info/licenses/LICENSE.md +9 -0
- superiorvision-0.30.0.dev0.dist-info/top_level.txt +2 -0
- supervision/__init__.py +318 -0
- supervision/annotators/__init__.py +0 -0
- supervision/annotators/base.py +21 -0
- supervision/annotators/core.py +3551 -0
- supervision/annotators/utils.py +541 -0
- supervision/assets/__init__.py +4 -0
- supervision/assets/downloader.py +166 -0
- supervision/assets/list.py +84 -0
- supervision/classification/__init__.py +0 -0
- supervision/classification/core.py +216 -0
- supervision/config.py +13 -0
- supervision/dataset/__init__.py +0 -0
- supervision/dataset/core.py +1313 -0
- supervision/dataset/formats/__init__.py +0 -0
- supervision/dataset/formats/coco.py +708 -0
- supervision/dataset/formats/createml.py +331 -0
- supervision/dataset/formats/labelme.py +405 -0
- supervision/dataset/formats/pascal_voc.py +449 -0
- supervision/dataset/formats/yolo.py +502 -0
- supervision/dataset/utils.py +265 -0
- supervision/detection/__init__.py +0 -0
- supervision/detection/compact_mask.py +1927 -0
- supervision/detection/core.py +3864 -0
- supervision/detection/line_zone.py +924 -0
- supervision/detection/overlap_filter.py +426 -0
- supervision/detection/tensor_utils.py +1596 -0
- supervision/detection/tensor_views.py +48 -0
- supervision/detection/tools/__init__.py +0 -0
- supervision/detection/tools/csv_sink.py +242 -0
- supervision/detection/tools/inference_slicer.py +776 -0
- supervision/detection/tools/json_sink.py +215 -0
- supervision/detection/tools/polygon_zone.py +266 -0
- supervision/detection/tools/smoother.py +218 -0
- supervision/detection/tools/transformers.py +265 -0
- supervision/detection/utils/__init__.py +12 -0
- supervision/detection/utils/_typing.py +11 -0
- supervision/detection/utils/boxes.py +510 -0
- supervision/detection/utils/converters.py +830 -0
- supervision/detection/utils/internal.py +806 -0
- supervision/detection/utils/iou_and_nms.py +1606 -0
- supervision/detection/utils/masks.py +625 -0
- supervision/detection/utils/polygons.py +128 -0
- supervision/detection/utils/vlms.py +97 -0
- supervision/detection/vlm.py +945 -0
- supervision/draw/__init__.py +0 -0
- supervision/draw/base.py +14 -0
- supervision/draw/color.py +598 -0
- supervision/draw/utils.py +527 -0
- supervision/geometry/__init__.py +0 -0
- supervision/geometry/core.py +208 -0
- supervision/geometry/utils.py +54 -0
- supervision/key_points/__init__.py +0 -0
- supervision/key_points/annotators.py +997 -0
- supervision/key_points/core.py +1506 -0
- supervision/key_points/skeletons.py +2646 -0
- supervision/keypoint/__init__.py +13 -0
- supervision/keypoint/annotators.py +13 -0
- supervision/keypoint/core.py +8 -0
- supervision/metrics/__init__.py +40 -0
- supervision/metrics/core.py +74 -0
- supervision/metrics/detection.py +1632 -0
- supervision/metrics/f1_score.py +815 -0
- supervision/metrics/mean_average_precision.py +1723 -0
- supervision/metrics/mean_average_recall.py +734 -0
- supervision/metrics/precision.py +815 -0
- supervision/metrics/recall.py +774 -0
- supervision/metrics/utils/__init__.py +0 -0
- supervision/metrics/utils/matching.py +56 -0
- supervision/metrics/utils/object_size.py +256 -0
- supervision/metrics/utils/utils.py +9 -0
- supervision/py.typed +0 -0
- supervision/tracker/__init__.py +5 -0
- supervision/tracker/byte_tracker/__init__.py +0 -0
- supervision/tracker/byte_tracker/core.py +448 -0
- supervision/tracker/byte_tracker/kalman_filter.py +186 -0
- supervision/tracker/byte_tracker/matching.py +211 -0
- supervision/tracker/byte_tracker/single_object_track.py +194 -0
- supervision/tracker/byte_tracker/utils.py +34 -0
- supervision/utils/__init__.py +0 -0
- supervision/utils/conversion.py +223 -0
- supervision/utils/deprecate.py +54 -0
- supervision/utils/file.py +209 -0
- supervision/utils/image.py +988 -0
- supervision/utils/internal.py +209 -0
- supervision/utils/iterables.py +103 -0
- supervision/utils/logger.py +61 -0
- supervision/utils/notebook.py +124 -0
- supervision/utils/tensor.py +362 -0
- supervision/utils/video.py +533 -0
- supervision/validators/__init__.py +379 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
from scipy.optimize import linear_sum_assignment
|
|
8
|
+
|
|
9
|
+
# The public utility dispatches on tensor inputs and keeps IoU on the source
|
|
10
|
+
# device. The older ``iou_and_nms`` implementation is NumPy-only, which made
|
|
11
|
+
# the tracker silently fall back to host arrays after its Kalman prediction.
|
|
12
|
+
from supervision.detection.utils import box_iou_batch
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from supervision.tracker.byte_tracker.core import STrack
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def indices_to_matches(
|
|
19
|
+
cost_matrix: np.ndarray, indices: np.ndarray, thresh: float
|
|
20
|
+
) -> tuple[np.ndarray, tuple, tuple]:
|
|
21
|
+
matched_cost = cost_matrix[tuple(zip(*indices))]
|
|
22
|
+
matched_mask = matched_cost <= thresh
|
|
23
|
+
|
|
24
|
+
matches = indices[matched_mask]
|
|
25
|
+
unmatched_a = tuple(set(range(cost_matrix.shape[0])) - set(matches[:, 0]))
|
|
26
|
+
unmatched_b = tuple(set(range(cost_matrix.shape[1])) - set(matches[:, 1]))
|
|
27
|
+
return matches, unmatched_a, unmatched_b
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def linear_assignment(
|
|
31
|
+
cost_matrix: np.ndarray | torch.Tensor, thresh: float
|
|
32
|
+
) -> tuple[np.ndarray, tuple[int], tuple[int, int]]:
|
|
33
|
+
if isinstance(cost_matrix, torch.Tensor):
|
|
34
|
+
return _linear_assignment_tensor(cost_matrix, thresh)
|
|
35
|
+
if cost_matrix.size == 0:
|
|
36
|
+
return (
|
|
37
|
+
np.empty((0, 2), dtype=int),
|
|
38
|
+
tuple(range(cost_matrix.shape[0])),
|
|
39
|
+
tuple(range(cost_matrix.shape[1])),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assignment_cost_matrix = cost_matrix.copy()
|
|
43
|
+
assignment_cost_matrix[assignment_cost_matrix > thresh] = thresh + 1e-4
|
|
44
|
+
row_ind, col_ind = linear_sum_assignment(assignment_cost_matrix)
|
|
45
|
+
indices = np.column_stack((row_ind, col_ind))
|
|
46
|
+
|
|
47
|
+
return indices_to_matches(cost_matrix, indices, thresh)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _linear_assignment_tensor(
|
|
51
|
+
cost_matrix: torch.Tensor, thresh: float
|
|
52
|
+
) -> tuple[torch.Tensor, tuple[int, ...], tuple[int, ...]]:
|
|
53
|
+
"""Hungarian assignment without materialising the cost matrix on the host.
|
|
54
|
+
|
|
55
|
+
ByteTrack owns Python track objects, so the final unmatched identifiers are
|
|
56
|
+
intentionally Python tuples. Costs and the assignment itself stay on the
|
|
57
|
+
input tensor's device.
|
|
58
|
+
"""
|
|
59
|
+
rows, cols = cost_matrix.shape
|
|
60
|
+
if rows == 0 or cols == 0:
|
|
61
|
+
return (
|
|
62
|
+
torch.empty((0, 2), dtype=torch.long, device=cost_matrix.device),
|
|
63
|
+
tuple(range(rows)),
|
|
64
|
+
tuple(range(cols)),
|
|
65
|
+
)
|
|
66
|
+
size = max(rows, cols)
|
|
67
|
+
fill = cost_matrix.new_tensor(thresh + 1e-4)
|
|
68
|
+
cost = cost_matrix.new_full((size, size), thresh + 1e-4)
|
|
69
|
+
cost[:rows, :cols] = torch.minimum(cost_matrix, fill)
|
|
70
|
+
# Standard Kuhn-Munkres augmenting-path algorithm. Potentials, predecessor
|
|
71
|
+
# state, and candidate costs stay as tensors on the source device. Each
|
|
72
|
+
# iteration reduces a whole candidate row at once; only the chosen column
|
|
73
|
+
# index crosses into Python to drive the finite control loop.
|
|
74
|
+
u = cost.new_zeros(size + 1)
|
|
75
|
+
v = cost.new_zeros(size + 1)
|
|
76
|
+
p = torch.zeros(size + 1, dtype=torch.long, device=cost.device)
|
|
77
|
+
way = torch.zeros(size + 1, dtype=torch.long, device=cost.device)
|
|
78
|
+
for i in range(1, size + 1):
|
|
79
|
+
p[0] = i
|
|
80
|
+
j0 = 0
|
|
81
|
+
minv = cost.new_full((size + 1,), torch.inf)
|
|
82
|
+
used = torch.zeros(size + 1, dtype=torch.bool, device=cost.device)
|
|
83
|
+
while True:
|
|
84
|
+
used[j0] = True
|
|
85
|
+
i0 = p[j0]
|
|
86
|
+
unused = ~used[1:]
|
|
87
|
+
cur = cost[i0 - 1] - u[i0] - v[1:]
|
|
88
|
+
better = unused & (cur < minv[1:])
|
|
89
|
+
minv[1:] = torch.where(better, cur, minv[1:])
|
|
90
|
+
way[1:] = torch.where(better, way.new_full((size,), j0), way[1:])
|
|
91
|
+
|
|
92
|
+
candidates = torch.where(
|
|
93
|
+
unused, minv[1:], minv.new_full((size,), torch.inf)
|
|
94
|
+
)
|
|
95
|
+
delta, next_column = torch.min(candidates, dim=0)
|
|
96
|
+
j1 = int(next_column.item()) + 1
|
|
97
|
+
|
|
98
|
+
used_columns = torch.nonzero(used, as_tuple=False).flatten()
|
|
99
|
+
u[p[used_columns]] += delta
|
|
100
|
+
v[used_columns] -= delta
|
|
101
|
+
minv[~used] -= delta
|
|
102
|
+
j0 = j1
|
|
103
|
+
if int(p[j0].item()) == 0:
|
|
104
|
+
break
|
|
105
|
+
while True:
|
|
106
|
+
j1 = int(way[j0].item())
|
|
107
|
+
p[j0] = p[j1]
|
|
108
|
+
j0 = j1
|
|
109
|
+
if j0 == 0:
|
|
110
|
+
break
|
|
111
|
+
pairs = torch.stack(
|
|
112
|
+
(
|
|
113
|
+
p[1:].clone() - 1,
|
|
114
|
+
torch.arange(size, device=cost.device),
|
|
115
|
+
),
|
|
116
|
+
dim=1,
|
|
117
|
+
)
|
|
118
|
+
pairs = pairs[(pairs[:, 0] < rows) & (pairs[:, 1] < cols)]
|
|
119
|
+
matched = pairs[cost_matrix[pairs[:, 0], pairs[:, 1]] <= thresh]
|
|
120
|
+
matched = matched[torch.argsort(matched[:, 0])]
|
|
121
|
+
matched_rows = torch.zeros(rows, dtype=torch.bool, device=cost.device)
|
|
122
|
+
matched_cols = torch.zeros(cols, dtype=torch.bool, device=cost.device)
|
|
123
|
+
matched_rows[matched[:, 0]] = True
|
|
124
|
+
matched_cols[matched[:, 1]] = True
|
|
125
|
+
return (
|
|
126
|
+
matched,
|
|
127
|
+
tuple(torch.nonzero(~matched_rows, as_tuple=False).flatten().cpu().tolist()),
|
|
128
|
+
tuple(torch.nonzero(~matched_cols, as_tuple=False).flatten().cpu().tolist()),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def iou_distance(
|
|
133
|
+
atracks: list[STrack], btracks: list[STrack]
|
|
134
|
+
) -> np.ndarray | torch.Tensor:
|
|
135
|
+
if (
|
|
136
|
+
len(atracks) > 0
|
|
137
|
+
and (
|
|
138
|
+
isinstance(atracks[0], torch.Tensor)
|
|
139
|
+
or isinstance(atracks[0].tlbr, torch.Tensor)
|
|
140
|
+
)
|
|
141
|
+
) or (
|
|
142
|
+
len(btracks) > 0
|
|
143
|
+
and (
|
|
144
|
+
isinstance(btracks[0], torch.Tensor)
|
|
145
|
+
or isinstance(btracks[0].tlbr, torch.Tensor)
|
|
146
|
+
)
|
|
147
|
+
):
|
|
148
|
+
first = atracks[0] if atracks else btracks[0]
|
|
149
|
+
first_box = first if isinstance(first, torch.Tensor) else first.tlbr
|
|
150
|
+
device = first_box.device
|
|
151
|
+
atlbrs = (
|
|
152
|
+
torch.stack(
|
|
153
|
+
[
|
|
154
|
+
track if isinstance(track, torch.Tensor) else track.tlbr
|
|
155
|
+
for track in atracks
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
if atracks
|
|
159
|
+
else torch.empty((0, 4), device=device)
|
|
160
|
+
)
|
|
161
|
+
btlbrs = (
|
|
162
|
+
torch.stack(
|
|
163
|
+
[
|
|
164
|
+
track if isinstance(track, torch.Tensor) else track.tlbr
|
|
165
|
+
for track in btracks
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
if btracks
|
|
169
|
+
else torch.empty((0, 4), device=device)
|
|
170
|
+
)
|
|
171
|
+
if not len(atlbrs) or not len(btlbrs):
|
|
172
|
+
return torch.empty((len(atlbrs), len(btlbrs)), device=device)
|
|
173
|
+
return 1 - box_iou_batch(atlbrs, btlbrs)
|
|
174
|
+
if (len(atracks) > 0 and isinstance(atracks[0], np.ndarray)) or (
|
|
175
|
+
len(btracks) > 0 and isinstance(btracks[0], np.ndarray)
|
|
176
|
+
):
|
|
177
|
+
atlbrs = atracks
|
|
178
|
+
btlbrs = btracks
|
|
179
|
+
else:
|
|
180
|
+
atlbrs = [track.tlbr for track in atracks]
|
|
181
|
+
btlbrs = [track.tlbr for track in btracks]
|
|
182
|
+
|
|
183
|
+
_ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
|
|
184
|
+
if _ious.size != 0:
|
|
185
|
+
_ious = box_iou_batch(np.asarray(atlbrs), np.asarray(btlbrs))
|
|
186
|
+
cost_matrix = 1 - _ious
|
|
187
|
+
|
|
188
|
+
return cost_matrix
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def fuse_score(
|
|
192
|
+
cost_matrix: np.ndarray | torch.Tensor, stracks: list[STrack]
|
|
193
|
+
) -> np.ndarray | torch.Tensor:
|
|
194
|
+
if isinstance(cost_matrix, torch.Tensor):
|
|
195
|
+
if cost_matrix.numel() == 0:
|
|
196
|
+
return cost_matrix
|
|
197
|
+
scores = torch.stack(
|
|
198
|
+
[
|
|
199
|
+
torch.as_tensor(track.score, device=cost_matrix.device)
|
|
200
|
+
for track in stracks
|
|
201
|
+
]
|
|
202
|
+
)
|
|
203
|
+
return 1 - (1 - cost_matrix) * scores.unsqueeze(0)
|
|
204
|
+
if cost_matrix.size == 0:
|
|
205
|
+
return cost_matrix
|
|
206
|
+
iou_sim = 1 - cost_matrix
|
|
207
|
+
det_scores = np.array([strack.score for strack in stracks])
|
|
208
|
+
det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
|
|
209
|
+
fuse_sim = iou_sim * det_scores
|
|
210
|
+
fuse_cost = 1 - fuse_sim
|
|
211
|
+
return fuse_cost
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from supervision.tracker.byte_tracker.kalman_filter import KalmanFilter
|
|
10
|
+
from supervision.tracker.byte_tracker.utils import IdCounter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TrackState(Enum):
|
|
14
|
+
New = 0
|
|
15
|
+
Tracked = 1
|
|
16
|
+
Lost = 2
|
|
17
|
+
Removed = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class STrack:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
tlwh: npt.NDArray[np.float32] | torch.Tensor,
|
|
24
|
+
score: float | torch.Tensor,
|
|
25
|
+
minimum_consecutive_frames: int,
|
|
26
|
+
shared_kalman: KalmanFilter,
|
|
27
|
+
internal_id_counter: IdCounter,
|
|
28
|
+
external_id_counter: IdCounter,
|
|
29
|
+
):
|
|
30
|
+
self.state = TrackState.New
|
|
31
|
+
self.is_activated = False
|
|
32
|
+
self.start_frame = 0
|
|
33
|
+
self.frame_id = 0
|
|
34
|
+
|
|
35
|
+
self._tlwh = torch.as_tensor(tlwh, dtype=torch.float32)
|
|
36
|
+
self.kalman_filter: KalmanFilter | None = None
|
|
37
|
+
self.shared_kalman = shared_kalman
|
|
38
|
+
self.mean: torch.Tensor | None = None
|
|
39
|
+
self.covariance: torch.Tensor | None = None
|
|
40
|
+
self.is_activated = False
|
|
41
|
+
|
|
42
|
+
self.score = torch.as_tensor(
|
|
43
|
+
score, dtype=self._tlwh.dtype, device=self._tlwh.device
|
|
44
|
+
)
|
|
45
|
+
self.tracklet_len = 0
|
|
46
|
+
|
|
47
|
+
self.minimum_consecutive_frames = minimum_consecutive_frames
|
|
48
|
+
|
|
49
|
+
self.internal_id_counter = internal_id_counter
|
|
50
|
+
self.external_id_counter = external_id_counter
|
|
51
|
+
self.internal_track_id = self.internal_id_counter.NO_ID
|
|
52
|
+
self.external_track_id = self.external_id_counter.NO_ID
|
|
53
|
+
|
|
54
|
+
def predict(self) -> None:
|
|
55
|
+
assert self.mean is not None
|
|
56
|
+
assert self.covariance is not None
|
|
57
|
+
assert self.kalman_filter is not None
|
|
58
|
+
mean_state = self.mean.clone()
|
|
59
|
+
if self.state != TrackState.Tracked:
|
|
60
|
+
mean_state[7] = 0
|
|
61
|
+
self.mean, self.covariance = self.kalman_filter.predict(
|
|
62
|
+
mean_state, self.covariance
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def multi_predict(stracks: list[STrack], shared_kalman: KalmanFilter) -> None:
|
|
67
|
+
if len(stracks) > 0:
|
|
68
|
+
multi_mean_states = []
|
|
69
|
+
multi_covariance = []
|
|
70
|
+
for i, st in enumerate(stracks):
|
|
71
|
+
assert st.mean is not None
|
|
72
|
+
assert st.covariance is not None
|
|
73
|
+
multi_mean_states.append(st.mean.clone())
|
|
74
|
+
multi_covariance.append(st.covariance)
|
|
75
|
+
if st.state != TrackState.Tracked:
|
|
76
|
+
multi_mean_states[i][7] = 0
|
|
77
|
+
|
|
78
|
+
predicted_mean, predicted_covariance = shared_kalman.multi_predict(
|
|
79
|
+
torch.stack(multi_mean_states), torch.stack(multi_covariance)
|
|
80
|
+
)
|
|
81
|
+
for i, (mean, cov) in enumerate(zip(predicted_mean, predicted_covariance)):
|
|
82
|
+
stracks[i].mean = mean
|
|
83
|
+
stracks[i].covariance = cov
|
|
84
|
+
|
|
85
|
+
def activate(self, kalman_filter: KalmanFilter, frame_id: int) -> None:
|
|
86
|
+
"""Start a new tracklet after its first detection."""
|
|
87
|
+
self.kalman_filter = kalman_filter
|
|
88
|
+
self.internal_track_id = self.internal_id_counter.new_id()
|
|
89
|
+
self.mean, self.covariance = self.kalman_filter.initiate(
|
|
90
|
+
self.tlwh_to_xyah(self._tlwh)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.tracklet_len = 1
|
|
94
|
+
self.state = TrackState.Tracked
|
|
95
|
+
if frame_id == 1 and self.tracklet_len >= self.minimum_consecutive_frames:
|
|
96
|
+
self.is_activated = True
|
|
97
|
+
self.external_track_id = self.external_id_counter.new_id()
|
|
98
|
+
|
|
99
|
+
self.frame_id = frame_id
|
|
100
|
+
self.start_frame = frame_id
|
|
101
|
+
|
|
102
|
+
def re_activate(self, new_track: STrack, frame_id: int) -> None:
|
|
103
|
+
assert self.kalman_filter is not None
|
|
104
|
+
assert self.mean is not None
|
|
105
|
+
assert self.covariance is not None
|
|
106
|
+
self.mean, self.covariance = self.kalman_filter.update(
|
|
107
|
+
self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh)
|
|
108
|
+
)
|
|
109
|
+
self.tracklet_len = 0
|
|
110
|
+
self.state = TrackState.Tracked
|
|
111
|
+
|
|
112
|
+
self.frame_id = frame_id
|
|
113
|
+
self.score = new_track.score
|
|
114
|
+
|
|
115
|
+
def update(self, new_track: STrack, frame_id: int) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Update a matched track.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
new_track: The new track data.
|
|
121
|
+
frame_id: The current frame ID.
|
|
122
|
+
"""
|
|
123
|
+
assert self.kalman_filter is not None
|
|
124
|
+
assert self.mean is not None
|
|
125
|
+
assert self.covariance is not None
|
|
126
|
+
self.frame_id = frame_id
|
|
127
|
+
self.tracklet_len += 1
|
|
128
|
+
|
|
129
|
+
new_tlwh = new_track.tlwh
|
|
130
|
+
self.mean, self.covariance = self.kalman_filter.update(
|
|
131
|
+
self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh)
|
|
132
|
+
)
|
|
133
|
+
self.state = TrackState.Tracked
|
|
134
|
+
if self.tracklet_len >= self.minimum_consecutive_frames:
|
|
135
|
+
self.is_activated = True
|
|
136
|
+
if self.external_track_id == self.external_id_counter.NO_ID:
|
|
137
|
+
self.external_track_id = self.external_id_counter.new_id()
|
|
138
|
+
|
|
139
|
+
self.score = new_track.score
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def tlwh(self) -> torch.Tensor:
|
|
143
|
+
"""Get current position in bounding box format `(top left x, top left y,
|
|
144
|
+
width, height)`.
|
|
145
|
+
"""
|
|
146
|
+
if self.mean is None:
|
|
147
|
+
return self._tlwh.clone()
|
|
148
|
+
ret = self.mean[:4].clone()
|
|
149
|
+
ret[2] *= ret[3]
|
|
150
|
+
ret[:2] -= ret[2:] / 2
|
|
151
|
+
return ret
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def tlbr(self) -> torch.Tensor:
|
|
155
|
+
"""Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
|
|
156
|
+
`(top left, bottom right)`.
|
|
157
|
+
"""
|
|
158
|
+
ret = self.tlwh.clone()
|
|
159
|
+
ret[2:] += ret[:2]
|
|
160
|
+
return ret
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def tlwh_to_xyah(
|
|
164
|
+
tlwh: npt.NDArray[np.float32] | torch.Tensor,
|
|
165
|
+
) -> torch.Tensor:
|
|
166
|
+
"""Convert bounding box to format `(center x, center y, aspect ratio,
|
|
167
|
+
height)`, where the aspect ratio is `width / height`.
|
|
168
|
+
"""
|
|
169
|
+
ret = torch.as_tensor(tlwh, dtype=torch.float32).clone()
|
|
170
|
+
ret[:2] += ret[2:] / 2
|
|
171
|
+
ret[2] /= ret[3]
|
|
172
|
+
return ret
|
|
173
|
+
|
|
174
|
+
def to_xyah(self) -> torch.Tensor:
|
|
175
|
+
return self.tlwh_to_xyah(self.tlwh)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def tlbr_to_tlwh(
|
|
179
|
+
tlbr: npt.NDArray[np.float32] | torch.Tensor,
|
|
180
|
+
) -> torch.Tensor:
|
|
181
|
+
ret = torch.as_tensor(tlbr, dtype=torch.float32).clone()
|
|
182
|
+
ret[2:] -= ret[:2]
|
|
183
|
+
return ret
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def tlwh_to_tlbr(
|
|
187
|
+
tlwh: npt.NDArray[np.float32] | torch.Tensor,
|
|
188
|
+
) -> torch.Tensor:
|
|
189
|
+
ret = torch.as_tensor(tlwh, dtype=torch.float32).clone()
|
|
190
|
+
ret[2:] += ret[:2]
|
|
191
|
+
return ret
|
|
192
|
+
|
|
193
|
+
def __repr__(self) -> str:
|
|
194
|
+
return f"OT_{self.internal_track_id}_({self.start_frame}-{self.frame_id})"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class IdCounter:
|
|
2
|
+
def __init__(self, start_id: int = 0) -> None:
|
|
3
|
+
"""
|
|
4
|
+
Initialize the ID counter.
|
|
5
|
+
|
|
6
|
+
Args:
|
|
7
|
+
start_id: The starting integer for the counter.
|
|
8
|
+
|
|
9
|
+
Raises:
|
|
10
|
+
ValueError: If start_id is less than or equal to -1.
|
|
11
|
+
"""
|
|
12
|
+
self.start_id = start_id
|
|
13
|
+
if self.start_id <= self.NO_ID:
|
|
14
|
+
raise ValueError(f"start_id must be greater than {self.NO_ID}")
|
|
15
|
+
self.reset()
|
|
16
|
+
|
|
17
|
+
def reset(self) -> None:
|
|
18
|
+
"""Reset the counter to the initial start_id."""
|
|
19
|
+
self._id = self.start_id
|
|
20
|
+
|
|
21
|
+
def new_id(self) -> int:
|
|
22
|
+
"""
|
|
23
|
+
Get the current ID and increment the counter.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The newly assigned ID.
|
|
27
|
+
"""
|
|
28
|
+
returned_id = self._id
|
|
29
|
+
self._id += 1
|
|
30
|
+
return returned_id
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def NO_ID(self) -> int:
|
|
34
|
+
return -1
|
|
File without changes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
from supervision.draw.base import ImageType
|
|
11
|
+
from supervision.utils.deprecate import deprecated, void
|
|
12
|
+
|
|
13
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_cv2_image_for_class_method(
|
|
17
|
+
annotate_func: F,
|
|
18
|
+
) -> F:
|
|
19
|
+
"""
|
|
20
|
+
Decorates `BaseAnnotator.annotate` implementations, converts scene to
|
|
21
|
+
an image type used internally by the annotators, converts back when annotation
|
|
22
|
+
is complete.
|
|
23
|
+
|
|
24
|
+
Assumes the annotators modify the scene in-place.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@functools.wraps(annotate_func)
|
|
31
|
+
def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> Any:
|
|
32
|
+
if isinstance(scene, np.ndarray):
|
|
33
|
+
return annotate_func(self, scene, *args, **kwargs)
|
|
34
|
+
|
|
35
|
+
if isinstance(scene, Image.Image):
|
|
36
|
+
scene_np = pillow_to_cv2(scene)
|
|
37
|
+
annotated_np = annotate_func(self, scene_np, *args, **kwargs)
|
|
38
|
+
scene.paste(cv2_to_pillow(annotated_np))
|
|
39
|
+
return scene
|
|
40
|
+
|
|
41
|
+
raise TypeError(f"Unsupported image type: {type(scene)}")
|
|
42
|
+
|
|
43
|
+
return cast(F, wrapper)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@deprecated( # type: ignore[untyped-decorator]
|
|
47
|
+
target=ensure_cv2_image_for_class_method,
|
|
48
|
+
deprecated_in="0.27.0",
|
|
49
|
+
remove_in="0.31.0",
|
|
50
|
+
)
|
|
51
|
+
def ensure_cv2_image_for_annotation(
|
|
52
|
+
annotate_func: F,
|
|
53
|
+
) -> F:
|
|
54
|
+
return cast(F, void(annotate_func))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ensure_cv2_image_for_standalone_function(
|
|
58
|
+
image_processing_fun: F,
|
|
59
|
+
) -> F:
|
|
60
|
+
"""
|
|
61
|
+
Decorates image processing functions that accept np.ndarray, converting `image` to
|
|
62
|
+
np.ndarray, converts back when processing is complete.
|
|
63
|
+
|
|
64
|
+
Assumes the annotators do NOT modify the scene in-place.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
TypeError: If `image` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@functools.wraps(image_processing_fun)
|
|
71
|
+
def wrapper(image: ImageType, *args: Any, **kwargs: Any) -> Any:
|
|
72
|
+
if isinstance(image, np.ndarray):
|
|
73
|
+
return image_processing_fun(image, *args, **kwargs)
|
|
74
|
+
|
|
75
|
+
if isinstance(image, Image.Image):
|
|
76
|
+
scene = pillow_to_cv2(image)
|
|
77
|
+
annotated = image_processing_fun(scene, *args, **kwargs)
|
|
78
|
+
return cv2_to_pillow(annotated)
|
|
79
|
+
|
|
80
|
+
raise TypeError(f"Unsupported image type: {type(image)}")
|
|
81
|
+
|
|
82
|
+
return cast(F, wrapper)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ensure_pil_image_for_class_method(
|
|
86
|
+
annotate_func: F,
|
|
87
|
+
) -> F:
|
|
88
|
+
"""
|
|
89
|
+
Decorates image processing functions that accept np.ndarray, converting `image` to
|
|
90
|
+
PIL image, converts back when processing is complete.
|
|
91
|
+
|
|
92
|
+
Assumes the annotators modify the scene in-place.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@functools.wraps(annotate_func)
|
|
99
|
+
def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> Any:
|
|
100
|
+
if isinstance(scene, np.ndarray):
|
|
101
|
+
scene_pil = cv2_to_pillow(scene)
|
|
102
|
+
annotated_pil = annotate_func(self, scene_pil, *args, **kwargs)
|
|
103
|
+
np.copyto(scene, pillow_to_cv2(annotated_pil))
|
|
104
|
+
return scene
|
|
105
|
+
|
|
106
|
+
if isinstance(scene, Image.Image):
|
|
107
|
+
return cast(ImageType, annotate_func(self, scene, *args, **kwargs))
|
|
108
|
+
|
|
109
|
+
raise TypeError(f"Unsupported image type: {type(scene)}")
|
|
110
|
+
|
|
111
|
+
return cast(F, wrapper)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@deprecated( # type: ignore[untyped-decorator]
|
|
115
|
+
target=ensure_pil_image_for_class_method,
|
|
116
|
+
deprecated_in="0.27.0",
|
|
117
|
+
remove_in="0.31.0",
|
|
118
|
+
)
|
|
119
|
+
def ensure_pil_image_for_annotation(
|
|
120
|
+
annotate_func: F,
|
|
121
|
+
) -> F:
|
|
122
|
+
return cast(F, void(annotate_func))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@deprecated( # type: ignore[untyped-decorator]
|
|
126
|
+
target=ensure_cv2_image_for_standalone_function,
|
|
127
|
+
deprecated_in="0.27.0",
|
|
128
|
+
remove_in="0.31.0",
|
|
129
|
+
)
|
|
130
|
+
def ensure_cv2_image_for_processing(
|
|
131
|
+
image_processing_fun: F,
|
|
132
|
+
) -> F:
|
|
133
|
+
return cast(F, void(image_processing_fun))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def images_to_cv2(
|
|
137
|
+
images: list[npt.NDArray[np.uint8] | Image.Image],
|
|
138
|
+
) -> list[npt.NDArray[np.uint8]]:
|
|
139
|
+
"""
|
|
140
|
+
Converts images provided either as Pillow images or OpenCV
|
|
141
|
+
images into OpenCV format.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
images: Images to be converted
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of input images in OpenCV format
|
|
148
|
+
(with order preserved).
|
|
149
|
+
|
|
150
|
+
"""
|
|
151
|
+
result: list[npt.NDArray[np.uint8]] = []
|
|
152
|
+
for image in images:
|
|
153
|
+
if isinstance(image, Image.Image):
|
|
154
|
+
result.append(pillow_to_cv2(image))
|
|
155
|
+
else:
|
|
156
|
+
result.append(image)
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def pillow_to_cv2(image: Image.Image) -> npt.NDArray[np.uint8]:
|
|
161
|
+
"""
|
|
162
|
+
Converts Pillow image into OpenCV image, handling RGB -> BGR
|
|
163
|
+
conversion. Palette images are first expanded to RGB so palette indices are
|
|
164
|
+
resolved to their actual colors.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
image: Pillow image in RGB, grayscale, or palette mode.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Input image converted to OpenCV format.
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
```pycon
|
|
174
|
+
>>> from PIL import Image
|
|
175
|
+
>>> from supervision.utils.conversion import pillow_to_cv2
|
|
176
|
+
>>> image = Image.new("RGB", (10, 10), color=(255, 0, 0))
|
|
177
|
+
>>> scene = pillow_to_cv2(image)
|
|
178
|
+
>>> scene.shape
|
|
179
|
+
(10, 10, 3)
|
|
180
|
+
>>> scene[0, 0].tolist()
|
|
181
|
+
[0, 0, 255]
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
"""
|
|
185
|
+
if image.mode == "P":
|
|
186
|
+
image = image.convert("RGB")
|
|
187
|
+
|
|
188
|
+
scene = np.array(image)
|
|
189
|
+
if scene.ndim == 2:
|
|
190
|
+
return cast(npt.NDArray[np.uint8], scene.astype(np.uint8, copy=False))
|
|
191
|
+
scene = cv2.cvtColor(scene, cv2.COLOR_RGB2BGR)
|
|
192
|
+
# cvtColor already returns uint8 here, so astype is a no-op other than the
|
|
193
|
+
# full-image copy it forces; copy=False keeps the dtype guard without it.
|
|
194
|
+
return cast(npt.NDArray[np.uint8], scene.astype(np.uint8, copy=False))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image:
|
|
198
|
+
"""
|
|
199
|
+
Converts OpenCV image into Pillow image, handling BGR -> RGB
|
|
200
|
+
conversion.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
image: OpenCV image (in BGR format).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Input image converted to Pillow format.
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
```pycon
|
|
210
|
+
>>> import numpy as np
|
|
211
|
+
>>> from supervision.utils.conversion import cv2_to_pillow
|
|
212
|
+
>>> scene = np.zeros((10, 10, 3), dtype=np.uint8)
|
|
213
|
+
>>> scene[:, :, 2] = 255
|
|
214
|
+
>>> image = cv2_to_pillow(scene)
|
|
215
|
+
>>> image.size
|
|
216
|
+
(10, 10)
|
|
217
|
+
>>> image.getpixel((0, 0))
|
|
218
|
+
(255, 0, 0)
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
"""
|
|
222
|
+
rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
|
223
|
+
return Image.fromarray(rgb_image)
|