euler-eval 2.12.0__tar.gz → 2.13.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 (56) hide show
  1. {euler_eval-2.12.0 → euler_eval-2.13.0}/PKG-INFO +11 -4
  2. {euler_eval-2.12.0 → euler_eval-2.13.0}/README.md +10 -3
  3. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/cli.py +45 -5
  4. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/data.py +68 -10
  5. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/evaluate.py +213 -11
  6. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval.egg-info/PKG-INFO +11 -4
  7. {euler_eval-2.12.0 → euler_eval-2.13.0}/pyproject.toml +1 -1
  8. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_config.py +52 -1
  9. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_data_builders.py +85 -1
  10. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_sparse_depth.py +104 -0
  11. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/__init__.py +0 -0
  12. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/config_paths.py +0 -0
  13. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/__init__.py +0 -0
  14. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/absrel.py +0 -0
  15. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/daniel_error.py +0 -0
  16. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  17. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  18. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/depth_standard.py +0 -0
  19. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/fid_kid.py +0 -0
  20. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  21. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  22. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  23. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/lpips_metric.py +0 -0
  24. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/normal_consistency.py +0 -0
  25. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/psnr.py +0 -0
  26. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  27. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  28. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  29. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/rho_a.py +0 -0
  30. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/rmse.py +0 -0
  31. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  32. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/ssim.py +0 -0
  33. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/tail_errors.py +0 -0
  34. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/metrics/utils.py +0 -0
  35. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/sanity_checker.py +0 -0
  36. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  37. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval.egg-info/SOURCES.txt +0 -0
  38. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  39. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval.egg-info/entry_points.txt +0 -0
  40. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval.egg-info/requires.txt +0 -0
  41. {euler_eval-2.12.0 → euler_eval-2.13.0}/euler_eval.egg-info/top_level.txt +0 -0
  42. {euler_eval-2.12.0 → euler_eval-2.13.0}/init_cache.py +0 -0
  43. {euler_eval-2.12.0 → euler_eval-2.13.0}/setup.cfg +0 -0
  44. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_alignment.py +0 -0
  45. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_cli_device.py +0 -0
  46. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_data.py +0 -0
  47. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_depth_alignment_output.py +0 -0
  48. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_depth_standard.py +0 -0
  49. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_evaluate_helpers.py +0 -0
  50. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_init_cache.py +0 -0
  51. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_integration.py +0 -0
  52. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_meta_output.py +0 -0
  53. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_metrics_utils.py +0 -0
  54. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_rgb_fid_output.py +0 -0
  55. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_rho_a.py +0 -0
  56. {euler_eval-2.12.0 → euler_eval-2.13.0}/tests/test_save_results.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.12.0
3
+ Version: 2.13.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -149,7 +149,7 @@ This pre-downloads:
149
149
  # Evaluate with default settings (auto-selects CUDA when available)
150
150
  depth-eval config.json --batch-size 32
151
151
 
152
- # Evaluate with sky masking enabled (requires gt.segmentation in config)
152
+ # Evaluate with sky masking enabled (requires gt.segmentation or gt.semantic_segmentation)
153
153
  depth-eval config.json --mask-sky -v
154
154
 
155
155
  # Skip RGB evaluation, only evaluate depth
@@ -241,7 +241,13 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
241
241
 
242
242
  Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`). euler-loading inline selectors are also accepted in `path`, such as `/data/muses.zip:test` or `/data/muses.zip:test#scope=rgb`.
243
243
 
244
- For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction uses a dense depth-like map under `datasets[].depth`, `datasets[].relative_depth`, or `datasets[].affine_depth`. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels.
244
+ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction uses a dense depth-like map under `datasets[].depth`, `datasets[].relative_depth`, or `datasets[].affine_depth`. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels. For MUSES through `euler_loading.loaders.muses`, `gt.camera_extrinsics` normally resolves to the direct `lidar2rgb` transform and no extra lidar pose is needed. If a dataset exposes separate lidar and camera poses in a shared frame instead, provide optional `gt.lidar_extrinsics`; the evaluator composes `inv(camera_pose) @ lidar_pose` before projection.
245
+
246
+ Sparse depth does not require segmentation GT. `gt.segmentation` and
247
+ `gt.semantic_segmentation` are optional aliases for the same sky-mask source.
248
+ They are loaded only when `--mask-sky` is set; in sparse depth evaluation that
249
+ mask excludes sky pixels from projected-point metrics and from scale/shift
250
+ fitting. Without `--mask-sky`, the segmentation entry is not loaded or used.
245
251
 
246
252
  #### GT section
247
253
 
@@ -251,10 +257,11 @@ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.dep
251
257
  | `gt.depth.path` | no\* | Path to GT depth dataset |
252
258
  | `gt.sparse_depth.path` | no\* | Path to sparse pointcloud GT dataset, e.g. `sparse_depth` with `(N,C)` points whose first columns are `x,y,z` in meters |
253
259
  | `gt.rays.path` | no\* | Path to GT ray direction map dataset (for rays evaluation) |
254
- | `gt.segmentation.path` | no | Path to GT segmentation (needed for `--mask-sky`) |
260
+ | `gt.segmentation.path` / `gt.semantic_segmentation.path` | no | Path to GT segmentation (needed for `--mask-sky`; use one key, not both) |
255
261
  | `gt.calibration.path` | no | Path to calibration data (camera intrinsics matrices) |
256
262
  | `gt.intrinsics.path` | required with `gt.sparse_depth` | Path to camera intrinsics matrices for pointcloud projection |
257
263
  | `gt.camera_extrinsics.path` | required with `gt.sparse_depth` | Path to source-to-camera extrinsics, e.g. MUSES `lidar2rgb`, for pointcloud projection |
264
+ | `gt.lidar_extrinsics.path` | no | Optional lidar sensor pose in the same shared frame as `gt.camera_extrinsics`; when provided, `gt.camera_extrinsics` is interpreted as the camera pose and both are composed for projection |
258
265
  | `gt.name` | no | Display name for ground truth (default: `"GT"`) |
259
266
 
260
267
  \* At least one of `gt.rgb.path`, `gt.depth.path`, `gt.sparse_depth.path`, or `gt.rays.path` is required.
@@ -111,7 +111,7 @@ This pre-downloads:
111
111
  # Evaluate with default settings (auto-selects CUDA when available)
112
112
  depth-eval config.json --batch-size 32
113
113
 
114
- # Evaluate with sky masking enabled (requires gt.segmentation in config)
114
+ # Evaluate with sky masking enabled (requires gt.segmentation or gt.semantic_segmentation)
115
115
  depth-eval config.json --mask-sky -v
116
116
 
117
117
  # Skip RGB evaluation, only evaluate depth
@@ -203,7 +203,13 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
203
203
 
204
204
  Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`). euler-loading inline selectors are also accepted in `path`, such as `/data/muses.zip:test` or `/data/muses.zip:test#scope=rgb`.
205
205
 
