euler-loading 2.8.0__tar.gz → 2.9.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 (53) hide show
  1. {euler_loading-2.8.0 → euler_loading-2.9.0}/PKG-INFO +1 -1
  2. {euler_loading-2.8.0 → euler_loading-2.9.0}/README.md +14 -2
  3. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/_resolution.py +1 -0
  4. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/__init__.py +4 -0
  5. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/cpu/__init__.py +2 -0
  6. euler_loading-2.9.0/euler_loading/loaders/cpu/muses.py +180 -0
  7. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/generate/loaders.json +169 -0
  8. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/gpu/__init__.py +2 -0
  9. euler_loading-2.9.0/euler_loading/loaders/gpu/muses.py +186 -0
  10. euler_loading-2.9.0/euler_loading/loaders/muses.py +12 -0
  11. {euler_loading-2.8.0 → euler_loading-2.9.0}/package-lock.json +1 -1
  12. {euler_loading-2.8.0 → euler_loading-2.9.0}/pyproject.toml +1 -1
  13. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_loaders.py +229 -0
  14. {euler_loading-2.8.0 → euler_loading-2.9.0}/.github/workflows/workflow.yml +0 -0
  15. {euler_loading-2.8.0 → euler_loading-2.9.0}/.gitignore +0 -0
  16. {euler_loading-2.8.0 → euler_loading-2.9.0}/docs/loader-attributes.md +0 -0
  17. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/__init__.py +0 -0
  18. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/_dataset_contract.py +0 -0
  19. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/_ds_crawler_utils.py +0 -0
  20. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/_metadata.py +0 -0
  21. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/_writing.py +0 -0
  22. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/dataset.py +0 -0
  23. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/indexing.py +0 -0
  24. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/_annotations.py +0 -0
  25. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/_writer_utils.py +0 -0
  26. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/contracts.py +0 -0
  27. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/cpu/generic.py +0 -0
  28. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  29. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  30. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  31. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/generate/__init__.py +0 -0
  32. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/generate/__main__.py +0 -0
  33. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/generic.py +0 -0
  34. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/gpu/generic.py +0 -0
  35. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  36. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  37. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  38. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  39. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/loaders/vkitti2.py +0 -0
  40. {euler_loading-2.8.0 → euler_loading-2.9.0}/euler_loading/preprocessing.py +0 -0
  41. {euler_loading-2.8.0 → euler_loading-2.9.0}/example.py +0 -0
  42. {euler_loading-2.8.0 → euler_loading-2.9.0}/sample_rds.py +0 -0
  43. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/__init__.py +0 -0
  44. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/conftest.py +0 -0
  45. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/example_rds_calib.json +0 -0
  46. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_dataset.py +0 -0
  47. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_id_schema.py +0 -0
  48. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_indexing.py +0 -0
  49. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_preprocessing.py +0 -0
  50. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_real_dataset.py +0 -0
  51. {euler_loading-2.8.0 → euler_loading-2.9.0}/tests/test_writing.py +0 -0
  52. {euler_loading-2.8.0 → euler_loading-2.9.0}/vkitti_cpu_example_output.json +0 -0
  53. {euler_loading-2.8.0 → euler_loading-2.9.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.8.0
3
+ Version: 2.9.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
@@ -309,7 +309,7 @@ When `Modality.loader` is `None`, euler-loading resolves the loader from the ds-
309
309
  }
