euler-eval 2.16.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 (66) hide show
  1. {euler_eval-2.16.0 → euler_eval-2.17.0}/PKG-INFO +72 -1
  2. {euler_eval-2.16.0 → euler_eval-2.17.0}/README.md +71 -0
  3. euler_eval-2.17.0/euler_eval/__init__.py +65 -0
  4. euler_eval-2.17.0/euler_eval/calibration.py +258 -0
  5. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/evaluate.py +8 -221
  6. euler_eval-2.17.0/euler_eval/validation.py +593 -0
  7. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval.egg-info/PKG-INFO +72 -1
  8. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval.egg-info/SOURCES.txt +4 -1
  9. {euler_eval-2.16.0 → euler_eval-2.17.0}/pyproject.toml +1 -1
  10. euler_eval-2.17.0/tests/test_validation.py +417 -0
  11. euler_eval-2.16.0/euler_eval/__init__.py +0 -1
  12. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/cli.py +0 -0
  13. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/config_paths.py +0 -0
  14. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/data.py +0 -0
  15. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/__init__.py +0 -0
  16. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/absrel.py +0 -0
  17. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/daniel_error.py +0 -0
  18. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  19. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  20. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/depth_standard.py +0 -0
  21. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/fid_kid.py +0 -0
  22. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  23. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  24. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  25. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/lpips_metric.py +0 -0
  26. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/normal_consistency.py +0 -0
  27. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/points3d_cloud.py +0 -0
  28. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/points3d_decomposition.py +0 -0
  29. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/points3d_distance.py +0 -0
  30. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/points3d_geometry.py +0 -0
  31. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/psnr.py +0 -0
  32. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  33. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  34. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  35. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/rho_a.py +0 -0
  36. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/rmse.py +0 -0
  37. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  38. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/ssim.py +0 -0
  39. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/tail_errors.py +0 -0
  40. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/metrics/utils.py +0 -0
  41. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/sanity_checker.py +0 -0
  42. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  43. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  44. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval.egg-info/entry_points.txt +0 -0
  45. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval.egg-info/requires.txt +0 -0
  46. {euler_eval-2.16.0 → euler_eval-2.17.0}/euler_eval.egg-info/top_level.txt +0 -0
  47. {euler_eval-2.16.0 → euler_eval-2.17.0}/init_cache.py +0 -0
  48. {euler_eval-2.16.0 → euler_eval-2.17.0}/setup.cfg +0 -0
  49. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_alignment.py +0 -0
  50. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_cli_device.py +0 -0
  51. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_config.py +0 -0
  52. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_data.py +0 -0
  53. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_data_builders.py +0 -0
  54. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_depth_alignment_output.py +0 -0
  55. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_depth_standard.py +0 -0
  56. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_evaluate_helpers.py +0 -0
  57. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_init_cache.py +0 -0
  58. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_integration.py +0 -0
  59. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_meta_output.py +0 -0
  60. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_metrics_utils.py +0 -0
  61. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_points_3d.py +0 -0
  62. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_points_3d_sparse.py +0 -0
  63. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_rgb_fid_output.py +0 -0
  64. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_rho_a.py +0 -0
  65. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_save_results.py +0 -0
  66. {euler_eval-2.16.0 → euler_eval-2.17.0}/tests/test_sparse_depth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.16.0
3
+ Version: 2.17.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -221,6 +221,77 @@ The interval bounds are `near=[MIN, near_max)`, `mid=[near_max, mid_max)`, and
221
221
  | `mid` | `[9.290856529, 35.954189863)` |
222
222
  | `far` | `[35.954189863, 80.0]` |
223
223
 
