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,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)