310
310
  ```
311
311
 
312
- `loader` is the module name (`vkitti2`, `real_drive_sim`, or `generic_dense_depth`) and `function` is the function within that module. The GPU variant is used by default.
312
+ `loader` is the module name (`vkitti2`, `real_drive_sim`, `muses`, or `generic_dense_depth`) and `function` is the function within that module. The GPU variant is used by default.
313
313
 
314
314
  Writer resolution uses the same module and function metadata:
315
315
 
@@ -439,6 +439,18 @@ All built-in loaders accept both filesystem paths (`str`) and in-memory buffers
439
439
  | `sky_mask` | Binary mask where class ID == 29 (sky) |
440
440
  | `calibration` | Per-sensor calibration from JSON: returns `dict[sensor_name, {"K": (3,3), "T": (4,4), "distortion": (8,)}]` (use with `hierarchical_modalities`) |
441
441
 
442
+ ### MUSES (`euler_loading.loaders.muses`)
443
+
444
+ | Function | Description |
445
+ |----------|-------------|
446
+ | `rgb` | Frame-camera RGB image as float32, normalised to [0, 1] |
447
+ | `reference_rgb` | Clear-weather reference RGB image as float32, normalised to [0, 1] |
448
+ | `semantic_segmentation` | Single-channel Cityscapes `labelIds` or `labelTrainIds` PNG as class IDs |
449
+ | `semantic_segmentation_color` | Cityscapes `labelColor` PNG as RGB uint8 labels |
450
+ | `panoptic_segmentation` | COCO-style RGB panoptic PNG decoded to integer segment IDs |
451
+ | `lidar_point_cloud` / `point_cloud` | Lidar `.bin` file as `(N, 6)` float64: `x, y, z, intensity, ring, timestamp` |
452
+ | `sparse_depth` | Alias for MUSES lidar points for sparse-depth style workflows |
453
+
442
454
  ### Generic Dense Depth (`euler_loading.loaders.gpu.generic_dense_depth`)
443
455
 
444
456
  A format-agnostic loader that infers the loading strategy from the file extension. Useful for datasets that don't have a dedicated loader module.
@@ -450,7 +462,7 @@ A format-agnostic loader that infers the loading strategy from the file extensio
450
462
  | `sky_mask` | Binary mask by comparing pixels against `meta["sky_mask"]` (`[R, G, B]`). Requires `meta` |
451
463
  | `read_intrinsics` | Returns `meta["intrinsics"]` as a `(3, 3)` tensor. Ignores path; requires `meta` |
452
464
 
453
- CPU variants of all loaders live under `euler_loading.loaders.cpu.{vkitti2,real_drive_sim,generic_dense_depth}`.
465
+ CPU variants of all loaders live under `euler_loading.loaders.cpu.{vkitti2,real_drive_sim,muses,generic_dense_depth}`.
454
466
 
455
467
  ### Flattening hierarchical modalities
456
468
 
@@ -23,6 +23,7 @@ _LOADER_MODULES: dict[str, str] = {
23
23
  "real_drive_sim": "euler_loading.loaders.gpu.real_drive_sim",
24
24
  "generic_dense_depth": "euler_loading.loaders.gpu.generic_dense_depth",
25
25
  "generic": "euler_loading.loaders.gpu.generic",
26
+ "muses": "euler_loading.loaders.gpu.muses",
26
27
  }
27
28
 
28
29
 
@@ -23,4 +23,8 @@ Available submodules:
23
23
 
24
24
  - :mod:`euler_loading.loaders.gpu.vkitti2` — Virtual KITTI 2 (torch)
25
25
  - :mod:`euler_loading.loaders.cpu.vkitti2` — Virtual KITTI 2 (numpy)
26
+ - :mod:`euler_loading.loaders.gpu.real_drive_sim` — Real Drive Sim (torch)
27
+ - :mod:`euler_loading.loaders.cpu.real_drive_sim` — Real Drive Sim (numpy)
28
+ - :mod:`euler_loading.loaders.gpu.muses` — MUSES (torch)
29
+ - :mod:`euler_loading.loaders.cpu.muses` — MUSES (numpy)
26
30
  """
@@ -8,4 +8,6 @@ requiring torch.
8
8
  Available submodules:
9
9
 
10
10
  - :mod:`euler_loading.loaders.cpu.vkitti2` — Virtual KITTI 2
11
+ - :mod:`euler_loading.loaders.cpu.real_drive_sim` — Real Drive Sim
12
+ - :mod:`euler_loading.loaders.cpu.muses` — MUSES
11
13
  """
@@ -0,0 +1,180 @@
1
+ """CPU-oriented loader functions for the MUSES dataset.
2
+
3
+ MUSES stores RGB camera frames, reference frames, semantic labels, panoptic
4
+ labels, and lidar point clouds in separate modality folders. These loaders
5
+ return NumPy arrays for CPU-based processing.
6
+
7
+ Return types
8
+ ------------
9
+ - **rgb** / **reference_rgb** -- ``np.ndarray`` of shape ``(H, W, 3)``
10
+ float32 in ``[0, 1]``.
11
+ - **semantic_segmentation** -- ``np.ndarray`` of shape ``(H, W)`` int64
12
+ containing raw Cityscapes ``labelIds`` or ``labelTrainIds``.
13
+ - **semantic_segmentation_color** -- ``np.ndarray`` of shape ``(H, W, 3)``
14
+ uint8 containing Cityscapes RGB colours.
15
+ - **panoptic_segmentation** -- ``np.ndarray`` of shape ``(H, W)`` int64
16
+ containing COCO-style panoptic segment IDs decoded from RGB.
17
+ - **lidar_point_cloud** / **sparse_depth** -- ``np.ndarray`` of shape
18
+ ``(N, 6)`` float64 with columns ``x, y, z, intensity, ring, timestamp``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ from typing import Any, BinaryIO, Union
25
+
26
+ import numpy as np
27
+ from PIL import Image
28
+
29
+ from euler_loading.loaders._annotations import modality_meta
30
+
31
+ _LIDAR_COLUMNS = ["x", "y", "z", "intensity", "ring", "timestamp"]
32
+
33
+
34
+ def _load_image_rgb(path: Union[str, BinaryIO]) -> np.ndarray:
35
+ """Load an image file as ``(H, W, 3)`` float32 in ``[0, 1]``."""
36
+ with Image.open(path) as image:
37
+ return np.array(image.convert("RGB"), dtype=np.float32) / 255.0
38
+
39
+
40
+ def _load_single_channel_labels(path: Union[str, BinaryIO]) -> np.ndarray:
41
+ """Load a single-channel semantic label image as int64."""
42
+ with Image.open(path) as image:
43
+ arr = np.array(image, dtype=np.int64)
44
+ if arr.ndim != 2:
45
+ raise ValueError(
46
+ "MUSES semantic_segmentation expects a single-channel labelIds "
47
+ "or labelTrainIds PNG. Use semantic_segmentation_color for "
48
+ "labelColor PNG files."
49
+ )
50
+ return arr
51
+
52
+
53
+ def _load_rgb_uint8(path: Union[str, BinaryIO]) -> np.ndarray:
54
+ """Load an RGB image without normalisation."""
55
+ with Image.open(path) as image:
56
+ return np.array(image.convert("RGB"), dtype=np.uint8)
57
+
58
+
59
+ def _decode_panoptic_rgb(rgb: np.ndarray) -> np.ndarray:
60
+ """Decode COCO-style RGB panoptic IDs as ``R + 256*G + 256^2*B``."""
61
+ values = rgb.astype(np.int64)
62
+ return values[:, :, 0] + 256 * values[:, :, 1] + 65536 * values[:, :, 2]
63
+
64
+
65
+ def _load_lidar_array(path: Union[str, BinaryIO]) -> np.ndarray:
66
+ """Load a MUSES lidar ``.bin`` file as an ``(N, 6)`` float64 array."""
67
+ if isinstance(path, (str, os.PathLike)):
68
+ arr = np.fromfile(path, dtype=np.float64)
69
+ else:
70
+ arr = np.frombuffer(path.read(), dtype=np.float64).copy()
71
+
72
+ if arr.size % 6 != 0:
73
+ name = getattr(path, "name", path)
74
+ raise ValueError(
75
+ f"MUSES lidar file {name!r} contains {arr.size} float64 values, "
76
+ "which is not divisible by 6."
77
+ )
78
+ return arr.reshape((-1, 6))
79
+
80
+
81
+ @modality_meta(
82
+ modality_type="rgb",
83
+ dtype="float32",
84
+ shape="HWC",
85
+ file_formats=[".png"],
86
+ output_range=[0.0, 1.0],
87
+ )
88
+ def rgb(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
89
+ """Load a MUSES frame-camera RGB image as float32 in ``[0, 1]``."""
90
+ return _load_image_rgb(path)
91
+
92
+
93
+ @modality_meta(
94
+ modality_type="rgb",
95
+ dtype="float32",
96
+ shape="HWC",
97
+ file_formats=[".png"],
98
+ output_range=[0.0, 1.0],
99
+ meta={"source": "reference_frames"},
100
+ )
101
+ def reference_rgb(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
102
+ """Load a MUSES clear-weather reference RGB image as float32 in ``[0, 1]``."""
103
+ return _load_image_rgb(path)
104
+
105
+
106
+ @modality_meta(
107
+ modality_type="semantic_segmentation",
108
+ dtype="int64",
109
+ shape="HW",
110
+ file_formats=[".png"],
111
+ meta={
112
+ "encoding": "single_channel",
113
+ "supported_suffixes": ["labelIds", "labelTrainIds"],
114
+ },
115
+ )
116
+ def semantic_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
117
+ """Load MUSES ``labelIds`` or ``labelTrainIds`` semantic labels."""
118
+ return _load_single_channel_labels(path)
119
+
120
+
121
+ @modality_meta(
122
+ modality_type="semantic_segmentation_color",
123
+ dtype="uint8",
124
+ shape="HWC",
125
+ file_formats=[".png"],
126
+ meta={"encoding": "rgb", "supported_suffixes": ["labelColor"]},
127
+ )
128
+ def semantic_segmentation_color(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
129
+ """Load MUSES ``labelColor`` semantic labels as RGB uint8 values."""
130
+ return _load_rgb_uint8(path)
131
+
132
+
133
+ @modality_meta(
134
+ modality_type="panoptic_segmentation",
135
+ dtype="int64",
136
+ shape="HW",
137
+ file_formats=[".png"],
138
+ meta={"encoding": "coco_rgb_id"},
139
+ )
140
+ def panoptic_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
141
+ """Load a MUSES panoptic PNG as an integer segment-ID map."""
142
+ return _decode_panoptic_rgb(_load_rgb_uint8(path))
143
+
144
+
145
+ @modality_meta(
146
+ modality_type="point_cloud",
147
+ dtype="float64",
148
+ shape="NC",
149
+ file_formats=[".bin"],
150
+ meta={
151
+ "columns": _LIDAR_COLUMNS,
152
+ "coordinate_unit": "meters",
153
+ "timestamp_unit": "seconds",
154
+ },
155
+ )
156
+ def lidar_point_cloud(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
157
+ """Load a MUSES lidar point cloud as ``(N, 6)`` float64."""
158
+ return _load_lidar_array(path)
159
+
160
+
161
+ def point_cloud(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
162
+ """Alias for :func:`lidar_point_cloud`."""
163
+ return _load_lidar_array(path)
164
+
165
+
166
+ @modality_meta(
167
+ modality_type="sparse_depth",
168
+ dtype="float64",
169
+ shape="NC",
170
+ file_formats=[".bin"],
171
+ meta={
172
+ "representation": "point_cloud",
173
+ "columns": _LIDAR_COLUMNS,
174
+ "coordinate_unit": "meters",
175
+ "timestamp_unit": "seconds",
176
+ },
177
+ )
178
+ def sparse_depth(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
179
+ """Load MUSES lidar points for sparse-depth style workflows."""
180
+ return _load_lidar_array(path)
@@ -219,6 +219,175 @@
219
219
  }
220
220
  ]
221
221
  },
222
+ {
223
+ "name": "muses",
224
+ "modalities": [
225
+ {
226
+ "type": "point_cloud",
227
+ "function": "lidar_point_cloud",
228
+ "hierarchical": false,
229
+ "dtype": "float64",
230
+ "access": "key",
231
+ "file_formats": [
232
+ ".bin"
233
+ ],
234
+ "meta": {
235
+ "columns": [
236
+ "x",
237
+ "y",
238
+ "z",
239
+ "intensity",
240
+ "ring",
241
+ "timestamp"
242
+ ],
243
+ "coordinate_unit": "meters",
244
+ "timestamp_unit": "seconds"
245
+ },
246
+ "cpu": {
247
+ "shape": "NC"
248
+ },
249
+ "gpu": {
250
+ "shape": "NC"
251
+ }
252
+ },
253
+ {
254
+ "type": "panoptic_segmentation",
255
+ "function": "panoptic_segmentation",
256
+ "hierarchical": false,
257
+ "dtype": "int64",
258
+ "access": "key",
259
+ "file_formats": [
260
+ ".png"
261
+ ],
262
+ "meta": {
263
+ "encoding": "coco_rgb_id"
264
+ },
265
+ "cpu": {
266
+ "shape": "HW"
267
+ },
268
+ "gpu": {
269
+ "shape": "1HW"
270
+ }
271
+ },
272
+ {
273
+ "type": "rgb",
274
+ "function": "reference_rgb",
275
+ "hierarchical": false,
276
+ "dtype": "float32",
277
+ "access": "key",
278
+ "output_range": [
279
+ 0.0,
280
+ 1.0
281
+ ],
282
+ "file_formats": [
283
+ ".png"
284
+ ],
285
+ "meta": {
286
+ "source": "reference_frames"
287
+ },
288
+ "cpu": {
289
+ "shape": "HWC"
290
+ },
291
+ "gpu": {
292
+ "shape": "CHW"
293
+ }
294
+ },
295
+ {
296
+ "type": "rgb",
297
+ "function": "rgb",
298
+ "hierarchical": false,
299
+ "dtype": "float32",
300
+ "access": "key",
301
+ "output_range": [
302
+ 0.0,
303
+ 1.0
304
+ ],
305
+ "file_formats": [
306
+ ".png"
307
+ ],
308
+ "cpu": {
309
+ "shape": "HWC"
310
+ },
311
+ "gpu": {
312
+ "shape": "CHW"
313
+ }
314
+ },
315
+ {
316
+ "type": "semantic_segmentation",
317
+ "function": "semantic_segmentation",
318
+ "hierarchical": false,
319
+ "dtype": "int64",
320
+ "access": "key",
321
+ "file_formats": [
322
+ ".png"
323
+ ],
324
+ "meta": {
325
+ "encoding": "single_channel",
326
+ "supported_suffixes": [
327
+ "labelIds",
328
+ "labelTrainIds"
329
+ ]
330
+ },
331
+ "cpu": {
332
+ "shape": "HW"
333
+ },
334
+ "gpu": {
335
+ "shape": "1HW"
336
+ }
337
+ },
338
+ {
339
+ "type": "semantic_segmentation_color",
340
+ "function": "semantic_segmentation_color",
341
+ "hierarchical": false,
342
+ "dtype": "uint8",
343
+ "access": "key",
344
+ "file_formats": [
345
+ ".png"
346
+ ],
347
+ "meta": {
348
+ "encoding": "rgb",
349
+ "supported_suffixes": [
350
+ "labelColor"
351
+ ]
352
+ },
353
+ "cpu": {
354
+ "shape": "HWC"
355
+ },
356
+ "gpu": {
357
+ "shape": "CHW"
358
+ }
359
+ },
360
+ {
361
+ "type": "sparse_depth",
362
+ "function": "sparse_depth",
363
+ "hierarchical": false,
364
+ "dtype": "float64",
365
+ "access": "key",
366
+ "file_formats": [
367
+ ".bin"
368
+ ],
369
+ "meta": {
370
+ "representation": "point_cloud",
371
+ "columns": [
372
+ "x",
373
+ "y",
374
+ "z",
375
+ "intensity",
376
+ "ring",
377
+ "timestamp"
378
+ ],
379
+ "coordinate_unit": "meters",
380
+ "timestamp_unit": "seconds"
381
+ },
382
+ "cpu": {
383
+ "shape": "NC"
384
+ },
385
+ "gpu": {
386
+ "shape": "NC"
387
+ }
388
+ }
389
+ ]
390
+ },
222
391
  {
223
392
  "name": "real_drive_sim",
224
393
  "modalities": [
@@ -7,4 +7,6 @@ All loaders return **torch tensors** suitable for direct GPU-based training.
7
7
  Available submodules:
8
8
 
9
9
  - :mod:`euler_loading.loaders.gpu.vkitti2` — Virtual KITTI 2
10
+ - :mod:`euler_loading.loaders.gpu.real_drive_sim` — Real Drive Sim
11
+ - :mod:`euler_loading.loaders.gpu.muses` — MUSES
10
12
  """
@@ -0,0 +1,186 @@
1
+ """GPU-oriented loader functions for the MUSES dataset.
2
+
3
+ MUSES stores RGB camera frames, reference frames, semantic labels, panoptic
4
+ labels, and lidar point clouds in separate modality folders. These loaders
5
+ return PyTorch tensors for GPU-oriented training pipelines.
6
+
7
+ Return types
8
+ ------------
9
+ - **rgb** / **reference_rgb** -- ``torch.FloatTensor`` of shape
10
+ ``(3, H, W)`` in ``[0, 1]``.
11
+ - **semantic_segmentation** -- ``torch.LongTensor`` of shape ``(1, H, W)``
12
+ containing raw Cityscapes ``labelIds`` or ``labelTrainIds``.
13
+ - **semantic_segmentation_color** -- ``torch.ByteTensor`` of shape
14
+ ``(3, H, W)`` containing Cityscapes RGB colours.
15
+ - **panoptic_segmentation** -- ``torch.LongTensor`` of shape ``(1, H, W)``
16
+ containing COCO-style panoptic segment IDs decoded from RGB.
17
+ - **lidar_point_cloud** / **sparse_depth** -- ``torch.DoubleTensor`` of shape
18
+ ``(N, 6)`` with columns ``x, y, z, intensity, ring, timestamp``.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ from typing import Any, BinaryIO, Union
25
+
26
+ import numpy as np
27
+ import torch
28
+ from PIL import Image
29
+
30
+ from euler_loading.loaders._annotations import modality_meta
31
+
32
+ _LIDAR_COLUMNS = ["x", "y", "z", "intensity", "ring", "timestamp"]
33
+
34
+
35
+ def _load_image_rgb(path: Union[str, BinaryIO]) -> np.ndarray:
36
+ """Load an image file as ``(H, W, 3)`` float32 in ``[0, 1]``."""
37
+ with Image.open(path) as image:
38
+ return np.array(image.convert("RGB"), dtype=np.float32) / 255.0
39
+
40
+
41
+ def _load_single_channel_labels(path: Union[str, BinaryIO]) -> np.ndarray:
42
+ """Load a single-channel semantic label image as int64."""
43
+ with Image.open(path) as image:
44
+ arr = np.array(image, dtype=np.int64)
45
+ if arr.ndim != 2:
46
+ raise ValueError(
47
+ "MUSES semantic_segmentation expects a single-channel labelIds "
48
+ "or labelTrainIds PNG. Use semantic_segmentation_color for "
49
+ "labelColor PNG files."
50
+ )
51
+ return arr
52
+
53
+
54
+ def _load_rgb_uint8(path: Union[str, BinaryIO]) -> np.ndarray:
55
+ """Load an RGB image without normalisation."""
56
+ with Image.open(path) as image:
57
+ return np.array(image.convert("RGB"), dtype=np.uint8)
58
+
59
+
60
+ def _decode_panoptic_rgb(rgb: np.ndarray) -> np.ndarray:
61
+ """Decode COCO-style RGB panoptic IDs as ``R + 256*G + 256^2*B``."""
62
+ values = rgb.astype(np.int64)
63
+ return values[:, :, 0] + 256 * values[:, :, 1] + 65536 * values[:, :, 2]
64
+
65
+
66
+ def _load_lidar_array(path: Union[str, BinaryIO]) -> np.ndarray:
67
+ """Load a MUSES lidar ``.bin`` file as an ``(N, 6)`` float64 array."""
68
+ if isinstance(path, (str, os.PathLike)):
69
+ arr = np.fromfile(path, dtype=np.float64)
70
+ else:
71
+ arr = np.frombuffer(path.read(), dtype=np.float64).copy()
72
+
73
+ if arr.size % 6 != 0:
74
+ name = getattr(path, "name", path)
75
+ raise ValueError(
76
+ f"MUSES lidar file {name!r} contains {arr.size} float64 values, "
77
+ "which is not divisible by 6."
78
+ )
79
+ return arr.reshape((-1, 6))
80
+
81
+
82
+ @modality_meta(
83
+ modality_type="rgb",
84
+ dtype="float32",
85
+ shape="CHW",
86
+ file_formats=[".png"],
87
+ output_range=[0.0, 1.0],
88
+ )
89
+ def rgb(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
90
+ """Load a MUSES frame-camera RGB image as a float32 tensor in ``[0, 1]``."""
91
+ arr = _load_image_rgb(path)
92
+ return torch.from_numpy(arr).permute(2, 0, 1).contiguous()
93
+
94
+
95
+ @modality_meta(
96
+ modality_type="rgb",
97
+ dtype="float32",
98
+ shape="CHW",
99
+ file_formats=[".png"],
100
+ output_range=[0.0, 1.0],
101
+ meta={"source": "reference_frames"},
102
+ )
103
+ def reference_rgb(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
104
+ """Load a MUSES clear-weather reference RGB image as float32 in ``[0, 1]``."""
105
+ arr = _load_image_rgb(path)
106
+ return torch.from_numpy(arr).permute(2, 0, 1).contiguous()
107
+
108
+
109
+ @modality_meta(
110
+ modality_type="semantic_segmentation",
111
+ dtype="int64",
112
+ shape="1HW",
113
+ file_formats=[".png"],
114
+ meta={
115
+ "encoding": "single_channel",
116
+ "supported_suffixes": ["labelIds", "labelTrainIds"],
117
+ },
118
+ )
119
+ def semantic_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
120
+ """Load MUSES ``labelIds`` or ``labelTrainIds`` semantic labels."""
121
+ arr = _load_single_channel_labels(path)
122
+ return torch.from_numpy(arr).unsqueeze(0).contiguous()
123
+
124
+
125
+ @modality_meta(
126
+ modality_type="semantic_segmentation_color",
127
+ dtype="uint8",
128
+ shape="CHW",
129
+ file_formats=[".png"],
130
+ meta={"encoding": "rgb", "supported_suffixes": ["labelColor"]},
131
+ )
132
+ def semantic_segmentation_color(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
133
+ """Load MUSES ``labelColor`` semantic labels as RGB uint8 values."""
134
+ arr = _load_rgb_uint8(path)
135
+ return torch.from_numpy(arr).permute(2, 0, 1).contiguous()
136
+
137
+
138
+ @modality_meta(
139
+ modality_type="panoptic_segmentation",
140
+ dtype="int64",
141
+ shape="1HW",
142
+ file_formats=[".png"],
143
+ meta={"encoding": "coco_rgb_id"},
144
+ )
145
+ def panoptic_segmentation(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
146
+ """Load a MUSES panoptic PNG as an integer segment-ID map."""
147
+ arr = _decode_panoptic_rgb(_load_rgb_uint8(path))
148
+ return torch.from_numpy(arr).unsqueeze(0).contiguous()
149
+
150
+
151
+ @modality_meta(
152
+ modality_type="point_cloud",
153
+ dtype="float64",
154
+ shape="NC",
155
+ file_formats=[".bin"],
156
+ meta={
157
+ "columns": _LIDAR_COLUMNS,
158
+ "coordinate_unit": "meters",
159
+ "timestamp_unit": "seconds",
160
+ },
161
+ )
162
+ def lidar_point_cloud(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
163
+ """Load a MUSES lidar point cloud as ``(N, 6)`` float64."""
164
+ return torch.from_numpy(_load_lidar_array(path)).contiguous()
165
+
166
+
167
+ def point_cloud(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
168
+ """Alias for :func:`lidar_point_cloud`."""
169
+ return torch.from_numpy(_load_lidar_array(path)).contiguous()
170
+
171
+
172
+ @modality_meta(
173
+ modality_type="sparse_depth",
174
+ dtype="float64",
175
+ shape="NC",
176
+ file_formats=[".bin"],
177
+ meta={
178
+ "representation": "point_cloud",
179
+ "columns": _LIDAR_COLUMNS,
180
+ "coordinate_unit": "meters",
181
+ "timestamp_unit": "seconds",
182
+ },
183
+ )
184
+ def sparse_depth(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> torch.Tensor:
185
+ """Load MUSES lidar points for sparse-depth style workflows."""
186
+ return torch.from_numpy(_load_lidar_array(path)).contiguous()
@@ -0,0 +1,12 @@
1
+ """Loader functions for the MUSES dataset.
2
+
3
+ This module re-exports the GPU-oriented loaders from
4
+ :mod:`euler_loading.loaders.gpu.muses` for convenience. For explicit
5
+ control, import from :mod:`~euler_loading.loaders.gpu` or
6
+ :mod:`~euler_loading.loaders.cpu` directly::
7
+
8
+ from euler_loading.loaders.gpu import muses # torch tensors (CHW)
9
+ from euler_loading.loaders.cpu import muses # numpy arrays (HWC)
10
+ """
11
+
12
+ from euler_loading.loaders.gpu.muses import * # noqa: F401,F403
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "euler-lo",
2
+ "name": "euler-load-muses",
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.8.0"
3
+ version = "2.9.0"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  dependencies = [
@@ -2,22 +2,27 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import io
5
6
  from pathlib import Path
6
7
 
7
8
  import numpy as np
8
9
  import pytest
9
10
  import torch
11
+ from PIL import Image
10
12
 
11
13
  from euler_loading.loaders import vkitti2
12
14
  from euler_loading.loaders import generic as generic_top
15
+ from euler_loading.loaders import muses as muses_top
13
16
  from euler_loading.loaders.gpu import vkitti2 as gpu_vkitti2
14
17
  from euler_loading.loaders.gpu import real_drive_sim as gpu_rds
15
18
  from euler_loading.loaders.gpu import generic as gpu_generic
16
19
  from euler_loading.loaders.gpu import generic_dense_depth as gpu_generic_dense_depth
20
+ from euler_loading.loaders.gpu import muses as gpu_muses
17
21
  from euler_loading.loaders.cpu import vkitti2 as cpu_vkitti2
18
22
  from euler_loading.loaders.cpu import real_drive_sim as cpu_rds
19
23
  from euler_loading.loaders.cpu import generic as cpu_generic
20
24
  from euler_loading.loaders.cpu import generic_dense_depth as cpu_generic_dense_depth
25
+ from euler_loading.loaders.cpu import muses as cpu_muses
21
26
 
22
27
  # ---------------------------------------------------------------------------
23
28
  # Shared fixtures
@@ -143,6 +148,230 @@ class TestGenericDenseDepthAttributes:
143
148
  np.testing.assert_allclose(result, expected)
144
149
 
145
150
 
151
+ # ---------------------------------------------------------------------------
152
+ # MUSES loader smoke tests
153
+ # ---------------------------------------------------------------------------
154
+
155
+ MUSES_LOADER_NAMES = [
156
+ "rgb",
157
+ "reference_rgb",
158
+ "semantic_segmentation",
159
+ "semantic_segmentation_color",
160
+ "panoptic_segmentation",
161
+ "lidar_point_cloud",
162
+ "point_cloud",
163
+ "sparse_depth",
164
+ ]
165
+
166
+
167
+ @pytest.fixture()
168
+ def muses_rgb_path(tmp_path):
169
+ arr = np.array(
170
+ [
171
+ [[0, 64, 128], [255, 128, 0]],
172
+ [[32, 16, 8], [4, 2, 1]],
173
+ ],
174
+ dtype=np.uint8,
175
+ )
176
+ path = tmp_path / "REC0001_frame_000001_frame_camera.png"
177
+ Image.fromarray(arr, mode="RGB").save(path)
178
+ return str(path), arr
179
+
180
+
181
+ @pytest.fixture()
182
+ def muses_semantic_path(tmp_path):
183
+ arr = np.array([[0, 7], [24, 255]], dtype=np.uint8)
184
+ path = tmp_path / "REC0001_frame_000001_gt_labelTrainIds.png"
185
+ Image.fromarray(arr, mode="L").save(path)
186
+ return str(path), arr
187
+
188
+
189
+ @pytest.fixture()
190
+ def muses_semantic_color_path(tmp_path):
191
+ arr = np.array(
192
+ [
193
+ [[128, 64, 128], [70, 70, 70]],
194
+ [[220, 20, 60], [0, 0, 142]],
195
+ ],
196
+ dtype=np.uint8,
197
+ )
198
+ path = tmp_path / "REC0001_frame_000001_gt_labelColor.png"
199
+ Image.fromarray(arr, mode="RGB").save(path)
200
+ return str(path), arr
201
+
202
+
203
+ @pytest.fixture()
204
+ def muses_panoptic_path(tmp_path):
205
+ arr = np.array(
206
+ [
207
+ [[1, 0, 0], [2, 1, 0]],
208
+ [[3, 2, 1], [255, 255, 255]],
209
+ ],
210
+ dtype=np.uint8,
211
+ )
212
+ path = tmp_path / "REC0001_frame_000001_gt_panoptic.png"
213
+ Image.fromarray(arr, mode="RGB").save(path)
214
+ expected = (
215
+ arr[:, :, 0].astype(np.int64)
216
+ + 256 * arr[:, :, 1].astype(np.int64)
217
+ + 65536 * arr[:, :, 2].astype(np.int64)
218
+ )
219
+ return str(path), expected
220
+
221
+
222
+ @pytest.fixture()
223
+ def muses_lidar_path(tmp_path):
224
+ arr = np.array(
225
+ [
226
+ [1.0, 2.0, 3.0, 19.0, 1.0, 1671182560.125],
227
+ [4.0, 5.0, 6.0, 27.0, 5.0, 1671182560.250],
228
+ ],
229
+ dtype=np.float64,
230
+ )
231
+ path = tmp_path / "REC0001_frame_000001_lidar.bin"
232
+ arr.tofile(path)
233
+ return str(path), arr
234
+
235
+
236
+ class TestMUSESModuleContents:
237
+ """The muses module exposes the expected loader functions."""
238
+
239
+ @pytest.mark.parametrize("name", MUSES_LOADER_NAMES)
240
+ def test_top_level_has_callable(self, name):
241
+ assert callable(getattr(muses_top, name))
242
+
243
+ @pytest.mark.parametrize("name", MUSES_LOADER_NAMES)
244
+ def test_gpu_has_callable(self, name):
245
+ assert callable(getattr(gpu_muses, name))
246
+
247
+ @pytest.mark.parametrize("name", MUSES_LOADER_NAMES)
248
+ def test_cpu_has_callable(self, name):
249
+ assert callable(getattr(cpu_muses, name))
250
+
251
+ def test_reference_rgb_is_marked_as_rgb_modality(self):
252
+ assert gpu_muses.reference_rgb._modality_meta["type"] == "rgb"
253
+ assert cpu_muses.reference_rgb._modality_meta["type"] == "rgb"
254
+
255
+
256
+ class TestMUSESGPULoaders:
257
+ """GPU MUSES loaders produce torch tensors from minimal on-disk data."""
258
+
259
+ def test_rgb_shape_dtype_and_range(self, muses_rgb_path):
260
+ path, _ = muses_rgb_path
261
+ result = gpu_muses.rgb(path)
262
+ assert isinstance(result, torch.Tensor)
263
+ assert result.dtype == torch.float32
264
+ assert result.shape == (3, 2, 2)
265
+ assert result.min() >= 0.0
266
+ assert result.max() <= 1.0
267
+
268
+ def test_reference_rgb_matches_rgb_loader(self, muses_rgb_path):
269
+ path, _ = muses_rgb_path
270
+ assert torch.equal(gpu_muses.reference_rgb(path), gpu_muses.rgb(path))
271
+
272
+ def test_semantic_segmentation_is_single_channel_long(self, muses_semantic_path):
273
+ path, expected = muses_semantic_path
274
+ result = gpu_muses.semantic_segmentation(path)
275
+ assert result.dtype == torch.int64
276
+ assert result.shape == (1, 2, 2)
277
+ assert torch.equal(result, torch.from_numpy(expected.astype(np.int64)).unsqueeze(0))
278
+
279
+ def test_semantic_color_is_chw_uint8(self, muses_semantic_color_path):
280
+ path, expected = muses_semantic_color_path
281
+ result = gpu_muses.semantic_segmentation_color(path)
282
+ assert result.dtype == torch.uint8
283
+ assert result.shape == (3, 2, 2)
284
+ assert torch.equal(result, torch.from_numpy(expected).permute(2, 0, 1))
285
+
286
+ def test_panoptic_segmentation_decodes_rgb_ids(self, muses_panoptic_path):
287
+ path, expected = muses_panoptic_path
288
+ result = gpu_muses.panoptic_segmentation(path)
289
+ assert result.dtype == torch.int64
290
+ assert result.shape == (1, 2, 2)
291
+ assert torch.equal(result, torch.from_numpy(expected).unsqueeze(0))
292
+
293
+ def test_lidar_point_cloud_preserves_float64_columns(self, muses_lidar_path):
294
+ path, expected = muses_lidar_path
295
+ result = gpu_muses.lidar_point_cloud(path)
296
+ assert result.dtype == torch.float64
297
+ assert result.shape == (2, 6)
298
+ assert torch.equal(result, torch.from_numpy(expected))
299
+
300
+ def test_point_cloud_alias_matches_lidar_point_cloud(self, muses_lidar_path):
301
+ path, _ = muses_lidar_path
302
+ assert torch.equal(gpu_muses.point_cloud(path), gpu_muses.lidar_point_cloud(path))
303
+
304
+ def test_sparse_depth_matches_lidar_point_cloud(self, muses_lidar_path):
305
+ path, _ = muses_lidar_path
306
+ assert torch.equal(gpu_muses.sparse_depth(path), gpu_muses.lidar_point_cloud(path))
307
+
308
+
309
+ class TestMUSESCPULoaders:
310
+ """CPU MUSES loaders produce numpy arrays from minimal on-disk data."""
311
+
312
+ def test_rgb_shape_dtype_and_range(self, muses_rgb_path):
313
+ path, _ = muses_rgb_path
314
+ result = cpu_muses.rgb(path)
315
+ assert isinstance(result, np.ndarray)
316
+ assert result.dtype == np.float32
317
+ assert result.shape == (2, 2, 3)
318
+ assert result.min() >= 0.0
319
+ assert result.max() <= 1.0
320
+
321
+ def test_semantic_segmentation_is_hw_int64(self, muses_semantic_path):
322
+ path, expected = muses_semantic_path
323
+ result = cpu_muses.semantic_segmentation(path)
324
+ assert result.dtype == np.int64
325
+ assert result.shape == (2, 2)
326
+ assert np.array_equal(result, expected.astype(np.int64))
327
+
328
+ def test_semantic_color_is_hwc_uint8(self, muses_semantic_color_path):
329
+ path, expected = muses_semantic_color_path
330
+ result = cpu_muses.semantic_segmentation_color(path)
331
+ assert result.dtype == np.uint8
332
+ assert result.shape == (2, 2, 3)
333
+ assert np.array_equal(result, expected)
334
+
335
+ def test_panoptic_segmentation_decodes_rgb_ids(self, muses_panoptic_path):
336
+ path, expected = muses_panoptic_path
337
+ result = cpu_muses.panoptic_segmentation(path)
338
+ assert result.dtype == np.int64
339
+ assert result.shape == (2, 2)
340
+ assert np.array_equal(result, expected)
341
+
342
+ def test_lidar_point_cloud_preserves_float64_columns(self, muses_lidar_path):
343
+ path, expected = muses_lidar_path
344
+ result = cpu_muses.lidar_point_cloud(path)
345
+ assert result.dtype == np.float64
346
+ assert result.shape == (2, 6)
347
+ assert np.array_equal(result, expected)
348
+
349
+ def test_point_cloud_alias_matches_lidar_point_cloud(self, muses_lidar_path):
350
+ path, _ = muses_lidar_path
351
+ assert np.array_equal(cpu_muses.point_cloud(path), cpu_muses.lidar_point_cloud(path))
352
+
353
+ def test_lidar_point_cloud_supports_binary_streams(self, muses_lidar_path):
354
+ _, expected = muses_lidar_path
355
+ stream = io.BytesIO(expected.tobytes())
356
+ result = cpu_muses.lidar_point_cloud(stream)
357
+ assert np.array_equal(result, expected)
358
+
359
+
360
+ class TestMUSESBackwardCompat:
361
+ """``from euler_loading.loaders import muses`` returns GPU loaders."""
362
+
363
+ def test_top_level_rgb_matches_gpu(self, muses_rgb_path):
364
+ path, _ = muses_rgb_path
365
+ assert torch.equal(muses_top.rgb(path), gpu_muses.rgb(path))
366
+
367
+ def test_top_level_panoptic_matches_gpu(self, muses_panoptic_path):
368
+ path, _ = muses_panoptic_path
369
+ assert torch.equal(
370
+ muses_top.panoptic_segmentation(path),
371
+ gpu_muses.panoptic_segmentation(path),
372
+ )
373
+
374
+
146
375
  # ---------------------------------------------------------------------------
147
376
  # GPU loader smoke tests
148
377
  # ---------------------------------------------------------------------------
File without changes
File without changes