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