224
+ ## Programmatic use (in-training validation)
225
+
226
+ Besides the file-based CLI, `euler_eval.validation` exposes the same depth
227
+ metric semantics for **in-memory predictions** — e.g. scoring a training
228
+ run's validation pass against dense *or sparse* GT without writing
229
+ predictions to disk:
230
+
231
+ ```python
232
+ from euler_eval import (
233
+ DepthValidationAggregator,
234
+ build_validation_gt_dataset,
235
+ evaluate_dense_depth_sample,
236
+ evaluate_sparse_depth_sample,
237
+ get_sample_intrinsics,
238
+ get_sample_pointcloud_to_camera_extrinsics,
239
+ summarize_reduced_state,
240
+ )
241
+
242
+ # GT-only dataset from euler-loading compatible paths (.ds_crawler indices,
243
+ # inline :split#scope= selectors supported).
244
+ dataset = build_validation_gt_dataset(
245
+ sparse_depth_path="/data/muses.zip:val#scope=lidar",
246
+ rgb_path="/data/muses.zip:val#scope=rgb",
247
+ intrinsics_path="/data/muses.zip:val#scope=intrinsics",
248
+ camera_extrinsics_path="/data/muses.zip:val#scope=extrinsics",
249
+ )
250
+
251
+ aggregator = DepthValidationAggregator()
252
+ for i in range(len(dataset)):
253
+ sample = dataset[i]
254
+ depth_pred = my_model(sample["rgb"]) # (H, W) metres, planar z
255
+ K = get_sample_intrinsics(sample)
256
+ lidar2cam, _ = get_sample_pointcloud_to_camera_extrinsics(sample)
257
+ aggregator.update(
258
+ evaluate_sparse_depth_sample(
259
+ depth_pred,
260
+ sample["sparse_depth"], # (N, C>=3) lidar points
261
+ K,
262
+ lidar2cam,
263
+ alignment="none", # "affine" for relative depth
264
+ )
265
+ )
266
+
267
+ summary = aggregator.summary()
268
+ print(summary["standard"]["image_mean"]["absrel"])
269
+ ```
270
+
271
+ Key pieces:
272
+
273
+ - `evaluate_dense_depth_sample(pred, gt, valid_mask=None, alignment=...,
274
+ min_depth=..., max_depth=...)` — one dense prediction vs a dense GT map.
275
+ GT at another resolution is aligned to the prediction plane; returns the
276
+ standard metric set (`absrel`, `sqrel`, `mae`, `rmse`, `rmse_log`, `log10`,
277
+ `silog`, `delta1-3`) plus pooled pixel statistics, or `None` when too few
278
+ valid pixels remain.
279
+ - `evaluate_sparse_depth_sample(pred, point_cloud, intrinsics,
280
+ camera_extrinsics, lidar_extrinsics=None, pred_is_radial=False, ...)` —
281
+ projects the sparse GT cloud into the prediction plane (radial depth,
282
+ nearest-z occlusion handling), converts a planar prediction to radial with
283
+ the same intrinsics, and scores only the projected pixels — matching the
284
+ CLI's `gt.sparse_depth` pipeline. Pass the intrinsics of the *actual
285
+ prediction plane* (i.e. adjusted for any crop/resize of the model input).
286
+ - `alignment="none"` scores metric predictions as-is; `"affine"` fits
287
+ least-squares scale+shift first (relative/affine depth models).
288
+ - `DepthValidationAggregator` accumulates per-sample results into the CLI's
289
+ `image_mean` / `image_median` / `pixel_pool` reducers. For multi-process
290
+ validation, sum `aggregator.reduced_state()` vectors across ranks (fixed
291
+ key order via `DepthValidationAggregator.state_keys()`) and rebuild
292
+ mean-based summaries with `summarize_reduced_state(...)`.
293
+ - Inputs accept torch tensors or numpy arrays; all math runs on CPU numpy.
294
+
224
295
  ## Configuration
225
296
 
226
297
  ### `config.json`
@@ -183,6 +183,77 @@ The interval bounds are `near=[MIN, near_max)`, `mid=[near_max, mid_max)`, and
183
183
  | `mid` | `[9.290856529, 35.954189863)` |
184
184
  | `far` | `[35.954189863, 80.0]` |
185
185
 
