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.
Files changed (96) hide show
  1. superiorvision/__init__.py +19 -0
  2. superiorvision-0.30.0.dev0.dist-info/METADATA +398 -0
  3. superiorvision-0.30.0.dev0.dist-info/RECORD +96 -0
  4. superiorvision-0.30.0.dev0.dist-info/WHEEL +5 -0
  5. superiorvision-0.30.0.dev0.dist-info/licenses/LICENSE.md +9 -0
  6. superiorvision-0.30.0.dev0.dist-info/top_level.txt +2 -0
  7. supervision/__init__.py +318 -0
  8. supervision/annotators/__init__.py +0 -0
  9. supervision/annotators/base.py +21 -0
  10. supervision/annotators/core.py +3551 -0
  11. supervision/annotators/utils.py +541 -0
  12. supervision/assets/__init__.py +4 -0
  13. supervision/assets/downloader.py +166 -0
  14. supervision/assets/list.py +84 -0
  15. supervision/classification/__init__.py +0 -0
  16. supervision/classification/core.py +216 -0
  17. supervision/config.py +13 -0
  18. supervision/dataset/__init__.py +0 -0
  19. supervision/dataset/core.py +1313 -0
  20. supervision/dataset/formats/__init__.py +0 -0
  21. supervision/dataset/formats/coco.py +708 -0
  22. supervision/dataset/formats/createml.py +331 -0
  23. supervision/dataset/formats/labelme.py +405 -0
  24. supervision/dataset/formats/pascal_voc.py +449 -0
  25. supervision/dataset/formats/yolo.py +502 -0
  26. supervision/dataset/utils.py +265 -0
  27. supervision/detection/__init__.py +0 -0
  28. supervision/detection/compact_mask.py +1927 -0
  29. supervision/detection/core.py +3864 -0
  30. supervision/detection/line_zone.py +924 -0
  31. supervision/detection/overlap_filter.py +426 -0
  32. supervision/detection/tensor_utils.py +1596 -0
  33. supervision/detection/tensor_views.py +48 -0
  34. supervision/detection/tools/__init__.py +0 -0
  35. supervision/detection/tools/csv_sink.py +242 -0
  36. supervision/detection/tools/inference_slicer.py +776 -0
  37. supervision/detection/tools/json_sink.py +215 -0
  38. supervision/detection/tools/polygon_zone.py +266 -0
  39. supervision/detection/tools/smoother.py +218 -0
  40. supervision/detection/tools/transformers.py +265 -0
  41. supervision/detection/utils/__init__.py +12 -0
  42. supervision/detection/utils/_typing.py +11 -0
  43. supervision/detection/utils/boxes.py +510 -0
  44. supervision/detection/utils/converters.py +830 -0
  45. supervision/detection/utils/internal.py +806 -0
  46. supervision/detection/utils/iou_and_nms.py +1606 -0
  47. supervision/detection/utils/masks.py +625 -0
  48. supervision/detection/utils/polygons.py +128 -0
  49. supervision/detection/utils/vlms.py +97 -0
  50. supervision/detection/vlm.py +945 -0
  51. supervision/draw/__init__.py +0 -0
  52. supervision/draw/base.py +14 -0
  53. supervision/draw/color.py +598 -0
  54. supervision/draw/utils.py +527 -0
  55. supervision/geometry/__init__.py +0 -0
  56. supervision/geometry/core.py +208 -0
  57. supervision/geometry/utils.py +54 -0
  58. supervision/key_points/__init__.py +0 -0
  59. supervision/key_points/annotators.py +997 -0
  60. supervision/key_points/core.py +1506 -0
  61. supervision/key_points/skeletons.py +2646 -0
  62. supervision/keypoint/__init__.py +13 -0
  63. supervision/keypoint/annotators.py +13 -0
  64. supervision/keypoint/core.py +8 -0
  65. supervision/metrics/__init__.py +40 -0
  66. supervision/metrics/core.py +74 -0
  67. supervision/metrics/detection.py +1632 -0
  68. supervision/metrics/f1_score.py +815 -0
  69. supervision/metrics/mean_average_precision.py +1723 -0
  70. supervision/metrics/mean_average_recall.py +734 -0
  71. supervision/metrics/precision.py +815 -0
  72. supervision/metrics/recall.py +774 -0
  73. supervision/metrics/utils/__init__.py +0 -0
  74. supervision/metrics/utils/matching.py +56 -0
  75. supervision/metrics/utils/object_size.py +256 -0
  76. supervision/metrics/utils/utils.py +9 -0
  77. supervision/py.typed +0 -0
  78. supervision/tracker/__init__.py +5 -0
  79. supervision/tracker/byte_tracker/__init__.py +0 -0
  80. supervision/tracker/byte_tracker/core.py +448 -0
  81. supervision/tracker/byte_tracker/kalman_filter.py +186 -0
  82. supervision/tracker/byte_tracker/matching.py +211 -0
  83. supervision/tracker/byte_tracker/single_object_track.py +194 -0
  84. supervision/tracker/byte_tracker/utils.py +34 -0
  85. supervision/utils/__init__.py +0 -0
  86. supervision/utils/conversion.py +223 -0
  87. supervision/utils/deprecate.py +54 -0
  88. supervision/utils/file.py +209 -0
  89. supervision/utils/image.py +988 -0
  90. supervision/utils/internal.py +209 -0
  91. supervision/utils/iterables.py +103 -0
  92. supervision/utils/logger.py +61 -0
  93. supervision/utils/notebook.py +124 -0
  94. supervision/utils/tensor.py +362 -0
  95. supervision/utils/video.py +533 -0
  96. 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