euler-eval 2.10.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.10.0 → euler_eval-2.12.0}/PKG-INFO +9 -5
  2. {euler_eval-2.10.0 → euler_eval-2.12.0}/README.md +8 -4
  3. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/cli.py +74 -40
  4. euler_eval-2.12.0/euler_eval/config_paths.py +127 -0
  5. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/data.py +15 -3
  6. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/evaluate.py +33 -2
  7. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval.egg-info/PKG-INFO +9 -5
  8. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval.egg-info/SOURCES.txt +1 -0
  9. {euler_eval-2.10.0 → euler_eval-2.12.0}/pyproject.toml +1 -1
  10. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_config.py +48 -0
  11. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_data_builders.py +34 -2
  12. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_save_results.py +27 -0
  13. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_sparse_depth.py +60 -0
  14. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/__init__.py +0 -0
  15. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/__init__.py +0 -0
  16. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/absrel.py +0 -0
  17. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/daniel_error.py +0 -0
  18. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  19. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  20. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/depth_standard.py +0 -0
  21. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/fid_kid.py +0 -0
  22. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  23. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  24. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  25. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/lpips_metric.py +0 -0
  26. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/normal_consistency.py +0 -0
  27. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/psnr.py +0 -0
  28. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  29. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  30. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  31. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/rho_a.py +0 -0
  32. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/rmse.py +0 -0
  33. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  34. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/ssim.py +0 -0
  35. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/tail_errors.py +0 -0
  36. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/metrics/utils.py +0 -0
  37. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/sanity_checker.py +0 -0
  38. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  39. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  40. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval.egg-info/entry_points.txt +0 -0
  41. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval.egg-info/requires.txt +0 -0
  42. {euler_eval-2.10.0 → euler_eval-2.12.0}/euler_eval.egg-info/top_level.txt +0 -0
  43. {euler_eval-2.10.0 → euler_eval-2.12.0}/init_cache.py +0 -0
  44. {euler_eval-2.10.0 → euler_eval-2.12.0}/setup.cfg +0 -0
  45. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_alignment.py +0 -0
  46. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_cli_device.py +0 -0
  47. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_data.py +0 -0
  48. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_depth_alignment_output.py +0 -0
  49. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_depth_standard.py +0 -0
  50. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_evaluate_helpers.py +0 -0
  51. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_init_cache.py +0 -0
  52. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_integration.py +0 -0
  53. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_meta_output.py +0 -0
  54. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_metrics_utils.py +0 -0
  55. {euler_eval-2.10.0 → euler_eval-2.12.0}/tests/test_rgb_fid_output.py +0 -0
  56. {euler_eval-2.10.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.10.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
 
@@ -13,8 +13,8 @@ import zipfile
13
13
  from pathlib import Path
14
14
 
15
15
  import torch
16
- from euler_loading import Modality
17
16
 
17
+ from .config_paths import normalize_modality_path
18
18
  from .data import (
19
19
  build_depth_eval_dataset,
20
20
  build_rays_eval_dataset,
@@ -41,22 +41,6 @@ except ImportError:
41
41
  from euler_metric_naming import AxisDeclaration, MetricDescription, MetricNamespace
42
42
 
43
43
 
44
- def _normalize_modality_path(
45
- path: str | Path,
46
- *,
47
- modality_key: str,
48
- split: str | None = None,
49
- ) -> Path:
50
- """Return the filesystem/archive root for an euler-loading modality path."""
51
- modality = Modality(
52
- str(path),
53
- modality_type=modality_key,
54
- metadata_scope=modality_key,
55
- split=split,
56
- )
57
- return Path(modality.path)
58
-
59
-
60
44
  # ── Eval namespace ──────────────────────────────────────────────────────────
61
45
  # MetricNamespace subclass for eval context with custom axis declarations,
62
46
  # following the Normed Metric Namespacing convention (§3.6, §4, §5.2).
@@ -167,6 +151,8 @@ def _rgb_eval_axes(*, benchmark: bool = False) -> dict[str, AxisDeclaration]:
167
151
 
168
152
  _RAYS_EVAL_AXES: dict[str, AxisDeclaration] = {}
169
153
 
154
+ _PRED_DEPTH_KEYS = ("depth", "relative_depth", "affine_depth")
155
+
170
156
  # Downstream eval consumers validate metricSet.metricNamespace with a stricter
171
157
  # first-segment rule than euler_metric_naming's modality validator. Keep the
172
158
  # public Python sparse_depth result keys, but use a wire-compatible metric root
@@ -578,6 +564,34 @@ def print_device_info(requested_device: str, resolved_device: str) -> None:
578
564
  pass
579
565
 
580
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
+
581
595
  def validate_gt_config(gt: dict) -> None:
582
596
  """Validate the ``gt`` section of the configuration.
583
597
 
@@ -616,9 +630,8 @@ def validate_gt_config(gt: dict) -> None:
616
630
  "camera_extrinsics",
617
631
  ):
618
632
  if modality in gt and "path" in gt[modality]:
619
- p = _normalize_modality_path(
633
+ p = normalize_modality_path(
620
634
  gt[modality]["path"],
621
- modality_key=modality,
622
635
  split=gt[modality].get("split"),
623
636
  )
624
637
  if not p.exists():
@@ -636,18 +649,19 @@ def validate_dataset_entry(entry: dict, index: int) -> None:
636
649
  raise ValueError(f"{label} must have a 'name' field")
637
650
 
638
651
  has_rgb = "rgb" in entry and "path" in entry.get("rgb", {})
639
- 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
640
654
  has_rays = "rays" in entry and "path" in entry.get("rays", {})
641
655
  if not has_rgb and not has_depth and not has_rays:
642
656
  raise ValueError(
643
- 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'"
644
659
  )
645
660
 
646
- for modality in ("rgb", "depth", "rays"):
661
+ for modality in ("rgb", *_PRED_DEPTH_KEYS, "rays"):
647
662
  if modality in entry and "path" in entry[modality]:
648
- p = _normalize_modality_path(
663
+ p = normalize_modality_path(
649
664
  entry[modality]["path"],
650
- modality_key=modality,
651
665
  split=entry[modality].get("split"),
652
666
  )
653
667
  if not p.exists():
@@ -769,27 +783,28 @@ def save_results(
769
783
  """
770
784
  output_file = dataset_config.get("output_file")
771
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)
772
789
  if (
773
- modality is not None
774
- and modality in dataset_config
775
- 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]
776
793
  ):
777
794
  output_file = (
778
- _normalize_modality_path(
779
- dataset_config[modality]["path"],
780
- modality_key=modality,
781
- split=dataset_config[modality].get("split"),
795
+ normalize_modality_path(
796
+ dataset_config[selected_modality]["path"],
797
+ split=dataset_config[selected_modality].get("split"),
782
798
  )
783
799
  / "eval.json"
784
800
  )
785
801
  else:
786
802
  # Default: save alongside first available modality path
787
- for mod in ("depth", "rgb", "rays"):
803
+ for mod in (*_PRED_DEPTH_KEYS, "rgb", "rays"):
788
804
  if mod in dataset_config and "path" in dataset_config[mod]:
789
805
  output_file = (
790
- _normalize_modality_path(
806
+ normalize_modality_path(
791
807
  dataset_config[mod]["path"],
792
- modality_key=mod,
793
808
  split=dataset_config[mod].get("split"),
794
809
  )
795
810
  / "eval.json"
@@ -1008,7 +1023,8 @@ def main():
1008
1023
  # Evaluate each prediction dataset
1009
1024
  for dataset_config in config["datasets"]:
1010
1025
  ds_name = dataset_config["name"]
1011
- 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
1012
1028
  has_rgb = "rgb" in dataset_config and "path" in dataset_config["rgb"]
1013
1029
  has_rays = "rays" in dataset_config and "path" in dataset_config["rays"]
1014
1030
 
@@ -1031,17 +1047,22 @@ def main():
1031
1047
  and not gt_sparse_depth_path
1032
1048
  and not args.skip_depth
1033
1049
  ):
1034
- pred_depth_path = dataset_config["depth"]["path"]
1035
- 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")
1036
1052
  print(f"\n[DEPTH] Evaluating: '{ds_name}'")
1037
1053
  print(f" GT: {gt_depth_path}")
1038
1054
  print(f" Pred: {pred_depth_path}")
1055
+ if pred_depth_key != "depth":
1056
+ print(f" Pred depth entry: {pred_depth_key}")
1039
1057
 
1040
1058
  depth_dataset = build_depth_eval_dataset(
1041
1059
  gt_depth_path=gt_depth_path,
1042
1060
  pred_depth_path=pred_depth_path,
1043
1061
  calibration_path=calibration_path,
1044
1062
  segmentation_path=segmentation_path,
1063
+ pred_depth_metadata_scope=(
1064
+ pred_depth_key if pred_depth_key != "depth" else None
1065
+ ),
1045
1066
  gt_depth_split=gt_depth_split,
1046
1067
  pred_depth_split=pred_depth_split,
1047
1068
  calibration_split=calibration_split,
@@ -1070,6 +1091,7 @@ def main():
1070
1091
  if args.benchmark_depth_range
1071
1092
  else None
1072
1093
  ),
1094
+ input_space_hint=_prediction_depth_space_hint(pred_depth_key),
1073
1095
  )
1074
1096
 
1075
1097
  if sanity_checker is not None:
@@ -1121,6 +1143,7 @@ def main():
1121
1143
  "pred": {
1122
1144
  "path": pred_depth_path,
1123
1145
  "split": pred_depth_split,
1146
+ "entry": pred_depth_key,
1124
1147
  "dimensions": depth_spatial.get("pred_dimensions"),
1125
1148
  },
1126
1149
  "spatial_alignment": {
@@ -1210,11 +1233,13 @@ def main():
1210
1233
  # -- Sparse pointcloud depth evaluation --
1211
1234
  sparse_depth_dataset = None
1212
1235
  if has_depth and gt_sparse_depth_path and not args.skip_depth:
1213
- pred_depth_path = dataset_config["depth"]["path"]
1214
- 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")
1215
1238
  print(f"\n[SPARSE_DEPTH] Evaluating: '{ds_name}'")
1216
1239
  print(f" GT sparse pointcloud: {gt_sparse_depth_path}")
1217
1240
  print(f" Pred dense depth: {pred_depth_path}")
1241
+ if pred_depth_key != "depth":
1242
+ print(f" Pred depth entry: {pred_depth_key}")
1218
1243
  print(f" Intrinsics: {intrinsics_path}")
1219
1244
  print(f" Camera extrinsics: {camera_extrinsics_path}")
1220
1245
 
@@ -1224,6 +1249,9 @@ def main():
1224
1249
  intrinsics_path=intrinsics_path,
1225
1250
  camera_extrinsics_path=camera_extrinsics_path,
1226
1251
  segmentation_path=segmentation_path,
1252
+ pred_depth_metadata_scope=(
1253
+ pred_depth_key if pred_depth_key != "depth" else None
1254
+ ),
1227
1255
  gt_sparse_depth_split=gt_sparse_depth_split,
1228
1256
  pred_depth_split=pred_depth_split,
1229
1257
  intrinsics_split=intrinsics_split,
@@ -1251,6 +1279,7 @@ def main():
1251
1279
  if args.benchmark_depth_range
1252
1280
  else None
1253
1281
  ),
1282
+ input_space_hint=_prediction_depth_space_hint(pred_depth_key),
1254
1283
  )
