euler-eval 2.11.0__tar.gz → 2.12.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.11.0 → euler_eval-2.12.0}/PKG-INFO +9 -5
  2. {euler_eval-2.11.0 → euler_eval-2.12.0}/README.md +8 -4
  3. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/cli.py +69 -15
  4. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/config_paths.py +2 -0
  5. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/data.py +12 -0
  6. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/evaluate.py +33 -2
  7. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval.egg-info/PKG-INFO +9 -5
  8. {euler_eval-2.11.0 → euler_eval-2.12.0}/pyproject.toml +1 -1
  9. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_config.py +29 -0
  10. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_data_builders.py +23 -2
  11. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_save_results.py +12 -0
  12. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_sparse_depth.py +60 -0
  13. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/__init__.py +0 -0
  14. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/__init__.py +0 -0
  15. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/absrel.py +0 -0
  16. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/daniel_error.py +0 -0
  17. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  18. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  19. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/depth_standard.py +0 -0
  20. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/fid_kid.py +0 -0
  21. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  22. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  23. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  24. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/lpips_metric.py +0 -0
  25. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/normal_consistency.py +0 -0
  26. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/psnr.py +0 -0
  27. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  28. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  29. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  30. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/rho_a.py +0 -0
  31. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/rmse.py +0 -0
  32. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  33. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/ssim.py +0 -0
  34. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/tail_errors.py +0 -0
  35. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/metrics/utils.py +0 -0
  36. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/sanity_checker.py +0 -0
  37. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  38. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval.egg-info/SOURCES.txt +0 -0
  39. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  40. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval.egg-info/entry_points.txt +0 -0
  41. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval.egg-info/requires.txt +0 -0
  42. {euler_eval-2.11.0 → euler_eval-2.12.0}/euler_eval.egg-info/top_level.txt +0 -0
  43. {euler_eval-2.11.0 → euler_eval-2.12.0}/init_cache.py +0 -0
  44. {euler_eval-2.11.0 → euler_eval-2.12.0}/setup.cfg +0 -0
  45. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_alignment.py +0 -0
  46. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_cli_device.py +0 -0
  47. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_data.py +0 -0
  48. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_depth_alignment_output.py +0 -0
  49. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_depth_standard.py +0 -0
  50. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_evaluate_helpers.py +0 -0
  51. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_init_cache.py +0 -0
  52. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_integration.py +0 -0
  53. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_meta_output.py +0 -0
  54. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_metrics_utils.py +0 -0
  55. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_rgb_fid_output.py +0 -0
  56. {euler_eval-2.11.0 → euler_eval-2.12.0}/tests/test_rho_a.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.11.0
3
+ Version: 2.12.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -241,7 +241,7 @@ 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 still uses `datasets[].depth` because the model output is a dense depth map. 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.
245
245
 
246
246
  #### GT section
247
247
 
@@ -261,17 +261,21 @@ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.dep
261
261
 
262
262
  #### Prediction datasets
263
263
 
264
- Each entry in `datasets` can include `rgb`, `depth`, `rays`, or any combination:
264
+ Each entry in `datasets` can include `rgb`, one dense depth-like prediction (`depth`, `relative_depth`, or `affine_depth`), `rays`, or any combination:
265
265
 
266
266
  | Field | Required | Description |
267
267
  |---|---|---|
268
268
  | `name` | yes | Display name for this prediction dataset |
269
269
  | `rgb.path` | no\* | Path to predicted RGB dataset |
270
- | `depth.path` | no\* | Path to predicted dense depth dataset; also used when evaluating against sparse pointcloud GT |
270
+ | `depth.path` | no\* | Path to predicted dense metric depth dataset; also used when evaluating against sparse pointcloud GT |
271
+ | `relative_depth.path` | no\* | Path to predicted dense relative depth dataset; evaluated through the same depth pipeline with scale/shift alignment support |
272
+ | `affine_depth.path` | no\* | Path to predicted dense affine-depth dataset; evaluated through the same depth pipeline with scale/shift alignment support |
271
273
  | `rays.path` | no\* | Path to predicted ray direction map dataset |
272
274
  | `output_file` | no | Custom output path for results JSON (default: `eval.json` inside the first available modality path) |
273
275
 
274
- \* At least one of `rgb.path`, `depth.path`, or `rays.path` is required.
276
+ \* At least one of `rgb.path`, `depth.path`, `relative_depth.path`, `affine_depth.path`, or `rays.path` is required. Use only one dense depth-like entry per prediction dataset.
277
+
278
+ When `relative_depth` or `affine_depth` is used, `--depth-alignment auto_affine` treats the entry as declared non-metric depth and runs scale/shift alignment even if the raw value range is not normalized. `--depth-alignment none` still disables calibration.
275
279
 