206
- For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction uses a dense depth-like map under `datasets[].depth`, `datasets[].relative_depth`, or `datasets[].affine_depth`. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels.
206
+ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction uses a dense depth-like map under `datasets[].depth`, `datasets[].relative_depth`, or `datasets[].affine_depth`. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels. For MUSES through `euler_loading.loaders.muses`, `gt.camera_extrinsics` normally resolves to the direct `lidar2rgb` transform and no extra lidar pose is needed. If a dataset exposes separate lidar and camera poses in a shared frame instead, provide optional `gt.lidar_extrinsics`; the evaluator composes `inv(camera_pose) @ lidar_pose` before projection.
207
+
208
+ Sparse depth does not require segmentation GT. `gt.segmentation` and
209
+ `gt.semantic_segmentation` are optional aliases for the same sky-mask source.
210
+ They are loaded only when `--mask-sky` is set; in sparse depth evaluation that
211
+ mask excludes sky pixels from projected-point metrics and from scale/shift
212
+ fitting. Without `--mask-sky`, the segmentation entry is not loaded or used.
207
213
 
208
214
  #### GT section
209
215
 
@@ -213,10 +219,11 @@ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.dep
213
219
  | `gt.depth.path` | no\* | Path to GT depth dataset |
214
220
  | `gt.sparse_depth.path` | no\* | Path to sparse pointcloud GT dataset, e.g. `sparse_depth` with `(N,C)` points whose first columns are `x,y,z` in meters |
215
221
  | `gt.rays.path` | no\* | Path to GT ray direction map dataset (for rays evaluation) |
216
- | `gt.segmentation.path` | no | Path to GT segmentation (needed for `--mask-sky`) |
222
+ | `gt.segmentation.path` / `gt.semantic_segmentation.path` | no | Path to GT segmentation (needed for `--mask-sky`; use one key, not both) |
217
223
  | `gt.calibration.path` | no | Path to calibration data (camera intrinsics matrices) |
218
224
  | `gt.intrinsics.path` | required with `gt.sparse_depth` | Path to camera intrinsics matrices for pointcloud projection |
219
225
  | `gt.camera_extrinsics.path` | required with `gt.sparse_depth` | Path to source-to-camera extrinsics, e.g. MUSES `lidar2rgb`, for pointcloud projection |
226
+ | `gt.lidar_extrinsics.path` | no | Optional lidar sensor pose in the same shared frame as `gt.camera_extrinsics`; when provided, `gt.camera_extrinsics` is interpreted as the camera pose and both are composed for projection |
220
227
  | `gt.name` | no | Display name for ground truth (default: `"GT"`) |
221
228
 
222
229
  \* At least one of `gt.rgb.path`, `gt.depth.path`, `gt.sparse_depth.path`, or `gt.rays.path` is required.
@@ -152,6 +152,7 @@ def _rgb_eval_axes(*, benchmark: bool = False) -> dict[str, AxisDeclaration]:
152
152
  _RAYS_EVAL_AXES: dict[str, AxisDeclaration] = {}
153
153
 
154
154
  _PRED_DEPTH_KEYS = ("depth", "relative_depth", "affine_depth")
155
+ _GT_SEGMENTATION_KEYS = ("segmentation", "semantic_segmentation")
155
156
 
156
157
  # Downstream eval consumers validate metricSet.metricNamespace with a stricter
157
158
  # first-segment rule than euler_metric_naming's modality validator. Keep the
@@ -592,6 +593,24 @@ def _prediction_depth_space_hint(key: str | None) -> str | None:
592
593
  return None
593
594
 
594
595
 
596
+ def _gt_segmentation_entry(gt: dict) -> tuple[str | None, dict | None]:
597
+ """Return the configured GT segmentation entry, accepting legacy aliases."""
598
+ matches = [
599
+ key
600
+ for key in _GT_SEGMENTATION_KEYS
601
+ if key in gt and "path" in gt.get(key, {})
602
+ ]
603
+ if not matches:
604
+ return None, None
605
+ if len(matches) > 1:
606
+ raise ValueError(
607
+ "gt contains multiple segmentation entries "
608
+ f"{matches}. Use only one of {list(_GT_SEGMENTATION_KEYS)}."
609
+ )
610
+ key = matches[0]
611
+ return key, gt[key]
612
+
613
+
595
614
  def validate_gt_config(gt: dict) -> None:
