eye-cv 1.0.0__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 (94) hide show
  1. eye/__init__.py +115 -0
  2. eye/__init___supervision_original.py +120 -0
  3. eye/annotators/__init__.py +0 -0
  4. eye/annotators/base.py +22 -0
  5. eye/annotators/core.py +2699 -0
  6. eye/annotators/line.py +107 -0
  7. eye/annotators/modern.py +529 -0
  8. eye/annotators/trace.py +142 -0
  9. eye/annotators/utils.py +177 -0
  10. eye/assets/__init__.py +2 -0
  11. eye/assets/downloader.py +95 -0
  12. eye/assets/list.py +83 -0
  13. eye/classification/__init__.py +0 -0
  14. eye/classification/core.py +188 -0
  15. eye/config.py +2 -0
  16. eye/core/__init__.py +0 -0
  17. eye/core/trackers/__init__.py +1 -0
  18. eye/core/trackers/botsort_tracker.py +336 -0
  19. eye/core/trackers/bytetrack_tracker.py +284 -0
  20. eye/core/trackers/sort_tracker.py +200 -0
  21. eye/core/tracking.py +146 -0
  22. eye/dataset/__init__.py +0 -0
  23. eye/dataset/core.py +919 -0
  24. eye/dataset/formats/__init__.py +0 -0
  25. eye/dataset/formats/coco.py +258 -0
  26. eye/dataset/formats/pascal_voc.py +279 -0
  27. eye/dataset/formats/yolo.py +272 -0
  28. eye/dataset/utils.py +259 -0
  29. eye/detection/__init__.py +0 -0
  30. eye/detection/auto_convert.py +155 -0
  31. eye/detection/core.py +1529 -0
  32. eye/detection/detections_enhanced.py +392 -0
  33. eye/detection/line_zone.py +859 -0
  34. eye/detection/lmm.py +184 -0
  35. eye/detection/overlap_filter.py +270 -0
  36. eye/detection/tools/__init__.py +0 -0
  37. eye/detection/tools/csv_sink.py +181 -0
  38. eye/detection/tools/inference_slicer.py +288 -0
  39. eye/detection/tools/json_sink.py +142 -0
  40. eye/detection/tools/polygon_zone.py +202 -0
  41. eye/detection/tools/smoother.py +123 -0
  42. eye/detection/tools/smoothing.py +179 -0
  43. eye/detection/tools/smoothing_config.py +202 -0
  44. eye/detection/tools/transformers.py +247 -0
  45. eye/detection/utils.py +1175 -0
  46. eye/draw/__init__.py +0 -0
  47. eye/draw/color.py +154 -0
  48. eye/draw/utils.py +374 -0
  49. eye/filters.py +112 -0
  50. eye/geometry/__init__.py +0 -0
  51. eye/geometry/core.py +128 -0
  52. eye/geometry/utils.py +47 -0
  53. eye/keypoint/__init__.py +0 -0
  54. eye/keypoint/annotators.py +442 -0
  55. eye/keypoint/core.py +687 -0
  56. eye/keypoint/skeletons.py +2647 -0
  57. eye/metrics/__init__.py +21 -0
  58. eye/metrics/core.py +72 -0
  59. eye/metrics/detection.py +843 -0
  60. eye/metrics/f1_score.py +648 -0
  61. eye/metrics/mean_average_precision.py +628 -0
  62. eye/metrics/mean_average_recall.py +697 -0
  63. eye/metrics/precision.py +653 -0
  64. eye/metrics/recall.py +652 -0
  65. eye/metrics/utils/__init__.py +0 -0
  66. eye/metrics/utils/object_size.py +158 -0
  67. eye/metrics/utils/utils.py +9 -0
  68. eye/py.typed +0 -0
  69. eye/quick.py +104 -0
  70. eye/tracker/__init__.py +0 -0
  71. eye/tracker/byte_tracker/__init__.py +0 -0
  72. eye/tracker/byte_tracker/core.py +386 -0
  73. eye/tracker/byte_tracker/kalman_filter.py +205 -0
  74. eye/tracker/byte_tracker/matching.py +69 -0
  75. eye/tracker/byte_tracker/single_object_track.py +178 -0
  76. eye/tracker/byte_tracker/utils.py +18 -0
  77. eye/utils/__init__.py +0 -0
  78. eye/utils/conversion.py +132 -0
  79. eye/utils/file.py +159 -0
  80. eye/utils/image.py +794 -0
  81. eye/utils/internal.py +200 -0
  82. eye/utils/iterables.py +84 -0
  83. eye/utils/notebook.py +114 -0
  84. eye/utils/video.py +307 -0
  85. eye/utils_eye/__init__.py +1 -0
  86. eye/utils_eye/geometry.py +71 -0
  87. eye/utils_eye/nms.py +55 -0
  88. eye/validators/__init__.py +140 -0
  89. eye/web.py +271 -0
  90. eye_cv-1.0.0.dist-info/METADATA +319 -0
  91. eye_cv-1.0.0.dist-info/RECORD +94 -0
  92. eye_cv-1.0.0.dist-info/WHEEL +5 -0
  93. eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
  94. eye_cv-1.0.0.dist-info/top_level.txt +1 -0