276
280
  #### `euler_train` section (optional)
277
281
 
@@ -203,7 +203,7 @@ 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 still uses `datasets[].depth` because the model output is a dense depth map. 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.
207
207
 
208
208
  #### GT section
209
209
 
@@ -223,17 +223,21 @@ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.dep
223
223
 
224
224
  #### Prediction datasets
225
225
 
226
- Each entry in `datasets` can include `rgb`, `depth`, `rays`, or any combination:
226
+ Each entry in `datasets` can include `rgb`, one dense depth-like prediction (`depth`, `relative_depth`, or `affine_depth`), `rays`, or any combination:
227
227
 
228
228
  | Field | Required | Description |
229
229
  |---|---|---|
230
230
  | `name` | yes | Display name for this prediction dataset |
231
231
  | `rgb.path` | no\* | Path to predicted RGB dataset |
232
- | `depth.path` | no\* | Path to predicted dense depth dataset; also used when evaluating against sparse pointcloud GT |
232
+ | `depth.path` | no\* | Path to predicted dense metric depth dataset; also used when evaluating against sparse pointcloud GT |
233
+ | `relative_depth.path` | no\* | Path to predicted dense relative depth dataset; evaluated through the same depth pipeline with scale/shift alignment support |
234
+ | `affine_depth.path` | no\* | Path to predicted dense affine-depth dataset; evaluated through the same depth pipeline with scale/shift alignment support |
233
235
  | `rays.path` | no\* | Path to predicted ray direction map dataset |
234
236
  | `output_file` | no | Custom output path for results JSON (default: `eval.json` inside the first available modality path) |
235
237
 
236
- \* At least one of `rgb.path`, `depth.path`, or `rays.path` is required.
238
+ \* At least one of `rgb.path`, `depth.path`, `relative_depth.path`, `affine_depth.path`, or `rays.path` is required. Use only one dense depth-like entry per prediction dataset.
239
+
240
+ When `relative_depth` or `affine_depth` is used, `--depth-alignment auto_affine` treats the entry as declared non-metric depth and runs scale/shift alignment even if the raw value range is not normalized. `--depth-alignment none` still disables calibration.
237
241
 
238
242
  #### `euler_train` section (optional)
239
243
 
@@ -151,6 +151,8 @@ def _rgb_eval_axes(*, benchmark: bool = False) -> dict[str, AxisDeclaration]:
151
151
 
152
152
  _RAYS_EVAL_AXES: dict[str, AxisDeclaration] = {}
153
153
 
154
+ _PRED_DEPTH_KEYS = ("depth", "relative_depth", "affine_depth")
155
+
154
156
  # Downstream eval consumers validate metricSet.metricNamespace with a stricter
155
157
  # first-segment rule than euler_metric_naming's modality validator. Keep the
156
158
  # public Python sparse_depth result keys, but use a wire-compatible metric root
@@ -562,6 +564,34 @@ def print_device_info(requested_device: str, resolved_device: str) -> None:
562
564
  pass
563
565
 
564
566
 
567
+ def _prediction_depth_entry(
568
+ entry: dict, *, label: str = "Prediction dataset"
569
+ ) -> tuple[str | None, dict | None]:
570
+ """Return the configured dense-depth prediction entry, if any."""
571
+ matches = [
572
+ key
573
+ for key in _PRED_DEPTH_KEYS
574
+ if key in entry and "path" in entry.get(key, {})
575
+ ]
576
+ if not matches:
577
+ return None, None
578
+ if len(matches) > 1:
579
+ raise ValueError(
580
+ f"{label} contains multiple dense depth entries "
581
+ f"{matches}. Use only one of {list(_PRED_DEPTH_KEYS)}."
582
+ )
583
+ key = matches[0]
584
+ return key, entry[key]
585
+
586
+
587
+ def _prediction_depth_space_hint(key: str | None) -> str | None:
588
+ if key == "relative_depth":
589
+ return "relative"
590
+ if key == "affine_depth":
591
+ return "affine"
592
+ return None
593
+
594
+
565
595
  def validate_gt_config(gt: dict) -> None:
