euler-eval 2.26.0__tar.gz → 2.28.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 (74) hide show
  1. {euler_eval-2.26.0 → euler_eval-2.28.0}/PKG-INFO +3 -3
  2. {euler_eval-2.26.0 → euler_eval-2.28.0}/README.md +2 -2
  3. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/cli.py +72 -68
  4. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/data.py +31 -1
  5. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/evaluate.py +107 -0
  6. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval.egg-info/PKG-INFO +3 -3
  7. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval.egg-info/SOURCES.txt +1 -0
  8. {euler_eval-2.26.0 → euler_eval-2.28.0}/pyproject.toml +1 -1
  9. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_cli_device.py +20 -0
  10. euler_eval-2.28.0/tests/test_cli_sparse_outputs.py +237 -0
  11. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_config.py +36 -0
  12. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_data_builders.py +42 -0
  13. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_depth_alignment_output.py +129 -0
  14. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_evaluate_helpers.py +44 -1
  15. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_integration.py +17 -0
  16. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_points_3d_sparse.py +30 -0
  17. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_sparse_depth.py +21 -0
  18. {euler_eval-2.26.0 → euler_eval-2.28.0}/LICENSE +0 -0
  19. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/THIRD_PARTY_NOTICES.md +0 -0
  20. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/__init__.py +0 -0
  21. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/calibration.py +0 -0
  22. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/config_paths.py +0 -0
  23. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metric_sets.py +0 -0
  24. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/__init__.py +0 -0
  25. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/_natural_scene_models.py +0 -0
  26. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/absrel.py +0 -0
  27. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/daniel_error.py +0 -0
  28. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  29. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  30. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/depth_standard.py +0 -0
  31. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/fade.py +0 -0
  32. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/fid_kid.py +0 -0
  33. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  34. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  35. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  36. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/lpips_metric.py +0 -0
  37. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/niqe.py +0 -0
  38. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/normal_consistency.py +0 -0
  39. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/points3d_cloud.py +0 -0
  40. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/points3d_decomposition.py +0 -0
  41. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/points3d_distance.py +0 -0
  42. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/points3d_geometry.py +0 -0
  43. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/psnr.py +0 -0
  44. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  45. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  46. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  47. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/rho_a.py +0 -0
  48. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/rmse.py +0 -0
  49. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  50. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/ssim.py +0 -0
  51. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/tail_errors.py +0 -0
  52. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/metrics/utils.py +0 -0
  53. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/sanity_checker.py +0 -0
  54. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  55. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval/validation.py +0 -0
  56. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  57. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval.egg-info/entry_points.txt +0 -0
  58. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval.egg-info/requires.txt +0 -0
  59. {euler_eval-2.26.0 → euler_eval-2.28.0}/euler_eval.egg-info/top_level.txt +0 -0
  60. {euler_eval-2.26.0 → euler_eval-2.28.0}/init_cache.py +0 -0
  61. {euler_eval-2.26.0 → euler_eval-2.28.0}/setup.cfg +0 -0
  62. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_alignment.py +0 -0
  63. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_data.py +0 -0
  64. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_depth_standard.py +0 -0
  65. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_domain_metrics.py +0 -0
  66. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_init_cache.py +0 -0
  67. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_meta_output.py +0 -0
  68. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_metrics_utils.py +0 -0
  69. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_normal_consistency.py +0 -0
  70. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_points_3d.py +0 -0
  71. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_rgb_fid_output.py +0 -0
  72. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_rho_a.py +0 -0
  73. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_save_results.py +0 -0
  74. {euler_eval-2.26.0 → euler_eval-2.28.0}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.26.0
3
+ Version: 2.28.0
4
4
  Summary: Evaluation toolkit for depth, RGB, ray and 3D point-map predictions
5
5
  Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
6
6
  License: MIT
@@ -158,10 +158,10 @@ straight out of the archive. Full schema: [Configuration](docs/configuration.md)
158
158
  | Modality | Config key | Scored against | Headline metrics |
159
159
  |---|---|---|---|
160
160
  | **Depth** | `depth`, `relative_depth`, `affine_depth` | dense GT depth | `absrel`…`delta3`, PSNR/SSIM/LPIPS/FID/KID, surface-normal consistency, depth-edge F1 |
161
- | **Sparse depth** | `depth` + `gt.sparse_depth` | a LiDAR-style point cloud, projected into the prediction plane | pointwise depth metrics at projected points, plus directed 3D completeness |
161
+ | **Sparse depth** | `depth` + `gt.sparse_depth` | a LiDAR-style point cloud, projected into the prediction plane | pointwise depth metrics at projected points |
162
162
  | **RGB** | `rgb` | GT RGB | PSNR, SSIM, LPIPS, FID, SCE, edge F1, tail errors, HF energy ratio, depth-binned photometric error; optional NIQE/FADE dehazing metrics |
163
163
  | **Rays** | `rays` | GT ray direction map | ρ_A (angular-accuracy AUC), angular error, threshold percentages |
164
- | **Points-3D** | `points_3d` | GT point map, or GT depth unprojected on the fly | 3D EPE/RMSE/δ, radial-vs-lateral decomposition, true-3D normals and edge F1, Chamfer / F-score |
164
+ | **Points-3D** | `points_3d` | GT point map, GT depth unprojected on the fly, or a sparse point cloud | 3D EPE/RMSE/δ, radial-vs-lateral decomposition, true-3D normals and edge F1, Chamfer / F-score |
165
165
 
166
166
  A run evaluates whichever modalities are configured on both sides; a depth-only
167
167
  prediction is a perfectly ordinary run. The full inventory, with the key each
@@ -109,10 +109,10 @@ straight out of the archive. Full schema: [Configuration](docs/configuration.md)
109
109
  | Modality | Config key | Scored against | Headline metrics |
110
110
  |---|---|---|---|
111
111
  | **Depth** | `depth`, `relative_depth`, `affine_depth` | dense GT depth | `absrel`…`delta3`, PSNR/SSIM/LPIPS/FID/KID, surface-normal consistency, depth-edge F1 |
112
- | **Sparse depth** | `depth` + `gt.sparse_depth` | a LiDAR-style point cloud, projected into the prediction plane | pointwise depth metrics at projected points, plus directed 3D completeness |
112
+ | **Sparse depth** | `depth` + `gt.sparse_depth` | a LiDAR-style point cloud, projected into the prediction plane | pointwise depth metrics at projected points |
113
113
  | **RGB** | `rgb` | GT RGB | PSNR, SSIM, LPIPS, FID, SCE, edge F1, tail errors, HF energy ratio, depth-binned photometric error; optional NIQE/FADE dehazing metrics |
114
114
  | **Rays** | `rays` | GT ray direction map | ρ_A (angular-accuracy AUC), angular error, threshold percentages |
115
- | **Points-3D** | `points_3d` | GT point map, or GT depth unprojected on the fly | 3D EPE/RMSE/δ, radial-vs-lateral decomposition, true-3D normals and edge F1, Chamfer / F-score |
115
+ | **Points-3D** | `points_3d` | GT point map, GT depth unprojected on the fly, or a sparse point cloud | 3D EPE/RMSE/δ, radial-vs-lateral decomposition, true-3D normals and edge F1, Chamfer / F-score |
116
116
 
117
117
  A run evaluates whichever modalities are configured on both sides; a depth-only
118
118
  prediction is a perfectly ordinary run. The full inventory, with the key each
@@ -30,6 +30,7 @@ from .data import (
30
30
  get_rays_metadata,
31
31
  get_rgb_metadata,
32
32
  get_sparse_depth_metadata,
33
+ validate_sky_depth,
33
34
  )
34
35
  from .evaluate import (
35
36
  evaluate_depth_samples,
@@ -906,7 +907,16 @@ def validate_dataset_entry(entry: dict, index: int) -> None:
906
907
  "or 'points_3d.path'"
907
908
  )
908
909
 
909
- for modality in ("rgb", *_PRED_DEPTH_KEYS, "rays", "points_3d"):
910
+ if "sky_mask" in entry:
911
+ if not isinstance(entry["sky_mask"], dict) or not entry["sky_mask"].get("path"):
912
+ raise ValueError(f"{label}.sky_mask must have a 'path' field")
913
+ if not has_depth:
914
+ raise ValueError(
915
+ f"{label}.sky_mask requires a dense depth prediction "
916
+ "('depth', 'relative_depth', or 'affine_depth')"
917
+ )
918
+
919
+ for modality in ("rgb", *_PRED_DEPTH_KEYS, "rays", "points_3d", "sky_mask"):
910
920
  if modality in entry and "path" in entry[modality]:
911
921
  p = normalize_modality_path(
912
922
  entry[modality]["path"],
@@ -1166,6 +1176,17 @@ def main():
1166
1176
  action="store_true",
1167
1177
  help="Mask sky regions from metrics using GT segmentation",
1168
1178
  )
1179
+ parser.add_argument(
1180
+ "--sky-depth",
1181
+ type=float,
1182
+ default=None,
1183
+ metavar="METERS",
1184
+ help=(
1185
+ "Cap GT and predicted depths at this finite positive value after "
1186
+ "conversion/alignment; fill predicted sky_mask pixels with it "
1187
+ "(default: disabled)"
1188
+ ),
1189
+ )
1169
1190
  parser.add_argument(
1170
1191
  "--no-sanity-check",
1171
1192
  action="store_true",
@@ -1219,6 +1240,10 @@ def main():
1219
1240
  )
1220
1241
 
1221
1242
  args = parser.parse_args()
1243
+ try:
1244
+ validate_sky_depth(args.sky_depth)
1245
+ except ValueError as e:
1246
+ parser.error(f"--sky-depth: {e}")
1222
1247
 
1223
1248
  selected_metric_sets = resolve_metric_sets(args.domains)
1224
1249
 
@@ -1237,6 +1262,8 @@ def main():
1237
1262
  configure_torch_runtime(args.device)
1238
1263
  print_device_info(requested_device, args.device)
1239
1264
  print(f"Depth alignment: {args.depth_alignment}")
1265
+ if args.sky_depth is not None:
1266
+ print(f"Sky depth: {args.sky_depth} meters (depth cap and predicted sky fill)")
1240
1267
  print(f"RGB FID backend: {args.rgb_fid_backend}")
1241
1268
  print(
1242
1269
  "Metric sets: "
@@ -1320,6 +1347,11 @@ def main():
1320
1347
  ds_name = dataset_config["name"]
1321
1348
  pred_depth_key, pred_depth_config = _prediction_depth_entry(dataset_config)
1322
1349
  has_depth = pred_depth_config is not None
1350
+ pred_sky_mask_config = (
1351
+ dataset_config.get("sky_mask", {}) if args.sky_depth is not None else {}
1352
+ )
1353
+ pred_sky_mask_path = pred_sky_mask_config.get("path")
1354
+ pred_sky_mask_split = pred_sky_mask_config.get("split")
1323
1355
  has_rgb = "rgb" in dataset_config and "path" in dataset_config["rgb"]
1324
1356
  has_rays = "rays" in dataset_config and "path" in dataset_config["rays"]
1325
1357
  has_points_3d = (
@@ -1364,6 +1396,8 @@ def main():
1364
1396
  depth_dataset = build_depth_eval_dataset(
1365
1397
  gt_depth_path=gt_depth_path,
1366
1398
  pred_depth_path=pred_depth_path,
1399
+ pred_sky_mask_path=pred_sky_mask_path,
1400
+ pred_sky_mask_split=pred_sky_mask_split,
1367
1401
  calibration_path=calibration_path,
1368
1402
  segmentation_path=segmentation_path,
1369
1403
  pred_depth_metadata_scope=(
@@ -1392,6 +1426,7 @@ def main():
1392
1426
  verbose=args.verbose,
1393
1427
  sanity_checker=sanity_checker,
1394
1428
  sky_mask_enabled=args.mask_sky,
1429
+ sky_depth=args.sky_depth,
1395
1430
  alignment_mode=args.depth_alignment,
1396
1431
  benchmark_depth_range=(
1397
1432
  tuple(args.benchmark_depth_range)
@@ -1452,6 +1487,7 @@ def main():
1452
1487
  "split": pred_depth_split,
1453
1488
  "entry": pred_depth_key,
1454
1489
  "dimensions": depth_spatial.get("pred_dimensions"),
1490
+ "sky_mask": pred_sky_mask_config or None,
1455
1491
  },
1456
1492
  "spatial_alignment": {
1457
1493
  "method": depth_spatial.get("method", "none"),
@@ -1463,6 +1499,7 @@ def main():
1463
1499
  "eval_params": {
1464
1500
  "sky_masking": args.mask_sky,
1465
1501
  "depth_alignment_mode": args.depth_alignment,
1502
+ "sky_depth": args.sky_depth,
1466
1503
  "batch_size": args.batch_size,
1467
1504
  "num_workers": args.num_workers,
1468
1505
  "benchmark_depth_range": (
@@ -1555,6 +1592,8 @@ def main():
1555
1592
  sparse_depth_dataset = build_sparse_depth_eval_dataset(
1556
1593
  gt_sparse_depth_path=gt_sparse_depth_path,
1557
1594
  pred_depth_path=pred_depth_path,
1595
+ pred_sky_mask_path=pred_sky_mask_path,
1596
+ pred_sky_mask_split=pred_sky_mask_split,
1558
1597
  intrinsics_path=intrinsics_path,
1559
1598
  camera_extrinsics_path=camera_extrinsics_path,
1560
1599
  lidar_extrinsics_path=lidar_extrinsics_path,
@@ -1585,6 +1624,7 @@ def main():
1585
1624
  verbose=args.verbose,
1586
1625
  sanity_checker=sanity_checker,
1587
1626
  sky_mask_enabled=args.mask_sky,
1627
+ sky_depth=args.sky_depth,
1588
1628
  alignment_mode=args.depth_alignment,
1589
1629
  benchmark_depth_range=(
1590
1630
  tuple(args.benchmark_depth_range)
@@ -1640,6 +1680,7 @@ def main():
1640
1680
  "split": pred_depth_split,
1641
1681
  "entry": pred_depth_key,
1642
1682
  "dimensions": sparse_depth_spatial.get("pred_dimensions"),
1683
+ "sky_mask": pred_sky_mask_config or None,
1643
1684
  },
1644
1685
  "calibration": {
1645
1686
  "intrinsics_path": intrinsics_path,
@@ -1661,6 +1702,7 @@ def main():
1661
1702
  "eval_params": {
1662
1703
  "sky_masking": args.mask_sky,
1663
1704
  "depth_alignment_mode": args.depth_alignment,
1705
+ "sky_depth": args.sky_depth,
1664
1706
  "num_workers": args.num_workers,
1665
1707
  "benchmark_depth_range": (
1666
1708
  list(args.benchmark_depth_range)
@@ -2198,80 +2240,42 @@ def main():
2198
2240
  # -- Points-3D against sparse pointcloud GT --
2199
2241
  # When the GT is a sparse LiDAR cloud (gt.sparse_depth), evaluate the
2200
2242
  # *3D* metrics (Chamfer/F-score completeness + per-correspondence 3D
2201
- # error) against the sparse GT cloud. Two prediction sources are
2202
- # supported, with the native point map preferred when both are present:
2203
- # - a predicted points_3d map, scored directly (3D similarity gauge);
2204
- # - a dense depth map, unprojected with the GT intrinsics (affine
2205
- # gauge). Independent of the sparse-depth pointwise branch; gated by
2206
- # --skip-points-3d.
2243
+ # error) against the sparse GT cloud. This is an atomic points_3d
2244
+ # evaluation and therefore requires an explicit predicted points_3d map;
2245
+ # a depth prediction is handled only by the sparse-depth branch above.
2207
2246
  if (
2208
2247
  gt_sparse_depth_path
2209
- and (has_points_3d or has_depth)
2248
+ and has_points_3d
2210
2249
  and not args.skip_points_3d
2211
2250
  ):
2212
- p3s_pred_is_depth = not has_points_3d
2213
2251
  print(f"\n[POINTS_3D · SPARSE] Evaluating: '{ds_name}'")
2214
2252
  print(f" GT sparse pointcloud: {gt_sparse_depth_path}")
2215
- if p3s_pred_is_depth:
2216
- p3s_pred_path = pred_depth_config["path"]
2217
- p3s_pred_split = pred_depth_config.get("split")
2218
- p3s_pred_entry = pred_depth_key
2219
- print(f" Pred dense depth: {p3s_pred_path}")
2220
- if pred_depth_key != "depth":
2221
- print(f" Pred depth entry: {pred_depth_key}")
2222
- points_3d_sparse_dataset = build_sparse_depth_eval_dataset(
2223
- gt_sparse_depth_path=gt_sparse_depth_path,
2224
- pred_depth_path=p3s_pred_path,
2225
- intrinsics_path=intrinsics_path,
2226
- camera_extrinsics_path=camera_extrinsics_path,
2227
- lidar_extrinsics_path=lidar_extrinsics_path,
2228
- segmentation_path=segmentation_path,
2229
- pred_depth_metadata_scope=(
2230
- pred_depth_key if pred_depth_key != "depth" else None
2231
- ),
2232
- segmentation_modality_key=segmentation_key or "segmentation",
2233
- gt_sparse_depth_split=gt_sparse_depth_split,
2234
- pred_depth_split=p3s_pred_split,
2235
- intrinsics_split=intrinsics_split,
2236
- camera_extrinsics_split=camera_extrinsics_split,
2237
- lidar_extrinsics_split=lidar_extrinsics_split,
2238
- segmentation_split=segmentation_split,
2239
- )
2240
- p3ds_meta = get_sparse_depth_metadata(points_3d_sparse_dataset)
2241
- p3s_pred_is_radial = p3ds_meta["pred_radial_depth"]
2242
- p3s_alignment_mode = args.depth_alignment
2243
- p3s_input_hint = _prediction_depth_space_hint(pred_depth_key)
2244
- print(f" pred_radial_depth: {p3s_pred_is_radial}")
2245
- else:
2246
- p3s_pred_path = dataset_config["points_3d"]["path"]
2247
- p3s_pred_split = dataset_config["points_3d"].get("split")
2248
- p3s_pred_entry = "points_3d"
2249
- print(f" Pred point map: {p3s_pred_path}")
2250
- points_3d_sparse_dataset = build_points_3d_sparse_eval_dataset(
2251
- gt_sparse_depth_path=gt_sparse_depth_path,
2252
- pred_points_3d_path=p3s_pred_path,
2253
- intrinsics_path=intrinsics_path,
2254
- camera_extrinsics_path=camera_extrinsics_path,
2255
- lidar_extrinsics_path=lidar_extrinsics_path,
2256
- segmentation_path=segmentation_path,
2257
- segmentation_modality_key=segmentation_key or "segmentation",
2258
- gt_sparse_depth_split=gt_sparse_depth_split,
2259
- pred_points_3d_split=p3s_pred_split,
2260
- intrinsics_split=intrinsics_split,
2261
- camera_extrinsics_split=camera_extrinsics_split,
2262
- lidar_extrinsics_split=lidar_extrinsics_split,
2263
- segmentation_split=segmentation_split,
2264
- )
2265
- p3ds_meta = get_points_3d_metadata(points_3d_sparse_dataset)
2266
- p3s_pred_is_radial = True
2267
- p3s_alignment_mode = args.points_3d_alignment
2268
- p3s_input_hint = None
2253
+ p3s_pred_path = dataset_config["points_3d"]["path"]
2254
+ p3s_pred_split = dataset_config["points_3d"].get("split")
2255
+ print(f" Pred point map: {p3s_pred_path}")
2256
+ points_3d_sparse_dataset = build_points_3d_sparse_eval_dataset(
2257
+ gt_sparse_depth_path=gt_sparse_depth_path,
2258
+ pred_points_3d_path=p3s_pred_path,
2259
+ intrinsics_path=intrinsics_path,
2260
+ camera_extrinsics_path=camera_extrinsics_path,
2261
+ lidar_extrinsics_path=lidar_extrinsics_path,
2262
+ segmentation_path=segmentation_path,
2263
+ segmentation_modality_key=segmentation_key or "segmentation",
2264
+ gt_sparse_depth_split=gt_sparse_depth_split,
2265
+ pred_points_3d_split=p3s_pred_split,
2266
+ intrinsics_split=intrinsics_split,
2267
+ camera_extrinsics_split=camera_extrinsics_split,
2268
+ lidar_extrinsics_split=lidar_extrinsics_split,
2269
+ segmentation_split=segmentation_split,
2270
+ )
2271
+ p3ds_meta = get_points_3d_metadata(points_3d_sparse_dataset)
2272
+ p3s_alignment_mode = args.points_3d_alignment
2269
2273
  et_eval_datasets["points_3d_sparse"] = points_3d_sparse_dataset
2270
2274
  print(f" Matched pairs: {len(points_3d_sparse_dataset)}")
2271
2275
 
2272
2276
  points_3d_sparse_results = evaluate_points_3d_sparse_samples(
2273
2277
  dataset=points_3d_sparse_dataset,
2274
- pred_is_radial=p3s_pred_is_radial,
2278
+ pred_is_radial=True,
2275
2279
  gt_name=gt.get("name", "GT"),
2276
2280
  pred_name=ds_name,
2277
2281
  num_workers=args.num_workers,
@@ -2279,8 +2283,8 @@ def main():
2279
2283
  sanity_checker=sanity_checker,
2280
2284
  sky_mask_enabled=args.mask_sky,
2281
2285
  alignment_mode=p3s_alignment_mode,
2282
- input_space_hint=p3s_input_hint,
2283
- pred_is_depth=p3s_pred_is_depth,
2286
+ input_space_hint=None,
2287
+ pred_is_depth=False,
2284
2288
  )
2285
2289
 
2286
2290
  if sanity_checker is not None:
@@ -2335,8 +2339,8 @@ def main():
2335
2339
  "pred": {
2336
2340
  "path": p3s_pred_path,
2337
2341
  "split": p3s_pred_split,
2338
- "entry": p3s_pred_entry,
2339
- "representation": "depth" if p3s_pred_is_depth else "points_3d",
2342
+ "entry": "points_3d",
2343
+ "representation": "points_3d",
2340
2344
  "dimensions": p3s_spatial.get("pred_dimensions"),
2341
2345
  },
2342
2346
  "calibration": {
@@ -614,6 +614,12 @@ def align_to_prediction(gt: np.ndarray, pred: np.ndarray) -> np.ndarray:
614
614
  return result.astype(bool) if is_bool else result
615
615
 
616
616
 
617
+ def validate_sky_depth(sky_depth: Optional[float]) -> None:
618
+ """Require a finite positive depth cap when sky-depth processing is enabled."""
619
+ if sky_depth is not None and (not np.isfinite(sky_depth) or sky_depth <= 0):
620
+ raise ValueError("sky_depth must be a finite positive value")
621
+
622
+
617
623
  def compute_scale_and_shift(
618
624
  pred: np.ndarray,
619
625
  gt: np.ndarray,
@@ -760,11 +766,14 @@ def build_depth_eval_dataset(
760
766
  segmentation_split: Optional[str] = None,
761
767
  pred_depth_metadata_scope: Optional[str] = None,
762
768
  segmentation_modality_key: str = "segmentation",
769
+ pred_sky_mask_path: Optional[str] = None,
770
+ pred_sky_mask_split: Optional[str] = None,
763
771
  ) -> MultiModalDataset:
764
772
  """Build a MultiModalDataset for depth evaluation.
765
773
 
766
774
  The returned dataset yields samples with keys ``"gt"``, ``"pred"``,
767
- and optionally ``"calibration"`` and ``"segmentation"``.
775
+ and optionally ``"calibration"``, ``"segmentation"`` (GT), and
776
+ ``"pred_sky_mask"`` (prediction, True = sky).
768
777
 
769
778
  Loaders are resolved automatically from each dataset directory's
770
779
  ds-crawler index metadata.
@@ -781,6 +790,9 @@ def build_depth_eval_dataset(
781
790
  pred_depth_metadata_scope: Optional metadata scope for the prediction
782
791
  depth modality, useful when loading depth-like outputs such as
783
792
  ``relative_depth`` or ``affine_depth`` through the depth loader.
793
+ pred_sky_mask_path: Optional predicted ``sky_mask`` dataset, paired
794
+ with depth by sample ID using its own loader metadata.
795
+ pred_sky_mask_split: Optional split for the predicted sky mask.
784
796
 
785
797
  Returns:
786
798
  A MultiModalDataset instance.
@@ -799,6 +811,13 @@ def build_depth_eval_dataset(
799
811
  split=pred_depth_split,
800
812
  ),
801
813
  }
814
+ if pred_sky_mask_path is not None:
815
+ modalities["pred_sky_mask"] = _modality(
816
+ path=pred_sky_mask_path,
817
+ modality_key="sky_mask",
818
+ used_as="output",
819
+ split=pred_sky_mask_split,
820
+ )
802
821
 
803
822
  hierarchical = {}
804
823
  if calibration_path is not None:
@@ -840,6 +859,8 @@ def build_sparse_depth_eval_dataset(
840
859
  segmentation_split: Optional[str] = None,
841
860
  pred_depth_metadata_scope: Optional[str] = None,
842
861
  segmentation_modality_key: str = "segmentation",
862
+ pred_sky_mask_path: Optional[str] = None,
863
+ pred_sky_mask_split: Optional[str] = None,
843
864
  ) -> MultiModalDataset:
844
865
  """Build a MultiModalDataset for sparse pointcloud depth evaluation.
845
866
 
@@ -855,6 +876,8 @@ def build_sparse_depth_eval_dataset(
855
876
  such as ``relative_depth`` or ``affine_depth`` while still loading it
856
877
  as a dense depth map. Segmentation remains optional for sparse depth and is
857
878
  only loaded by callers that request sky masking.
879
+ ``pred_sky_mask_path`` optionally adds an ID-paired ``"pred_sky_mask"``
880
+ prediction (True = sky), with its own ``pred_sky_mask_split`` selector.
858
881
  """
859
882
  modalities = {
860
883
  "gt": _modality(
@@ -870,6 +893,13 @@ def build_sparse_depth_eval_dataset(
870
893
  split=pred_depth_split,
871
894
  ),
872
895
  }
896
+ if pred_sky_mask_path is not None:
897
+ modalities["pred_sky_mask"] = _modality(
898
+ path=pred_sky_mask_path,
899
+ modality_key="sky_mask",
900
+ used_as="output",
901
+ split=pred_sky_mask_split,
902
+ )
873
903
 
874
904
  hierarchical = _sparse_projection_hierarchical(
875
905
  intrinsics_path=intrinsics_path,
@@ -38,6 +38,7 @@ from .data import (
38
38
  to_numpy_points_3d,
39
39
  to_numpy_rgb,
40
40
  unproject_depth_to_points,
41
+ validate_sky_depth,
41
42
  )
42
43
  from .metric_sets import resolve_metric_sets
43
44
  from .metrics import (
@@ -504,6 +505,48 @@ def _get_sky_mask(sample: dict) -> Optional[np.ndarray]:
504
505
  return ~sky # invert: True = non-sky = valid
505
506
 
506
507
 
508
+ def _get_prediction_sky_mask(
509
+ sample: dict, depth_pred: np.ndarray
510
+ ) -> Optional[np.ndarray]:
511
+ """Return the predicted sky mask on the depth grid (True = sky)."""
512
+ mask_data = sample.get("pred_sky_mask")
513
+ if mask_data is None:
514
+ return None
515
+ return align_to_prediction(to_numpy_mask(mask_data), depth_pred)
516
+
517
+
518
+ def _apply_sky_depth(
519
+ depth_gt: np.ndarray,
520
+ depth_pred_raw: np.ndarray,
521
+ depth_pred_aligned: np.ndarray,
522
+ sky_depth: Optional[float],
523
+ pred_sky_mask: Optional[np.ndarray],
524
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
525
+ """Cap evaluation depths and fill predicted sky after conversion/alignment.
526
+
527
+ Preserve the shared prediction object when no alignment was needed, since
528
+ evaluators use its identity to avoid computing the same metrics twice.
529
+ NaNs and nonpositive values remain invalid unless filled by the sky mask;
530
+ positive infinity is truncated to the cap like any other larger depth.
531
+ """
532
+ if sky_depth is None:
533
+ return depth_gt, depth_pred_raw, depth_pred_aligned
534
+
535
+ def _cap_prediction(depth):
536
+ capped = np.minimum(depth, sky_depth)
537
+ if pred_sky_mask is not None:
538
+ capped[pred_sky_mask] = sky_depth
539
+ return capped
540
+
541
+ aligned_is_raw = depth_pred_aligned is depth_pred_raw
542
+ depth_gt = np.minimum(depth_gt, sky_depth)
543
+ depth_pred_raw = _cap_prediction(depth_pred_raw)
544
+ depth_pred_aligned = (
545
+ depth_pred_raw if aligned_is_raw else _cap_prediction(depth_pred_aligned)
546
+ )
547
+ return depth_gt, depth_pred_raw, depth_pred_aligned
548
+
549
+
507
550
  # Calibration extraction lives in euler_eval.calibration (imported above with
508
551
  # private aliases that tests import from euler_eval.evaluate).
509
552
 
@@ -582,6 +625,7 @@ def evaluate_depth_samples(
582
625
  alignment_mode: str = "auto_affine",
583
626
  benchmark_depth_range: Optional[tuple[float, float]] = None,
584
627
  input_space_hint: Optional[str] = None,
628
+ sky_depth: Optional[float] = None,
585
629
  ) -> dict:
586
630
  """Evaluate all depth metrics from a MultiModalDataset.
587
631
 
@@ -607,6 +651,11 @@ def evaluate_depth_samples(
607
651
  input_space_hint: Optional declaration from the prediction config.
608
652
  ``relative`` or ``affine`` makes ``auto_affine`` run alignment
609
653
  even when values are outside the normalized range.
654
+ sky_depth: Optional finite positive maximum evaluation depth in meters.
655
+ After conversion/alignment, cap GT and both prediction branches and
656
+ fill ``sample["pred_sky_mask"]`` pixels (True = sky) with this value.
657
+ Predicted sky is excluded from affine fitting, but remains scored
658
+ unless excluded by GT validity or ``sky_mask_enabled``.
610
659
 
611
660
  Returns:
612
661
  Dictionary containing depth aggregate/per-file metrics with:
@@ -614,6 +663,7 @@ def evaluate_depth_samples(
614
663
  backward-compatible canonical ``depth``, and optionally
615
664
  per-space ``depth_benchmark`` summaries.
616
665
  """
666
+ validate_sky_depth(sky_depth)
617
667
  valid_alignment_modes = {"none", "auto_affine", "affine"}
618
668
  if alignment_mode not in valid_alignment_modes:
619
669
  raise ValueError(
@@ -1095,6 +1145,11 @@ def evaluate_depth_samples(
1095
1145
 
1096
1146
  depth_gt = process_depth(depth_gt, is_radial, intrinsics_K)
1097
1147
  depth_pred_raw = process_depth(depth_pred, is_radial, intrinsics_K)
1148
+ pred_sky_mask = (
1149
+ _get_prediction_sky_mask(sample, depth_pred)
1150
+ if sky_depth is not None
1151
+ else None
1152
+ )
1098
1153
 
1099
1154
  if alignment_mode == "none":
1100
1155
  depth_pred_aligned = depth_pred_raw
@@ -1114,6 +1169,8 @@ def evaluate_depth_samples(
1114
1169
  )
1115
1170
  if sky_valid is not None:
1116
1171
  fit_mask = fit_mask & sky_valid
1172
+ if pred_sky_mask is not None:
1173
+ fit_mask &= ~pred_sky_mask
1117
1174
  depth_pred_aligned, s, t = compute_scale_and_shift(
1118
1175
  fit_source,
1119
1176
  depth_gt,
@@ -1130,6 +1187,11 @@ def evaluate_depth_samples(
1130
1187
  else:
1131
1188
  depth_pred_aligned = depth_pred_raw
1132
1189
 
1190
+ depth_gt, depth_pred_raw, depth_pred_aligned = _apply_sky_depth(
1191
+ depth_gt, depth_pred_raw, depth_pred_aligned,
1192
+ sky_depth, pred_sky_mask,
1193
+ )
1194
+
1133
1195
  if verbose and not logged_stats:
1134
1196
  _log_sample_stats(depth_gt, depth_pred_aligned, "DEPTH")
1135
1197
  logged_stats = True
@@ -1577,6 +1639,7 @@ def evaluate_sparse_depth_samples(
1577
1639
  alignment_mode: str = "auto_affine",
1578
1640
  benchmark_depth_range: Optional[tuple[float, float]] = None,
1579
1641
  input_space_hint: Optional[str] = None,
1642
+ sky_depth: Optional[float] = None,
1580
1643
  ) -> dict:
1581
1644
  """Evaluate dense depth predictions against sparse pointcloud GT.
1582
1645
 
@@ -1584,7 +1647,10 @@ def evaluate_sparse_depth_samples(
1584
1647
  dense prediction image plane, and metrics are computed only at projected
1585
1648
  valid pixels. The emitted metric categories intentionally exclude dense
1586
1649
  image/geometric metrics such as SSIM, LPIPS, FID, normals, and edge F1.
1650
+ ``sky_depth`` caps projected GT and predictions and fills optional
1651
+ ``sample["pred_sky_mask"]`` pixels, as in :func:`evaluate_depth_samples`.
1587
1652
  """
1653
+ validate_sky_depth(sky_depth)
1588
1654
  valid_alignment_modes = {"none", "auto_affine", "affine"}
1589
1655
  if alignment_mode not in valid_alignment_modes:
1590
1656
  raise ValueError(
@@ -1838,6 +1904,11 @@ def evaluate_sparse_depth_samples(
1838
1904
  depth_pred_raw = process_depth(
1839
1905
  depth_pred, pred_is_radial, intrinsics_K
1840
1906
  )
1907
+ pred_sky_mask = (
1908
+ _get_prediction_sky_mask(sample, depth_pred)
1909
+ if sky_depth is not None
1910
+ else None
1911
+ )
1841
1912
 
1842
1913
  if alignment_mode == "none":
1843
1914
  depth_pred_aligned = depth_pred_raw
@@ -1853,6 +1924,8 @@ def evaluate_sparse_depth_samples(
1853
1924
  )
1854
1925
  if sky_valid is not None:
1855
1926
  fit_mask &= sky_valid
1927
+ if pred_sky_mask is not None:
1928
+ fit_mask &= ~pred_sky_mask
1856
1929
  depth_pred_aligned, s, t = compute_scale_and_shift(
1857
1930
  fit_source,
1858
1931
  sparse_depth_gt,
@@ -1869,6 +1942,11 @@ def evaluate_sparse_depth_samples(
1869
1942
  else:
1870
1943
  depth_pred_aligned = depth_pred_raw
1871
1944
 
1945
+ sparse_depth_gt, depth_pred_raw, depth_pred_aligned = _apply_sky_depth(
1946
+ sparse_depth_gt, depth_pred_raw, depth_pred_aligned,
1947
+ sky_depth, pred_sky_mask,
1948
+ )
1949
+
1872
1950
  if verbose and not logged_stats:
1873
1951
  _log_sample_stats(sparse_depth_gt, depth_pred_aligned, "SPARSE DEPTH")
1874
1952
  logged_stats = True
@@ -3498,6 +3576,7 @@ def evaluate_points_3d_sparse_samples(
3498
3576
  input_space_hint: Optional[str] = None,
3499
3577
  max_cloud_points: int = POINTS3D_DEFAULT_MAX_POINTS,
3500
3578
  pred_is_depth: bool = True,
3579
+ sky_depth: Optional[float] = None,
3501
3580
  ) -> dict:
3502
3581
  """Evaluate a depth **or** ``points_3d`` prediction against sparse GT.
3503
3582
 
@@ -3555,6 +3634,9 @@ def evaluate_points_3d_sparse_samples(
3555
3634
  max_cloud_points: Per-cloud subsample cap for the cloud-distance query.
3556
3635
  pred_is_depth: Whether the prediction is a dense depth map (unprojected)
3557
3636
  or a native ``points_3d`` map (scored directly).
3637
+ sky_depth: Optional depth cap and predicted-sky fill value, supported
3638
+ for depth predictions. GT points are capped radially, preserving
3639
+ their directions, to match the sparse-depth evaluation range.
3558
3640
 
3559
3641
  Returns:
3560
3642
  Dict shaped like :func:`evaluate_points_3d_samples`:
@@ -3562,6 +3644,9 @@ def evaluate_points_3d_sparse_samples(
3562
3644
  ``per_file_metrics``, ``dataset_info``, ``space_info`` and
3563
3645
  ``spatial_info``.
3564
3646
  """
3647
+ validate_sky_depth(sky_depth)
3648
+ if sky_depth is not None and not pred_is_depth:
3649
+ raise ValueError("sky_depth is only supported for depth predictions")
3565
3650
  if pred_is_depth:
3566
3651
  valid_alignment_modes = {"none", "auto_affine", "affine"}
3567
3652
  valid_input_space_hints = {None, "relative", "affine"}
@@ -3614,6 +3699,8 @@ def evaluate_points_3d_sparse_samples(
3614
3699
  dataset_max_depth,
3615
3700
  float(np.max(finite_depths)),
3616
3701
  )
3702
+ if sky_depth is not None:
3703
+ dataset_max_depth = min(dataset_max_depth, sky_depth)
3617
3704
  f_a_max_threshold = dataset_max_depth / 20.0
3618
3705
 
3619
3706
  resolved_domain: Optional[str] = fov_domain
@@ -3884,6 +3971,11 @@ def evaluate_points_3d_sparse_samples(
3884
3971
  depth_pred_raw = process_depth(
3885
3972
  depth_pred, pred_is_radial, intrinsics_K
3886
3973
  )
3974
+ pred_sky_mask = (
3975
+ _get_prediction_sky_mask(sample, depth_pred)
3976
+ if sky_depth is not None
3977
+ else None
3978
+ )
3887
3979
  if alignment_mode == "none":
3888
3980
  depth_pred_aligned = depth_pred_raw
3889
3981
  else:
@@ -3904,6 +3996,8 @@ def evaluate_points_3d_sparse_samples(
3904
3996
  )
3905
3997
  if sky_valid is not None:
3906
3998
  fit_mask &= sky_valid
3999
+ if pred_sky_mask is not None:
4000
+ fit_mask &= ~pred_sky_mask
3907
4001
  depth_pred_aligned, s_fit, t_fit = compute_scale_and_shift(
3908
4002
  fit_source,
3909
4003
  gt_radial,
@@ -3920,6 +4014,19 @@ def evaluate_points_3d_sparse_samples(
3920
4014
  else:
3921
4015
  depth_pred_aligned = depth_pred_raw
3922
4016
 
4017
+ if sky_depth is not None:
4018
+ # Keep actual GT ray directions, including subpixel
4019
+ # offsets, while limiting each point's radial depth.
4020
+ gt_scale = np.ones_like(gt_radial)
4021
+ beyond_cap = gt_radial > sky_depth
4022
+ gt_scale[beyond_cap] = sky_depth / gt_radial[beyond_cap]
4023
+ gt_point_map = gt_point_map * gt_scale[..., None]
4024
+ gt_cloud = gt_point_map[gt_valid]
4025
+ gt_radial, depth_pred_raw, depth_pred_aligned = _apply_sky_depth(
4026
+ gt_radial, depth_pred_raw, depth_pred_aligned,
4027
+ sky_depth, pred_sky_mask,
4028
+ )
4029
+
3923
4030
  if verbose and not logged_stats:
3924
4031
  _log_sample_stats(gt_radial, depth_pred_raw, "SPARSE POINTS_3D")
3925
4032
  logged_stats = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.26.0
3
+ Version: 2.28.0
4
4
  Summary: Evaluation toolkit for depth, RGB, ray and 3D point-map predictions
5
5
  Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
6
6
  License: MIT
@@ -158,10 +158,10 @@ straight out of the archive. Full schema: [Configuration](docs/configuration.md)
158
158
  | Modality | Config key | Scored against | Headline metrics |
159
159
  |---|---|---|---|
160
160
  | **Depth** | `depth`, `relative_depth`, `affine_depth` | dense GT depth | `absrel`…`delta3`, PSNR/SSIM/LPIPS/FID/KID, surface-normal consistency, depth-edge F1 |
161
- | **Sparse depth** | `depth` + `gt.sparse_depth` | a LiDAR-style point cloud, projected into the prediction plane | pointwise depth metrics at projected points, plus directed 3D completeness |
161
+ | **Sparse depth** | `depth` + `gt.sparse_depth` | a LiDAR-style point cloud, projected into the prediction plane | pointwise depth metrics at projected points |
162
162
  | **RGB** | `rgb` | GT RGB | PSNR, SSIM, LPIPS, FID, SCE, edge F1, tail errors, HF energy ratio, depth-binned photometric error; optional NIQE/FADE dehazing metrics |
163
163
  | **Rays** | `rays` | GT ray direction map | ρ_A (angular-accuracy AUC), angular error, threshold percentages |
164
- | **Points-3D** | `points_3d` | GT point map, or GT depth unprojected on the fly | 3D EPE/RMSE/δ, radial-vs-lateral decomposition, true-3D normals and edge F1, Chamfer / F-score |
164
+ | **Points-3D** | `points_3d` | GT point map, GT depth unprojected on the fly, or a sparse point cloud | 3D EPE/RMSE/δ, radial-vs-lateral decomposition, true-3D normals and edge F1, Chamfer / F-score |
165
165
 
166
166
  A run evaluates whichever modalities are configured on both sides; a depth-only
167
167
  prediction is a perfectly ordinary run. The full inventory, with the key each
@@ -50,6 +50,7 @@ euler_eval/metrics/utils.py
50
50
  euler_eval/utils/hierarchy_parser.py
51
51
  tests/test_alignment.py
52
52
  tests/test_cli_device.py
53
+ tests/test_cli_sparse_outputs.py
53
54
  tests/test_config.py
54
55
  tests/test_data.py
55
56
  tests/test_data_builders.py