596
615
  """Validate the ``gt`` section of the configuration.
597
616
 
@@ -619,15 +638,19 @@ def validate_gt_config(gt: dict) -> None:
619
638
  "gt.camera_extrinsics.path for pointcloud projection"
620
639
  )
621
640
 
641
+ _gt_segmentation_entry(gt)
642
+
622
643
  for modality in (
623
644
  "rgb",
624
645
  "depth",
625
646
  "sparse_depth",
626
647
  "rays",
627
648
  "segmentation",
649
+ "semantic_segmentation",
628
650
  "calibration",
629
651
  "intrinsics",
630
652
  "camera_extrinsics",
653
+ "lidar_extrinsics",
631
654
  ):
632
655
  if modality in gt and "path" in gt[modality]:
633
656
  p = normalize_modality_path(
@@ -962,16 +985,18 @@ def main():
962
985
  print("-" * 60)
963
986
 
964
987
  # Check sky masking prerequisites
988
+ segmentation_key, segmentation_entry = _gt_segmentation_entry(config["gt"])
965
989
  if args.mask_sky:
966
- if "segmentation" not in config["gt"]:
990
+ if segmentation_entry is None:
967
991
  print(
968
- "Warning: --mask-sky requires gt.segmentation in config. "
992
+ "Warning: --mask-sky requires gt.segmentation or "
993
+ "gt.semantic_segmentation in config. "
969
994
  "Sky masking disabled.",
970
995
  file=sys.stderr,
971
996
  )
972
997
  args.mask_sky = False
973
998
  else:
974
- print("Sky masking enabled")
999
+ print(f"Sky masking enabled ({segmentation_key})")
975
1000
  print("-" * 60)
976
1001
 
977
1002
  # Initialize sanity checker if not disabled
@@ -1006,8 +1031,11 @@ def main():
1006
1031
  calibration_path = gt.get("calibration", {}).get("path")
1007
1032
  intrinsics_path = gt.get("intrinsics", {}).get("path")
1008
1033
  camera_extrinsics_path = gt.get("camera_extrinsics", {}).get("path")
1034
+ lidar_extrinsics_path = gt.get("lidar_extrinsics", {}).get("path")
1009
1035
  segmentation_path = (
1010
- gt.get("segmentation", {}).get("path") if args.mask_sky else None
1036
+ segmentation_entry.get("path")
1037
+ if args.mask_sky and segmentation_entry is not None
1038
+ else None
1011
1039
  )
1012
1040
  gt_depth_split = gt.get("depth", {}).get("split")
1013
1041
  gt_sparse_depth_split = gt.get("sparse_depth", {}).get("split")
@@ -1016,8 +1044,11 @@ def main():
1016
1044
  calibration_split = gt.get("calibration", {}).get("split")
1017
1045
  intrinsics_split = gt.get("intrinsics", {}).get("split")
1018
1046
  camera_extrinsics_split = gt.get("camera_extrinsics", {}).get("split")
1047
+ lidar_extrinsics_split = gt.get("lidar_extrinsics", {}).get("split")
1019
1048
  segmentation_split = (
1020
- gt.get("segmentation", {}).get("split") if args.mask_sky else None
1049
+ segmentation_entry.get("split")
1050
+ if args.mask_sky and segmentation_entry is not None
1051
+ else None
1021
1052
  )
1022
1053
 
1023
1054
  # Evaluate each prediction dataset
@@ -1063,6 +1094,7 @@ def main():
1063
1094
  pred_depth_metadata_scope=(
1064
1095
  pred_depth_key if pred_depth_key != "depth" else None
1065
1096
  ),
1097
+ segmentation_modality_key=segmentation_key or "segmentation",
1066
1098
  gt_depth_split=gt_depth_split,
1067
1099
  pred_depth_split=pred_depth_split,
1068
1100
  calibration_split=calibration_split,
@@ -1242,20 +1274,25 @@ def main():
1242
1274
  print(f" Pred depth entry: {pred_depth_key}")
1243
1275
  print(f" Intrinsics: {intrinsics_path}")
1244
1276
  print(f" Camera extrinsics: {camera_extrinsics_path}")
1277
+ if lidar_extrinsics_path:
1278
+ print(f" Lidar extrinsics: {lidar_extrinsics_path}")
1245
1279
 
1246
1280
  sparse_depth_dataset = build_sparse_depth_eval_dataset(
1247
1281
  gt_sparse_depth_path=gt_sparse_depth_path,
1248
1282
  pred_depth_path=pred_depth_path,
1249
1283
  intrinsics_path=intrinsics_path,
1250
1284
  camera_extrinsics_path=camera_extrinsics_path,
1285
+ lidar_extrinsics_path=lidar_extrinsics_path,
1251
1286
  segmentation_path=segmentation_path,
1252
1287
  pred_depth_metadata_scope=(
1253
1288
  pred_depth_key if pred_depth_key != "depth" else None
1254
1289
  ),
1290
+ segmentation_modality_key=segmentation_key or "segmentation",
1255
1291
  gt_sparse_depth_split=gt_sparse_depth_split,
1256
1292
  pred_depth_split=pred_depth_split,
1257
1293
  intrinsics_split=intrinsics_split,
1258
1294
  camera_extrinsics_split=camera_extrinsics_split,
1295
+ lidar_extrinsics_split=lidar_extrinsics_split,
1259
1296
  segmentation_split=segmentation_split,
1260
1297
  )
1261
1298
  et_eval_datasets["sparse_depth"] = sparse_depth_dataset
@@ -1334,6 +1371,8 @@ def main():
1334
1371
  "intrinsics_split": intrinsics_split,
1335
1372
  "camera_extrinsics_path": camera_extrinsics_path,
1336
1373
  "camera_extrinsics_split": camera_extrinsics_split,
1374
+ "lidar_extrinsics_path": lidar_extrinsics_path,
1375
+ "lidar_extrinsics_split": lidar_extrinsics_split,
1337
1376
  },
1338
1377
  "spatial_alignment": {
1339
1378
  "method": sparse_depth_spatial.get(
@@ -1450,6 +1489,7 @@ def main():
1450
1489
  gt_depth_split=gt_depth_split,
1451
1490
  calibration_split=calibration_split,
1452
1491
  segmentation_split=segmentation_split,
1492
+ segmentation_modality_key=segmentation_key or "segmentation",
1453
1493
  )
1454
1494
  et_eval_datasets["rgb"] = rgb_dataset
1455
1495
 
@@ -13,7 +13,7 @@ import torch
13
13
  from ds_crawler import get_dataset_contract, index_dataset_from_path
14
14
  from euler_loading import Modality, MultiModalDataset, resolve_loader_module
15
15
 
16
- from .config_paths import build_modality
16
+ from .config_paths import build_modality, parse_modality_path
17
17
  from .metrics.utils import convert_planar_to_radial
18
18
 
19
19
  # ---------------------------------------------------------------------------
@@ -168,6 +168,26 @@ def to_numpy_extrinsics(data: Any) -> np.ndarray:
168
168
  return arr.astype(np.float32, copy=False)
169
169
 
170
170
 
171
+ def compose_sensor_to_camera_extrinsics(
172
+ source_sensor_extrinsics: Any,
173
+ camera_extrinsics: Any,
174
+ ) -> np.ndarray:
175
+ """Compose per-sensor poses into a source-to-camera transform.
176
+
177
+ Both inputs are interpreted as sensor-to-common-frame transforms, e.g.
178
+ ``T_common_from_lidar`` and ``T_common_from_camera``. The returned matrix
179
+ maps source sensor points directly into the camera frame:
180
+
181
+ ``T_camera_from_source = inv(T_common_from_camera) @ T_common_from_source``.
182
+ """
183
+ source_to_common = to_numpy_extrinsics(source_sensor_extrinsics).astype(
184
+ np.float64
185
+ )
186
+ camera_to_common = to_numpy_extrinsics(camera_extrinsics).astype(np.float64)
187
+ common_to_camera = np.linalg.inv(camera_to_common)
188
+ return (common_to_camera @ source_to_common).astype(np.float32)
189
+
190
+
171
191
  def to_numpy_point_cloud(data: Any) -> np.ndarray:
172
192
  """Convert point-cloud data to ``(N, C)`` float32 numpy array.
173
193
 