566
596
  """Validate the ``gt`` section of the configuration.
567
597
 
@@ -619,14 +649,16 @@ def validate_dataset_entry(entry: dict, index: int) -> None:
619
649
  raise ValueError(f"{label} must have a 'name' field")
620
650
 
621
651
  has_rgb = "rgb" in entry and "path" in entry.get("rgb", {})
622
- has_depth = "depth" in entry and "path" in entry.get("depth", {})
652
+ depth_key, _ = _prediction_depth_entry(entry, label=label)
653
+ has_depth = depth_key is not None
623
654
  has_rays = "rays" in entry and "path" in entry.get("rays", {})
624
655
  if not has_rgb and not has_depth and not has_rays:
625
656
  raise ValueError(
626
- f"{label} must have at least 'rgb.path', 'depth.path', or 'rays.path'"
657
+ f"{label} must have at least 'rgb.path', 'depth.path', "
658
+ "'relative_depth.path', 'affine_depth.path', or 'rays.path'"
627
659
  )
628
660
 
629
- for modality in ("rgb", "depth", "rays"):
661
+ for modality in ("rgb", *_PRED_DEPTH_KEYS, "rays"):
630
662
  if modality in entry and "path" in entry[modality]:
631
663
  p = normalize_modality_path(
632
664
  entry[modality]["path"],
@@ -751,21 +783,24 @@ def save_results(
751
783
  """
752
784
  output_file = dataset_config.get("output_file")
753
785
  if output_file is None:
786
+ selected_modality = modality
787
+ if selected_modality == "depth" and "depth" not in dataset_config:
788
+ selected_modality, _ = _prediction_depth_entry(dataset_config)
754
789
  if (
755
- modality is not None
756
- and modality in dataset_config
757
- and "path" in dataset_config[modality]
790
+ selected_modality is not None
791
+ and selected_modality in dataset_config
792
+ and "path" in dataset_config[selected_modality]
758
793
  ):
759
794
  output_file = (
760
795
  normalize_modality_path(
761
- dataset_config[modality]["path"],
762
- split=dataset_config[modality].get("split"),
796
+ dataset_config[selected_modality]["path"],
797
+ split=dataset_config[selected_modality].get("split"),
763
798
  )
764
799
  / "eval.json"
765
800
  )
766
801
  else:
767
802
  # Default: save alongside first available modality path
768
- for mod in ("depth", "rgb", "rays"):
803
+ for mod in (*_PRED_DEPTH_KEYS, "rgb", "rays"):
769
804
  if mod in dataset_config and "path" in dataset_config[mod]:
770
805
  output_file = (
771
806
  normalize_modality_path(
@@ -988,7 +1023,8 @@ def main():
988
1023
  # Evaluate each prediction dataset
989
1024
  for dataset_config in config["datasets"]:
990
1025
  ds_name = dataset_config["name"]
991
- has_depth = "depth" in dataset_config and "path" in dataset_config["depth"]
1026
+ pred_depth_key, pred_depth_config = _prediction_depth_entry(dataset_config)
1027
+ has_depth = pred_depth_config is not None
992
1028
  has_rgb = "rgb" in dataset_config and "path" in dataset_config["rgb"]
993
1029
  has_rays = "rays" in dataset_config and "path" in dataset_config["rays"]
994
1030
 
@@ -1011,17 +1047,22 @@ def main():
1011
1047
  and not gt_sparse_depth_path
1012
1048
  and not args.skip_depth
1013
1049
  ):
1014
- pred_depth_path = dataset_config["depth"]["path"]
1015
- pred_depth_split = dataset_config["depth"].get("split")
1050
+ pred_depth_path = pred_depth_config["path"]
1051
+ pred_depth_split = pred_depth_config.get("split")
1016
1052
  print(f"\n[DEPTH] Evaluating: '{ds_name}'")
1017
1053
  print(f" GT: {gt_depth_path}")
1018
1054
  print(f" Pred: {pred_depth_path}")
1055
+ if pred_depth_key != "depth":
1056
+ print(f" Pred depth entry: {pred_depth_key}")
1019
1057
 
1020
1058
  depth_dataset = build_depth_eval_dataset(
1021
1059
  gt_depth_path=gt_depth_path,
1022
1060
  pred_depth_path=pred_depth_path,
1023
1061
  calibration_path=calibration_path,
1024
1062
  segmentation_path=segmentation_path,
1063
+ pred_depth_metadata_scope=(
1064
+ pred_depth_key if pred_depth_key != "depth" else None
1065
+ ),
1025
1066
  gt_depth_split=gt_depth_split,
1026
1067
  pred_depth_split=pred_depth_split,
1027
1068
  calibration_split=calibration_split,
@@ -1050,6 +1091,7 @@ def main():
1050
1091
  if args.benchmark_depth_range
1051
1092
  else None
1052
1093
  ),
1094
+ input_space_hint=_prediction_depth_space_hint(pred_depth_key),
1053
1095
  )