1255
1284
 
1256
1285
  if sanity_checker is not None:
@@ -1297,6 +1326,7 @@ def main():
1297
1326
  "pred": {
1298
1327
  "path": pred_depth_path,
1299
1328
  "split": pred_depth_split,
1329
+ "entry": pred_depth_key,
1300
1330
  "dimensions": sparse_depth_spatial.get("pred_dimensions"),
1301
1331
  },
1302
1332
  "calibration": {
@@ -1652,7 +1682,11 @@ def main():
1652
1682
 
1653
1683
  # Save per-modality results to respective dataset paths
1654
1684
  if depth_save:
1655
- 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
+ )
1656
1690
  depth_label = (
1657
1691
  "Sparse depth"
1658
1692
  if _SPARSE_DEPTH_METRIC_ROOT in depth_save
@@ -0,0 +1,127 @@
1
+ """Helpers for euler-loading modality path selectors in config files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from urllib.parse import parse_qsl
8
+
9
+ from ds_crawler import validate_metadata_scope
10
+ from ds_crawler.zip_utils import validate_split_name
11
+ from euler_loading import Modality
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ModalityPath:
16
+ path: str
17
+ split: str | None = None
18
+ metadata_scope: str | None = None
19
+
20
+
21
+ def _parse_inline_scope(path: str) -> tuple[str, str | None]:
22
+ candidate_path, sep, fragment = path.rpartition("#")
23
+ if not sep:
24
+ return path, None
25
+
26
+ params = parse_qsl(fragment, keep_blank_values=True)
27
+ scope_values = [value for key, value in params if key == "scope"]
28
+ if not scope_values:
29
+ return path, None
30
+
31
+ unsupported = [key for key, _ in params if key != "scope"]
32
+ if unsupported:
33
+ raise ValueError(
34
+ "Unsupported modality path fragment parameter(s): "
35
+ f"{', '.join(sorted(set(unsupported)))}. "
36
+ "Only '#scope=<metadata_scope>' is supported."
37
+ )
38
+ if len(scope_values) > 1:
39
+ raise ValueError("Modality path may contain at most one '#scope=' selector.")
40
+
41
+ return candidate_path, validate_metadata_scope(scope_values[0])
42
+
43
+
44
+ def _parse_inline_split(path: str) -> tuple[str, str | None]:
45
+ colon_pos = path.rfind(":")
46
+ if colon_pos <= 1:
47
+ return path, None
48
+
49
+ candidate_split = path[colon_pos + 1 :]
50
+ candidate_path = path[:colon_pos]
51
+
52
+ try:
53
+ split = validate_split_name(candidate_split)
54
+ except ValueError:
55
+ return path, None
56
+
57
+ return candidate_path, split
58
+
59
+
60
+ def parse_modality_path(
61
+ path: str | Path,
62
+ *,
63
+ split: str | None = None,
64
+ metadata_scope: str | None = None,
65
+ ) -> ModalityPath:
66
+ """Parse euler-loading inline split/scope selectors from a config path."""
67
+ raw_path = str(path)
68
+ parsed_path, inline_scope = _parse_inline_scope(raw_path)
69
+ parsed_path, inline_split = _parse_inline_split(parsed_path)
70
+
71
+ if inline_split is not None and split is not None:
72
+ raise ValueError(
73
+ f"Modality path {raw_path!r} contains an inline split "
74
+ f"({inline_split!r}) but an explicit split={split!r} was also "
75
+ "provided. Use one or the other, not both."
76
+ )
77
+
78
+ normalized_scope = (
79
+ validate_metadata_scope(metadata_scope)
80
+ if metadata_scope is not None
81
+ else None
82
+ )
83
+ if inline_scope is not None:
84
+ if normalized_scope is not None and normalized_scope != inline_scope:
85
+ raise ValueError(
86
+ f"Modality path {raw_path!r} contains an inline metadata scope "
87
+ f"({inline_scope!r}) but an explicit "
88
+ f"metadata_scope={metadata_scope!r} was also provided. Use one "
89
+ "metadata scope selector."
90
+ )
91
+ normalized_scope = inline_scope
92
+
93
+ return ModalityPath(
94
+ path=parsed_path,
95
+ split=split if split is not None else inline_split,
96
+ metadata_scope=normalized_scope,
97
+ )
98
+
99
+
100
+ def normalize_modality_path(path: str | Path, *, split: str | None = None) -> Path:
101
+ """Return the filesystem/archive root for a config modality path."""
102
+ return Path(parse_modality_path(path, split=split).path)
103
+
104
+
105
+ def build_modality(
106
+ *,
107
+ path: str | Path,
108
+ modality_key: str,
109
+ split: str | None = None,
110
+ metadata_scope: str | None = None,
111
+ loader=None,
112
+ used_as: str | None = None,
113
+ ) -> Modality:
114
+ """Create a Modality after parsing inline config selectors locally."""
115
+ parsed = parse_modality_path(
116
+ path,
117
+ split=split,
118
+ metadata_scope=metadata_scope,
119
+ )
120
+ return Modality(
121
+ path=parsed.path,
122
+ loader=loader,
123
+ used_as=used_as,
124
+ modality_type=modality_key,
125
+ metadata_scope=parsed.metadata_scope or modality_key,
126
+ split=parsed.split,
127
+ )
@@ -13,6 +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
17
  from .metrics.utils import convert_planar_to_radial
17
18
 
18
19
  # ---------------------------------------------------------------------------
@@ -570,16 +571,17 @@ def _modality(
570
571
  path: str,
571
572
  modality_key: str,
572
573
  split: Optional[str] = None,
574
+ metadata_scope: Optional[str] = None,
573
575
  loader: Optional[Callable[..., Any]] = None,
574
576
  used_as: Optional[str] = None,
575
577
  ) -> Modality:
576
578
  """Create an euler-loading modality with an explicit metadata selector."""
577
- return Modality(
579
+ return build_modality(
578
580
  path=path,
579
581
  loader=loader,
580
582
  used_as=used_as,
581
- modality_type=modality_key,
582
- metadata_scope=modality_key,
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.10.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
 
@@ -3,6 +3,7 @@ init_cache.py
3
3
  pyproject.toml
4
4
  euler_eval/__init__.py
5
5
  euler_eval/cli.py
6
+ euler_eval/config_paths.py
6
7
  euler_eval/data.py
7
8
  euler_eval/evaluate.py
8
9
  euler_eval/sanity_checker.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-eval"
7
- version = "2.10.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"
@@ -65,6 +65,25 @@ class TestValidateGtConfig:
65
65
  gt = {"rgb": {"path": f"{rgb_path}#scope=rgb"}}
66
66
  validate_gt_config(gt)
67
67
 
68
+ def test_inline_scope_intrinsics_gt_path_is_valid(self, tmp_path):
69
+ depth_path = tmp_path / "depth"
70
+ depth_path.mkdir()
71
+ intrinsics_path = tmp_path / "frame_camera_trainvaltest.zip"
72
+ with zipfile.ZipFile(intrinsics_path, "w") as zf:
73
+ zf.writestr("dummy.txt", "x")
74
+ gt = {
75
+ "depth": {"path": str(depth_path)},
76
+ "intrinsics": {"path": f"{intrinsics_path}#scope=intrinsics"},
77
+ }
78
+ validate_gt_config(gt)
79
+
80
+ def test_inline_split_and_scope_gt_path_is_valid(self, tmp_path):
81
+ rgb_path = tmp_path / "reference.zip"
82
+ with zipfile.ZipFile(rgb_path, "w") as zf:
83
+ zf.writestr("dummy.txt", "x")
84
+ gt = {"rgb": {"path": f"{rgb_path}:fog_day#scope=rgb"}}
85
+ validate_gt_config(gt)
86
+
68
87
  def test_sparse_depth_requires_projection_modalities(self, tmp_path):
69
88
  sparse_path = tmp_path / "sparse_depth"
70
89
  sparse_path.mkdir()
@@ -108,6 +127,35 @@ class TestValidateDatasetEntry:
108
127
  }
109
128
  validate_dataset_entry(entry, 0)
110
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
+
111
159
  def test_valid_rgb_only(self, tmp_path):
112
160
  rgb_path = tmp_path / "rgb"
113
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
 
@@ -192,3 +213,14 @@ def test_sky_mask_loader_resolution_strips_inline_split(monkeypatch):
192
213
  assert calls == [
193
214
  ("/datasets/shared.zip", {"metadata_scope": "segmentation"}),
194
215
  ]
216
+
217
+
218
+ def test_modality_parses_inline_split_and_scope():
219
+ modality = data._modality(
220
+ path="/datasets/frame_camera_trainvaltest.zip:fog_day#scope=intrinsics",
221
+ modality_key="intrinsics",
222
+ )
223
+
224
+ assert modality.path == "/datasets/frame_camera_trainvaltest.zip"
225
+ assert modality.split == "fog_day"
226
+ assert modality.metadata_scope == "intrinsics"
@@ -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()
@@ -229,6 +241,21 @@ class TestSaveResultsZip:
229
241
  stored = json.loads(zf.read("eval.json"))
230
242
  assert stored == results
231
243
 
244
+ def test_inline_zip_scope_writes_eval_json_into_zip(self, tmp_path):
245
+ zp = tmp_path / "dataset.zip"
246
+ with zipfile.ZipFile(zp, "w") as zf:
247
+ zf.writestr("image_001.png", b"\x89PNG")
248
+
249
+ results = {"rgb": {"psnr": 30.0}}
250
+ config = {"name": "model", "rgb": {"path": f"{zp}#scope=rgb"}}
251
+ out = save_results(results, config, modality="rgb")
252
+
253
+ assert out == zp / "eval.json"
254
+ with zipfile.ZipFile(zp, "r") as zf:
255
+ assert "eval.json" in zf.namelist()
256
+ stored = json.loads(zf.read("eval.json"))
257
+ assert stored == results
258
+
232
259
  def test_preserves_existing_zip_contents(self, tmp_path):
233
260
  zp = tmp_path / "dataset.zip"
234
261
  with zipfile.ZipFile(zp, "w") as zf:
@@ -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