euler-eval 2.20.0__tar.gz → 2.22.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 (65) hide show
  1. {euler_eval-2.20.0 → euler_eval-2.22.0}/PKG-INFO +3 -3
  2. {euler_eval-2.20.0 → euler_eval-2.22.0}/README.md +2 -2
  3. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/cli.py +36 -1
  4. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/evaluate.py +92 -5
  5. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/__init__.py +2 -0
  6. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/points3d_cloud.py +86 -2
  7. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval.egg-info/PKG-INFO +3 -3
  8. {euler_eval-2.20.0 → euler_eval-2.22.0}/pyproject.toml +1 -1
  9. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_meta_output.py +20 -0
  10. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_points_3d.py +26 -0
  11. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_points_3d_sparse.py +17 -0
  12. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/__init__.py +0 -0
  13. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/calibration.py +0 -0
  14. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/config_paths.py +0 -0
  15. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/data.py +0 -0
  16. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/absrel.py +0 -0
  17. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/daniel_error.py +0 -0
  18. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  19. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  20. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/depth_standard.py +0 -0
  21. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/fid_kid.py +0 -0
  22. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  23. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  24. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  25. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/lpips_metric.py +0 -0
  26. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/normal_consistency.py +0 -0
  27. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/points3d_decomposition.py +0 -0
  28. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/points3d_distance.py +0 -0
  29. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/points3d_geometry.py +0 -0
  30. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/psnr.py +0 -0
  31. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  32. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  33. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  34. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/rho_a.py +0 -0
  35. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/rmse.py +0 -0
  36. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  37. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/ssim.py +0 -0
  38. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/tail_errors.py +0 -0
  39. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/metrics/utils.py +0 -0
  40. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/sanity_checker.py +0 -0
  41. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  42. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval/validation.py +0 -0
  43. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval.egg-info/SOURCES.txt +0 -0
  44. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  45. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval.egg-info/entry_points.txt +0 -0
  46. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval.egg-info/requires.txt +0 -0
  47. {euler_eval-2.20.0 → euler_eval-2.22.0}/euler_eval.egg-info/top_level.txt +0 -0
  48. {euler_eval-2.20.0 → euler_eval-2.22.0}/init_cache.py +0 -0
  49. {euler_eval-2.20.0 → euler_eval-2.22.0}/setup.cfg +0 -0
  50. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_alignment.py +0 -0
  51. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_cli_device.py +0 -0
  52. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_config.py +0 -0
  53. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_data.py +0 -0
  54. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_data_builders.py +0 -0
  55. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_depth_alignment_output.py +0 -0
  56. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_depth_standard.py +0 -0
  57. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_evaluate_helpers.py +0 -0
  58. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_init_cache.py +0 -0
  59. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_integration.py +0 -0
  60. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_metrics_utils.py +0 -0
  61. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_rgb_fid_output.py +0 -0
  62. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_rho_a.py +0 -0
  63. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_save_results.py +0 -0
  64. {euler_eval-2.20.0 → euler_eval-2.22.0}/tests/test_sparse_depth.py +0 -0
  65. {euler_eval-2.20.0 → euler_eval-2.22.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.20.0
3
+ Version: 2.22.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -493,7 +493,7 @@ a `points_3d` canonical alias, keyed as
493
493
  | Euclidean 3D agreement | `point_error.{image_mean,image_median,pixel_pool}.*` | 3D EPE `mae3d`, `rmse3d`, `median3d`/`p90`/`p95`, relative error `rel_median`/`rel_p90`, and 3D δ-accuracy `acc_<τ>` (within τ m) / `acc_rel_<τ>` |
494
494
  | Error decomposition | `error_decomposition.{radial_*,lateral_*,lateral_fraction}` + `angular_error.*` + `rho_a.*` | Splits each error vector into a **radial** (≈ depth) and **lateral** (≈ camera-model) component; `lateral_fraction ∈ [0,1]` attributes blame. Angular ray error and ρ_A (on native directions) measure the implied camera model |
495
495
  | Geometric | `geometric.normal_consistency.*`, `geometric.point_edge_f1.*` | True-3D surface normals (cross-product of point-map tangents, no focal assumption) and a 3D discontinuity F1 |
496
- | Cloud distance | `cloud_distance.chamfer.*`, `cloud_distance.fscore.tau_<τ>.*` | Correspondence-free Chamfer (accuracy/completeness/distance/median) and F-score (precision/recall/f1) at distance thresholds τ |
496
+ | Cloud distance | `cloud_distance.chamfer.*`, `cloud_distance.fscore.tau_<τ>.*`, `cloud_distance.f_a` | Correspondence-free Chamfer (accuracy/completeness/distance/median), F-score (precision/recall/f1) at fixed distance thresholds τ, and F-score AUC (`f_a`) integrated up to 1/20 of the dataset's maximum GT depth |
497
497
 
498
498
  #### Points-3D gauge alignment
499
499
 
@@ -607,7 +607,7 @@ For sparse depth outputs, the internal Python result dict still uses `sparse_dep
607
607
 
608
608
  For points_3d outputs, the internal Python result dict uses `points_3d_native`, `points_3d_metric`, and `points_3d` (canonical alias), but serialized `eval.json` metric paths are rooted at `points3d.eval` (no underscore) to satisfy the namespace first-segment rule, mirroring `sparsedepth`. The `metric` space is emitted only when a gauge alignment is applied; otherwise only `native` is present and aliased.
609
609
 
610
- When the points_3d metrics come from a sparse pointcloud GT (`gt.sparse_depth` + a predicted `points_3d` map or a dense depth prediction), they use the same `points3d.eval` namespace and are written as `eval.json` beside the selected prediction modality (or to the configured `output_file`). Only the `point_error`, `error_decomposition`, and (directed) `cloud_distance` categories are present; the `metric` space is emitted only when the gauge (`--points-3d-alignment` for a point-map prediction, `--depth-alignment` for a depth prediction) calibrates the prediction.
610
+ When the points_3d metrics come from a sparse pointcloud GT (`gt.sparse_depth` + a predicted `points_3d` map or a dense depth prediction), they use the same `points3d.eval` namespace and are written as `eval.json` beside the selected prediction modality (or to the configured `output_file`). Only the `point_error`, `error_decomposition`, and `cloud_distance` categories are present. Fixed-threshold cloud diagnostics are directed (completeness/recall), while `cloud_distance.f_a` performs the symmetric comparison required by the F-score AUC definition. The `metric` space is emitted only when the gauge (`--points-3d-alignment` for a point-map prediction, `--depth-alignment` for a depth prediction) calibrates the prediction.
611
611
 
612
612
  Previous single-depth structure (kept under `depth`) is:
613
613
 
@@ -455,7 +455,7 @@ a `points_3d` canonical alias, keyed as
455
455
  | Euclidean 3D agreement | `point_error.{image_mean,image_median,pixel_pool}.*` | 3D EPE `mae3d`, `rmse3d`, `median3d`/`p90`/`p95`, relative error `rel_median`/`rel_p90`, and 3D δ-accuracy `acc_<τ>` (within τ m) / `acc_rel_<τ>` |
456
456
  | Error decomposition | `error_decomposition.{radial_*,lateral_*,lateral_fraction}` + `angular_error.*` + `rho_a.*` | Splits each error vector into a **radial** (≈ depth) and **lateral** (≈ camera-model) component; `lateral_fraction ∈ [0,1]` attributes blame. Angular ray error and ρ_A (on native directions) measure the implied camera model |
457
457
  | Geometric | `geometric.normal_consistency.*`, `geometric.point_edge_f1.*` | True-3D surface normals (cross-product of point-map tangents, no focal assumption) and a 3D discontinuity F1 |
458
- | Cloud distance | `cloud_distance.chamfer.*`, `cloud_distance.fscore.tau_<τ>.*` | Correspondence-free Chamfer (accuracy/completeness/distance/median) and F-score (precision/recall/f1) at distance thresholds τ |
458
+ | Cloud distance | `cloud_distance.chamfer.*`, `cloud_distance.fscore.tau_<τ>.*`, `cloud_distance.f_a` | Correspondence-free Chamfer (accuracy/completeness/distance/median), F-score (precision/recall/f1) at fixed distance thresholds τ, and F-score AUC (`f_a`) integrated up to 1/20 of the dataset's maximum GT depth |
459
459
 
460
460
  #### Points-3D gauge alignment
461
461
 
@@ -569,7 +569,7 @@ For sparse depth outputs, the internal Python result dict still uses `sparse_dep
569
569
 
570
570
  For points_3d outputs, the internal Python result dict uses `points_3d_native`, `points_3d_metric`, and `points_3d` (canonical alias), but serialized `eval.json` metric paths are rooted at `points3d.eval` (no underscore) to satisfy the namespace first-segment rule, mirroring `sparsedepth`. The `metric` space is emitted only when a gauge alignment is applied; otherwise only `native` is present and aliased.
571
571
 
572
- When the points_3d metrics come from a sparse pointcloud GT (`gt.sparse_depth` + a predicted `points_3d` map or a dense depth prediction), they use the same `points3d.eval` namespace and are written as `eval.json` beside the selected prediction modality (or to the configured `output_file`). Only the `point_error`, `error_decomposition`, and (directed) `cloud_distance` categories are present; the `metric` space is emitted only when the gauge (`--points-3d-alignment` for a point-map prediction, `--depth-alignment` for a depth prediction) calibrates the prediction.
572
+ When the points_3d metrics come from a sparse pointcloud GT (`gt.sparse_depth` + a predicted `points_3d` map or a dense depth prediction), they use the same `points3d.eval` namespace and are written as `eval.json` beside the selected prediction modality (or to the configured `output_file`). Only the `point_error`, `error_decomposition`, and `cloud_distance` categories are present. Fixed-threshold cloud diagnostics are directed (completeness/recall), while `cloud_distance.f_a` performs the symmetric comparison required by the F-score AUC definition. The `metric` space is emitted only when the gauge (`--points-3d-alignment` for a point-map prediction, `--depth-alignment` for a depth prediction) calibrates the prediction.
573
573
 
574
574
  Previous single-depth structure (kept under `depth`) is:
575
575
 
@@ -8,6 +8,7 @@ import argparse
8
8
  import importlib.metadata
9
9
  import json
10
10
  import math
11
+ import platform
11
12
  import sys
12
13
  import zipfile
13
14
  from pathlib import Path
@@ -503,6 +504,13 @@ def _points_3d_descriptions() -> dict:
503
504
  "f1": MetricDescription(
504
505
  is_higher_better=True, min_value=0.0, max_value=1.0, display_name="F1"
505
506
  ),
507
+ "f_a": MetricDescription(
508
+ is_higher_better=True,
509
+ scale="percentage",
510
+ min_value=0.0,
511
+ max_value=100.0,
512
+ display_name="F-score AUC (F_A)",
513
+ ),
506
514
  # cloud_distance
507
515
  "chamfer.accuracy": MetricDescription(
508
516
  is_higher_better=False, unit="meters", display_name="Chamfer Accuracy"
@@ -554,6 +562,24 @@ def _get_version() -> str:
554
562
  return "0.0.0"
555
563
 
556
564
 
565
+ def _package_metadata() -> dict:
566
+ """Return stable package/runtime metadata for evaluation provenance."""
567
+ return {
568
+ "name": "euler-eval",
569
+ "version": _get_version(),
570
+ "python_version": platform.python_version(),
571
+ "torch_version": str(torch.__version__),
572
+ "cuda_version": torch.version.cuda,
573
+ }
574
+
575
+
576
+ def _log_package_metadata() -> dict:
577
+ """Emit and return the package metadata recorded at workflow startup."""
578
+ metadata = _package_metadata()
579
+ print(f"Evaluation package: {json.dumps(metadata, sort_keys=True)}")
580
+ return metadata
581
+
582
+
557
583
  def _sparse_depth_metric_set_envelope(
558
584
  namespace: _EvalNamespace,
559
585
  *,
@@ -1173,6 +1199,9 @@ def main():
1173
1199
 
1174
1200
  args = parser.parse_args()
1175
1201
 
1202
+ # Emit producer provenance before configuration loading or evaluation work.
1203
+ package_metadata = _log_package_metadata()
1204
+
1176
1205
  # Load and validate config
1177
1206
  try:
1178
1207
  config = load_config(args.config)
@@ -1283,7 +1312,12 @@ def main():
1283
1312
 
1284
1313
  # Register evaluation as running before work begins
1285
1314
  if et_run is not None:
1286
- et_run.add_evaluation(ds_name, name=ds_name, status="running")
1315
+ et_run.add_evaluation(
1316
+ ds_name,
1317
+ name=ds_name,
1318
+ status="running",
1319
+ metadata={"package": package_metadata},
1320
+ )
1287
1321
 
1288
1322
  # -- Depth evaluation --
1289
1323
  depth_dataset = None
@@ -2362,6 +2396,7 @@ def main():
2362
2396
  # name=ds_name,
2363
2397
  # status="completed",
2364
2398
  metadata={
2399
+ "package": package_metadata,
2365
2400
  "results": {
2366
2401
  k: v for k, v in all_results.items() if k != "per_file_metrics"
2367
2402
  }
@@ -2854,6 +2854,40 @@ def evaluate_rays_samples(
2854
2854
  # ---------------------------------------------------------------------------
2855
2855
 
2856
2856
 
2857
+ def _points_3d_max_gt_depth(
2858
+ dataset: MultiModalDataset,
2859
+ *,
2860
+ gt_is_depth: bool,
2861
+ gt_depth_is_radial: bool,
2862
+ ) -> float:
2863
+ """Scan the GT once for the dataset maximum radial 3D depth."""
2864
+ max_depth = 0.0
2865
+ for index in range(len(dataset)):
2866
+ sample = dataset[index]
2867
+ if gt_is_depth:
2868
+ intrinsics = _get_intrinsics_K(sample)
2869
+ if intrinsics is None:
2870
+ _, entry_id = _extract_hierarchy(sample)
2871
+ raise ValueError(
2872
+ f"Sample {entry_id!r}: points_3d GT synthesis from depth "
2873
+ "requires intrinsics (gt.intrinsics or gt.calibration)."
2874
+ )
2875
+ gt_points = unproject_depth_to_points(
2876
+ to_numpy_depth(sample["gt"]),
2877
+ intrinsics,
2878
+ depth_is_radial=gt_depth_is_radial,
2879
+ )
2880
+ else:
2881
+ gt_points = to_numpy_points_3d(sample["gt"])
2882
+ finite = np.isfinite(gt_points).all(axis=-1)
2883
+ if np.any(finite):
2884
+ depths = np.linalg.norm(gt_points[finite], axis=-1)
2885
+ positive = depths[depths > 0]
2886
+ if positive.size:
2887
+ max_depth = max(max_depth, float(np.max(positive)))
2888
+ return max_depth
2889
+
2890
+
2857
2891
  def evaluate_points_3d_samples(
2858
2892
  dataset: MultiModalDataset,
2859
2893
  fov_domain: Optional[str] = None,
@@ -2922,6 +2956,14 @@ def evaluate_points_3d_samples(
2922
2956
  if num_samples == 0:
2923
2957
  raise ValueError("Dataset has no matched samples")
2924
2958
 
2959
+ print("Scanning points_3d GT depth range for F_A...")
2960
+ dataset_max_depth = _points_3d_max_gt_depth(
2961
+ dataset,
2962
+ gt_is_depth=gt_is_depth,
2963
+ gt_depth_is_radial=gt_depth_is_radial,
2964
+ )
2965
+ f_a_max_threshold = dataset_max_depth / 20.0
2966
+
2925
2967
  # Resolve the gauge alignment to apply.
2926
2968
  if alignment_mode == "auto":
2927
2969
  resolved_mode = "similarity" if input_space_hint == "relative" else "none"
@@ -3224,7 +3266,10 @@ def evaluate_points_3d_samples(
3224
3266
  st["edge_images"].append(ef)
3225
3267
 
3226
3268
  cd = compute_cloud_distance_metrics(
3227
- pv_sp, gv, max_points=max_cloud_points
3269
+ pv_sp,
3270
+ gv,
3271
+ max_points=max_cloud_points,
3272
+ fscore_auc_max_threshold=f_a_max_threshold,
3228
3273
  )
3229
3274
  if cd:
3230
3275
  st["cloud_images"].append(cd)
@@ -3325,6 +3370,8 @@ def evaluate_points_3d_samples(
3325
3370
  "threshold_deg": threshold_deg,
3326
3371
  "evaluated_points": total_evaluated_points,
3327
3372
  "gt_synthesized_from_depth": gt_is_depth,
3373
+ "max_depth": dataset_max_depth,
3374
+ "f_a_max_threshold": f_a_max_threshold,
3328
3375
  },
3329
3376
  "space_info": {
3330
3377
  "input_space_detected": input_space_detected,
@@ -3405,9 +3452,11 @@ def evaluate_points_3d_sparse_samples(
3405
3452
 
3406
3453
  - ``cloud_distance`` — the **primary** geometry metric here. Because the
3407
3454
  GT is sparse and the prediction dense, only the correspondence-free
3408
- ``gt→pred`` direction is meaningful, so this reports **completeness**
3409
- (mean/median nearest-predicted distance per GT return) and **recall**
3410
- at each threshold (see :func:`compute_sparse_cloud_distance_metrics`).
3455
+ ``gt→pred`` direction is used for the fixed-threshold diagnostics, so
3456
+ these report **completeness** (mean/median nearest-predicted distance
3457
+ per GT return) and **recall**. ``f_a`` additionally performs the
3458
+ symmetric comparison required for F-score AUC (see
3459
+ :func:`compute_sparse_cloud_distance_metrics`).
3411
3460
  - ``point_error`` — Euclidean 3D agreement (EPE/RMSE/percentiles/δ-acc)
3412
3461
  computed at the projected sparse correspondences.
3413
3462
  - ``error_decomposition`` — radial (≈depth) vs lateral (≈camera-model)
@@ -3462,6 +3511,39 @@ def evaluate_points_3d_sparse_samples(
3462
3511
  if num_samples == 0:
3463
3512
  raise ValueError("Dataset has no matched samples")
3464
3513
 
3514
+ print("Scanning sparse points_3d GT depth range for F_A...")
3515
+ dataset_max_depth = 0.0
3516
+ for sample_index in range(num_samples):
3517
+ range_sample = dataset[sample_index]
3518
+ range_cloud = to_numpy_point_cloud(range_sample["gt"])
3519
+ if pred_is_depth:
3520
+ range_shape = to_numpy_depth(range_sample["pred"]).shape[:2]
3521
+ else:
3522
+ range_shape = to_numpy_points_3d(range_sample["pred"]).shape[:2]
3523
+ range_intrinsics = _get_intrinsics_K(range_sample)
3524
+ range_extrinsics, _ = _get_pointcloud_to_camera_extrinsics(range_sample)
3525
+ if range_intrinsics is None or range_extrinsics is None:
3526
+ # The main evaluation loop emits the detailed sample-specific
3527
+ # configuration error; do not duplicate it in this range scan.
3528
+ continue
3529
+ range_map, range_mask, _ = project_point_cloud_to_point_map(
3530
+ range_cloud,
3531
+ range_intrinsics,
3532
+ range_extrinsics,
3533
+ range_shape,
3534
+ )
3535
+ if np.any(range_mask):
3536
+ range_depths = np.linalg.norm(range_map[range_mask], axis=-1)
3537
+ finite_depths = range_depths[
3538
+ np.isfinite(range_depths) & (range_depths > 0)
3539
+ ]
3540
+ if finite_depths.size:
3541
+ dataset_max_depth = max(
3542
+ dataset_max_depth,
3543
+ float(np.max(finite_depths)),
3544
+ )
3545
+ f_a_max_threshold = dataset_max_depth / 20.0
3546
+
3465
3547
  resolved_domain: Optional[str] = fov_domain
3466
3548
  threshold_deg: Optional[float] = (
3467
3549
  get_threshold_for_domain(resolved_domain) if resolved_domain else None
@@ -3557,7 +3639,10 @@ def evaluate_points_3d_sparse_samples(
3557
3639
  store["dec_images"].append(dec)
3558
3640
  fval["error_decomposition"] = dict(dec)
3559
3641
  cd = compute_sparse_cloud_distance_metrics(
3560
- pred_map[pred_ok], gt_cloud, max_points=max_cloud_points
3642
+ pred_map[pred_ok],
3643
+ gt_cloud,
3644
+ max_points=max_cloud_points,
3645
+ fscore_auc_max_threshold=f_a_max_threshold,
3561
3646
  )
3562
3647
  if cd:
3563
3648
  store["cloud_images"].append(cd)
@@ -3975,6 +4060,8 @@ def evaluate_points_3d_sparse_samples(
3975
4060
  "projected_pixels": total_projected_pixels,
3976
4061
  "evaluated_points": total_evaluated_points,
3977
4062
  "gt_synthesized_from_depth": False,
4063
+ "max_depth": dataset_max_depth,
4064
+ "f_a_max_threshold": f_a_max_threshold,
3978
4065
  },
3979
4066
  "space_info": {
3980
4067
  "input_space_detected": input_space_detected,
@@ -105,6 +105,7 @@ from .points3d_geometry import (
105
105
  )
106
106
  from .points3d_cloud import (
107
107
  compute_cloud_distance_metrics,
108
+ compute_fscore_auc,
108
109
  compute_sparse_cloud_distance_metrics,
109
110
  FSCORE_THRESHOLDS as POINTS3D_FSCORE_THRESHOLDS,
110
111
  DEFAULT_MAX_POINTS as POINTS3D_DEFAULT_MAX_POINTS,
@@ -197,6 +198,7 @@ __all__ = [
197
198
  "compute_point_normal_angles",
198
199
  "compute_point_edge_f1",
199
200
  "compute_cloud_distance_metrics",
201
+ "compute_fscore_auc",
200
202
  "compute_sparse_cloud_distance_metrics",
201
203
  "POINTS3D_FSCORE_THRESHOLDS",
202
204
  "POINTS3D_DEFAULT_MAX_POINTS",
@@ -36,11 +36,66 @@ def _subsample(points: np.ndarray, max_points: int) -> np.ndarray:
36
36
  return points[idx]
37
37
 
38
38
 
39
+ def compute_fscore_auc(
40
+ dist_pred_to_gt: np.ndarray,
41
+ dist_gt_to_pred: np.ndarray,
42
+ max_threshold: float,
43
+ ) -> Optional[float]:
44
+ """Return the normalized area under the point-cloud F1 curve.
45
+
46
+ ``F_A`` integrates the symmetric nearest-neighbour F1 score over all
47
+ distance tolerances from zero through ``max_threshold``. The empirical
48
+ precision and recall curves are step functions, so this implementation
49
+ integrates their harmonic mean exactly rather than choosing a sampling
50
+ resolution. The result is reported as a percentage in ``[0, 100]``.
51
+ """
52
+ pred_dist = np.asarray(dist_pred_to_gt, dtype=np.float64).reshape(-1)
53
+ gt_dist = np.asarray(dist_gt_to_pred, dtype=np.float64).reshape(-1)
54
+ if (
55
+ pred_dist.size == 0
56
+ or gt_dist.size == 0
57
+ or not np.isfinite(max_threshold)
58
+ or max_threshold <= 0
59
+ ):
60
+ return None
61
+
62
+ pred_dist = np.sort(pred_dist[np.isfinite(pred_dist)])
63
+ gt_dist = np.sort(gt_dist[np.isfinite(gt_dist)])
64
+ if pred_dist.size == 0 or gt_dist.size == 0:
65
+ return None
66
+
67
+ events = np.unique(
68
+ np.concatenate(
69
+ [
70
+ np.array([0.0, max_threshold], dtype=np.float64),
71
+ pred_dist[(pred_dist >= 0.0) & (pred_dist < max_threshold)],
72
+ gt_dist[(gt_dist >= 0.0) & (gt_dist < max_threshold)],
73
+ ]
74
+ )
75
+ )
76
+ left = events[:-1]
77
+ widths = np.diff(events)
78
+ # Immediately to the right of an event, all distances equal to that event
79
+ # satisfy the paper's threshold test. Point values at the discontinuities
80
+ # have zero measure and therefore do not affect the integral.
81
+ precision = np.searchsorted(pred_dist, left, side="right") / pred_dist.size
82
+ recall = np.searchsorted(gt_dist, left, side="right") / gt_dist.size
83
+ denominator = precision + recall
84
+ f1 = np.divide(
85
+ 2.0 * precision * recall,
86
+ denominator,
87
+ out=np.zeros_like(denominator),
88
+ where=denominator > 0,
89
+ )
90
+ return float(np.sum(f1 * widths) / max_threshold * 100.0)
91
+
92
+
39
93
  def compute_cloud_distance_metrics(
40
94
  pred_points: np.ndarray,
41
95
  gt_points: np.ndarray,
42
96
  max_points: int = DEFAULT_MAX_POINTS,
43
97
  thresholds: tuple = FSCORE_THRESHOLDS,
98
+ fscore_auc_max_threshold: Optional[float] = None,
44
99
  ) -> Optional[dict]:
45
100
  """Chamfer distance and F-score between two 3D point sets.
46
101
 
@@ -49,6 +104,8 @@ def compute_cloud_distance_metrics(
49
104
  gt_points: ``(M, 3)`` ground-truth points.
50
105
  max_points: Per-cloud subsample cap (``None`` to disable).
51
106
  thresholds: F-score distance thresholds in metres.
107
+ fscore_auc_max_threshold: When provided, also compute ``f_a``, the
108
+ normalized F1 AUC through this distance threshold, as a percentage.
52
109
 
53
110
  Returns:
54
111
  Dict with a ``chamfer`` block (``accuracy``, ``completeness``,
@@ -98,7 +155,16 @@ def compute_cloud_distance_metrics(
98
155
  "f1": f1,
99
156
  }
100
157
 
101
- return {"chamfer": chamfer, "fscore": fscore}
158
+ result = {"chamfer": chamfer, "fscore": fscore}
159
+ if fscore_auc_max_threshold is not None:
160
+ f_a = compute_fscore_auc(
161
+ dist_pred_to_gt,
162
+ dist_gt_to_pred,
163
+ fscore_auc_max_threshold,
164
+ )
165
+ if f_a is not None:
166
+ result["f_a"] = f_a
167
+ return result
102
168
 
103
169
 
104
170
  def compute_sparse_cloud_distance_metrics(
@@ -106,6 +172,7 @@ def compute_sparse_cloud_distance_metrics(
106
172
  gt_points: np.ndarray,
107
173
  max_points: int = DEFAULT_MAX_POINTS,
108
174
  thresholds: tuple = FSCORE_THRESHOLDS,
175
+ fscore_auc_max_threshold: Optional[float] = None,
109
176
  ) -> Optional[dict]:
110
177
  """Directed (GT→pred) cloud agreement for a sparse GT point set.
111
178
 
@@ -130,6 +197,9 @@ def compute_sparse_cloud_distance_metrics(
130
197
  gt_points: ``(M, 3)`` ground-truth (sparse) points.
131
198
  max_points: Per-cloud subsample cap (``None`` to disable).
132
199
  thresholds: Recall distance thresholds in metres.
200
+ fscore_auc_max_threshold: When provided, compute symmetric ``f_a`` as
201
+ the normalized F1 AUC through this distance threshold. The fixed
202
+ threshold metrics remain directed because the GT is sparse.
133
203
 
134
204
  Returns:
135
205
  Dict with a ``chamfer`` block (``completeness``, ``median``) and an
@@ -160,4 +230,18 @@ def compute_sparse_cloud_distance_metrics(
160
230
  recall = float(np.mean(dist_gt_to_pred < tau))
161
231
  fscore[threshold_key(tau)] = {"recall": recall}
162
232
 
163
- return {"chamfer": chamfer, "fscore": fscore}
233
+ result = {"chamfer": chamfer, "fscore": fscore}
234
+ if fscore_auc_max_threshold is not None:
235
+ # F_A is defined from symmetric F1, so it additionally needs the
236
+ # prediction-to-GT distances even though the sparse diagnostic leaves
237
+ # above intentionally remain completeness/recall-only.
238
+ tree_gt = cKDTree(gt)
239
+ dist_pred_to_gt, _ = tree_gt.query(pred, k=1)
240
+ f_a = compute_fscore_auc(
241
+ dist_pred_to_gt,
242
+ dist_gt_to_pred,
243
+ fscore_auc_max_threshold,
244
+ )
245
+ if f_a is not None:
246
+ result["f_a"] = f_a
247
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.20.0
3
+ Version: 2.22.0
4
4
  Summary: Depth map evaluation toolkit with comprehensive metrics
5
5
  Author: Depth Eval Contributors
6
6
  License: MIT
@@ -493,7 +493,7 @@ a `points_3d` canonical alias, keyed as
493
493
  | Euclidean 3D agreement | `point_error.{image_mean,image_median,pixel_pool}.*` | 3D EPE `mae3d`, `rmse3d`, `median3d`/`p90`/`p95`, relative error `rel_median`/`rel_p90`, and 3D δ-accuracy `acc_<τ>` (within τ m) / `acc_rel_<τ>` |
494
494
  | Error decomposition | `error_decomposition.{radial_*,lateral_*,lateral_fraction}` + `angular_error.*` + `rho_a.*` | Splits each error vector into a **radial** (≈ depth) and **lateral** (≈ camera-model) component; `lateral_fraction ∈ [0,1]` attributes blame. Angular ray error and ρ_A (on native directions) measure the implied camera model |
495
495
  | Geometric | `geometric.normal_consistency.*`, `geometric.point_edge_f1.*` | True-3D surface normals (cross-product of point-map tangents, no focal assumption) and a 3D discontinuity F1 |
496
- | Cloud distance | `cloud_distance.chamfer.*`, `cloud_distance.fscore.tau_<τ>.*` | Correspondence-free Chamfer (accuracy/completeness/distance/median) and F-score (precision/recall/f1) at distance thresholds τ |
496
+ | Cloud distance | `cloud_distance.chamfer.*`, `cloud_distance.fscore.tau_<τ>.*`, `cloud_distance.f_a` | Correspondence-free Chamfer (accuracy/completeness/distance/median), F-score (precision/recall/f1) at fixed distance thresholds τ, and F-score AUC (`f_a`) integrated up to 1/20 of the dataset's maximum GT depth |
497
497
 
498
498
  #### Points-3D gauge alignment
499
499
 
@@ -607,7 +607,7 @@ For sparse depth outputs, the internal Python result dict still uses `sparse_dep
607
607
 
608
608
  For points_3d outputs, the internal Python result dict uses `points_3d_native`, `points_3d_metric`, and `points_3d` (canonical alias), but serialized `eval.json` metric paths are rooted at `points3d.eval` (no underscore) to satisfy the namespace first-segment rule, mirroring `sparsedepth`. The `metric` space is emitted only when a gauge alignment is applied; otherwise only `native` is present and aliased.
609
609
 
610
- When the points_3d metrics come from a sparse pointcloud GT (`gt.sparse_depth` + a predicted `points_3d` map or a dense depth prediction), they use the same `points3d.eval` namespace and are written as `eval.json` beside the selected prediction modality (or to the configured `output_file`). Only the `point_error`, `error_decomposition`, and (directed) `cloud_distance` categories are present; the `metric` space is emitted only when the gauge (`--points-3d-alignment` for a point-map prediction, `--depth-alignment` for a depth prediction) calibrates the prediction.
610
+ When the points_3d metrics come from a sparse pointcloud GT (`gt.sparse_depth` + a predicted `points_3d` map or a dense depth prediction), they use the same `points3d.eval` namespace and are written as `eval.json` beside the selected prediction modality (or to the configured `output_file`). Only the `point_error`, `error_decomposition`, and `cloud_distance` categories are present. Fixed-threshold cloud diagnostics are directed (completeness/recall), while `cloud_distance.f_a` performs the symmetric comparison required by the F-score AUC definition. The `metric` space is emitted only when the gauge (`--points-3d-alignment` for a point-map prediction, `--depth-alignment` for a depth prediction) calibrates the prediction.
611
611
 
612
612
  Previous single-depth structure (kept under `depth`) is:
613
613
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-eval"
7
- version = "2.20.0"
7
+ version = "2.22.0"
8
8
  description = "Depth map evaluation toolkit with comprehensive metrics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,5 +1,6 @@
1
1
  """Tests for the structured meta block in eval.json output."""
2
2
 
3
+ import json
3
4
  import re
4
5
 
5
6
  from euler_eval.cli import (
@@ -7,6 +8,8 @@ from euler_eval.cli import (
7
8
  _clean_per_file_metrics,
8
9
  _depth_eval_axes,
9
10
  _DEPTH_EVAL_DESCRIPTIONS,
11
+ _log_package_metadata,
12
+ _package_metadata,
10
13
  _sparse_depth_eval_axes,
11
14
  _SPARSE_DEPTH_EVAL_DESCRIPTIONS,
12
15
  _SPARSE_DEPTH_METRIC_NAMESPACE,
@@ -26,6 +29,23 @@ _RGB_EVAL_AXES = _rgb_eval_axes()
26
29
  _METRIC_NAMESPACE_RE = re.compile(r"^[a-z0-9]+(?:\.[a-z0-9_]+)+$")
27
30
 
28
31
 
32
+ class TestPackageMetadataLogging:
33
+ def test_package_metadata_contains_runtime_provenance(self):
34
+ metadata = _package_metadata()
35
+ assert metadata["name"] == "euler-eval"
36
+ assert metadata["version"]
37
+ assert metadata["python_version"]
38
+ assert metadata["torch_version"]
39
+ assert "cuda_version" in metadata
40
+
41
+ def test_package_metadata_log_is_structured(self, capsys):
42
+ metadata = _log_package_metadata()
43
+ output = capsys.readouterr().out.strip()
44
+ prefix = "Evaluation package: "
45
+ assert output.startswith(prefix)
46
+ assert json.loads(output[len(prefix):]) == metadata
47
+
48
+
29
49
  def _flatten_numeric_metrics(obj, prefix=""):
30
50
  results = []
31
51
  for key, value in obj.items():
@@ -22,6 +22,7 @@ from euler_eval.evaluate import evaluate_points_3d_samples
22
22
  from euler_eval.metrics import (
23
23
  compute_cloud_distance_metrics,
24
24
  compute_decomposition_metrics,
25
+ compute_fscore_auc,
25
26
  compute_point_edge_f1,
26
27
  compute_point_error_metrics,
27
28
  compute_point_normal_angles,
@@ -245,6 +246,12 @@ class TestGeometry:
245
246
 
246
247
 
247
248
  class TestCloud:
249
+ def test_fscore_auc_extremes(self):
250
+ perfect = np.zeros(4, dtype=np.float64)
251
+ missed = np.full(4, 2.0, dtype=np.float64)
252
+ assert compute_fscore_auc(perfect, perfect, 1.0) == 100.0
253
+ assert compute_fscore_auc(missed, missed, 1.0) == 0.0
254
+
248
255
  def test_perfect_clouds(self):
249
256
  gt, _ = _gt_pointmap()
250
257
  pts = gt.reshape(-1, 3)
@@ -252,6 +259,16 @@ class TestCloud:
252
259
  assert m["chamfer"]["distance"] < 1e-6
253
260
  assert m["fscore"]["tau_0_1"]["f1"] == 1.0
254
261
 
262
+ def test_cloud_f_a_uses_requested_depth_range(self):
263
+ gt, _ = _gt_pointmap()
264
+ pts = gt.reshape(-1, 3)
265
+ m = compute_cloud_distance_metrics(
266
+ pts,
267
+ pts,
268
+ fscore_auc_max_threshold=0.5,
269
+ )
270
+ assert m["f_a"] == 100.0
271
+
255
272
  def test_shifted_cloud(self):
256
273
  gt, _ = _gt_pointmap()
257
274
  pts = gt.reshape(-1, 3)
@@ -288,6 +305,10 @@ class TestEvaluate:
288
305
  assert pe["acc_0_05"] == 100.0
289
306
  assert result["points_3d"]["error_decomposition"]["rho_a"]["mean"] > 0.95
290
307
  assert result["dataset_info"]["fov_domain"] == "sfov"
308
+ assert result["dataset_info"]["f_a_max_threshold"] == pytest.approx(
309
+ result["dataset_info"]["max_depth"] / 20.0
310
+ )
311
+ assert result["points_3d"]["cloud_distance"]["f_a"] == 100.0
291
312
 
292
313
  def test_similarity_recovers_relative_prediction(self):
293
314
  gt, K = _gt_pointmap()
@@ -304,6 +325,10 @@ class TestEvaluate:
304
325
  metric_mae = result["points_3d_metric"]["point_error"]["image_mean"]["mae3d"]
305
326
  assert metric_mae < 1e-3
306
327
  assert native_mae > metric_mae
328
+ assert "f_a" in result["points_3d_native"]["cloud_distance"]
329
+ assert result["points_3d_metric"]["cloud_distance"]["f_a"] == pytest.approx(
330
+ 100.0, abs=1e-3
331
+ )
307
332
  assert result["points_3d"] is result["points_3d_metric"]
308
333
 
309
334
  def test_auto_with_relative_hint_aligns(self):
@@ -418,6 +443,7 @@ class TestCliNamespace:
418
443
  assert envelope["axes"]["space"]["position"] == 0
419
444
  assert envelope["axes"]["reduction"]["position"] == 2
420
445
  assert "mae3d" in envelope["metricDescriptions"]
446
+ assert "f_a" in envelope["metricDescriptions"]
421
447
 
422
448
  def test_real_metric_tree_paths_conform(self):
423
449
  """Flattened paths of an actual eval result live under points3d.eval."""
@@ -132,6 +132,18 @@ class TestSparseCloud:
132
132
  for tau_block in m["fscore"].values():
133
133
  assert set(tau_block) == {"recall"}
134
134
 
135
+ def test_f_a_is_available_without_publishing_symmetric_diagnostics(self):
136
+ points = np.random.RandomState(2).randn(100, 3).astype(np.float32)
137
+ m = compute_sparse_cloud_distance_metrics(
138
+ points,
139
+ points,
140
+ fscore_auc_max_threshold=1.0,
141
+ )
142
+ assert m["f_a"] == 100.0
143
+ assert set(m["chamfer"]) == {"completeness", "median"}
144
+ for tau_block in m["fscore"].values():
145
+ assert set(tau_block) == {"recall"}
146
+
135
147
  def test_offset_cloud_increases_completeness(self):
136
148
  depth = _dense_depth()
137
149
  K = _make_intrinsics(400.0, 400.0, 30.0, 20.0)
@@ -223,6 +235,10 @@ class TestEvaluate:
223
235
  assert p["point_error"]["image_mean"]["mae3d"] < 1e-4
224
236
  assert p["cloud_distance"]["chamfer"]["completeness"] < 1e-4
225
237
  assert p["cloud_distance"]["fscore"]["tau_0_1"]["recall"] == 1.0
238
+ assert "f_a" in p["cloud_distance"]
239
+ assert res["dataset_info"]["f_a_max_threshold"] == pytest.approx(
240
+ res["dataset_info"]["max_depth"] / 20.0
241
+ )
226
242
  assert p["error_decomposition"]["rho_a"]["mean"] > 0.9
227
243
  # No dense-neighbourhood geometric metrics on sparse GT.
228
244
  assert "geometric" not in p
@@ -393,6 +409,7 @@ class TestEvaluateNativePoints:
393
409
  assert p["point_error"]["image_mean"]["mae3d"] < 1e-4
394
410
  assert p["cloud_distance"]["chamfer"]["completeness"] < 1e-4
395
411
  assert p["cloud_distance"]["fscore"]["tau_0_1"]["recall"] == 1.0
412
+ assert 0.0 <= p["cloud_distance"]["f_a"] <= 100.0
396
413
 
397
414
  def test_similarity_recovers_relative_pointmap(self):
398
415
  ds, gt_dense, K = self._pointmap_dataset(None)
File without changes
File without changes