@@ -518,7 +538,11 @@ def compute_scale_and_shift(
518
538
  # ---------------------------------------------------------------------------
519
539
 
520
540
 
521
- def _resolve_sky_mask_loader(segmentation_path: str) -> Callable[..., Any]:
541
+ def _resolve_sky_mask_loader(
542
+ segmentation_path: str,
543
+ *,
544
+ modality_key: str = "segmentation",
545
+ ) -> Callable[..., Any]:
522
546
  """Resolve the ``sky_mask`` loader for a segmentation dataset.
523
547
 
524
548
  The segmentation dataset's ds-crawler index declares a loader module
@@ -526,7 +550,7 @@ def _resolve_sky_mask_loader(segmentation_path: str) -> Callable[..., Any]:
526
550
  its ``sky_mask`` function so that raw class-segmentation data is
527
551
  converted to a boolean sky mask at load time.
528
552
  """
529
- modality = _modality(path=segmentation_path, modality_key="segmentation")
553
+ modality = _modality(path=segmentation_path, modality_key=modality_key)
530
554
  try:
531
555
  index = index_dataset_from_path(
532
556
  modality.path,
@@ -596,6 +620,7 @@ def build_depth_eval_dataset(
596
620
  calibration_split: Optional[str] = None,
597
621
  segmentation_split: Optional[str] = None,
598
622
  pred_depth_metadata_scope: Optional[str] = None,
623
+ segmentation_modality_key: str = "segmentation",
599
624
  ) -> MultiModalDataset:
600
625
  """Build a MultiModalDataset for depth evaluation.
601
626
 
@@ -644,10 +669,13 @@ def build_depth_eval_dataset(
644
669
  split=calibration_split,
645
670
  )
646
671
  if segmentation_path is not None:
647
- sky_fn = _resolve_sky_mask_loader(segmentation_path)
672
+ sky_fn = _resolve_sky_mask_loader(
673
+ segmentation_path,
674
+ modality_key=segmentation_modality_key,
675
+ )
648
676
  hierarchical["segmentation"] = _modality(
649
677
  path=segmentation_path,
650
- modality_key="segmentation",
678
+ modality_key=segmentation_modality_key,
651
679
  loader=sky_fn,
652
680
  split=segmentation_split,
653
681
  )
@@ -663,22 +691,31 @@ def build_sparse_depth_eval_dataset(
663
691
  pred_depth_path: str,
664
692
  intrinsics_path: str,
665
693
  camera_extrinsics_path: str,
694
+ lidar_extrinsics_path: Optional[str] = None,
666
695
  segmentation_path: Optional[str] = None,
667
696
  gt_sparse_depth_split: Optional[str] = None,
668
697
  pred_depth_split: Optional[str] = None,
669
698
  intrinsics_split: Optional[str] = None,
670
699
  camera_extrinsics_split: Optional[str] = None,
700
+ lidar_extrinsics_split: Optional[str] = None,
671
701
  segmentation_split: Optional[str] = None,
672
702
  pred_depth_metadata_scope: Optional[str] = None,
703
+ segmentation_modality_key: str = "segmentation",
673
704
  ) -> MultiModalDataset:
674
705
  """Build a MultiModalDataset for sparse pointcloud depth evaluation.
675
706
 
676
707
  The returned dataset yields dense predicted depth under ``"pred"``,
677
708
  sparse GT point clouds under ``"gt"``, and hierarchical camera
678
709
  calibration under ``"intrinsics"`` and ``"camera_extrinsics"``.
710
+ When ``lidar_extrinsics_path`` is supplied, ``camera_extrinsics`` and
711
+ ``lidar_extrinsics`` are treated as per-sensor poses in a shared frame and
712
+ composed into the source-to-camera transform used for projection. Without
713
+ ``lidar_extrinsics_path``, ``camera_extrinsics`` remains the direct
714
+ source-to-camera transform, such as MUSES ``lidar2rgb``.
679
715
  ``pred_depth_metadata_scope`` can select a depth-like prediction scope
680
716
  such as ``relative_depth`` or ``affine_depth`` while still loading it
681
- as a dense depth map.
717
+ as a dense depth map. Segmentation remains optional for sparse depth and is
718
+ only loaded by callers that request sky masking.
682
719
  """
683
720
  modalities = {
684
721
  "gt": _modality(
@@ -707,11 +744,28 @@ def build_sparse_depth_eval_dataset(
707
744
  split=camera_extrinsics_split,
708
745
  ),
709
746
  }
747
+ if lidar_extrinsics_path is not None:
748
+ lidar_scope = (
749
+ parse_modality_path(
750
+ lidar_extrinsics_path,
751
+ split=lidar_extrinsics_split,
752
+ ).metadata_scope
753
+ or "lidar_extrinsics"
754
+ )
755
+ hierarchical["lidar_extrinsics"] = _modality(
756
+ path=lidar_extrinsics_path,
757
+ modality_key="camera_extrinsics",
758
+ metadata_scope=lidar_scope,
759
+ split=lidar_extrinsics_split,
760
+ )
710
761
  if segmentation_path is not None:
711
- sky_fn = _resolve_sky_mask_loader(segmentation_path)
762
+ sky_fn = _resolve_sky_mask_loader(
763
+ segmentation_path,
764
+ modality_key=segmentation_modality_key,
765
+ )
712
766
  hierarchical["segmentation"] = _modality(
713
767
  path=segmentation_path,
714
- modality_key="segmentation",
768
+ modality_key=segmentation_modality_key,
715
769
  loader=sky_fn,
716
770
  split=segmentation_split,
717
771
  )
@@ -733,6 +787,7 @@ def build_rgb_eval_dataset(
733
787
  gt_depth_split: Optional[str] = None,
734
788
  calibration_split: Optional[str] = None,
735
789
  segmentation_split: Optional[str] = None,
790
+ segmentation_modality_key: str = "segmentation",
736
791
  ) -> MultiModalDataset:
737
792
  """Build a MultiModalDataset for RGB evaluation.
738
793
 
@@ -785,10 +840,13 @@ def build_rgb_eval_dataset(
785
840
  split=calibration_split,
786
841
  )
787
842
  if segmentation_path is not None:
788
- sky_fn = _resolve_sky_mask_loader(segmentation_path)
843
+ sky_fn = _resolve_sky_mask_loader(
844
+ segmentation_path,
845
+ modality_key=segmentation_modality_key,
846
+ )
789
847
  hierarchical["segmentation"] = _modality(
790
848
  path=segmentation_path,
791
- modality_key="segmentation",
849
+ modality_key=segmentation_modality_key,
792
850
  loader=sky_fn,
793
851
  split=segmentation_split,
794
852
  )
@@ -5,11 +5,11 @@ Runs all metrics over depth and RGB datasets loaded via euler_loading.
5
5
 
6
6
  import copy
7
7
  import tempfile
8
+ from collections.abc import Mapping
8
9
  from pathlib import Path
10
+ from typing import Iterator, Optional, Tuple
9
11
 
10
12
  import numpy as np
11
- import torch
12
- from typing import Iterator, Optional, Tuple
13
13
  from PIL import Image
14
14
  from torch.utils.data import DataLoader, Subset
15
15
  from tqdm import tqdm
@@ -19,6 +19,7 @@ from euler_loading import MultiModalDataset
19
19
  from .data import (
20
20
  align_to_prediction,
21
21
  classify_spatial_alignment,
22
+ compose_sensor_to_camera_extrinsics,
22
23
  compute_scale_and_shift,
23
24
  process_depth,
24
25
  project_point_cloud_to_depth_map,
@@ -499,6 +500,15 @@ def _get_first_hierarchical_value(sample: dict, key: str):
499
500
  return data
500
501
 
501
502
 
503
+ def _iter_hierarchical_values(sample: dict, key: str) -> list:
504
+ data = sample.get(key)
505
+ if data is None:
506
+ return []
507
+ if isinstance(data, dict):
508
+ return [data, *data.values()]
509
+ return [data]
510
+
511
+
502
512
  def _get_intrinsics_K(sample: dict) -> Optional[np.ndarray]:
503
513
  """Extract (3,3) intrinsics matrix from sample calibration data."""
504
514
  K_data = _get_first_hierarchical_value(sample, "intrinsics")
@@ -509,14 +519,197 @@ def _get_intrinsics_K(sample: dict) -> Optional[np.ndarray]:
509
519
  return to_numpy_intrinsics(K_data)
510
520
 
511
521
 
512
- def _get_camera_extrinsics(sample: dict) -> Optional[np.ndarray]:
513
- """Extract a source-to-camera extrinsics matrix from a sample."""
514
- T_data = _get_first_hierarchical_value(sample, "camera_extrinsics")
515
- if T_data is None:
516
- T_data = _get_first_hierarchical_value(sample, "extrinsics")
517
- if T_data is None:
522
+ _DIRECT_SOURCE_TO_CAMERA_KEYS = (
523
+ "lidar2rgb",
524
+ "lidar2camera",
525
+ "lidar2cam",
526
+ "lidar_to_rgb",
527
+ "lidar_to_camera",
528
+ "lidar_to_cam",
529
+ "source2rgb",
530
+ "source2camera",
531
+ "source_to_rgb",
532
+ "source_to_camera",
533
+ "sensor2rgb",
534
+ "sensor2camera",
535
+ "sensor_to_rgb",
536
+ "sensor_to_camera",
537
+ )
538
+ _SOURCE_SENSOR_POSE_KEYS = (
539
+ "lidar",
540
+ "lidar_pose",
541
+ "lidar_extrinsics",
542
+ "source",
543
+ "source_sensor",
544
+ "source_extrinsics",
545
+ "sensor",
546
+ "sensor_extrinsics",
547
+ "lidar2gnss",
548
+ "lidar2ego",
549
+ "lidar2vehicle",
550
+ "lidar2world",
551
+ "lidar_to_gnss",
552
+ "lidar_to_ego",
553
+ "lidar_to_vehicle",
554
+ "lidar_to_world",
555
+ )
556
+ _CAMERA_SENSOR_POSE_KEYS = (
557
+ "rgb",
558
+ "camera",
559
+ "cam",
560
+ "frame_camera",
561
+ "rgb_pose",
562
+ "camera_pose",
563
+ "camera_extrinsics",
564
+ "target",
565
+ "target_sensor",
566
+ "target_extrinsics",
567
+ "rgb2gnss",
568
+ "camera2gnss",
569
+ "cam2gnss",
570
+ "frame_camera2gnss",
571
+ "rgb2ego",
572
+ "camera2ego",
573
+ "cam2ego",
574
+ "frame_camera2ego",
575
+ "rgb2vehicle",
576
+ "camera2vehicle",
577
+ "cam2vehicle",
578
+ "frame_camera2vehicle",
579
+ "rgb2world",
580
+ "camera2world",
581
+ "cam2world",
582
+ "frame_camera2world",
583
+ )
584
+ _MATRIX_WRAPPER_KEYS = ("T", "transform", "matrix", "extrinsics", "pose")
585
+
586
+
587
+ def _as_extrinsics_or_none(data) -> Optional[np.ndarray]:
588
+ try:
589
+ return to_numpy_extrinsics(data)
590
+ except (TypeError, ValueError):
591
+ return None
592
+
593
+
594
+ def _extract_wrapped_extrinsics(data) -> Optional[np.ndarray]:
595
+ matrix = _as_extrinsics_or_none(data)
596
+ if matrix is not None:
597
+ return matrix
598
+
599
+ if not isinstance(data, Mapping):
600
+ return None
601
+
602
+ for key in _MATRIX_WRAPPER_KEYS:
603
+ if key in data:
604
+ matrix = _extract_wrapped_extrinsics(data[key])
605
+ if matrix is not None:
606
+ return matrix
607
+
608
+ if len(data) == 1:
609
+ return _extract_wrapped_extrinsics(next(iter(data.values())))
610
+
611
+ return None
612
+
613
+
614
+ def _extract_named_extrinsics(data, names: tuple[str, ...]) -> Optional[np.ndarray]:
615
+ if not isinstance(data, Mapping):
616
+ return _extract_wrapped_extrinsics(data)
617
+
618
+ for name in names:
619
+ if name in data:
620
+ matrix = _extract_wrapped_extrinsics(data[name])
621
+ if matrix is not None:
622
+ return matrix
623
+
624
+ for key in ("extrinsics", "sensors", "calibration", "poses"):
625
+ if key in data and isinstance(data[key], Mapping):
626
+ matrix = _extract_named_extrinsics(data[key], names)
627
+ if matrix is not None:
628
+ return matrix
629
+
630
+ if len(data) == 1:
631
+ return _extract_named_extrinsics(next(iter(data.values())), names)
632
+
633
+ return None
634
+
635
+
636
+ def _extract_direct_source_to_camera(data) -> Optional[np.ndarray]:
637
+ matrix = _as_extrinsics_or_none(data)
638
+ if matrix is not None:
639
+ return matrix
640
+
641
+ if not isinstance(data, Mapping):
518
642
  return None
519
- return to_numpy_extrinsics(T_data)
643
+
644
+ for key in _DIRECT_SOURCE_TO_CAMERA_KEYS:
645
+ if key in data:
646
+ matrix = _extract_wrapped_extrinsics(data[key])
647
+ if matrix is not None:
648
+ return matrix
649
+
650
+ for key in ("extrinsics", "calibration"):
651
+ if key in data and isinstance(data[key], Mapping):
652
+ matrix = _extract_direct_source_to_camera(data[key])
653
+ if matrix is not None:
654
+ return matrix
655
+
656
+ if len(data) == 1:
657
+ return _extract_direct_source_to_camera(next(iter(data.values())))
658
+
659
+ return None
660
+
661
+
662
+ def _get_pointcloud_to_camera_extrinsics(
663
+ sample: dict,
664
+ ) -> tuple[Optional[np.ndarray], Optional[str]]:
665
+ """Extract or compose the transform used for sparse point projection.
666
+
667
+ The common MUSES path supplies a direct ``lidar2rgb`` transform via
668
+ ``camera_extrinsics``. Some datasets instead expose separate lidar and
669
+ camera sensor poses in a shared frame; in that case we compose them to the
670
+ same source-to-camera transform expected by the projector.
671
+ """
672
+ lidar_pose = None
673
+ for value in _iter_hierarchical_values(sample, "lidar_extrinsics"):
674
+ lidar_pose = _extract_named_extrinsics(value, _SOURCE_SENSOR_POSE_KEYS)
675
+ if lidar_pose is not None:
676
+ break
677
+
678
+ camera_values = _iter_hierarchical_values(sample, "camera_extrinsics")
679
+ if not camera_values:
680
+ camera_values = _iter_hierarchical_values(sample, "extrinsics")
681
+
682
+ if lidar_pose is not None:
683
+ for value in camera_values:
684
+ camera_pose = _extract_named_extrinsics(value, _CAMERA_SENSOR_POSE_KEYS)
685
+ if camera_pose is not None:
686
+ return (
687
+ compose_sensor_to_camera_extrinsics(lidar_pose, camera_pose),
688
+ "composed_lidar_and_camera_sensor_poses",
689
+ )
690
+ return None, None
691
+
692
+ for value in camera_values:
693
+ direct = _extract_direct_source_to_camera(value)
694
+ if direct is not None:
695
+ return direct, "direct_source_to_camera"
696
+
697
+ for value in camera_values:
698
+ lidar_pose = _extract_named_extrinsics(value, _SOURCE_SENSOR_POSE_KEYS)
699
+ camera_pose = _extract_named_extrinsics(value, _CAMERA_SENSOR_POSE_KEYS)
700
+ if lidar_pose is not None and camera_pose is not None:
701
+ return (
702
+ compose_sensor_to_camera_extrinsics(lidar_pose, camera_pose),
703
+ "composed_lidar_and_camera_sensor_poses",
704
+ )
705
+
706
+ return None, None
707
+
708
+
709
+ def _get_camera_extrinsics(sample: dict) -> Optional[np.ndarray]:
710
+ """Backward-compatible wrapper for direct sparse projection callers."""
711
+ transform, _ = _get_pointcloud_to_camera_extrinsics(sample)
712
+ return transform
520
713
 
521
714
 
522
715
  # ---------------------------------------------------------------------------
@@ -1658,6 +1851,7 @@ def evaluate_sparse_depth_samples(
1658
1851
  total_input_points = 0
1659
1852
  total_projected_pixels = 0
1660
1853
  total_evaluated_pixels = 0
1854
+ projection_extrinsics_source: Optional[str] = None
1661
1855
 
1662
1856
  if alignment_mode == "none":
1663
1857
  print("Sparse depth alignment mode: none")
@@ -1711,15 +1905,22 @@ def evaluate_sparse_depth_samples(
1711
1905
  point_cloud = to_numpy_point_cloud(sample["gt"])
1712
1906
  depth_pred = to_numpy_depth(sample["pred"])
1713
1907
  intrinsics_K = _get_intrinsics_K(sample)
1714
- camera_extrinsics = _get_camera_extrinsics(sample)
1908
+ camera_extrinsics, extrinsics_source = (
1909
+ _get_pointcloud_to_camera_extrinsics(sample)
1910
+ )
1715
1911
  if intrinsics_K is None:
1716
1912
  raise ValueError(
1717
1913
  f"Sample {entry_id!r} is missing intrinsics for sparse depth projection."
1718
1914
  )
1719
1915
  if camera_extrinsics is None:
1720
1916
  raise ValueError(
1721
- f"Sample {entry_id!r} is missing camera_extrinsics for sparse depth projection."
1917
+ f"Sample {entry_id!r} is missing usable camera_extrinsics "
1918
+ "for sparse depth projection. Provide a direct "
1919
+ "source-to-camera transform (for MUSES, lidar2rgb) or "
1920
+ "separate lidar_extrinsics and camera_extrinsics sensor poses."
1722
1921
  )
1922
+ if projection_extrinsics_source is None:
1923
+ projection_extrinsics_source = extrinsics_source
1723
1924
 
1724
1925
  if i == 0:
1725
1926
  pred_native_dims = depth_pred.shape[:2]
@@ -2015,6 +2216,7 @@ def evaluate_sparse_depth_samples(
2015
2216
  if pred_native_dims
2016
2217
  else None,
2017
2218
  "method": "pointcloud_projection",
2219
+ "extrinsics_source": projection_extrinsics_source,
2018
2220
  "evaluated_dimensions": {
2019
2221
  "height": pred_native_dims[0],
2020
2222
  "width": pred_native_dims[1],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.12.0
3
+ Version: 2.13.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -149,7 +149,7 @@ This pre-downloads:
149
149
  # Evaluate with default settings (auto-selects CUDA when available)
150
150
  depth-eval config.json --batch-size 32
151
151
 
152
- # Evaluate with sky masking enabled (requires gt.segmentation in config)
152
+ # Evaluate with sky masking enabled (requires gt.segmentation or gt.semantic_segmentation)
153
153
  depth-eval config.json --mask-sky -v
154
154
 
155
155
  # Skip RGB evaluation, only evaluate depth
@@ -241,7 +241,13 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
241
241
 
242
242
  Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`). euler-loading inline selectors are also accepted in `path`, such as `/data/muses.zip:test` or `/data/muses.zip:test#scope=rgb`.
243
243
 
244
- For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction uses a dense depth-like map under `datasets[].depth`, `datasets[].relative_depth`, or `datasets[].affine_depth`. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels.
244
+ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction uses a dense depth-like map under `datasets[].depth`, `datasets[].relative_depth`, or `datasets[].affine_depth`. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels. For MUSES through `euler_loading.loaders.muses`, `gt.camera_extrinsics` normally resolves to the direct `lidar2rgb` transform and no extra lidar pose is needed. If a dataset exposes separate lidar and camera poses in a shared frame instead, provide optional `gt.lidar_extrinsics`; the evaluator composes `inv(camera_pose) @ lidar_pose` before projection.
245
+
246
+ Sparse depth does not require segmentation GT. `gt.segmentation` and
247
+ `gt.semantic_segmentation` are optional aliases for the same sky-mask source.
248
+ They are loaded only when `--mask-sky` is set; in sparse depth evaluation that
249
+ mask excludes sky pixels from projected-point metrics and from scale/shift
250
+ fitting. Without `--mask-sky`, the segmentation entry is not loaded or used.
245
251
 
246
252
  #### GT section
247
253
 
@@ -251,10 +257,11 @@ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.dep
251
257
  | `gt.depth.path` | no\* | Path to GT depth dataset |
252
258
  | `gt.sparse_depth.path` | no\* | Path to sparse pointcloud GT dataset, e.g. `sparse_depth` with `(N,C)` points whose first columns are `x,y,z` in meters |
253
259
  | `gt.rays.path` | no\* | Path to GT ray direction map dataset (for rays evaluation) |
254
- | `gt.segmentation.path` | no | Path to GT segmentation (needed for `--mask-sky`) |
260
+ | `gt.segmentation.path` / `gt.semantic_segmentation.path` | no | Path to GT segmentation (needed for `--mask-sky`; use one key, not both) |
255
261
  | `gt.calibration.path` | no | Path to calibration data (camera intrinsics matrices) |
256
262
  | `gt.intrinsics.path` | required with `gt.sparse_depth` | Path to camera intrinsics matrices for pointcloud projection |
257
263
  | `gt.camera_extrinsics.path` | required with `gt.sparse_depth` | Path to source-to-camera extrinsics, e.g. MUSES `lidar2rgb`, for pointcloud projection |
264
+ | `gt.lidar_extrinsics.path` | no | Optional lidar sensor pose in the same shared frame as `gt.camera_extrinsics`; when provided, `gt.camera_extrinsics` is interpreted as the camera pose and both are composed for projection |
258
265
  | `gt.name` | no | Display name for ground truth (default: `"GT"`) |
259
266
 
260
267
  \* At least one of `gt.rgb.path`, `gt.depth.path`, `gt.sparse_depth.path`, or `gt.rays.path` is required.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-eval"
7
- version = "2.12.0"
7
+ version = "2.13.0"
8
8
  description = "Depth map evaluation toolkit with comprehensive metrics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -4,7 +4,12 @@ import json
4
4
  import zipfile
5
5
  import pytest
6
6
 
7
- from euler_eval.cli import load_config, validate_dataset_entry, validate_gt_config
7
+ from euler_eval.cli import (
8
+ _gt_segmentation_entry,
9
+ load_config,
10
+ validate_dataset_entry,
11
+ validate_gt_config,
12
+ )
8
13
 
9
14
 
10
15
  # ---------------------------------------------------------------------------
@@ -39,6 +44,36 @@ class TestValidateGtConfig:
39
44
  }
40
45
  validate_gt_config(gt)
41
46
 
47
+ def test_valid_with_semantic_segmentation_alias(self, tmp_path):
48
+ """GT config accepts semantic_segmentation as a segmentation alias."""
49
+ for name in ("depth", "semantic_segmentation"):
50
+ (tmp_path / name).mkdir()
51
+
52
+ gt = {
53
+ "depth": {"path": str(tmp_path / "depth")},
54
+ "semantic_segmentation": {
55
+ "path": str(tmp_path / "semantic_segmentation")
56
+ },
57
+ }
58
+ validate_gt_config(gt)
59
+ key, entry = _gt_segmentation_entry(gt)
60
+ assert key == "semantic_segmentation"
61
+ assert entry == gt["semantic_segmentation"]
62
+
63
+ def test_rejects_duplicate_segmentation_aliases(self, tmp_path):
64
+ for name in ("depth", "segmentation", "semantic_segmentation"):
65
+ (tmp_path / name).mkdir()
66
+
67
+ gt = {
68
+ "depth": {"path": str(tmp_path / "depth")},
69
+ "segmentation": {"path": str(tmp_path / "segmentation")},
70
+ "semantic_segmentation": {
71
+ "path": str(tmp_path / "semantic_segmentation")
72
+ },
73
+ }
74
+ with pytest.raises(ValueError, match="multiple segmentation entries"):
75
+ validate_gt_config(gt)
76
+
42
77
  def test_depth_only_gt_is_valid(self, tmp_path):
43
78
  depth_path = tmp_path / "depth"
44
79
  depth_path.mkdir()
@@ -101,6 +136,22 @@ class TestValidateGtConfig:
101
136
  }
102
137
  validate_gt_config(gt)
103
138
 
139
+ def test_sparse_depth_gt_accepts_optional_lidar_extrinsics(self, tmp_path):
140
+ for name in (
141
+ "sparse_depth",
142
+ "intrinsics",
143
+ "camera_extrinsics",
144
+ "lidar_extrinsics",
145
+ ):
146
+ (tmp_path / name).mkdir()
147
+ gt = {
148
+ "sparse_depth": {"path": str(tmp_path / "sparse_depth")},
149
+ "intrinsics": {"path": str(tmp_path / "intrinsics")},
150
+ "camera_extrinsics": {"path": str(tmp_path / "camera_extrinsics")},
151
+ "lidar_extrinsics": {"path": str(tmp_path / "lidar_extrinsics")},
152
+ }
153
+ validate_gt_config(gt)
154
+
104
155
  def test_nonexistent_path(self, tmp_path):
105
156
  rgb_path = tmp_path / "rgb"
106
157
  rgb_path.mkdir()
@@ -35,7 +35,7 @@ def test_depth_builder_sets_modality_scopes(monkeypatch):
35
35
  monkeypatch.setattr(
36
36
  data,
37
37
  "_resolve_sky_mask_loader",
38
- lambda path: sky_loader,
38
+ lambda path, *, modality_key="segmentation": sky_loader,
39
39
  )
40
40
 
41
41
  dataset = data.build_depth_eval_dataset(
@@ -100,6 +100,27 @@ def test_sparse_depth_builder_sets_projection_modality_scopes(monkeypatch):
100
100
  key="camera_extrinsics",
101
101
  split="pose",
102
102
  )
103
+ assert "segmentation" not in dataset.hierarchical_modalities
104
+
105
+
106
+ def test_sparse_depth_builder_adds_optional_lidar_extrinsics(monkeypatch):
107
+ _install_captured_dataset(monkeypatch)
108
+
109
+ dataset = data.build_sparse_depth_eval_dataset(
110
+ gt_sparse_depth_path="/datasets/shared",
111
+ pred_depth_path="/predictions/shared",
112
+ intrinsics_path="/datasets/shared",
113
+ camera_extrinsics_path="/datasets/shared",
114
+ lidar_extrinsics_path="/datasets/shared",
115
+ lidar_extrinsics_split="pose",
116
+ )
117
+
118
+ _assert_modality(
119
+ dataset.hierarchical_modalities["lidar_extrinsics"],
120
+ key="camera_extrinsics",
121
+ split="pose",
122
+ scope="lidar_extrinsics",
123
+ )
103
124
 
104
125
 
105
126
  def test_sparse_depth_builder_can_load_relative_depth_prediction_scope(monkeypatch):
@@ -121,6 +142,40 @@ def test_sparse_depth_builder_can_load_relative_depth_prediction_scope(monkeypat
121
142
  )
122
143
 
123
144
 
145
+ def test_sparse_depth_builder_uses_semantic_segmentation_scope(monkeypatch):
146
+ _install_captured_dataset(monkeypatch)
147
+ calls = []
148
+
149
+ def sky_loader(path, meta=None):
150
+ return None
151
+
152
+ def resolve_sky_mask_loader(path, *, modality_key="segmentation"):
153
+ calls.append((path, modality_key))
154
+ return sky_loader
155
+
156
+ monkeypatch.setattr(
157
+ data,
158
+ "_resolve_sky_mask_loader",
159
+ resolve_sky_mask_loader,
160
+ )
161
+
162
+ dataset = data.build_sparse_depth_eval_dataset(
163
+ gt_sparse_depth_path="/datasets/shared",
164
+ pred_depth_path="/predictions/shared",
165
+ intrinsics_path="/datasets/shared",
166
+ camera_extrinsics_path="/datasets/shared",
167
+ segmentation_path="/datasets/shared",
168
+ segmentation_modality_key="semantic_segmentation",
169
+ )
170
+
171
+ assert calls == [("/datasets/shared", "semantic_segmentation")]
172
+ _assert_modality(
173
+ dataset.hierarchical_modalities["segmentation"],
174
+ key="semantic_segmentation",
175
+ loader=sky_loader,
176
+ )
177
+
178
+
124
179
  def test_rgb_builder_sets_rgb_and_auxiliary_modality_scopes(monkeypatch):
125
180
  _install_captured_dataset(monkeypatch)
126
181
 
@@ -215,6 +270,35 @@ def test_sky_mask_loader_resolution_strips_inline_split(monkeypatch):
215
270
  ]
216
271
 
217
272
 
273
+ def test_sky_mask_loader_resolution_accepts_semantic_segmentation_scope(monkeypatch):
274
+ calls = []
275
+
276
+ def fake_index_dataset_from_path(path, **kwargs):
277
+ calls.append((path, kwargs))
278
+ return {"euler_loading": {"loader": "muses"}}
279
+
280
+ def sky_mask(path, meta=None):
281
+ return None
282
+
283
+ monkeypatch.setattr(data, "index_dataset_from_path", fake_index_dataset_from_path)
284
+ monkeypatch.setattr(
285
+ data,
286
+ "resolve_loader_module",
287
+ lambda name: SimpleNamespace(sky_mask=sky_mask),
288
+ )
289
+
290
+ assert (
291
+ data._resolve_sky_mask_loader(
292
+ "/datasets/shared",
293
+ modality_key="semantic_segmentation",
294
+ )
295
+ is sky_mask
296
+ )
297
+ assert calls == [
298
+ ("/datasets/shared", {"metadata_scope": "semantic_segmentation"}),
299
+ ]
300
+
301
+
218
302
  def test_modality_parses_inline_split_and_scope():
219
303
  modality = data._modality(
220
304
  path="/datasets/frame_camera_trainvaltest.zip:fog_day#scope=intrinsics",
@@ -3,6 +3,7 @@
3
3
  import numpy as np
4
4
 
5
5
  from euler_eval.data import (
6
+ compose_sensor_to_camera_extrinsics,
6
7
  project_point_cloud_to_depth_map,
7
8
  to_numpy_extrinsics,
8
9
  to_numpy_point_cloud,
@@ -29,6 +30,22 @@ def test_to_numpy_extrinsics_expands_3x4_matrix():
29
30
  np.testing.assert_array_equal(result[3], np.array([0.0, 0.0, 0.0, 1.0]))
30
31
 
31
32
 
33
+ def test_compose_sensor_to_camera_extrinsics_uses_lidar_and_camera_poses():
34
+ lidar_to_world = np.eye(4, dtype=np.float32)
35
+ lidar_to_world[0, 3] = 12.0
36
+ camera_to_world = np.eye(4, dtype=np.float32)
37
+ camera_to_world[0, 3] = 10.0
38
+
39
+ result = compose_sensor_to_camera_extrinsics(
40
+ lidar_to_world,
41
+ camera_to_world,
42
+ )
43
+
44
+ expected = np.eye(4, dtype=np.float32)
45
+ expected[0, 3] = 2.0
46
+ np.testing.assert_allclose(result, expected)
47
+
48
+
32
49
  def test_project_point_cloud_to_depth_map_uses_nearest_point_per_pixel():
33
50
  intrinsics = np.eye(3, dtype=np.float32)
34
51
  extrinsics = np.eye(4, dtype=np.float32)
@@ -115,6 +132,52 @@ class _AffineSparseDataset(_OneSampleSparseDataset):
115
132
  self.pred[1, 2] = np.sqrt(24.0) * 10.0 + 7.0
116
133
 
117
134
 
135
+ class _SeparateSensorPoseSparseDataset(_OneSampleSparseDataset):
136
+ def __init__(self):
137
+ super().__init__()
138
+ self.point_cloud = np.array([[0.0, 1.0, 1.0, 0.0]], dtype=np.float32)
139
+ self.camera_extrinsics = np.eye(4, dtype=np.float32)
140
+ self.camera_extrinsics[0, 3] = 10.0
141
+ self.lidar_extrinsics = np.eye(4, dtype=np.float32)
142
+ self.lidar_extrinsics[0, 3] = 12.0
143
+ self.pred = np.zeros((3, 3), dtype=np.float32)
144
+ self.pred[1, 2] = np.sqrt(6.0)
145
+
146
+ def __getitem__(self, index):
147
+ if index != 0:
148
+ raise IndexError(index)
149
+ return {
150
+ "id": "000000",
151
+ "full_id": "/000000",
152
+ "gt": self.point_cloud,
153
+ "pred": self.pred,
154
+ "intrinsics": {"intrinsics": self.intrinsics},
155
+ "camera_extrinsics": {"camera_pose": self.camera_extrinsics},
156
+ "lidar_extrinsics": {"lidar_pose": self.lidar_extrinsics},
157
+ }
158
+
159
+
160
+ class _SharedCalibrationSparseDataset(_SeparateSensorPoseSparseDataset):
161
+ def __getitem__(self, index):
162
+ if index != 0:
163
+ raise IndexError(index)
164
+ return {
165
+ "id": "000000",
166
+ "full_id": "/000000",
167
+ "gt": self.point_cloud,
168
+ "pred": self.pred,
169
+ "intrinsics": {"intrinsics": self.intrinsics},
170
+ "camera_extrinsics": {
171
+ "calib": {
172
+ "extrinsics": {
173
+ "lidar2ego": self.lidar_extrinsics,
174
+ "rgb2ego": self.camera_extrinsics,
175
+ }
176
+ }
177
+ },
178
+ }
179
+
180
+
118
181
  def test_sparse_depth_eval_reports_only_pointwise_depth_metrics():
119
182
  dataset = _OneSampleSparseDataset()
120
183
 
@@ -135,6 +198,47 @@ def test_sparse_depth_eval_reports_only_pointwise_depth_metrics():
135
198
  np.testing.assert_allclose(metrics["depth_metrics"]["rmse"]["median"], 0.0)
136
199
 
137
200
 
201
+ def test_sparse_depth_eval_composes_separate_lidar_and_camera_poses():
202
+ result = evaluate_sparse_depth_samples(
203
+ _SeparateSensorPoseSparseDataset(),
204
+ pred_is_radial=True,
205
+ num_workers=0,
206
+ alignment_mode="none",
207
+ )
208
+
209
+ assert result["dataset_info"]["projected_pixels"] == 1
210
+ assert result["dataset_info"]["evaluated_pixels"] == 1
211
+ assert (
212
+ result["spatial_info"]["extrinsics_source"]
213
+ == "composed_lidar_and_camera_sensor_poses"
214
+ )
215
+ np.testing.assert_allclose(
216
+ result["sparse_depth_metric"]["depth_metrics"]["rmse"]["median"],
217
+ 0.0,
218
+ atol=1e-6,
219
+ )
220
+
221
+
222
+ def test_sparse_depth_eval_composes_sensor_poses_from_shared_calibration():
223
+ result = evaluate_sparse_depth_samples(
224
+ _SharedCalibrationSparseDataset(),
225
+ pred_is_radial=True,
226
+ num_workers=0,
227
+ alignment_mode="none",
228
+ )
229
+
230
+ assert result["dataset_info"]["projected_pixels"] == 1
231
+ assert (
232
+ result["spatial_info"]["extrinsics_source"]
233
+ == "composed_lidar_and_camera_sensor_poses"
234
+ )
235
+ np.testing.assert_allclose(
236
+ result["sparse_depth_metric"]["depth_metrics"]["rmse"]["median"],
237
+ 0.0,
238
+ atol=1e-6,
239
+ )
240
+
241
+
138
242
  def test_sparse_depth_eval_auto_affine_aligns_relative_depth():
139
243
  result = evaluate_sparse_depth_samples(
140
244
  _RelativeSparseDataset(),
File without changes
File without changes