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,1313 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import chain
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
import cv2
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
from tqdm.auto import tqdm
|
|
16
|
+
|
|
17
|
+
from supervision.classification.core import Classifications
|
|
18
|
+
from supervision.config import CLASS_NAME_DATA_FIELD
|
|
19
|
+
from supervision.dataset.formats.coco import (
|
|
20
|
+
load_coco_annotations,
|
|
21
|
+
save_coco_annotations,
|
|
22
|
+
)
|
|
23
|
+
from supervision.dataset.formats.createml import (
|
|
24
|
+
load_createml_annotations,
|
|
25
|
+
save_createml_annotations,
|
|
26
|
+
)
|
|
27
|
+
from supervision.dataset.formats.labelme import (
|
|
28
|
+
load_labelme_annotations,
|
|
29
|
+
save_labelme_annotations,
|
|
30
|
+
)
|
|
31
|
+
from supervision.dataset.formats.pascal_voc import (
|
|
32
|
+
load_pascal_voc_annotations,
|
|
33
|
+
save_pascal_voc_annotations,
|
|
34
|
+
)
|
|
35
|
+
from supervision.dataset.formats.yolo import (
|
|
36
|
+
load_yolo_annotations,
|
|
37
|
+
save_data_yaml,
|
|
38
|
+
save_yolo_annotations,
|
|
39
|
+
)
|
|
40
|
+
from supervision.dataset.utils import (
|
|
41
|
+
build_class_index_mapping,
|
|
42
|
+
check_no_basename_collisions,
|
|
43
|
+
map_detections_class_id,
|
|
44
|
+
merge_class_lists,
|
|
45
|
+
save_dataset_images,
|
|
46
|
+
train_test_split,
|
|
47
|
+
)
|
|
48
|
+
from supervision.detection.core import Detections
|
|
49
|
+
from supervision.utils.internal import warn_deprecated
|
|
50
|
+
from supervision.utils.iterables import find_duplicates
|
|
51
|
+
|
|
52
|
+
_IMAGE_FILE_EXTENSIONS = frozenset(
|
|
53
|
+
{".bmp", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp"}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseDataset(ABC):
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def __len__(self) -> int:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def split(
|
|
64
|
+
self,
|
|
65
|
+
split_ratio: float = 0.8,
|
|
66
|
+
random_state: int | None = None,
|
|
67
|
+
shuffle: bool = True,
|
|
68
|
+
) -> tuple[BaseDataset, BaseDataset]:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DetectionDataset(BaseDataset):
|
|
73
|
+
"""
|
|
74
|
+
Contains information about a detection dataset. Handles lazy image loading
|
|
75
|
+
and annotation retrieval, dataset splitting, conversions into multiple
|
|
76
|
+
formats.
|
|
77
|
+
|
|
78
|
+
Attributes:
|
|
79
|
+
classes: List containing dataset class names.
|
|
80
|
+
images:
|
|
81
|
+
Accepts a list of image paths. Passing a dict
|
|
82
|
+
(``Dict[str, np.ndarray]``) is deprecated in ``0.30.0`` and will
|
|
83
|
+
be removed in ``0.33.0``; use a list of paths instead.
|
|
84
|
+
When a list of paths is provided, images are loaded lazily on
|
|
85
|
+
demand, which is more memory-efficient.
|
|
86
|
+
annotations: Dictionary mapping
|
|
87
|
+
image path to annotations. The dictionary keys match
|
|
88
|
+
match the keys in `images` or entries in the list of
|
|
89
|
+
image paths.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
classes: list[str],
|
|
95
|
+
images: list[str] | dict[str, npt.NDArray[np.uint8]],
|
|
96
|
+
annotations: dict[str, Detections],
|
|
97
|
+
) -> None:
|
|
98
|
+
self.classes = classes
|
|
99
|
+
|
|
100
|
+
if set(images) != set(annotations):
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"The keys of the images and annotations dictionaries must match."
|
|
103
|
+
)
|
|
104
|
+
self.annotations = {
|
|
105
|
+
image_path: deepcopy(annotation)
|
|
106
|
+
for image_path, annotation in annotations.items()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
np_classes = np.array(self.classes)
|
|
110
|
+
for image_path, annotation in self.annotations.items():
|
|
111
|
+
class_ids = annotation.class_id
|
|
112
|
+
if class_ids is None:
|
|
113
|
+
continue
|
|
114
|
+
if not np.issubdtype(class_ids.dtype, np.integer):
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Detection annotation for image {image_path!r} contains "
|
|
117
|
+
f"non-integer class_id values with dtype {class_ids.dtype}."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
invalid_class_ids = class_ids[
|
|
121
|
+
(class_ids < 0) | (class_ids >= len(self.classes))
|
|
122
|
+
]
|
|
123
|
+
if len(invalid_class_ids) > 0:
|
|
124
|
+
valid_range = (
|
|
125
|
+
"empty"
|
|
126
|
+
if len(self.classes) == 0
|
|
127
|
+
else f"[0, {len(self.classes) - 1}]"
|
|
128
|
+
)
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Detection annotation for image {image_path!r} contains "
|
|
131
|
+
f"class_id {int(invalid_class_ids[0])}, outside the valid "
|
|
132
|
+
f"range {valid_range} for {len(self.classes)} classes."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
annotation.data[CLASS_NAME_DATA_FIELD] = np_classes[class_ids]
|
|
136
|
+
|
|
137
|
+
# Eliminate duplicates while preserving order
|
|
138
|
+
self.image_paths = list(dict.fromkeys(images))
|
|
139
|
+
|
|
140
|
+
self._images_in_memory: dict[str, npt.NDArray[np.uint8]] = {}
|
|
141
|
+
if isinstance(images, dict):
|
|
142
|
+
self._images_in_memory = images
|
|
143
|
+
warn_deprecated(
|
|
144
|
+
"Passing a `Dict[str, np.ndarray]` into `DetectionDataset` is "
|
|
145
|
+
"deprecated in `0.30.0` and will be removed in `0.33.0`. Use "
|
|
146
|
+
"a list of paths `List[str]` instead."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]:
|
|
150
|
+
"""Assumes that image is in dataset."""
|
|
151
|
+
if self._images_in_memory:
|
|
152
|
+
return self._images_in_memory[image_path]
|
|
153
|
+
image = cv2.imread(image_path)
|
|
154
|
+
if image is None:
|
|
155
|
+
raise ValueError(f"Could not read image from path: {image_path}")
|
|
156
|
+
return cast(npt.NDArray[np.uint8], image)
|
|
157
|
+
|
|
158
|
+
def __len__(self) -> int:
|
|
159
|
+
return len(self._images_in_memory) or len(self.image_paths)
|
|
160
|
+
|
|
161
|
+
def __getitem__(self, i: int) -> tuple[str, npt.NDArray[np.uint8], Detections]:
|
|
162
|
+
"""
|
|
163
|
+
Returns:
|
|
164
|
+
The image path, image data,
|
|
165
|
+
and its corresponding annotation at index i.
|
|
166
|
+
"""
|
|
167
|
+
image_path = self.image_paths[i]
|
|
168
|
+
image = self._get_image(image_path)
|
|
169
|
+
annotation = self.annotations[image_path]
|
|
170
|
+
return image_path, image, annotation
|
|
171
|
+
|
|
172
|
+
def __iter__(self) -> Iterator[tuple[str, npt.NDArray[np.uint8], Detections]]:
|
|
173
|
+
"""
|
|
174
|
+
Iterate over the images and annotations in the dataset.
|
|
175
|
+
|
|
176
|
+
Yields:
|
|
177
|
+
Tuples containing the image path, image data, and its annotation.
|
|
178
|
+
"""
|
|
179
|
+
for i in range(len(self)):
|
|
180
|
+
image_path, image, annotation = self[i]
|
|
181
|
+
yield image_path, image, annotation
|
|
182
|
+
|
|
183
|
+
def __eq__(self, other: object) -> bool:
|
|
184
|
+
if not isinstance(other, DetectionDataset):
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
if self.classes != other.classes:
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
if self.image_paths != other.image_paths:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
if self._images_in_memory or other._images_in_memory:
|
|
194
|
+
if not np.array_equal(
|
|
195
|
+
list(self._images_in_memory.values()),
|
|
196
|
+
list(other._images_in_memory.values()),
|
|
197
|
+
):
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
if self.annotations != other.annotations:
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def split(
|
|
206
|
+
self,
|
|
207
|
+
split_ratio: float = 0.8,
|
|
208
|
+
random_state: int | None = None,
|
|
209
|
+
shuffle: bool = True,
|
|
210
|
+
) -> tuple[DetectionDataset, DetectionDataset]:
|
|
211
|
+
"""
|
|
212
|
+
Splits the dataset into two parts (training and testing)
|
|
213
|
+
using the provided split_ratio. The input dataset is not mutated.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
split_ratio: The ratio of the training
|
|
217
|
+
set to the entire dataset.
|
|
218
|
+
random_state: The seed for the random number generator.
|
|
219
|
+
This is used for reproducibility.
|
|
220
|
+
shuffle: Whether to shuffle the data before splitting.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
A tuple containing
|
|
224
|
+
the training and testing datasets.
|
|
225
|
+
|
|
226
|
+
Examples:
|
|
227
|
+
```pycon
|
|
228
|
+
>>> import numpy as np
|
|
229
|
+
>>> import supervision as sv
|
|
230
|
+
>>> ds = sv.DetectionDataset(
|
|
231
|
+
... classes=['dog', 'person'],
|
|
232
|
+
... images={
|
|
233
|
+
... 'img1.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
|
|
234
|
+
... 'img2.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
|
|
235
|
+
... },
|
|
236
|
+
... annotations={
|
|
237
|
+
... 'img1.jpg': sv.Detections(xyxy=np.array([[10, 10, 20, 20]])),
|
|
238
|
+
... 'img2.jpg': sv.Detections(xyxy=np.array([[30, 30, 40, 40]])),
|
|
239
|
+
... }
|
|
240
|
+
... )
|
|
241
|
+
>>> train_ds, test_ds = ds.split(split_ratio=0.5, random_state=42)
|
|
242
|
+
>>> len(train_ds), len(test_ds)
|
|
243
|
+
(1, 1)
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
train_paths, test_paths = train_test_split(
|
|
249
|
+
data=self.image_paths,
|
|
250
|
+
train_ratio=split_ratio,
|
|
251
|
+
random_state=random_state,
|
|
252
|
+
shuffle=shuffle,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
train_annotations = {path: self.annotations[path] for path in train_paths}
|
|
256
|
+
test_annotations = {path: self.annotations[path] for path in test_paths}
|
|
257
|
+
|
|
258
|
+
train_dataset = DetectionDataset(
|
|
259
|
+
classes=self.classes,
|
|
260
|
+
images=train_paths,
|
|
261
|
+
annotations=train_annotations,
|
|
262
|
+
)
|
|
263
|
+
test_dataset = DetectionDataset(
|
|
264
|
+
classes=self.classes,
|
|
265
|
+
images=test_paths,
|
|
266
|
+
annotations=test_annotations,
|
|
267
|
+
)
|
|
268
|
+
if self._images_in_memory:
|
|
269
|
+
train_dataset._images_in_memory = {
|
|
270
|
+
path: self._images_in_memory[path] for path in train_paths
|
|
271
|
+
}
|
|
272
|
+
test_dataset._images_in_memory = {
|
|
273
|
+
path: self._images_in_memory[path] for path in test_paths
|
|
274
|
+
}
|
|
275
|
+
return train_dataset, test_dataset
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def merge(cls, dataset_list: list[DetectionDataset]) -> DetectionDataset:
|
|
279
|
+
"""
|
|
280
|
+
Merge a list of `DetectionDataset` objects into a single
|
|
281
|
+
`DetectionDataset` object.
|
|
282
|
+
|
|
283
|
+
This method takes a list of `DetectionDataset` objects and combines
|
|
284
|
+
their respective fields (`classes`, `images`,
|
|
285
|
+
`annotations`) into a single `DetectionDataset` object.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
dataset_list: A list of `DetectionDataset`
|
|
289
|
+
objects to merge.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
A single `DetectionDataset` object containing
|
|
293
|
+
the merged data from the input list.
|
|
294
|
+
|
|
295
|
+
Examples:
|
|
296
|
+
```pycon
|
|
297
|
+
>>> import numpy as np
|
|
298
|
+
>>> import supervision as sv
|
|
299
|
+
>>> ds_1 = sv.DetectionDataset(
|
|
300
|
+
... classes=['dog', 'person'],
|
|
301
|
+
... images={'img1.jpg': np.zeros((100, 100, 3), dtype=np.uint8)},
|
|
302
|
+
... annotations={'img1.jpg': sv.Detections.empty()}
|
|
303
|
+
... )
|
|
304
|
+
>>> len(ds_1)
|
|
305
|
+
1
|
|
306
|
+
>>> ds_1.classes
|
|
307
|
+
['dog', 'person']
|
|
308
|
+
>>> ds_2 = sv.DetectionDataset(
|
|
309
|
+
... classes=['cat'],
|
|
310
|
+
... images={'img2.jpg': np.zeros((100, 100, 3), dtype=np.uint8)},
|
|
311
|
+
... annotations={'img2.jpg': sv.Detections.empty()}
|
|
312
|
+
... )
|
|
313
|
+
>>> len(ds_2)
|
|
314
|
+
1
|
|
315
|
+
>>> ds_2.classes
|
|
316
|
+
['cat']
|
|
317
|
+
>>> ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
|
318
|
+
>>> len(ds_merged)
|
|
319
|
+
2
|
|
320
|
+
>>> ds_merged.classes
|
|
321
|
+
['cat', 'dog', 'person']
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def is_in_memory(dataset: DetectionDataset) -> bool:
|
|
327
|
+
return len(dataset._images_in_memory) > 0 or len(dataset.image_paths) == 0
|
|
328
|
+
|
|
329
|
+
def is_lazy(dataset: DetectionDataset) -> bool:
|
|
330
|
+
return len(dataset._images_in_memory) == 0
|
|
331
|
+
|
|
332
|
+
all_in_memory = all([is_in_memory(dataset) for dataset in dataset_list])
|
|
333
|
+
all_lazy = all([is_lazy(dataset) for dataset in dataset_list])
|
|
334
|
+
if not all_in_memory and not all_lazy:
|
|
335
|
+
raise ValueError(
|
|
336
|
+
"Merging lazy and in-memory DetectionDatasets is not supported."
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
images_in_memory = {}
|
|
340
|
+
for dataset in dataset_list:
|
|
341
|
+
images_in_memory.update(dataset._images_in_memory)
|
|
342
|
+
|
|
343
|
+
image_paths = list(
|
|
344
|
+
chain.from_iterable(dataset.image_paths for dataset in dataset_list)
|
|
345
|
+
)
|
|
346
|
+
image_paths_unique = list(dict.fromkeys(image_paths))
|
|
347
|
+
if len(image_paths) != len(image_paths_unique):
|
|
348
|
+
duplicates = find_duplicates(image_paths)
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"Image paths {duplicates} are not unique across datasets."
|
|
351
|
+
)
|
|
352
|
+
image_paths = image_paths_unique
|
|
353
|
+
|
|
354
|
+
classes = merge_class_lists(
|
|
355
|
+
class_lists=[dataset.classes for dataset in dataset_list]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
annotations = {}
|
|
359
|
+
for dataset in dataset_list:
|
|
360
|
+
annotations.update(dataset.annotations)
|
|
361
|
+
for dataset in dataset_list:
|
|
362
|
+
class_index_mapping = build_class_index_mapping(
|
|
363
|
+
source_classes=dataset.classes, target_classes=classes
|
|
364
|
+
)
|
|
365
|
+
for image_path in dataset.image_paths:
|
|
366
|
+
annotations[image_path] = map_detections_class_id(
|
|
367
|
+
source_to_target_mapping=class_index_mapping,
|
|
368
|
+
detections=annotations[image_path],
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
merged_dataset = cls(
|
|
372
|
+
classes=classes,
|
|
373
|
+
images=image_paths,
|
|
374
|
+
annotations=annotations,
|
|
375
|
+
)
|
|
376
|
+
if all_in_memory:
|
|
377
|
+
merged_dataset._images_in_memory = images_in_memory
|
|
378
|
+
return merged_dataset
|
|
379
|
+
|
|
380
|
+
def as_pascal_voc(
|
|
381
|
+
self,
|
|
382
|
+
images_directory_path: str | None = None,
|
|
383
|
+
annotations_directory_path: str | None = None,
|
|
384
|
+
min_image_area_percentage: float = 0.0,
|
|
385
|
+
max_image_area_percentage: float = 1.0,
|
|
386
|
+
approximation_percentage: float = 0.0,
|
|
387
|
+
show_progress: bool = False,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""
|
|
390
|
+
Exports the dataset to PASCAL VOC format. This method saves the images
|
|
391
|
+
and their corresponding annotations in PASCAL VOC format. Both output
|
|
392
|
+
layouts are preflighted before any files are written so a collision in
|
|
393
|
+
either target fails without partial output.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
images_directory_path: The path to the directory
|
|
397
|
+
where the images should be saved.
|
|
398
|
+
If not provided, images will not be saved.
|
|
399
|
+
annotations_directory_path: The path to
|
|
400
|
+
the directory where the annotations in PASCAL VOC format should be
|
|
401
|
+
saved. If not provided, annotations will not be saved.
|
|
402
|
+
min_image_area_percentage: The minimum percentage of
|
|
403
|
+
detection area relative to
|
|
404
|
+
the image area for a detection to be included.
|
|
405
|
+
Argument is used only for segmentation datasets.
|
|
406
|
+
max_image_area_percentage: The maximum percentage
|
|
407
|
+
of detection area relative to
|
|
408
|
+
the image area for a detection to be included.
|
|
409
|
+
Argument is used only for segmentation datasets.
|
|
410
|
+
approximation_percentage: The percentage of
|
|
411
|
+
polygon points to be removed from the input polygon,
|
|
412
|
+
in the range [0, 1). Argument is used only for segmentation datasets.
|
|
413
|
+
show_progress: If True, display a progress bar during saving.
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
ValueError: If two image paths share the same basename (when
|
|
417
|
+
images_directory_path is set) or the same stem (when
|
|
418
|
+
annotations_directory_path is set), which would cause one
|
|
419
|
+
output file to overwrite another. Rename images to ensure
|
|
420
|
+
unique basenames before exporting a merged dataset.
|
|
421
|
+
"""
|
|
422
|
+
if images_directory_path:
|
|
423
|
+
check_no_basename_collisions(
|
|
424
|
+
image_paths=self.image_paths,
|
|
425
|
+
key=lambda image_path: Path(image_path).name,
|
|
426
|
+
output_kind="image",
|
|
427
|
+
)
|
|
428
|
+
if annotations_directory_path:
|
|
429
|
+
check_no_basename_collisions(
|
|
430
|
+
image_paths=self.image_paths,
|
|
431
|
+
key=lambda image_path: f"{Path(image_path).stem}.xml",
|
|
432
|
+
output_kind="Pascal VOC annotation",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if images_directory_path:
|
|
436
|
+
save_dataset_images(
|
|
437
|
+
dataset=self,
|
|
438
|
+
images_directory_path=images_directory_path,
|
|
439
|
+
show_progress=show_progress,
|
|
440
|
+
)
|
|
441
|
+
if annotations_directory_path:
|
|
442
|
+
save_pascal_voc_annotations(
|
|
443
|
+
dataset=self,
|
|
444
|
+
annotations_directory_path=annotations_directory_path,
|
|
445
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
446
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
447
|
+
approximation_percentage=approximation_percentage,
|
|
448
|
+
show_progress=show_progress,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
def from_pascal_voc(
|
|
453
|
+
cls,
|
|
454
|
+
images_directory_path: str,
|
|
455
|
+
annotations_directory_path: str,
|
|
456
|
+
force_masks: bool = False,
|
|
457
|
+
show_progress: bool = False,
|
|
458
|
+
) -> DetectionDataset:
|
|
459
|
+
"""
|
|
460
|
+
Creates a Dataset instance from PASCAL VOC formatted data.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
images_directory_path: Path to the directory containing the images.
|
|
464
|
+
annotations_directory_path: Path to the directory
|
|
465
|
+
containing the PASCAL VOC XML annotations.
|
|
466
|
+
force_masks: If True, forces masks to
|
|
467
|
+
be loaded for all annotations, regardless of whether they are present.
|
|
468
|
+
show_progress: If True, display a progress bar during loading.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
A DetectionDataset instance containing
|
|
472
|
+
the loaded images and annotations.
|
|
473
|
+
|
|
474
|
+
Examples:
|
|
475
|
+
```python
|
|
476
|
+
import roboflow
|
|
477
|
+
from roboflow import Roboflow
|
|
478
|
+
import supervision as sv
|
|
479
|
+
|
|
480
|
+
roboflow.login()
|
|
481
|
+
|
|
482
|
+
rf = Roboflow()
|
|
483
|
+
|
|
484
|
+
project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
|
|
485
|
+
dataset = project.version(PROJECT_VERSION).download("voc")
|
|
486
|
+
|
|
487
|
+
ds = sv.DetectionDataset.from_pascal_voc(
|
|
488
|
+
images_directory_path=f"{dataset.location}/train/images",
|
|
489
|
+
annotations_directory_path=f"{dataset.location}/train/labels",
|
|
490
|
+
# pass show_progress=True to enable a tqdm progress bar
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
ds.classes
|
|
494
|
+
# ['dog', 'person']
|
|
495
|
+
```
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
classes, image_paths, annotations = load_pascal_voc_annotations(
|
|
499
|
+
images_directory_path=images_directory_path,
|
|
500
|
+
annotations_directory_path=annotations_directory_path,
|
|
501
|
+
force_masks=force_masks,
|
|
502
|
+
show_progress=show_progress,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
return DetectionDataset(
|
|
506
|
+
classes=classes, images=image_paths, annotations=annotations
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def from_yolo(
|
|
511
|
+
cls,
|
|
512
|
+
images_directory_path: str,
|
|
513
|
+
annotations_directory_path: str,
|
|
514
|
+
data_yaml_path: str,
|
|
515
|
+
force_masks: bool = False,
|
|
516
|
+
is_obb: bool = False,
|
|
517
|
+
show_progress: bool = False,
|
|
518
|
+
) -> DetectionDataset:
|
|
519
|
+
"""
|
|
520
|
+
Creates a Dataset instance from YOLO formatted data.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
images_directory_path: The path to the
|
|
524
|
+
directory containing the images.
|
|
525
|
+
annotations_directory_path: The path to the directory
|
|
526
|
+
containing the YOLO annotation files.
|
|
527
|
+
data_yaml_path: The path to the data
|
|
528
|
+
YAML file containing class information.
|
|
529
|
+
force_masks: If True, forces
|
|
530
|
+
masks to be loaded for all annotations,
|
|
531
|
+
regardless of whether they are present.
|
|
532
|
+
is_obb: If True, loads the annotations in OBB format.
|
|
533
|
+
OBB annotations are defined as `[class_id, x, y, x, y, x, y, x, y]`,
|
|
534
|
+
where pairs of [x, y] are box corners.
|
|
535
|
+
show_progress: If True, display a progress bar during loading.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
A DetectionDataset instance
|
|
539
|
+
containing the loaded images and annotations.
|
|
540
|
+
|
|
541
|
+
Examples:
|
|
542
|
+
```python
|
|
543
|
+
import roboflow
|
|
544
|
+
from roboflow import Roboflow
|
|
545
|
+
import supervision as sv
|
|
546
|
+
|
|
547
|
+
roboflow.login()
|
|
548
|
+
rf = Roboflow()
|
|
549
|
+
|
|
550
|
+
project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
|
|
551
|
+
dataset = project.version(PROJECT_VERSION).download("yolov5")
|
|
552
|
+
|
|
553
|
+
ds = sv.DetectionDataset.from_yolo(
|
|
554
|
+
images_directory_path=f"{dataset.location}/train/images",
|
|
555
|
+
annotations_directory_path=f"{dataset.location}/train/labels",
|
|
556
|
+
data_yaml_path=f"{dataset.location}/data.yaml",
|
|
557
|
+
# pass show_progress=True to enable a tqdm progress bar
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
ds.classes
|
|
561
|
+
# ['dog', 'person']
|
|
562
|
+
```
|
|
563
|
+
"""
|
|
564
|
+
classes, image_paths, annotations = load_yolo_annotations(
|
|
565
|
+
images_directory_path=images_directory_path,
|
|
566
|
+
annotations_directory_path=annotations_directory_path,
|
|
567
|
+
data_yaml_path=data_yaml_path,
|
|
568
|
+
force_masks=force_masks,
|
|
569
|
+
is_obb=is_obb,
|
|
570
|
+
show_progress=show_progress,
|
|
571
|
+
)
|
|
572
|
+
return DetectionDataset(
|
|
573
|
+
classes=classes, images=image_paths, annotations=annotations
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
def as_yolo(
|
|
577
|
+
self,
|
|
578
|
+
images_directory_path: str | None = None,
|
|
579
|
+
annotations_directory_path: str | None = None,
|
|
580
|
+
data_yaml_path: str | None = None,
|
|
581
|
+
min_image_area_percentage: float = 0.0,
|
|
582
|
+
max_image_area_percentage: float = 1.0,
|
|
583
|
+
approximation_percentage: float = 0.0,
|
|
584
|
+
is_obb: bool = False,
|
|
585
|
+
show_progress: bool = False,
|
|
586
|
+
) -> None:
|
|
587
|
+
"""
|
|
588
|
+
Exports the dataset to YOLO format. This method saves the
|
|
589
|
+
images and their corresponding annotations in YOLO format.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
images_directory_path: The path to the
|
|
593
|
+
directory where the images should be saved.
|
|
594
|
+
If not provided, images will not be saved.
|
|
595
|
+
annotations_directory_path: The path to the
|
|
596
|
+
directory where the annotations in
|
|
597
|
+
YOLO format should be saved. If not provided,
|
|
598
|
+
annotations will not be saved.
|
|
599
|
+
data_yaml_path: The path where the data.yaml
|
|
600
|
+
file should be saved.
|
|
601
|
+
If not provided, the file will not be saved.
|
|
602
|
+
min_image_area_percentage: The minimum percentage of
|
|
603
|
+
detection area relative to
|
|
604
|
+
the image area for a detection to be included.
|
|
605
|
+
Argument is used only for segmentation datasets.
|
|
606
|
+
max_image_area_percentage: The maximum percentage
|
|
607
|
+
of detection area relative to
|
|
608
|
+
the image area for a detection to be included.
|
|
609
|
+
Argument is used only for segmentation datasets.
|
|
610
|
+
approximation_percentage: The percentage of polygon points to
|
|
611
|
+
be removed from the input polygon, in the range [0, 1).
|
|
612
|
+
This is useful for simplifying the annotations.
|
|
613
|
+
Argument is used only for segmentation datasets.
|
|
614
|
+
is_obb: If True, exports annotations in OBB format
|
|
615
|
+
(`class_id x1 y1 x2 y2 x3 y3 x4 y4`) using the oriented
|
|
616
|
+
corners stored in `detections.data["xyxyxyxy"]`. Mirrors
|
|
617
|
+
`from_yolo(..., is_obb=True)`. Masks are ignored when
|
|
618
|
+
`is_obb=True`.
|
|
619
|
+
show_progress: If True, display a progress bar during saving.
|
|
620
|
+
|
|
621
|
+
Raises:
|
|
622
|
+
ValueError: If two image paths share the same basename (when
|
|
623
|
+
images_directory_path is set) or the same annotation
|
|
624
|
+
file name (when annotations_directory_path is set),
|
|
625
|
+
which would cause one output file to overwrite another.
|
|
626
|
+
"""
|
|
627
|
+
if is_obb and (
|
|
628
|
+
min_image_area_percentage != 0.0
|
|
629
|
+
or max_image_area_percentage != 1.0
|
|
630
|
+
or approximation_percentage != 0.0
|
|
631
|
+
):
|
|
632
|
+
import warnings
|
|
633
|
+
|
|
634
|
+
warnings.warn(
|
|
635
|
+
"`min_image_area_percentage`, `max_image_area_percentage`, and "
|
|
636
|
+
"`approximation_percentage` have no effect when `is_obb=True`; "
|
|
637
|
+
"OBB annotations use corner coordinates directly.",
|
|
638
|
+
UserWarning,
|
|
639
|
+
stacklevel=2,
|
|
640
|
+
)
|
|
641
|
+
# Pre-flight: validate output uniqueness before writing any file
|
|
642
|
+
if images_directory_path:
|
|
643
|
+
check_no_basename_collisions(
|
|
644
|
+
image_paths=self.image_paths,
|
|
645
|
+
key=lambda image_path: Path(image_path).name,
|
|
646
|
+
output_kind="image",
|
|
647
|
+
)
|
|
648
|
+
if annotations_directory_path:
|
|
649
|
+
check_no_basename_collisions(
|
|
650
|
+
image_paths=self.image_paths,
|
|
651
|
+
key=lambda image_path: Path(image_path).stem + ".txt",
|
|
652
|
+
output_kind="YOLO annotation",
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
if images_directory_path is not None:
|
|
656
|
+
save_dataset_images(
|
|
657
|
+
dataset=self,
|
|
658
|
+
images_directory_path=images_directory_path,
|
|
659
|
+
show_progress=show_progress,
|
|
660
|
+
)
|
|
661
|
+
if annotations_directory_path is not None:
|
|
662
|
+
save_yolo_annotations(
|
|
663
|
+
dataset=self,
|
|
664
|
+
annotations_directory_path=annotations_directory_path,
|
|
665
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
666
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
667
|
+
approximation_percentage=approximation_percentage,
|
|
668
|
+
show_progress=show_progress,
|
|
669
|
+
is_obb=is_obb,
|
|
670
|
+
)
|
|
671
|
+
if data_yaml_path is not None:
|
|
672
|
+
save_data_yaml(data_yaml_path=data_yaml_path, classes=self.classes)
|
|
673
|
+
|
|
674
|
+
@classmethod
|
|
675
|
+
def from_labelme(
|
|
676
|
+
cls,
|
|
677
|
+
images_directory_path: str,
|
|
678
|
+
annotations_directory_path: str,
|
|
679
|
+
force_masks: bool = False,
|
|
680
|
+
) -> DetectionDataset:
|
|
681
|
+
"""
|
|
682
|
+
Creates a Dataset instance from LabelMe formatted data.
|
|
683
|
+
|
|
684
|
+
LabelMe stores one JSON file per image, each containing a list of
|
|
685
|
+
``shapes``. ``rectangle`` shapes are loaded as bounding boxes and
|
|
686
|
+
``polygon`` shapes as masks (with their bounding boxes); other shape
|
|
687
|
+
types are skipped. Class names are inferred from the labels present in
|
|
688
|
+
the files. When an image file contains a ``polygon`` shape, or when
|
|
689
|
+
``force_masks=True`` is set, both ``rectangle`` and ``polygon`` shapes
|
|
690
|
+
produce masks: rectangles via a four-corner polygon fill.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
images_directory_path: The path to the
|
|
694
|
+
directory containing the images.
|
|
695
|
+
annotations_directory_path: The path to the directory
|
|
696
|
+
containing the LabelMe ``.json`` annotation files.
|
|
697
|
+
force_masks: If True, forces masks to be loaded for all
|
|
698
|
+
annotations, regardless of whether polygon shapes are present.
|
|
699
|
+
Requires ``imageWidth`` and ``imageHeight`` in every JSON file.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
A DetectionDataset instance containing
|
|
703
|
+
the loaded images and annotations.
|
|
704
|
+
|
|
705
|
+
Raises:
|
|
706
|
+
ValueError: If an annotation is malformed - for example
|
|
707
|
+
``imagePath`` is empty or resolves to ``..``, a shape is
|
|
708
|
+
missing its ``label`` or ``points``, or a mask is required but
|
|
709
|
+
``imageWidth`` / ``imageHeight`` are missing or zero.
|
|
710
|
+
|
|
711
|
+
Examples:
|
|
712
|
+
```python
|
|
713
|
+
import supervision as sv
|
|
714
|
+
|
|
715
|
+
ds = sv.DetectionDataset.from_labelme(
|
|
716
|
+
images_directory_path="<IMAGES_DIRECTORY_PATH>",
|
|
717
|
+
annotations_directory_path="<ANNOTATIONS_DIRECTORY_PATH>",
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
ds.classes
|
|
721
|
+
# ['dog', 'person']
|
|
722
|
+
```
|
|
723
|
+
"""
|
|
724
|
+
classes, image_paths, annotations = load_labelme_annotations(
|
|
725
|
+
images_directory_path=images_directory_path,
|
|
726
|
+
annotations_directory_path=annotations_directory_path,
|
|
727
|
+
force_masks=force_masks,
|
|
728
|
+
)
|
|
729
|
+
return DetectionDataset(
|
|
730
|
+
classes=classes, images=image_paths, annotations=annotations
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
def as_labelme(
|
|
734
|
+
self,
|
|
735
|
+
images_directory_path: str | None = None,
|
|
736
|
+
annotations_directory_path: str | None = None,
|
|
737
|
+
) -> None:
|
|
738
|
+
"""
|
|
739
|
+
Exports the dataset to LabelMe format. This method saves the images and
|
|
740
|
+
their corresponding annotations as per-image LabelMe ``.json`` files.
|
|
741
|
+
Masked detections are written as ``polygon`` shapes whose vertices
|
|
742
|
+
approximate the mask contour, so masks are not bit-exact on round-trip.
|
|
743
|
+
Because the bounding box is recomputed from the quantized polygon contour
|
|
744
|
+
on re-import, bounding boxes for masked detections may also shift by
|
|
745
|
+
approximately one pixel after a save-load cycle.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
images_directory_path: The path to the directory
|
|
749
|
+
where the images should be saved.
|
|
750
|
+
If not provided, images will not be saved.
|
|
751
|
+
annotations_directory_path: The path to the directory where the
|
|
752
|
+
LabelMe ``.json`` files should be saved.
|
|
753
|
+
If not provided, annotations will not be saved.
|
|
754
|
+
|
|
755
|
+
Examples:
|
|
756
|
+
```python
|
|
757
|
+
import supervision as sv
|
|
758
|
+
|
|
759
|
+
ds = sv.DetectionDataset(...)
|
|
760
|
+
|
|
761
|
+
ds.as_labelme(
|
|
762
|
+
images_directory_path="<IMAGES_DIRECTORY_PATH>",
|
|
763
|
+
annotations_directory_path="<ANNOTATIONS_DIRECTORY_PATH>",
|
|
764
|
+
)
|
|
765
|
+
```
|
|
766
|
+
"""
|
|
767
|
+
if images_directory_path is not None:
|
|
768
|
+
save_dataset_images(
|
|
769
|
+
dataset=self, images_directory_path=images_directory_path
|
|
770
|
+
)
|
|
771
|
+
if annotations_directory_path is not None:
|
|
772
|
+
save_labelme_annotations(
|
|
773
|
+
dataset=self,
|
|
774
|
+
annotations_directory_path=annotations_directory_path,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
@classmethod
|
|
778
|
+
def from_createml(
|
|
779
|
+
cls,
|
|
780
|
+
images_directory_path: str,
|
|
781
|
+
annotations_path: str,
|
|
782
|
+
show_progress: bool = False,
|
|
783
|
+
) -> DetectionDataset:
|
|
784
|
+
"""
|
|
785
|
+
Creates a Dataset instance from CreateML formatted data.
|
|
786
|
+
|
|
787
|
+
CreateML stores object-detection annotations in a single JSON file as a
|
|
788
|
+
list of per-image entries, with each box expressed as a pixel-space
|
|
789
|
+
centre point plus width and height. Class names are inferred from the
|
|
790
|
+
labels present in the file.
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
images_directory_path: The path to the directory containing the
|
|
794
|
+
images.
|
|
795
|
+
annotations_path: The path to the CreateML json annotation file.
|
|
796
|
+
show_progress: If True, display a progress bar during loading.
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
A DetectionDataset instance containing the loaded images and
|
|
800
|
+
annotations.
|
|
801
|
+
|
|
802
|
+
Examples:
|
|
803
|
+
```python
|
|
804
|
+
import roboflow
|
|
805
|
+
from roboflow import Roboflow
|
|
806
|
+
import supervision as sv
|
|
807
|
+
|
|
808
|
+
roboflow.login()
|
|
809
|
+
rf = Roboflow()
|
|
810
|
+
|
|
811
|
+
project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
|
|
812
|
+
dataset = project.version(PROJECT_VERSION).download("createml")
|
|
813
|
+
|
|
814
|
+
ds = sv.DetectionDataset.from_createml(
|
|
815
|
+
images_directory_path=f"{dataset.location}/train",
|
|
816
|
+
annotations_path=f"{dataset.location}/train/_annotations.createml.json",
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
ds.classes
|
|
820
|
+
# ['dog', 'person']
|
|
821
|
+
```
|
|
822
|
+
"""
|
|
823
|
+
classes, image_paths, annotations = load_createml_annotations(
|
|
824
|
+
images_directory_path=images_directory_path,
|
|
825
|
+
annotations_path=annotations_path,
|
|
826
|
+
show_progress=show_progress,
|
|
827
|
+
)
|
|
828
|
+
return DetectionDataset(
|
|
829
|
+
classes=classes, images=image_paths, annotations=annotations
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
def as_createml(
|
|
833
|
+
self,
|
|
834
|
+
images_directory_path: str | None = None,
|
|
835
|
+
annotations_path: str | None = None,
|
|
836
|
+
show_progress: bool = False,
|
|
837
|
+
) -> None:
|
|
838
|
+
"""
|
|
839
|
+
Exports the dataset to CreateML format. This method saves the
|
|
840
|
+
images and their corresponding annotations in CreateML format.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
images_directory_path: The path to the directory where the images
|
|
844
|
+
should be saved. If not provided, images will not be saved.
|
|
845
|
+
annotations_path: The path to the CreateML json annotation file.
|
|
846
|
+
If not provided, the annotations will not be saved.
|
|
847
|
+
show_progress: If True, display a progress bar while saving images.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
None. Side-effects only: writes images and/or annotation file.
|
|
851
|
+
|
|
852
|
+
Examples:
|
|
853
|
+
```python
|
|
854
|
+
import supervision as sv
|
|
855
|
+
|
|
856
|
+
ds = sv.DetectionDataset(classes=["dog"], images=[], annotations={})
|
|
857
|
+
ds.as_createml(
|
|
858
|
+
images_directory_path="/tmp/images",
|
|
859
|
+
annotations_path="/tmp/annotations.json",
|
|
860
|
+
)
|
|
861
|
+
```
|
|
862
|
+
"""
|
|
863
|
+
if images_directory_path is not None:
|
|
864
|
+
save_dataset_images(
|
|
865
|
+
dataset=self,
|
|
866
|
+
images_directory_path=images_directory_path,
|
|
867
|
+
show_progress=show_progress,
|
|
868
|
+
)
|
|
869
|
+
if annotations_path is not None:
|
|
870
|
+
save_createml_annotations(
|
|
871
|
+
dataset=self,
|
|
872
|
+
annotations_path=annotations_path,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
@classmethod
|
|
876
|
+
def from_coco(
|
|
877
|
+
cls,
|
|
878
|
+
images_directory_path: str,
|
|
879
|
+
annotations_path: str,
|
|
880
|
+
force_masks: bool = False,
|
|
881
|
+
show_progress: bool = False,
|
|
882
|
+
*,
|
|
883
|
+
use_iscrowd: bool = True,
|
|
884
|
+
) -> DetectionDataset:
|
|
885
|
+
"""
|
|
886
|
+
Creates a Dataset instance from COCO formatted data.
|
|
887
|
+
|
|
888
|
+
Args:
|
|
889
|
+
images_directory_path: The path to the
|
|
890
|
+
directory containing the images.
|
|
891
|
+
annotations_path: The path to the json annotation files.
|
|
892
|
+
force_masks: If True,
|
|
893
|
+
forces masks to be loaded for all annotations,
|
|
894
|
+
regardless of whether they are present.
|
|
895
|
+
show_progress: If True, display a progress bar during loading.
|
|
896
|
+
use_iscrowd: If True, includes COCO ``iscrowd`` and ``area``
|
|
897
|
+
annotation fields in ``Detections.data``.
|
|
898
|
+
Returns:
|
|
899
|
+
A DetectionDataset instance containing
|
|
900
|
+
the loaded images and annotations.
|
|
901
|
+
|
|
902
|
+
Examples:
|
|
903
|
+
```python
|
|
904
|
+
import roboflow
|
|
905
|
+
from roboflow import Roboflow
|
|
906
|
+
import supervision as sv
|
|
907
|
+
|
|
908
|
+
roboflow.login()
|
|
909
|
+
rf = Roboflow()
|
|
910
|
+
|
|
911
|
+
project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
|
|
912
|
+
dataset = project.version(PROJECT_VERSION).download("coco")
|
|
913
|
+
|
|
914
|
+
ds = sv.DetectionDataset.from_coco(
|
|
915
|
+
images_directory_path=f"{dataset.location}/train",
|
|
916
|
+
annotations_path=f"{dataset.location}/train/_annotations.coco.json",
|
|
917
|
+
# pass show_progress=True to enable a tqdm progress bar
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
ds.classes
|
|
921
|
+
# ['dog', 'person']
|
|
922
|
+
```
|
|
923
|
+
"""
|
|
924
|
+
classes, images, annotations = load_coco_annotations(
|
|
925
|
+
images_directory_path=images_directory_path,
|
|
926
|
+
annotations_path=annotations_path,
|
|
927
|
+
force_masks=force_masks,
|
|
928
|
+
use_iscrowd=use_iscrowd,
|
|
929
|
+
show_progress=show_progress,
|
|
930
|
+
)
|
|
931
|
+
return DetectionDataset(classes=classes, images=images, annotations=annotations)
|
|
932
|
+
|
|
933
|
+
def as_coco(
|
|
934
|
+
self,
|
|
935
|
+
images_directory_path: str | None = None,
|
|
936
|
+
annotations_path: str | None = None,
|
|
937
|
+
min_image_area_percentage: float = 0.0,
|
|
938
|
+
max_image_area_percentage: float = 1.0,
|
|
939
|
+
approximation_percentage: float = 0.0,
|
|
940
|
+
starting_image_id: int = 1,
|
|
941
|
+
starting_annotation_id: int = 1,
|
|
942
|
+
show_progress: bool = False,
|
|
943
|
+
) -> tuple[int, int]:
|
|
944
|
+
"""
|
|
945
|
+
Exports the dataset to COCO format. This method saves the
|
|
946
|
+
images and their corresponding annotations in COCO format.
|
|
947
|
+
|
|
948
|
+
!!! tip
|
|
949
|
+
|
|
950
|
+
The format of the mask is determined automatically based on its structure:
|
|
951
|
+
|
|
952
|
+
- If a mask contains multiple disconnected components or holes, it will be
|
|
953
|
+
saved using the Run-Length Encoding (RLE) format for efficient storage and
|
|
954
|
+
processing.
|
|
955
|
+
- If a mask consists of a single, contiguous region without any holes, it
|
|
956
|
+
will be encoded as a polygon, preserving the outline of the object.
|
|
957
|
+
|
|
958
|
+
This automatic selection ensures that the masks are stored in the most
|
|
959
|
+
appropriate and space-efficient format, complying with COCO dataset
|
|
960
|
+
standards.
|
|
961
|
+
|
|
962
|
+
Args:
|
|
963
|
+
images_directory_path: The path to the directory
|
|
964
|
+
where the images should be saved.
|
|
965
|
+
If not provided, images will not be saved.
|
|
966
|
+
annotations_path: The path to COCO annotation file.
|
|
967
|
+
min_image_area_percentage: The minimum percentage of
|
|
968
|
+
detection area relative to
|
|
969
|
+
the image area for a detection to be included.
|
|
970
|
+
Argument is used only for segmentation datasets.
|
|
971
|
+
max_image_area_percentage: The maximum percentage of
|
|
972
|
+
detection area relative to
|
|
973
|
+
the image area for a detection to be included.
|
|
974
|
+
Argument is used only for segmentation datasets.
|
|
975
|
+
approximation_percentage: The percentage of polygon points
|
|
976
|
+
to be removed from the input polygon,
|
|
977
|
+
in the range [0, 1). This is useful for simplifying the annotations.
|
|
978
|
+
Argument is used only for segmentation datasets.
|
|
979
|
+
starting_image_id: First image id to assign in the exported file.
|
|
980
|
+
Defaults to ``1``. Override when exporting multiple splits into
|
|
981
|
+
a coordinated COCO collection so ids remain unique across the
|
|
982
|
+
set (see example below).
|
|
983
|
+
starting_annotation_id: First annotation id to assign in the
|
|
984
|
+
exported file. Defaults to ``1``. Override for the same
|
|
985
|
+
multi-split reason as ``starting_image_id``.
|
|
986
|
+
show_progress: If True, display a progress bar during saving.
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
A ``(next_image_id, next_annotation_id)`` tuple containing the
|
|
990
|
+
first unused ids after this export. Feed them straight back into
|
|
991
|
+
``starting_image_id`` and ``starting_annotation_id`` on the next
|
|
992
|
+
split so ids stay globally unique. When ``annotations_path`` is
|
|
993
|
+
``None`` (images-only export) the starting ids are returned
|
|
994
|
+
unchanged so chaining still composes.
|
|
995
|
+
|
|
996
|
+
Example:
|
|
997
|
+
```python
|
|
998
|
+
# Exporting train, valid, and test splits with non-colliding ids
|
|
999
|
+
# so the three annotation files can later be merged into one COCO.
|
|
1000
|
+
next_image_id, next_annotation_id = train_ds.as_coco(
|
|
1001
|
+
images_directory_path="out/train/images",
|
|
1002
|
+
annotations_path="out/train/annotations.json",
|
|
1003
|
+
)
|
|
1004
|
+
next_image_id, next_annotation_id = valid_ds.as_coco(
|
|
1005
|
+
images_directory_path="out/valid/images",
|
|
1006
|
+
annotations_path="out/valid/annotations.json",
|
|
1007
|
+
starting_image_id=next_image_id,
|
|
1008
|
+
starting_annotation_id=next_annotation_id,
|
|
1009
|
+
)
|
|
1010
|
+
_, _ = test_ds.as_coco(
|
|
1011
|
+
images_directory_path="out/test/images",
|
|
1012
|
+
annotations_path="out/test/annotations.json",
|
|
1013
|
+
starting_image_id=next_image_id,
|
|
1014
|
+
starting_annotation_id=next_annotation_id,
|
|
1015
|
+
) # return value not needed — no further split
|
|
1016
|
+
```
|
|
1017
|
+
"""
|
|
1018
|
+
if images_directory_path is not None:
|
|
1019
|
+
save_dataset_images(
|
|
1020
|
+
dataset=self,
|
|
1021
|
+
images_directory_path=images_directory_path,
|
|
1022
|
+
show_progress=show_progress,
|
|
1023
|
+
)
|
|
1024
|
+
if annotations_path is not None:
|
|
1025
|
+
return save_coco_annotations(
|
|
1026
|
+
dataset=self,
|
|
1027
|
+
annotation_path=annotations_path,
|
|
1028
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
1029
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
1030
|
+
approximation_percentage=approximation_percentage,
|
|
1031
|
+
starting_image_id=starting_image_id,
|
|
1032
|
+
starting_annotation_id=starting_annotation_id,
|
|
1033
|
+
show_progress=show_progress,
|
|
1034
|
+
)
|
|
1035
|
+
return starting_image_id, starting_annotation_id
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
@dataclass
|
|
1039
|
+
class ClassificationDataset(BaseDataset):
|
|
1040
|
+
"""
|
|
1041
|
+
Contains information about a classification dataset, handles lazy image
|
|
1042
|
+
loading, dataset splitting.
|
|
1043
|
+
|
|
1044
|
+
Attributes:
|
|
1045
|
+
classes: List containing dataset class names.
|
|
1046
|
+
images:
|
|
1047
|
+
List of image paths or dictionary mapping image name to image data.
|
|
1048
|
+
annotations: Dictionary mapping
|
|
1049
|
+
image name to annotations.
|
|
1050
|
+
"""
|
|
1051
|
+
|
|
1052
|
+
def __init__(
|
|
1053
|
+
self,
|
|
1054
|
+
classes: list[str],
|
|
1055
|
+
images: list[str] | dict[str, npt.NDArray[np.uint8]],
|
|
1056
|
+
annotations: dict[str, Classifications],
|
|
1057
|
+
) -> None:
|
|
1058
|
+
self.classes = classes
|
|
1059
|
+
|
|
1060
|
+
if set(images) != set(annotations):
|
|
1061
|
+
raise ValueError(
|
|
1062
|
+
"The keys of the images and annotations dictionaries must match."
|
|
1063
|
+
)
|
|
1064
|
+
self.annotations = annotations
|
|
1065
|
+
|
|
1066
|
+
# Eliminate duplicates while preserving order
|
|
1067
|
+
self.image_paths = list(dict.fromkeys(images))
|
|
1068
|
+
|
|
1069
|
+
self._images_in_memory: dict[str, npt.NDArray[np.uint8]] = {}
|
|
1070
|
+
if isinstance(images, dict):
|
|
1071
|
+
self._images_in_memory = images
|
|
1072
|
+
warn_deprecated(
|
|
1073
|
+
"Passing a `Dict[str, np.ndarray]` into `ClassificationDataset` is "
|
|
1074
|
+
"deprecated and will be removed in a future release. Use "
|
|
1075
|
+
"a list of paths `List[str]` instead."
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]:
|
|
1079
|
+
"""Assumes that image is in dataset."""
|
|
1080
|
+
if self._images_in_memory:
|
|
1081
|
+
return self._images_in_memory[image_path]
|
|
1082
|
+
image = cv2.imread(image_path)
|
|
1083
|
+
if image is None:
|
|
1084
|
+
raise ValueError(f"Could not read image from path: {image_path}")
|
|
1085
|
+
return cast(npt.NDArray[np.uint8], image)
|
|
1086
|
+
|
|
1087
|
+
def __len__(self) -> int:
|
|
1088
|
+
return len(self._images_in_memory) or len(self.image_paths)
|
|
1089
|
+
|
|
1090
|
+
def __getitem__(self, i: int) -> tuple[str, npt.NDArray[np.uint8], Classifications]:
|
|
1091
|
+
"""
|
|
1092
|
+
Returns:
|
|
1093
|
+
The image path, image data,
|
|
1094
|
+
and its corresponding annotation at index i.
|
|
1095
|
+
"""
|
|
1096
|
+
image_path = self.image_paths[i]
|
|
1097
|
+
image = self._get_image(image_path)
|
|
1098
|
+
annotation = self.annotations[image_path]
|
|
1099
|
+
return image_path, image, annotation
|
|
1100
|
+
|
|
1101
|
+
def __iter__(
|
|
1102
|
+
self,
|
|
1103
|
+
) -> Iterator[tuple[str, npt.NDArray[np.uint8], Classifications]]:
|
|
1104
|
+
"""
|
|
1105
|
+
Iterate over the images and annotations in the dataset.
|
|
1106
|
+
|
|
1107
|
+
Yields:
|
|
1108
|
+
Tuples containing the image path, image data, and its annotation.
|
|
1109
|
+
"""
|
|
1110
|
+
for i in range(len(self)):
|
|
1111
|
+
image_path, image, annotation = self[i]
|
|
1112
|
+
yield image_path, image, annotation
|
|
1113
|
+
|
|
1114
|
+
def __eq__(self, other: object) -> bool:
|
|
1115
|
+
if not isinstance(other, ClassificationDataset):
|
|
1116
|
+
return False
|
|
1117
|
+
|
|
1118
|
+
if self.classes != other.classes:
|
|
1119
|
+
return False
|
|
1120
|
+
|
|
1121
|
+
if self.image_paths != other.image_paths:
|
|
1122
|
+
return False
|
|
1123
|
+
|
|
1124
|
+
if self._images_in_memory or other._images_in_memory:
|
|
1125
|
+
if not np.array_equal(
|
|
1126
|
+
list(self._images_in_memory.values()),
|
|
1127
|
+
list(other._images_in_memory.values()),
|
|
1128
|
+
):
|
|
1129
|
+
return False
|
|
1130
|
+
|
|
1131
|
+
if self.annotations != other.annotations:
|
|
1132
|
+
return False
|
|
1133
|
+
|
|
1134
|
+
return True
|
|
1135
|
+
|
|
1136
|
+
def split(
|
|
1137
|
+
self,
|
|
1138
|
+
split_ratio: float = 0.8,
|
|
1139
|
+
random_state: int | None = None,
|
|
1140
|
+
shuffle: bool = True,
|
|
1141
|
+
) -> tuple[ClassificationDataset, ClassificationDataset]:
|
|
1142
|
+
"""
|
|
1143
|
+
Splits the dataset into two parts (training and testing)
|
|
1144
|
+
using the provided split_ratio.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
split_ratio: The ratio of the training
|
|
1148
|
+
set to the entire dataset.
|
|
1149
|
+
random_state: The seed for the
|
|
1150
|
+
random number generator. This is used for reproducibility.
|
|
1151
|
+
shuffle: Whether to shuffle the data before splitting.
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
A tuple containing
|
|
1155
|
+
the training and testing datasets.
|
|
1156
|
+
|
|
1157
|
+
Examples:
|
|
1158
|
+
```pycon
|
|
1159
|
+
>>> import numpy as np
|
|
1160
|
+
>>> import supervision as sv
|
|
1161
|
+
>>> cd = sv.ClassificationDataset(
|
|
1162
|
+
... classes=['cat', 'dog'],
|
|
1163
|
+
... images={
|
|
1164
|
+
... 'img1.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
|
|
1165
|
+
... 'img2.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
|
|
1166
|
+
... },
|
|
1167
|
+
... annotations={
|
|
1168
|
+
... 'img1.jpg': sv.Classifications(class_id=np.array([0])),
|
|
1169
|
+
... 'img2.jpg': sv.Classifications(class_id=np.array([1])),
|
|
1170
|
+
... }
|
|
1171
|
+
... )
|
|
1172
|
+
>>> train_cd, test_cd = cd.split(split_ratio=0.5, random_state=42)
|
|
1173
|
+
>>> len(train_cd), len(test_cd)
|
|
1174
|
+
(1, 1)
|
|
1175
|
+
|
|
1176
|
+
```
|
|
1177
|
+
"""
|
|
1178
|
+
train_paths, test_paths = train_test_split(
|
|
1179
|
+
data=self.image_paths,
|
|
1180
|
+
train_ratio=split_ratio,
|
|
1181
|
+
random_state=random_state,
|
|
1182
|
+
shuffle=shuffle,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
train_input: list[str] | dict[str, npt.NDArray[np.uint8]]
|
|
1186
|
+
test_input: list[str] | dict[str, npt.NDArray[np.uint8]]
|
|
1187
|
+
if self._images_in_memory:
|
|
1188
|
+
train_input = {path: self._images_in_memory[path] for path in train_paths}
|
|
1189
|
+
test_input = {path: self._images_in_memory[path] for path in test_paths}
|
|
1190
|
+
else:
|
|
1191
|
+
train_input = train_paths
|
|
1192
|
+
test_input = test_paths
|
|
1193
|
+
train_annotations = {path: self.annotations[path] for path in train_paths}
|
|
1194
|
+
test_annotations = {path: self.annotations[path] for path in test_paths}
|
|
1195
|
+
|
|
1196
|
+
train_dataset = ClassificationDataset(
|
|
1197
|
+
classes=self.classes,
|
|
1198
|
+
images=train_input,
|
|
1199
|
+
annotations=train_annotations,
|
|
1200
|
+
)
|
|
1201
|
+
test_dataset = ClassificationDataset(
|
|
1202
|
+
classes=self.classes,
|
|
1203
|
+
images=test_input,
|
|
1204
|
+
annotations=test_annotations,
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
return train_dataset, test_dataset
|
|
1208
|
+
|
|
1209
|
+
def as_folder_structure(
|
|
1210
|
+
self, root_directory_path: str, show_progress: bool = False
|
|
1211
|
+
) -> None:
|
|
1212
|
+
"""
|
|
1213
|
+
Saves the dataset as a multi-class folder structure.
|
|
1214
|
+
|
|
1215
|
+
Args:
|
|
1216
|
+
root_directory_path: The path to the directory
|
|
1217
|
+
where the dataset will be saved.
|
|
1218
|
+
show_progress: If True, display a progress bar during saving.
|
|
1219
|
+
"""
|
|
1220
|
+
os.makedirs(root_directory_path, exist_ok=True)
|
|
1221
|
+
|
|
1222
|
+
for class_name in self.classes:
|
|
1223
|
+
os.makedirs(os.path.join(root_directory_path, class_name), exist_ok=True)
|
|
1224
|
+
|
|
1225
|
+
for image_save_path, image, annotation in tqdm(
|
|
1226
|
+
self,
|
|
1227
|
+
total=len(self),
|
|
1228
|
+
desc="Saving classification images",
|
|
1229
|
+
disable=not show_progress,
|
|
1230
|
+
):
|
|
1231
|
+
image_name = Path(image_save_path).name
|
|
1232
|
+
class_id = (
|
|
1233
|
+
annotation.class_id[0]
|
|
1234
|
+
if annotation.confidence is None
|
|
1235
|
+
else annotation.get_top_k(1)[0][0]
|
|
1236
|
+
)
|
|
1237
|
+
class_name = self.classes[class_id]
|
|
1238
|
+
image_save_path = os.path.join(root_directory_path, class_name, image_name)
|
|
1239
|
+
cv2.imwrite(image_save_path, image)
|
|
1240
|
+
|
|
1241
|
+
@classmethod
|
|
1242
|
+
def from_folder_structure(
|
|
1243
|
+
cls, root_directory_path: str, show_progress: bool = False
|
|
1244
|
+
) -> ClassificationDataset:
|
|
1245
|
+
"""
|
|
1246
|
+
Load data from a multiclass folder structure into a ClassificationDataset.
|
|
1247
|
+
|
|
1248
|
+
Args:
|
|
1249
|
+
root_directory_path: The path to the dataset directory. Hidden
|
|
1250
|
+
entries, root-level files, nested directories, and files whose
|
|
1251
|
+
suffix is not a supported image extension are ignored.
|
|
1252
|
+
show_progress: If True, display a progress bar during loading.
|
|
1253
|
+
|
|
1254
|
+
Returns:
|
|
1255
|
+
The dataset.
|
|
1256
|
+
|
|
1257
|
+
Examples:
|
|
1258
|
+
```python
|
|
1259
|
+
import roboflow
|
|
1260
|
+
from roboflow import Roboflow
|
|
1261
|
+
import supervision as sv
|
|
1262
|
+
|
|
1263
|
+
roboflow.login()
|
|
1264
|
+
rf = Roboflow()
|
|
1265
|
+
|
|
1266
|
+
project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
|
|
1267
|
+
dataset = project.version(PROJECT_VERSION).download("folder")
|
|
1268
|
+
|
|
1269
|
+
cd = sv.ClassificationDataset.from_folder_structure(
|
|
1270
|
+
root_directory_path=f"{dataset.location}/train"
|
|
1271
|
+
# pass show_progress=True to enable a tqdm progress bar
|
|
1272
|
+
)
|
|
1273
|
+
```
|
|
1274
|
+
"""
|
|
1275
|
+
root_directory = Path(root_directory_path)
|
|
1276
|
+
classes = sorted(
|
|
1277
|
+
{
|
|
1278
|
+
entry.name
|
|
1279
|
+
for entry in root_directory.iterdir()
|
|
1280
|
+
if entry.is_dir() and not entry.name.startswith(".")
|
|
1281
|
+
}
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
image_paths = []
|
|
1285
|
+
annotations = {}
|
|
1286
|
+
|
|
1287
|
+
for class_id, class_name in enumerate(
|
|
1288
|
+
tqdm(
|
|
1289
|
+
classes,
|
|
1290
|
+
total=len(classes),
|
|
1291
|
+
desc="Loading classification dataset",
|
|
1292
|
+
disable=not show_progress,
|
|
1293
|
+
)
|
|
1294
|
+
):
|
|
1295
|
+
class_directory = root_directory / class_name
|
|
1296
|
+
|
|
1297
|
+
for image_path in sorted(
|
|
1298
|
+
class_directory.iterdir(), key=lambda path: path.name
|
|
1299
|
+
):
|
|
1300
|
+
if image_path.name.startswith(".") or not image_path.is_file():
|
|
1301
|
+
continue
|
|
1302
|
+
if image_path.suffix.lower() not in _IMAGE_FILE_EXTENSIONS:
|
|
1303
|
+
continue
|
|
1304
|
+
image_paths.append(str(image_path))
|
|
1305
|
+
annotations[str(image_path)] = Classifications(
|
|
1306
|
+
class_id=np.array([class_id]),
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
return cls(
|
|
1310
|
+
classes=classes,
|
|
1311
|
+
images=image_paths,
|
|
1312
|
+
annotations=annotations,
|
|
1313
|
+
)
|