1054
1096
 
1055
1097
  if sanity_checker is not None:
@@ -1101,6 +1143,7 @@ def main():
1101
1143
  "pred": {
1102
1144
  "path": pred_depth_path,
1103
1145
  "split": pred_depth_split,
1146
+ "entry": pred_depth_key,
1104
1147
  "dimensions": depth_spatial.get("pred_dimensions"),
1105
1148
  },
1106
1149
  "spatial_alignment": {
@@ -1190,11 +1233,13 @@ def main():
1190
1233
  # -- Sparse pointcloud depth evaluation --
1191
1234
  sparse_depth_dataset = None
1192
1235
  if has_depth and gt_sparse_depth_path and not args.skip_depth:
1193
- pred_depth_path = dataset_config["depth"]["path"]
1194
- pred_depth_split = dataset_config["depth"].get("split")
1236
+ pred_depth_path = pred_depth_config["path"]
1237
+ pred_depth_split = pred_depth_config.get("split")
1195
1238
  print(f"\n[SPARSE_DEPTH] Evaluating: '{ds_name}'")
1196
1239
  print(f" GT sparse pointcloud: {gt_sparse_depth_path}")
1197
1240
  print(f" Pred dense depth: {pred_depth_path}")
1241
+ if pred_depth_key != "depth":
1242
+ print(f" Pred depth entry: {pred_depth_key}")
1198
1243
  print(f" Intrinsics: {intrinsics_path}")
1199
1244
  print(f" Camera extrinsics: {camera_extrinsics_path}")
1200
1245
 
@@ -1204,6 +1249,9 @@ def main():
1204
1249
  intrinsics_path=intrinsics_path,
1205
1250
  camera_extrinsics_path=camera_extrinsics_path,
1206
1251
  segmentation_path=segmentation_path,
1252
+ pred_depth_metadata_scope=(
1253
+ pred_depth_key if pred_depth_key != "depth" else None
1254
+ ),
1207
1255
  gt_sparse_depth_split=gt_sparse_depth_split,
1208
1256
  pred_depth_split=pred_depth_split,
1209
1257
  intrinsics_split=intrinsics_split,
@@ -1231,6 +1279,7 @@ def main():
1231
1279
  if args.benchmark_depth_range
1232
1280
  else None
1233
1281
  ),
1282
+ input_space_hint=_prediction_depth_space_hint(pred_depth_key),
1234
1283
  )
1235
1284
 
1236
1285
  if sanity_checker is not None:
@@ -1277,6 +1326,7 @@ def main():
1277
1326
  "pred": {
1278
1327
  "path": pred_depth_path,
1279
1328
  "split": pred_depth_split,
1329
+ "entry": pred_depth_key,
1280
1330
  "dimensions": sparse_depth_spatial.get("pred_dimensions"),
1281
1331
  },
