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,997 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
|
|
9
|
+
from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes
|
|
10
|
+
from supervision.draw.base import ImageType
|
|
11
|
+
from supervision.draw.color import Color
|
|
12
|
+
from supervision.draw.utils import draw_rounded_rectangle
|
|
13
|
+
from supervision.geometry.core import Rect
|
|
14
|
+
from supervision.key_points.core import KeyPoints
|
|
15
|
+
from supervision.key_points.skeletons import SKELETONS_BY_VERTEX_COUNT
|
|
16
|
+
from supervision.utils.conversion import ensure_cv2_image_for_class_method
|
|
17
|
+
from supervision.utils.logger import _get_logger
|
|
18
|
+
from supervision.utils.tensor import to_numpy
|
|
19
|
+
|
|
20
|
+
logger = _get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _validate_edge_indices(edge: tuple[int, int], vertex_count: int) -> tuple[int, int]:
|
|
24
|
+
"""Validate 1-based skeleton edges and return zero-based vertex indexes."""
|
|
25
|
+
vertex_a, vertex_b = edge
|
|
26
|
+
if not (1 <= vertex_a <= vertex_count and 1 <= vertex_b <= vertex_count):
|
|
27
|
+
raise ValueError(
|
|
28
|
+
"Edge indices must use the 1-based convention and be within the "
|
|
29
|
+
f"available keypoint range [1, {vertex_count}], got {edge}."
|
|
30
|
+
)
|
|
31
|
+
# Public skeleton definitions are 1-based; keypoint arrays are zero-based.
|
|
32
|
+
return vertex_a - 1, vertex_b - 1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseKeyPointAnnotator(ABC):
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VertexAnnotator(BaseKeyPointAnnotator):
|
|
42
|
+
"""
|
|
43
|
+
A class that specializes in drawing skeleton vertices on images. It uses
|
|
44
|
+
specified key points to determine the locations where the vertices should be
|
|
45
|
+
drawn.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
color: Color = Color.ROBOFLOW,
|
|
51
|
+
radius: int = 4,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Args:
|
|
55
|
+
color: The color to use for annotating key points.
|
|
56
|
+
radius: The radius of the circles used to represent the key points.
|
|
57
|
+
"""
|
|
58
|
+
self.color = color
|
|
59
|
+
self.radius = radius
|
|
60
|
+
|
|
61
|
+
@ensure_cv2_image_for_class_method
|
|
62
|
+
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
|
63
|
+
"""
|
|
64
|
+
Annotates the given scene with skeleton vertices based on the provided key
|
|
65
|
+
points. It draws circles at each key point location. Anchors marked as
|
|
66
|
+
not visible via ``key_points.visible`` are skipped.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
scene: The image where skeleton vertices will be drawn. `ImageType` is a
|
|
70
|
+
flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
71
|
+
key_points: A collection of key points where each key point consists of x
|
|
72
|
+
and y coordinates.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
|
76
|
+
or `PIL.Image.Image`)
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```pycon
|
|
83
|
+
>>> import numpy as np
|
|
84
|
+
>>> import supervision as sv
|
|
85
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
86
|
+
>>> key_points = sv.KeyPoints(
|
|
87
|
+
... xy=np.array(
|
|
88
|
+
... [[[400, 200], [300, 500], [500, 500]]],
|
|
89
|
+
... dtype=np.float32,
|
|
90
|
+
... ),
|
|
91
|
+
... class_id=np.array([0]),
|
|
92
|
+
... visible=np.array([[True, True, True]]),
|
|
93
|
+
... )
|
|
94
|
+
>>> annotator = sv.VertexAnnotator(
|
|
95
|
+
... color=sv.Color.ROBOFLOW, radius=10
|
|
96
|
+
... )
|
|
97
|
+
>>> result = annotator.annotate(image.copy(), key_points)
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
"""
|
|
101
|
+
if len(key_points) == 0:
|
|
102
|
+
return scene
|
|
103
|
+
|
|
104
|
+
# OpenCV is the explicit CPU boundary. Transfer each tensor once per call.
|
|
105
|
+
points = to_numpy(key_points.xy)
|
|
106
|
+
visible = to_numpy(key_points.visible)
|
|
107
|
+
for detection_index, xy in enumerate(points):
|
|
108
|
+
for point_index, (x, y) in enumerate(xy):
|
|
109
|
+
if np.allclose((x, y), 0):
|
|
110
|
+
continue
|
|
111
|
+
if visible is not None and not visible[detection_index, point_index]:
|
|
112
|
+
continue
|
|
113
|
+
cv2.circle(
|
|
114
|
+
img=scene,
|
|
115
|
+
center=(int(x), int(y)),
|
|
116
|
+
radius=self.radius,
|
|
117
|
+
color=self.color.as_bgr(),
|
|
118
|
+
thickness=-1,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return scene
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class EdgeAnnotator(BaseKeyPointAnnotator):
|
|
125
|
+
"""
|
|
126
|
+
A class that specializes in drawing skeleton edges on images using specified key
|
|
127
|
+
points. It connects key points with lines to form the skeleton structure.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
color: Color = Color.ROBOFLOW,
|
|
133
|
+
thickness: int = 2,
|
|
134
|
+
edges: (
|
|
135
|
+
Sequence[tuple[int, int]] | dict[int, Sequence[tuple[int, int]]] | None
|
|
136
|
+
) = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Args:
|
|
140
|
+
color: The color to use for the edges.
|
|
141
|
+
thickness: The thickness of the edges.
|
|
142
|
+
edges: The edges to draw. If set to ``None``, will attempt to
|
|
143
|
+
auto-detect the skeleton by vertex count. A
|
|
144
|
+
``Sequence[tuple[int, int]]`` applies a single skeleton to
|
|
145
|
+
every instance. A ``dict[int, Sequence[tuple[int, int]]]``
|
|
146
|
+
maps ``class_id`` to skeleton edges, enabling correct
|
|
147
|
+
rendering for datasets with multiple skeleton types.
|
|
148
|
+
"""
|
|
149
|
+
self.color = color
|
|
150
|
+
self.thickness = thickness
|
|
151
|
+
self.edges = edges
|
|
152
|
+
|
|
153
|
+
@ensure_cv2_image_for_class_method
|
|
154
|
+
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
|
155
|
+
"""
|
|
156
|
+
Annotates the given scene by drawing lines between specified key points to form
|
|
157
|
+
edges. Edges where either endpoint is marked as not visible via
|
|
158
|
+
``key_points.visible`` are skipped.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
scene: The image where skeleton edges will be drawn. `ImageType` is a
|
|
162
|
+
flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
163
|
+
key_points: A collection of key points where each key point consists of x
|
|
164
|
+
and y coordinates.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
|
168
|
+
or `PIL.Image.Image`)
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
Single-skeleton example:
|
|
175
|
+
|
|
176
|
+
```pycon
|
|
177
|
+
>>> import numpy as np
|
|
178
|
+
>>> import supervision as sv
|
|
179
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
180
|
+
>>> key_points = sv.KeyPoints(
|
|
181
|
+
... xy=np.array(
|
|
182
|
+
... [[[400, 200], [300, 500], [500, 500]]],
|
|
183
|
+
... dtype=np.float32,
|
|
184
|
+
... ),
|
|
185
|
+
... class_id=np.array([0]),
|
|
186
|
+
... visible=np.array([[True, True, True]]),
|
|
187
|
+
... )
|
|
188
|
+
>>> annotator = sv.EdgeAnnotator(
|
|
189
|
+
... color=sv.Color.ROBOFLOW,
|
|
190
|
+
... thickness=3,
|
|
191
|
+
... edges=[(1, 2), (1, 3)],
|
|
192
|
+
... )
|
|
193
|
+
>>> result = annotator.annotate(image.copy(), key_points)
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Multi-skeleton example with per-class edges:
|
|
198
|
+
|
|
199
|
+
```pycon
|
|
200
|
+
>>> import numpy as np
|
|
201
|
+
>>> import supervision as sv
|
|
202
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
203
|
+
>>> key_points = sv.KeyPoints(
|
|
204
|
+
... xy=np.array(
|
|
205
|
+
... [[[400, 200], [300, 500], [500, 500]],
|
|
206
|
+
... [[700, 300], [650, 500], [0, 0]]],
|
|
207
|
+
... dtype=np.float32,
|
|
208
|
+
... ),
|
|
209
|
+
... class_id=np.array([0, 1]),
|
|
210
|
+
... visible=np.array(
|
|
211
|
+
... [[True, True, True],
|
|
212
|
+
... [True, True, False]],
|
|
213
|
+
... ),
|
|
214
|
+
... )
|
|
215
|
+
>>> annotator = sv.EdgeAnnotator(
|
|
216
|
+
... color=sv.Color.ROBOFLOW,
|
|
217
|
+
... thickness=3,
|
|
218
|
+
... edges={0: [(1, 2), (1, 3)], 1: [(1, 2)]},
|
|
219
|
+
... )
|
|
220
|
+
>>> result = annotator.annotate(image.copy(), key_points)
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
"""
|
|
224
|
+
if len(key_points) == 0:
|
|
225
|
+
return scene
|
|
226
|
+
|
|
227
|
+
points = to_numpy(key_points.xy)
|
|
228
|
+
visible = to_numpy(key_points.visible)
|
|
229
|
+
class_ids = to_numpy(key_points.class_id)
|
|
230
|
+
for detection_index, xy in enumerate(points):
|
|
231
|
+
if isinstance(self.edges, dict):
|
|
232
|
+
class_id = (
|
|
233
|
+
int(class_ids[detection_index]) if class_ids is not None else None
|
|
234
|
+
)
|
|
235
|
+
if class_id is None:
|
|
236
|
+
raise ValueError(
|
|
237
|
+
"edges is a dict but class_id is None; "
|
|
238
|
+
"KeyPoints must have class_id set."
|
|
239
|
+
)
|
|
240
|
+
if class_id not in self.edges:
|
|
241
|
+
raise ValueError(f"No edges defined for class_id={class_id}.")
|
|
242
|
+
edges = self.edges[class_id]
|
|
243
|
+
elif self.edges:
|
|
244
|
+
edges = self.edges
|
|
245
|
+
else:
|
|
246
|
+
_looked_up = SKELETONS_BY_VERTEX_COUNT.get(len(xy))
|
|
247
|
+
if not _looked_up:
|
|
248
|
+
logger.warning("No skeleton found with %d vertices", len(xy))
|
|
249
|
+
continue
|
|
250
|
+
edges = _looked_up
|
|
251
|
+
|
|
252
|
+
for edge in edges:
|
|
253
|
+
idx_a, idx_b = _validate_edge_indices(edge=edge, vertex_count=len(xy))
|
|
254
|
+
xy_a = xy[idx_a]
|
|
255
|
+
xy_b = xy[idx_b]
|
|
256
|
+
if np.allclose(xy_a, 0) or np.allclose(xy_b, 0):
|
|
257
|
+
continue
|
|
258
|
+
if visible is not None:
|
|
259
|
+
if (
|
|
260
|
+
not visible[detection_index, idx_a]
|
|
261
|
+
or not visible[detection_index, idx_b]
|
|
262
|
+
):
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
cv2.line(
|
|
266
|
+
img=scene,
|
|
267
|
+
pt1=(int(xy_a[0]), int(xy_a[1])),
|
|
268
|
+
pt2=(int(xy_b[0]), int(xy_b[1])),
|
|
269
|
+
color=self.color.as_bgr(),
|
|
270
|
+
thickness=self.thickness,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return scene
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _BaseVertexEllipseAnnotator(BaseKeyPointAnnotator):
|
|
277
|
+
"""Private base for ellipse-based keypoint annotators.
|
|
278
|
+
|
|
279
|
+
Handles sigma/color validation, sorting, covariance extraction and
|
|
280
|
+
eigendecomposition shared by all VertexEllipse* variants.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(
|
|
284
|
+
self,
|
|
285
|
+
sigma: float | Sequence[float] = (1.0, 2.0, 3.0),
|
|
286
|
+
color: Color | Sequence[Color] = (Color.GREEN, Color.YELLOW, Color.RED),
|
|
287
|
+
max_axis: float | None = None,
|
|
288
|
+
) -> None:
|
|
289
|
+
sigma_seq: Sequence[float] = (
|
|
290
|
+
(sigma,) if isinstance(sigma, (int, float)) else sigma
|
|
291
|
+
)
|
|
292
|
+
color_seq: Sequence[Color] = (color,) if isinstance(color, Color) else color
|
|
293
|
+
|
|
294
|
+
if len(sigma_seq) == 0:
|
|
295
|
+
raise ValueError("sigma must contain at least one value")
|
|
296
|
+
if any(s <= 0 for s in sigma_seq):
|
|
297
|
+
raise ValueError("All sigma values must be positive")
|
|
298
|
+
if max_axis is not None and max_axis <= 0:
|
|
299
|
+
raise ValueError("max_axis must be positive when provided")
|
|
300
|
+
if len(color_seq) != len(sigma_seq):
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"color length ({len(color_seq)}) must match "
|
|
303
|
+
f"sigma length ({len(sigma_seq)})"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
sorted_indices = sorted(
|
|
307
|
+
range(len(sigma_seq)), key=lambda i: sigma_seq[i], reverse=True
|
|
308
|
+
)
|
|
309
|
+
self.sigma = [sigma_seq[i] for i in sorted_indices]
|
|
310
|
+
self.color = [color_seq[i] for i in sorted_indices]
|
|
311
|
+
self.max_axis = max_axis
|
|
312
|
+
|
|
313
|
+
def _get_covariances(self, key_points: KeyPoints) -> npt.NDArray[np.float32]:
|
|
314
|
+
covariances = key_points.data.get("covariance")
|
|
315
|
+
if covariances is None:
|
|
316
|
+
raise ValueError(
|
|
317
|
+
"key_points.data must contain 'covariance' with shape (N, K, 2, 2)."
|
|
318
|
+
)
|
|
319
|
+
covariances_array = cast(
|
|
320
|
+
npt.NDArray[np.float32],
|
|
321
|
+
np.asarray(to_numpy(covariances), dtype=np.float32),
|
|
322
|
+
)
|
|
323
|
+
expected_shape = (*key_points.xy.shape[:2], 2, 2)
|
|
324
|
+
if covariances_array.shape != expected_shape:
|
|
325
|
+
raise ValueError(
|
|
326
|
+
f"Expected covariance shape {expected_shape}, "
|
|
327
|
+
f"got {covariances_array.shape}."
|
|
328
|
+
)
|
|
329
|
+
return covariances_array
|
|
330
|
+
|
|
331
|
+
def _decompose_covariance(
|
|
332
|
+
self, covariance: npt.NDArray[np.float32]
|
|
333
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]] | None:
|
|
334
|
+
"""Eigendecompose a 2x2 covariance, returning sorted (eigenvalues, vectors)."""
|
|
335
|
+
if not np.isfinite(covariance).all():
|
|
336
|
+
return None
|
|
337
|
+
try:
|
|
338
|
+
eigenvalues, eigenvectors = np.linalg.eigh(covariance.astype(np.float64))
|
|
339
|
+
except np.linalg.LinAlgError:
|
|
340
|
+
return None
|
|
341
|
+
if not np.isfinite(eigenvalues).all() or np.any(eigenvalues <= 0):
|
|
342
|
+
return None
|
|
343
|
+
order = np.argsort(eigenvalues)[::-1]
|
|
344
|
+
return eigenvalues[order], eigenvectors[:, order]
|
|
345
|
+
|
|
346
|
+
def _iter_ellipse_params(
|
|
347
|
+
self, key_points: KeyPoints
|
|
348
|
+
) -> list[list[tuple[tuple[int, int], tuple[int, int], float, float, Color]]]:
|
|
349
|
+
"""Return ellipse params grouped by sigma level (outermost first)."""
|
|
350
|
+
covariances = self._get_covariances(key_points)
|
|
351
|
+
points = to_numpy(key_points.xy)
|
|
352
|
+
visible = to_numpy(key_points.visible)
|
|
353
|
+
levels: list[
|
|
354
|
+
list[tuple[tuple[int, int], tuple[int, int], float, float, Color]]
|
|
355
|
+
] = [[] for _ in self.sigma]
|
|
356
|
+
for detection_index, xy in enumerate(points):
|
|
357
|
+
for point_index, (x, y) in enumerate(xy):
|
|
358
|
+
if np.allclose((x, y), 0):
|
|
359
|
+
continue
|
|
360
|
+
if visible is not None and not visible[detection_index, point_index]:
|
|
361
|
+
continue
|
|
362
|
+
covariance = covariances[detection_index, point_index]
|
|
363
|
+
decomposition = self._decompose_covariance(covariance)
|
|
364
|
+
if decomposition is None:
|
|
365
|
+
continue
|
|
366
|
+
eigenvalues, eigenvectors = decomposition
|
|
367
|
+
angle = float(
|
|
368
|
+
np.degrees(np.arctan2(eigenvectors[1, 0], eigenvectors[0, 0]))
|
|
369
|
+
)
|
|
370
|
+
center = (round(x), round(y))
|
|
371
|
+
for level_idx, (sigma, color) in enumerate(zip(self.sigma, self.color)):
|
|
372
|
+
axes = sigma * np.sqrt(eigenvalues)
|
|
373
|
+
if self.max_axis is not None:
|
|
374
|
+
axes = np.minimum(axes, self.max_axis)
|
|
375
|
+
axis_lengths = (
|
|
376
|
+
max(1, round(axes[0])),
|
|
377
|
+
max(1, round(axes[1])),
|
|
378
|
+
)
|
|
379
|
+
levels[level_idx].append(
|
|
380
|
+
(center, axis_lengths, angle, sigma, color)
|
|
381
|
+
)
|
|
382
|
+
return levels
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class VertexEllipseAreaAnnotator(_BaseVertexEllipseAnnotator):
|
|
386
|
+
"""
|
|
387
|
+
Draws filled semi-transparent covariance ellipses at multiple sigma levels
|
|
388
|
+
around each keypoint, each ring in a different color. This produces a
|
|
389
|
+
bullseye-like uncertainty visualization where inner rings represent higher
|
|
390
|
+
probability density.
|
|
391
|
+
|
|
392
|
+
!!! warning
|
|
393
|
+
|
|
394
|
+
This annotator uses `key_points.data["covariance"]` with shape
|
|
395
|
+
`(N, K, 2, 2)` in pixel coordinates.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
def __init__(
|
|
399
|
+
self,
|
|
400
|
+
sigma: float | Sequence[float] = (1.0, 2.0, 3.0),
|
|
401
|
+
color: Color | Sequence[Color] = (Color.GREEN, Color.YELLOW, Color.RED),
|
|
402
|
+
opacity: float = 0.4,
|
|
403
|
+
max_axis: float | None = None,
|
|
404
|
+
) -> None:
|
|
405
|
+
"""
|
|
406
|
+
Args:
|
|
407
|
+
sigma: Sigma multipliers for each ring, drawn from outermost to
|
|
408
|
+
innermost. Accepts a single float or a sequence of floats.
|
|
409
|
+
Defaults to ``(1.0, 2.0, 3.0)``.
|
|
410
|
+
color: The color for each sigma level. Accepts a single
|
|
411
|
+
``Color`` or a sequence of colors (one per sigma level).
|
|
412
|
+
Defaults to ``(Color.GREEN, Color.YELLOW, Color.RED)``.
|
|
413
|
+
opacity: Opacity of the overlay mask. Must be between ``0`` and
|
|
414
|
+
``1``.
|
|
415
|
+
max_axis: Optional cap for ellipse semi-axis lengths in pixels.
|
|
416
|
+
"""
|
|
417
|
+
super().__init__(sigma=sigma, color=color, max_axis=max_axis)
|
|
418
|
+
self.opacity = opacity
|
|
419
|
+
|
|
420
|
+
@ensure_cv2_image_for_class_method
|
|
421
|
+
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
|
422
|
+
"""
|
|
423
|
+
Draws filled semi-transparent covariance ellipses around each keypoint.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
scene: The image to annotate. ``ImageType`` accepts either
|
|
427
|
+
``numpy.ndarray`` or ``PIL.Image.Image``.
|
|
428
|
+
key_points: Key points with covariance data in
|
|
429
|
+
``key_points.data["covariance"]``.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
The annotated image, matching the type of ``scene``.
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
```pycon
|
|
439
|
+
>>> import numpy as np
|
|
440
|
+
>>> import supervision as sv
|
|
441
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
442
|
+
>>> key_points = sv.KeyPoints(
|
|
443
|
+
... xy=np.array(
|
|
444
|
+
... [[[400, 200], [300, 500], [500, 500]]],
|
|
445
|
+
... dtype=np.float32,
|
|
446
|
+
... ),
|
|
447
|
+
... class_id=np.array([0]),
|
|
448
|
+
... visible=np.array([[True, True, True]]),
|
|
449
|
+
... data={
|
|
450
|
+
... "covariance": np.array(
|
|
451
|
+
... [[[[800, 0], [0, 400]],
|
|
452
|
+
... [[400, 0], [0, 800]],
|
|
453
|
+
... [[600, 0], [0, 600]]]],
|
|
454
|
+
... dtype=np.float32,
|
|
455
|
+
... )
|
|
456
|
+
... },
|
|
457
|
+
... )
|
|
458
|
+
>>> annotator = sv.VertexEllipseAreaAnnotator(
|
|
459
|
+
... sigma=[1.0, 2.0],
|
|
460
|
+
... color=[sv.Color.GREEN, sv.Color.RED],
|
|
461
|
+
... )
|
|
462
|
+
>>> result = annotator.annotate(image.copy(), key_points)
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
"""
|
|
466
|
+
if len(key_points) == 0:
|
|
467
|
+
return scene
|
|
468
|
+
|
|
469
|
+
overlay = scene.copy()
|
|
470
|
+
for level in self._iter_ellipse_params(key_points):
|
|
471
|
+
for center, axis_lengths, angle, _sigma, color in level:
|
|
472
|
+
cv2.ellipse(
|
|
473
|
+
img=overlay,
|
|
474
|
+
center=center,
|
|
475
|
+
axes=axis_lengths,
|
|
476
|
+
angle=angle,
|
|
477
|
+
startAngle=0,
|
|
478
|
+
endAngle=360,
|
|
479
|
+
color=color.as_bgr(),
|
|
480
|
+
thickness=-1,
|
|
481
|
+
lineType=cv2.LINE_AA,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
cv2.addWeighted(overlay, self.opacity, scene, 1 - self.opacity, 0, dst=scene)
|
|
485
|
+
return scene
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class VertexEllipseOutlineAnnotator(_BaseVertexEllipseAnnotator):
|
|
489
|
+
"""
|
|
490
|
+
Draws stroke-only concentric covariance ellipse rings at multiple sigma
|
|
491
|
+
levels around each keypoint.
|
|
492
|
+
|
|
493
|
+
!!! warning
|
|
494
|
+
|
|
495
|
+
This annotator uses `key_points.data["covariance"]` with shape
|
|
496
|
+
`(N, K, 2, 2)` in pixel coordinates.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
def __init__(
|
|
500
|
+
self,
|
|
501
|
+
sigma: float | Sequence[float] = (1.0, 2.0, 3.0),
|
|
502
|
+
color: Color | Sequence[Color] = (Color.GREEN, Color.YELLOW, Color.RED),
|
|
503
|
+
thickness: int = 2,
|
|
504
|
+
max_axis: float | None = None,
|
|
505
|
+
) -> None:
|
|
506
|
+
"""
|
|
507
|
+
Args:
|
|
508
|
+
sigma: Sigma multipliers for each ring, drawn from outermost to
|
|
509
|
+
innermost. Accepts a single float or a sequence of floats.
|
|
510
|
+
Defaults to ``(1.0, 2.0, 3.0)``.
|
|
511
|
+
color: The color for each sigma level. Accepts a single
|
|
512
|
+
``Color`` or a sequence of colors (one per sigma level).
|
|
513
|
+
Defaults to ``(Color.GREEN, Color.YELLOW, Color.RED)``.
|
|
514
|
+
thickness: Line thickness of the ellipse outlines.
|
|
515
|
+
max_axis: Optional cap for ellipse semi-axis lengths in pixels.
|
|
516
|
+
"""
|
|
517
|
+
super().__init__(sigma=sigma, color=color, max_axis=max_axis)
|
|
518
|
+
self.thickness = thickness
|
|
519
|
+
|
|
520
|
+
@ensure_cv2_image_for_class_method
|
|
521
|
+
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
|
522
|
+
"""
|
|
523
|
+
Draws stroke-only covariance ellipse outlines around each keypoint.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
scene: The image to annotate. ``ImageType`` accepts either
|
|
527
|
+
``numpy.ndarray`` or ``PIL.Image.Image``.
|
|
528
|
+
key_points: Key points with covariance data in
|
|
529
|
+
``key_points.data["covariance"]``.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
The annotated image, matching the type of ``scene``.
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
```pycon
|
|
539
|
+
>>> import numpy as np
|
|
540
|
+
>>> import supervision as sv
|
|
541
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
542
|
+
>>> key_points = sv.KeyPoints(
|
|
543
|
+
... xy=np.array(
|
|
544
|
+
... [[[400, 200], [300, 500], [500, 500]]],
|
|
545
|
+
... dtype=np.float32,
|
|
546
|
+
... ),
|
|
547
|
+
... class_id=np.array([0]),
|
|
548
|
+
... visible=np.array([[True, True, True]]),
|
|
549
|
+
... data={
|
|
550
|
+
... "covariance": np.array(
|
|
551
|
+
... [[[[800, 0], [0, 400]],
|
|
552
|
+
... [[400, 0], [0, 800]],
|
|
553
|
+
... [[600, 0], [0, 600]]]],
|
|
554
|
+
... dtype=np.float32,
|
|
555
|
+
... )
|
|
556
|
+
... },
|
|
557
|
+
... )
|
|
558
|
+
>>> annotator = sv.VertexEllipseOutlineAnnotator(
|
|
559
|
+
... sigma=[1.0, 2.0],
|
|
560
|
+
... color=[sv.Color.GREEN, sv.Color.RED],
|
|
561
|
+
... thickness=2,
|
|
562
|
+
... )
|
|
563
|
+
>>> result = annotator.annotate(image.copy(), key_points)
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
"""
|
|
567
|
+
if len(key_points) == 0:
|
|
568
|
+
return scene
|
|
569
|
+
|
|
570
|
+
for level in self._iter_ellipse_params(key_points):
|
|
571
|
+
for center, axis_lengths, angle, _sigma, color in level:
|
|
572
|
+
cv2.ellipse(
|
|
573
|
+
img=scene,
|
|
574
|
+
center=center,
|
|
575
|
+
axes=axis_lengths,
|
|
576
|
+
angle=angle,
|
|
577
|
+
startAngle=0,
|
|
578
|
+
endAngle=360,
|
|
579
|
+
color=color.as_bgr(),
|
|
580
|
+
thickness=self.thickness,
|
|
581
|
+
lineType=cv2.LINE_AA,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
return scene
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class VertexEllipseHaloAnnotator(_BaseVertexEllipseAnnotator):
|
|
588
|
+
"""
|
|
589
|
+
Draws filled covariance ellipses with a radial fade: full opacity at the
|
|
590
|
+
center, smoothly falling off to zero at the ellipse boundary. The falloff
|
|
591
|
+
follows a power curve controlled by ``decay``, producing a soft glow that
|
|
592
|
+
is strongest near the keypoint.
|
|
593
|
+
|
|
594
|
+
!!! warning
|
|
595
|
+
|
|
596
|
+
This annotator uses `key_points.data["covariance"]` with shape
|
|
597
|
+
`(N, K, 2, 2)` in pixel coordinates.
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
_DECAY: float = 2.0
|
|
601
|
+
|
|
602
|
+
def __init__(
|
|
603
|
+
self,
|
|
604
|
+
sigma: float | Sequence[float] = (1.0, 2.0, 3.0),
|
|
605
|
+
color: Color | Sequence[Color] = (Color.GREEN, Color.YELLOW, Color.RED),
|
|
606
|
+
opacity: float = 0.6,
|
|
607
|
+
max_axis: float | None = None,
|
|
608
|
+
) -> None:
|
|
609
|
+
"""
|
|
610
|
+
Args:
|
|
611
|
+
sigma: Sigma multipliers for each ring, drawn from outermost to
|
|
612
|
+
innermost. Accepts a single float or a sequence of floats.
|
|
613
|
+
Defaults to ``(1.0, 2.0, 3.0)``.
|
|
614
|
+
color: The color for each sigma level. Accepts a single
|
|
615
|
+
``Color`` or a sequence of colors (one per sigma level).
|
|
616
|
+
Defaults to ``(Color.GREEN, Color.YELLOW, Color.RED)``.
|
|
617
|
+
opacity: Peak opacity at the ellipse center. Must be between ``0``
|
|
618
|
+
and ``1``.
|
|
619
|
+
max_axis: Optional cap for ellipse semi-axis lengths in pixels.
|
|
620
|
+
"""
|
|
621
|
+
super().__init__(sigma=sigma, color=color, max_axis=max_axis)
|
|
622
|
+
self.opacity = opacity
|
|
623
|
+
|
|
624
|
+
@ensure_cv2_image_for_class_method
|
|
625
|
+
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
|
626
|
+
"""
|
|
627
|
+
Draws radially-fading covariance ellipses around each keypoint.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
scene: The image to annotate. ``ImageType`` accepts either
|
|
631
|
+
``numpy.ndarray`` or ``PIL.Image.Image``.
|
|
632
|
+
key_points: Key points with covariance data in
|
|
633
|
+
``key_points.data["covariance"]``.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
The annotated image, matching the type of ``scene``.
|
|
637
|
+
|
|
638
|
+
Raises:
|
|
639
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
640
|
+
|
|
641
|
+
Example:
|
|
642
|
+
```pycon
|
|
643
|
+
>>> import numpy as np
|
|
644
|
+
>>> import supervision as sv
|
|
645
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
646
|
+
>>> key_points = sv.KeyPoints(
|
|
647
|
+
... xy=np.array(
|
|
648
|
+
... [[[400, 200], [300, 500], [500, 500]]],
|
|
649
|
+
... dtype=np.float32,
|
|
650
|
+
... ),
|
|
651
|
+
... class_id=np.array([0]),
|
|
652
|
+
... visible=np.array([[True, True, True]]),
|
|
653
|
+
... data={
|
|
654
|
+
... "covariance": np.array(
|
|
655
|
+
... [[[[800, 0], [0, 400]],
|
|
656
|
+
... [[400, 0], [0, 800]],
|
|
657
|
+
... [[600, 0], [0, 600]]]],
|
|
658
|
+
... dtype=np.float32,
|
|
659
|
+
... )
|
|
660
|
+
... },
|
|
661
|
+
... )
|
|
662
|
+
>>> annotator = sv.VertexEllipseHaloAnnotator(
|
|
663
|
+
... sigma=[1.0, 2.0],
|
|
664
|
+
... color=[sv.Color.GREEN, sv.Color.RED],
|
|
665
|
+
... )
|
|
666
|
+
>>> result = annotator.annotate(image.copy(), key_points)
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
"""
|
|
670
|
+
if len(key_points) == 0:
|
|
671
|
+
return scene
|
|
672
|
+
|
|
673
|
+
h, w = scene.shape[:2]
|
|
674
|
+
composite: npt.NDArray[np.float32] = scene.astype(np.float32)
|
|
675
|
+
|
|
676
|
+
for level in self._iter_ellipse_params(key_points):
|
|
677
|
+
for center, axis_lengths, angle, _sigma, color in level:
|
|
678
|
+
ax, ay = axis_lengths
|
|
679
|
+
if ax == 0 or ay == 0:
|
|
680
|
+
continue
|
|
681
|
+
|
|
682
|
+
pad = 2
|
|
683
|
+
roi_half_w = ax + pad
|
|
684
|
+
roi_half_h = ay + pad
|
|
685
|
+
cx, cy = center
|
|
686
|
+
|
|
687
|
+
x_min = max(cx - roi_half_w, 0)
|
|
688
|
+
x_max = min(cx + roi_half_w, w)
|
|
689
|
+
y_min = max(cy - roi_half_h, 0)
|
|
690
|
+
y_max = min(cy + roi_half_h, h)
|
|
691
|
+
if x_min >= x_max or y_min >= y_max:
|
|
692
|
+
continue
|
|
693
|
+
|
|
694
|
+
ys = np.arange(y_min, y_max, dtype=np.float32) - cy
|
|
695
|
+
xs = np.arange(x_min, x_max, dtype=np.float32) - cx
|
|
696
|
+
grid_x, grid_y = np.meshgrid(xs, ys)
|
|
697
|
+
|
|
698
|
+
angle_rad = np.radians(-angle)
|
|
699
|
+
cos_a = np.cos(angle_rad)
|
|
700
|
+
sin_a = np.sin(angle_rad)
|
|
701
|
+
rx = grid_x * cos_a - grid_y * sin_a
|
|
702
|
+
ry = grid_x * sin_a + grid_y * cos_a
|
|
703
|
+
|
|
704
|
+
dist_sq = (rx / ax) ** 2 + (ry / ay) ** 2
|
|
705
|
+
inside = dist_sq <= 1.0
|
|
706
|
+
|
|
707
|
+
falloff = np.zeros_like(dist_sq)
|
|
708
|
+
falloff[inside] = (1.0 - dist_sq[inside]) ** self._DECAY
|
|
709
|
+
|
|
710
|
+
scaled_alpha = falloff * self.opacity
|
|
711
|
+
|
|
712
|
+
bgr = np.array(color.as_bgr(), dtype=np.float32)
|
|
713
|
+
roi = composite[y_min:y_max, x_min:x_max]
|
|
714
|
+
alpha_3 = scaled_alpha[:, :, np.newaxis]
|
|
715
|
+
roi[:] = roi * (1 - alpha_3) + bgr * alpha_3
|
|
716
|
+
|
|
717
|
+
np.copyto(scene, composite.astype(np.uint8))
|
|
718
|
+
return scene
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
VertexEllipseAnnotator = VertexEllipseAreaAnnotator
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class VertexLabelAnnotator:
|
|
725
|
+
"""
|
|
726
|
+
A class that draws labels of skeleton vertices on images. It uses specified key
|
|
727
|
+
points to determine the locations where the vertices should be drawn.
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
def __init__(
|
|
731
|
+
self,
|
|
732
|
+
color: Color | list[Color] = Color.ROBOFLOW,
|
|
733
|
+
text_color: Color | list[Color] = Color.WHITE,
|
|
734
|
+
text_scale: float = 0.5,
|
|
735
|
+
text_thickness: int = 1,
|
|
736
|
+
text_padding: int = 10,
|
|
737
|
+
border_radius: int = 0,
|
|
738
|
+
smart_position: bool = False,
|
|
739
|
+
):
|
|
740
|
+
"""
|
|
741
|
+
Args:
|
|
742
|
+
color: The color to use for each keypoint label. If a list is
|
|
743
|
+
provided, the colors will be used in order for each keypoint.
|
|
744
|
+
text_color: The color to use for the labels. If a list is
|
|
745
|
+
provided, the colors will be used in order for each keypoint.
|
|
746
|
+
text_scale: The scale of the text.
|
|
747
|
+
text_thickness: The thickness of the text.
|
|
748
|
+
text_padding: The padding around the text.
|
|
749
|
+
border_radius: The radius of the rounded corners of the boxes.
|
|
750
|
+
Set to a high value to produce circles.
|
|
751
|
+
smart_position: Spread out the labels to avoid overlap.
|
|
752
|
+
"""
|
|
753
|
+
self.border_radius: int = border_radius
|
|
754
|
+
self.color: Color | list[Color] = color
|
|
755
|
+
self.text_color: Color | list[Color] = text_color
|
|
756
|
+
self.text_scale: float = text_scale
|
|
757
|
+
self.text_thickness: int = text_thickness
|
|
758
|
+
self.text_padding: int = text_padding
|
|
759
|
+
self.smart_position = smart_position
|
|
760
|
+
|
|
761
|
+
@ensure_cv2_image_for_class_method
|
|
762
|
+
def annotate(
|
|
763
|
+
self,
|
|
764
|
+
scene: ImageType,
|
|
765
|
+
key_points: KeyPoints,
|
|
766
|
+
labels: list[str] | dict[int, list[str]] | None = None,
|
|
767
|
+
) -> ImageType:
|
|
768
|
+
"""
|
|
769
|
+
Draws labels at skeleton vertex positions on the image. Vertices
|
|
770
|
+
marked not visible via ``key_points.visible`` are skipped.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
scene: The image where vertex labels will be drawn. `ImageType` is a
|
|
774
|
+
flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
|
775
|
+
key_points: A collection of key points where each key point consists of x
|
|
776
|
+
and y coordinates.
|
|
777
|
+
labels: Labels to display at each keypoint. If ``None``, keypoint
|
|
778
|
+
indices are used. A ``list[str]`` applies the same labels to
|
|
779
|
+
every instance. A ``dict[int, list[str]]`` maps ``class_id``
|
|
780
|
+
to per-class label lists, enabling correct labeling for
|
|
781
|
+
datasets with multiple skeleton types.
|
|
782
|
+
|
|
783
|
+
Returns:
|
|
784
|
+
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
|
785
|
+
or `PIL.Image.Image`)
|
|
786
|
+
|
|
787
|
+
Raises:
|
|
788
|
+
TypeError: If `scene` is not a `numpy.ndarray` or `PIL.Image.Image`.
|
|
789
|
+
|
|
790
|
+
Example:
|
|
791
|
+
Single-skeleton example:
|
|
792
|
+
|
|
793
|
+
```pycon
|
|
794
|
+
>>> import numpy as np
|
|
795
|
+
>>> import supervision as sv
|
|
796
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
797
|
+
>>> key_points = sv.KeyPoints(
|
|
798
|
+
... xy=np.array(
|
|
799
|
+
... [[[400, 200], [300, 500], [500, 500]]],
|
|
800
|
+
... dtype=np.float32,
|
|
801
|
+
... ),
|
|
802
|
+
... class_id=np.array([0]),
|
|
803
|
+
... visible=np.array([[True, True, True]]),
|
|
804
|
+
... )
|
|
805
|
+
>>> annotator = sv.VertexLabelAnnotator(
|
|
806
|
+
... color=sv.Color.ROBOFLOW,
|
|
807
|
+
... text_color=sv.Color.WHITE,
|
|
808
|
+
... border_radius=5,
|
|
809
|
+
... )
|
|
810
|
+
>>> result = annotator.annotate(
|
|
811
|
+
... scene=image.copy(),
|
|
812
|
+
... key_points=key_points,
|
|
813
|
+
... labels=["head", "L-foot", "R-foot"],
|
|
814
|
+
... )
|
|
815
|
+
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
Multi-skeleton example with per-class labels:
|
|
819
|
+
|
|
820
|
+
```pycon
|
|
821
|
+
>>> import numpy as np
|
|
822
|
+
>>> import supervision as sv
|
|
823
|
+
>>> image = np.zeros((800, 800, 3), dtype=np.uint8)
|
|
824
|
+
>>> key_points = sv.KeyPoints(
|
|
825
|
+
... xy=np.array(
|
|
826
|
+
... [[[400, 200], [300, 500], [500, 500]],
|
|
827
|
+
... [[700, 300], [650, 500], [0, 0]]],
|
|
828
|
+
... dtype=np.float32,
|
|
829
|
+
... ),
|
|
830
|
+
... class_id=np.array([0, 1]),
|
|
831
|
+
... visible=np.array(
|
|
832
|
+
... [[True, True, True],
|
|
833
|
+
... [True, True, False]],
|
|
834
|
+
... ),
|
|
835
|
+
... )
|
|
836
|
+
>>> annotator = sv.VertexLabelAnnotator(
|
|
837
|
+
... color=sv.Color.ROBOFLOW,
|
|
838
|
+
... text_color=sv.Color.WHITE,
|
|
839
|
+
... border_radius=5,
|
|
840
|
+
... )
|
|
841
|
+
>>> result = annotator.annotate(
|
|
842
|
+
... scene=image.copy(),
|
|
843
|
+
... key_points=key_points,
|
|
844
|
+
... labels={
|
|
845
|
+
... 0: ["head", "L-foot", "R-foot"],
|
|
846
|
+
... 1: ["top", "bottom", "pad"],
|
|
847
|
+
... },
|
|
848
|
+
... )
|
|
849
|
+
|
|
850
|
+
```
|
|
851
|
+
"""
|
|
852
|
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
|
853
|
+
|
|
854
|
+
points = to_numpy(key_points.xy)
|
|
855
|
+
visible = to_numpy(key_points.visible)
|
|
856
|
+
class_ids = to_numpy(key_points.class_id)
|
|
857
|
+
skeletons_count, points_count, _ = points.shape
|
|
858
|
+
if skeletons_count == 0:
|
|
859
|
+
return scene
|
|
860
|
+
|
|
861
|
+
all_anchors: list[tuple[int, int]] = []
|
|
862
|
+
all_labels: list[str] = []
|
|
863
|
+
all_colors: list[Color] = []
|
|
864
|
+
all_text_colors: list[Color] = []
|
|
865
|
+
|
|
866
|
+
for i in range(skeletons_count):
|
|
867
|
+
xy = points[i]
|
|
868
|
+
|
|
869
|
+
class_id = int(class_ids[i]) if class_ids is not None else None
|
|
870
|
+
instance_labels = self._resolve_labels(labels, points_count, class_id)
|
|
871
|
+
instance_colors = self._resolve_color_list(self.color, points_count)
|
|
872
|
+
instance_text_colors = self._resolve_color_list(
|
|
873
|
+
self.text_color, points_count
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
for j in range(points_count):
|
|
877
|
+
if visible is not None:
|
|
878
|
+
if not visible[i, j]:
|
|
879
|
+
continue
|
|
880
|
+
elif np.allclose(xy[j], 0):
|
|
881
|
+
continue
|
|
882
|
+
|
|
883
|
+
anchor = (int(xy[j][0]), int(xy[j][1]))
|
|
884
|
+
all_anchors.append(anchor)
|
|
885
|
+
all_labels.append(instance_labels[j])
|
|
886
|
+
all_colors.append(instance_colors[j])
|
|
887
|
+
all_text_colors.append(instance_text_colors[j])
|
|
888
|
+
|
|
889
|
+
if not all_anchors:
|
|
890
|
+
return scene
|
|
891
|
+
|
|
892
|
+
xyxy = np.array(
|
|
893
|
+
[
|
|
894
|
+
self.get_text_bounding_box(
|
|
895
|
+
text=label,
|
|
896
|
+
font=font,
|
|
897
|
+
text_scale=self.text_scale,
|
|
898
|
+
text_thickness=self.text_thickness,
|
|
899
|
+
center_coordinates=anchor,
|
|
900
|
+
)
|
|
901
|
+
for anchor, label in zip(all_anchors, all_labels)
|
|
902
|
+
]
|
|
903
|
+
)
|
|
904
|
+
xyxy_padded = pad_boxes(xyxy=xyxy, px=self.text_padding)
|
|
905
|
+
|
|
906
|
+
if self.smart_position:
|
|
907
|
+
xyxy_padded = spread_out_boxes(xyxy_padded)
|
|
908
|
+
xyxy = pad_boxes(xyxy=xyxy_padded, px=-self.text_padding)
|
|
909
|
+
|
|
910
|
+
for text, color, text_color, box, box_padded in zip(
|
|
911
|
+
all_labels, all_colors, all_text_colors, xyxy, xyxy_padded
|
|
912
|
+
):
|
|
913
|
+
draw_rounded_rectangle(
|
|
914
|
+
scene=scene,
|
|
915
|
+
rect=Rect.from_xyxy(box_padded),
|
|
916
|
+
color=color,
|
|
917
|
+
border_radius=self.border_radius,
|
|
918
|
+
)
|
|
919
|
+
cv2.putText(
|
|
920
|
+
img=scene,
|
|
921
|
+
text=text,
|
|
922
|
+
org=(box[0], box[3]),
|
|
923
|
+
fontFace=font,
|
|
924
|
+
fontScale=self.text_scale,
|
|
925
|
+
color=text_color.as_bgr(),
|
|
926
|
+
thickness=self.text_thickness,
|
|
927
|
+
lineType=cv2.LINE_AA,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
return scene
|
|
931
|
+
|
|
932
|
+
@staticmethod
|
|
933
|
+
def get_text_bounding_box(
|
|
934
|
+
text: str,
|
|
935
|
+
font: int,
|
|
936
|
+
text_scale: float,
|
|
937
|
+
text_thickness: int,
|
|
938
|
+
center_coordinates: tuple[int, int],
|
|
939
|
+
) -> tuple[int, int, int, int]:
|
|
940
|
+
text_w, text_h = cv2.getTextSize(
|
|
941
|
+
text=text,
|
|
942
|
+
fontFace=font,
|
|
943
|
+
fontScale=text_scale,
|
|
944
|
+
thickness=text_thickness,
|
|
945
|
+
)[0]
|
|
946
|
+
center_x, center_y = center_coordinates
|
|
947
|
+
return (
|
|
948
|
+
center_x - text_w // 2,
|
|
949
|
+
center_y - text_h // 2,
|
|
950
|
+
center_x + text_w // 2,
|
|
951
|
+
center_y + text_h // 2,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
@staticmethod
|
|
955
|
+
def _resolve_labels(
|
|
956
|
+
labels: list[str] | dict[int, list[str]] | None,
|
|
957
|
+
points_count: int,
|
|
958
|
+
class_id: int | None = None,
|
|
959
|
+
) -> list[str]:
|
|
960
|
+
"""Return the label list for a single instance."""
|
|
961
|
+
if labels is None:
|
|
962
|
+
return [str(j) for j in range(points_count)]
|
|
963
|
+
|
|
964
|
+
resolved: list[str]
|
|
965
|
+
if isinstance(labels, dict):
|
|
966
|
+
if class_id is None:
|
|
967
|
+
raise ValueError(
|
|
968
|
+
"labels is a dict but class_id is None; "
|
|
969
|
+
"KeyPoints must have class_id set."
|
|
970
|
+
)
|
|
971
|
+
if class_id not in labels:
|
|
972
|
+
raise ValueError(f"No labels defined for class_id={class_id}.")
|
|
973
|
+
resolved = labels[class_id]
|
|
974
|
+
else:
|
|
975
|
+
resolved = labels
|
|
976
|
+
|
|
977
|
+
if len(resolved) != points_count:
|
|
978
|
+
raise ValueError(
|
|
979
|
+
f"Number of labels ({len(resolved)}) must match "
|
|
980
|
+
f"number of key points ({points_count})."
|
|
981
|
+
)
|
|
982
|
+
return resolved
|
|
983
|
+
|
|
984
|
+
@staticmethod
|
|
985
|
+
def _resolve_color_list(
|
|
986
|
+
colors: Color | list[Color],
|
|
987
|
+
points_count: int,
|
|
988
|
+
) -> list[Color]:
|
|
989
|
+
"""Return a per-keypoint color list for a single instance."""
|
|
990
|
+
if isinstance(colors, list):
|
|
991
|
+
if len(colors) != points_count:
|
|
992
|
+
raise ValueError(
|
|
993
|
+
f"Number of colors ({len(colors)}) must match "
|
|
994
|
+
f"number of key points ({points_count})."
|
|
995
|
+
)
|
|
996
|
+
return colors
|
|
997
|
+
return [colors] * points_count
|