eye/dataset/core.py ADDED
@@ -0,0 +1,919 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass
6
+ from itertools import chain
7
+ from pathlib import Path
8
+ from typing import Dict, Iterator, List, Optional, Tuple, Union
9
+
10
+ import cv2
11
+ import numpy as np
12
+
13
+ from eye.classification.core import Classifications
14
+ from eye.dataset.formats.coco import (
15
+ load_coco_annotations,
16
+ save_coco_annotations,
17
+ )
18
+ from eye.dataset.formats.pascal_voc import (
19
+ detections_to_pascal_voc,
20
+ load_pascal_voc_annotations,
21
+ )
22
+ from eye.dataset.formats.yolo import (
23
+ load_yolo_annotations,
24
+ save_data_yaml,
25
+ save_yolo_annotations,
26
+ )
27
+ from eye.dataset.utils import (
28
+ build_class_index_mapping,
29
+ map_detections_class_id,
30
+ merge_class_lists,
31
+ save_dataset_images,
32
+ train_test_split,
33
+ )
34
+ from eye.detection.core import Detections
35
+ from eye.utils.internal import deprecated, warn_deprecated
36
+ from eye.utils.iterables import find_duplicates
37
+
38
+
39
+ class BaseDataset(ABC):
40
+ @abstractmethod
41
+ def __len__(self) -> int:
42
+ pass
43
+
44
+ @abstractmethod
45
+ def split(
46
+ self,
47
+ split_ratio: float = 0.8,
48
+ random_state: Optional[int] = None,
49
+ shuffle: bool = True,
50
+ ) -> Tuple[BaseDataset, BaseDataset]:
51
+ pass
52
+
53
+
54
+ class DetectionDataset(BaseDataset):
55
+ """
56
+ Contains information about a detection dataset. Handles lazy image loading
57
+ and annotation retrieval, dataset splitting, conversions into multiple
58
+ formats.
59
+
60
+ Attributes:
61
+ classes (List[str]): List containing dataset class names.
62
+ images (Union[List[str], Dict[str, np.ndarray]]):
63
+ Accepts a list of image paths, or dictionaries of loaded cv2 images
64
+ with paths as keys. If you pass a list of paths, the dataset will
65
+ lazily load images on demand, which is much more memory-efficient.
66
+ annotations (Dict[str, Detections]): Dictionary mapping
67
+ image path to annotations. The dictionary keys match
68
+ match the keys in `images` or entries in the list of
69
+ image paths.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ classes: List[str],
75
+ images: Union[List[str], Dict[str, np.ndarray]],
76
+ annotations: Dict[str, Detections],
77
+ ) -> None:
78
+ self.classes = classes
79
+
80
+ if set(images) != set(annotations):
81
+ raise ValueError(
82
+ "The keys of the images and annotations dictionaries must match."
83
+ )
84
+ self.annotations = annotations
85
+
86
+ # Eliminate duplicates while preserving order
87
+ self.image_paths = list(dict.fromkeys(images))
88
+
89
+ self._images_in_memory: Dict[str, np.ndarray] = {}
90
+ if isinstance(images, dict):
91
+ self._images_in_memory = images
92
+ warn_deprecated(
93
+ "Passing a `Dict[str, np.ndarray]` into `DetectionDataset` is "
94
+ "deprecated and will be removed in `eye-0.26.0`. Use "
95
+ "a list of paths `List[str]` instead."
96
+ )
97
+ # TODO: when eye-0.26.0 is released, and Dict[str, np.ndarray]
98
+ # for images is no longer supported, also simplify the rest of
99
+ # the code. E.g. list(images) is no longer needed, and merge can
100
+ # be simplified.
101
+
102
+ @property
103
+ @deprecated(
104
+ "`DetectionDataset.images` property is deprecated and will be removed in "
105
+ "`eye-0.26.0`. Iterate with `for path, image, annotation in dataset:` "
106
+ "instead."
107
+ )
108
+ def images(self) -> Dict[str, np.ndarray]:
109
+ """
110
+ Load all images to memory and return them as a dictionary.
111
+
112
+ !!! warning
113
+
114
+ Only use this when you need all images at once.
115
+ It is much more memory-efficient to initialize dataset with
116
+ image paths and use `for path, image, annotation in dataset:`.
117
+ """
118
+ if self._images_in_memory:
119
+ return self._images_in_memory
120
+
121
+ images = {image_path: cv2.imread(image_path) for image_path in self.image_paths}
122
+ return images
123
+
124
+ def _get_image(self, image_path: str) -> np.ndarray:
125
+ """Assumes that image is in dataset"""
126
+ if self._images_in_memory:
127
+ return self._images_in_memory[image_path]
128
+ return cv2.imread(image_path)
129
+
130
+ def __len__(self) -> int:
131
+ return len(self._images_in_memory) or len(self.image_paths)
132
+
133
+ def __getitem__(self, i: int) -> Tuple[str, np.ndarray, Detections]:
134
+ """
135
+ Returns:
136
+ Tuple[str, np.ndarray, Detections]: The image path, image data,
137
+ and its corresponding annotation at index i.
138
+ """
139
+ image_path = self.image_paths[i]
140
+ image = self._get_image(image_path)
141
+ annotation = self.annotations[image_path]
142
+ return image_path, image, annotation
143
+
144
+ def __iter__(self) -> Iterator[Tuple[str, np.ndarray, Detections]]:
145
+ """
146
+ Iterate over the images and annotations in the dataset.
147
+
148
+ Yields:
149
+ Iterator[Tuple[str, np.ndarray, Detections]]:
150
+ An iterator that yields tuples containing the image path,
151
+ the image data, and its corresponding annotation.
152
+ """
153
+ for i in range(len(self)):
154
+ image_path, image, annotation = self[i]
155
+ yield image_path, image, annotation
156
+
157
+ def __eq__(self, other) -> bool:
158
+ if not isinstance(other, DetectionDataset):
159
+ return False
160
+
161
+ if set(self.classes) != set(other.classes):
162
+ return False
163
+
164
+ if self.image_paths != other.image_paths:
165
+ return False
166
+
167
+ if self._images_in_memory or other._images_in_memory:
168
+ if not np.array_equal(
169
+ list(self._images_in_memory.values()),
170
+ list(other._images_in_memory.values()),
171
+ ):
172
+ return False
173
+
174
+ if self.annotations != other.annotations:
175
+ return False
176
+
177
+ return True
178
+
179
+ def split(
180
+ self,
181
+ split_ratio: float = 0.8,
182
+ random_state: Optional[int] = None,
183
+ shuffle: bool = True,
184
+ ) -> Tuple[DetectionDataset, DetectionDataset]:
185
+ """
186
+ Splits the dataset into two parts (training and testing)
187
+ using the provided split_ratio.
188
+
189
+ Args:
190
+ split_ratio (float): The ratio of the training
191
+ set to the entire dataset.
192
+ random_state (Optional[int]): The seed for the random number generator.
193
+ This is used for reproducibility.
194
+ shuffle (bool): Whether to shuffle the data before splitting.
195
+
196
+ Returns:
197
+ Tuple[DetectionDataset, DetectionDataset]: A tuple containing
198
+ the training and testing datasets.
199
+
200
+ Examples:
201
+ ```python
202
+ import eye as sv
203
+
204
+ ds = sv.DetectionDataset(...)
205
+ train_ds, test_ds = ds.split(split_ratio=0.7, random_state=42, shuffle=True)
206
+ len(train_ds), len(test_ds)
207
+ # (700, 300)
208
+ ```
209
+ """
210
+
211
+ train_paths, test_paths = train_test_split(
212
+ data=self.image_paths,
213
+ train_ratio=split_ratio,
214
+ random_state=random_state,
215
+ shuffle=shuffle,
216
+ )
217
+
218
+ train_input: Union[List[str], Dict[str, np.ndarray]]
219
+ test_input: Union[List[str], Dict[str, np.ndarray]]
220
+ if self._images_in_memory:
221
+ train_input = {path: self._images_in_memory[path] for path in train_paths}
222
+ test_input = {path: self._images_in_memory[path] for path in test_paths}
223
+ else:
224
+ train_input = train_paths
225
+ test_input = test_paths
226
+ train_annotations = {path: self.annotations[path] for path in train_paths}
227
+ test_annotations = {path: self.annotations[path] for path in test_paths}
228
+
229
+ train_dataset = DetectionDataset(
230
+ classes=self.classes,
231
+ images=train_input,
232
+ annotations=train_annotations,
233
+ )
234
+ test_dataset = DetectionDataset(
235
+ classes=self.classes,
236
+ images=test_input,
237
+ annotations=test_annotations,
238
+ )
239
+ return train_dataset, test_dataset
240
+
241
+ @classmethod
242
+ def merge(cls, dataset_list: List[DetectionDataset]) -> DetectionDataset:
243
+ """
244
+ Merge a list of `DetectionDataset` objects into a single
245
+ `DetectionDataset` object.
246
+
247
+ This method takes a list of `DetectionDataset` objects and combines
248
+ their respective fields (`classes`, `images`,
249
+ `annotations`) into a single `DetectionDataset` object.
250
+
251
+ Args:
252
+ dataset_list (List[DetectionDataset]): A list of `DetectionDataset`
253
+ objects to merge.
254
+
255
+ Returns:
256
+ (DetectionDataset): A single `DetectionDataset` object containing
257
+ the merged data from the input list.
258
+
259
+ Examples:
260
+ ```python
261
+ import eye as sv
262
+
263
+ ds_1 = sv.DetectionDataset(...)
264
+ len(ds_1)
265
+ # 100
266
+ ds_1.classes
267
+ # ['dog', 'person']
268
+
269
+ ds_2 = sv.DetectionDataset(...)
270
+ len(ds_2)
271
+ # 200
272
+ ds_2.classes
273
+ # ['cat']
274
+
275
+ ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
276
+ len(ds_merged)
277
+ # 300
278
+ ds_merged.classes
279
+ # ['cat', 'dog', 'person']
280
+ ```
281
+ """
282
+
283
+ def is_in_memory(dataset: DetectionDataset) -> bool:
284
+ return len(dataset._images_in_memory) > 0 or len(dataset.image_paths) == 0
285
+
286
+ def is_lazy(dataset: DetectionDataset) -> bool:
287
+ return len(dataset._images_in_memory) == 0
288
+
289
+ all_in_memory = all([is_in_memory(dataset) for dataset in dataset_list])
290
+ all_lazy = all([is_lazy(dataset) for dataset in dataset_list])
291
+ if not all_in_memory and not all_lazy:
292
+ raise ValueError(
293
+ "Merging lazy and in-memory DetectionDatasets is not supported."
294
+ )
295
+
296
+ images_in_memory = {}
297
+ for dataset in dataset_list:
298
+ images_in_memory.update(dataset._images_in_memory)
299
+
300
+ image_paths = list(
301
+ chain.from_iterable(dataset.image_paths for dataset in dataset_list)
302
+ )
303
+ image_paths_unique = list(dict.fromkeys(image_paths))
304
+ if len(image_paths) != len(image_paths_unique):
305
+ duplicates = find_duplicates(image_paths)
306
+ raise ValueError(
307
+ f"Image paths {duplicates} are not unique across datasets."
308
+ )
309
+ image_paths = image_paths_unique
310
+
311
+ classes = merge_class_lists(
312
+ class_lists=[dataset.classes for dataset in dataset_list]
313
+ )
314
+
315
+ annotations = {}
316
+ for dataset in dataset_list:
317
+ annotations.update(dataset.annotations)
318
+ for dataset in dataset_list:
319
+ class_index_mapping = build_class_index_mapping(
320
+ source_classes=dataset.classes, target_classes=classes
321
+ )
322
+ for image_path in dataset.image_paths:
323
+ annotations[image_path] = map_detections_class_id(
324
+ source_to_target_mapping=class_index_mapping,
325
+ detections=annotations[image_path],
326
+ )
327
+
328
+ return cls(
329
+ classes=classes,
330
+ images=images_in_memory or image_paths,
331
+ annotations=annotations,
332
+ )
333
+
334
+ def as_pascal_voc(
335
+ self,
336
+ images_directory_path: Optional[str] = None,
337
+ annotations_directory_path: Optional[str] = None,
338
+ min_image_area_percentage: float = 0.0,
339
+ max_image_area_percentage: float = 1.0,
340
+ approximation_percentage: float = 0.0,
341
+ ) -> None:
342
+ """
343
+ Exports the dataset to PASCAL VOC format. This method saves the images
344
+ and their corresponding annotations in PASCAL VOC format.
345
+
346
+ Args:
347
+ images_directory_path (Optional[str]): The path to the directory
348
+ where the images should be saved.
349
+ If not provided, images will not be saved.
350
+ annotations_directory_path (Optional[str]): The path to
351
+ the directory where the annotations in PASCAL VOC format should be
352
+ saved. If not provided, annotations will not be saved.
353
+ min_image_area_percentage (float): The minimum percentage of
354
+ detection area relative to
355
+ the image area for a detection to be included.
356
+ Argument is used only for segmentation datasets.
357
+ max_image_area_percentage (float): The maximum percentage
358
+ of detection area relative to
359
+ the image area for a detection to be included.
360
+ Argument is used only for segmentation datasets.
361
+ approximation_percentage (float): The percentage of
362
+ polygon points to be removed from the input polygon,
363
+ in the range [0, 1). Argument is used only for segmentation datasets.
364
+ """
365
+ if images_directory_path:
366
+ save_dataset_images(
367
+ dataset=self,
368
+ images_directory_path=images_directory_path,
369
+ )
370
+ if annotations_directory_path:
371
+ Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
372
+ for image_path, image, annotations in self:
373
+ annotation_name = Path(image_path).stem
374
+ annotations_path = os.path.join(
375
+ annotations_directory_path, f"{annotation_name}.xml"
376
+ )
377
+ image_name = Path(image_path).name
378
+ pascal_voc_xml = detections_to_pascal_voc(
379
+ detections=annotations,
380
+ classes=self.classes,
381
+ filename=image_name,
382
+ image_shape=image.shape, # type: ignore
383
+ min_image_area_percentage=min_image_area_percentage,
384
+ max_image_area_percentage=max_image_area_percentage,
385
+ approximation_percentage=approximation_percentage,
386
+ )
387
+
388
+ with open(annotations_path, "w") as f:
389
+ f.write(pascal_voc_xml)
390
+
391
+ @classmethod
392
+ def from_pascal_voc(
393
+ cls,
394
+ images_directory_path: str,
395
+ annotations_directory_path: str,
396
+ force_masks: bool = False,
397
+ ) -> DetectionDataset:
398
+ """
399
+ Creates a Dataset instance from PASCAL VOC formatted data.
400
+
401
+ Args:
402
+ images_directory_path (str): Path to the directory containing the images.
403
+ annotations_directory_path (str): Path to the directory
404
+ containing the PASCAL VOC XML annotations.
405
+ force_masks (bool): If True, forces masks to
406
+ be loaded for all annotations, regardless of whether they are present.
407
+
408
+ Returns:
409
+ DetectionDataset: A DetectionDataset instance containing
410
+ the loaded images and annotations.
411
+
412
+ Examples:
413
+ ```python
414
+ import roboflow
415
+ from roboflow import Roboflow
416
+ import eye as sv
417
+
418
+ roboflow.login()
419
+
420
+ rf = Roboflow()
421
+
422
+ project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
423
+ dataset = project.version(PROJECT_VERSION).download("voc")
424
+
425
+ ds = sv.DetectionDataset.from_pascal_voc(
426
+ images_directory_path=f"{dataset.location}/train/images",
427
+ annotations_directory_path=f"{dataset.location}/train/labels"
428
+ )
429
+
430
+ ds.classes
431
+ # ['dog', 'person']
432
+ ```
433
+ """
434
+
435
+ classes, image_paths, annotations = load_pascal_voc_annotations(
436
+ images_directory_path=images_directory_path,
437
+ annotations_directory_path=annotations_directory_path,
438
+ force_masks=force_masks,
439
+ )
440
+
441
+ return DetectionDataset(
442
+ classes=classes, images=image_paths, annotations=annotations
443
+ )
444
+
445
+ @classmethod
446
+ def from_yolo(
447
+ cls,
448
+ images_directory_path: str,
449
+ annotations_directory_path: str,
450
+ data_yaml_path: str,
451
+ force_masks: bool = False,
452
+ is_obb: bool = False,
453
+ ) -> DetectionDataset:
454
+ """
455
+ Creates a Dataset instance from YOLO formatted data.
456
+
457
+ Args:
458
+ images_directory_path (str): The path to the
459
+ directory containing the images.
460
+ annotations_directory_path (str): The path to the directory
461
+ containing the YOLO annotation files.
462
+ data_yaml_path (str): The path to the data
463
+ YAML file containing class information.
464
+ force_masks (bool): If True, forces
465
+ masks to be loaded for all annotations,
466
+ regardless of whether they are present.
467
+ is_obb (bool): If True, loads the annotations in OBB format.
468
+ OBB annotations are defined as `[class_id, x, y, x, y, x, y, x, y]`,
469
+ where pairs of [x, y] are box corners.
470
+
471
+ Returns:
472
+ DetectionDataset: A DetectionDataset instance
473
+ containing the loaded images and annotations.
474
+
475
+ Examples:
476
+ ```python
477
+ import roboflow
478
+ from roboflow import Roboflow
479
+ import eye as sv
480
+
481
+ roboflow.login()
482
+ rf = Roboflow()
483
+
484
+ project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
485
+ dataset = project.version(PROJECT_VERSION).download("yolov5")
486
+
487
+ ds = sv.DetectionDataset.from_yolo(
488
+ images_directory_path=f"{dataset.location}/train/images",
489
+ annotations_directory_path=f"{dataset.location}/train/labels",
490
+ data_yaml_path=f"{dataset.location}/data.yaml"
491
+ )
492
+
493
+ ds.classes
494
+ # ['dog', 'person']
495
+ ```
496
+ """
497
+ classes, image_paths, annotations = load_yolo_annotations(
498
+ images_directory_path=images_directory_path,
499
+ annotations_directory_path=annotations_directory_path,
500
+ data_yaml_path=data_yaml_path,
501
+ force_masks=force_masks,
502
+ is_obb=is_obb,
503
+ )
504
+ return DetectionDataset(
505
+ classes=classes, images=image_paths, annotations=annotations
506
+ )
507
+
508
+ def as_yolo(
509
+ self,
510
+ images_directory_path: Optional[str] = None,
511
+ annotations_directory_path: Optional[str] = None,
512
+ data_yaml_path: Optional[str] = None,
513
+ min_image_area_percentage: float = 0.0,
514
+ max_image_area_percentage: float = 1.0,
515
+ approximation_percentage: float = 0.0,
516
+ ) -> None:
517
+ """
518
+ Exports the dataset to YOLO format. This method saves the
519
+ images and their corresponding annotations in YOLO format.
520
+
521
+ Args:
522
+ images_directory_path (Optional[str]): The path to the
523
+ directory where the images should be saved.
524
+ If not provided, images will not be saved.
525
+ annotations_directory_path (Optional[str]): The path to the
526
+ directory where the annotations in
527
+ YOLO format should be saved. If not provided,
528
+ annotations will not be saved.
529
+ data_yaml_path (Optional[str]): The path where the data.yaml
530
+ file should be saved.
531
+ If not provided, the file will not be saved.
532
+ min_image_area_percentage (float): The minimum percentage of
533
+ detection area relative to
534
+ the image area for a detection to be included.
535
+ Argument is used only for segmentation datasets.
536
+ max_image_area_percentage (float): The maximum percentage
537
+ of detection area relative to
538
+ the image area for a detection to be included.
539
+ Argument is used only for segmentation datasets.
540
+ approximation_percentage (float): The percentage of polygon points to
541
+ be removed from the input polygon, in the range [0, 1).
542
+ This is useful for simplifying the annotations.
543
+ Argument is used only for segmentation datasets.
544
+ """
545
+ if images_directory_path is not None:
546
+ save_dataset_images(
547
+ dataset=self, images_directory_path=images_directory_path
548
+ )
549
+ if annotations_directory_path is not None:
550
+ save_yolo_annotations(
551
+ dataset=self,
552
+ annotations_directory_path=annotations_directory_path,
553
+ min_image_area_percentage=min_image_area_percentage,
554
+ max_image_area_percentage=max_image_area_percentage,
555
+ approximation_percentage=approximation_percentage,
556
+ )
557
+ if data_yaml_path is not None:
558
+ save_data_yaml(data_yaml_path=data_yaml_path, classes=self.classes)
559
+
560
+ @classmethod
561
+ def from_coco(
562
+ cls,
563
+ images_directory_path: str,
564
+ annotations_path: str,
565
+ force_masks: bool = False,
566
+ ) -> DetectionDataset:
567
+ """
568
+ Creates a Dataset instance from COCO formatted data.
569
+
570
+ Args:
571
+ images_directory_path (str): The path to the
572
+ directory containing the images.
573
+ annotations_path (str): The path to the json annotation files.
574
+ force_masks (bool): If True,
575
+ forces masks to be loaded for all annotations,
576
+ regardless of whether they are present.
577
+
578
+ Returns:
579
+ DetectionDataset: A DetectionDataset instance containing
580
+ the loaded images and annotations.
581
+
582
+ Examples:
583
+ ```python
584
+ import roboflow
585
+ from roboflow import Roboflow
586
+ import eye as sv
587
+
588
+ roboflow.login()
589
+ rf = Roboflow()
590
+
591
+ project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
592
+ dataset = project.version(PROJECT_VERSION).download("coco")
593
+
594
+ ds = sv.DetectionDataset.from_coco(
595
+ images_directory_path=f"{dataset.location}/train",
596
+ annotations_path=f"{dataset.location}/train/_annotations.coco.json",
597
+ )
598
+
599
+ ds.classes
600
+ # ['dog', 'person']
601
+ ```
602
+ """
603
+ classes, images, annotations = load_coco_annotations(
604
+ images_directory_path=images_directory_path,
605
+ annotations_path=annotations_path,
606
+ force_masks=force_masks,
607
+ )
608
+ return DetectionDataset(classes=classes, images=images, annotations=annotations)
609
+
610
+ def as_coco(
611
+ self,
612
+ images_directory_path: Optional[str] = None,
613
+ annotations_path: Optional[str] = None,
614
+ min_image_area_percentage: float = 0.0,
615
+ max_image_area_percentage: float = 1.0,
616
+ approximation_percentage: float = 0.0,
617
+ ) -> None:
618
+ """
619
+ Exports the dataset to COCO format. This method saves the
620
+ images and their corresponding annotations in COCO format.
621
+
622
+ !!! tip
623
+
624
+ The format of the mask is determined automatically based on its structure:
625
+
626
+ - If a mask contains multiple disconnected components or holes, it will be
627
+ saved using the Run-Length Encoding (RLE) format for efficient storage and
628
+ processing.
629
+ - If a mask consists of a single, contiguous region without any holes, it
630
+ will be encoded as a polygon, preserving the outline of the object.
631
+
632
+ This automatic selection ensures that the masks are stored in the most
633
+ appropriate and space-efficient format, complying with COCO dataset
634
+ standards.
635
+
636
+ Args:
637
+ images_directory_path (Optional[str]): The path to the directory
638
+ where the images should be saved.
639
+ If not provided, images will not be saved.
640
+ annotations_path (Optional[str]): The path to COCO annotation file.
641
+ min_image_area_percentage (float): The minimum percentage of
642
+ detection area relative to
643
+ the image area for a detection to be included.
644
+ Argument is used only for segmentation datasets.
645
+ max_image_area_percentage (float): The maximum percentage of
646
+ detection area relative to
647
+ the image area for a detection to be included.
648
+ Argument is used only for segmentation datasets.
649
+ approximation_percentage (float): The percentage of polygon points
650
+ to be removed from the input polygon,
651
+ in the range [0, 1). This is useful for simplifying the annotations.
652
+ Argument is used only for segmentation datasets.
653
+ """
654
+ if images_directory_path is not None:
655
+ save_dataset_images(
656
+ dataset=self, images_directory_path=images_directory_path
657
+ )
658
+ if annotations_path is not None:
659
+ save_coco_annotations(
660
+ dataset=self,
661
+ annotation_path=annotations_path,
662
+ min_image_area_percentage=min_image_area_percentage,
663
+ max_image_area_percentage=max_image_area_percentage,
664
+ approximation_percentage=approximation_percentage,
665
+ )
666
+
667
+
668
+ @dataclass
669
+ class ClassificationDataset(BaseDataset):
670
+ """
671
+ Contains information about a classification dataset, handles lazy image
672
+ loading, dataset splitting.
673
+
674
+ Attributes:
675
+ classes (List[str]): List containing dataset class names.
676
+ images (Union[List[str], Dict[str, np.ndarray]]):
677
+ List of image paths or dictionary mapping image name to image data.
678
+ annotations (Dict[str, Classifications]): Dictionary mapping
679
+ image name to annotations.
680
+ """
681
+
682
+ def __init__(
683
+ self,
684
+ classes: List[str],
685
+ images: Union[List[str], Dict[str, np.ndarray]],
686
+ annotations: Dict[str, Classifications],
687
+ ) -> None:
688
+ self.classes = classes
689
+
690
+ if set(images) != set(annotations):
691
+ raise ValueError(
692
+ "The keys of the images and annotations dictionaries must match."
693
+ )
694
+ self.annotations = annotations
695
+
696
+ # Eliminate duplicates while preserving order
697
+ self.image_paths = list(dict.fromkeys(images))
698
+
699
+ self._images_in_memory: Dict[str, np.ndarray] = {}
700
+ if isinstance(images, dict):
701
+ self._images_in_memory = images
702
+ warn_deprecated(
703
+ "Passing a `Dict[str, np.ndarray]` into `ClassificationDataset` is "
704
+ "deprecated and will be removed in a future release. Use "
705
+ "a list of paths `List[str]` instead."
706
+ )
707
+
708
+ @property
709
+ @deprecated(
710
+ "`DetectionDataset.images` property is deprecated and will be removed in "
711
+ "`eye-0.26.0`. Iterate with `for path, image, annotation in dataset:` "
712
+ "instead."
713
+ )
714
+ def images(self) -> Dict[str, np.ndarray]:
715
+ """
716
+ Load all images to memory and return them as a dictionary.
717
+
718
+ !!! warning
719
+
720
+ Only use this when you need all images at once.
721
+ It is much more memory-efficient to initialize dataset with
722
+ image paths and use `for path, image, annotation in dataset:`.
723
+ """
724
+ if self._images_in_memory:
725
+ return self._images_in_memory
726
+
727
+ images = {image_path: cv2.imread(image_path) for image_path in self.image_paths}
728
+ return images
729
+
730
+ def _get_image(self, image_path: str) -> np.ndarray:
731
+ """Assumes that image is in dataset"""
732
+ if self._images_in_memory:
733
+ return self._images_in_memory[image_path]
734
+ return cv2.imread(image_path)
735
+
736
+ def __len__(self) -> int:
737
+ return len(self._images_in_memory) or len(self.image_paths)
738
+
739
+ def __getitem__(self, i: int) -> Tuple[str, np.ndarray, Classifications]:
740
+ """
741
+ Returns:
742
+ Tuple[str, np.ndarray, Classifications]: The image path, image data,
743
+ and its corresponding annotation at index i.
744
+ """
745
+ image_path = self.image_paths[i]
746
+ image = self._get_image(image_path)
747
+ annotation = self.annotations[image_path]
748
+ return image_path, image, annotation
749
+
750
+ def __iter__(self) -> Iterator[Tuple[str, np.ndarray, Classifications]]:
751
+ """
752
+ Iterate over the images and annotations in the dataset.
753
+
754
+ Yields:
755
+ Iterator[Tuple[str, np.ndarray, Detections]]:
756
+ An iterator that yields tuples containing the image path,
757
+ the image data, and its corresponding annotation.
758
+ """
759
+ for i in range(len(self)):
760
+ image_path, image, annotation = self[i]
761
+ yield image_path, image, annotation
762
+
763
+ def __eq__(self, other) -> bool:
764
+ if not isinstance(other, ClassificationDataset):
765
+ return False
766
+
767
+ if set(self.classes) != set(other.classes):
768
+ return False
769
+
770
+ if self.image_paths != other.image_paths:
771
+ return False
772
+
773
+ if self._images_in_memory or other._images_in_memory:
774
+ if not np.array_equal(
775
+ list(self._images_in_memory.values()),
776
+ list(other._images_in_memory.values()),
777
+ ):
778
+ return False
779
+
780
+ if self.annotations != other.annotations:
781
+ return False
782
+
783
+ return True
784
+
785
+ def split(
786
+ self,
787
+ split_ratio: float = 0.8,
788
+ random_state: Optional[int] = None,
789
+ shuffle: bool = True,
790
+ ) -> Tuple[ClassificationDataset, ClassificationDataset]:
791
+ """
792
+ Splits the dataset into two parts (training and testing)
793
+ using the provided split_ratio.
794
+
795
+ Args:
796
+ split_ratio (float): The ratio of the training
797
+ set to the entire dataset.
798
+ random_state (Optional[int]): The seed for the
799
+ random number generator. This is used for reproducibility.
800
+ shuffle (bool): Whether to shuffle the data before splitting.
801
+
802
+ Returns:
803
+ Tuple[ClassificationDataset, ClassificationDataset]: A tuple containing
804
+ the training and testing datasets.
805
+
806
+ Examples:
807
+ ```python
808
+ import eye as sv
809
+
810
+ cd = sv.ClassificationDataset(...)
811
+ train_cd,test_cd = cd.split(split_ratio=0.7, random_state=42,shuffle=True)
812
+ len(train_cd), len(test_cd)
813
+ # (700, 300)
814
+ ```
815
+ """
816
+ train_paths, test_paths = train_test_split(
817
+ data=self.image_paths,
818
+ train_ratio=split_ratio,
819
+ random_state=random_state,
820
+ shuffle=shuffle,
821
+ )
822
+
823
+ train_input: Union[List[str], Dict[str, np.ndarray]]
824
+ test_input: Union[List[str], Dict[str, np.ndarray]]
825
+ if self._images_in_memory:
826
+ train_input = {path: self._images_in_memory[path] for path in train_paths}
827
+ test_input = {path: self._images_in_memory[path] for path in test_paths}
828
+ else:
829
+ train_input = train_paths
830
+ test_input = test_paths
831
+ train_annotations = {path: self.annotations[path] for path in train_paths}
832
+ test_annotations = {path: self.annotations[path] for path in test_paths}
833
+
834
+ train_dataset = ClassificationDataset(
835
+ classes=self.classes,
836
+ images=train_input,
837
+ annotations=train_annotations,
838
+ )
839
+ test_dataset = ClassificationDataset(
840
+ classes=self.classes,
841
+ images=test_input,
842
+ annotations=test_annotations,
843
+ )
844
+
845
+ return train_dataset, test_dataset
846
+
847
+ def as_folder_structure(self, root_directory_path: str) -> None:
848
+ """
849
+ Saves the dataset as a multi-class folder structure.
850
+
851
+ Args:
852
+ root_directory_path (str): The path to the directory
853
+ where the dataset will be saved.
854
+ """
855
+ os.makedirs(root_directory_path, exist_ok=True)
856
+
857
+ for class_name in self.classes:
858
+ os.makedirs(os.path.join(root_directory_path, class_name), exist_ok=True)
859
+
860
+ for image_save_path, image, annotation in self:
861
+ image_name = Path(image_save_path).name
862
+ class_id = (
863
+ annotation.class_id[0]
864
+ if annotation.confidence is None
865
+ else annotation.get_top_k(1)[0][0]
866
+ )
867
+ class_name = self.classes[class_id]
868
+ image_save_path = os.path.join(root_directory_path, class_name, image_name)
869
+ cv2.imwrite(image_save_path, image)
870
+
871
+ @classmethod
872
+ def from_folder_structure(cls, root_directory_path: str) -> ClassificationDataset:
873
+ """
874
+ Load data from a multiclass folder structure into a ClassificationDataset.
875
+
876
+ Args:
877
+ root_directory_path (str): The path to the dataset directory.
878
+
879
+ Returns:
880
+ ClassificationDataset: The dataset.
881
+
882
+ Examples:
883
+ ```python
884
+ import roboflow
885
+ from roboflow import Roboflow
886
+ import eye as sv
887
+
888
+ roboflow.login()
889
+ rf = Roboflow()
890
+
891
+ project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
892
+ dataset = project.version(PROJECT_VERSION).download("folder")
893
+
894
+ cd = sv.ClassificationDataset.from_folder_structure(
895
+ root_directory_path=f"{dataset.location}/train"
896
+ )
897
+ ```
898
+ """
899
+ classes = os.listdir(root_directory_path)
900
+ classes = sorted(set(classes))
901
+
902
+ image_paths = []
903
+ annotations = {}
904
+
905
+ for class_name in classes:
906
+ class_id = classes.index(class_name)
907
+
908
+ for image in os.listdir(os.path.join(root_directory_path, class_name)):
909
+ image_path = str(os.path.join(root_directory_path, class_name, image))
910
+ image_paths.append(image_path)
911
+ annotations[image_path] = Classifications(
912
+ class_id=np.array([class_id]),
913
+ )
914
+
915
+ return cls(
916
+ classes=classes,
917
+ images=image_paths,
918
+ annotations=annotations,
919
+ )