1282
1332
  "calibration": {
@@ -1632,7 +1682,11 @@ def main():
1632
1682
 
1633
1683
  # Save per-modality results to respective dataset paths
1634
1684
  if depth_save:
1635
- depth_out = save_results(depth_save, dataset_config, modality="depth")
1685
+ depth_out = save_results(
1686
+ depth_save,
1687
+ dataset_config,
1688
+ modality=pred_depth_key or "depth",
1689
+ )
1636
1690
  depth_label = (
1637
1691
  "Sparse depth"
1638
1692
  if _SPARSE_DEPTH_METRIC_ROOT in depth_save
@@ -107,6 +107,7 @@ def build_modality(
107
107
  path: str | Path,
108
108
  modality_key: str,
109
109
  split: str | None = None,
110
+ metadata_scope: str | None = None,
110
111
  loader=None,
111
112
  used_as: str | None = None,
112
113
  ) -> Modality:
@@ -114,6 +115,7 @@ def build_modality(
114
115
  parsed = parse_modality_path(
115
116
  path,
116
117
  split=split,
118
+ metadata_scope=metadata_scope,
117
119
  )
118
120
  return Modality(
119
121
  path=parsed.path,
@@ -571,6 +571,7 @@ def _modality(
571
571
  path: str,
572
572
  modality_key: str,
573
573
  split: Optional[str] = None,
574
+ metadata_scope: Optional[str] = None,
574
575
  loader: Optional[Callable[..., Any]] = None,
575
576
  used_as: Optional[str] = None,
576
577
  ) -> Modality:
@@ -580,6 +581,7 @@ def _modality(
580
581
  loader=loader,
581
582
  used_as=used_as,
582
583
  modality_key=modality_key,
584
+ metadata_scope=metadata_scope,
583
585
  split=split,
584
586
  )
585
587
 
@@ -593,6 +595,7 @@ def build_depth_eval_dataset(
593
595
  pred_depth_split: Optional[str] = None,
594
596
  calibration_split: Optional[str] = None,
595
597
  segmentation_split: Optional[str] = None,
598
+ pred_depth_metadata_scope: Optional[str] = None,
596
599
  ) -> MultiModalDataset:
597
600
  """Build a MultiModalDataset for depth evaluation.
598
601
 
@@ -611,6 +614,9 @@ def build_depth_eval_dataset(
611
614
  pred_depth_split: Optional split name for the prediction depth modality.
612
615
  calibration_split: Optional split name for the calibration modality.
613
616
  segmentation_split: Optional split name for the segmentation modality.
617
+ pred_depth_metadata_scope: Optional metadata scope for the prediction
618
+ depth modality, useful when loading depth-like outputs such as
619
+ ``relative_depth`` or ``affine_depth`` through the depth loader.
614
620
 
615
621
  Returns:
616
622
  A MultiModalDataset instance.
@@ -624,6 +630,7 @@ def build_depth_eval_dataset(
624
630
  "pred": _modality(
625
631
  path=pred_depth_path,
626
632
  modality_key="depth",
633
+ metadata_scope=pred_depth_metadata_scope,
627
634
  used_as="output",
628
635
  split=pred_depth_split,
629
636
  ),
@@ -662,12 +669,16 @@ def build_sparse_depth_eval_dataset(
662
669
  intrinsics_split: Optional[str] = None,
663
670
  camera_extrinsics_split: Optional[str] = None,
664
671
  segmentation_split: Optional[str] = None,
672
+ pred_depth_metadata_scope: Optional[str] = None,
665
673
  ) -> MultiModalDataset:
666
674
  """Build a MultiModalDataset for sparse pointcloud depth evaluation.
667
675
 
668
676
  The returned dataset yields dense predicted depth under ``"pred"``,
669
677
  sparse GT point clouds under ``"gt"``, and hierarchical camera
670
678
  calibration under ``"intrinsics"`` and ``"camera_extrinsics"``.
679
+ ``pred_depth_metadata_scope`` can select a depth-like prediction scope
680
+ such as ``relative_depth`` or ``affine_depth`` while still loading it
681
+ as a dense depth map.
671
682
  """
672
683
  modalities = {
673
684
  "gt": _modality(
@@ -678,6 +689,7 @@ def build_sparse_depth_eval_dataset(
678
689
  "pred": _modality(
679
690
  path=pred_depth_path,
680
691
  modality_key="depth",
692
+ metadata_scope=pred_depth_metadata_scope,
681
693
  used_as="output",
682
694
  split=pred_depth_split,
683
695
  ),
@@ -552,6 +552,7 @@ def evaluate_depth_samples(
552
552
  sky_mask_enabled: bool = False,
553
553
  alignment_mode: str = "auto_affine",
554
554
  benchmark_depth_range: Optional[tuple[float, float]] = None,
555
+ input_space_hint: Optional[str] = None,
555
556
  ) -> dict:
556
557
  """Evaluate all depth metrics from a MultiModalDataset.
557
558
 
@@ -574,6 +575,9 @@ def evaluate_depth_samples(
574
575
  benchmark_depth_range: Optional ``(min_meters, max_meters)`` tuple.
575
576
  When set, also computes depth metrics for pixels within this
576
577
  range, subdivided into square-root-scaled near/mid/far bins.
578
+ input_space_hint: Optional declaration from the prediction config.
579
+ ``relative`` or ``affine`` makes ``auto_affine`` run alignment
580
+ even when values are outside the normalized range.
577
581
 
578
582
  Returns:
579
583
  Dictionary containing depth aggregate/per-file metrics with:
@@ -587,6 +591,12 @@ def evaluate_depth_samples(
587
591
  f"Invalid alignment_mode '{alignment_mode}'. "
588
592
  f"Expected one of {sorted(valid_alignment_modes)}."
589
593
  )
594
+ valid_input_space_hints = {None, "relative", "affine"}
595
+ if input_space_hint not in valid_input_space_hints:
596
+ raise ValueError(
597
+ f"Invalid input_space_hint '{input_space_hint}'. "
598
+ "Expected None, 'relative', or 'affine'."
599
+ )
590
600
 
591
601
  num_samples = len(dataset)
592
602
  if num_samples == 0:
@@ -1007,7 +1017,14 @@ def evaluate_depth_samples(
1007
1017
  if i == 0:
1008
1018
  pred_min = float(np.nanmin(depth_pred))
1009
1019
  pred_max = float(np.nanmax(depth_pred))
1010
- if pred_max <= 1.0 + 1e-3 and pred_min >= -1.0 - 1e-3:
1020
+ if input_space_hint in {"relative", "affine"}:
1021
+ normalized_predictions = True
1022
+ input_space_detected = input_space_hint
1023
+ print(
1024
+ f" Detected native depth space: {input_space_hint} "
1025
+ f"(declared; range [{pred_min:.3f}, {pred_max:.3f}])"
1026
+ )
1027
+ elif pred_max <= 1.0 + 1e-3 and pred_min >= -1.0 - 1e-3:
1011
1028
  normalized_predictions = True
1012
1029
  input_space_detected = "normalized"
1013
1030
  print(
@@ -1496,6 +1513,7 @@ def evaluate_sparse_depth_samples(
1496
1513
  sky_mask_enabled: bool = False,
1497
1514
  alignment_mode: str = "auto_affine",
1498
1515
  benchmark_depth_range: Optional[tuple[float, float]] = None,
1516
+ input_space_hint: Optional[str] = None,
1499
1517
  ) -> dict:
1500
1518
  """Evaluate dense depth predictions against sparse pointcloud GT.
1501
1519
 
@@ -1510,6 +1528,12 @@ def evaluate_sparse_depth_samples(
1510
1528
  f"Invalid alignment_mode '{alignment_mode}'. "
1511
1529
  f"Expected one of {sorted(valid_alignment_modes)}."
1512
1530
  )
1531
+ valid_input_space_hints = {None, "relative", "affine"}
1532
+ if input_space_hint not in valid_input_space_hints:
1533
+ raise ValueError(
1534
+ f"Invalid input_space_hint '{input_space_hint}'. "
1535
+ "Expected None, 'relative', or 'affine'."
1536
+ )
1513
1537
 
1514
1538
  num_samples = len(dataset)
1515
1539
  if num_samples == 0:
@@ -1700,7 +1724,14 @@ def evaluate_sparse_depth_samples(
1700
1724
  if i == 0:
1701
1725
  pred_native_dims = depth_pred.shape[:2]
1702
1726
  finite_pred = depth_pred[np.isfinite(depth_pred)]
1703
- if finite_pred.size and finite_pred.max() <= 1.0 + 1e-3 and finite_pred.min() >= -1.0 - 1e-3:
1727
+ if input_space_hint in {"relative", "affine"} and finite_pred.size:
1728
+ normalized_predictions = True
1729
+ input_space_detected = input_space_hint
1730
+ print(
1731
+ f" Detected native depth space: {input_space_hint} "
1732
+ f"(declared; range [{float(finite_pred.min()):.3f}, {float(finite_pred.max()):.3f}])"
1733
+ )
1734
+ elif finite_pred.size and finite_pred.max() <= 1.0 + 1e-3 and finite_pred.min() >= -1.0 - 1e-3:
1704
1735
  normalized_predictions = True
1705
1736
  input_space_detected = "normalized"
1706
1737
  print(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.11.0
3
+ Version: 2.12.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -241,7 +241,7 @@ 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 still uses `datasets[].depth` because the model output is a dense depth map. 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.
245
245
 
246
246
  #### GT section
247
247
 
@@ -261,17 +261,21 @@ For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.dep
261
261
 
262
262
  #### Prediction datasets
263
263
 
264
- Each entry in `datasets` can include `rgb`, `depth`, `rays`, or any combination:
264
+ Each entry in `datasets` can include `rgb`, one dense depth-like prediction (`depth`, `relative_depth`, or `affine_depth`), `rays`, or any combination:
265
265
 
266
266
  | Field | Required | Description |
267
267
  |---|---|---|
268
268
  | `name` | yes | Display name for this prediction dataset |
269
269
  | `rgb.path` | no\* | Path to predicted RGB dataset |
270
- | `depth.path` | no\* | Path to predicted dense depth dataset; also used when evaluating against sparse pointcloud GT |
270
+ | `depth.path` | no\* | Path to predicted dense metric depth dataset; also used when evaluating against sparse pointcloud GT |
271
+ | `relative_depth.path` | no\* | Path to predicted dense relative depth dataset; evaluated through the same depth pipeline with scale/shift alignment support |
272
+ | `affine_depth.path` | no\* | Path to predicted dense affine-depth dataset; evaluated through the same depth pipeline with scale/shift alignment support |
271
273
  | `rays.path` | no\* | Path to predicted ray direction map dataset |
272
274
  | `output_file` | no | Custom output path for results JSON (default: `eval.json` inside the first available modality path) |
273
275
 
274
- \* At least one of `rgb.path`, `depth.path`, or `rays.path` is required.
276
+ \* At least one of `rgb.path`, `depth.path`, `relative_depth.path`, `affine_depth.path`, or `rays.path` is required. Use only one dense depth-like entry per prediction dataset.
277
+
278
+ When `relative_depth` or `affine_depth` is used, `--depth-alignment auto_affine` treats the entry as declared non-metric depth and runs scale/shift alignment even if the raw value range is not normalized. `--depth-alignment none` still disables calibration.
275
279
 
276
280
  #### `euler_train` section (optional)
277
281
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-eval"
7
- version = "2.11.0"
7
+ version = "2.12.0"
8
8
  description = "Depth map evaluation toolkit with comprehensive metrics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -127,6 +127,35 @@ class TestValidateDatasetEntry:
127
127
  }
128
128
  validate_dataset_entry(entry, 0)
129
129
 
130
+ def test_valid_relative_depth_only(self, tmp_path):
131
+ depth_path = tmp_path / "relative_depth"
132
+ depth_path.mkdir()
133
+ entry = {
134
+ "name": "model_a",
135
+ "relative_depth": {"path": str(depth_path)},
136
+ }
137
+ validate_dataset_entry(entry, 0)
138
+
139
+ def test_valid_affine_depth_only(self, tmp_path):
140
+ depth_path = tmp_path / "affine_depth"
141
+ depth_path.mkdir()
142
+ entry = {
143
+ "name": "model_a",
144
+ "affine_depth": {"path": str(depth_path)},
145
+ }
146
+ validate_dataset_entry(entry, 0)
147
+
148
+ def test_multiple_depth_prediction_entries_are_rejected(self, tmp_path):
149
+ for name in ("depth", "relative_depth"):
150
+ (tmp_path / name).mkdir()
151
+ entry = {
152
+ "name": "model_a",
153
+ "depth": {"path": str(tmp_path / "depth")},
154
+ "relative_depth": {"path": str(tmp_path / "relative_depth")},
155
+ }
156
+ with pytest.raises(ValueError, match="multiple dense depth entries"):
157
+ validate_dataset_entry(entry, 0)
158
+
130
159
  def test_valid_rgb_only(self, tmp_path):
131
160
  rgb_path = tmp_path / "rgb"
132
161
  rgb_path.mkdir()
@@ -15,8 +15,10 @@ def _install_captured_dataset(monkeypatch):
15
15
  monkeypatch.setattr(data, "MultiModalDataset", _CapturedDataset)
16
16
 
17
17
 
18
- def _assert_modality(modality, *, key, split=None, used_as=None, loader=None):
19
- assert modality.metadata_scope == key
18
+ def _assert_modality(
19
+ modality, *, key, split=None, used_as=None, loader=None, scope=None
20
+ ):
21
+ assert modality.metadata_scope == (scope or key)
20
22
  assert modality.modality_type == key
21
23
  assert modality.split == split
22
24
  assert modality.used_as == used_as
@@ -100,6 +102,25 @@ def test_sparse_depth_builder_sets_projection_modality_scopes(monkeypatch):
100
102
  )
101
103
 
102
104
 
105
+ def test_sparse_depth_builder_can_load_relative_depth_prediction_scope(monkeypatch):
106
+ _install_captured_dataset(monkeypatch)
107
+
108
+ dataset = data.build_sparse_depth_eval_dataset(
109
+ gt_sparse_depth_path="/datasets/shared",
110
+ pred_depth_path="/predictions/shared",
111
+ intrinsics_path="/datasets/shared",
112
+ camera_extrinsics_path="/datasets/shared",
113
+ pred_depth_metadata_scope="relative_depth",
114
+ )
115
+
116
+ _assert_modality(
117
+ dataset.modalities["pred"],
118
+ key="depth",
119
+ scope="relative_depth",
120
+ used_as="output",
121
+ )
122
+
123
+
103
124
  def test_rgb_builder_sets_rgb_and_auxiliary_modality_scopes(monkeypatch):
104
125
  _install_captured_dataset(monkeypatch)
105
126
 
@@ -126,6 +126,18 @@ class TestSaveResultsDirectory:
126
126
  assert out.exists()
127
127
  assert json.loads(out.read_text()) == results
128
128
 
129
+ def test_saves_to_relative_depth_path(self, tmp_path):
130
+ depth_dir = tmp_path / "pred_relative_depth"
131
+ depth_dir.mkdir()
132
+
133
+ results = {"depth": {"rmse": 0.5}}
134
+ config = {"name": "model", "relative_depth": {"path": str(depth_dir)}}
135
+ out = save_results(results, config, modality="depth")
136
+
137
+ assert out == depth_dir / "eval.json"
138
+ assert out.exists()
139
+ assert json.loads(out.read_text()) == results
140
+
129
141
  def test_saves_to_rgb_path_when_no_depth(self, tmp_path):
130
142
  rgb_dir = tmp_path / "pred_rgb"
131
143
  rgb_dir.mkdir()
@@ -99,6 +99,22 @@ class _ZeroValidSparseDataset(_OneSampleSparseDataset):
99
99
  self.pred = np.zeros((3, 3), dtype=np.float32)
100
100
 
101
101
 
102
+ class _RelativeSparseDataset(_OneSampleSparseDataset):
103
+ def __init__(self):
104
+ super().__init__()
105
+ self.pred = np.zeros((3, 3), dtype=np.float32)
106
+ self.pred[1, 1] = (np.sqrt(12.0) - 3.0) / 2.0
107
+ self.pred[1, 2] = (np.sqrt(24.0) - 3.0) / 2.0
108
+
109
+
110
+ class _AffineSparseDataset(_OneSampleSparseDataset):
111
+ def __init__(self):
112
+ super().__init__()
113
+ self.pred = np.zeros((3, 3), dtype=np.float32)
114
+ self.pred[1, 1] = np.sqrt(12.0) * 10.0 + 7.0
115
+ self.pred[1, 2] = np.sqrt(24.0) * 10.0 + 7.0
116
+
117
+
102
118
  def test_sparse_depth_eval_reports_only_pointwise_depth_metrics():
103
119
  dataset = _OneSampleSparseDataset()
104
120
 
@@ -119,6 +135,50 @@ def test_sparse_depth_eval_reports_only_pointwise_depth_metrics():
119
135
  np.testing.assert_allclose(metrics["depth_metrics"]["rmse"]["median"], 0.0)
120
136
 
121
137
 
138
+ def test_sparse_depth_eval_auto_affine_aligns_relative_depth():
139
+ result = evaluate_sparse_depth_samples(
140
+ _RelativeSparseDataset(),
141
+ pred_is_radial=True,
142
+ num_workers=0,
143
+ alignment_mode="auto_affine",
144
+ )
145
+
146
+ assert result["space_info"]["input_space_detected"] == "normalized"
147
+ assert result["space_info"]["calibration_applied"] is True
148
+ assert result["space_info"]["emitted_spaces"] == ["native", "metric"]
149
+
150
+ native_rmse = result["sparse_depth_native"]["depth_metrics"]["rmse"]["median"]
151
+ metric_rmse = result["sparse_depth_metric"]["depth_metrics"]["rmse"]["median"]
152
+ assert metric_rmse < native_rmse
153
+ np.testing.assert_allclose(metric_rmse, 0.0, atol=1e-6)
154
+
155
+ per_file = result["per_file_metrics"]["files"][0]["metrics"]
156
+ assert "sparse_depth_native" in per_file
157
+ assert "sparse_depth_metric" in per_file
158
+ assert (
159
+ per_file["sparse_depth_metric"]["depth_metrics"]["rmse"]
160
+ < per_file["sparse_depth_native"]["depth_metrics"]["rmse"]
161
+ )
162
+
163
+
164
+ def test_sparse_depth_eval_auto_affine_uses_declared_affine_depth_hint():
165
+ result = evaluate_sparse_depth_samples(
166
+ _AffineSparseDataset(),
167
+ pred_is_radial=True,
168
+ num_workers=0,
169
+ alignment_mode="auto_affine",
170
+ input_space_hint="affine",
171
+ )
172
+
173
+ assert result["space_info"]["input_space_detected"] == "affine"
174
+ assert result["space_info"]["calibration_applied"] is True
175
+
176
+ native_rmse = result["sparse_depth_native"]["depth_metrics"]["rmse"]["median"]
177
+ metric_rmse = result["sparse_depth_metric"]["depth_metrics"]["rmse"]["median"]
178
+ assert metric_rmse < native_rmse
179
+ np.testing.assert_allclose(metric_rmse, 0.0, atol=1e-5)
180
+
181
+
122
182
  def test_sparse_depth_per_file_reports_zero_valid_support():
123
183
  result = evaluate_sparse_depth_samples(
124
184
  _ZeroValidSparseDataset(),
File without changes
File without changes