186
+ ## Programmatic use (in-training validation)
187
+
188
+ Besides the file-based CLI, `euler_eval.validation` exposes the same depth
189
+ metric semantics for **in-memory predictions** — e.g. scoring a training
190
+ run's validation pass against dense *or sparse* GT without writing
191
+ predictions to disk:
192
+
193
+ ```python
194
+ from euler_eval import (
195
+ DepthValidationAggregator,
196
+ build_validation_gt_dataset,
197
+ evaluate_dense_depth_sample,
198
+ evaluate_sparse_depth_sample,
199
+ get_sample_intrinsics,
200
+ get_sample_pointcloud_to_camera_extrinsics,
201
+ summarize_reduced_state,
202
+ )
203
+
204
+ # GT-only dataset from euler-loading compatible paths (.ds_crawler indices,
205
+ # inline :split#scope= selectors supported).
206
+ dataset = build_validation_gt_dataset(
207
+ sparse_depth_path="/data/muses.zip:val#scope=lidar",
208
+ rgb_path="/data/muses.zip:val#scope=rgb",
209
+ intrinsics_path="/data/muses.zip:val#scope=intrinsics",
210
+ camera_extrinsics_path="/data/muses.zip:val#scope=extrinsics",
211
+ )
212
+
213
+ aggregator = DepthValidationAggregator()
214
+ for i in range(len(dataset)):
215
+ sample = dataset[i]
216
+ depth_pred = my_model(sample["rgb"]) # (H, W) metres, planar z
217
+ K = get_sample_intrinsics(sample)
218
+ lidar2cam, _ = get_sample_pointcloud_to_camera_extrinsics(sample)
219
+ aggregator.update(
220
+ evaluate_sparse_depth_sample(
221
+ depth_pred,
222
+ sample["sparse_depth"], # (N, C>=3) lidar points
223
+ K,
224
+ lidar2cam,
225
+ alignment="none", # "affine" for relative depth
226
+ )
227
+ )
228
+
229
+ summary = aggregator.summary()
230
+ print(summary["standard"]["image_mean"]["absrel"])
231
+ ```
232
+
233
+ Key pieces:
234
+
235
+ - `evaluate_dense_depth_sample(pred, gt, valid_mask=None, alignment=...,
236
+ min_depth=..., max_depth=...)` — one dense prediction vs a dense GT map.
237
+ GT at another resolution is aligned to the prediction plane; returns the
238
+ standard metric set (`absrel`, `sqrel`, `mae`, `rmse`, `rmse_log`, `log10`,
239
+ `silog`, `delta1-3`) plus pooled pixel statistics, or `None` when too few
240
+ valid pixels remain.
241
+ - `evaluate_sparse_depth_sample(pred, point_cloud, intrinsics,
242
+ camera_extrinsics, lidar_extrinsics=None, pred_is_radial=False, ...)` —
243
+ projects the sparse GT cloud into the prediction plane (radial depth,
244
+ nearest-z occlusion handling), converts a planar prediction to radial with
245
+ the same intrinsics, and scores only the projected pixels — matching the
246
+ CLI's `gt.sparse_depth` pipeline. Pass the intrinsics of the *actual
247
+ prediction plane* (i.e. adjusted for any crop/resize of the model input).
248
+ - `alignment="none"` scores metric predictions as-is; `"affine"` fits
249
+ least-squares scale+shift first (relative/affine depth models).
250
+ - `DepthValidationAggregator` accumulates per-sample results into the CLI's
251
+ `image_mean` / `image_median` / `pixel_pool` reducers. For multi-process
252
+ validation, sum `aggregator.reduced_state()` vectors across ranks (fixed
253
+ key order via `DepthValidationAggregator.state_keys()`) and rebuild
254
+ mean-based summaries with `summarize_reduced_state(...)`.
255
+ - Inputs accept torch tensors or numpy arrays; all math runs on CPU numpy.
256
+
186
257
  ## Configuration
187
258
 
188
259
  ### `config.json`
