euler-eval 2.29.0__tar.gz → 2.30.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 (105) hide show
  1. {euler_eval-2.29.0/euler_eval.egg-info → euler_eval-2.30.0}/PKG-INFO +1 -1
  2. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/README.md +6 -0
  3. euler_eval-2.30.0/docs/feature-request-unified-evaluation-framework.md +381 -0
  4. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/evaluate.py +49 -0
  5. {euler_eval-2.29.0 → euler_eval-2.30.0/euler_eval.egg-info}/PKG-INFO +1 -1
  6. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval.egg-info/SOURCES.txt +1 -0
  7. {euler_eval-2.29.0 → euler_eval-2.30.0}/pyproject.toml +1 -1
  8. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_depth_alignment_output.py +20 -0
  9. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_evaluate_helpers.py +28 -0
  10. {euler_eval-2.29.0 → euler_eval-2.30.0}/LICENSE +0 -0
  11. {euler_eval-2.29.0 → euler_eval-2.30.0}/MANIFEST.in +0 -0
  12. {euler_eval-2.29.0 → euler_eval-2.30.0}/README.md +0 -0
  13. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/alignment.md +0 -0
  14. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/cli.md +0 -0
  15. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/configuration.md +0 -0
  16. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/distributions.md +0 -0
  17. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/metrics.md +0 -0
  18. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/output.md +0 -0
  19. {euler_eval-2.29.0 → euler_eval-2.30.0}/docs/validation.md +0 -0
  20. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/THIRD_PARTY_NOTICES.md +0 -0
  21. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/__init__.py +0 -0
  22. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/calibration.py +0 -0
  23. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/cli.py +0 -0
  24. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/config_paths.py +0 -0
  25. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/data.py +0 -0
  26. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/distribution_contract.py +0 -0
  27. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/distributions.py +0 -0
  28. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metric_sets.py +0 -0
  29. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/__init__.py +0 -0
  30. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/_natural_scene_models.py +0 -0
  31. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/absrel.py +0 -0
  32. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/daniel_error.py +0 -0
  33. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  34. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  35. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/depth_standard.py +0 -0
  36. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/fade.py +0 -0
  37. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/fid_kid.py +0 -0
  38. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  39. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  40. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  41. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/lpips_metric.py +0 -0
  42. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/niqe.py +0 -0
  43. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/normal_consistency.py +0 -0
  44. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/points3d_cloud.py +0 -0
  45. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/points3d_decomposition.py +0 -0
  46. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/points3d_distance.py +0 -0
  47. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/points3d_geometry.py +0 -0
  48. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/psnr.py +0 -0
  49. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  50. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  51. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  52. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/rho_a.py +0 -0
  53. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/rmse.py +0 -0
  54. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  55. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/ssim.py +0 -0
  56. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/tail_errors.py +0 -0
  57. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/metrics/utils.py +0 -0
  58. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/sanity_checker.py +0 -0
  59. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/schemas/distribution-v1.schema.json +0 -0
  60. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  61. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval/validation.py +0 -0
  62. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  63. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval.egg-info/entry_points.txt +0 -0
  64. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval.egg-info/requires.txt +0 -0
  65. {euler_eval-2.29.0 → euler_eval-2.30.0}/euler_eval.egg-info/top_level.txt +0 -0
  66. {euler_eval-2.29.0 → euler_eval-2.30.0}/init_cache.py +0 -0
  67. {euler_eval-2.29.0 → euler_eval-2.30.0}/scripts/generate_distribution_fixtures.py +0 -0
  68. {euler_eval-2.29.0 → euler_eval-2.30.0}/setup.cfg +0 -0
  69. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/__init__.py +0 -0
  70. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/conftest.py +0 -0
  71. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/distribution_contract_utils.py +0 -0
  72. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/README.md +0 -0
  73. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/counts-depth.eval.json +0 -0
  74. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/counts-points3d.eval.json +0 -0
  75. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/counts-sparsedepth.eval.json +0 -0
  76. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/future-sum-by-depth.eval.json +0 -0
  77. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/histogram-only.eval.json +0 -0
  78. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/per-file-only.eval.json +0 -0
  79. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/fixtures/distributions/scalar-only.eval.json +0 -0
  80. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/mock_files/calibration_output.json +0 -0
  81. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/mock_files/depth_output.json +0 -0
  82. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/mock_files/rgb_output.json +0 -0
  83. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/mock_files/segmentation_output.json +0 -0
  84. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_alignment.py +0 -0
  85. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_cli_device.py +0 -0
  86. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_cli_sparse_outputs.py +0 -0
  87. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_config.py +0 -0
  88. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_data.py +0 -0
  89. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_data_builders.py +0 -0
  90. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_depth_standard.py +0 -0
  91. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_distribution_contract.py +0 -0
  92. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_distributions.py +0 -0
  93. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_domain_metrics.py +0 -0
  94. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_init_cache.py +0 -0
  95. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_integration.py +0 -0
  96. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_meta_output.py +0 -0
  97. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_metrics_utils.py +0 -0
  98. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_normal_consistency.py +0 -0
  99. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_points_3d.py +0 -0
  100. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_points_3d_sparse.py +0 -0
  101. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_rgb_fid_output.py +0 -0
  102. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_rho_a.py +0 -0
  103. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_save_results.py +0 -0
  104. {euler_eval-2.29.0 → euler_eval-2.30.0}/tests/test_sparse_depth.py +0 -0
  105. {euler_eval-2.29.0 → euler_eval-2.30.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.29.0
3
+ Version: 2.30.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
@@ -10,6 +10,12 @@
10
10
  | [Results & output](output.md) | Where files land, the anatomy of `eval.json`, 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
+ ## Proposed features
14
+
15
+ - [Unified evaluation framework](feature-request-unified-evaluation-framework.md):
16
+ a shared evaluation lifecycle with explicit modality-specific preparation,
17
+ metric dependencies, populations, aggregation and output compatibility.
18
+
13
19
  ## Concepts in one page
14
20
 
15
21
  **A dataset is an indexed root, not a folder of files.** Every path in the
@@ -0,0 +1,381 @@
1
+ # Feature request: a unified evaluation framework
2
+
3
+ Status: proposed for a future release. This document specifies an architectural
4
+ change; it does not introduce a new API or change evaluation behavior.
5
+
6
+ ## Recommendation
7
+
8
+ Use a **modality-agnostic evaluation lifecycle with modality-aware preparation,
9
+ observations and metric definitions**. Represent each evaluation as an explicit
10
+ plan of typed steps and dependencies. The same plan should drive computation,
11
+ aggregation, explanations and output metadata.
12
+
13
+ Depth, RGB, rays and point clouds can share this lifecycle without sharing one
14
+ preprocessing function, validity mask or mathematical reduction. A metric may
15
+ consume several modalities, a whole image, two point sets, a group of frames or
16
+ dataset-wide features. Those requirements belong in its definition.
17
+
18
+ Start with ordinary Python data classes, protocols and a small dependency
19
+ executor. Keep the current CLI and library entry points as compatibility
20
+ wrappers while moving their computation into the shared framework.
21
+
22
+ ## Problem and motivation
23
+
24
+ Today, adding a core metric requires wiring computation and storage in
25
+ [`evaluate.py`](../euler_eval/evaluate.py), descriptions and paths in
26
+ [`cli.py`](../euler_eval/cli.py), and sometimes separate per-file and dataset
27
+ serialization logic. Deferred CPU/GPU batch results also patch mutable output
28
+ dictionaries. The distribution contract is attached through a later tree walk
29
+ in [`distribution_contract.py`](../euler_eval/distribution_contract.py).
30
+
31
+ This makes several questions difficult to answer from one place:
32
+
33
+ - Which transformed prediction and GT does a metric consume?
34
+ - Which observations are included, and when is that selection applied?
35
+ - Is a dataset result a mean of image scores, a pooled observation statistic,
36
+ or a computation over the dataset as a whole?
37
+ - Do a scalar and its associated distribution describe the same population?
38
+ - When is a deferred result complete, and which accumulators receive it?
39
+
40
+ The recent depth benchmark-range fix is a concrete example: the default
41
+ distribution needed its selection explicitly connected to the benchmark
42
+ population after sky-depth processing. Shared population definitions should
43
+ make that relationship part of the plan.
44
+
45
+ As a metric author, I should be able to declare inputs, computation, supported
46
+ reductions and result semantics once. As a user, I should be able to inspect
47
+ how any emitted result was produced. As a consumer, euler-view should receive
48
+ stable output with enough provenance to distinguish incompatible evaluations.
49
+
50
+ ## Unified flow
51
+
52
+ ```mermaid
53
+ flowchart TD
54
+ C[Configuration and selected metric sets] --> P[Build and validate evaluation plan]
55
+ M[Input metadata and available capabilities] --> P
56
+ P --> S[Resolve required inputs and sample or group identities]
57
+ S --> A[Prepare typed artifacts through declared adapter steps]
58
+ A --> V[Resolve views, populations and correspondences]
59
+ V --> K[Compute declared observations, scores or features]
60
+ K --> I[Completed item contributions]
61
+ I --> R[Reducers or retained dataset inputs]
62
+ R --> F[Finalize dataset or group computations]
63
+ K --> N[Optional dependent stages]
64
+ F --> N
65
+ N --> O[Typed result collection]
66
+ I --> O
67
+ F --> O
68
+ O --> E[Compatibility output adapter]
69
+ P -.-> X[Plan explanation and execution provenance]
70
+ O -.-> X
71
+ ```
72
+
73
+ This is a lifecycle overview. An individual plan is a directed acyclic graph:
74
+ preparation may itself require correspondence construction before alignment,
75
+ and later metrics may consume artifacts or results from earlier stages. The
76
+ diagram does not mandate that every metric executes every box.
77
+
78
+ For example, RGB plus GT depth can produce depth-binned photometric scores;
79
+ depth plus calibration can produce a point map; a future reconstruction stage
80
+ can consume several frames and produce a cloud for geometric evaluation.
81
+ Each dependency must be explicit, including its coordinate system and scope.
82
+
83
+ ## Responsibilities and contracts
84
+
85
+ The initial implementation should formalize these concepts. Names below are
86
+ design suggestions, not a promised public Python API.
87
+
88
+ | Concept | Required responsibility |
89
+ |---|---|
90
+ | `EvaluationPlan` and step definitions | Requested outputs, typed dependencies, execution scope, effective policies and implementation versions. Validate unsupported combinations and cycles before expensive work. |
91
+ | Artifact specification and reference | Payload kind, GT/prediction role, source identity, units, coordinate frame, spatial layout, transformation lineage and ownership. A reference may point to memory or managed storage. |
92
+ | View and population definitions | Evaluated representation, eligibility predicates, selection order, correspondence policy and observation unit. Record distinct source and target populations where needed. |
93
+ | `MetricSpec` | Required artifacts or observations, computation, validity requirements, parameters, supported scopes/reducers and semantic descriptions. Metric sets select these definitions. |
94
+ | `ReducerSpec` | Accepted contribution type, state, update/finalize operations, merge capability, weighting, precision and retention requirements. |
95
+ | Contribution and result records | Stable request/item identity, payload, status, counts and provenance references. Results are completed values; pending work belongs to execution state. |
96
+
97
+ The shared core owns planning, dependency resolution, resource lifetime,
98
+ batch completion, aggregation and result collection. It dispatches by declared
99
+ capabilities and contracts. Modality-specific adapters and metric kernels own
100
+ geometry, image preprocessing, feature extraction and domain-specific math.
101
+
102
+ Metric definitions should supply descriptions, units and allowed reductions to
103
+ the exporter. Adding a metric using existing capabilities should not require
104
+ new modality conditionals in the executor or manual CLI output-tree assembly.
105
+
106
+ ### Inputs and execution scope
107
+
108
+ Treat the required inputs as named roles, rather than assuming every operation
109
+ takes a `(prediction, GT)` array pair. Support declarations for:
110
+
111
+ - One item: paired images, predicted image alone, ray maps, or two point sets.
112
+ - A group: a scene, sequence, temporal window or another explicitly identified
113
+ collection, including any ordering requirement.
114
+ - A dataset: feature collections, accumulated statistics or other retained
115
+ inputs needed after the item stream completes.
116
+
117
+ An execution batch is a performance choice, not a statistical grouping. A
118
+ batch size must not silently define a sample, population or dataset reducer.
119
+
120
+ Each request identifies its matching/join policy and required modalities. A
121
+ missing optional modality should affect the requests that need it; it should
122
+ not silently shrink unrelated evaluation populations. Preserve existing
123
+ matching behavior during migration, and make any change to that behavior an
124
+ explicit, separately tested semantic change.
125
+
126
+ NIQE/FADE kernels require predictions only, although today's RGB evaluation
127
+ workflow pairs GT and predictions. Express that kernel requirement now;
128
+ enabling a standalone prediction-only CLI workflow can be a separate feature.
129
+
130
+ ### Artifacts, views and populations
131
+
132
+ An artifact must carry enough information to reject invalid combinations:
133
+ metres versus unspecified relative values, incompatible coordinate frames,
134
+ unaligned image grids, or sparse samples supplied to dense-neighbourhood
135
+ metrics. Calibration assumptions and inferred input spaces must be recorded.
136
+
137
+ A population is a named selection over a declared observation domain. It need
138
+ not be a dense Boolean mask: it may be projected correspondences, valid
139
+ directions, selected images, or a directed set of nearest-neighbour queries.
140
+ Two populations with the same display name are not automatically compatible.
141
+
142
+ Separate fitting and scoring populations. For depth, the current intended
143
+ order must remain explicit:
144
+
145
+ 1. Spatial preparation, calibration handling and planar/radial conversion.
146
+ 2. Alignment fitting on its declared fitting population, where requested.
147
+ 3. GT/prediction sky-depth capping and predicted-sky filling under the existing
148
+ policy, with GT sky exclusion recorded separately.
149
+ 4. Scoring validity and benchmark membership on evaluated GT. The benchmark
150
+ `all` interval includes both endpoints.
151
+ 5. Scalar and distribution computations for the selected space/population.
152
+
153
+ An unrestricted scalar and a benchmark-filtered histogram may intentionally
154
+ coexist, as they currently do. Their internal identities must expose that
155
+ difference. Scalar and distribution requests for the *same* population must
156
+ consume the same selected observations.
157
+
158
+ Additional requirements such as positive inputs for logarithms or a valid
159
+ neighbourhood for normals belong to the relevant computation. Explicitly name
160
+ the resulting population or support policy. Likewise, RGB sky filling and
161
+ RGB pixel exclusion are different operations; the framework must preserve
162
+ that distinction and the current unfilled inputs used for NIQE/FADE.
163
+
164
+ ## Mapping current modalities into the framework
165
+
166
+ | Evaluation family | Preparation and dependencies | Computation and scope |
167
+ |---|---|---|
168
+ | Dense depth | Spatial alignment, intrinsics, radial/planar conversion, optional affine fitting, sky policy and benchmark populations | Corresponding-pixel errors and standard reducers; dense geometry; image scores; dataset feature metrics |
169
+ | Sparse depth | Point-cloud transforms, projection, visibility/collision policy, depth preparation and alignment | Errors at evaluated projected correspondences; dense-neighbourhood metrics remain unavailable |
170
+ | RGB | Image layout/range handling, spatial alignment and metric-specific sky policy | Pixel errors, local/image scores, prediction-only quality kernels and dataset FID |
171
+ | RGB with depth | RGB inputs plus explicitly registered GT depth and calibration | Photometric observations selected by GT depth; demonstrates an existing metric with multiple modalities |
172
+ | Rays | Direction preparation, spatial alignment, validity and resolved FoV policy | Angular observations, thresholds and accuracy curves; declared exact/approximate summaries |
173
+ | Dense points-3D | Point maps or unprojected depth, coordinate-frame handling and optional gauge fitting | Correspondence errors, radial/lateral decomposition, dense geometry and separate cloud comparisons |
174
+ | Sparse points-3D | Sparse cloud visibility and projection plus predicted points or the existing programmatic depth conversion | Correspondence errors and cloud queries with explicitly declared directions |
175
+
176
+ Preserve current entry-point eligibility: representing an existing library
177
+ conversion in the plan does not automatically enable an additional CLI result
178
+ family. For point-map metrics, also preserve computations intentionally made in
179
+ the native frame even when aligned results are available.
180
+
181
+ Correspondence error and cloud distance require different observations.
182
+ Nearest-neighbour cloud queries must declare their source, target, direction
183
+ and subsampling policy. Current sparse fixed-threshold diagnostics are
184
+ GT-to-prediction completeness/recall, while optional F-score AUC also needs the
185
+ reverse direction. Keep those requirements separate. Do not pool points from
186
+ unrelated frames before nearest-neighbour matching; a scene-level comparison
187
+ would require an explicit scene artifact and common coordinate frame.
188
+
189
+ The ray evaluator already uses a histogram approximation for its pooled
190
+ median. Migrating it must preserve and disclose that approximation instead of
191
+ implicitly promising an exact quantile.
192
+
193
+ ## Observations and aggregation
194
+
195
+ Share computations only when their full input, view, population and numerical
196
+ policies match. Reuse references or chunks where practical; the framework
197
+ must not require retaining all pixel errors in memory.
198
+
199
+ A scalar, histogram and curve are different outputs derived from declared
200
+ observations. A depth histogram associated with `rmse` bins absolute residuals;
201
+ the points-3D counterpart bins Euclidean correspondence errors. Neither is a
202
+ distribution of repeated estimates of dataset RMSE.
203
+
204
+ | Requested result | Contribution/state | Final computation |
205
+ |---|---|---|
206
+ | Pixel-pool depth RMSE | Squared-error sum and valid count | `sqrt(sum_sq / count)` |
207
+ | Mean per-image RMSE | Valid image RMSE sum and image count | Divide; preserve equal image weighting |
208
+ | Error-magnitude histogram | Fixed-bin counts | Elementwise sum |
209
+ | Error sum by GT-depth bin | Fixed-bin error sums | Elementwise sum |
210
+ | RMSE by GT-depth bin | Squared-error sums and counts for matching bins | Square root of each bin's mean squared error |
211
+ | Exact quantile | Retained values or another exact supported state | Quantile over the declared population |
212
+ | LPIPS dataset mean | Completed per-image scores and validity | Declared image-score reducer |
213
+ | FID | Compatible GT/prediction feature collections, or sufficient feature moments if supported by that backend | One dataset-level calculation after ingestion |
214
+ | KID | Feature collections plus declared subset selection/seed policy | Dataset-level estimator with its required retained state |
215
+
216
+ All reducers have `update` and `finalize`; only reducers declaring a compatible
217
+ merge operation may combine partial states. State may be sufficient statistics,
218
+ retained values or ordered/global inputs. Do not force all reducers into sums
219
+ or average finalized shard scores. In particular, per-shard FID values cannot
220
+ be averaged to obtain dataset FID. Backend-specific feature preprocessing,
221
+ model weights and covariance conventions are part of FID compatibility.
222
+
223
+ Define merge compatibility from the complete resolved semantics: observation,
224
+ units, view, population policy, reducer, bin edges/intervals, implementation
225
+ and estimator settings. Registry IDs and matching output paths alone are
226
+ insufficient. Check partition identity separately so overlapping inputs are
227
+ not double counted. Integer counts must remain exact; floating accumulation
228
+ and backend equivalence require declared tolerances.
229
+
230
+ For matching populations and fixed bins, dataset histogram counts must equal
231
+ the sum of item contributions. Benchmark subsets and canonical aliases must
232
+ not be accumulated again as additional observations. Exact RMSE continues to
233
+ use sufficient statistics, independently of histogram resolution.
234
+
235
+ ## Execution and later stages
236
+
237
+ Compile selected requests into explicit dependencies. Resolve metadata-based
238
+ choices early; leave data-dependent choices marked unresolved until the
239
+ required input is available, then record the decision and its policy. Reject
240
+ missing required capabilities and incompatible artifacts with useful messages.
241
+
242
+ CPU kernels, GPU batches and dataset computations should emit typed completed
243
+ contributions into the same coordinator. Completion is tracked per logical
244
+ request and item/group; each contribution reaches its reducer once. A failed
245
+ or retried batch must not duplicate counts. Exported dictionaries must not be
246
+ used as pending-result storage.
247
+
248
+ Define `ok`, `no_observations`, `skipped`, `unavailable` and `failed` outcomes
249
+ with reasons and relevant counts. Empty or failed measurements must not enter
250
+ means as zero. The plan declares whether a failure stops the run or leaves a
251
+ result unavailable, preserving existing failure behavior during migration.
252
+
253
+ Use bounded queues and explicit artifact ownership. Release intermediates
254
+ after their consumers finish; declare temporary-file/feature retention and
255
+ clean it up on completion or failure. Expensive optional metrics should only
256
+ load models and compute features when selected. Shared preparation should not
257
+ force an entire dataset or all modalities into memory.
258
+
259
+ Allow a later stage to consume an earlier artifact or typed result through a
260
+ declared dependency. Examples include frame fusion followed by cloud scoring,
261
+ temporal metrics over ordered frame groups, and summaries derived from
262
+ completed dataset results. Specify group membership, ordering, frame transforms
263
+ and missing-input behavior. These examples are extension requirements, not
264
+ requests to implement reconstruction or temporal metrics in this refactor.
265
+
266
+ Initially, a local dependency executor is sufficient. A distributed scheduler,
267
+ plugin marketplace and user-authored workflow language are outside this
268
+ feature's scope. Merge contracts prepare suitable computations for later
269
+ parallel execution without making distribution a prerequisite.
270
+
271
+ ## Transparency and output compatibility
272
+
273
+ Provide a proposed `--explain` mode and a programmatic plan description. They
274
+ should identify inputs, transforms, fitting/scoring populations, computation
275
+ dependencies, scope, reducer, approximation and requested output paths.
276
+ An explanation that does not evaluate data must mark inferred settings as
277
+ pending. Runtime provenance records resolved choices and contribution counts;
278
+ it should be possible to follow an output back to its plan definition without
279
+ dumping arrays or masks.
280
+
281
+ Use a structured internal result identity containing evaluation family, view,
282
+ population, measurement, reducer, scope/group and output kind, referencing the
283
+ resolved semantic definitions. A path is an export representation of that
284
+ identity. Canonical aliases reference existing results and perform no work.
285
+
286
+ Keep the current `eval.json` envelope, per-modality files, metric paths, aliases,
287
+ library return shapes, and distribution v1 leaves/registry during migration.
288
+ One serialization layer should translate typed results to those contracts.
289
+ Generate descriptions from metric definitions rather than rediscovering
290
+ semantics through dictionary names. Keep transport validation, including
291
+ JavaScript-safe integer counts and nonfinite-value handling, at that boundary.
292
+
293
+ Preserve existing `distributionPopulation` metadata. A later, separately
294
+ versioned provenance extension can describe per-result views and populations
295
+ for all output kinds, including scalar results. Distinguish evaluation
296
+ semantics versions from wire-schema versions: an internal refactor must not
297
+ silently change metric meaning, and a new meaning does not justify pretending
298
+ older results used the new policy.
299
+
300
+ euler-view should be able to ingest compatibility output without changes.
301
+ Future provenance can support richer explanations and comparison checks, but
302
+ this feature request contains no euler-view implementation work.
303
+
304
+ ## Delivery plan
305
+
306
+ 1. **Characterize existing behavior.** Inventory current metric definitions,
307
+ requirements, populations, reduction rules, aliases and exceptions. Capture
308
+ representative existing outputs and numerical behavior, including known
309
+ policy differences between CLI and training validation.
310
+ 2. **Introduce the contracts through depth.** Extract prepared observations,
311
+ explicit populations and standard/distribution reducers. Reuse the
312
+ sufficient-statistic foundation in
313
+ [`depth_standard.py`](../euler_eval/metrics/depth_standard.py) and the typed
314
+ sample/aggregator approach in [`validation.py`](../euler_eval/validation.py).
315
+ Move dense and sparse depth plus training validation onto the same core
316
+ where semantics match. Keep heavyweight optional metrics outside the
317
+ lightweight validation API.
318
+ 3. **Validate the design against different computations.** Migrate RGB image
319
+ and prediction-only kernels, LPIPS completion, dataset FID/KID paths where
320
+ supported, and RGB-with-depth metrics. This milestone must demonstrate
321
+ image, dataset and multiple-modality dependencies before treating the
322
+ abstraction as stable.
323
+ 4. **Migrate rays and points.** Preserve frame choices, directional cloud
324
+ comparisons, sparse eligibility and approximation policies. Share declared
325
+ preparation where compatible and retain specialized kernels.
326
+ 5. **Complete explanations and remove duplicate wiring.** Generate result
327
+ descriptions and compatibility output from definitions. Document how to
328
+ add a metric, adapter and reducer. Stabilize extension contracts only after
329
+ the current modalities exercise them.
330
+
331
+ Each milestone must leave the existing entry points usable. Numerical or
332
+ selection-policy corrections discovered during migration should be reviewed,
333
+ tested and release-noted separately from structural changes.
334
+
335
+ ## Acceptance criteria
336
+
337
+ - All current evaluation families use the same planning, contribution and
338
+ result lifecycle while preserving modality-specific preparation.
339
+ - A metric using existing artifact and reducer types can be added through its
340
+ kernel and declaration without editing the executor or CLI tree-building
341
+ logic. Its descriptions and supported output forms come from that definition.
342
+ - Plans cover a pixel metric, an image metric, a prediction-only kernel, a
343
+ dataset feature metric and a metric requiring RGB plus depth. A small test
344
+ stage can consume an upstream artifact/result without adding a scheduler
345
+ branch for that modality.
346
+ - Depth parity cases cover dense/sparse inputs, native/aligned spaces,
347
+ planar/radial conversion, benchmark boundaries, sky capping/filling,
348
+ excluded/invalid inputs and empty populations. Fitting and scoring
349
+ populations are independently inspectable.
350
+ - Scalars and distributions requesting the same population agree on selected
351
+ observations. Fixed-bin dataset counts equal summed item counts, and
352
+ aliases/subset summaries do not double count.
353
+ - Reducer tests distinguish pooled RMSE from mean image RMSE, verify compatible
354
+ partition merging, reject incompatible state merges and preserve exact or
355
+ approximate quantile policies. Dataset metrics are finalized at their
356
+ declared scope instead of averaging arbitrary batch/shard scores.
357
+ - Cloud tests preserve coordinate frames, correspondence/nearest-neighbour
358
+ distinctions, sparse directions and subsampling. RGB tests preserve filled
359
+ versus unfilled inputs and dependencies on optional depth.
360
+ - CPU and batched implementations agree within metric-specific tolerances;
361
+ integer histograms agree exactly. Completion-order tests detect lost or
362
+ duplicated contributions, including failures and empty results. Stochastic
363
+ estimator checks use declared seed/subset policies.
364
+ - Public CLI/library compatibility fixtures retain result paths, aliases,
365
+ values within the existing tolerances, metadata and distribution v1 schema
366
+ validity. Changes in eligibility, weights or semantics are explicit changes.
367
+ - Explanation output identifies effective policies and unresolved choices;
368
+ result provenance includes resolved dependencies, counts and outcome reasons.
369
+ - Streaming checks demonstrate bounded intermediate memory for streaming
370
+ reducers and cleanup of retained resources. Existing tests remain usable
371
+ without datasets, network access or a GPU; backend stubs can exercise
372
+ scheduling and feature contracts.
373
+
374
+ ## Design boundary
375
+
376
+ The framework should make the computation inspectable and extensible through
377
+ shared contracts. It should preserve specialized mathematical kernels and
378
+ make their differences visible. Its success is measured by fewer independent
379
+ places to wire a metric, demonstrable scalar/distribution population agreement,
380
+ and the ability to express the current non-pixel and dataset computations
381
+ within the same lifecycle.
@@ -547,6 +547,35 @@ def _get_sky_mask(sample: dict) -> Optional[np.ndarray]:
547
547
  return ~sky # invert: True = non-sky = valid
548
548
 
549
549
 
550
+ class _SkyMaskCoverage:
551
+ """Count the samples whose GT sky mask removed any pixel.
552
+
553
+ A wrong ``skyclass`` in the segmentation's dataset head, or segmentation
554
+ that matches no sample, yields masks without sky, and metrics then include
555
+ the sky without any error.
556
+ """
557
+
558
+ def __init__(self) -> None:
559
+ self.samples = 0
560
+ self.with_sky = 0
561
+
562
+ def add(self, sky_valid: Optional[np.ndarray]) -> None:
563
+ """Record one sample's :func:`_get_sky_mask` result."""
564
+ self.samples += 1
565
+ if sky_valid is not None and not np.all(sky_valid):
566
+ self.with_sky += 1
567
+
568
+ def warn_if_empty(self, modality: str) -> None:
569
+ if self.samples and not self.with_sky:
570
+ print(
571
+ f"Warning: --mask-sky found no sky in any of the {self.samples} "
572
+ f"{modality} samples, so no sky was masked. Check that the GT "
573
+ "segmentation matches these samples and that its dataset head "
574
+ "declares the right 'skyclass' (`euler-loading <segmentation "
575
+ "path>` reports the sky it decodes)."
576
+ )
577
+
578
+
550
579
  def _get_prediction_sky_mask(
551
580
  sample: dict, depth_pred: np.ndarray
552
581
  ) -> Optional[np.ndarray]:
@@ -1134,6 +1163,7 @@ def evaluate_depth_samples(
1134
1163
  try:
1135
1164
  print("Computing per-image depth metrics...")
1136
1165
  depth_iter = _prefetched_iter(dataset, num_workers)
1166
+ sky_coverage = _SkyMaskCoverage()
1137
1167
  for i, sample in tqdm(
1138
1168
  depth_iter,
1139
1169
  total=num_samples,
@@ -1181,6 +1211,7 @@ def evaluate_depth_samples(
1181
1211
  sky_valid = None
1182
1212
  if sky_mask_enabled:
1183
1213
  sky_valid = _get_sky_mask(sample)
1214
+ sky_coverage.add(sky_valid)
1184
1215
  if (
1185
1216
  sky_valid is not None
1186
1217
  and sky_valid.shape[:2] != depth_pred.shape[:2]
@@ -1600,6 +1631,8 @@ def evaluate_depth_samples(
1600
1631
  entry_id,
1601
1632
  )
1602
1633
 
1634
+ sky_coverage.warn_if_empty("depth")
1635
+
1603
1636
  if depth_metrics_batcher is not None:
1604
1637
  print("Computing batched GPU depth metrics (tail flush)...")
1605
1638
  depth_metrics_batcher.finalize()
@@ -1946,6 +1979,7 @@ def evaluate_sparse_depth_samples(
1946
1979
  try:
1947
1980
  print("Computing sparse pointcloud depth metrics...")
1948
1981
  depth_iter = _prefetched_iter(dataset, num_workers)
1982
+ sky_coverage = _SkyMaskCoverage()
1949
1983
  for i, sample in tqdm(
1950
1984
  depth_iter,
1951
1985
  total=num_samples,
@@ -2013,6 +2047,7 @@ def evaluate_sparse_depth_samples(
2013
2047
  sky_valid = None
2014
2048
  if sky_mask_enabled:
2015
2049
  sky_valid = _get_sky_mask(sample)
2050
+ sky_coverage.add(sky_valid)
2016
2051
  if sky_valid is not None and sky_valid.shape[:2] != depth_pred.shape[:2]:
2017
2052
  sky_valid = align_to_prediction(sky_valid, depth_pred)
2018
2053
 
@@ -2200,6 +2235,8 @@ def evaluate_sparse_depth_samples(
2200
2235
  if np.isfinite(sv):
2201
2236
  sanity_checker.validate_depth_silog(sv, entry_id)
2202
2237
 
2238
+ sky_coverage.warn_if_empty("sparse depth")
2239
+
2203
2240
  print("Aggregating sparse depth results...")
2204
2241
 
2205
2242
  emit_native = alignment_applied or normalized_predictions
@@ -2457,6 +2494,7 @@ def evaluate_rgb_samples(
2457
2494
  try:
2458
2495
  print("Computing per-image RGB metrics...")
2459
2496
  rgb_iter = _prefetched_iter(dataset, num_workers)
2497
+ sky_coverage = _SkyMaskCoverage()
2460
2498
  for i, sample in tqdm(
2461
2499
  rgb_iter,
2462
2500
  total=num_samples,
@@ -2489,6 +2527,7 @@ def evaluate_rgb_samples(
2489
2527
  sky_valid = None
2490
2528
  if sky_mask_enabled:
2491
2529
  sky_valid = _get_sky_mask(sample)
2530
+ sky_coverage.add(sky_valid)
2492
2531
  if sky_valid is not None and sky_valid.shape[:2] != img_pred.shape[:2]:
2493
2532
  sky_valid = align_to_prediction(sky_valid, img_pred)
2494
2533
 
@@ -2748,6 +2787,8 @@ def evaluate_rgb_samples(
2748
2787
  if depth_binned_entry is not None:
2749
2788
  sanity_checker.validate_depth_binned(depth_binned_entry, entry_id)
2750
2789
 
2790
+ sky_coverage.warn_if_empty("RGB")
2791
+
2751
2792
  if rgb_lpips_batcher is not None:
2752
2793
  print("Computing batched RGB LPIPS (tail flush)...")
2753
2794
  rgb_lpips_batcher.finalize()
@@ -3371,6 +3412,7 @@ def evaluate_points_3d_samples(
3371
3412
  try:
3372
3413
  print("Computing per-image points_3d metrics...")
3373
3414
  points_iter = _prefetched_iter(dataset, num_workers)
3415
+ sky_coverage = _SkyMaskCoverage()
3374
3416
  for i, sample in tqdm(
3375
3417
  points_iter,
3376
3418
  total=num_samples,
@@ -3429,6 +3471,7 @@ def evaluate_points_3d_samples(
3429
3471
  valid = finite & (gt_norm > 0)
3430
3472
  if sky_mask_enabled:
3431
3473
  sky_valid = _get_sky_mask(sample)
3474
+ sky_coverage.add(sky_valid)
3432
3475
  if sky_valid is not None:
3433
3476
  if sky_valid.shape[:2] != pts_pred.shape[:2]:
3434
3477
  sky_valid = align_to_prediction(sky_valid, pts_pred)
@@ -3613,6 +3656,8 @@ def evaluate_points_3d_samples(
3613
3656
  {"id": entry_id, "metrics": metrics_dict},
3614
3657
  )
3615
3658
 
3659
+ sky_coverage.warn_if_empty("points_3d")
3660
+
3616
3661
  print("Aggregating points_3d results...")
3617
3662
  if _agg_total > 0:
3618
3663
  cumsum = np.cumsum(_agg_hist)
@@ -4018,6 +4063,7 @@ def evaluate_points_3d_sparse_samples(
4018
4063
  try:
4019
4064
  print("Computing sparse points_3d metrics...")
4020
4065
  points_iter = _prefetched_iter(dataset, num_workers)
4066
+ sky_coverage = _SkyMaskCoverage()
4021
4067
  for i, sample in tqdm(
4022
4068
  points_iter,
4023
4069
  total=num_samples,
@@ -4096,6 +4142,7 @@ def evaluate_points_3d_sparse_samples(
4096
4142
  sky_valid = None
4097
4143
  if sky_mask_enabled:
4098
4144
  sky_valid = _get_sky_mask(sample)
4145
+ sky_coverage.add(sky_valid)
4099
4146
  if sky_valid is not None and sky_valid.shape[:2] != pred_shape:
4100
4147
  ref = depth_pred if pred_is_depth else pts_pred
4101
4148
  sky_valid = align_to_prediction(sky_valid, ref)
@@ -4340,6 +4387,8 @@ def evaluate_points_3d_sparse_samples(
4340
4387
  {"id": entry_id, "metrics": metrics_dict},
4341
4388
  )
4342
4389
 
4390
+ sky_coverage.warn_if_empty("sparse points_3d")
4391
+
4343
4392
  print("Aggregating sparse points_3d results...")
4344
4393
  if _agg_total > 0:
4345
4394
  cumsum = np.cumsum(_agg_hist)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.29.0
3
+ Version: 2.30.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
@@ -8,6 +8,7 @@ docs/alignment.md
8
8
  docs/cli.md
9
9
  docs/configuration.md
10
10
  docs/distributions.md
11
+ docs/feature-request-unified-evaluation-framework.md
11
12
  docs/metrics.md
12
13
  docs/output.md
13
14
  docs/validation.md
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-eval"
7
- version = "2.29.0"
7
+ version = "2.30.0"
8
8
  description = "Evaluation toolkit for depth, RGB, ray and 3D point-map predictions"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -534,6 +534,26 @@ def test_prediction_sky_fill_still_respects_gt_sky_exclusion(monkeypatch):
534
534
  assert result["depth_metric"]["standard"]["pixel_pool"]["mae"] == 0.0
535
535
 
536
536
 
537
+ @pytest.mark.parametrize("has_sky, warned", [(False, True), (True, False)])
538
+ def test_sky_masking_that_masks_nothing_is_reported(
539
+ monkeypatch, capsys, has_sky, warned
540
+ ):
541
+ # A wrong 'skyclass' in the segmentation head decodes to masks without sky.
542
+ _patch_depth_metrics(monkeypatch)
543
+ sample = {
544
+ "id": "frame",
545
+ "gt": np.array([[10, 20], [30, 40]], dtype=np.float32),
546
+ "pred": np.array([[10, 20], [30, 40]], dtype=np.float32),
547
+ "segmentation": np.array([[False, False], [False, has_sky]]),
548
+ }
549
+ eval_mod.evaluate_depth_samples(
550
+ _DummyDepthDataset([sample]), is_radial=True, device="cpu",
551
+ num_workers=0, alignment_mode="none", sky_mask_enabled=True,
552
+ )
553
+ out = capsys.readouterr().out
554
+ assert ("--mask-sky found no sky in any of the 1 depth samples" in out) is warned
555
+
556
+
537
557
  @pytest.mark.parametrize("sky_depth", [None, 50.0])
538
558
  def test_cli_uses_prediction_sky_mask_and_records_provenance(
539
559
  monkeypatch, indexed_sky_prediction, tmp_path, sky_depth
@@ -9,6 +9,7 @@ from euler_eval.evaluate import (
9
9
  _get_intrinsics_K,
10
10
  _get_prediction_sky_mask,
11
11
  _get_sky_mask,
12
+ _SkyMaskCoverage,
12
13
  )
13
14
 
14
15
  # ---------------------------------------------------------------------------
@@ -76,6 +77,33 @@ class TestGetSkyMask:
76
77
  np.testing.assert_array_equal(result, ~sky_a)
77
78
 
78
79
 
80
+ # ---------------------------------------------------------------------------
81
+ # _SkyMaskCoverage
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ class TestSkyMaskCoverage:
86
+ def test_warns_when_no_sample_has_sky(self, capsys):
87
+ coverage = _SkyMaskCoverage()
88
+ coverage.add(np.ones((2, 2), dtype=bool)) # all valid: no sky
89
+ coverage.add(None) # no segmentation matched the sample
90
+ coverage.warn_if_empty("depth")
91
+ out = capsys.readouterr().out
92
+ assert "--mask-sky found no sky in any of the 2 depth samples" in out
93
+ assert "'skyclass'" in out
94
+
95
+ def test_silent_when_any_sample_has_sky(self, capsys):
96
+ coverage = _SkyMaskCoverage()
97
+ coverage.add(np.ones((2, 2), dtype=bool))
98
+ coverage.add(np.array([[False, True], [True, True]]))
99
+ coverage.warn_if_empty("depth")
100
+ assert capsys.readouterr().out == ""
101
+
102
+ def test_silent_when_sky_masking_never_ran(self, capsys):
103
+ _SkyMaskCoverage().warn_if_empty("depth")
104
+ assert capsys.readouterr().out == ""
105
+
106
+
79
107
  # ---------------------------------------------------------------------------
80
108
  # _get_intrinsics_K
81
109
  # ---------------------------------------------------------------------------
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes