euler-loading 2.18.0__tar.gz → 2.19.0__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 (58) hide show
  1. {euler_loading-2.18.0 → euler_loading-2.19.0}/PKG-INFO +1 -1
  2. {euler_loading-2.18.0 → euler_loading-2.19.0}/README.md +18 -0
  3. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/_writing.py +20 -4
  4. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/dataset.py +11 -1
  5. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/generic.py +62 -0
  6. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/generic.py +62 -0
  7. {euler_loading-2.18.0 → euler_loading-2.19.0}/package-lock.json +1 -1
  8. {euler_loading-2.18.0 → euler_loading-2.19.0}/pyproject.toml +1 -1
  9. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_loaders.py +41 -0
  10. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_writing.py +65 -0
  11. {euler_loading-2.18.0 → euler_loading-2.19.0}/.github/workflows/workflow.yml +0 -0
  12. {euler_loading-2.18.0 → euler_loading-2.19.0}/.gitignore +0 -0
  13. {euler_loading-2.18.0 → euler_loading-2.19.0}/docs/loader-attributes.md +0 -0
  14. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/__init__.py +0 -0
  15. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/_dataset_contract.py +0 -0
  16. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/_ds_crawler_utils.py +0 -0
  17. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/_metadata.py +0 -0
  18. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/_resolution.py +0 -0
  19. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/indexing.py +0 -0
  20. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/__init__.py +0 -0
  21. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/_annotations.py +0 -0
  22. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/_princeton_dense.py +0 -0
  23. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/_writer_utils.py +0 -0
  24. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/contracts.py +0 -0
  25. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/__init__.py +0 -0
  26. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  27. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/muses.py +0 -0
  28. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/princeton_dense.py +0 -0
  29. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  30. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  31. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/generate/__init__.py +0 -0
  32. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/generate/__main__.py +0 -0
  33. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/generate/loaders.json +0 -0
  34. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/generic.py +0 -0
  35. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/__init__.py +0 -0
  36. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  37. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/muses.py +0 -0
  38. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/princeton_dense.py +0 -0
  39. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  40. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  41. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/muses.py +0 -0
  42. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/princeton_dense.py +0 -0
  43. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  44. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/loaders/vkitti2.py +0 -0
  45. {euler_loading-2.18.0 → euler_loading-2.19.0}/euler_loading/preprocessing.py +0 -0
  46. {euler_loading-2.18.0 → euler_loading-2.19.0}/example.py +0 -0
  47. {euler_loading-2.18.0 → euler_loading-2.19.0}/gen_loaders.sh +0 -0
  48. {euler_loading-2.18.0 → euler_loading-2.19.0}/sample_rds.py +0 -0
  49. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/__init__.py +0 -0
  50. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/conftest.py +0 -0
  51. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/example_rds_calib.json +0 -0
  52. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_dataset.py +0 -0
  53. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_id_schema.py +0 -0
  54. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_indexing.py +0 -0
  55. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_preprocessing.py +0 -0
  56. {euler_loading-2.18.0 → euler_loading-2.19.0}/tests/test_real_dataset.py +0 -0
  57. {euler_loading-2.18.0 → euler_loading-2.19.0}/vkitti_cpu_example_output.json +0 -0
  58. {euler_loading-2.18.0 → euler_loading-2.19.0}/vkitti_gpu_example_output.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-loading
3
- Version: 2.18.0
3
+ Version: 2.19.0
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
@@ -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.
@@ -28,6 +28,8 @@ OutputDestination = str | os.PathLike[str] | DatasetWriter | ZipDatasetWriter
28
28
 
29
29
  def _wrap_writer_save_index(
30
30
  writer: DatasetWriter | ZipDatasetWriter,
31
+ *,
32
+ metadata_scope: str | None = None,
31
33
  ) -> DatasetWriter | ZipDatasetWriter:
32
34
  original_save_index = writer.save_index
33
35
 
@@ -72,7 +74,12 @@ def _wrap_writer_save_index(
72
74
  path = original_save_index(filename=filename)
73
75
 
74
76
  if isinstance(writer, ZipDatasetWriter):
75
- entry_name = f".ds_crawler/{filename}"
77
+ metadata_prefix = (
78
+ f".ds_crawler/{metadata_scope}"
79
+ if metadata_scope is not None
80
+ else ".ds_crawler"
81
+ )
82
+ entry_name = f"{metadata_prefix}/{filename}"
76
83
  archive_path = Path(writer.root)
77
84
  with zipfile.ZipFile(archive_path, "r") as source_zip:
78
85
  output_index = json.loads(source_zip.read(entry_name).decode("utf-8"))
@@ -113,8 +120,15 @@ def create_dataset_writer_from_index(
113
120
  index_output: Mapping[str, Any],
114
121
  root: str | os.PathLike[str],
115
122
  zip: bool = False,
123
+ metadata_scope: str | None = None,
116
124
  ) -> DatasetWriter | ZipDatasetWriter:
117
- """Create a ds-crawler writer that mirrors an existing index's metadata."""
125
+ """Create a ds-crawler writer that mirrors an existing index's metadata.
126
+
127
+ ``metadata_scope`` stores the generated artifacts below
128
+ ``.ds_crawler/<metadata_scope>/`` and lets ds-crawler maintain the shared
129
+ ``.ds_crawler/scopes.json`` manifest. This is useful when several logical
130
+ modalities share one physical output root or archive.
131
+ """
118
132
  indexing = index_output.get("indexing")
119
133
  hierarchy = indexing.get("hierarchy") if isinstance(indexing, Mapping) else None
120
134
  id_cfg = indexing.get("id") if isinstance(indexing, Mapping) else None
@@ -133,8 +147,9 @@ def create_dataset_writer_from_index(
133
147
  root,
134
148
  head=contract.to_mapping(),
135
149
  separator=separator,
150
+ metadata_scope=metadata_scope,
136
151
  )
137
- return _wrap_writer_save_index(writer)
152
+ return _wrap_writer_save_index(writer, metadata_scope=metadata_scope)
138
153
 
139
154
  name = as_non_empty_str(index_output.get("name"))
140
155
  type_name = as_non_empty_str(index_output.get("type"))
@@ -160,9 +175,10 @@ def create_dataset_writer_from_index(
160
175
  type=type_name,
161
176
  euler_train=dict(euler_train) if isinstance(euler_train, Mapping) else None,
162
177
  separator=separator,
178
+ metadata_scope=metadata_scope,
163
179
  **properties,
164
180
  )
165
- return _wrap_writer_save_index(writer)
181
+ return _wrap_writer_save_index(writer, metadata_scope=metadata_scope)
166
182
 
167
183
 
168
184
  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,6 +1,6 @@
1
1
  [project]
2
2
  name = "euler-loading"
3
- version = "2.18.0"
3
+ version = "2.19.0"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  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
  # ---------------------------------------------------------------------------
@@ -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: