maite-datasets 0.0.3__py3-none-any.whl → 0.0.4a0__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.
@@ -3,12 +3,18 @@
3
3
  from maite_datasets._builder import to_image_classification_dataset, to_object_detection_dataset
4
4
  from maite_datasets._collate import collate_as_torch, collate_as_numpy, collate_as_list
5
5
  from maite_datasets._validate import validate_dataset
6
+ from maite_datasets._reader._factory import create_dataset_reader
7
+ from maite_datasets._reader._coco import COCODatasetReader
8
+ from maite_datasets._reader._yolo import YOLODatasetReader
6
9
 
7
10
  __all__ = [
8
11
  "collate_as_list",
9
12
  "collate_as_numpy",
10
13
  "collate_as_torch",
14
+ "create_dataset_reader",
11
15
  "to_image_classification_dataset",
12
16
  "to_object_detection_dataset",
13
17
  "validate_dataset",
18
+ "COCODatasetReader",
19
+ "YOLODatasetReader",
14
20
  ]
@@ -22,11 +22,12 @@ from maite_datasets._protocols import (
22
22
  DatasetMetadata,
23
23
  ImageClassificationDataset,
24
24
  ObjectDetectionDataset,
25
+ DatumMetadata,
25
26
  )
26
27
 
27
28
 
28
- def _ensure_id(index: int, metadata: dict[str, Any]) -> dict[str, Any]:
29
- return {"id": index, **metadata} if "id" not in metadata else metadata
29
+ def _ensure_id(index: int, metadata: dict[str, Any]) -> DatumMetadata:
30
+ return DatumMetadata(**({"id": index, **metadata} if "id" not in metadata else metadata))
30
31
 
31
32
 
