euler-loading 2.12.0__tar.gz → 2.17.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 (57) hide show
  1. {euler_loading-2.12.0 → euler_loading-2.17.0}/PKG-INFO +1 -1
  2. {euler_loading-2.12.0 → euler_loading-2.17.0}/README.md +19 -2
  3. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_resolution.py +2 -0
  4. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/__init__.py +4 -0
  5. euler_loading-2.17.0/euler_loading/loaders/_princeton_dense.py +314 -0
  6. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/__init__.py +2 -0
  7. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/generic.py +33 -0
  8. euler_loading-2.17.0/euler_loading/loaders/cpu/princeton_dense.py +160 -0
  9. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generate/loaders.json +149 -0
  10. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/__init__.py +2 -0
  11. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/generic.py +31 -0
  12. euler_loading-2.17.0/euler_loading/loaders/gpu/princeton_dense.py +163 -0
  13. euler_loading-2.17.0/euler_loading/loaders/princeton_dense.py +12 -0
  14. {euler_loading-2.12.0 → euler_loading-2.17.0}/package-lock.json +1 -1
  15. {euler_loading-2.12.0 → euler_loading-2.17.0}/pyproject.toml +1 -1
  16. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_loaders.py +325 -0
  17. {euler_loading-2.12.0 → euler_loading-2.17.0}/.github/workflows/workflow.yml +0 -0
  18. {euler_loading-2.12.0 → euler_loading-2.17.0}/.gitignore +0 -0
  19. {euler_loading-2.12.0 → euler_loading-2.17.0}/docs/loader-attributes.md +0 -0
  20. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/__init__.py +0 -0
  21. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_dataset_contract.py +0 -0
  22. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_ds_crawler_utils.py +0 -0
  23. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_metadata.py +0 -0
  24. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_writing.py +0 -0
  25. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/dataset.py +0 -0
  26. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/indexing.py +0 -0
  27. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/_annotations.py +0 -0
  28. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/_writer_utils.py +0 -0
  29. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/contracts.py +0 -0
  30. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  31. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/muses.py +0 -0
  32. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  33. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  34. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generate/__init__.py +0 -0
  35. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generate/__main__.py +0 -0
  36. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generic.py +0 -0
  37. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  38. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/muses.py +0 -0
  39. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  40. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  41. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/muses.py +0 -0
  42. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  43. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/vkitti2.py +0 -0
  44. {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/preprocessing.py +0 -0
  45. {euler_loading-2.12.0 → euler_loading-2.17.0}/example.py +0 -0
  46. {euler_loading-2.12.0 → euler_loading-2.17.0}/sample_rds.py +0 -0
  47. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/__init__.py +0 -0
  48. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/conftest.py +0 -0
  49. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/example_rds_calib.json +0 -0
  50. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_dataset.py +0 -0
  51. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_id_schema.py +0 -0
  52. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_indexing.py +0 -0
  53. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_preprocessing.py +0 -0
  54. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_real_dataset.py +0 -0
  55. {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_writing.py +0 -0
  56. {euler_loading-2.12.0 → euler_loading-2.17.0}/vkitti_cpu_example_output.json +0 -0
  57. {euler_loading-2.12.0 → euler_loading-2.17.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.12.0
3
+ Version: 2.17.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
@@ -381,7 +381,7 @@ When `Modality.loader` is `None`, euler-loading resolves the loader from the ds-
381
381
  }
382
382
  ```
383
383
 
384
- `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.
384
+ `loader` is the module name (`vkitti2`, `real_drive_sim`, `muses`, `princeton_dense`, `generic`, or `generic_dense_depth`) and `function` is the function within that module. The GPU variant is used by default.
385
385
 
386
386
  Writer resolution uses the same module and function metadata:
387
387
 
@@ -530,6 +530,15 @@ All built-in loaders accept both filesystem paths (`str`) and in-memory buffers
530
530
  | `read_intrinsics` | RGB camera intrinsics from MUSES `calib.json` as a `(3, 3)` matrix; defaults to `sensor="rgb"` |
531
531
  | `read_extrinsics` | Camera extrinsics from MUSES `calib.json` as a `(4, 4)` matrix; defaults to `transform="lidar2rgb"` for sparse-depth reprojection into RGB pixels |
532
532
 
533
+ ### Princeton DENSE / SeeingThroughFog (`euler_loading.loaders.princeton_dense`)
534
+
535
+ | Function | Description |
536
+ |----------|-------------|
537
+ | `rgb` | Left/right stereo 12-bit Bayer TIFF as RGB float32, normalised to [0, 1] |
538
+ | `sparse_depth` | Lidar `.bin` point cloud as `(N, 5)` float32: `x, y, z, intensity, ring` |
539
+ | `read_intrinsics` | Left stereo camera intrinsics `K` from `calib_cam_stereo_left.json` as a `(3, 3)` matrix |
540
+ | `read_extrinsics` | Transform from HDL64 lidar frame `lidar_hdl64_s3_roof` to left camera optical frame `cam_stereo_left_optical` from `calib_tf_tree_full.json` as a `(4, 4)` matrix |
541
+
533
542
  ### Generic Dense Depth (`euler_loading.loaders.gpu.generic_dense_depth`)
534
543
 
535
544
  A format-agnostic loader that infers the loading strategy from the file extension. Useful for datasets that don't have a dedicated loader module.
@@ -541,7 +550,15 @@ A format-agnostic loader that infers the loading strategy from the file extensio
541
550
  | `sky_mask` | Binary mask by comparing pixels against `meta["sky_mask"]` (`[R, G, B]`). Requires `meta` |
542
551
  | `read_intrinsics` | Returns `meta["intrinsics"]` as a `(3, 3)` tensor. Ignores path; requires `meta` |
543
552
 
544
- CPU variants of all loaders live under `euler_loading.loaders.cpu.{vkitti2,real_drive_sim,muses,generic_dense_depth}`.
553
+ ### Generic NumPy Modalities (`euler_loading.loaders.gpu.generic`)
554
+
555
+ Generic loaders for `.npy` and `.npz` modalities without dataset-specific decoding.
556
+
557
+ | Function | Description |
558
+ |----------|-------------|
559
+ | `points_3d` | Dense 3D point map from NumPy files stored as `(3, H, W)` float32 |
560
+
561
+ CPU variants of all loaders live under `euler_loading.loaders.cpu.{vkitti2,real_drive_sim,muses,princeton_dense,generic,generic_dense_depth}`.
545
562
 
546
563
  ### Flattening hierarchical modalities
547
564
 
@@ -24,6 +24,8 @@ _LOADER_MODULES: dict[str, str] = {
24
24
  "generic_dense_depth": "euler_loading.loaders.gpu.generic_dense_depth",
25
25
  "generic": "euler_loading.loaders.gpu.generic",
26
26
  "muses": "euler_loading.loaders.gpu.muses",
27
+ "princeton_dense": "euler_loading.loaders.gpu.princeton_dense",
28
+ "seeing_through_fog": "euler_loading.loaders.gpu.princeton_dense",
27
29
  }
28
30
 
29
31
 
@@ -27,4 +27,8 @@ Available submodules:
27
27
  - :mod:`euler_loading.loaders.cpu.real_drive_sim` — Real Drive Sim (numpy)
28
28
  - :mod:`euler_loading.loaders.gpu.muses` — MUSES (torch)
29
29
  - :mod:`euler_loading.loaders.cpu.muses` — MUSES (numpy)
30
+ - :mod:`euler_loading.loaders.gpu.princeton_dense` — Princeton DENSE /
31
+ SeeingThroughFog (torch)
32
+ - :mod:`euler_loading.loaders.cpu.princeton_dense` — Princeton DENSE /
33
+ SeeingThroughFog (numpy)
30
34
  """
@@ -0,0 +1,314 @@
1
+ """Shared helpers for Princeton DENSE / SeeingThroughFog loaders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from collections import deque
8
+ from collections.abc import Mapping
9
+ from typing import Any, BinaryIO, Union
10
+
11
+ import numpy as np
12
+ from PIL import Image
13
+
14
+ _DEFAULT_BAYER_PATTERN = "GBRG"
15
+ _DEFAULT_RAW_MAX_VALUE = 4095.0
16
+ DEFAULT_CAMERA_FRAME = "cam_stereo_left_optical"
17
+ DEFAULT_LIDAR_FRAME = "lidar_hdl64_s3_roof"
18
+ LIDAR_COLUMNS = ["x", "y", "z", "intensity", "ring"]
19
+
20
+
21
+ def _value_from_context(
22
+ meta: Mapping[str, Any] | None,
23
+ attributes: Mapping[str, Any] | None,
24
+ keys: tuple[str, ...],
25
+ ) -> Any:
26
+ for source in (attributes, meta):
27
+ if not isinstance(source, Mapping):
28
+ continue
29
+ for key in keys:
30
+ if key in source:
31
+ return source[key]
32
+ return None
33
+
34
+
35
+ def _rgb_scale(
36
+ raw: np.ndarray,
37
+ meta: Mapping[str, Any] | None,
38
+ attributes: Mapping[str, Any] | None,
39
+ ) -> float:
40
+ value = _value_from_context(
41
+ meta,
42
+ attributes,
43
+ ("rgb_max_value", "raw_max_value", "white_level"),
44
+ )
45
+ if value is not None:
46
+ return float(value)
47
+
48
+ if np.issubdtype(raw.dtype, np.integer):
49
+ if raw.dtype == np.uint8:
50
+ return 255.0
51
+ if raw.size and float(raw.max()) <= _DEFAULT_RAW_MAX_VALUE:
52
+ return _DEFAULT_RAW_MAX_VALUE
53
+ return float(np.iinfo(raw.dtype).max)
54
+
55
+ return 1.0
56
+
57
+
58
+ def _bayer_pattern(
59
+ meta: Mapping[str, Any] | None,
60
+ attributes: Mapping[str, Any] | None,
61
+ ) -> str:
62
+ value = _value_from_context(meta, attributes, ("bayer_pattern", "cfa_pattern"))
63
+ if value is None:
64
+ return _DEFAULT_BAYER_PATTERN
65
+ pattern = str(value).upper()
66
+ if len(pattern) != 4 or any(channel not in "RGB" for channel in pattern):
67
+ raise ValueError("bayer_pattern must be a 4-character pattern using R, G, and B")
68
+ return pattern
69
+
70
+
71
+ def _interpolate_channel(values: np.ndarray, mask: np.ndarray) -> np.ndarray:
72
+ values_pad = np.pad(values, 1, mode="edge")
73
+ mask_pad = np.pad(mask.astype(np.float32), 1, mode="edge")
74
+
75
+ sums = np.zeros_like(values, dtype=np.float32)
76
+ counts = np.zeros_like(values, dtype=np.float32)
77
+ for y_offset in range(3):
78
+ for x_offset in range(3):
79
+ y = slice(y_offset, y_offset + values.shape[0])
80
+ x = slice(x_offset, x_offset + values.shape[1])
81
+ sums += values_pad[y, x]
82
+ counts += mask_pad[y, x]
83
+
84
+ interpolated = np.divide(
85
+ sums,
86
+ counts,
87
+ out=np.zeros_like(sums, dtype=np.float32),
88
+ where=counts > 0,
89
+ )
90
+ return np.where(mask, values, interpolated)
91
+
92
+
93
+ def _demosaic_bayer(raw: np.ndarray, pattern: str) -> np.ndarray:
94
+ raw_f32 = raw.astype(np.float32, copy=False)
95
+ channel_values = {
96
+ "R": np.zeros(raw.shape, dtype=np.float32),
97
+ "G": np.zeros(raw.shape, dtype=np.float32),
98
+ "B": np.zeros(raw.shape, dtype=np.float32),
99
+ }
100
+ channel_masks = {
101
+ "R": np.zeros(raw.shape, dtype=bool),
102
+ "G": np.zeros(raw.shape, dtype=bool),
103
+ "B": np.zeros(raw.shape, dtype=bool),
104
+ }
105
+
106
+ positions = (
107
+ (slice(0, None, 2), slice(0, None, 2), pattern[0]),
108
+ (slice(0, None, 2), slice(1, None, 2), pattern[1]),
109
+ (slice(1, None, 2), slice(0, None, 2), pattern[2]),
110
+ (slice(1, None, 2), slice(1, None, 2), pattern[3]),
111
+ )
112
+ for row_slice, col_slice, channel in positions:
113
+ channel_values[channel][row_slice, col_slice] = raw_f32[row_slice, col_slice]
114
+ channel_masks[channel][row_slice, col_slice] = True
115
+
116
+ channels = [
117
+ _interpolate_channel(channel_values[channel], channel_masks[channel])
118
+ for channel in ("R", "G", "B")
119
+ ]
120
+ return np.stack(channels, axis=-1)
121
+
122
+
123
+ def load_rgb_array(
124
+ path: Union[str, BinaryIO],
125
+ meta: Mapping[str, Any] | None = None,
126
+ attributes: Mapping[str, Any] | None = None,
127
+ ) -> np.ndarray:
128
+ """Load a SeeingThroughFog RGB TIFF as an ``(H, W, 3)`` float32 array."""
129
+ with Image.open(path) as image:
130
+ raw = np.array(image)
131
+
132
+ if raw.ndim == 2:
133
+ rgb = _demosaic_bayer(raw, _bayer_pattern(meta, attributes))
134
+ scale = _rgb_scale(raw, meta, attributes)
135
+ elif raw.ndim == 3 and raw.shape[-1] >= 3:
136
+ rgb = raw[..., :3].astype(np.float32, copy=False)
137
+ scale = _rgb_scale(raw, meta, attributes)
138
+ else:
139
+ name = getattr(path, "name", path)
140
+ raise ValueError(f"Unsupported Princeton RGB image shape for {name!r}: {raw.shape}")
141
+
142
+ if scale <= 0:
143
+ raise ValueError(f"RGB scale must be positive, got {scale!r}")
144
+ return np.clip(rgb / scale, 0.0, 1.0).astype(np.float32, copy=False)
145
+
146
+
147
+ def load_sparse_depth_array(path: Union[str, BinaryIO]) -> np.ndarray:
148
+ """Load a SeeingThroughFog lidar ``.bin`` file as ``(N, 5)`` float32."""
149
+ if isinstance(path, (str, os.PathLike)):
150
+ values = np.fromfile(path, dtype=np.float32)
151
+ else:
152
+ values = np.frombuffer(path.read(), dtype=np.float32).copy()
153
+
154
+ if values.size % 5 != 0:
155
+ name = getattr(path, "name", path)
156
+ raise ValueError(
157
+ f"Princeton sparse-depth file {name!r} contains {values.size} "
158
+ "float32 values, which is not divisible by 5."
159
+ )
160
+ return values.reshape((-1, 5))
161
+
162
+
163
+ def _load_json(path: Union[str, BinaryIO]) -> Any:
164
+ if isinstance(path, (str, os.PathLike)):
165
+ with open(path) as f:
166
+ return json.load(f)
167
+ return json.load(path)
168
+
169
+
170
+ def load_intrinsics_array(path: Union[str, BinaryIO]) -> np.ndarray:
171
+ """Load the left stereo camera intrinsics matrix from ROS camera-info JSON."""
172
+ data = _load_json(path)
173
+ if not isinstance(data, Mapping) or "K" not in data:
174
+ name = getattr(path, "name", path)
175
+ raise ValueError(f"Princeton camera calibration {name!r} must contain a K array.")
176
+
177
+ K = np.asarray(data["K"], dtype=np.float32)
178
+ if K.shape == (9,):
179
+ K = K.reshape(3, 3)
180
+ if K.shape != (3, 3):
181
+ raise ValueError(f"Princeton camera K must have shape (3, 3), got {K.shape}.")
182
+ return K
183
+
184
+
185
+ def _select_frame(
186
+ meta: Mapping[str, Any] | None,
187
+ attributes: Mapping[str, Any] | None,
188
+ keys: tuple[str, ...],
189
+ default: str,
190
+ ) -> str:
191
+ value = _value_from_context(meta, attributes, keys)
192
+ if value is None:
193
+ return default
194
+ frame = str(value)
195
+ aliases = {
196
+ "camera": DEFAULT_CAMERA_FRAME,
197
+ "cam_stereo_left": DEFAULT_CAMERA_FRAME,
198
+ "left": DEFAULT_CAMERA_FRAME,
199
+ "rgb": DEFAULT_CAMERA_FRAME,
200
+ "lidar": DEFAULT_LIDAR_FRAME,
201
+ "hdl64": DEFAULT_LIDAR_FRAME,
202
+ "hdl64_s3": DEFAULT_LIDAR_FRAME,
203
+ "lidar_hdl64": DEFAULT_LIDAR_FRAME,
204
+ }
205
+ return aliases.get(frame, frame)
206
+
207
+
208
+ def _matrix_from_transform(transform: Mapping[str, Any]) -> np.ndarray:
209
+ translation = transform.get("translation")
210
+ rotation = transform.get("rotation")
211
+ if not isinstance(translation, Mapping) or not isinstance(rotation, Mapping):
212
+ raise ValueError("TF transform entries must contain translation and rotation objects.")
213
+
214
+ x = float(rotation.get("x", 0.0))
215
+ y = float(rotation.get("y", 0.0))
216
+ z = float(rotation.get("z", 0.0))
217
+ w = float(rotation.get("w", 1.0))
218
+ norm = float(np.sqrt(x * x + y * y + z * z + w * w))
219
+ if norm <= 0.0:
220
+ raise ValueError("TF quaternion must have non-zero norm.")
221
+ x /= norm
222
+ y /= norm
223
+ z /= norm
224
+ w /= norm
225
+
226
+ matrix = np.eye(4, dtype=np.float64)
227
+ matrix[:3, :3] = np.array(
228
+ [
229
+ [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
230
+ [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
231
+ [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
232
+ ],
233
+ dtype=np.float64,
234
+ )
235
+ matrix[:3, 3] = [
236
+ float(translation.get("x", 0.0)),
237
+ float(translation.get("y", 0.0)),
238
+ float(translation.get("z", 0.0)),
239
+ ]
240
+ return matrix
241
+
242
+
243
+ def _tf_edges(data: Any) -> dict[str, list[tuple[str, np.ndarray]]]:
244
+ if not isinstance(data, list):
245
+ raise ValueError("Princeton TF calibration must be a JSON list of transforms.")
246
+
247
+ edges: dict[str, list[tuple[str, np.ndarray]]] = {}
248
+ for entry in data:
249
+ if not isinstance(entry, Mapping):
250
+ continue
251
+ header = entry.get("header")
252
+ child = entry.get("child_frame_id")
253
+ transform = entry.get("transform")
254
+ if not isinstance(header, Mapping) or not isinstance(child, str):
255
+ continue
256
+ parent = header.get("frame_id")
257
+ if not isinstance(parent, str) or not isinstance(transform, Mapping):
258
+ continue
259
+
260
+ # ROS TransformStamped stores the child pose in the parent frame.
261
+ # This matrix maps coordinates from child frame to parent frame.
262
+ parent_from_child = _matrix_from_transform(transform)
263
+ child_from_parent = np.linalg.inv(parent_from_child)
264
+ edges.setdefault(child, []).append((parent, parent_from_child))
265
+ edges.setdefault(parent, []).append((child, child_from_parent))
266
+ return edges
267
+
268
+
269
+ def _compose_tf(
270
+ edges: Mapping[str, list[tuple[str, np.ndarray]]],
271
+ source_frame: str,
272
+ target_frame: str,
273
+ ) -> np.ndarray:
274
+ queue: deque[tuple[str, np.ndarray]] = deque(
275
+ [(source_frame, np.eye(4, dtype=np.float64))]
276
+ )
277
+ seen = {source_frame}
278
+
279
+ while queue:
280
+ frame, frame_from_source = queue.popleft()
281
+ if frame == target_frame:
282
+ return frame_from_source.astype(np.float32)
283
+ for next_frame, next_from_frame in edges.get(frame, []):
284
+ if next_frame in seen:
285
+ continue
286
+ seen.add(next_frame)
287
+ queue.append((next_frame, next_from_frame @ frame_from_source))
288
+
289
+ available = ", ".join(sorted(edges.keys()))
290
+ raise KeyError(
291
+ f"No TF path from {source_frame!r} to {target_frame!r}. "
292
+ f"Available frames: {available}"
293
+ )
294
+
295
+
296
+ def load_extrinsics_array(
297
+ path: Union[str, BinaryIO],
298
+ meta: Mapping[str, Any] | None = None,
299
+ attributes: Mapping[str, Any] | None = None,
300
+ ) -> np.ndarray:
301
+ """Load a 4x4 transform mapping HDL64 lidar points into the camera frame."""
302
+ source_frame = _select_frame(
303
+ meta,
304
+ attributes,
305
+ ("source_frame", "source_sensor", "from_frame", "from_sensor", "source"),
306
+ DEFAULT_LIDAR_FRAME,
307
+ )
308
+ target_frame = _select_frame(
309
+ meta,
310
+ attributes,
311
+ ("target_frame", "target_sensor", "to_frame", "to_sensor", "target", "camera"),
312
+ DEFAULT_CAMERA_FRAME,
313
+ )
314
+ return _compose_tf(_tf_edges(_load_json(path)), source_frame, target_frame)
@@ -10,4 +10,6 @@ Available submodules:
10
10
  - :mod:`euler_loading.loaders.cpu.vkitti2` — Virtual KITTI 2
11
11
  - :mod:`euler_loading.loaders.cpu.real_drive_sim` — Real Drive Sim
12
12
  - :mod:`euler_loading.loaders.cpu.muses` — MUSES
13
+ - :mod:`euler_loading.loaders.cpu.princeton_dense` — Princeton DENSE /
14
+ SeeingThroughFog
13
15
  """
@@ -12,6 +12,7 @@ Return types
12
12
  ------------
13
13
  - **map_2d** / **scattering_coefficient** -- ``np.ndarray`` of shape ``(H, W)`` float32.
14
14
  - **map_3d** / **atmospheric_light** -- ``np.ndarray`` of shape ``(H, W, C)`` float32.
15
+ - **points_3d** -- ``np.ndarray`` of shape ``(3, H, W)`` float32.
15
16
  - **spherical_map** -- ``np.ndarray`` of shape ``(H, W, C)`` float32.
16
17
  - **intrinsics** -- ``np.ndarray`` of shape ``(3, 3)`` float32.
17
18
  - **sh_coeffs** -- ``np.ndarray`` of shape ``(N, 3)`` float32.
@@ -23,6 +24,7 @@ Usage::
23
24
 
24
25
  Modality("/data/my_dataset/scattering_coefficient", loader=generic.scattering_coefficient)
25
26
  Modality("/data/my_dataset/atmospheric_light", loader=generic.atmospheric_light)
27
+ Modality("/data/my_dataset/points_3d", loader=generic.points_3d)
26
28
  Modality("/data/my_dataset/spherical_map", loader=generic.spherical_map)
27
29
  Modality("/data/my_dataset/intrinsics", loader=generic.intrinsics)
28
30
  Modality("/data/my_dataset/sh_coeffs", loader=generic.sh_coeffs)
@@ -85,6 +87,14 @@ def _write_numpy(path: Union[str, BinaryIO], value: Any) -> None:
85
87
  raise ValueError(f"Unsupported output extension: {ext}")
86
88
 
87
89
 
90
+ def _as_3d_points(value: Any) -> np.ndarray:
91
+ """Return a float32 ``(3, H, W)`` array for dense 3D point maps."""
92
+ arr = to_numpy(value).astype(np.float32)
93
+ if arr.ndim != 3 or arr.shape[0] != 3:
94
+ raise ValueError(f"points_3d must have shape (3, H, W), got {arr.shape}")
95
+ return arr
96
+
97
+
88
98
  # ---------------------------------------------------------------------------
89
99
  # Public loaders
90
100
  # ---------------------------------------------------------------------------
@@ -124,6 +134,23 @@ def map_3d(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, at
124
134
  return np.transpose(arr, (1, 2, 0))
125
135
 
126
136
 
137
+ @modality_meta(
138
+ modality_type="points_3d",
139
+ dtype="float32",
140
+ shape="3HW",
141
+ file_formats=[".npy", ".npz"],
142
+ )
143
+ def points_3d(path: Union[str, BinaryIO], meta: dict[str, Any] | None = None, *, attributes: dict[str, Any] | None = None) -> np.ndarray:
144
+ """Load dense 3D points as a ``(3, H, W)`` float32 array.
145
+
146
+ Files are expected to store the per-pixel 3D point coordinates directly in
147
+ ``(3, H, W)`` layout. Unlike :func:`map_3d`, this loader preserves the
148
+ channel-first representation on CPU because it is a coordinate point map,
149
+ not a generic HWC image-style map.
150
+ """
151
+ return _as_3d_points(_load_numpy(path))
152
+
153
+
127
154
  @modality_meta(
128
155
  modality_type="scattering_coefficient",
129
156
  dtype="float32",
@@ -239,6 +266,12 @@ def write_map_3d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] |
239
266
  raise ValueError(f"Unsupported map_3d output extension: {ext}")
240
267
 
241
268
 
269
+ @mark_stream_supported
270
+ def write_points_3d(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
271
+ """Write dense 3D points to NumPy formats in ``(3, H, W)`` layout."""
272
+ _write_numpy(path, _as_3d_points(value))
273
+
274
+
242
275
  @mark_stream_supported
243
276
  def write_scattering_coefficient(path: Union[str, BinaryIO], value: Any, meta: dict[str, Any] | None = None) -> None:
244
277
  """Write a scattering-coefficient map (delegates to :func:`write_map_2d`)."""
@@ -0,0 +1,160 @@
1
+ """CPU-oriented loaders for the Princeton DENSE / SeeingThroughFog dataset.
2
+
3
+ The SeeingThroughFog release stores stereo-camera frames as 12-bit Bayer TIFF
4
+ images and Velodyne lidar point clouds as binary ``float32`` records.
5
+
6
+ Return types
7
+ ------------
8
+ - **rccb** -- ``np.ndarray`` of shape ``(H, W, 3)`` float32 in ``[0, 1]``.
9
+ - **rgb** -- ``np.ndarray`` of shape ``(H, W, 3)`` float32 in ``[0, 1]``.
10
+ - **sparse_depth** -- ``np.ndarray`` of shape ``(N, 5)`` float32 with columns
11
+ ``x, y, z, intensity, ring``.
12
+ - **read_intrinsics** -- ``np.ndarray`` of shape ``(3, 3)`` float32 from
13
+ ``calib_cam_stereo_left.json``.
14
+ - **read_extrinsics** -- ``np.ndarray`` of shape ``(4, 4)`` float32 from
15
+ ``calib_tf_tree_full.json``, mapping HDL64 lidar points into the left camera
16
+ optical frame by default.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any, BinaryIO, Union
22
+
23
+ import numpy as np
24
+ from PIL import Image
25
+
26
+ from euler_loading.loaders._annotations import modality_meta
27
+ from euler_loading.loaders._princeton_dense import DEFAULT_CAMERA_FRAME
28
+ from euler_loading.loaders._princeton_dense import DEFAULT_LIDAR_FRAME
29
+ from euler_loading.loaders._princeton_dense import LIDAR_COLUMNS
30
+ from euler_loading.loaders._princeton_dense import load_extrinsics_array
31
+ from euler_loading.loaders._princeton_dense import load_intrinsics_array
32
+ from euler_loading.loaders._princeton_dense import load_rgb_array
33
+ from euler_loading.loaders._princeton_dense import load_sparse_depth_array
34
+
35
+
36
+ @modality_meta(
37
+ modality_type="rccb",
38
+ dtype="float32",
39
+ shape="HWC",
40
+ file_formats=[".tif", ".tiff"],
41
+ output_range=[0.0, 1.0],
42
+ meta={
43
+ "dataset": "SeeingThroughFog",
44
+ "raw_bit_depth": 12,
45
+ "bayer_pattern": "GBRG",
46
+ "raw_max_value": 4095.0,
47
+ },
48
+ )
49
+ def rccb(
50
+ path: Union[str, BinaryIO],
51
+ meta: dict[str, Any] | None = None,
52
+ *,
53
+ attributes: dict[str, Any] | None = None,
54
+ ) -> np.ndarray:
55
+ """Load a SeeingThroughFog 12-bit Bayer TIFF as RCCB float32 in ``[0, 1]``."""
56
+ return load_rgb_array(path, meta, attributes)
57
+
58
+
59
+ @modality_meta(
60
+ modality_type="rgb",
61
+ dtype="float32",
62
+ shape="HWC",
63
+ file_formats=[".png"],
64
+ output_range=[0.0, 1.0],
65
+ )
66
+ def rgb(
67
+ path: Union[str, BinaryIO],
68
+ meta: dict[str, Any] | None = None,
69
+ *,
70
+ attributes: dict[str, Any] | None = None,
71
+ ) -> np.ndarray:
72
+ """Load a SeeingThroughFog plain 8-bit PNG as RGB float32 in ``[0, 1]``."""
73
+ return np.array(Image.open(path).convert("RGB"), dtype=np.float32) / 255.0
74
+
75
+
76
+ @modality_meta(
77
+ modality_type="sparse_depth",
78
+ dtype="float32",
79
+ shape="NC",
80
+ file_formats=[".bin"],
81
+ meta={
82
+ "dataset": "SeeingThroughFog",
83
+ "representation": "point_cloud",
84
+ "columns": LIDAR_COLUMNS,
85
+ "coordinate_unit": "meters",
86
+ },
87
+ )
88
+ def sparse_depth(
89
+ path: Union[str, BinaryIO],
90
+ meta: dict[str, Any] | None = None,
91
+ *,
92
+ attributes: dict[str, Any] | None = None,
93
+ ) -> np.ndarray:
94
+ """Load a SeeingThroughFog lidar point cloud as ``(N, 5)`` float32."""
95
+ return load_sparse_depth_array(path)
96
+
97
+
98
+ def lidar_point_cloud(
99
+ path: Union[str, BinaryIO],
100
+ meta: dict[str, Any] | None = None,
101
+ *,
102
+ attributes: dict[str, Any] | None = None,
103
+ ) -> np.ndarray:
104
+ """Alias for :func:`sparse_depth`."""
105
+ return load_sparse_depth_array(path)
106
+
107
+
108
+ def point_cloud(
109
+ path: Union[str, BinaryIO],
110
+ meta: dict[str, Any] | None = None,
111
+ *,
112
+ attributes: dict[str, Any] | None = None,
113
+ ) -> np.ndarray:
114
+ """Alias for :func:`sparse_depth`."""
115
+ return load_sparse_depth_array(path)
116
+
117
+
118
+ @modality_meta(
119
+ modality_type="intrinsics",
120
+ dtype="float32",
121
+ hierarchical=True,
122
+ shape="3x3",
123
+ file_formats=[".json"],
124
+ meta={
125
+ "dataset": "SeeingThroughFog",
126
+ "camera_frame": DEFAULT_CAMERA_FRAME,
127
+ "source": "calib_cam_stereo_left",
128
+ },
129
+ )
130
+ def read_intrinsics(
131
+ path: Union[str, BinaryIO],
132
+ meta: dict[str, Any] | None = None,
133
+ *,
134
+ attributes: dict[str, Any] | None = None,
135
+ ) -> np.ndarray:
136
+ """Load the left stereo camera intrinsics matrix ``K``."""
137
+ return load_intrinsics_array(path)
138
+
139
+
140
+ @modality_meta(
141
+ modality_type="camera_extrinsics",
142
+ dtype="float32",
143
+ hierarchical=True,
144
+ shape="4x4",
145
+ file_formats=[".json"],
146
+ meta={
147
+ "dataset": "SeeingThroughFog",
148
+ "default_source_frame": DEFAULT_LIDAR_FRAME,
149
+ "default_target_frame": DEFAULT_CAMERA_FRAME,
150
+ "transform_direction": "source_frame_to_target_frame",
151
+ },
152
+ )
153
+ def read_extrinsics(
154
+ path: Union[str, BinaryIO],
155
+ meta: dict[str, Any] | None = None,
156
+ *,
157
+ attributes: dict[str, Any] | None = None,
158
+ ) -> np.ndarray:
159
+ """Load the default ``lidar_hdl64_s3_roof`` to left-camera transform."""
160
+ return load_extrinsics_array(path, meta, attributes)