@@ -0,0 +1,65 @@
1
+ """Depth evaluation package.
2
+
3
+ Public programmatic API (lazily imported so that ``import euler_eval`` stays
4
+ cheap; heavy metric backends only load on first use):
5
+
6
+ * :mod:`euler_eval.validation` — in-training/programmatic depth validation
7
+ (``evaluate_dense_depth_sample``, ``evaluate_sparse_depth_sample``,
8
+ ``DepthValidationAggregator``, ``build_validation_gt_dataset``).
9
+ * :mod:`euler_eval.calibration` — calibration extraction from euler-loading
10
+ samples.
11
+ * :mod:`euler_eval.metrics` — pure-array metric functions.
12
+ * :mod:`euler_eval.data` — dataset builders, converters, projection, and
13
+ alignment helpers.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ _VALIDATION_EXPORTS = (
21
+ "DepthSampleEvaluation",
22
+ "DepthValidationAggregator",
23
+ "VALIDATION_ALIGNMENT_MODES",
24
+ "build_validation_gt_dataset",
25
+ "evaluate_dense_depth_sample",
26
+ "evaluate_sparse_depth_sample",
27
+ "summarize_reduced_state",
28
+ )
29
+ _CALIBRATION_EXPORTS = (
30
+ "get_sample_intrinsics",
31
+ "get_sample_pointcloud_to_camera_extrinsics",
32
+ )
33
+
34
+ __all__ = [*_VALIDATION_EXPORTS, *_CALIBRATION_EXPORTS]
35
+
36
+ if TYPE_CHECKING: # pragma: no cover - static import surface for type checkers
37
+ from .calibration import ( # noqa: F401
38
+ get_sample_intrinsics,
39
+ get_sample_pointcloud_to_camera_extrinsics,
40
+ )
41
+ from .validation import ( # noqa: F401
42
+ VALIDATION_ALIGNMENT_MODES,
43
+ DepthSampleEvaluation,
44
+ DepthValidationAggregator,
45
+ build_validation_gt_dataset,
46
+ evaluate_dense_depth_sample,
47
+ evaluate_sparse_depth_sample,
48
+ summarize_reduced_state,
49
+ )
50
+
51
+
52
+ def __getattr__(name: str):
53
+ if name in _VALIDATION_EXPORTS:
54
+ from . import validation
55
+
56
+ return getattr(validation, name)
57
+ if name in _CALIBRATION_EXPORTS:
58
+ from . import calibration
59
+
60
+ return getattr(calibration, name)
61
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
62
+
63
+
64
+ def __dir__() -> list[str]:
65
+ return sorted(set(globals()) | set(__all__))
@@ -0,0 +1,258 @@
1
+ """Camera-calibration extraction from euler-loading samples.
2
+
3
+ MultiModalDataset samples carry calibration either directly (a ``(3, 3)`` /
4
+ ``(4, 4)`` tensor per sample) or wrapped in hierarchical-modality dicts and
5
+ nested calibration payloads (e.g. a MUSES ``calib.json`` with named
6
+ ``lidar2rgb`` transforms). This module owns the tolerant extraction logic
7
+ shared by the CLI evaluators (:mod:`euler_eval.evaluate`) and the
8
+ programmatic validation interface (:mod:`euler_eval.validation`).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Mapping
14
+ from typing import Optional
15
+
16
+ import numpy as np
17
+
18
+ from .data import (
19
+ compose_sensor_to_camera_extrinsics,
20
+ to_numpy_extrinsics,
21
+ to_numpy_intrinsics,
22
+ )
23
+
24
+ __all__ = [
25
+ "get_first_hierarchical_value",
26
+ "iter_hierarchical_values",
27
+ "get_sample_intrinsics",
28
+ "get_sample_pointcloud_to_camera_extrinsics",
29
+ "extract_wrapped_extrinsics",
30
+ "extract_named_extrinsics",
31
+ "extract_direct_source_to_camera",
32
+ "DIRECT_SOURCE_TO_CAMERA_KEYS",
33
+ "SOURCE_SENSOR_POSE_KEYS",
34
+ "CAMERA_SENSOR_POSE_KEYS",
35
+ "MATRIX_WRAPPER_KEYS",
36
+ ]
37
+
38
+
39
+ def get_first_hierarchical_value(sample: dict, key: str):
40
+ """Return ``sample[key]``, unwrapping hierarchical ``{file_id: value}`` dicts."""
41
+ data = sample.get(key)
42
+ if data is None:
43
+ return None
44
+ if isinstance(data, dict):
45
+ if not data:
46
+ return None
47
+ return next(iter(data.values()))
48
+ return data
49
+
50
+
51
+ def iter_hierarchical_values(sample: dict, key: str) -> list:
52
+ """Return candidate values for ``sample[key]`` (dict itself plus entries)."""
53
+ data = sample.get(key)
54
+ if data is None:
55
+ return []
56
+ if isinstance(data, dict):
57
+ return [data, *data.values()]
58
+ return [data]
59
+
60
+
61
+ def get_sample_intrinsics(sample: dict) -> Optional[np.ndarray]:
62
+ """Extract a ``(3, 3)`` intrinsics matrix from sample calibration data."""
63
+ K_data = get_first_hierarchical_value(sample, "intrinsics")
64
+ if K_data is None:
65
+ K_data = get_first_hierarchical_value(sample, "calibration")
66
+ if K_data is None:
67
+ return None
68
+ return to_numpy_intrinsics(K_data)
69
+
70
+
71
+ DIRECT_SOURCE_TO_CAMERA_KEYS = (
72
+ "lidar2rgb",
73
+ "lidar2camera",
74
+ "lidar2cam",
75
+ "lidar_to_rgb",
76
+ "lidar_to_camera",
77
+ "lidar_to_cam",
78
+ "source2rgb",
79
+ "source2camera",
80
+ "source_to_rgb",
81
+ "source_to_camera",
82
+ "sensor2rgb",
83
+ "sensor2camera",
84
+ "sensor_to_rgb",
85
+ "sensor_to_camera",
86
+ )
87
+ SOURCE_SENSOR_POSE_KEYS = (
88
+ "lidar",
89
+ "lidar_pose",
90
+ "lidar_extrinsics",
91
+ "source",
92
+ "source_sensor",
93
+ "source_extrinsics",
94
+ "sensor",
95
+ "sensor_extrinsics",
96
+ "lidar2gnss",
97
+ "lidar2ego",
98
+ "lidar2vehicle",
99
+ "lidar2world",
100
+ "lidar_to_gnss",
101
+ "lidar_to_ego",
102
+ "lidar_to_vehicle",
103
+ "lidar_to_world",
104
+ )
105
+ CAMERA_SENSOR_POSE_KEYS = (
106
+ "rgb",
107
+ "camera",
108
+ "cam",
109
+ "frame_camera",
110
+ "rgb_pose",
111
+ "camera_pose",
112
+ "camera_extrinsics",
113
+ "target",
114
+ "target_sensor",
115
+ "target_extrinsics",
116
+ "rgb2gnss",
117
+ "camera2gnss",
118
+ "cam2gnss",
119
+ "frame_camera2gnss",
120
+ "rgb2ego",
121
+ "camera2ego",
122
+ "cam2ego",
123
+ "frame_camera2ego",
124
+ "rgb2vehicle",
125
+ "camera2vehicle",
126
+ "cam2vehicle",
127
+ "frame_camera2vehicle",
128
+ "rgb2world",
129
+ "camera2world",
130
+ "cam2world",
131
+ "frame_camera2world",
132
+ )
133
+ MATRIX_WRAPPER_KEYS = ("T", "transform", "matrix", "extrinsics", "pose")
134
+
135
+
136
+ def _as_extrinsics_or_none(data) -> Optional[np.ndarray]:
137
+ try:
138
+ return to_numpy_extrinsics(data)
139
+ except (TypeError, ValueError):
140
+ return None
141
+
142
+
143
+ def extract_wrapped_extrinsics(data) -> Optional[np.ndarray]:
144
+ """Extract a ``(4, 4)`` transform from a value or common wrapper mappings."""
145
+ matrix = _as_extrinsics_or_none(data)
146
+ if matrix is not None:
147
+ return matrix
148
+
149
+ if not isinstance(data, Mapping):
150
+ return None
151
+
152
+ for key in MATRIX_WRAPPER_KEYS:
153
+ if key in data:
154
+ matrix = extract_wrapped_extrinsics(data[key])
155
+ if matrix is not None:
156
+ return matrix
157
+
158
+ if len(data) == 1:
159
+ return extract_wrapped_extrinsics(next(iter(data.values())))
160
+
161
+ return None
162
+
163
+
164
+ def extract_named_extrinsics(data, names: tuple[str, ...]) -> Optional[np.ndarray]:
165
+ """Extract a transform stored under one of ``names`` in a calibration payload."""
166
+ if not isinstance(data, Mapping):
167
+ return extract_wrapped_extrinsics(data)
168
+
169
+ for name in names:
170
+ if name in data:
171
+ matrix = extract_wrapped_extrinsics(data[name])
172
+ if matrix is not None:
173
+ return matrix
174
+
175
+ for key in ("extrinsics", "sensors", "calibration", "poses"):
176
+ if key in data and isinstance(data[key], Mapping):
177
+ matrix = extract_named_extrinsics(data[key], names)
178
+ if matrix is not None:
179
+ return matrix
180
+
181
+ if len(data) == 1:
182
+ return extract_named_extrinsics(next(iter(data.values())), names)
183
+
184
+ return None
185
+
186
+
187
+ def extract_direct_source_to_camera(data) -> Optional[np.ndarray]:
188
+ """Extract a direct source→camera transform (e.g. MUSES ``lidar2rgb``)."""
189
+ matrix = _as_extrinsics_or_none(data)
190
+ if matrix is not None:
191
+ return matrix
192
+
193
+ if not isinstance(data, Mapping):
194
+ return None
195
+
196
+ for key in DIRECT_SOURCE_TO_CAMERA_KEYS:
197
+ if key in data:
198
+ matrix = extract_wrapped_extrinsics(data[key])
199
+ if matrix is not None:
200
+ return matrix
201
+
202
+ for key in ("extrinsics", "calibration"):
203
+ if key in data and isinstance(data[key], Mapping):
204
+ matrix = extract_direct_source_to_camera(data[key])
205
+ if matrix is not None:
206
+ return matrix
207
+
208
+ if len(data) == 1:
209
+ return extract_direct_source_to_camera(next(iter(data.values())))
210
+
211
+ return None
212
+
213
+
214
+ def get_sample_pointcloud_to_camera_extrinsics(
215
+ sample: dict,
216
+ ) -> tuple[Optional[np.ndarray], Optional[str]]:
217
+ """Extract or compose the transform used for sparse point projection.
218
+
219
+ The common MUSES path supplies a direct ``lidar2rgb`` transform via
220
+ ``camera_extrinsics``. Some datasets instead expose separate lidar and
221
+ camera sensor poses in a shared frame; in that case we compose them to the
222
+ same source-to-camera transform expected by the projector.
223
+ """
224
+ lidar_pose = None
225
+ for value in iter_hierarchical_values(sample, "lidar_extrinsics"):
226
+ lidar_pose = extract_named_extrinsics(value, SOURCE_SENSOR_POSE_KEYS)
227
+ if lidar_pose is not None:
228
+ break
229
+
230
+ camera_values = iter_hierarchical_values(sample, "camera_extrinsics")
231
+ if not camera_values:
232
+ camera_values = iter_hierarchical_values(sample, "extrinsics")
233
+
234
+ if lidar_pose is not None:
235
+ for value in camera_values:
236
+ camera_pose = extract_named_extrinsics(value, CAMERA_SENSOR_POSE_KEYS)
237
+ if camera_pose is not None:
238
+ return (
239
+ compose_sensor_to_camera_extrinsics(lidar_pose, camera_pose),
240
+ "composed_lidar_and_camera_sensor_poses",
241
+ )
242
+ return None, None
243
+
244
+ for value in camera_values:
245
+ direct = extract_direct_source_to_camera(value)
246
+ if direct is not None:
247
+ return direct, "direct_source_to_camera"
248
+
249
+ for value in camera_values:
250
+ lidar_pose = extract_named_extrinsics(value, SOURCE_SENSOR_POSE_KEYS)
251
+ camera_pose = extract_named_extrinsics(value, CAMERA_SENSOR_POSE_KEYS)
252
+ if lidar_pose is not None and camera_pose is not None:
253
+ return (
254
+ compose_sensor_to_camera_extrinsics(lidar_pose, camera_pose),
255
+ "composed_lidar_and_camera_sensor_poses",
256
+ )
257
+
258
+ return None, None