euler-loading 2.18.0__tar.gz → 2.19.1__tar.gz

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 (59) hide show
  1. {euler_loading-2.18.0 → euler_loading-2.19.1}/PKG-INFO +2 -2
  2. {euler_loading-2.18.0 → euler_loading-2.19.1}/README.md +18 -0
  3. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/_writing.py +29 -6
  4. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/dataset.py +11 -1
  5. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/generic.py +62 -0
  6. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/generic.py +62 -0
  7. {euler_loading-2.18.0 → euler_loading-2.19.1}/package-lock.json +1 -1
  8. {euler_loading-2.18.0 → euler_loading-2.19.1}/pyproject.toml +2 -2
  9. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_loaders.py +41 -0
  10. euler_loading-2.19.1/tests/test_python_compat.py +29 -0
  11. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_writing.py +65 -0
  12. {euler_loading-2.18.0 → euler_loading-2.19.1}/.github/workflows/workflow.yml +0 -0
  13. {euler_loading-2.18.0 → euler_loading-2.19.1}/.gitignore +0 -0
  14. {euler_loading-2.18.0 → euler_loading-2.19.1}/docs/loader-attributes.md +0 -0
  15. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/__init__.py +0 -0
  16. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/_dataset_contract.py +0 -0
  17. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/_ds_crawler_utils.py +0 -0
  18. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/_metadata.py +0 -0
  19. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/_resolution.py +0 -0
  20. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/indexing.py +0 -0
  21. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/__init__.py +0 -0
  22. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/_annotations.py +0 -0
  23. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/_princeton_dense.py +0 -0
  24. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/_writer_utils.py +0 -0
  25. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/contracts.py +0 -0
  26. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/__init__.py +0 -0
  27. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  28. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/muses.py +0 -0
  29. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/princeton_dense.py +0 -0
  30. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  31. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  32. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/generate/__init__.py +0 -0
  33. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/generate/__main__.py +0 -0
  34. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/generate/loaders.json +0 -0
  35. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/generic.py +0 -0
  36. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/__init__.py +0 -0
  37. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  38. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/muses.py +0 -0
  39. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/princeton_dense.py +0 -0
  40. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  41. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  42. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/muses.py +0 -0
  43. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/princeton_dense.py +0 -0
  44. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/real_drive_sim.py +0 -0
  45. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/loaders/vkitti2.py +0 -0
  46. {euler_loading-2.18.0 → euler_loading-2.19.1}/euler_loading/preprocessing.py +0 -0
  47. {euler_loading-2.18.0 → euler_loading-2.19.1}/example.py +0 -0
  48. {euler_loading-2.18.0 → euler_loading-2.19.1}/gen_loaders.sh +0 -0
  49. {euler_loading-2.18.0 → euler_loading-2.19.1}/sample_rds.py +0 -0
  50. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/__init__.py +0 -0
  51. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/conftest.py +0 -0
  52. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/example_rds_calib.json +0 -0
  53. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_dataset.py +0 -0
  54. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_id_schema.py +0 -0
  55. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_indexing.py +0 -0
  56. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_preprocessing.py +0 -0
  57. {euler_loading-2.18.0 → euler_loading-2.19.1}/tests/test_real_dataset.py +0 -0
  58. {euler_loading-2.18.0 → euler_loading-2.19.1}/vkitti_cpu_example_output.json +0 -0
  59. {euler_loading-2.18.0 → euler_loading-2.19.1}/vkitti_gpu_example_output.json +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-loading
3
- Version: 2.18.0
3
+ Version: 2.19.1
4
4
  Summary: Multi-modal PyTorch dataloader using ds-crawler indices
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: ds-crawler>=2.3.0
7
- Requires-Dist: euler-dataset-contract
7
+ Requires-Dist: euler-dataset-contract>=0.2.1
8
8
  Requires-Dist: numpy
9
9
  Requires-Dist: pillow
10
10
  Provides-Extra: dev
@@ -190,6 +190,24 @@ when doing so is unambiguous:
190
190
 
191
191
  This is an additive layout. Existing roots with `.ds_crawler/index.json` continue to load unchanged, and a scoped modality falls back to that legacy location when the scoped artifacts are not present.
192
192
 
193
+ Output writers can create the same layout natively. Pass a scope when creating
194
+ each modality writer; ds-crawler writes the scoped artifacts and maintains
195
+ `.ds_crawler/scopes.json`:
196
+
197
+ ```python
198
+ rgb_writer = dataset.create_output_writer(
199
+ "rgb", output_root, metadata_scope="rgb"
200
+ )
201
+ depth_writer = dataset.create_output_writer(
202
+ "sparse_depth", output_root, metadata_scope="sparse_depth"
203
+ )
204
+ ```
205
+
206
+ The standalone `create_dataset_writer_from_index(...)` helper accepts the same
207
+ `metadata_scope` argument. For ZIP output, one `ZipDatasetWriter` owns and
208
+ finalizes one archive, so do not open multiple ZIP writers concurrently for the
209
+ same path.
210
+
193
211
  ### `MultiModalDataset.modality_paths()`
194
212
 
195
213
  Returns a dict mapping each regular modality name to `{"path": ..., "origin_path": ...}` and includes `split` and `metadata_scope` when configured.
@@ -7,7 +7,7 @@ import tempfile
7
7
  import zipfile
8
8
  from collections.abc import Mapping
9
9
  from pathlib import Path
10
- from typing import Any, Callable
10
+ from typing import Any, Callable, Union
11
11
 
12
12
  from ds_crawler import DatasetWriter, ZipDatasetWriter, get_dataset_contract
13
13
 
@@ -23,11 +23,20 @@ logger = logging.getLogger(__name__)
23
23
  _DEFAULT_WRITER_INDEX_FILENAME = "output.json"
24
24
 
25
25
 
26
- OutputDestination = str | os.PathLike[str] | DatasetWriter | ZipDatasetWriter
26
+ # Type aliases are evaluated at import time even with postponed annotations.
27
+ # ``typing.Union`` keeps this module importable on the supported Python 3.9.
28
+ OutputDestination = Union[
29
+ str,
30
+ os.PathLike[str],
31
+ DatasetWriter,
32
+ ZipDatasetWriter,
33
+ ]
27
34
 
28
35
 
29
36
  def _wrap_writer_save_index(
30
37
  writer: DatasetWriter | ZipDatasetWriter,
38
+ *,
39
+ metadata_scope: str | None = None,
31
40
  ) -> DatasetWriter | ZipDatasetWriter:
32
41
  original_save_index = writer.save_index
33
42
 
@@ -72,7 +81,12 @@ def _wrap_writer_save_index(
72
81
  path = original_save_index(filename=filename)
73
82
 
74
83
  if isinstance(writer, ZipDatasetWriter):
75
- entry_name = f".ds_crawler/{filename}"
84
+ metadata_prefix = (
85
+ f".ds_crawler/{metadata_scope}"
86
+ if metadata_scope is not None
87
+ else ".ds_crawler"
88
+ )
89
+ entry_name = f"{metadata_prefix}/{filename}"
76
90
  archive_path = Path(writer.root)
77
91
  with zipfile.ZipFile(archive_path, "r") as source_zip:
78
92
  output_index = json.loads(source_zip.read(entry_name).decode("utf-8"))
@@ -113,8 +127,15 @@ def create_dataset_writer_from_index(
113
127
  index_output: Mapping[str, Any],
114
128
  root: str | os.PathLike[str],
115
129
  zip: bool = False,
130
+ metadata_scope: str | None = None,
116
131
  ) -> DatasetWriter | ZipDatasetWriter:
117
- """Create a ds-crawler writer that mirrors an existing index's metadata."""
132
+ """Create a ds-crawler writer that mirrors an existing index's metadata.
133
+
134
+ ``metadata_scope`` stores the generated artifacts below
135
+ ``.ds_crawler/<metadata_scope>/`` and lets ds-crawler maintain the shared
136
+ ``.ds_crawler/scopes.json`` manifest. This is useful when several logical
137
+ modalities share one physical output root or archive.
138
+ """
118
139
  indexing = index_output.get("indexing")
119
140
  hierarchy = indexing.get("hierarchy") if isinstance(indexing, Mapping) else None
120
141
  id_cfg = indexing.get("id") if isinstance(indexing, Mapping) else None
@@ -133,8 +154,9 @@ def create_dataset_writer_from_index(
133
154
  root,
134
155
  head=contract.to_mapping(),
135
156
  separator=separator,
157
+ metadata_scope=metadata_scope,
136
158
  )
137
- return _wrap_writer_save_index(writer)
159
+ return _wrap_writer_save_index(writer, metadata_scope=metadata_scope)
138
160
 
139
161
  name = as_non_empty_str(index_output.get("name"))
140
162
  type_name = as_non_empty_str(index_output.get("type"))
@@ -160,9 +182,10 @@ def create_dataset_writer_from_index(
160
182
  type=type_name,
161
183
  euler_train=dict(euler_train) if isinstance(euler_train, Mapping) else None,
162
184
  separator=separator,
185
+ metadata_scope=metadata_scope,
163
186
  **properties,
164
187
  )
165
- return _wrap_writer_save_index(writer)
188
+ return _wrap_writer_save_index(writer, metadata_scope=metadata_scope)
166
189
 
167
190
 
168
191
  def _resolve_output_destination(
@@ -969,13 +969,23 @@ class MultiModalDataset(_BaseDataset):
969
969
  root: str | os.PathLike[str],
970
970
  *,
971
971
  zip: bool = False,
972
+ metadata_scope: str | None = None,
972
973
  ) -> DatasetWriter | ZipDatasetWriter:
973
- """Create a ds-crawler writer preconfigured from a modality's index."""
974
+ """Create a ds-crawler writer preconfigured from a modality's index.
975
+
976
+ Args:
977
+ modality_name: Modality whose index metadata should be mirrored.
978
+ root: Output directory or, when ``zip=True``, output archive.
979
+ zip: Create a :class:`ds_crawler.ZipDatasetWriter`.
980
+ metadata_scope: Optional namespace below ``.ds_crawler``. Scoped
981
+ writers also update ``.ds_crawler/scopes.json``.
982
+ """
974
983
  index_output = self.get_modality_index(modality_name)
975
984
  return create_dataset_writer_from_index(
976
985
  index_output=index_output,
977
986
  root=root,
978
987
  zip=zip,
988
+ metadata_scope=metadata_scope,
979
989
  )
980
990
 
981
991
  def get_writer(self, modality_name: str) -> Callable[..., Any]:
@@ -11,6 +11,8 @@ Supported file extensions:
11
11
  Return types
12
12
  ------------
13
13
  - **map_2d** / **scattering_coefficient** -- ``np.ndarray`` of shape ``(H, W)`` float32.
14
+ - **semantic_segmentation** / **instance_segmentation** -- integer
15
+ ``np.ndarray`` of shape ``(H, W)``.
14
16
  - **map_3d** / **atmospheric_light** -- ``np.ndarray`` of shape ``(H, W, C)`` float32.
15
17
  - **points_3d** -- ``np.ndarray`` of shape ``(3, H, W)`` float32.
16
18
  - **spherical_map** -- ``np.ndarray`` of shape ``(H, W, C)`` float32.
@@ -71,6 +73,20 @@ def _load_numpy(path: Union[str, BinaryIO]) -> np.ndarray:
71
73
  return arr.astype(np.float32)
72
74
 
73
75
 
76
+ def _load_label_map(path: Union[str, BinaryIO], dtype: np.dtype) -> np.ndarray:
77
+ """Load a standardized HW integer label map without converting to float."""
78
+ ext = os.path.splitext(_get_name(path))[1].lower()
79
+ if ext == _NPZ_EXTENSION:
80
+ with np.load(path) as npz:
81
+ arr = next(iter(npz.values()))
82
+ else:
83
+ arr = np.load(path)
84
+ arr = np.asarray(arr)
85
+ if arr.ndim != 2:
86
+ raise ValueError(f"segmentation map must have shape (H, W), got {arr.shape}")
87
+ return arr.astype(dtype, copy=False)
88
+
89
+
74
90
  def _write_numpy(path: Union[str, BinaryIO], value: Any) -> None:
75
91
  """Write an array to ``.npy`` or ``.npz`` based on extension."""
76
92
  ensure_parent(path)
@@ -116,6 +132,28 @@ def map_2d(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, at
116
132
  return _load_numpy(path)
117
133
 
118
134
 
135
+ @modality_meta(
136
+ modality_type="semantic_segmentation",
137
+ dtype="uint8",
138
+ shape="HW",
139
+ file_formats=[".npy", ".npz"],
140
+ )
141
+ def semantic_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
142
+ """Load an HW uint8 class-id map; 255 is the standardized void id."""
143
+ return _load_label_map(path, np.dtype(np.uint8))
144
+
145
+
146
+ @modality_meta(
147
+ modality_type="instance_segmentation",
148
+ dtype="uint16",
149
+ shape="HW",
150
+ file_formats=[".npy", ".npz"],
151
+ )
152
+ def instance_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
153
+ """Load an HW uint16 instance-id map; 0 denotes stuff or void."""
154
+ return _load_label_map(path, np.dtype(np.uint16))
155
+
156
+
119
157
  @modality_meta(
120
158
  modality_type="map_3d",
121
159
  dtype="float32",
@@ -244,6 +282,30 @@ def write_map_2d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] |
244
282
  _write_numpy(path, arr)
245
283
 
246
284
 
285
+ def _write_segmentation(path: Union[str, BinaryIO], value: Any, dtype: np.dtype, name: str) -> None:
286
+ arr = to_hw(value, name=name).astype(dtype, copy=False)
287
+ ensure_parent(path)
288
+ ext = os.path.splitext(_get_name(path))[1].lower()
289
+ if ext == _NPY_EXTENSION:
290
+ np.save(path, arr)
291
+ elif ext == _NPZ_EXTENSION:
292
+ np.savez_compressed(path, data=arr)
293
+ else:
294
+ raise ValueError(f"Unsupported output extension: {ext}")
295
+
296
+
297
+ @mark_stream_supported
298
+ def write_semantic_segmentation(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
299
+ """Write the standardized HW uint8 semantic map (255 means void)."""
300
+ _write_segmentation(path, value, np.dtype(np.uint8), "semantic_segmentation")
301
+
302
+
303
+ @mark_stream_supported
304
+ def write_instance_segmentation(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
305
+ """Write the standardized HW uint16 instance map (0 means stuff/void)."""
306
+ _write_segmentation(path, value, np.dtype(np.uint16), "instance_segmentation")
307
+
308
+
247
309
  @mark_stream_supported
248
310
  def write_map_3d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
249
311
  """Write a 3D map to NumPy formats based on extension.
@@ -11,6 +11,8 @@ Supported file extensions:
11
11
  Return types
12
12
  ------------
13
13
  - **map_2d** / **scattering_coefficient** -- ``torch.FloatTensor`` of shape ``(H, W)``.
14
+ - **semantic_segmentation** / **instance_segmentation** -- integer
15
+ ``torch.Tensor`` of shape ``(H, W)``.
14
16
  - **map_3d** / **atmospheric_light** -- ``torch.FloatTensor`` of shape ``(C, H, W)``.
15
17
  - **points_3d** -- ``torch.FloatTensor`` of shape ``(3, H, W)``.
16
18
  - **spherical_map** -- ``torch.FloatTensor`` of shape ``(C, H, W)``.
@@ -72,6 +74,20 @@ def _load_numpy(path: Union[str, BinaryIO]) -> np.ndarray:
72
74
  return arr.astype(np.float32)
73
75
 
74
76
 
77
+ def _load_label_map(path: Union[str, BinaryIO], dtype: np.dtype) -> np.ndarray:
78
+ """Load a standardized HW integer label map without converting to float."""
79
+ ext = os.path.splitext(_get_name(path))[1].lower()
80
+ if ext == _NPZ_EXTENSION:
81
+ with np.load(path) as npz:
82
+ arr = next(iter(npz.values()))
83
+ else:
84
+ arr = np.load(path)
85
+ arr = np.asarray(arr)
86
+ if arr.ndim != 2:
87
+ raise ValueError(f"segmentation map must have shape (H, W), got {arr.shape}")
88
+ return arr.astype(dtype, copy=False)
89
+
90
+
75
91
  def _write_numpy(path: Union[str, BinaryIO], value: Any) -> None:
76
92
  """Write an array to ``.npy`` or ``.npz`` based on extension."""
77
93
  ensure_parent(path)
@@ -118,6 +134,28 @@ def map_2d(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, at
118
134
  return torch.from_numpy(arr).contiguous()
119
135
 
120
136
 
137
+ @modality_meta(
138
+ modality_type="semantic_segmentation",
139
+ dtype="uint8",
140
+ shape="HW",
141
+ file_formats=[".npy", ".npz"],
142
+ )
143
+ def semantic_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
144
+ """Load an HW uint8 class-id map; 255 is the standardized void id."""
145
+ return torch.from_numpy(_load_label_map(path, np.dtype(np.uint8))).contiguous()
146
+
147
+
148
+ @modality_meta(
149
+ modality_type="instance_segmentation",
150
+ dtype="uint16",
151
+ shape="HW",
152
+ file_formats=[".npy", ".npz"],
153
+ )
154
+ def instance_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
155
+ """Load an HW uint16 instance-id map; 0 denotes stuff or void."""
156
+ return torch.from_numpy(_load_label_map(path, np.dtype(np.uint16))).contiguous()
157
+
158
+
121
159
  @modality_meta(
122
160
  modality_type="map_3d",
123
161
  dtype="float32",
@@ -244,6 +282,30 @@ def write_map_2d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] |
244
282
  _write_numpy(path, arr)
245
283
 
246
284
 
285
+ def _write_segmentation(path: Union[str, BinaryIO], value: Any, dtype: np.dtype, name: str) -> None:
286
+ arr = to_hw(value, name=name).astype(dtype, copy=False)
287
+ ensure_parent(path)
288
+ ext = os.path.splitext(_get_name(path))[1].lower()
289
+ if ext == _NPY_EXTENSION:
290
+ np.save(path, arr)
291
+ elif ext == _NPZ_EXTENSION:
292
+ np.savez_compressed(path, data=arr)
293
+ else:
294
+ raise ValueError(f"Unsupported output extension: {ext}")
295
+
296
+
297
+ @mark_stream_supported
298
+ def write_semantic_segmentation(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
299
+ """Write the standardized HW uint8 semantic map (255 means void)."""
300
+ _write_segmentation(path, value, np.dtype(np.uint8), "semantic_segmentation")
301
+
302
+
303
+ @mark_stream_supported
304
+ def write_instance_segmentation(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
305
+ """Write the standardized HW uint16 instance map (0 means stuff/void)."""
306
+ _write_segmentation(path, value, np.dtype(np.uint16), "instance_segmentation")
307
+
308
+
247
309
  @mark_stream_supported
248
310
  def write_map_3d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
249
311
  """Write a 3D map to NumPy formats based on extension.
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "euler-loading-3d",
2
+ "name": "ds-crawler-writing",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {}
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "euler-loading"
3
- version = "2.18.0"
3
+ version = "2.19.1"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  dependencies = [
7
7
  "numpy",
8
8
  "Pillow",
9
9
  "ds-crawler>=2.3.0",
10
- "euler-dataset-contract",
10
+ "euler-dataset-contract>=0.2.1",
11
11
  ]
12
12
 
13
13
  [project.optional-dependencies]
@@ -1179,6 +1179,8 @@ class TestRDSWriters:
1179
1179
  GENERIC_LOADER_NAMES = [
1180
1180
  "map_2d",
1181
1181
  "map_3d",
1182
+ "semantic_segmentation",
1183
+ "instance_segmentation",
1182
1184
  "points_3d",
1183
1185
  "scattering_coefficient",
1184
1186
  "atmospheric_light",
@@ -1187,6 +1189,8 @@ GENERIC_LOADER_NAMES = [
1187
1189
  GENERIC_WRITER_NAMES = [
1188
1190
  "write_map_2d",
1189
1191
  "write_map_3d",
1192
+ "write_semantic_segmentation",
1193
+ "write_instance_segmentation",
1190
1194
  "write_points_3d",
1191
1195
  "write_scattering_coefficient",
1192
1196
  "write_atmospheric_light",
@@ -1345,6 +1349,43 @@ class TestGenericBackwardCompat:
1345
1349
  assert torch.equal(top, gpu)
1346
1350
 
1347
1351
 
1352
+ class TestGenericSegmentation:
1353
+ """Generic segmentation codecs preserve the standardized CUPS format."""
1354
+
1355
+ @pytest.mark.parametrize(
1356
+ ("function", "writer", "dtype", "torch_dtype", "values"),
1357
+ [
1358
+ ("semantic_segmentation", "write_semantic_segmentation", np.uint8, torch.uint8, [[0, 18], [255, 7]]),
1359
+ ("instance_segmentation", "write_instance_segmentation", np.uint16, torch.uint16, [[0, 1], [257, 65535]]),
1360
+ ],
1361
+ )
1362
+ def test_cpu_gpu_npy_roundtrip(self, tmp_path, function, writer, dtype, torch_dtype, values):
1363
+ expected = np.asarray(values, dtype=dtype)
1364
+ path = tmp_path / f"{function}.npy"
1365
+ getattr(cpu_generic, writer)(str(path), expected)
1366
+
1367
+ cpu_value = getattr(cpu_generic, function)(str(path))
1368
+ gpu_value = getattr(gpu_generic, function)(str(path))
1369
+ assert cpu_value.dtype == dtype
1370
+ assert np.array_equal(cpu_value, expected)
1371
+ assert gpu_value.dtype == torch_dtype
1372
+ assert torch.equal(gpu_value, torch.from_numpy(expected))
1373
+
1374
+ def test_writer_accepts_singleton_channel(self, tmp_path):
1375
+ value = np.array([[[0, 1], [2, 255]]], dtype=np.uint8)
1376
+ path = tmp_path / "semantic.npz"
1377
+ cpu_generic.write_semantic_segmentation(str(path), value)
1378
+ loaded = cpu_generic.semantic_segmentation(str(path))
1379
+ assert loaded.shape == (2, 2)
1380
+ assert np.array_equal(loaded, value[0])
1381
+
1382
+ def test_loader_rejects_non_hw_storage(self, tmp_path):
1383
+ path = tmp_path / "semantic.npy"
1384
+ np.save(path, np.zeros((1, 2, 3), dtype=np.uint8))
1385
+ with pytest.raises(ValueError, match=r"must have shape \(H, W\)"):
1386
+ cpu_generic.semantic_segmentation(str(path))
1387
+
1388
+
1348
1389
  # ---------------------------------------------------------------------------
1349
1390
  # Generic map_2d / map_3d loader tests
1350
1391
  # ---------------------------------------------------------------------------
@@ -0,0 +1,29 @@
1
+ """Static checks for constructs evaluated at the Python version floor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from pathlib import Path
7
+
8
+
9
+ PACKAGE_ROOT = Path(__file__).parents[1] / "euler_loading"
10
+
11
+
12
+ def test_no_runtime_pep604_type_aliases() -> None:
13
+ """PEP 604 unions in assignment expressions fail during Python 3.9 imports."""
14
+ offenders: list[str] = []
15
+ for path in PACKAGE_ROOT.rglob("*.py"):
16
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
17
+ for statement in tree.body:
18
+ if not isinstance(statement, ast.Assign):
19
+ continue
20
+ if any(
21
+ isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr)
22
+ for node in ast.walk(statement.value)
23
+ ):
24
+ offenders.append(f"{path.relative_to(PACKAGE_ROOT)}:{statement.lineno}")
25
+
26
+ assert offenders == [], (
27
+ "Runtime PEP 604 aliases are not Python 3.9 compatible; use "
28
+ f"typing.Union/Optional instead: {offenders}"
29
+ )
@@ -240,6 +240,71 @@ class TestWriteSample:
240
240
  output_index = json.load(io.TextIOWrapper(entry, encoding="utf-8"))
241
241
  assert output_index["type"] == "txt"
242
242
 
243
+ def test_output_writer_writes_scoped_metadata_and_manifest(self, tmp_path):
244
+ def writer(path: str, value: Any, meta: dict[str, Any] | None = None) -> None:
245
+ with open(path, "w", encoding="utf-8") as f:
246
+ f.write(str(value))
247
+
248
+ index = _flat_index("txt", ["f001"])
249
+ with patch(
250
+ "euler_loading.dataset.index_dataset_from_path",
251
+ return_value=index,
252
+ ):
253
+ ds = MultiModalDataset(
254
+ modalities={
255
+ "depth": Modality(
256
+ "/data/depth", loader=dummy_loader, writer=writer
257
+ )
258
+ }
259
+ )
260
+
261
+ output_writer = ds.create_output_writer(
262
+ "depth", tmp_path / "out", metadata_scope="depth"
263
+ )
264
+ ds.write_sample(0, {"depth": "prediction"}, output_writer)
265
+ output_writer.save_index()
266
+
267
+ scoped_index = tmp_path / "out" / ".ds_crawler" / "depth" / "output.json"
268
+ manifest_path = tmp_path / "out" / ".ds_crawler" / "scopes.json"
269
+ assert json.loads(scoped_index.read_text())["type"] == "txt"
270
+ manifest = json.loads(manifest_path.read_text())
271
+ assert manifest["scopes"]["depth"]["metadata_dir"] == ".ds_crawler/depth"
272
+ assert "output.json" in manifest["scopes"]["depth"]["files"]
273
+
274
+ def test_zip_output_writer_writes_scoped_metadata_and_manifest(self, tmp_path):
275
+ def writer(path: str, value: Any, meta: dict[str, Any] | None = None) -> None:
276
+ with open(path, "w", encoding="utf-8") as f:
277
+ f.write(str(value))
278
+
279
+ index = _flat_index("txt", ["f001"])
280
+ with patch(
281
+ "euler_loading.dataset.index_dataset_from_path",
282
+ return_value=index,
283
+ ):
284
+ ds = MultiModalDataset(
285
+ modalities={
286
+ "depth": Modality(
287
+ "/data/depth", loader=dummy_loader, writer=writer
288
+ )
289
+ }
290
+ )
291
+
292
+ output_writer = ds.create_output_writer(
293
+ "depth", tmp_path / "out.zip", zip=True, metadata_scope="depth"
294
+ )
295
+ ds.write_sample(0, {"depth": "prediction"}, output_writer)
296
+ output_writer.save_index()
297
+
298
+ with zipfile.ZipFile(tmp_path / "out.zip") as zf:
299
+ scoped_index = json.loads(
300
+ zf.read(".ds_crawler/depth/output.json").decode("utf-8")
301
+ )
302
+ manifest = json.loads(
303
+ zf.read(".ds_crawler/scopes.json").decode("utf-8")
304
+ )
305
+ assert scoped_index["type"] == "txt"
306
+ assert manifest["scopes"]["depth"]["metadata_dir"] == ".ds_crawler/depth"
307
+
243
308
  def test_write_sample_records_attributes_on_dataset_writer_entry(self, tmp_path):
244
309
  """write_sample(attributes={modality: {...}}) lands on the output entry."""
245
310
  def writer(path: str, value: Any, meta: dict[str, Any] | None = None) -> None: