euler-loading 2.12.0__tar.gz → 2.14.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.14.0}/PKG-INFO +1 -1
  2. {euler_loading-2.12.0 → euler_loading-2.14.0}/README.md +11 -2
  3. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/_resolution.py +2 -0
  4. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/__init__.py +4 -0
  5. euler_loading-2.14.0/euler_loading/loaders/_princeton_dense.py +314 -0
  6. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/cpu/__init__.py +2 -0
  7. euler_loading-2.14.0/euler_loading/loaders/cpu/princeton_dense.py +141 -0
  8. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/generate/loaders.json +112 -0
  9. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/gpu/__init__.py +2 -0
  10. euler_loading-2.14.0/euler_loading/loaders/gpu/princeton_dense.py +142 -0
  11. euler_loading-2.14.0/euler_loading/loaders/princeton_dense.py +12 -0
  12. {euler_loading-2.12.0 → euler_loading-2.14.0}/package-lock.json +1 -1
  13. {euler_loading-2.12.0 → euler_loading-2.14.0}/pyproject.toml +1 -1
  14. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_loaders.py +212 -0
  15. {euler_loading-2.12.0 → euler_loading-2.14.0}/.github/workflows/workflow.yml +0 -0
  16. {euler_loading-2.12.0 → euler_loading-2.14.0}/.gitignore +0 -0
  17. {euler_loading-2.12.0 → euler_loading-2.14.0}/docs/loader-attributes.md +0 -0
  18. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/__init__.py +0 -0
  19. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/_dataset_contract.py +0 -0
  20. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/_ds_crawler_utils.py +0 -0
  21. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/_metadata.py +0 -0
  22. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/_writing.py +0 -0
  23. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/dataset.py +0 -0
  24. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/indexing.py +0 -0
  25. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/_annotations.py +0 -0
  26. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/_writer_utils.py +0 -0
  27. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/contracts.py +0 -0
  28. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/cpu/generic.py +0 -0
  29. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  30. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/cpu/muses.py +0 -0
  31. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  32. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  33. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/generate/__init__.py +0 -0
  34. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/generate/__main__.py +0 -0
  35. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/generic.py +0 -0
  36. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/gpu/generic.py +0 -0
  37. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  38. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/gpu/muses.py +0 -0
  39. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  40. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  41. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/muses.py +0 -0
  42. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  43. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/loaders/vkitti2.py +0 -0
  44. {euler_loading-2.12.0 → euler_loading-2.14.0}/euler_loading/preprocessing.py +0 -0
  45. {euler_loading-2.12.0 → euler_loading-2.14.0}/example.py +0 -0
  46. {euler_loading-2.12.0 → euler_loading-2.14.0}/sample_rds.py +0 -0
  47. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/__init__.py +0 -0
  48. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/conftest.py +0 -0
  49. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/example_rds_calib.json +0 -0
  50. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_dataset.py +0 -0
  51. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_id_schema.py +0 -0
  52. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_indexing.py +0 -0
  53. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_preprocessing.py +0 -0
  54. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_real_dataset.py +0 -0
  55. {euler_loading-2.12.0 → euler_loading-2.14.0}/tests/test_writing.py +0 -0
  56. {euler_loading-2.12.0 → euler_loading-2.14.0}/vkitti_cpu_example_output.json +0 -0
  57. {euler_loading-2.12.0 → euler_loading-2.14.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.14.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`, 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,7 @@ 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
+ CPU variants of all loaders live under `euler_loading.loaders.cpu.{vkitti2,real_drive_sim,muses,princeton_dense,generic_dense_depth}`.
545
554
 
546
555
  ### Flattening hierarchical modalities
547
556
 
@@ -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
  """
@@ -0,0 +1,141 @@
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
+ - **rgb** -- ``np.ndarray`` of shape ``(H, W, 3)`` float32 in ``[0, 1]``.
9
+ - **sparse_depth** -- ``np.ndarray`` of shape ``(N, 5)`` float32 with columns
10
+ ``x, y, z, intensity, ring``.
11
+ - **read_intrinsics** -- ``np.ndarray`` of shape ``(3, 3)`` float32 from
12
+ ``calib_cam_stereo_left.json``.
13
+ - **read_extrinsics** -- ``np.ndarray`` of shape ``(4, 4)`` float32 from
14
+ ``calib_tf_tree_full.json``, mapping HDL64 lidar points into the left camera
15
+ optical frame by default.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, BinaryIO, Union
21
+
22
+ import numpy as np
23
+
24
+ from euler_loading.loaders._annotations import modality_meta
25
+ from euler_loading.loaders._princeton_dense import DEFAULT_CAMERA_FRAME
26
+ from euler_loading.loaders._princeton_dense import DEFAULT_LIDAR_FRAME
27
+ from euler_loading.loaders._princeton_dense import LIDAR_COLUMNS
28
+ from euler_loading.loaders._princeton_dense import load_extrinsics_array
29
+ from euler_loading.loaders._princeton_dense import load_intrinsics_array
30
+ from euler_loading.loaders._princeton_dense import load_rgb_array
31
+ from euler_loading.loaders._princeton_dense import load_sparse_depth_array
32
+
33
+
34
+ @modality_meta(
35
+ modality_type="rgb",
36
+ dtype="float32",
37
+ shape="HWC",
38
+ file_formats=[".tif", ".tiff"],
39
+ output_range=[0.0, 1.0],
40
+ meta={
41
+ "dataset": "SeeingThroughFog",
42
+ "raw_bit_depth": 12,
43
+ "bayer_pattern": "GBRG",
44
+ "raw_max_value": 4095.0,
45
+ },
46
+ )
47
+ def rgb(
48
+ path: Union[str, BinaryIO],
49
+ meta: dict[str, Any] | None = None,
50
+ *,
51
+ attributes: dict[str, Any] | None = None,
52
+ ) -> np.ndarray:
53
+ """Load a SeeingThroughFog 12-bit Bayer TIFF as RGB float32 in ``[0, 1]``."""
54
+ return load_rgb_array(path, meta, attributes)
55
+
56
+
57
+ @modality_meta(
58
+ modality_type="sparse_depth",
59
+ dtype="float32",
60
+ shape="NC",
61
+ file_formats=[".bin"],
62
+ meta={
63
+ "dataset": "SeeingThroughFog",
64
+ "representation": "point_cloud",
65
+ "columns": LIDAR_COLUMNS,
66
+ "coordinate_unit": "meters",
67
+ },
68
+ )
69
+ def sparse_depth(
70
+ path: Union[str, BinaryIO],
71
+ meta: dict[str, Any] | None = None,
72
+ *,
73
+ attributes: dict[str, Any] | None = None,
74
+ ) -> np.ndarray:
75
+ """Load a SeeingThroughFog lidar point cloud as ``(N, 5)`` float32."""
76
+ return load_sparse_depth_array(path)
77
+
78
+
79
+ def lidar_point_cloud(
80
+ path: Union[str, BinaryIO],
81
+ meta: dict[str, Any] | None = None,
82
+ *,
83
+ attributes: dict[str, Any] | None = None,
84
+ ) -> np.ndarray:
85
+ """Alias for :func:`sparse_depth`."""
86
+ return load_sparse_depth_array(path)
87
+
88
+
89
+ def point_cloud(
90
+ path: Union[str, BinaryIO],
91
+ meta: dict[str, Any] | None = None,
92
+ *,
93
+ attributes: dict[str, Any] | None = None,
94
+ ) -> np.ndarray:
95
+ """Alias for :func:`sparse_depth`."""
96
+ return load_sparse_depth_array(path)
97
+
98
+
99
+ @modality_meta(
100
+ modality_type="intrinsics",
101
+ dtype="float32",
102
+ hierarchical=True,
103
+ shape="3x3",
104
+ file_formats=[".json"],
105
+ meta={
106
+ "dataset": "SeeingThroughFog",
107
+ "camera_frame": DEFAULT_CAMERA_FRAME,
108
+ "source": "calib_cam_stereo_left",
109
+ },
110
+ )
111
+ def read_intrinsics(
112
+ path: Union[str, BinaryIO],
113
+ meta: dict[str, Any] | None = None,
114
+ *,
115
+ attributes: dict[str, Any] | None = None,
116
+ ) -> np.ndarray:
117
+ """Load the left stereo camera intrinsics matrix ``K``."""
118
+ return load_intrinsics_array(path)
119
+
120
+
121
+ @modality_meta(
122
+ modality_type="camera_extrinsics",
123
+ dtype="float32",
124
+ hierarchical=True,
125
+ shape="4x4",
126
+ file_formats=[".json"],
127
+ meta={
128
+ "dataset": "SeeingThroughFog",
129
+ "default_source_frame": DEFAULT_LIDAR_FRAME,
130
+ "default_target_frame": DEFAULT_CAMERA_FRAME,
131
+ "transform_direction": "source_frame_to_target_frame",
132
+ },
133
+ )
134
+ def read_extrinsics(
135
+ path: Union[str, BinaryIO],
136
+ meta: dict[str, Any] | None = None,
137
+ *,
138
+ attributes: dict[str, Any] | None = None,
139
+ ) -> np.ndarray:
140
+ """Load the default ``lidar_hdl64_s3_roof`` to left-camera transform."""
141
+ return load_extrinsics_array(path, meta, attributes)
@@ -453,6 +453,109 @@
453
453
  }
454
454
  ]
455
455
  },
456
+ {
457
+ "name": "princeton_dense",
458
+ "modalities": [
459
+ {
460
+ "type": "camera_extrinsics",
461
+ "function": "read_extrinsics",
462
+ "hierarchical": true,
463
+ "dtype": "float32",
464
+ "access": "key",
465
+ "file_formats": [
466
+ ".json"
467
+ ],
468
+ "meta": {
469
+ "dataset": "SeeingThroughFog",
470
+ "default_source_frame": "lidar_hdl64_s3_roof",
471
+ "default_target_frame": "cam_stereo_left_optical",
472
+ "transform_direction": "source_frame_to_target_frame"
473
+ },
474
+ "cpu": {
475
+ "shape": "4x4"
476
+ },
477
+ "gpu": {
478
+ "shape": "4x4"
479
+ }
480
+ },
481
+ {
482
+ "type": "intrinsics",
483
+ "function": "read_intrinsics",
484
+ "hierarchical": true,
485
+ "dtype": "float32",
486
+ "access": "key",
487
+ "file_formats": [
488
+ ".json"
489
+ ],
490
+ "meta": {
491
+ "dataset": "SeeingThroughFog",
492
+ "camera_frame": "cam_stereo_left_optical",
493
+ "source": "calib_cam_stereo_left"
494
+ },
495
+ "cpu": {
496
+ "shape": "3x3"
497
+ },
498
+ "gpu": {
499
+ "shape": "3x3"
500
+ }
501
+ },
502
+ {
503
+ "type": "rgb",
504
+ "function": "rgb",
505
+ "hierarchical": false,
506
+ "dtype": "float32",
507
+ "access": "key",
508
+ "output_range": [
509
+ 0.0,
510
+ 1.0
511
+ ],
512
+ "file_formats": [
513
+ ".tif",
514
+ ".tiff"
515
+ ],
516
+ "meta": {
517
+ "dataset": "SeeingThroughFog",
518
+ "raw_bit_depth": 12,
519
+ "bayer_pattern": "GBRG",
520
+ "raw_max_value": 4095.0
521
+ },
522
+ "cpu": {
523
+ "shape": "HWC"
524
+ },
525
+ "gpu": {
526
+ "shape": "CHW"
527
+ }
528
+ },
529
+ {
530
+ "type": "sparse_depth",
531
+ "function": "sparse_depth",
532
+ "hierarchical": false,
533
+ "dtype": "float32",
534
+ "access": "key",
535
+ "file_formats": [
536
+ ".bin"
537
+ ],
538
+ "meta": {
539
+ "dataset": "SeeingThroughFog",
540
+ "representation": "point_cloud",
541
+ "columns": [
542
+ "x",
543
+ "y",
544
+ "z",
545
+ "intensity",
546
+ "ring"
547
+ ],
548
+ "coordinate_unit": "meters"
549
+ },
550
+ "cpu": {
551
+ "shape": "NC"
552
+ },
553
+ "gpu": {
554
+ "shape": "NC"
555
+ }
556
+ }
557
+ ]
558
+ },
456
559
  {
457
560
  "name": "real_drive_sim",
458
561
  "modalities": [
@@ -465,6 +568,9 @@
465
568
  "file_formats": [
466
569
  ".json"
467
570
  ],
571
+ "cpu": {
572
+ "shape": "dict"
573
+ },
468
574
  "gpu": {
469
575
  "shape": "dict"
470
576
  }
@@ -490,6 +596,9 @@
490
596
  "distortion"
491
597
  ]
492
598
  },
599
+ "cpu": {
600
+ "shape": "dict"
601
+ },
493
602
  "gpu": {
494
603
  "shape": "dict"
495
604
  }
@@ -546,6 +655,9 @@
546
655
  "meta": {
547
656
  "sensor": "CS_FRONT"
548
657
  },
658
+ "cpu": {
659
+ "shape": "3x3"
660
+ },
549
661
  "gpu": {
550
662
  "shape": "3x3"
551
663
  }
@@ -9,4 +9,6 @@ Available submodules:
9
9
  - :mod:`euler_loading.loaders.gpu.vkitti2` — Virtual KITTI 2
10
10
  - :mod:`euler_loading.loaders.gpu.real_drive_sim` — Real Drive Sim
11
11
  - :mod:`euler_loading.loaders.gpu.muses` — MUSES
12
+ - :mod:`euler_loading.loaders.gpu.princeton_dense` — Princeton DENSE /
13
+ SeeingThroughFog
12
14
  """
@@ -0,0 +1,142 @@
1
+ """GPU-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
+ - **rgb** -- ``torch.FloatTensor`` of shape ``(3, H, W)`` in ``[0, 1]``.
9
+ - **sparse_depth** -- ``torch.FloatTensor`` of shape ``(N, 5)`` with columns
10
+ ``x, y, z, intensity, ring``.
11
+ - **read_intrinsics** -- ``torch.FloatTensor`` of shape ``(3, 3)`` from
12
+ ``calib_cam_stereo_left.json``.
13
+ - **read_extrinsics** -- ``torch.FloatTensor`` of shape ``(4, 4)`` from
14
+ ``calib_tf_tree_full.json``, mapping HDL64 lidar points into the left camera
15
+ optical frame by default.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, BinaryIO, Union
21
+
22
+ import torch
23
+
24
+ from euler_loading.loaders._annotations import modality_meta
25
+ from euler_loading.loaders._princeton_dense import DEFAULT_CAMERA_FRAME
26
+ from euler_loading.loaders._princeton_dense import DEFAULT_LIDAR_FRAME
27
+ from euler_loading.loaders._princeton_dense import LIDAR_COLUMNS
28
+ from euler_loading.loaders._princeton_dense import load_extrinsics_array
29
+ from euler_loading.loaders._princeton_dense import load_intrinsics_array
30
+ from euler_loading.loaders._princeton_dense import load_rgb_array
31
+ from euler_loading.loaders._princeton_dense import load_sparse_depth_array
32
+
33
+
34
+ @modality_meta(
35
+ modality_type="rgb",
36
+ dtype="float32",
37
+ shape="CHW",
38
+ file_formats=[".tif", ".tiff"],
39
+ output_range=[0.0, 1.0],
40
+ meta={
41
+ "dataset": "SeeingThroughFog",
42
+ "raw_bit_depth": 12,
43
+ "bayer_pattern": "GBRG",
44
+ "raw_max_value": 4095.0,
45
+ },
46
+ )
47
+ def rgb(
48
+ path: Union[str, BinaryIO],
49
+ meta: dict[str, Any] | None = None,
50
+ *,
51
+ attributes: dict[str, Any] | None = None,
52
+ ) -> torch.Tensor:
53
+ """Load a SeeingThroughFog 12-bit Bayer TIFF as an RGB tensor."""
54
+ arr = load_rgb_array(path, meta, attributes)
55
+ return torch.from_numpy(arr).permute(2, 0, 1).contiguous()
56
+
57
+
58
+ @modality_meta(
59
+ modality_type="sparse_depth",
60
+ dtype="float32",
61
+ shape="NC",
62
+ file_formats=[".bin"],
63
+ meta={
64
+ "dataset": "SeeingThroughFog",
65
+ "representation": "point_cloud",
66
+ "columns": LIDAR_COLUMNS,
67
+ "coordinate_unit": "meters",
68
+ },
69
+ )
70
+ def sparse_depth(
71
+ path: Union[str, BinaryIO],
72
+ meta: dict[str, Any] | None = None,
73
+ *,
74
+ attributes: dict[str, Any] | None = None,
75
+ ) -> torch.Tensor:
76
+ """Load a SeeingThroughFog lidar point cloud as ``(N, 5)`` float32."""
77
+ return torch.from_numpy(load_sparse_depth_array(path)).contiguous()
78
+
79
+
80
+ def lidar_point_cloud(
81
+ path: Union[str, BinaryIO],
82
+ meta: dict[str, Any] | None = None,
83
+ *,
84
+ attributes: dict[str, Any] | None = None,
85
+ ) -> torch.Tensor:
86
+ """Alias for :func:`sparse_depth`."""
87
+ return torch.from_numpy(load_sparse_depth_array(path)).contiguous()
88
+
89
+
90
+ def point_cloud(
91
+ path: Union[str, BinaryIO],
92
+ meta: dict[str, Any] | None = None,
93
+ *,
94
+ attributes: dict[str, Any] | None = None,
95
+ ) -> torch.Tensor:
96
+ """Alias for :func:`sparse_depth`."""
97
+ return torch.from_numpy(load_sparse_depth_array(path)).contiguous()
98
+
99
+
100
+ @modality_meta(
101
+ modality_type="intrinsics",
102
+ dtype="float32",
103
+ hierarchical=True,
104
+ shape="3x3",
105
+ file_formats=[".json"],
106
+ meta={
107
+ "dataset": "SeeingThroughFog",
108
+ "camera_frame": DEFAULT_CAMERA_FRAME,
109
+ "source": "calib_cam_stereo_left",
110
+ },
111
+ )
112
+ def read_intrinsics(
113
+ path: Union[str, BinaryIO],
114
+ meta: dict[str, Any] | None = None,
115
+ *,
116
+ attributes: dict[str, Any] | None = None,
117
+ ) -> torch.Tensor:
118
+ """Load the left stereo camera intrinsics matrix ``K``."""
119
+ return torch.from_numpy(load_intrinsics_array(path)).contiguous()
120
+
121
+
122
+ @modality_meta(
123
+ modality_type="camera_extrinsics",
124
+ dtype="float32",
125
+ hierarchical=True,
126
+ shape="4x4",
127
+ file_formats=[".json"],
128
+ meta={
129
+ "dataset": "SeeingThroughFog",
130
+ "default_source_frame": DEFAULT_LIDAR_FRAME,
131
+ "default_target_frame": DEFAULT_CAMERA_FRAME,
132
+ "transform_direction": "source_frame_to_target_frame",
133
+ },
134
+ )
135
+ def read_extrinsics(
136
+ path: Union[str, BinaryIO],
137
+ meta: dict[str, Any] | None = None,
138
+ *,
139
+ attributes: dict[str, Any] | None = None,
140
+ ) -> torch.Tensor:
141
+ """Load the default ``lidar_hdl64_s3_roof`` to left-camera transform."""
142
+ return torch.from_numpy(load_extrinsics_array(path, meta, attributes)).contiguous()
@@ -0,0 +1,12 @@
1
+ """Loader functions for the Princeton DENSE / SeeingThroughFog dataset.
2
+
3
+ This module re-exports the GPU-oriented loaders from
4
+ :mod:`euler_loading.loaders.gpu.princeton_dense` for convenience. For
5
+ explicit control, import from :mod:`~euler_loading.loaders.gpu` or
6
+ :mod:`~euler_loading.loaders.cpu` directly::
7
+
8
+ from euler_loading.loaders.gpu import princeton_dense # torch tensors
9
+ from euler_loading.loaders.cpu import princeton_dense # numpy arrays
10
+ """
11
+
12
+ from euler_loading.loaders.gpu.princeton_dense import * # noqa: F401,F403
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "loading-multi-modal",
2
+ "name": "loading-dense",
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.12.0"
3
+ version = "2.14.0"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  dependencies = [
@@ -11,19 +11,23 @@ import pytest
11
11
  import torch
12
12
  from PIL import Image
13
13
 
14
+ from euler_loading import resolve_loader_module
14
15
  from euler_loading.loaders import vkitti2
15
16
  from euler_loading.loaders import generic as generic_top
16
17
  from euler_loading.loaders import muses as muses_top
18
+ from euler_loading.loaders import princeton_dense as princeton_dense_top
17
19
  from euler_loading.loaders.gpu import vkitti2 as gpu_vkitti2
18
20
  from euler_loading.loaders.gpu import real_drive_sim as gpu_rds
19
21
  from euler_loading.loaders.gpu import generic as gpu_generic
20
22
  from euler_loading.loaders.gpu import generic_dense_depth as gpu_generic_dense_depth
21
23
  from euler_loading.loaders.gpu import muses as gpu_muses
24
+ from euler_loading.loaders.gpu import princeton_dense as gpu_princeton_dense
22
25
  from euler_loading.loaders.cpu import vkitti2 as cpu_vkitti2
23
26
  from euler_loading.loaders.cpu import real_drive_sim as cpu_rds
24
27
  from euler_loading.loaders.cpu import generic as cpu_generic
25
28
  from euler_loading.loaders.cpu import generic_dense_depth as cpu_generic_dense_depth
26
29
  from euler_loading.loaders.cpu import muses as cpu_muses
30
+ from euler_loading.loaders.cpu import princeton_dense as cpu_princeton_dense
27
31
 
28
32
  # ---------------------------------------------------------------------------
29
33
  # Shared fixtures
@@ -149,6 +153,214 @@ class TestGenericDenseDepthAttributes:
149
153
  np.testing.assert_allclose(result, expected)
150
154
 
151
155
 
156
+ # ---------------------------------------------------------------------------
157
+ # Princeton DENSE / SeeingThroughFog loader smoke tests
158
+ # ---------------------------------------------------------------------------
159
+
160
+ PRINCETON_DENSE_LOADER_NAMES = [
161
+ "rgb",
162
+ "sparse_depth",
163
+ "read_intrinsics",
164
+ "read_extrinsics",
165
+ ]
166
+
167
+
168
+ @pytest.fixture()
169
+ def princeton_dense_rgb_path(tmp_path):
170
+ arr = np.full((4, 4), 2048, dtype=np.uint16)
171
+ path = tmp_path / "2018-02-05_12-09-01_00000.tiff"
172
+ Image.fromarray(arr).save(path)
173
+ return str(path)
174
+
175
+
176
+ @pytest.fixture()
177
+ def princeton_dense_sparse_depth_path(tmp_path):
178
+ arr = np.array(
179
+ [
180
+ [1.0, 2.0, 3.0, 128.0, 12.0],
181
+ [4.0, 5.0, 6.0, 255.0, 63.0],
182
+ ],
183
+ dtype=np.float32,
184
+ )
185
+ path = tmp_path / "2018-02-06_15-48-12_00200.bin"
186
+ arr.tofile(path)
187
+ return str(path), arr
188
+
189
+
190
+ @pytest.fixture()
191
+ def princeton_dense_intrinsics_path(tmp_path):
192
+ data = {
193
+ "K": [2612.86, 0.0, 966.525, 0.0, 2612.86, 508.297, 0.0, 0.0, 1.0],
194
+ "D": [-0.567575, 0.0, 0.0, 0.0, 0.0],
195
+ "R": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
196
+ "P": [2355.722801, 0.0, 988.138054, 0.0, 0.0, 2355.722801, 508.051838, 0.0, 0.0, 0.0, 1.0, 0.0],
197
+ "width": 1920,
198
+ "height": 1024,
199
+ "header": {"frame_id": "cam_stereo_left_optical"},
200
+ }
201
+ path = tmp_path / "calib_cam_stereo_left.json"
202
+ path.write_text(json.dumps(data))
203
+ return str(path), np.asarray(data["K"], dtype=np.float32).reshape(3, 3)
204
+
205
+
206
+ @pytest.fixture()
207
+ def princeton_dense_tf_tree_path(tmp_path):
208
+ data = [
209
+ {
210
+ "header": {"frame_id": "body"},
211
+ "child_frame_id": "lidar_hdl64_s3_roof",
212
+ "transform": {
213
+ "translation": {"x": 1.0, "y": 0.0, "z": 0.0},
214
+ "rotation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
215
+ },
216
+ },
217
+ {
218
+ "header": {"frame_id": "body"},
219
+ "child_frame_id": "cam_stereo_left_optical",
220
+ "transform": {
221
+ "translation": {"x": 0.0, "y": 2.0, "z": 0.0},
222
+ "rotation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
223
+ },
224
+ },
225
+ ]
226
+ path = tmp_path / "calib_tf_tree_full.json"
227
+ path.write_text(json.dumps(data))
228
+ expected = np.asarray(
229
+ [
230
+ [1.0, 0.0, 0.0, 1.0],
231
+ [0.0, 1.0, 0.0, -2.0],
232
+ [0.0, 0.0, 1.0, 0.0],
233
+ [0.0, 0.0, 0.0, 1.0],
234
+ ],
235
+ dtype=np.float32,
236
+ )
237
+ return str(path), expected
238
+
239
+
240
+ class TestPrincetonDenseModuleContents:
241
+ """The Princeton DENSE module exposes the expected loader functions."""
242
+
243
+ @pytest.mark.parametrize("name", PRINCETON_DENSE_LOADER_NAMES)
244
+ def test_top_level_has_callable(self, name):
245
+ assert callable(getattr(princeton_dense_top, name))
246
+
247
+ @pytest.mark.parametrize("name", PRINCETON_DENSE_LOADER_NAMES)
248
+ def test_gpu_has_callable(self, name):
249
+ assert callable(getattr(gpu_princeton_dense, name))
250
+
251
+ @pytest.mark.parametrize("name", PRINCETON_DENSE_LOADER_NAMES)
252
+ def test_cpu_has_callable(self, name):
253
+ assert callable(getattr(cpu_princeton_dense, name))
254
+
255
+ def test_auto_resolution_uses_gpu_module(self):
256
+ module = resolve_loader_module("princeton_dense")
257
+ assert module.__name__ == "euler_loading.loaders.gpu.princeton_dense"
258
+
259
+ def test_auto_resolution_supports_dataset_alias(self):
260
+ module = resolve_loader_module("seeing_through_fog")
261
+ assert module.__name__ == "euler_loading.loaders.gpu.princeton_dense"
262
+
263
+
264
+ class TestPrincetonDenseGPULoaders:
265
+ """GPU Princeton DENSE loaders produce torch tensors."""
266
+
267
+ def test_rgb_shape_dtype_and_range(self, princeton_dense_rgb_path):
268
+ result = gpu_princeton_dense.rgb(princeton_dense_rgb_path)
269
+ assert isinstance(result, torch.Tensor)
270
+ assert result.dtype == torch.float32
271
+ assert result.shape == (3, 4, 4)
272
+ assert result.min() >= 0.0
273
+ assert result.max() <= 1.0
274
+
275
+ def test_sparse_depth_preserves_float32_columns(self, princeton_dense_sparse_depth_path):
276
+ path, expected = princeton_dense_sparse_depth_path
277
+ result = gpu_princeton_dense.sparse_depth(path)
278
+ assert result.dtype == torch.float32
279
+ assert result.shape == (2, 5)
280
+ assert torch.equal(result, torch.from_numpy(expected))
281
+
282
+ def test_read_intrinsics_returns_camera_k(self, princeton_dense_intrinsics_path):
283
+ path, expected = princeton_dense_intrinsics_path
284
+ result = gpu_princeton_dense.read_intrinsics(path)
285
+ assert result.dtype == torch.float32
286
+ assert result.shape == (3, 3)
287
+ assert torch.equal(result, torch.from_numpy(expected))
288
+
289
+ def test_read_extrinsics_maps_hdl64_lidar_to_camera(self, princeton_dense_tf_tree_path):
290
+ path, expected = princeton_dense_tf_tree_path
291
+ result = gpu_princeton_dense.read_extrinsics(path)
292
+ assert result.dtype == torch.float32
293
+ assert result.shape == (4, 4)
294
+ assert torch.equal(result, torch.from_numpy(expected))
295
+
296
+
297
+ class TestPrincetonDenseCPULoaders:
298
+ """CPU Princeton DENSE loaders produce numpy arrays."""
299
+
300
+ def test_rgb_shape_dtype_and_range(self, princeton_dense_rgb_path):
301
+ result = cpu_princeton_dense.rgb(princeton_dense_rgb_path)
302
+ assert isinstance(result, np.ndarray)
303
+ assert result.dtype == np.float32
304
+ assert result.shape == (4, 4, 3)
305
+ assert result.min() >= 0.0
306
+ assert result.max() <= 1.0
307
+
308
+ def test_sparse_depth_preserves_float32_columns(self, princeton_dense_sparse_depth_path):
309
+ path, expected = princeton_dense_sparse_depth_path
310
+ result = cpu_princeton_dense.sparse_depth(path)
311
+ assert result.dtype == np.float32
312
+ assert result.shape == (2, 5)
313
+ assert np.array_equal(result, expected)
314
+
315
+ def test_sparse_depth_supports_binary_streams(self, princeton_dense_sparse_depth_path):
316
+ _, expected = princeton_dense_sparse_depth_path
317
+ stream = io.BytesIO(expected.tobytes())
318
+ result = cpu_princeton_dense.sparse_depth(stream)
319
+ assert np.array_equal(result, expected)
320
+
321
+ def test_read_intrinsics_returns_camera_k(self, princeton_dense_intrinsics_path):
322
+ path, expected = princeton_dense_intrinsics_path
323
+ result = cpu_princeton_dense.read_intrinsics(path)
324
+ assert result.dtype == np.float32
325
+ assert result.shape == (3, 3)
326
+ assert np.array_equal(result, expected)
327
+
328
+ def test_read_extrinsics_maps_hdl64_lidar_to_camera(self, princeton_dense_tf_tree_path):
329
+ path, expected = princeton_dense_tf_tree_path
330
+ result = cpu_princeton_dense.read_extrinsics(path)
331
+ assert result.dtype == np.float32
332
+ assert result.shape == (4, 4)
333
+ assert np.array_equal(result, expected)
334
+
335
+ def test_read_extrinsics_supports_frame_overrides(self, princeton_dense_tf_tree_path):
336
+ path, expected_lidar_to_camera = princeton_dense_tf_tree_path
337
+ result = cpu_princeton_dense.read_extrinsics(
338
+ path,
339
+ attributes={
340
+ "source_frame": "cam_stereo_left_optical",
341
+ "target_frame": "lidar_hdl64_s3_roof",
342
+ },
343
+ )
344
+ assert np.allclose(result, np.linalg.inv(expected_lidar_to_camera))
345
+
346
+
347
+ class TestPrincetonDenseBackwardCompat:
348
+ """``from euler_loading.loaders import princeton_dense`` returns GPU loaders."""
349
+
350
+ def test_top_level_rgb_matches_gpu(self, princeton_dense_rgb_path):
351
+ assert torch.equal(
352
+ princeton_dense_top.rgb(princeton_dense_rgb_path),
353
+ gpu_princeton_dense.rgb(princeton_dense_rgb_path),
354
+ )
355
+
356
+ def test_top_level_sparse_depth_matches_gpu(self, princeton_dense_sparse_depth_path):
357
+ path, _ = princeton_dense_sparse_depth_path
358
+ assert torch.equal(
359
+ princeton_dense_top.sparse_depth(path),
360
+ gpu_princeton_dense.sparse_depth(path),
361
+ )
362
+
363
+
152
364
  # ---------------------------------------------------------------------------
153
365
  # MUSES loader smoke tests
154
366
  # ---------------------------------------------------------------------------