euler-eval 2.31.0__tar.gz → 2.32.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 (114) hide show
  1. {euler_eval-2.31.0/euler_eval.egg-info → euler_eval-2.32.0}/PKG-INFO +5 -3
  2. {euler_eval-2.31.0 → euler_eval-2.32.0}/README.md +4 -2
  3. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/README.md +1 -1
  4. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/cli.md +4 -0
  5. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/configuration.md +6 -5
  6. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/output.md +133 -10
  7. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/cli.py +237 -24
  8. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/config_paths.py +55 -0
  9. euler_eval-2.32.0/euler_eval/protocol.py +145 -0
  10. {euler_eval-2.31.0 → euler_eval-2.32.0/euler_eval.egg-info}/PKG-INFO +5 -3
  11. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval.egg-info/SOURCES.txt +3 -0
  12. {euler_eval-2.31.0 → euler_eval-2.32.0}/pyproject.toml +1 -1
  13. euler_eval-2.32.0/tests/eval_output.py +16 -0
  14. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_cli_sparse_outputs.py +20 -7
  15. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_depth_alignment_output.py +2 -1
  16. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_mask_sky_pairing.py +2 -2
  17. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_mask_sky_prediction.py +2 -2
  18. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_points_3d_sparse.py +8 -8
  19. euler_eval-2.32.0/tests/test_protocol.py +398 -0
  20. euler_eval-2.32.0/tests/test_save_results.py +513 -0
  21. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_sky_iou_cli.py +3 -2
  22. euler_eval-2.31.0/tests/test_save_results.py +0 -301
  23. {euler_eval-2.31.0 → euler_eval-2.32.0}/LICENSE +0 -0
  24. {euler_eval-2.31.0 → euler_eval-2.32.0}/MANIFEST.in +0 -0
  25. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/alignment.md +0 -0
  26. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/distributions.md +0 -0
  27. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/feature-request-unified-evaluation-framework.md +0 -0
  28. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/metrics.md +0 -0
  29. {euler_eval-2.31.0 → euler_eval-2.32.0}/docs/validation.md +0 -0
  30. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/THIRD_PARTY_NOTICES.md +0 -0
  31. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/__init__.py +0 -0
  32. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/calibration.py +0 -0
  33. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/data.py +0 -0
  34. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/distribution_contract.py +0 -0
  35. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/distributions.py +0 -0
  36. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/evaluate.py +0 -0
  37. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metric_sets.py +0 -0
  38. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/__init__.py +0 -0
  39. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/_natural_scene_models.py +0 -0
  40. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/absrel.py +0 -0
  41. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/daniel_error.py +0 -0
  42. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  43. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  44. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/depth_standard.py +0 -0
  45. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/fade.py +0 -0
  46. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/fid_kid.py +0 -0
  47. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  48. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  49. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  50. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/lpips_metric.py +0 -0
  51. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/niqe.py +0 -0
  52. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/normal_consistency.py +0 -0
  53. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/points3d_cloud.py +0 -0
  54. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/points3d_decomposition.py +0 -0
  55. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/points3d_distance.py +0 -0
  56. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/points3d_geometry.py +0 -0
  57. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/psnr.py +0 -0
  58. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  59. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  60. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  61. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/rho_a.py +0 -0
  62. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/rmse.py +0 -0
  63. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  64. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/sky_iou.py +0 -0
  65. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/ssim.py +0 -0
  66. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/tail_errors.py +0 -0
  67. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/metrics/utils.py +0 -0
  68. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/sanity_checker.py +0 -0
  69. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/schemas/distribution-v1.schema.json +0 -0
  70. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  71. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval/validation.py +0 -0
  72. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  73. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval.egg-info/entry_points.txt +0 -0
  74. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval.egg-info/requires.txt +0 -0
  75. {euler_eval-2.31.0 → euler_eval-2.32.0}/euler_eval.egg-info/top_level.txt +0 -0
  76. {euler_eval-2.31.0 → euler_eval-2.32.0}/init_cache.py +0 -0
  77. {euler_eval-2.31.0 → euler_eval-2.32.0}/scripts/generate_distribution_fixtures.py +0 -0
  78. {euler_eval-2.31.0 → euler_eval-2.32.0}/setup.cfg +0 -0
  79. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/__init__.py +0 -0
  80. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/conftest.py +0 -0
  81. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/distribution_contract_utils.py +0 -0
  82. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/README.md +0 -0
  83. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/counts-depth.eval.json +0 -0
  84. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/counts-points3d.eval.json +0 -0
  85. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/counts-sparsedepth.eval.json +0 -0
  86. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/future-sum-by-depth.eval.json +0 -0
  87. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/histogram-only.eval.json +0 -0
  88. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/per-file-only.eval.json +0 -0
  89. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/fixtures/distributions/scalar-only.eval.json +0 -0
  90. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/mock_files/calibration_output.json +0 -0
  91. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/mock_files/depth_output.json +0 -0
  92. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/mock_files/rgb_output.json +0 -0
  93. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/mock_files/segmentation_output.json +0 -0
  94. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_alignment.py +0 -0
  95. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_cli_device.py +0 -0
  96. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_config.py +0 -0
  97. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_data.py +0 -0
  98. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_data_builders.py +0 -0
  99. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_depth_standard.py +0 -0
  100. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_distribution_contract.py +0 -0
  101. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_distributions.py +0 -0
  102. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_domain_metrics.py +0 -0
  103. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_evaluate_helpers.py +0 -0
  104. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_init_cache.py +0 -0
  105. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_integration.py +0 -0
  106. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_meta_output.py +0 -0
  107. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_metrics_utils.py +0 -0
  108. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_normal_consistency.py +0 -0
  109. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_points_3d.py +0 -0
  110. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_rgb_fid_output.py +0 -0
  111. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_rho_a.py +0 -0
  112. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_sky_iou.py +0 -0
  113. {euler_eval-2.31.0 → euler_eval-2.32.0}/tests/test_sparse_depth.py +0 -0
  114. {euler_eval-2.31.0 → euler_eval-2.32.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.31.0
3
+ Version: 2.32.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
@@ -133,8 +133,10 @@ euler-eval config.json --batch-size 32
133
133
 
134
134
  Ground truth and prediction files are matched by ID rather than by directory
135
135
  order, so an incomplete prediction set scores the frames it does have instead of
136
- silently comparing the wrong pairs. Results are written next to each prediction
137
- modality:
136
+ silently comparing the wrong pairs. Results are written into each prediction
137
+ dataset, under `.eval/` beside its `.ds_crawler/` metadata — e.g.
138
+ `/data/model_a/depth/.eval/eval.json`, or `.eval/<scope>/eval.json` for a scoped
139
+ dataset ([details](docs/output.md#where-results-land)):
138
140
 
139
141
  ```json
140
142
  {
@@ -83,8 +83,10 @@ euler-eval config.json --batch-size 32
83
83
 
84
84
  Ground truth and prediction files are matched by ID rather than by directory
85
85
  order, so an incomplete prediction set scores the frames it does have instead of
86
- silently comparing the wrong pairs. Results are written next to each prediction
87
- modality:
86
+ silently comparing the wrong pairs. Results are written into each prediction
87
+ dataset, under `.eval/` beside its `.ds_crawler/` metadata — e.g.
88
+ `/data/model_a/depth/.eval/eval.json`, or `.eval/<scope>/eval.json` for a scoped
89
+ dataset ([details](docs/output.md#where-results-land)):
88
90
 
89
91
  ```json
90
92
  {
@@ -7,7 +7,7 @@
7
7
  | [Spaces & alignment](alignment.md) | Why `native` and `metric` exist, depth affine fitting, points-3d gauge alignment, benchmark depth bins |
8
8
  | [Metrics](metrics.md) | The full metric inventory per modality, with the key each one is written under |
9
9
  | [Error distributions](distributions.md) | Optional per-sample and pooled RMSE histograms, bin settings, axes and JSON layout |
10
- | [Results & output](output.md) | Where files land, the anatomy of `eval.json`, per-file metrics, the sanity-check report |
10
+ | [Results & output](output.md) | Where files land, the anatomy of `eval.json`, comparing evaluations, per-file metrics, the sanity-check report |
11
11
  | [In-training validation](validation.md) | The programmatic API for scoring in-memory predictions during training |
12
12
 
13
13
  ## Proposed features
@@ -37,6 +37,7 @@ validation exits with status `1` before any dataset is opened.
37
37
  | `--distribution-bins` | `int` | `50` | Total histogram bins, including tails; minimum 3 |
38
38
  | `--distribution-scale` | `{log,linear}` | `log` | Spacing of finite histogram bins |
39
39
  | `--distribution-range` | `float float` | `0.001 100` | Finite error range `[MIN, MAX)` in evaluated space units; zero/small-error and overflow bins retain the tails |
40
+ | `--eval-name` | `str` | `eval` | Name this evaluation: results are written as `<name>.json` in each prediction dataset's `.eval/` folder, so differently named runs sit side by side; a dataset's `output_file` still wins ([details](output.md#where-results-land)) |
40
41
 
41
42
  Histogram settings can also be specified in the config's `distributions`
42
43
  section. A bin, scale or range flag enables histograms and overrides that
@@ -69,6 +70,9 @@ euler-eval config.json --sky-iou --sky-depth 100
69
70
  # Only depth evaluation
70
71
  euler-eval config.json --skip-rgb --skip-rays --skip-points-3d
71
72
 
73
+ # Keep a sky-masked run beside the default results: .eval/[<scope>/]no_sky.json
74
+ euler-eval config.json --mask-sky --eval-name no_sky
75
+
72
76
  # Force affine scale+shift alignment on all depth predictions
73
77
  euler-eval config.json --depth-alignment affine
74
78
 
@@ -127,7 +127,7 @@ would be ambiguous.
127
127
  | `sky_mask.path` | no | Auxiliary predicted sky mask for a dense depth entry; used with `--sky-depth`, `--mask-sky-prediction` and `--sky-iou` |
128
128
  | `rays.path` | no\* | Predicted ray direction map dataset |
129
129
  | `points_3d.path` | no\* | Predicted per-pixel 3D point map `(H,W,3)` |
130
- | `output_file` | no | Custom results path (default: `eval.json` in the first modality path) |
130
+ | `output_file` | no | Custom results path for every modality of this entry, replacing the `.eval/` layout and `--eval-name` (default: `.eval/[<scope>/][split_<split>/]eval.json` in each prediction modality's dataset root; see [where results land](output.md#where-results-land)) |
131
131
 
132
132
  \* At least one prediction modality must be present.
133
133
 
@@ -214,10 +214,11 @@ and evaluated as `points_3d`.
214
214
 
215
215
  An explicit `datasets[].points_3d` prediction can be evaluated separately
216
216
  against the same sparse cloud. That evaluation uses the points-3D similarity
217
- gauge, writes an atomic `points3d.eval` metric set beside the point-map
218
- prediction, and is controlled by `--skip-points-3d`. If a dataset provides both
219
- depth and `points_3d`, each prediction modality gets its own `eval.json` at its
220
- own path.
217
+ gauge, writes an atomic `points3d.eval` metric set to the point-map
218
+ prediction's results file, and is controlled by `--skip-points-3d`. If a dataset
219
+ provides both depth and `points_3d`, each prediction modality gets its own
220
+ results file, also when both live in one archive under separate scopes (see
221
+ [where results land](output.md#where-results-land)).
221
222
 
222
223
  Sparse depth does not require segmentation GT. `gt.segmentation` is loaded only
223
224
  when `--mask-sky` or `--sky-iou` is set. `--mask-sky` excludes sky pixels from
@@ -2,20 +2,56 @@
2
2
 
3
3
  ## Where results land
4
4
 
5
- One `eval.json` is written **per modality, per prediction dataset**, next to the
6
- prediction it scores — `/data/model_a/depth/eval.json` for the depth results of
7
- the dataset whose depth path is `/data/model_a/depth`, and so on. If that path
8
- passes through a `.zip`, the JSON is written *into* the archive rather than
9
- beside it, so zip-backed datasets stay self-contained.
5
+ Results are written **per modality, per prediction dataset**, into a `.eval/`
6
+ folder at the prediction dataset's root, beside its `.ds_crawler/` metadata:
10
7
 
11
- `output_file` in a dataset entry overrides the destination. It applies to the
12
- dataset as a whole, so when several modalities are evaluated for the same
13
- dataset the per-modality default locations are usually what you want.
8
+ ```text
9
+ /data/model_a/ # a datasets[].<modality>.path
10
+ .ds_crawler/
11
+ depth/ rgb/ # ds-crawler metadata scopes
12
+ .eval/
13
+ depth/
14
+ eval.json # depth results
15
+ no_sky.json # depth results of --eval-name no_sky
16
+ split_test/eval.json # depth results for "/data/model_a:test"
17
+ rgb/
18
+ eval.json # rgb results
19
+ ```
20
+
21
+ The folder below `.eval/` mirrors how the prediction was loaded:
22
+
23
+ | Prediction | Results file |
24
+ |---|---|
25
+ | Metadata scope with its own `.ds_crawler/<scope>/` | `.eval/<scope>/<name>.json` |
26
+ | Root-level `.ds_crawler/` metadata (no scope) | `.eval/<name>.json` |
27
+ | A split is selected (`:test` or `split`) | `split_<split>/` is added before the file name |
28
+
29
+ The scope is the path's `#scope=` selector, otherwise the config key (`depth`,
30
+ `relative_depth`, `rgb`, `points_3d`, …). As in euler-loading, a scope without
31
+ metadata of its own falls back to the root-level `.ds_crawler/`, and so do its
32
+ results. This way modalities and splits that share one dataset root or archive
33
+ each get their own file. If the root is a `.zip`, `.eval/` is written *into* the
34
+ archive (inside its top-level folder when the archive wraps everything in one
35
+ named after it), so zip-backed datasets stay self-contained.
36
+
37
+ `<name>` is `eval` unless the run is named with `--eval-name`:
38
+ `--mask-sky --eval-name no_sky` writes `no_sky.json` next to an earlier
39
+ `eval.json` instead of replacing it. Names use letters, digits, `.`, `-` and
40
+ `_`; `.json` is appended.
41
+
42
+ `output_file` in a dataset entry overrides all of this. It is one path for the
43
+ whole dataset entry: every modality evaluated for that entry is written to it,
44
+ and the last one written replaces the others, so use it for single-modality
45
+ entries.
46
+
47
+ Earlier releases wrote `eval.json` directly at the dataset root (inside the
48
+ archive for zips). Readers such as euler-view look in `.eval/<scope>/`, then
49
+ `.eval/`, then fall back to that legacy location.
14
50
 
15
51
  Each `eval.json` is atomic and declares one metric namespace. With
16
52
  `gt.sparse_depth`, a depth-family prediction writes only `sparsedepth.eval`; an
17
- explicit `points_3d` prediction writes only `points3d.eval` beside the point
18
- map. A depth prediction does not implicitly produce a second points-3D metric
53
+ explicit `points_3d` prediction writes only `points3d.eval` to the point
54
+ map's own results file. A depth prediction does not implicitly produce a second points-3D metric
19
55
  set.
20
56
 
21
57
  ## Anatomy of `eval.json`
@@ -37,6 +73,10 @@ set.
37
73
  "axes": { "space": { "…": "…" }, "category": { "…": "…" } },
38
74
  "metricDescriptions": { "absrel": { "…": "…" } }
39
75
  },
76
+ "protocol": {
77
+ "schemaVersion": "1.0",
78
+ "parameters": { "reference.gt.dataset": "muses", "method.depth_alignment": "auto_affine", "…": "…" }
79
+ },
40
80
  "dataset_info": { "num_pairs": 500, "gt_name": "GT", "pred_name": "model_a" },
41
81
  "meta": {
42
82
  "version": "2.25.0",
@@ -56,6 +96,7 @@ set.
56
96
  | Block | What it is for |
57
97
  |---|---|
58
98
  | `metricSet` | The euler-metric-naming envelope: producer, namespace, axis declarations and per-metric descriptions (unit, direction, bounds), plus how the spaces were resolved |
99
+ | `protocol` | The comparison keys: every setting that changes metric values, as a flat map ([Comparing evaluations](#comparing-evaluations)) |
59
100
  | `dataset_info` | How many pairs were actually matched, and the display names of both sides |
60
101
  | `meta` | Full provenance: package version, device, both source paths and their resolved dimensions, modality metadata, and the effective evaluation flags |
61
102
  | *metric tree* | The metrics themselves, under the modality root (`depth`, `rgb`, `rays`, `sparsedepth`, `points3d`) |
@@ -137,6 +178,88 @@ because these keys have no depth space. `metricDescriptions` gains `iou`,
137
178
  `precision` and `recall` (higher is better, bounds `[0, 1]`). Every other key
138
179
  decomposes exactly as before.
139
180
 
181
+ ## Comparing evaluations
182
+
183
+ `protocol.parameters` says how an evaluation was set up, as one flat map of
184
+ dotted keys. Its keys are the comparison keys: two evaluations of the same
185
+ modality were set up the same way when their maps are equal, and the keys whose
186
+ values differ say where. Nothing else in the file needs to be compared for that;
187
+ `meta` stays the full record of the run, paths and device included.
188
+
189
+ ```json
190
+ "protocol": {
191
+ "schemaVersion": "1.0",
192
+ "parameters": {
193
+ "reference.gt.modality": "sparse_depth",
194
+ "reference.gt.dataset": "seeingthroughfog",
195
+ "reference.gt.scope": "sparse_depth",
196
+ "reference.gt.split": "test",
197
+ "reference.intrinsics.dataset": "seeingthroughfog",
198
+ "reference.intrinsics.scope": "intrinsics",
199
+ "reference.intrinsics.split": "test",
200
+ "reference.camera_extrinsics.dataset": "seeingthroughfog",
201
+ "reference.camera_extrinsics.scope": "camera_extrinsics",
202
+ "reference.camera_extrinsics.split": "test",
203
+ "reference.lidar_extrinsics.dataset": null,
204
+ "reference.lidar_extrinsics.scope": null,
205
+ "reference.lidar_extrinsics.split": null,
206
+ "reference.segmentation.dataset": null,
207
+ "reference.segmentation.scope": null,
208
+ "reference.segmentation.split": null,
209
+ "population.samples": 943,
210
+ "population.gt_sky_masking": false,
211
+ "population.pred_sky_masking": false,
212
+ "population.evaluated_resolution": [1024, 1920],
213
+ "population.benchmark_depth_range_m": [0.01, 80.0],
214
+ "method.metrics_revision": 1,
215
+ "method.depth_alignment": "auto_affine",
216
+ "method.sky_depth_m": null
217
+ }
218
+ }
219
+ ```
220
+
221
+ The first segment of a key is its section:
222
+
223
+ | Section | Holds |
224
+ |---|---|
225
+ | `reference` | The GT the prediction is scored against (`gt`, with its config key as `modality`) and every other GT input the modality reads, each identified by `dataset` (the `dataset.id` of its `dataset-head.json`), metadata `scope` and `split` — not by file path, so the same data on another machine matches |
226
+ | `population` | Which samples and pixels were scored: the matched `samples`, sky masking, the grid the metrics were computed on and benchmark depth bins |
227
+ | `method` | How they were scored: alignment modes, the sky-depth cap, how GT and prediction resolutions were reconciled, the FID backend and `metrics_revision` |
228
+
229
+ What is recorded, per modality:
230
+
231
+ | Modality | `reference` inputs besides `gt` | `population` | `method` |
232
+ |---|---|---|---|
233
+ | `depth` | `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `pred_sky_masking`, `evaluated_resolution`, `benchmark_depth_range_m` | `depth_alignment`, `sky_depth_m`, `spatial_alignment` |
234
+ | `sparsedepth` | `intrinsics`, `camera_extrinsics`, `lidar_extrinsics`, `segmentation` | as `depth` | `depth_alignment`, `sky_depth_m` |
235
+ | `rgb` | `depth`, `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `evaluated_resolution`, `benchmark_depth_range_m` | `fid_backend`, `spatial_alignment` |
236
+ | `rays` | `calibration` | `samples`, `evaluated_resolution` | `spatial_alignment` |
237
+ | `points3d` (dense) | `intrinsics`, `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `evaluated_resolution` | `points_3d_alignment`, `spatial_alignment` |
238
+ | `points3d` (sparse GT) | `intrinsics`, `camera_extrinsics`, `lidar_extrinsics`, `segmentation` | `samples`, `gt_sky_masking`, `evaluated_resolution` | `points_3d_alignment` |
239
+
240
+ Every modality also has `method.metrics_revision`.
241
+
242
+ Rules that keep comparisons a plain map comparison:
243
+
244
+ - Only settings that change the value of a metric both files contain are
245
+ parameters. `--domain`, `--sky-iou` and histogram settings only add metrics
246
+ (histograms describe their bins in the distribution registry); device, batch
247
+ size, workers, paths and `--eval-name` do not change values.
248
+ - Every key of a modality is always written, `null` or `false` when off, so a
249
+ key missing from one side means "not recorded", never "off". Compare the union
250
+ of both key sets.
251
+ - Values are scalars or lists of scalars, never objects.
252
+ - `method.metrics_revision` changes only when a release changes metric values
253
+ for identical inputs and settings, which the [changelog](../CHANGELOG.md)
254
+ then says. Different revisions are not comparable even when every other
255
+ key matches.
256
+ - Settings are recorded, not what they caused. With `--mask-sky-prediction`,
257
+ `population.pred_sky_masking` is `true` for every prediction, while
258
+ `meta.pred_sky_masking` records how much each model's own sky removed;
259
+ `metricSet.metadata` records whether an alignment was actually applied.
260
+ - Renaming or removing a key bumps the major `schemaVersion`; adding one bumps
261
+ the minor version.
262
+
140
263
  ## Spaces and canonical aliases
141
264
 
142
265
  Depth-like and points-3d results are nested by space:
@@ -11,16 +11,19 @@ import importlib.metadata
11
11
  import json
12
12
  import math
13
13
  import platform
14
+ import re
14
15
  import sys
15
16
  import zipfile
16
17
  from dataclasses import replace
17
18
  from pathlib import Path
18
19
 
19
20
  import torch
21
+ from ds_crawler.zip_utils import get_zip_root_prefix
20
22
 
21
23
  from .config_paths import (
22
24
  GT_SEGMENTATION_KEYS,
23
25
  normalize_modality_path,
26
+ resolve_dataset_location,
24
27
  resolve_gt_segmentation_path,
25
28
  )
26
29
  from .data import (
@@ -53,6 +56,7 @@ from .metric_sets import (
53
56
  resolve_metric_sets,
54
57
  selected_domain_categories,
55
58
  )
59
+ from .protocol import build_protocol, evaluated_resolution, reference_identities
56
60
  from .sanity_checker import SanityChecker
57
61
 
58
62
  try:
@@ -1151,10 +1155,82 @@ def _save_json_to_zip(zip_path: Path, internal_name: str, data: dict) -> None:
1151
1155
  zf.writestr(internal_name, json_bytes)
1152
1156
 
1153
1157
 
1158
+ EVAL_DIR = ".eval"
1159
+ DEFAULT_EVAL_NAME = "eval"
1160
+ _EVAL_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
1161
+
1162
+
1163
+ def validate_eval_name(value: str) -> str:
1164
+ """Validate an evaluation name and return it without a ``.json`` suffix.
1165
+
1166
+ The name becomes the results file name (``<name>.json``) below each
1167
+ prediction dataset's ``.eval/`` folder, so it is one portable path
1168
+ segment, like a ds-crawler split or scope name.
1169
+
1170
+ Raises:
1171
+ ValueError: When *value* is not a plain name.
1172
+ """
1173
+ name = value.strip()
1174
+ if name.endswith(".json"):
1175
+ name = name[: -len(".json")]
1176
+ if not _EVAL_NAME_PATTERN.match(name):
1177
+ raise ValueError(
1178
+ f"{value!r} is not a valid evaluation name: use letters, digits, "
1179
+ "'.', '-' or '_', starting with a letter or digit (e.g. 'no_sky'); "
1180
+ "'.json' is appended."
1181
+ )
1182
+ return name
1183
+
1184
+
1185
+ def default_eval_path(
1186
+ modality_path: str | Path,
1187
+ *,
1188
+ modality_key: str,
1189
+ split: str | None = None,
1190
+ eval_name: str = DEFAULT_EVAL_NAME,
1191
+ ) -> Path:
1192
+ """Return where a prediction modality's results land by default.
1193
+
1194
+ Results go to a dataset-level ``.eval/`` folder beside ``.ds_crawler/``:
1195
+ ``.eval/<scope>/`` when the modality's metadata scope (the ``#scope=``
1196
+ selector, else the config key) has its own ``.ds_crawler/<scope>/``
1197
+ metadata, otherwise ``.eval/`` itself, matching how euler-loading falls
1198
+ back to root-level metadata. A selected split adds ``split_<split>/``.
1199
+ """
1200
+ root, scope, split = resolve_dataset_location(
1201
+ modality_path, modality_key=modality_key, split=split
1202
+ )
1203
+ eval_dir = root / EVAL_DIR
1204
+ if scope is not None:
1205
+ eval_dir = eval_dir / scope
1206
+ if split is not None:
1207
+ eval_dir = eval_dir / f"split_{split}"
1208
+ return eval_dir / f"{eval_name}.json"
1209
+
1210
+
1211
+ def _zip_internal_name(zip_path: Path, internal_name: str) -> str:
1212
+ """Place *internal_name* inside a zip's wrapper folder, if it has one.
1213
+
1214
+ ds-crawler treats a single top-level folder named after the archive
1215
+ (``model.zip`` -> ``model/``) as the dataset root. Writing beside that
1216
+ folder would end the single-root layout and hide ``.ds_crawler/``.
1217
+ """
1218
+ prefix = get_zip_root_prefix(zip_path)
1219
+ if (
1220
+ prefix
1221
+ and prefix.rstrip("/") == zip_path.stem
1222
+ and not internal_name.startswith(prefix)
1223
+ ):
1224
+ return prefix + internal_name
1225
+ return internal_name
1226
+
1227
+
1154
1228
  def save_results(
1155
1229
  results: dict,
1156
1230
  dataset_config: dict,
1157
1231
  modality: str | None = None,
1232
+ *,
1233
+ eval_name: str = DEFAULT_EVAL_NAME,
1158
1234
  ) -> Path:
1159
1235
  """Save results to output file.
1160
1236
 
@@ -1168,6 +1244,10 @@ def save_results(
1168
1244
  modality: When set, save to this specific modality's path
1169
1245
  (e.g. ``"depth"`` or ``"rgb"``). Falls back to the first
1170
1246
  available modality path when *None*.
1247
+ eval_name: Evaluation name (``--eval-name``); results are written
1248
+ as ``<eval_name>.json`` in the dataset's ``.eval/`` layout, see
1249
+ :func:`default_eval_path`. A dataset's explicit ``output_file``
1250
+ takes precedence over the whole layout.
1171
1251
 
1172
1252
  Returns:
1173
1253
  Path where results were saved.
@@ -1177,38 +1257,38 @@ def save_results(
1177
1257
  selected_modality = modality
1178
1258
  if selected_modality == "depth" and "depth" not in dataset_config:
1179
1259
  selected_modality, _ = _prediction_depth_entry(dataset_config)
1180
- if (
1260
+ if not (
1181
1261
  selected_modality is not None
1182
1262
  and selected_modality in dataset_config
1183
1263
  and "path" in dataset_config[selected_modality]
1184
1264
  ):
1185
- output_file = (
1186
- normalize_modality_path(
1187
- dataset_config[selected_modality]["path"],
1188
- split=dataset_config[selected_modality].get("split"),
1189
- )
1190
- / "eval.json"
1265
+ # Default: save alongside first available modality path
1266
+ selected_modality = next(
1267
+ (
1268
+ mod
1269
+ for mod in (*_PRED_DEPTH_KEYS, "rgb", "rays")
1270
+ if mod in dataset_config and "path" in dataset_config[mod]
1271
+ ),
1272
+ None,
1273
+ )
1274
+ if selected_modality is not None:
1275
+ entry = dataset_config[selected_modality]
1276
+ output_file = default_eval_path(
1277
+ entry["path"],
1278
+ modality_key=selected_modality,
1279
+ split=entry.get("split"),
1280
+ eval_name=eval_name,
1191
1281
  )
1192
1282
  else:
1193
- # Default: save alongside first available modality path
1194
- for mod in (*_PRED_DEPTH_KEYS, "rgb", "rays"):
1195
- if mod in dataset_config and "path" in dataset_config[mod]:
1196
- output_file = (
1197
- normalize_modality_path(
1198
- dataset_config[mod]["path"],
1199
- split=dataset_config[mod].get("split"),
1200
- )
1201
- / "eval.json"
1202
- )
1203
- break
1204
- if output_file is None:
1205
- output_file = Path("eval.json")
1283
+ output_file = Path(f"{eval_name}.json")
1206
1284
  else:
1207
1285
  output_file = Path(output_file)
1208
1286
 
1209
1287
  zip_path, internal_name = _find_zip_ancestor(output_file)
1210
1288
  if zip_path is not None and internal_name:
1211
- _save_json_to_zip(zip_path, internal_name, results)
1289
+ _save_json_to_zip(
1290
+ zip_path, _zip_internal_name(zip_path, internal_name), results
1291
+ )
1212
1292
  else:
1213
1293
  output_file.parent.mkdir(parents=True, exist_ok=True)
1214
1294
  with open(output_file, "w") as f:
@@ -1513,12 +1593,27 @@ def main():
1513
1593
  "--distribution-range", type=float, nargs=2, default=None, metavar=("MIN", "MAX"),
1514
1594
  help="Finite histogram range in evaluated space units (default: 0.001 100); tails are included",
1515
1595
  )
1596
+ parser.add_argument(
1597
+ "--eval-name",
1598
+ default=DEFAULT_EVAL_NAME,
1599
+ metavar="NAME",
1600
+ help=(
1601
+ "Name this evaluation: results are written as NAME.json in each "
1602
+ "prediction dataset's .eval/ folder, so differently named runs "
1603
+ "sit side by side (default: %(default)s). A dataset's "
1604
+ "output_file still takes precedence."
1605
+ ),
1606
+ )
1516
1607
 
1517
1608
  args = parser.parse_args()
1518
1609
  try:
1519
1610
  validate_sky_depth(args.sky_depth)
1520
1611
  except ValueError as e:
1521
1612
  parser.error(f"--sky-depth: {e}")
1613
+ try:
1614
+ args.eval_name = validate_eval_name(args.eval_name)
1615
+ except ValueError as e:
1616
+ parser.error(f"--eval-name: {e}")
1522
1617
 
1523
1618
  selected_metric_sets = resolve_metric_sets(args.domains)
1524
1619
 
@@ -1629,6 +1724,10 @@ def main():
1629
1724
  print("-" * 60)
1630
1725
 
1631
1726
  gt = config["gt"]
1727
+ protocol_identities = reference_identities(gt)
1728
+ protocol_benchmark_range = (
1729
+ list(args.benchmark_depth_range) if args.benchmark_depth_range else None
1730
+ )
1632
1731
  gt_depth_path = gt.get("depth", {}).get("path")
1633
1732
  gt_sparse_depth_path = gt.get("sparse_depth", {}).get("path")
1634
1733
  gt_rgb_path = gt.get("rgb", {}).get("path")
@@ -1825,6 +1924,23 @@ def main():
1825
1924
  "canonical_space": space_info.get("canonical_space", "metric"),
1826
1925
  },
1827
1926
  ),
1927
+ "protocol": build_protocol(
1928
+ protocol_identities,
1929
+ gt="depth",
1930
+ inputs=("calibration", "segmentation"),
1931
+ population={
1932
+ "samples": depth_dataset_info.get("num_pairs"),
1933
+ "gt_sky_masking": bool(args.mask_sky),
1934
+ "pred_sky_masking": bool(args.mask_sky_prediction),
1935
+ "evaluated_resolution": evaluated_resolution(depth_spatial),
1936
+ "benchmark_depth_range_m": protocol_benchmark_range,
1937
+ },
1938
+ method={
1939
+ "depth_alignment": args.depth_alignment,
1940
+ "sky_depth_m": args.sky_depth,
1941
+ "spatial_alignment": depth_spatial.get("method", "none"),
1942
+ },
1943
+ ),
1828
1944
  "dataset_info": depth_dataset_info,
1829
1945
  "meta": _clean_metric_tree({
1830
1946
  "version": _get_version(),
@@ -2063,6 +2179,29 @@ def main():
2063
2179
  "canonical_space": space_info.get("canonical_space", "metric"),
2064
2180
  },
2065
2181
  ),
2182
+ "protocol": build_protocol(
2183
+ protocol_identities,
2184
+ gt="sparse_depth",
2185
+ inputs=(
2186
+ "intrinsics",
2187
+ "camera_extrinsics",
2188
+ "lidar_extrinsics",
2189
+ "segmentation",
2190
+ ),
2191
+ population={
2192
+ "samples": sparse_depth_dataset_info.get("num_pairs"),
2193
+ "gt_sky_masking": bool(args.mask_sky),
2194
+ "pred_sky_masking": bool(args.mask_sky_prediction),
2195
+ "evaluated_resolution": evaluated_resolution(
2196
+ sparse_depth_spatial
2197
+ ),
2198
+ "benchmark_depth_range_m": protocol_benchmark_range,
2199
+ },
2200
+ method={
2201
+ "depth_alignment": args.depth_alignment,
2202
+ "sky_depth_m": args.sky_depth,
2203
+ },
2204
+ ),
2066
2205
  "dataset_info": sparse_depth_dataset_info,
2067
2206
  "meta": _clean_metric_tree({
2068
2207
  "version": _get_version(),
@@ -2294,6 +2433,21 @@ def main():
2294
2433
  ]
2295
2434
  },
2296
2435
  ),
2436
+ "protocol": build_protocol(
2437
+ protocol_identities,
2438
+ gt="rgb",
2439
+ inputs=("depth", "calibration", "segmentation"),
2440
+ population={
2441
+ "samples": rgb_dataset_info.get("num_pairs"),
2442
+ "gt_sky_masking": bool(args.mask_sky),
2443
+ "evaluated_resolution": evaluated_resolution(rgb_spatial),
2444
+ "benchmark_depth_range_m": protocol_benchmark_range,
2445
+ },
2446
+ method={
2447
+ "fid_backend": args.rgb_fid_backend,
2448
+ "spatial_alignment": rgb_spatial.get("method", "none"),
2449
+ },
2450
+ ),
2297
2451
  "dataset_info": rgb_dataset_info,
2298
2452
  "meta": _clean_metric_tree({
2299
2453
  "version": _get_version(),
@@ -2427,6 +2581,18 @@ def main():
2427
2581
  "threshold_deg": rays_dataset_info.get("threshold_deg"),
2428
2582
  },
2429
2583
  ),
2584
+ "protocol": build_protocol(
2585
+ protocol_identities,
2586
+ gt="rays",
2587
+ inputs=("calibration",),
2588
+ population={
2589
+ "samples": rays_dataset_info.get("num_pairs"),
2590
+ "evaluated_resolution": evaluated_resolution(rays_spatial),
2591
+ },
2592
+ method={
2593
+ "spatial_alignment": rays_spatial.get("method", "none"),
2594
+ },
2595
+ ),
2430
2596
  "dataset_info": rays_dataset_info,
2431
2597
  "meta": _clean_metric_tree({
2432
2598
  "version": _get_version(),
@@ -2588,6 +2754,20 @@ def main():
2588
2754
  "threshold_deg": p3_dataset_info.get("threshold_deg"),
2589
2755
  },
2590
2756
  ),
2757
+ "protocol": build_protocol(
2758
+ protocol_identities,
2759
+ gt="depth" if p3d_gt_is_depth else "points_3d",
2760
+ inputs=("intrinsics", "calibration", "segmentation"),
2761
+ population={
2762
+ "samples": p3_dataset_info.get("num_pairs"),
2763
+ "gt_sky_masking": bool(args.mask_sky),
2764
+ "evaluated_resolution": evaluated_resolution(p3_spatial),
2765
+ },
2766
+ method={
2767
+ "points_3d_alignment": args.points_3d_alignment,
2768
+ "spatial_alignment": p3_spatial.get("method", "none"),
2769
+ },
2770
+ ),
2591
2771
  "dataset_info": p3_dataset_info,
2592
2772
  "meta": _clean_metric_tree({
2593
2773
  "version": _get_version(),
@@ -2767,6 +2947,24 @@ def main():
2767
2947
  "threshold_deg": p3s_dataset_info.get("threshold_deg"),
2768
2948
  },
2769
2949
  ),
2950
+ "protocol": build_protocol(
2951
+ protocol_identities,
2952
+ gt="sparse_depth",
2953
+ inputs=(
2954
+ "intrinsics",
2955
+ "camera_extrinsics",
2956
+ "lidar_extrinsics",
2957
+ "segmentation",
2958
+ ),
2959
+ population={
2960
+ "samples": p3s_dataset_info.get("num_pairs"),
2961
+ "gt_sky_masking": bool(args.mask_sky),
2962
+ "evaluated_resolution": evaluated_resolution(p3s_spatial),
2963
+ },
2964
+ method={
2965
+ "points_3d_alignment": p3s_alignment_mode,
2966
+ },
2967
+ ),
2770
2968
  "dataset_info": p3s_dataset_info,
2771
2969
  "meta": _clean_metric_tree({
2772
2970
  "version": _get_version(),
@@ -2845,6 +3043,7 @@ def main():
2845
3043
  depth_save,
2846
3044
  dataset_config,
2847
3045
  modality=pred_depth_key or "depth",
3046
+ eval_name=args.eval_name,
2848
3047
  )
2849
3048
  depth_label = (
2850
3049
  "Sparse depth"
@@ -2853,14 +3052,27 @@ def main():
2853
3052
  )
2854
3053
  print(f"\n {depth_label} results saved to: {depth_out}")
2855
3054
  if rgb_save:
2856
- rgb_out = save_results(rgb_save, dataset_config, modality="rgb")
3055
+ rgb_out = save_results(
3056
+ rgb_save,
3057
+ dataset_config,
3058
+ modality="rgb",
3059
+ eval_name=args.eval_name,
3060
+ )
2857
3061
  print(f"\n RGB results saved to: {rgb_out}")
2858
3062
  if rays_save:
2859
- rays_out = save_results(rays_save, dataset_config, modality="rays")
3063
+ rays_out = save_results(
3064
+ rays_save,
3065
+ dataset_config,
3066
+ modality="rays",
3067
+ eval_name=args.eval_name,
3068
+ )
2860
3069
  print(f"\n Rays results saved to: {rays_out}")
2861
3070
  if points_3d_save:
2862
3071
  points_3d_out = save_results(
2863
- points_3d_save, dataset_config, modality="points_3d"
3072
+ points_3d_save,
3073
+ dataset_config,
3074
+ modality="points_3d",
3075
+ eval_name=args.eval_name,
2864
3076
  )
2865
3077
  print(f"\n Points-3D results saved to: {points_3d_out}")
2866
3078
  if points_3d_sparse_save:
@@ -2868,6 +3080,7 @@ def main():
2868
3080
  points_3d_sparse_save,
2869
3081
  dataset_config,
2870
3082
  modality="points_3d",
3083
+ eval_name=args.eval_name,
2871
3084
  )
2872
3085
  print(
2873
3086
  f"\n Points-3D (sparse GT) results saved to: {points_3d_sparse_out}"