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.
- {euler_loading-2.12.0 → euler_loading-2.17.0}/PKG-INFO +1 -1
- {euler_loading-2.12.0 → euler_loading-2.17.0}/README.md +19 -2
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_resolution.py +2 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/__init__.py +4 -0
- euler_loading-2.17.0/euler_loading/loaders/_princeton_dense.py +314 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/__init__.py +2 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/generic.py +33 -0
- euler_loading-2.17.0/euler_loading/loaders/cpu/princeton_dense.py +160 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generate/loaders.json +149 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/__init__.py +2 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/generic.py +31 -0
- euler_loading-2.17.0/euler_loading/loaders/gpu/princeton_dense.py +163 -0
- euler_loading-2.17.0/euler_loading/loaders/princeton_dense.py +12 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/package-lock.json +1 -1
- {euler_loading-2.12.0 → euler_loading-2.17.0}/pyproject.toml +1 -1
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_loaders.py +325 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/.github/workflows/workflow.yml +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/.gitignore +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/docs/loader-attributes.md +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/__init__.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_dataset_contract.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_ds_crawler_utils.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_metadata.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/_writing.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/dataset.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/indexing.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/_annotations.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/_writer_utils.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/contracts.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/muses.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generate/__init__.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generate/__main__.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/generic.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/muses.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/muses.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/real_drive_sim.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/loaders/vkitti2.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/euler_loading/preprocessing.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/example.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/sample_rds.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/__init__.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/conftest.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/example_rds_calib.json +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_dataset.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_id_schema.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_indexing.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_preprocessing.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_real_dataset.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/tests/test_writing.py +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/vkitti_cpu_example_output.json +0 -0
- {euler_loading-2.12.0 → euler_loading-2.17.0}/vkitti_gpu_example_output.json +0 -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
|
-
|
|
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)
|