32
33
  def _validate_data(
@@ -141,7 +142,7 @@ class CustomImageClassificationDataset(BaseAnnotatedDataset[Sequence[int]], Imag
141
142
  self.__class__.__name__ = name
142
143
  self.__class__.__qualname__ = name
143
144
 
144
- def __getitem__(self, idx: int, /) -> tuple[Array, Array, dict[str, Any]]:
145
+ def __getitem__(self, idx: int, /) -> tuple[Array, Array, DatumMetadata]:
145
146
  one_hot = [0.0] * len(self._index2label)
146
147
  one_hot[self._labels[idx]] = 1.0
147
148
  return (
@@ -206,7 +207,7 @@ class CustomObjectDetectionDataset(BaseAnnotatedDataset[Sequence[Sequence[int]]]
206
207
  def metadata(self) -> DatasetMetadata:
207
208
  return DatasetMetadata(id=self._id, index2label=self._index2label)
208
209
 
209
- def __getitem__(self, idx: int, /) -> tuple[Array, ObjectDetectionTarget, dict[str, Any]]:
210
+ def __getitem__(self, idx: int, /) -> tuple[Array, ObjectDetectionTarget, DatumMetadata]:
210
211
  return (
211
212
  self._images[idx],
212
213
  self.ObjectDetectionTarget(self._labels[idx], self._bboxes[idx], len(self._classes)),
@@ -1,14 +1,14 @@
1
1
  """
2
- Common type protocols used for interoperability with MAITE.
2
+ Common type protocols used for interoperability.
3
3
  """
4
4
 
5
+ from collections.abc import Iterator
5
6
  import sys
6
7
  from typing import (
7
8
  Any,
8
9
  Generic,
9
- Iterator,
10
- Mapping,
11
10
  Protocol,
11
+ TypeAlias,
12
12
  TypedDict,
13
13
  TypeVar,
14
14
  runtime_checkable,
@@ -36,29 +36,10 @@ See Also
36
36
  @runtime_checkable
37
37
  class Array(Protocol):
38
38
  """
39
- Protocol for array objects providing interoperability with DataEval.
39
+ Protocol for interoperable array objects.
40
40
 
41
41
  Supports common array representations with popular libraries like
42
42
  PyTorch, Tensorflow and JAX, as well as NumPy arrays.
43
-
44
- Example
45
- -------
46
- >>> import numpy as np
47
- >>> import torch
48
- >>> from maite_datasets._typing import Array
49
-
50
- Create array objects
51
-
52
- >>> ndarray = np.random.random((10, 10))
53
- >>> tensor = torch.tensor([1, 2, 3])
54
-
55
- Check type at runtime
56
-
57
- >>> isinstance(ndarray, Array)
58
- True
59
-
60
- >>> isinstance(tensor, Array)
61
- True
62
43
  """
63
44
 
64
45
  @property
@@ -71,6 +52,7 @@ class Array(Protocol):
71
52
 
72
53
  _T = TypeVar("_T")
73
54
  _T_co = TypeVar("_T_co", covariant=True)
55
+ _T_cn = TypeVar("_T_cn", contravariant=True)
74
56
 
75
57
 
76
58
  class DatasetMetadata(TypedDict, total=False):
@@ -89,6 +71,19 @@ class DatasetMetadata(TypedDict, total=False):
89
71
  index2label: NotRequired[ReadOnly[dict[int, str]]]
90
72
 
91
73
 
74
+ class DatumMetadata(TypedDict, total=False):
75
+ """
76
+ Datum level metadata required for all `AnnotatedDataset` classes.
77
+
78
+ Attributes
79
+ ----------
80
+ id : Required[str]
81
+ A unique identifier for the datum
82
+ """
83
+
84
+ id: Required[ReadOnly[str]]
85
+
86
+
92
87
  @runtime_checkable
93
88
  class Dataset(Generic[_T_co], Protocol):
94
89
  """
@@ -134,7 +129,7 @@ class AnnotatedDataset(Dataset[_T_co], Generic[_T_co], Protocol):
134
129
  # ========== IMAGE CLASSIFICATION DATASETS ==========
135
130
 
136
131
 
137
- ImageClassificationDatum: TypeAlias = tuple[ArrayLike, ArrayLike, Mapping[str, Any]]
132
+ ImageClassificationDatum: TypeAlias = tuple[ArrayLike, ArrayLike, DatumMetadata]
138
133
  """
139
134
  Type alias for an image classification datum tuple.
140
135
 
@@ -174,7 +169,7 @@ class ObjectDetectionTarget(Protocol):
174
169
  def scores(self) -> ArrayLike: ...
175
170
 
176
171
 
177
- ObjectDetectionDatum: TypeAlias = tuple[ArrayLike, ObjectDetectionTarget, Mapping[str, Any]]
172
+ ObjectDetectionDatum: TypeAlias = tuple[ArrayLike, ObjectDetectionTarget, DatumMetadata]
178
173
  """
179
174
  Type alias for an object detection datum tuple.
180
175
 
@@ -0,0 +1,6 @@
1
+ """
2
+ Dataset readers for common computer vision dataset formats.
3
+
4
+ This module provides standardized readers that for loading datasets
5
+ from directory structures.
6
+ """
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+ from maite_datasets._protocols import ArrayLike, ObjectDetectionDataset
11
+
12
+ _logger = logging.getLogger(__name__)
13
+
14
+
15
+ class _ObjectDetectionTarget:
16
+ """Internal implementation of ObjectDetectionTarget protocol."""
17
+
18
+ def __init__(self, boxes: ArrayLike, labels: ArrayLike, scores: ArrayLike) -> None:
19
+ self._boxes = np.asarray(boxes)
20
+ self._labels = np.asarray(labels)
21
+ self._scores = np.asarray(scores)
22
+
23
+ @property
24
+ def boxes(self) -> ArrayLike:
25
+ return self._boxes
26
+
27
+ @property
28
+ def labels(self) -> ArrayLike:
29
+ return self._labels
30
+
31
+ @property
32
+ def scores(self) -> ArrayLike:
33
+ return self._scores
34
+
35
+
36
+ class BaseDatasetReader(ABC):
37
+ """
38
+ Abstract base class for object detection dataset readers.
39
+
40
+ Provides common functionality for dataset path handling, validation,
41
+ and dataset creation while allowing format-specific implementations.
42
+
43
+ Parameters
44
+ ----------
45
+ dataset_path : str or Path
46
+ Root directory containing dataset files
47
+ dataset_id : str or None, default None
48
+ Dataset identifier. If None, uses dataset_path name
49
+ """
50
+
51
+ def __init__(self, dataset_path: str | Path, dataset_id: str | None = None) -> None:
52
+ self.dataset_path: Path = Path(dataset_path)
53
+ self.dataset_id: str = dataset_id or self.dataset_path.name
54
+
55
+ # Basic path validation
56
+ if not self.dataset_path.exists():
57
+ raise FileNotFoundError(f"Dataset path not found: {self.dataset_path}")
58
+
59
+ # Format-specific initialization
60
+ self._initialize_format_specific()
61
+
62
+ @abstractmethod
63
+ def _initialize_format_specific(self) -> None:
64
+ """Initialize format-specific components (annotations, classes, etc.)."""
65
+ pass
66
+
67
+ @abstractmethod
68
+ def _create_dataset_implementation(self) -> ObjectDetectionDataset:
69
+ """Create the format-specific dataset implementation."""
70
+ pass
71
+
72
+ @abstractmethod
73
+ def _validate_format_specific(self) -> tuple[list[str], dict[str, Any]]:
74
+ """Validate format-specific structure and return issues and stats."""
75
+ pass
76
+
77
+ @property
78
+ @abstractmethod
79
+ def index2label(self) -> dict[int, str]:
80
+ """Mapping from class index to class name."""
81
+ pass
82
+
83
+ def _validate_images_directory(self) -> tuple[list[str], dict[str, Any]]:
84
+ """Validate images directory and return issues and stats."""
85
+ issues = []
86
+ stats = {}
87
+
88
+ images_path = self.dataset_path / "images"
89
+ if not images_path.exists():
90
+ issues.append("Missing images/ directory")
91
+ return issues, stats
92
+
93
+ image_files = []
94
+ for ext in [".jpg", ".jpeg", ".png", ".bmp"]:
95
+ image_files.extend(images_path.glob(f"*{ext}"))
96
+ image_files.extend(images_path.glob(f"*{ext.upper()}"))
97
+
98
+ stats["num_images"] = len(image_files)
99
+ if len(image_files) == 0:
100
+ issues.append("No image files found in images/ directory")
101
+
102
+ return issues, stats
103
+
104
+ def validate_structure(self) -> dict[str, Any]:
105
+ """
106
+ Validate dataset directory structure and return diagnostic information.
107
+
108
+ Returns
109
+ -------
110
+ dict[str, Any]
111
+ Validation results containing:
112
+ - is_valid: bool indicating if structure is valid
113
+ - issues: list of validation issues found
114
+ - stats: dict with dataset statistics
115
+ """
116
+ # Validate images directory (common to all formats)
117
+ issues, stats = self._validate_images_directory()
118
+
119
+ # Format-specific validation
120
+ format_issues, format_stats = self._validate_format_specific()
121
+ issues.extend(format_issues)
122
+ stats.update(format_stats)
123
+
124
+ return {"is_valid": len(issues) == 0, "issues": issues, "stats": stats}
125
+
126
+ def get_dataset(self) -> ObjectDetectionDataset:
127
+ """
128
+ Get dataset conforming to MAITE ObjectDetectionDataset protocol.
129
+
130
+ Returns
131
+ -------
132
+ ObjectDetectionDataset
133
+ Dataset instance with MAITE-compatible interface
134
+ """
135
+ return self._create_dataset_implementation()
@@ -0,0 +1,291 @@
1
+ """Dataset reader for COCO detection format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from PIL import Image
12
+
13
+ from maite_datasets._protocols import DatasetMetadata, DatumMetadata, ObjectDetectionDataset, ObjectDetectionDatum
14
+ from maite_datasets._reader._base import _ObjectDetectionTarget, BaseDatasetReader
15
+
16
+ _logger = logging.getLogger(__name__)
17
+
18
+
19
+ class COCODatasetReader(BaseDatasetReader):
20
+ """
21
+ COCO format dataset reader conforming to MAITE protocols.
22
+
23
+ Reads COCO format object detection datasets from disk and provides
24
+ MAITE-compatible interface.
25
+
26
+ Directory Structure Requirements
27
+ --------------------------------
28
+ ```
29
+ dataset_root/
30
+ ├── images/
31
+ │ ├── image1.jpg
32
+ │ ├── image2.jpg
33
+ │ └── ...
34
+ ├── annotations.json # COCO format annotation file
35
+ └── classes.txt # Optional: one class name per line
36
+ ```
37
+
38
+ COCO Format Specifications
39
+ --------------------------
40
+ annotations.json structure:
41
+ ```json
42
+ {
43
+ "images": [
44
+ {
45
+ "id": 1,
46
+ "file_name": "image1.jpg",
47
+ "width": 640,
48
+ "height": 480
49
+ }
50
+ ],
51
+ "annotations": [
52
+ {
53
+ "id": 1,
54
+ "image_id": 1,
55
+ "category_id": 1,
56
+ "bbox": [100, 50, 200, 150], // [x, y, width, height]
57
+ "area": 30000
58
+ }
59
+ ],
60
+ "categories": [
61
+ {
62
+ "id": 1,
63
+ "name": "person"
64
+ }
65
+ ]
66
+ }
67
+ ```
68
+
69
+ classes.txt format (optional, one class per line, ordered by index):
70
+ ```
71
+ person
72
+ bicycle
73
+ car
74
+ motorcycle
75
+ ```
76
+
77
+ Parameters
78
+ ----------
79
+ dataset_path : str or Path
80
+ Root directory containing COCO dataset files
81
+ annotation_file : str, default "annotations.json"
82
+ Name of COCO annotation JSON file
83
+ images_dir : str, default "images"
84
+ Name of directory containing images
85
+ classes_file : str or None, default "classes.txt"
86
+ Optional file containing class names (one per line)
87
+ If None, uses category names from COCO annotations
88
+ dataset_id : str or None, default None
89
+ Dataset identifier. If None, uses dataset_path name
90
+
91
+ Notes
92
+ -----
93
+ COCO annotations should follow standard COCO format with:
94
+ - "images": list of image metadata
95
+ - "annotations": list of bounding box annotations
96
+ - "categories": list of category definitions
97
+
98
+ Bounding boxes are converted from COCO format (x, y, width, height)
99
+ to MAITE format (x1, y1, x2, y2).
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ dataset_path: str | Path,
105
+ annotation_file: str = "annotations.json",
106
+ images_dir: str = "images",
107
+ classes_file: str | None = "classes.txt",
108
+ dataset_id: str | None = None,
109
+ ) -> None:
110
+ self._annotation_file = annotation_file
111
+ self._images_dir = images_dir
112
+ self._classes_file = classes_file
113
+
114
+ # Initialize base class
115
+ super().__init__(dataset_path, dataset_id)
116
+
117
+ def _initialize_format_specific(self) -> None:
118
+ """Initialize COCO-specific components."""
119
+ self._images_path = self.dataset_path / self._images_dir
120
+ self._annotation_path = self.dataset_path / self._annotation_file
121
+ self._classes_path = self.dataset_path / self._classes_file if self._classes_file else None
122
+
123
+ if not self._annotation_path.exists():
124
+ raise FileNotFoundError(f"Annotation file not found: {self._annotation_path}")
125
+ if not self._images_path.exists():
126
+ raise FileNotFoundError(f"Images directory not found: {self._images_path}")
127
+
128
+ self._load_annotations()
129
+
130
+ @property
131
+ def index2label(self) -> dict[int, str]:
132
+ """Mapping from class index to class name."""
133
+ return self._index2label
134
+
135
+ def _create_dataset_implementation(self) -> ObjectDetectionDataset:
136
+ """Create COCO dataset implementation."""
137
+ return _COCODataset(self)
138
+
139
+ def _validate_format_specific(self) -> tuple[list[str], dict[str, Any]]:
140
+ """Validate COCO format specific files and structure."""
141
+ issues = []
142
+ stats = {}
143
+
144
+ annotation_path = self.dataset_path / self._annotation_file
145
+ if not annotation_path.exists():
146
+ issues.append(f"Missing {self._annotation_file} file")
147
+ return issues, stats
148
+
149
+ try:
150
+ with open(annotation_path) as f:
151
+ coco_data = json.load(f)
152
+ except json.JSONDecodeError as e:
153
+ issues.append(f"Invalid JSON in {self._annotation_file}: {e}")
154
+ return issues, stats
155
+
156
+ # Check required keys
157
+ required_keys = ["images", "annotations", "categories"]
158
+ for key in required_keys:
159
+ if key not in coco_data:
160
+ issues.append(f"Missing required key '{key}' in {self._annotation_file}")
161
+ else:
162
+ stats[f"num_{key}"] = len(coco_data[key])
163
+
164
+ # Check optional classes.txt
165
+ if self._classes_file:
166
+ classes_path = self.dataset_path / self._classes_file
167
+ if classes_path.exists():
168
+ try:
169
+ with open(classes_path) as f:
170
+ class_lines = [line.strip() for line in f if line.strip()]
171
+ stats["num_class_names"] = len(class_lines)
172
+ except Exception as e:
173
+ issues.append(f"Error reading {self._classes_file}: {e}")
174
+
175
+ return issues, stats
176
+
177
+ def _load_annotations(self) -> None:
178
+ """Load and parse COCO annotations."""
179
+ with open(self._annotation_path) as f:
180
+ self._coco_data = json.load(f)
181
+
182
+ # Build mappings
183
+ self._image_id_to_info = {img["id"]: img for img in self._coco_data["images"]}
184
+ self._category_id_to_idx = {cat["id"]: idx for idx, cat in enumerate(self._coco_data["categories"])}
185
+
186
+ # Group annotations by image
187
+ self.image_id_to_annotations: dict[int, list[dict[str, Any]]] = {}
188
+ for ann in self._coco_data["annotations"]:
189
+ img_id = ann["image_id"]
190
+ if img_id not in self.image_id_to_annotations:
191
+ self.image_id_to_annotations[img_id] = []
192
+ self.image_id_to_annotations[img_id].append(ann)
193
+
194
+ # Load class names
195
+ if self._classes_path and self._classes_path.exists():
196
+ with open(self._classes_path) as f:
197
+ class_names = [line.strip() for line in f if line.strip()]
198
+ else:
199
+ class_names = [cat["name"] for cat in self._coco_data["categories"]]
200
+
201
+ self._index2label = {idx: name for idx, name in enumerate(class_names)}
202
+
203
+
204
+ class _COCODataset:
205
+ """Internal COCO dataset implementation."""
206
+
207
+ def __init__(self, reader: COCODatasetReader) -> None:
208
+ self.reader = reader
209
+ self.image_ids = list(reader._image_id_to_info.keys())
210
+
211
+ @property
212
+ def metadata(self) -> DatasetMetadata:
213
+ return DatasetMetadata(
214
+ id=self.reader.dataset_id,
215
+ index2label=self.reader.index2label,
216
+ )
217
+
218
+ def __len__(self) -> int:
219
+ return len(self.image_ids)
220
+
221
+ def __getitem__(self, index: int) -> ObjectDetectionDatum:
222
+ image_id = self.image_ids[index]
223
+ image_info = self.reader._image_id_to_info[image_id]
224
+
225
+ # Load image
226
+ image_path = self.reader._images_path / image_info["file_name"]
227
+ image = np.array(Image.open(image_path).convert("RGB"))
228
+ image = np.transpose(image, (2, 0, 1)) # Convert to CHW format
229
+
230
+ # Get annotations for this image
231
+ annotations = self.reader.image_id_to_annotations.get(image_id, [])
232
+
233
+ if annotations:
234
+ boxes = []
235
+ labels = []
236
+ annotation_metadata = []
237
+
238
+ for ann in annotations:
239
+ # Convert COCO bbox (x, y, w, h) to (x1, y1, x2, y2)
240
+ x, y, w, h = ann["bbox"]
241
+ boxes.append([x, y, x + w, y + h])
242
+
243
+ # Map category_id to class index
244
+ cat_idx = self.reader._category_id_to_idx[ann["category_id"]]
245
+ labels.append(cat_idx)
246
+
247
+ # Collect annotation metadata
248
+ ann_meta = {
249
+ "annotation_id": ann["id"],
250
+ "category_id": ann["category_id"],
251
+ "area": ann.get("area", 0),
252
+ "iscrowd": ann.get("iscrowd", 0),
253
+ }
254
+ # Add any additional fields from annotation
255
+ for key, value in ann.items():
256
+ if key not in ["id", "image_id", "category_id", "bbox", "area", "iscrowd"]:
257
+ ann_meta[f"ann_{key}"] = value
258
+ annotation_metadata.append(ann_meta)
259
+
260
+ boxes = np.array(boxes, dtype=np.float32)
261
+ labels = np.array(labels, dtype=np.int64)
262
+ scores = np.ones(len(labels), dtype=np.float32) # Ground truth scores
263
+ else:
264
+ # Empty annotations
265
+ boxes = np.empty((0, 4), dtype=np.float32)
266
+ labels = np.empty(0, dtype=np.int64)
267
+ scores = np.empty(0, dtype=np.float32)
268
+ annotation_metadata = []
269
+
270
+ target = _ObjectDetectionTarget(boxes, labels, scores)
271
+
272
+ # Create comprehensive datum metadata
273
+ datum_metadata = DatumMetadata(
274
+ **{
275
+ "id": f"{self.reader.dataset_id}_{image_id}",
276
+ # Image-level metadata
277
+ "coco_image_id": image_id,
278
+ "file_name": image_info["file_name"],
279
+ "width": image_info["width"],
280
+ "height": image_info["height"],
281
+ # Optional COCO image fields
282
+ **{
283
+ key: value for key, value in image_info.items() if key not in ["id", "file_name", "width", "height"]
284
+ },
285
+ # Annotation metadata
286
+ "annotations": annotation_metadata,
287
+ "num_annotations": len(annotations),
288
+ }
289
+ )
290
+
291
+ return image, target, datum_metadata
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from maite_datasets._reader._base import BaseDatasetReader
7
+ from maite_datasets._reader._yolo import YOLODatasetReader
8
+ from maite_datasets._reader._coco import COCODatasetReader
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+
13
+ def create_dataset_reader(dataset_path: str | Path, format_hint: str | None = None) -> BaseDatasetReader:
14
+ """
15
+ Factory function to create appropriate dataset reader based on directory structure.
16
+
17
+ Parameters
18
+ ----------
19
+ dataset_path : str or Path
20
+ Root directory containing dataset files
21
+ format_hint : str or None, default None
22
+ Format hint ("coco" or "yolo"). If None, auto-detects based on file structure
23
+
24
+ Returns
25
+ -------
26
+ BaseDatasetReader
27
+ Appropriate reader instance for the detected format
28
+
29
+ Raises
30
+ ------
31
+ ValueError
32
+ If format cannot be determined or is unsupported
33
+ """
34
+ dataset_path = Path(dataset_path)
35
+
36
+ if format_hint:
37
+ format_hint = format_hint.lower()
38
+ if format_hint == "coco":
39
+ return COCODatasetReader(dataset_path)
40
+ elif format_hint == "yolo":
41
+ return YOLODatasetReader(dataset_path)
42
+ else:
43
+ raise ValueError(f"Unsupported format hint: {format_hint}")
44
+
45
+ # Auto-detect format
46
+ has_annotations_json = (dataset_path / "annotations.json").exists()
47
+ has_labels_dir = (dataset_path / "labels").exists()
48
+
49
+ if has_annotations_json and not has_labels_dir:
50
+ _logger.info(f"Detected COCO format for {dataset_path}")
51
+ return COCODatasetReader(dataset_path)
52
+ elif has_labels_dir and not has_annotations_json:
53
+ _logger.info(f"Detected YOLO format for {dataset_path}")
54
+ return YOLODatasetReader(dataset_path)
55
+ elif has_annotations_json and has_labels_dir:
56
+ raise ValueError(
57
+ f"Ambiguous format in {dataset_path}: both annotations.json and labels/ exist. "
58
+ "Use format_hint parameter to specify format."
59
+ )
60
+ else:
61
+ raise ValueError(
62
+ f"Cannot detect dataset format in {dataset_path}. "
63
+ "Expected either annotations.json (COCO) or labels/ directory (YOLO)."
64
+ )
@@ -0,0 +1,315 @@
1
+ """Dataset reader for YOLO detection format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = []
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from PIL import Image
12
+
13
+ from maite_datasets._protocols import DatasetMetadata, DatumMetadata, ObjectDetectionDataset, ObjectDetectionDatum
14
+ from maite_datasets._reader._base import _ObjectDetectionTarget, BaseDatasetReader
15
+
16
+
17
+ class YOLODatasetReader(BaseDatasetReader):
18
+ """
19
+ YOLO format dataset reader conforming to MAITE protocols.
20
+
21
+ Reads YOLO format object detection datasets from disk and provides
22
+ MAITE-compatible interface.
23
+
24
+ Directory Structure Requirements
25
+ --------------------------------
26
+ ```
27
+ dataset_root/
28
+ ├── images/
29
+ │ ├── image1.jpg
30
+ │ ├── image2.jpg
31
+ │ └── ...
32
+ ├── labels/
33
+ │ ├── image1.txt # YOLO format annotations
34
+ │ ├── image2.txt
35
+ │ └── ...
36
+ ├── classes.txt # Required: one class name per line
37
+ └── data.yaml # Optional: dataset metadata
38
+ ```
39
+
40
+ YOLO Format Specifications
41
+ --------------------------
42
+ Label file format (one line per object):
43
+ ```
44
+ class_id center_x center_y width height
45
+ 0 0.5 0.3 0.2 0.4
46
+ 1 0.7 0.8 0.1 0.2
47
+ ```
48
+ All YOLO coordinates are normalized to [0, 1] relative to image dimensions.
49
+
50
+ classes.txt format (required, one class per line, ordered by index):
51
+ ```
52
+ person
53
+ bicycle
54
+ car
55
+ motorcycle
56
+ ```
57
+
58
+ Parameters
59
+ ----------
60
+ dataset_path : str or Path
61
+ Root directory containing YOLO dataset files
62
+ images_dir : str, default "images"
63
+ Name of directory containing images
64
+ labels_dir : str, default "labels"
65
+ Name of directory containing YOLO label files
66
+ classes_file : str, default "classes.txt"
67
+ File containing class names (one per line)
68
+ dataset_id : str or None, default None
69
+ Dataset identifier. If None, uses dataset_path name
70
+ image_extensions : list[str], default [".jpg", ".jpeg", ".png", ".bmp"]
71
+ Supported image file extensions
72
+
73
+ Notes
74
+ -----
75
+ YOLO label files should contain one line per object:
76
+ `class_id center_x center_y width height`
77
+
78
+ All coordinates should be normalized to [0, 1] relative to image dimensions.
79
+ Coordinates are converted to absolute pixel values and MAITE format (x1, y1, x2, y2).
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ dataset_path: str | Path,
85
+ images_dir: str = "images",
86
+ labels_dir: str = "labels",
87
+ classes_file: str = "classes.txt",
88
+ dataset_id: str | None = None,
89
+ image_extensions: list[str] | None = None,
90
+ ) -> None:
91
+ self._images_dir = images_dir
92
+ self._labels_dir = labels_dir
93
+ self._classes_file = classes_file
94
+
95
+ if image_extensions is None:
96
+ image_extensions = [".jpg", ".jpeg", ".png", ".bmp"]
97
+ self._image_extensions = [ext.lower() for ext in image_extensions]
98
+
99
+ # Initialize base class
100
+ super().__init__(dataset_path, dataset_id)
101
+
102
+ def _initialize_format_specific(self) -> None:
103
+ """Initialize YOLO-specific components."""
104
+ self._images_path = self.dataset_path / self._images_dir
105
+ self._labels_path = self.dataset_path / self._labels_dir
106
+ self._classes_path = self.dataset_path / self._classes_file
107
+
108
+ if not self._images_path.exists():
109
+ raise FileNotFoundError(f"Images directory not found: {self._images_path}")
110
+ if not self._labels_path.exists():
111
+ raise FileNotFoundError(f"Labels directory not found: {self._labels_path}")
112
+ if not self._classes_path.exists():
113
+ raise FileNotFoundError(f"Classes file not found: {self._classes_path}")
114
+
115
+ self._load_class_names()
116
+ self._find_image_files()
117
+
118
+ @property
119
+ def index2label(self) -> dict[int, str]:
120
+ """Mapping from class index to class name."""
121
+ return self._index2label
122
+
123
+ def _create_dataset_implementation(self) -> ObjectDetectionDataset:
124
+ """Create YOLO dataset implementation."""
125
+ return _YOLODataset(self)
126
+
127
+ def _validate_format_specific(self) -> tuple[list[str], dict[str, Any]]:
128
+ """Validate YOLO format specific files and structure."""
129
+ issues = []
130
+ stats = {}
131
+
132
+ # Check labels directory
133
+ labels_path = self.dataset_path / self._labels_dir
134
+ if not labels_path.exists():
135
+ issues.append(f"Missing {self._labels_dir}/ directory")
136
+ else:
137
+ label_files = list(labels_path.glob("*.txt"))
138
+ stats["num_label_files"] = len(label_files)
139
+ if len(label_files) == 0:
140
+ issues.append(f"No label files found in {self._labels_dir}/ directory")
141
+ else:
142
+ # Validate label file format (sample check)
143
+ label_issues = self._validate_yolo_label_format(labels_path)
144
+ issues.extend(label_issues)
145
+
146
+ # Check required classes.txt
147
+ classes_path = self.dataset_path / self._classes_file
148
+ if not classes_path.exists():
149
+ issues.append(f"Missing required {self._classes_file} file")
150
+ else:
151
+ try:
152
+ with open(classes_path) as f:
153
+ class_lines = [line.strip() for line in f if line.strip()]
154
+ stats["num_classes"] = len(class_lines)
155
+ if len(class_lines) == 0:
156
+ issues.append(f"{self._classes_file} is empty")
157
+ except Exception as e:
158
+ issues.append(f"Error reading {self._classes_file}: {e}")
159
+
160
+ return issues, stats
161
+
162
+ def _validate_yolo_label_format(self, labels_path: Path) -> list[str]:
163
+ """Validate YOLO label file format (sample check)."""
164
+ issues = []
165
+ label_files = list(labels_path.glob("*.txt"))
166
+
167
+ if not label_files:
168
+ return issues
169
+
170
+ label_files.sort()
171
+ sample_label = label_files[0]
172
+ try:
173
+ with open(sample_label) as f:
174
+ for line_num, line in enumerate(f, 1):
175
+ if not line.strip():
176
+ continue
177
+
178
+ parts = line.strip().split()
179
+ if len(parts) != 5:
180
+ issues.append(
181
+ f"Invalid YOLO format in {sample_label.name} line {line_num}: "
182
+ f"expected 5 values, got {len(parts)}"
183
+ )
184
+ break
185
+
186
+ try:
187
+ coords = [float(x) for x in parts[1:]]
188
+ if not all(0 <= coord <= 1 for coord in coords):
189
+ issues.append(f"Coordinates out of range [0,1] in {sample_label.name} line {line_num}")
190
+ break
191
+ except ValueError:
192
+ issues.append(f"Invalid numeric values in {sample_label.name} line {line_num}")
193
+ break
194
+ except Exception as e:
195
+ issues.append(f"Error validating label file {sample_label.name}: {e}")
196
+
197
+ return issues
198
+
199
+ def _load_class_names(self) -> None:
200
+ """Load class names from classes file."""
201
+ with open(self._classes_path) as f:
202
+ class_names = [line.strip() for line in f if line.strip()]
203
+ self._index2label = {idx: name for idx, name in enumerate(class_names)}
204
+
205
+ def _find_image_files(self) -> None:
206
+ """Find all valid image files."""
207
+ self._image_files = []
208
+ for ext in self._image_extensions:
209
+ self._image_files.extend(self._images_path.glob(f"*{ext}"))
210
+ self._image_files.sort()
211
+
212
+ if not self._image_files:
213
+ raise ValueError(f"No image files found in {self._images_path}")
214
+
215
+
216
+ class _YOLODataset:
217
+ """Internal YOLO dataset implementation."""
218
+
219
+ def __init__(self, reader: YOLODatasetReader) -> None:
220
+ self.reader = reader
221
+
222
+ @property
223
+ def metadata(self) -> DatasetMetadata:
224
+ return DatasetMetadata(
225
+ id=self.reader.dataset_id,
226
+ index2label=self.reader.index2label,
227
+ )
228
+
229
+ def __len__(self) -> int:
230
+ return len(self.reader._image_files)
231
+
232
+ def __getitem__(self, index: int) -> ObjectDetectionDatum:
233
+ image_path = self.reader._image_files[index]
234
+
235
+ # Load image
236
+ image = np.array(Image.open(image_path).convert("RGB"))
237
+ img_height, img_width = image.shape[:2]
238
+ image = np.transpose(image, (2, 0, 1)) # Convert to CHW format
239
+
240
+ # Load corresponding label file
241
+ label_path = self.reader._labels_path / f"{image_path.stem}.txt"
242
+
243
+ annotation_metadata = []
244
+ if label_path.exists():
245
+ boxes = []
246
+ labels = []
247
+
248
+ with open(label_path) as f:
249
+ for line_num, line in enumerate(f):
250
+ if not line.strip():
251
+ continue
252
+
253
+ parts = line.strip().split()
254
+ if len(parts) != 5:
255
+ continue
256
+
257
+ class_id = int(parts[0])
258
+ center_x, center_y, width, height = map(float, parts[1:])
259
+
260
+ # Convert normalized YOLO format to absolute pixel coordinates
261
+ x1 = (center_x - width / 2) * img_width
262
+ y1 = (center_y - height / 2) * img_height
263
+ x2 = (center_x + width / 2) * img_width
264
+ y2 = (center_y + height / 2) * img_height
265
+
266
+ boxes.append([x1, y1, x2, y2])
267
+ labels.append(class_id)
268
+
269
+ # Store original YOLO format coordinates in metadata
270
+ ann_meta = {
271
+ "line_number": line_num + 1,
272
+ "class_id": class_id,
273
+ "yolo_center_x": center_x,
274
+ "yolo_center_y": center_y,
275
+ "yolo_width": width,
276
+ "yolo_height": height,
277
+ "absolute_bbox": [x1, y1, x2, y2],
278
+ }
279
+ annotation_metadata.append(ann_meta)
280
+
281
+ if boxes:
282
+ boxes = np.array(boxes, dtype=np.float32)
283
+ labels = np.array(labels, dtype=np.int64)
284
+ scores = np.ones(len(labels), dtype=np.float32) # Ground truth scores
285
+ else:
286
+ boxes = np.empty((0, 4), dtype=np.float32)
287
+ labels = np.empty(0, dtype=np.int64)
288
+ scores = np.empty(0, dtype=np.float32)
289
+ else:
290
+ # No label file - empty annotations
291
+ boxes = np.empty((0, 4), dtype=np.float32)
292
+ labels = np.empty(0, dtype=np.int64)
293
+ scores = np.empty(0, dtype=np.float32)
294
+
295
+ target = _ObjectDetectionTarget(boxes, labels, scores)
296
+
297
+ # Create comprehensive datum metadata
298
+ datum_metadata = DatumMetadata(
299
+ **{
300
+ "id": f"{self.reader.dataset_id}_{image_path.stem}",
301
+ # Image-level metadata
302
+ "file_name": image_path.name,
303
+ "file_path": str(image_path),
304
+ "width": img_width,
305
+ "height": img_height,
306
+ # Label file metadata
307
+ "label_file": label_path.name if label_path.exists() else None,
308
+ "label_file_exists": label_path.exists(),
309
+ # Annotation metadata
310
+ "annotations": annotation_metadata,
311
+ "num_annotations": len(annotation_metadata),
312
+ }
313
+ )
314
+
315
+ return image, target, datum_metadata
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maite-datasets
3
- Version: 0.0.3
3
+ Version: 0.0.4a0
4
4
  Summary: A collection of Image Classification and Object Detection task datasets conforming to the MAITE protocol.
5
5
  Author-email: Andrew Weng <andrew.weng@ariacoustics.com>, Ryan Wood <ryan.wood@ariacoustics.com>, Shaun Jullens <shaun.jullens@ariacoustics.com>
6
6
  License-Expression: MIT
@@ -1,15 +1,20 @@
1
- maite_datasets/__init__.py,sha256=81LNxx03O7FzWNZQbIrSovDrdpO_x74WkLPKBJy91gU,483
1
+ maite_datasets/__init__.py,sha256=53LW5bHMAr4uD6w2bvrPxgtROUIzaE-3LR6TR0dDucs,746
2
2
  maite_datasets/_base.py,sha256=BiWB_xvL4AtV0jxVjzpcZHuRTb52dTD0CQtu08DzoXA,8195
3
- maite_datasets/_builder.py,sha256=URhRCedvuqsy88N4lzQrwI-uL1kS1_kavP9fS402sPw,10036
3
+ maite_datasets/_builder.py,sha256=yESeNf4MBm4oSPKse6jSUt0XZrxkSU2etdK82wXiiUw,10071
4
4
  maite_datasets/_collate.py,sha256=-XuKeeMmOnSB0RgQbz8BjsoqQar9Tsf_qALZxijQ498,4063
5
5
  maite_datasets/_fileio.py,sha256=7S-hF3xU60AdcsPsfYR7rjbeGZUlv3JjGEZhGJOxGYU,5622
6
- maite_datasets/_protocols.py,sha256=uwnI2P-zJnpEHJ0eOJ7dO_7KehwHEtEqR4pYcJiEXNk,5312
6
+ maite_datasets/_protocols.py,sha256=aWrnUM1stZ9VInkBEynod_OdYq2ORSpew7yoF-Zeuig,5247
7
7
  maite_datasets/_types.py,sha256=S5DMyiUrkUjV9uM0ysKqxVoi7z5P7B3EPiLI4Fyq9Jc,1147
8
8
  maite_datasets/_validate.py,sha256=sP-5lYXkmkiTadJcy_LtEMiZ0m82xR0yELoxWORrZDQ,6904
9
9
  maite_datasets/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  maite_datasets/_mixin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  maite_datasets/_mixin/_numpy.py,sha256=GEuRyeprH-STh-_zktAp0Tg6NNyMdh1ThyhjW558NOo,860
12
12
  maite_datasets/_mixin/_torch.py,sha256=pkN2vMNsDk_h5wnD5899zIHsPtEADbGfmRyI5CdGonI,827
13
+ maite_datasets/_reader/__init__.py,sha256=VzrVOsmztPJV83um8tY5qdqU-HEPP15RlLClGbxTFlQ,164
14
+ maite_datasets/_reader/_base.py,sha256=nQc3wqI02N6sC9Rk98BT6vFJa5VP9pAZjKhpyqXfW0o,4299
15
+ maite_datasets/_reader/_coco.py,sha256=sCrOixqOuqgHpee3NjDTt_bhbxgKGean2mn2Py9mh5k,10127
16
+ maite_datasets/_reader/_factory.py,sha256=cI3Cw1yWj4hK2gn6N5bugXzGMcNwcCEkJ4AoynwOZvI,2222
17
+ maite_datasets/_reader/_yolo.py,sha256=29eQMzBuJpaKFMfC0Pu_RvrGnqbqQxY0XZmeSPaYArg,11626
13
18
  maite_datasets/image_classification/__init__.py,sha256=pcZojkdsiMoLgY4mKjoQY6WyEwiGYHxNrAGpnvn3zsY,308
14
19
  maite_datasets/image_classification/_cifar10.py,sha256=w7BPGZzUV1gXFoYRgxa6VOqKn1EgQi3x1rrA4nEUbeI,8470
15
20
  maite_datasets/image_classification/_mnist.py,sha256=6xDWY4qbY1hlcUZKvVZeQMvYbF0vLtaVzOuQUKJkcJU,8248
@@ -20,7 +25,7 @@ maite_datasets/object_detection/_milco.py,sha256=KEU4JFvCxfyMAb4RFMnxTMk_MggdEAV
20
25
  maite_datasets/object_detection/_seadrone.py,sha256=w_pSojLzgwdKrUSxaz8r7dPJVKGND6JSYl0S_BKOLH0,271282
21
26
  maite_datasets/object_detection/_voc.py,sha256=VuokKaOzI1wSfgG5DC7ufMbRDlG-b6Se3hg4eQzNQbE,19731
22
27
  maite_datasets/object_detection/_voc_torch.py,sha256=bjeawnNit7Llcf_cZY_9lcJYoUoAU-Wen6MMT-7QX3k,2917
23
- maite_datasets-0.0.3.dist-info/METADATA,sha256=hoOvbKjGriS10siM8HsRvepA3nfi-QgUcrpjGsHr1lM,3747
24
- maite_datasets-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- maite_datasets-0.0.3.dist-info/licenses/LICENSE,sha256=6h3J3R-ajGHh_isDSftzS5_jJjB9HH4TaI0vU-VscaY,1082
26
- maite_datasets-0.0.3.dist-info/RECORD,,
28
+ maite_datasets-0.0.4a0.dist-info/METADATA,sha256=P0IvwtwKWspvO8Tl_V1PuPfH_XbIf0E-0gj5Qxvgrug,3749
29
+ maite_datasets-0.0.4a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ maite_datasets-0.0.4a0.dist-info/licenses/LICENSE,sha256=6h3J3R-ajGHh_isDSftzS5_jJjB9HH4TaI0vU-VscaY,1082
31
+ maite_datasets-0.0.4a0.dist-info/RECORD,,