euler-eval 2.8.0__tar.gz → 2.10.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.
- {euler_eval-2.8.0 → euler_eval-2.10.0}/PKG-INFO +32 -6
- {euler_eval-2.8.0 → euler_eval-2.10.0}/README.md +31 -5
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/cli.py +153 -36
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/data.py +6 -5
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/evaluate.py +1 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/utils.py +1 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval.egg-info/PKG-INFO +32 -6
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval.egg-info/SOURCES.txt +1 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/pyproject.toml +1 -1
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_config.py +25 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_data_builders.py +23 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_meta_output.py +101 -0
- euler_eval-2.10.0/tests/test_metrics_utils.py +57 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_save_results.py +15 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_sparse_depth.py +23 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/__init__.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/__init__.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/absrel.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/daniel_error.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/depth_binned_error.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/depth_standard.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/fid_kid.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/high_freq_energy.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/lpips_metric.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/normal_consistency.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/psnr.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/rgb_lpips.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/rho_a.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/rmse.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/ssim.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/metrics/tail_errors.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/sanity_checker.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval/utils/hierarchy_parser.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval.egg-info/dependency_links.txt +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval.egg-info/entry_points.txt +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval.egg-info/requires.txt +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/euler_eval.egg-info/top_level.txt +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/init_cache.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/setup.cfg +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_alignment.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_cli_device.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_data.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_depth_alignment_output.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_depth_standard.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_evaluate_helpers.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_init_cache.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_integration.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_rgb_fid_output.py +0 -0
- {euler_eval-2.8.0 → euler_eval-2.10.0}/tests/test_rho_a.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-eval
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
4
4
|
Summary: Depth map evaluation toolkit with comprehensive metrics
|
|
5
5
|
Author: Depth Eval Contributors
|
|
6
6
|
License: MIT
|
|
@@ -45,7 +45,7 @@ A comprehensive evaluation toolkit for comparing predicted depth maps, RGB image
|
|
|
45
45
|
- **Depth metrics**: PSNR, SSIM, LPIPS, FID, KID, AbsRel, RMSE, Scale-Invariant Log Error, Normal Consistency, Depth Edge F1
|
|
46
46
|
- **RGB metrics**: PSNR, SSIM, LPIPS, FID, SCE (Structural Chromatic Error), Edge F1, Tail Errors (p95/p99), High-Frequency Energy Ratio, Depth-Binned Photometric Error
|
|
47
47
|
- **Rays metrics**: ρ_A (AUC of angular accuracy curve), Angular Error statistics and threshold percentages
|
|
48
|
-
- **Benchmark binning**: Optional depth-range benchmark that subdivides metrics into
|
|
48
|
+
- **Benchmark binning**: Optional depth-range benchmark that subdivides metrics into square-root-scaled near/mid/far bins
|
|
49
49
|
- **Sanity checking**: Automatic validation of metric results against configurable thresholds, with detailed warning reports
|
|
50
50
|
- **Sky masking**: Optional exclusion of sky regions from metrics using GT segmentation
|
|
51
51
|
- **Flexible dataset loading**: Automatic loader resolution via euler_loading and ds-crawler index metadata
|
|
@@ -141,7 +141,7 @@ This pre-downloads:
|
|
|
141
141
|
| `--metrics-config` | `str` | auto-detect | Path to `metrics_config.json` for sanity checking |
|
|
142
142
|
| `--depth-alignment` | `{none,auto_affine,affine}` | `auto_affine` | Depth calibration mode; outputs are emitted in semantic `native`/`metric` spaces and `depth` aliases the canonical branch |
|
|
143
143
|
| `--rgb-fid-backend` | `{builtin,clean-fid}` | `builtin` | RGB FID backend; `clean-fid` requires optional dependency |
|
|
144
|
-
| `--benchmark-depth-range` | `float float` | none | Depth range `[MIN, MAX]` in meters for benchmark evaluation; computes depth and RGB metrics for pixels within this range, subdivided into
|
|
144
|
+
| `--benchmark-depth-range` | `float float` | none | Depth range `[MIN, MAX]` in meters for benchmark evaluation; computes depth and RGB metrics for pixels within this range, subdivided into square-root-scaled near/mid/far bins (additive to regular metrics) |
|
|
145
145
|
|
|
146
146
|
### Examples
|
|
147
147
|
|
|
@@ -168,7 +168,7 @@ depth-eval config.json --depth-alignment affine
|
|
|
168
168
|
depth-eval config.json --rgb-fid-backend clean-fid
|
|
169
169
|
|
|
170
170
|
# Benchmark depth and RGB metrics within a depth range (near/mid/far bins)
|
|
171
|
-
depth-eval config.json --benchmark-depth-range 0.
|
|
171
|
+
depth-eval config.json --benchmark-depth-range 0.01 80.0
|
|
172
172
|
|
|
173
173
|
# Evaluate dense depth predictions against sparse pointcloud GT
|
|
174
174
|
depth-eval example_sparse_depth_config.json --skip-rgb --skip-rays
|
|
@@ -177,6 +177,30 @@ depth-eval example_sparse_depth_config.json --skip-rgb --skip-rays
|
|
|
177
177
|
depth-eval config.json --skip-rays
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
### Benchmark Depth Bins
|
|
181
|
+
|
|
182
|
+
`--benchmark-depth-range MIN MAX` first filters valid GT depth pixels to
|
|
183
|
+
`MIN <= depth <= MAX`. The `near`, `mid`, and `far` bins are then computed as
|
|
184
|
+
three equal-width intervals in square-root depth space:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
sqrt_min = sqrt(MIN)
|
|
188
|
+
sqrt_max = sqrt(MAX)
|
|
189
|
+
step = (sqrt_max - sqrt_min) / 3
|
|
190
|
+
near_max = (sqrt_min + step)^2
|
|
191
|
+
mid_max = (sqrt_min + 2 * step)^2
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The interval bounds are `near=[MIN, near_max)`, `mid=[near_max, mid_max)`, and
|
|
195
|
+
`far=[mid_max, MAX]`. For `--benchmark-depth-range 0.01 80.0`, this gives:
|
|
196
|
+
|
|
197
|
+
| Bin | Depth interval |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `all` | `[0.01, 80.0]` |
|
|
200
|
+
| `near` | `[0.01, 9.290856529)` |
|
|
201
|
+
| `mid` | `[9.290856529, 35.954189863)` |
|
|
202
|
+
| `far` | `[35.954189863, 80.0]` |
|
|
203
|
+
|
|
180
204
|
## Configuration
|
|
181
205
|
|
|
182
206
|
### `config.json`
|
|
@@ -215,7 +239,7 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
|
|
|
215
239
|
}
|
|
216
240
|
```
|
|
217
241
|
|
|
218
|
-
Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`).
|
|
242
|
+
Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`). euler-loading inline selectors are also accepted in `path`, such as `/data/muses.zip:test` or `/data/muses.zip:test#scope=rgb`.
|
|
219
243
|
|
|
220
244
|
For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction still uses `datasets[].depth` because the model output is a dense depth map. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels.
|
|
221
245
|
|
|
@@ -269,7 +293,7 @@ Dataset metadata (e.g. `radial_depth`, `rgb_range`, sparse point columns, and co
|
|
|
269
293
|
|
|
270
294
|
### Dataset Metadata
|
|
271
295
|
|
|
272
|
-
Each dataset directory must contain ds-crawler metadata artifacts, typically generated under `.ds_crawler/`:
|
|
296
|
+
Each dataset directory or archive must contain ds-crawler metadata artifacts, typically generated under `.ds_crawler/`:
|
|
273
297
|
|
|
274
298
|
```text
|
|
275
299
|
dataset-root/
|
|
@@ -279,6 +303,8 @@ dataset-root/
|
|
|
279
303
|
index.json
|
|
280
304
|
```
|
|
281
305
|
|
|
306
|
+
When one physical root or archive contains several logical modalities, the artifacts may instead be scoped under `.ds_crawler/<modality>/` with a `.ds_crawler/scopes.json` manifest. `euler-eval` passes modality scopes to euler-loading so paths such as `/data/muses.zip:test` can load `.ds_crawler/rgb/index.json`, `.ds_crawler/depth/index.json`, and related scoped artifacts from the same archive.
|
|
307
|
+
|
|
282
308
|
GT and prediction datasets are matched by hierarchy path and file ID through `MultiModalDataset`.
|
|
283
309
|
|
|
284
310
|
### `metrics_config.json`
|
|
@@ -7,7 +7,7 @@ A comprehensive evaluation toolkit for comparing predicted depth maps, RGB image
|
|
|
7
7
|
- **Depth metrics**: PSNR, SSIM, LPIPS, FID, KID, AbsRel, RMSE, Scale-Invariant Log Error, Normal Consistency, Depth Edge F1
|
|
8
8
|
- **RGB metrics**: PSNR, SSIM, LPIPS, FID, SCE (Structural Chromatic Error), Edge F1, Tail Errors (p95/p99), High-Frequency Energy Ratio, Depth-Binned Photometric Error
|
|
9
9
|
- **Rays metrics**: ρ_A (AUC of angular accuracy curve), Angular Error statistics and threshold percentages
|
|
10
|
-
- **Benchmark binning**: Optional depth-range benchmark that subdivides metrics into
|
|
10
|
+
- **Benchmark binning**: Optional depth-range benchmark that subdivides metrics into square-root-scaled near/mid/far bins
|
|
11
11
|
- **Sanity checking**: Automatic validation of metric results against configurable thresholds, with detailed warning reports
|
|
12
12
|
- **Sky masking**: Optional exclusion of sky regions from metrics using GT segmentation
|
|
13
13
|
- **Flexible dataset loading**: Automatic loader resolution via euler_loading and ds-crawler index metadata
|
|
@@ -103,7 +103,7 @@ This pre-downloads:
|
|
|
103
103
|
| `--metrics-config` | `str` | auto-detect | Path to `metrics_config.json` for sanity checking |
|
|
104
104
|
| `--depth-alignment` | `{none,auto_affine,affine}` | `auto_affine` | Depth calibration mode; outputs are emitted in semantic `native`/`metric` spaces and `depth` aliases the canonical branch |
|
|
105
105
|
| `--rgb-fid-backend` | `{builtin,clean-fid}` | `builtin` | RGB FID backend; `clean-fid` requires optional dependency |
|
|
106
|
-
| `--benchmark-depth-range` | `float float` | none | Depth range `[MIN, MAX]` in meters for benchmark evaluation; computes depth and RGB metrics for pixels within this range, subdivided into
|
|
106
|
+
| `--benchmark-depth-range` | `float float` | none | Depth range `[MIN, MAX]` in meters for benchmark evaluation; computes depth and RGB metrics for pixels within this range, subdivided into square-root-scaled near/mid/far bins (additive to regular metrics) |
|
|
107
107
|
|
|
108
108
|
### Examples
|
|
109
109
|
|
|
@@ -130,7 +130,7 @@ depth-eval config.json --depth-alignment affine
|
|
|
130
130
|
depth-eval config.json --rgb-fid-backend clean-fid
|
|
131
131
|
|
|
132
132
|
# Benchmark depth and RGB metrics within a depth range (near/mid/far bins)
|
|
133
|
-
depth-eval config.json --benchmark-depth-range 0.
|
|
133
|
+
depth-eval config.json --benchmark-depth-range 0.01 80.0
|
|
134
134
|
|
|
135
135
|
# Evaluate dense depth predictions against sparse pointcloud GT
|
|
136
136
|
depth-eval example_sparse_depth_config.json --skip-rgb --skip-rays
|
|
@@ -139,6 +139,30 @@ depth-eval example_sparse_depth_config.json --skip-rgb --skip-rays
|
|
|
139
139
|
depth-eval config.json --skip-rays
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
+
### Benchmark Depth Bins
|
|
143
|
+
|
|
144
|
+
`--benchmark-depth-range MIN MAX` first filters valid GT depth pixels to
|
|
145
|
+
`MIN <= depth <= MAX`. The `near`, `mid`, and `far` bins are then computed as
|
|
146
|
+
three equal-width intervals in square-root depth space:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
sqrt_min = sqrt(MIN)
|
|
150
|
+
sqrt_max = sqrt(MAX)
|
|
151
|
+
step = (sqrt_max - sqrt_min) / 3
|
|
152
|
+
near_max = (sqrt_min + step)^2
|
|
153
|
+
mid_max = (sqrt_min + 2 * step)^2
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The interval bounds are `near=[MIN, near_max)`, `mid=[near_max, mid_max)`, and
|
|
157
|
+
`far=[mid_max, MAX]`. For `--benchmark-depth-range 0.01 80.0`, this gives:
|
|
158
|
+
|
|
159
|
+
| Bin | Depth interval |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `all` | `[0.01, 80.0]` |
|
|
162
|
+
| `near` | `[0.01, 9.290856529)` |
|
|
163
|
+
| `mid` | `[9.290856529, 35.954189863)` |
|
|
164
|
+
| `far` | `[35.954189863, 80.0]` |
|
|
165
|
+
|
|
142
166
|
## Configuration
|
|
143
167
|
|
|
144
168
|
### `config.json`
|
|
@@ -177,7 +201,7 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
|
|
|
177
201
|
}
|
|
178
202
|
```
|
|
179
203
|
|
|
180
|
-
Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`).
|
|
204
|
+
Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`). euler-loading inline selectors are also accepted in `path`, such as `/data/muses.zip:test` or `/data/muses.zip:test#scope=rgb`.
|
|
181
205
|
|
|
182
206
|
For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction still uses `datasets[].depth` because the model output is a dense depth map. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels.
|
|
183
207
|
|
|
@@ -231,7 +255,7 @@ Dataset metadata (e.g. `radial_depth`, `rgb_range`, sparse point columns, and co
|
|
|
231
255
|
|
|
232
256
|
### Dataset Metadata
|
|
233
257
|
|
|
234
|
-
Each dataset directory must contain ds-crawler metadata artifacts, typically generated under `.ds_crawler/`:
|
|
258
|
+
Each dataset directory or archive must contain ds-crawler metadata artifacts, typically generated under `.ds_crawler/`:
|
|
235
259
|
|
|
236
260
|
```text
|
|
237
261
|
dataset-root/
|
|
@@ -241,6 +265,8 @@ dataset-root/
|
|
|
241
265
|
index.json
|
|
242
266
|
```
|
|
243
267
|
|
|
268
|
+
When one physical root or archive contains several logical modalities, the artifacts may instead be scoped under `.ds_crawler/<modality>/` with a `.ds_crawler/scopes.json` manifest. `euler-eval` passes modality scopes to euler-loading so paths such as `/data/muses.zip:test` can load `.ds_crawler/rgb/index.json`, `.ds_crawler/depth/index.json`, and related scoped artifacts from the same archive.
|
|
269
|
+
|
|
244
270
|
GT and prediction datasets are matched by hierarchy path and file ID through `MultiModalDataset`.
|
|
245
271
|
|
|
246
272
|
### `metrics_config.json`
|
|
@@ -13,6 +13,7 @@ import zipfile
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
import torch
|
|
16
|
+
from euler_loading import Modality
|
|
16
17
|
|
|
17
18
|
from .data import (
|
|
18
19
|
build_depth_eval_dataset,
|
|
@@ -40,6 +41,22 @@ except ImportError:
|
|
|
40
41
|
from euler_metric_naming import AxisDeclaration, MetricDescription, MetricNamespace
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def _normalize_modality_path(
|
|
45
|
+
path: str | Path,
|
|
46
|
+
*,
|
|
47
|
+
modality_key: str,
|
|
48
|
+
split: str | None = None,
|
|
49
|
+
) -> Path:
|
|
50
|
+
"""Return the filesystem/archive root for an euler-loading modality path."""
|
|
51
|
+
modality = Modality(
|
|
52
|
+
str(path),
|
|
53
|
+
modality_type=modality_key,
|
|
54
|
+
metadata_scope=modality_key,
|
|
55
|
+
split=split,
|
|
56
|
+
)
|
|
57
|
+
return Path(modality.path)
|
|
58
|
+
|
|
59
|
+
|
|
43
60
|
# ── Eval namespace ──────────────────────────────────────────────────────────
|
|
44
61
|
# MetricNamespace subclass for eval context with custom axis declarations,
|
|
45
62
|
# following the Normed Metric Namespacing convention (§3.6, §4, §5.2).
|
|
@@ -292,6 +309,10 @@ _SPARSE_DEPTH_EVAL_DESCRIPTIONS = {
|
|
|
292
309
|
"silog.p90",
|
|
293
310
|
}
|
|
294
311
|
}
|
|
312
|
+
_SPARSE_DEPTH_EVAL_DESCRIPTIONS["valid_pixel_count"] = MetricDescription(
|
|
313
|
+
unit="pixels",
|
|
314
|
+
display_name="Valid Pixel Count",
|
|
315
|
+
)
|
|
295
316
|
|
|
296
317
|
_RGB_EVAL_DESCRIPTIONS = {
|
|
297
318
|
"psnr": MetricDescription(is_higher_better=True, unit="dB", display_name="PSNR"),
|
|
@@ -389,7 +410,11 @@ def _sparse_depth_metric_set_envelope(
|
|
|
389
410
|
return envelope
|
|
390
411
|
|
|
391
412
|
|
|
392
|
-
def _clean_metric_tree(
|
|
413
|
+
def _clean_metric_tree(
|
|
414
|
+
tree: dict,
|
|
415
|
+
*,
|
|
416
|
+
preserve_empty_dict_keys: set[str] | None = None,
|
|
417
|
+
) -> dict:
|
|
393
418
|
"""Recursively sanitize a metric dict for JSON schema compliance.
|
|
394
419
|
|
|
395
420
|
- Removes entries where the value is ``None``
|
|
@@ -397,6 +422,7 @@ def _clean_metric_tree(tree: dict) -> dict:
|
|
|
397
422
|
- Recursively processes nested dicts and list items
|
|
398
423
|
- Prunes empty dicts after cleaning
|
|
399
424
|
"""
|
|
425
|
+
preserve_empty_dict_keys = preserve_empty_dict_keys or set()
|
|
400
426
|
cleaned = {}
|
|
401
427
|
for key, value in tree.items():
|
|
402
428
|
if value is None:
|
|
@@ -404,12 +430,20 @@ def _clean_metric_tree(tree: dict) -> dict:
|
|
|
404
430
|
if isinstance(value, float) and not math.isfinite(value):
|
|
405
431
|
continue
|
|
406
432
|
if isinstance(value, dict):
|
|
407
|
-
sub = _clean_metric_tree(
|
|
408
|
-
|
|
433
|
+
sub = _clean_metric_tree(
|
|
434
|
+
value,
|
|
435
|
+
preserve_empty_dict_keys=preserve_empty_dict_keys,
|
|
436
|
+
)
|
|
437
|
+
if sub or key in preserve_empty_dict_keys:
|
|
409
438
|
cleaned[key] = sub
|
|
410
439
|
elif isinstance(value, list):
|
|
411
440
|
cleaned[key] = [
|
|
412
|
-
_clean_metric_tree(
|
|
441
|
+
_clean_metric_tree(
|
|
442
|
+
item,
|
|
443
|
+
preserve_empty_dict_keys=preserve_empty_dict_keys,
|
|
444
|
+
)
|
|
445
|
+
if isinstance(item, dict)
|
|
446
|
+
else item
|
|
413
447
|
for item in value
|
|
414
448
|
]
|
|
415
449
|
else:
|
|
@@ -417,6 +451,71 @@ def _clean_metric_tree(tree: dict) -> dict:
|
|
|
417
451
|
return cleaned
|
|
418
452
|
|
|
419
453
|
|
|
454
|
+
def _empty_per_file_metric_ids(
|
|
455
|
+
node: dict,
|
|
456
|
+
hierarchy: tuple[str, ...] = (),
|
|
457
|
+
) -> list[str]:
|
|
458
|
+
"""Return file identifiers whose cleaned per-file metrics are empty."""
|
|
459
|
+
ids = []
|
|
460
|
+
for file_entry in node.get("files", []):
|
|
461
|
+
if not isinstance(file_entry, dict):
|
|
462
|
+
continue
|
|
463
|
+
if file_entry.get("metrics") == {}:
|
|
464
|
+
file_id = str(file_entry.get("id", "<missing-id>"))
|
|
465
|
+
ids.append("/".join((*hierarchy, file_id)))
|
|
466
|
+
|
|
467
|
+
children = node.get("children", {})
|
|
468
|
+
if isinstance(children, dict):
|
|
469
|
+
for child_name, child_node in children.items():
|
|
470
|
+
if isinstance(child_node, dict):
|
|
471
|
+
ids.extend(
|
|
472
|
+
_empty_per_file_metric_ids(
|
|
473
|
+
child_node,
|
|
474
|
+
(*hierarchy, str(child_name)),
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
return ids
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _clean_per_file_metrics(tree: dict, *, label: str = "per_file_metrics") -> dict:
|
|
481
|
+
"""Clean a per-file metric tree while preserving file-entry shape.
|
|
482
|
+
|
|
483
|
+
Some per-file metric entries can legitimately clean down to no scalar
|
|
484
|
+
metrics, for example when every metric for a file is ``None``/NaN after a
|
|
485
|
+
failed computation or an invalid sample. The generic cleaner prunes empty
|
|
486
|
+
dicts, but per-file entries are schema-shaped as ``{"id", "metrics"}``;
|
|
487
|
+
dropping the empty ``metrics`` object leaves an id-only file entry.
|
|
488
|
+
"""
|
|
489
|
+
cleaned = _clean_metric_tree(tree, preserve_empty_dict_keys={"metrics"})
|
|
490
|
+
empty_ids = _empty_per_file_metric_ids(cleaned)
|
|
491
|
+
if empty_ids:
|
|
492
|
+
sample = ", ".join(empty_ids[:10])
|
|
493
|
+
suffix = "" if len(empty_ids) <= 10 else f", ... +{len(empty_ids) - 10} more"
|
|
494
|
+
print(
|
|
495
|
+
f"Warning: {label} has {len(empty_ids)} file entries with no finite "
|
|
496
|
+
f"metrics after cleaning: {sample}{suffix}"
|
|
497
|
+
)
|
|
498
|
+
return cleaned
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _wrap_depth_space_pfm_metrics(
|
|
502
|
+
metrics: dict,
|
|
503
|
+
*,
|
|
504
|
+
metric_root: str,
|
|
505
|
+
canonical_key: str,
|
|
506
|
+
canonical_space: str | None = "metric",
|
|
507
|
+
) -> dict:
|
|
508
|
+
"""Wrap per-file depth-like metrics under the serialized namespace path."""
|
|
509
|
+
space_metrics = {
|
|
510
|
+
space: metrics[f"{canonical_key}_{space}"]
|
|
511
|
+
for space in ("native", "metric")
|
|
512
|
+
if f"{canonical_key}_{space}" in metrics
|
|
513
|
+
}
|
|
514
|
+
if not space_metrics and canonical_key in metrics:
|
|
515
|
+
space_metrics[canonical_space or "metric"] = metrics[canonical_key]
|
|
516
|
+
return {metric_root: {"eval": space_metrics}}
|
|
517
|
+
|
|
518
|
+
|
|
420
519
|
def _wrap_pfm_metrics(pfm: dict, wrapper_fn) -> dict:
|
|
421
520
|
"""Walk a ``perFileHierarchyNode`` tree, applying *wrapper_fn* to each
|
|
422
521
|
file entry's ``metrics`` dict. This restructures per-file metric keys
|
|
@@ -517,7 +616,11 @@ def validate_gt_config(gt: dict) -> None:
|
|
|
517
616
|
"camera_extrinsics",
|
|
518
617
|
):
|
|
519
618
|
if modality in gt and "path" in gt[modality]:
|
|
520
|
-
p =
|
|
619
|
+
p = _normalize_modality_path(
|
|
620
|
+
gt[modality]["path"],
|
|
621
|
+
modality_key=modality,
|
|
622
|
+
split=gt[modality].get("split"),
|
|
623
|
+
)
|
|
521
624
|
if not p.exists():
|
|
522
625
|
raise ValueError(f"gt.{modality}.path does not exist: {p}")
|
|
523
626
|
|
|
@@ -542,7 +645,11 @@ def validate_dataset_entry(entry: dict, index: int) -> None:
|
|
|
542
645
|
|
|
543
646
|
for modality in ("rgb", "depth", "rays"):
|
|
544
647
|
if modality in entry and "path" in entry[modality]:
|
|
545
|
-
p =
|
|
648
|
+
p = _normalize_modality_path(
|
|
649
|
+
entry[modality]["path"],
|
|
650
|
+
modality_key=modality,
|
|
651
|
+
split=entry[modality].get("split"),
|
|
652
|
+
)
|
|
546
653
|
if not p.exists():
|
|
547
654
|
raise ValueError(f"{label}.{modality}.path does not exist: {p}")
|
|
548
655
|
|
|
@@ -667,12 +774,26 @@ def save_results(
|
|
|
667
774
|
and modality in dataset_config
|
|
668
775
|
and "path" in dataset_config[modality]
|
|
669
776
|
):
|
|
670
|
-
output_file =
|
|
777
|
+
output_file = (
|
|
778
|
+
_normalize_modality_path(
|
|
779
|
+
dataset_config[modality]["path"],
|
|
780
|
+
modality_key=modality,
|
|
781
|
+
split=dataset_config[modality].get("split"),
|
|
782
|
+
)
|
|
783
|
+
/ "eval.json"
|
|
784
|
+
)
|
|
671
785
|
else:
|
|
672
786
|
# Default: save alongside first available modality path
|
|
673
787
|
for mod in ("depth", "rgb", "rays"):
|
|
674
788
|
if mod in dataset_config and "path" in dataset_config[mod]:
|
|
675
|
-
output_file =
|
|
789
|
+
output_file = (
|
|
790
|
+
_normalize_modality_path(
|
|
791
|
+
dataset_config[mod]["path"],
|
|
792
|
+
modality_key=mod,
|
|
793
|
+
split=dataset_config[mod].get("split"),
|
|
794
|
+
)
|
|
795
|
+
/ "eval.json"
|
|
796
|
+
)
|
|
676
797
|
break
|
|
677
798
|
if output_file is None:
|
|
678
799
|
output_file = Path("eval.json")
|
|
@@ -1065,21 +1186,18 @@ def main():
|
|
|
1065
1186
|
all_results[depth_key] = depth_results[depth_key]
|
|
1066
1187
|
depth_pfm = depth_results.get("per_file_metrics", {})
|
|
1067
1188
|
if depth_pfm:
|
|
1068
|
-
|
|
1189
|
+
canonical_space = space_info.get("canonical_space", "metric")
|
|
1190
|
+
depth_save["per_file_metrics"] = _clean_per_file_metrics(
|
|
1069
1191
|
_wrap_pfm_metrics(
|
|
1070
1192
|
depth_pfm,
|
|
1071
|
-
lambda m: (
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
for space in ("native", "metric")
|
|
1077
|
-
if f"depth_{space}" in m
|
|
1078
|
-
},
|
|
1079
|
-
},
|
|
1080
|
-
}
|
|
1193
|
+
lambda m: _wrap_depth_space_pfm_metrics(
|
|
1194
|
+
m,
|
|
1195
|
+
metric_root="depth",
|
|
1196
|
+
canonical_key="depth",
|
|
1197
|
+
canonical_space=canonical_space,
|
|
1081
1198
|
),
|
|
1082
|
-
)
|
|
1199
|
+
),
|
|
1200
|
+
label="depth per_file_metrics",
|
|
1083
1201
|
)
|
|
1084
1202
|
all_results.setdefault("per_file_metrics", {}).update(depth_pfm)
|
|
1085
1203
|
|
|
@@ -1258,21 +1376,18 @@ def main():
|
|
|
1258
1376
|
all_results[depth_key] = sparse_depth_results[depth_key]
|
|
1259
1377
|
sparse_depth_pfm = sparse_depth_results.get("per_file_metrics", {})
|
|
1260
1378
|
if sparse_depth_pfm:
|
|
1261
|
-
|
|
1379
|
+
canonical_space = space_info.get("canonical_space", "metric")
|
|
1380
|
+
depth_save["per_file_metrics"] = _clean_per_file_metrics(
|
|
1262
1381
|
_wrap_pfm_metrics(
|
|
1263
1382
|
sparse_depth_pfm,
|
|
1264
|
-
lambda m: (
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
for space in ("native", "metric")
|
|
1270
|
-
if f"sparse_depth_{space}" in m
|
|
1271
|
-
},
|
|
1272
|
-
},
|
|
1273
|
-
}
|
|
1383
|
+
lambda m: _wrap_depth_space_pfm_metrics(
|
|
1384
|
+
m,
|
|
1385
|
+
metric_root=_SPARSE_DEPTH_METRIC_ROOT,
|
|
1386
|
+
canonical_key="sparse_depth",
|
|
1387
|
+
canonical_space=canonical_space,
|
|
1274
1388
|
),
|
|
1275
|
-
)
|
|
1389
|
+
),
|
|
1390
|
+
label="sparse_depth per_file_metrics",
|
|
1276
1391
|
)
|
|
1277
1392
|
all_results.setdefault("per_file_metrics", {}).update(sparse_depth_pfm)
|
|
1278
1393
|
|
|
@@ -1417,11 +1532,12 @@ def main():
|
|
|
1417
1532
|
all_results["rgb_benchmark"] = rgb_benchmark
|
|
1418
1533
|
rgb_pfm = rgb_results.get("per_file_metrics", {})
|
|
1419
1534
|
if rgb_pfm:
|
|
1420
|
-
rgb_save["per_file_metrics"] =
|
|
1535
|
+
rgb_save["per_file_metrics"] = _clean_per_file_metrics(
|
|
1421
1536
|
_wrap_pfm_metrics(
|
|
1422
1537
|
rgb_pfm,
|
|
1423
1538
|
lambda m: {"rgb": {"eval": m.get("rgb", {})}},
|
|
1424
|
-
)
|
|
1539
|
+
),
|
|
1540
|
+
label="rgb per_file_metrics",
|
|
1425
1541
|
)
|
|
1426
1542
|
all_results.setdefault("per_file_metrics", {}).update(rgb_pfm)
|
|
1427
1543
|
|
|
@@ -1519,11 +1635,12 @@ def main():
|
|
|
1519
1635
|
all_results["rays"] = rays_metrics
|
|
1520
1636
|
rays_pfm = rays_results.get("per_file_metrics", {})
|
|
1521
1637
|
if rays_pfm:
|
|
1522
|
-
rays_save["per_file_metrics"] =
|
|
1638
|
+
rays_save["per_file_metrics"] = _clean_per_file_metrics(
|
|
1523
1639
|
_wrap_pfm_metrics(
|
|
1524
1640
|
rays_pfm,
|
|
1525
1641
|
lambda m: {"rays": {"eval": m.get("rays", {})}},
|
|
1526
|
-
)
|
|
1642
|
+
),
|
|
1643
|
+
label="rays per_file_metrics",
|
|
1527
1644
|
)
|
|
1528
1645
|
all_results.setdefault("per_file_metrics", {}).update(rays_pfm)
|
|
1529
1646
|
|
|
@@ -525,13 +525,14 @@ def _resolve_sky_mask_loader(segmentation_path: str) -> Callable[..., Any]:
|
|
|
525
525
|
its ``sky_mask`` function so that raw class-segmentation data is
|
|
526
526
|
converted to a boolean sky mask at load time.
|
|
527
527
|
"""
|
|
528
|
+
modality = _modality(path=segmentation_path, modality_key="segmentation")
|
|
528
529
|
try:
|
|
529
530
|
index = index_dataset_from_path(
|
|
530
|
-
|
|
531
|
-
metadata_scope=
|
|
531
|
+
modality.path,
|
|
532
|
+
metadata_scope=modality.metadata_scope,
|
|
532
533
|
)
|
|
533
534
|
except FileNotFoundError:
|
|
534
|
-
index = index_dataset_from_path(
|
|
535
|
+
index = index_dataset_from_path(modality.path)
|
|
535
536
|
# Resolve euler_loading metadata the same way euler-loading does:
|
|
536
537
|
# try the contract addon API first (handles addons.euler_loading),
|
|
537
538
|
# then fall back to top-level euler_loading for legacy indices.
|
|
@@ -546,14 +547,14 @@ def _resolve_sky_mask_loader(segmentation_path: str) -> Callable[..., Any]:
|
|
|
546
547
|
loader_name = euler_meta.get("loader")
|
|
547
548
|
if loader_name is None:
|
|
548
549
|
raise ValueError(
|
|
549
|
-
f"Segmentation dataset at {
|
|
550
|
+
f"Segmentation dataset at {modality.path!r} does not declare "
|
|
550
551
|
f"an 'euler_loading.loader' in its ds-crawler index."
|
|
551
552
|
)
|
|
552
553
|
module = resolve_loader_module(loader_name)
|
|
553
554
|
sky_fn = getattr(module, "sky_mask", None)
|
|
554
555
|
if sky_fn is None or not callable(sky_fn):
|
|
555
556
|
raise ValueError(
|
|
556
|
-
f"Loader module {loader_name!r} (resolved from {
|
|
557
|
+
f"Loader module {loader_name!r} (resolved from {modality.path!r}) "
|
|
557
558
|
f"does not expose a 'sky_mask' function."
|
|
558
559
|
)
|
|
559
560
|
return sky_fn
|
|
@@ -1613,6 +1613,7 @@ def evaluate_sparse_depth_samples(
|
|
|
1613
1613
|
for key, value in metrics["standard_metrics"].items()
|
|
1614
1614
|
},
|
|
1615
1615
|
"depth_metrics": {
|
|
1616
|
+
"valid_pixel_count": metrics["valid_pixel_count"],
|
|
1616
1617
|
"absrel": float(np.mean(absrel_arr)) if len(absrel_arr) > 0 else None,
|
|
1617
1618
|
"rmse": (
|
|
1618
1619
|
float(np.sqrt(np.mean(rmse_arr))) if len(rmse_arr) > 0 else None
|
|
@@ -145,6 +145,7 @@ def get_benchmark_depth_bins(
|
|
|
145
145
|
well-behaved as range_min approaches zero (unlike log).
|
|
146
146
|
|
|
147
147
|
For example:
|
|
148
|
+
[0.01, 80] -> near=[0.01, 9.29), mid=[9.29, 35.95), far=[35.95, 80].
|
|
148
149
|
[0.1, 80] -> near=[0.1, 10.19), mid=[10.19, 36.83), far=[36.83, 80].
|
|
149
150
|
[1, 80] -> near=[1, 13.31), mid=[13.31, 39.64), far=[39.64, 80].
|
|
150
151
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-eval
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.0
|
|
4
4
|
Summary: Depth map evaluation toolkit with comprehensive metrics
|
|
5
5
|
Author: Depth Eval Contributors
|
|
6
6
|
License: MIT
|
|
@@ -45,7 +45,7 @@ A comprehensive evaluation toolkit for comparing predicted depth maps, RGB image
|
|
|
45
45
|
- **Depth metrics**: PSNR, SSIM, LPIPS, FID, KID, AbsRel, RMSE, Scale-Invariant Log Error, Normal Consistency, Depth Edge F1
|
|
46
46
|
- **RGB metrics**: PSNR, SSIM, LPIPS, FID, SCE (Structural Chromatic Error), Edge F1, Tail Errors (p95/p99), High-Frequency Energy Ratio, Depth-Binned Photometric Error
|
|
47
47
|
- **Rays metrics**: ρ_A (AUC of angular accuracy curve), Angular Error statistics and threshold percentages
|
|
48
|
-
- **Benchmark binning**: Optional depth-range benchmark that subdivides metrics into
|
|
48
|
+
- **Benchmark binning**: Optional depth-range benchmark that subdivides metrics into square-root-scaled near/mid/far bins
|
|
49
49
|
- **Sanity checking**: Automatic validation of metric results against configurable thresholds, with detailed warning reports
|
|
50
50
|
- **Sky masking**: Optional exclusion of sky regions from metrics using GT segmentation
|
|
51
51
|
- **Flexible dataset loading**: Automatic loader resolution via euler_loading and ds-crawler index metadata
|
|
@@ -141,7 +141,7 @@ This pre-downloads:
|
|
|
141
141
|
| `--metrics-config` | `str` | auto-detect | Path to `metrics_config.json` for sanity checking |
|
|
142
142
|
| `--depth-alignment` | `{none,auto_affine,affine}` | `auto_affine` | Depth calibration mode; outputs are emitted in semantic `native`/`metric` spaces and `depth` aliases the canonical branch |
|
|
143
143
|
| `--rgb-fid-backend` | `{builtin,clean-fid}` | `builtin` | RGB FID backend; `clean-fid` requires optional dependency |
|
|
144
|
-
| `--benchmark-depth-range` | `float float` | none | Depth range `[MIN, MAX]` in meters for benchmark evaluation; computes depth and RGB metrics for pixels within this range, subdivided into
|
|
144
|
+
| `--benchmark-depth-range` | `float float` | none | Depth range `[MIN, MAX]` in meters for benchmark evaluation; computes depth and RGB metrics for pixels within this range, subdivided into square-root-scaled near/mid/far bins (additive to regular metrics) |
|
|
145
145
|
|
|
146
146
|
### Examples
|
|
147
147
|
|
|
@@ -168,7 +168,7 @@ depth-eval config.json --depth-alignment affine
|
|
|
168
168
|
depth-eval config.json --rgb-fid-backend clean-fid
|
|
169
169
|
|
|
170
170
|
# Benchmark depth and RGB metrics within a depth range (near/mid/far bins)
|
|
171
|
-
depth-eval config.json --benchmark-depth-range 0.
|
|
171
|
+
depth-eval config.json --benchmark-depth-range 0.01 80.0
|
|
172
172
|
|
|
173
173
|
# Evaluate dense depth predictions against sparse pointcloud GT
|
|
174
174
|
depth-eval example_sparse_depth_config.json --skip-rgb --skip-rays
|
|
@@ -177,6 +177,30 @@ depth-eval example_sparse_depth_config.json --skip-rgb --skip-rays
|
|
|
177
177
|
depth-eval config.json --skip-rays
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
### Benchmark Depth Bins
|
|
181
|
+
|
|
182
|
+
`--benchmark-depth-range MIN MAX` first filters valid GT depth pixels to
|
|
183
|
+
`MIN <= depth <= MAX`. The `near`, `mid`, and `far` bins are then computed as
|
|
184
|
+
three equal-width intervals in square-root depth space:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
sqrt_min = sqrt(MIN)
|
|
188
|
+
sqrt_max = sqrt(MAX)
|
|
189
|
+
step = (sqrt_max - sqrt_min) / 3
|
|
190
|
+
near_max = (sqrt_min + step)^2
|
|
191
|
+
mid_max = (sqrt_min + 2 * step)^2
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The interval bounds are `near=[MIN, near_max)`, `mid=[near_max, mid_max)`, and
|
|
195
|
+
`far=[mid_max, MAX]`. For `--benchmark-depth-range 0.01 80.0`, this gives:
|
|
196
|
+
|
|
197
|
+
| Bin | Depth interval |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `all` | `[0.01, 80.0]` |
|
|
200
|
+
| `near` | `[0.01, 9.290856529)` |
|
|
201
|
+
| `mid` | `[9.290856529, 35.954189863)` |
|
|
202
|
+
| `far` | `[35.954189863, 80.0]` |
|
|
203
|
+
|
|
180
204
|
## Configuration
|
|
181
205
|
|
|
182
206
|
### `config.json`
|
|
@@ -215,7 +239,7 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
|
|
|
215
239
|
}
|
|
216
240
|
```
|
|
217
241
|
|
|
218
|
-
Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`).
|
|
242
|
+
Each modality entry can optionally include a `split` field to select a specific split from the dataset (e.g. `{ "path": "/data/gt/depth", "split": "test" }`). euler-loading inline selectors are also accepted in `path`, such as `/data/muses.zip:test` or `/data/muses.zip:test#scope=rgb`.
|
|
219
243
|
|
|
220
244
|
For sparse pointcloud depth evaluation, use `gt.sparse_depth` instead of `gt.depth`. The prediction still uses `datasets[].depth` because the model output is a dense depth map. The evaluator projects the sparse GT point cloud into the prediction image plane using `gt.intrinsics` and `gt.camera_extrinsics`, then computes pointwise depth metrics only at projected valid pixels.
|
|
221
245
|
|
|
@@ -269,7 +293,7 @@ Dataset metadata (e.g. `radial_depth`, `rgb_range`, sparse point columns, and co
|
|
|
269
293
|
|
|
270
294
|
### Dataset Metadata
|
|
271
295
|
|
|
272
|
-
Each dataset directory must contain ds-crawler metadata artifacts, typically generated under `.ds_crawler/`:
|
|
296
|
+
Each dataset directory or archive must contain ds-crawler metadata artifacts, typically generated under `.ds_crawler/`:
|
|
273
297
|
|
|
274
298
|
```text
|
|
275
299
|
dataset-root/
|
|
@@ -279,6 +303,8 @@ dataset-root/
|
|
|
279
303
|
index.json
|
|
280
304
|
```
|
|
281
305
|
|
|
306
|
+
When one physical root or archive contains several logical modalities, the artifacts may instead be scoped under `.ds_crawler/<modality>/` with a `.ds_crawler/scopes.json` manifest. `euler-eval` passes modality scopes to euler-loading so paths such as `/data/muses.zip:test` can load `.ds_crawler/rgb/index.json`, `.ds_crawler/depth/index.json`, and related scoped artifacts from the same archive.
|
|
307
|
+
|
|
282
308
|
GT and prediction datasets are matched by hierarchy path and file ID through `MultiModalDataset`.
|
|
283
309
|
|
|
284
310
|
### `metrics_config.json`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tests for main.py config validation."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import zipfile
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from euler_eval.cli import load_config, validate_dataset_entry, validate_gt_config
|
|
@@ -50,6 +51,20 @@ class TestValidateGtConfig:
|
|
|
50
51
|
gt = {"rgb": {"path": str(rgb_path)}}
|
|
51
52
|
validate_gt_config(gt)
|
|
52
53
|
|
|
54
|
+
def test_inline_zip_split_gt_path_is_valid(self, tmp_path):
|
|
55
|
+
rgb_path = tmp_path / "reference.zip"
|
|
56
|
+
with zipfile.ZipFile(rgb_path, "w") as zf:
|
|
57
|
+
zf.writestr("dummy.txt", "x")
|
|
58
|
+
gt = {"rgb": {"path": f"{rgb_path}:fog_day"}}
|
|
59
|
+
validate_gt_config(gt)
|
|
60
|
+
|
|
61
|
+
def test_inline_scope_gt_path_is_valid(self, tmp_path):
|
|
62
|
+
rgb_path = tmp_path / "reference.zip"
|
|
63
|
+
with zipfile.ZipFile(rgb_path, "w") as zf:
|
|
64
|
+
zf.writestr("dummy.txt", "x")
|
|
65
|
+
gt = {"rgb": {"path": f"{rgb_path}#scope=rgb"}}
|
|
66
|
+
validate_gt_config(gt)
|
|
67
|
+
|
|
53
68
|
def test_sparse_depth_requires_projection_modalities(self, tmp_path):
|
|
54
69
|
sparse_path = tmp_path / "sparse_depth"
|
|
55
70
|
sparse_path.mkdir()
|
|
@@ -132,6 +147,16 @@ class TestValidateDatasetEntry:
|
|
|
132
147
|
with pytest.raises(ValueError, match="does not exist"):
|
|
133
148
|
validate_dataset_entry(entry, 0)
|
|
134
149
|
|
|
150
|
+
def test_inline_zip_split_dataset_path_is_valid(self, tmp_path):
|
|
151
|
+
rgb_path = tmp_path / "prediction.zip"
|
|
152
|
+
with zipfile.ZipFile(rgb_path, "w") as zf:
|
|
153
|
+
zf.writestr("dummy.txt", "x")
|
|
154
|
+
entry = {
|
|
155
|
+
"name": "model_a",
|
|
156
|
+
"rgb": {"path": f"{rgb_path}:fog_day"},
|
|
157
|
+
}
|
|
158
|
+
validate_dataset_entry(entry, 0)
|
|
159
|
+
|
|
135
160
|
def test_index_in_error_message(self, tmp_path):
|
|
136
161
|
entry = {"name": "test"}
|
|
137
162
|
with pytest.raises(ValueError, match=r"datasets\[3\]"):
|
|
@@ -169,3 +169,26 @@ def test_sky_mask_loader_resolution_uses_segmentation_scope(monkeypatch):
|
|
|
169
169
|
assert calls == [
|
|
170
170
|
("/datasets/shared", {"metadata_scope": "segmentation"}),
|
|
171
171
|
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_sky_mask_loader_resolution_strips_inline_split(monkeypatch):
|
|
175
|
+
calls = []
|
|
176
|
+
|
|
177
|
+
def fake_index_dataset_from_path(path, **kwargs):
|
|
178
|
+
calls.append((path, kwargs))
|
|
179
|
+
return {"euler_loading": {"loader": "vkitti2"}}
|
|
180
|
+
|
|
181
|
+
def sky_mask(path, meta=None):
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
monkeypatch.setattr(data, "index_dataset_from_path", fake_index_dataset_from_path)
|
|
185
|
+
monkeypatch.setattr(
|
|
186
|
+
data,
|
|
187
|
+
"resolve_loader_module",
|
|
188
|
+
lambda name: SimpleNamespace(sky_mask=sky_mask),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
assert data._resolve_sky_mask_loader("/datasets/shared.zip:fog_day") is sky_mask
|
|
192
|
+
assert calls == [
|
|
193
|
+
("/datasets/shared.zip", {"metadata_scope": "segmentation"}),
|
|
194
|
+
]
|
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from euler_eval.cli import (
|
|
6
6
|
_clean_metric_tree,
|
|
7
|
+
_clean_per_file_metrics,
|
|
7
8
|
_depth_eval_axes,
|
|
8
9
|
_DEPTH_EVAL_DESCRIPTIONS,
|
|
9
10
|
_sparse_depth_eval_axes,
|
|
@@ -15,6 +16,7 @@ from euler_eval.cli import (
|
|
|
15
16
|
_RGB_EVAL_DESCRIPTIONS,
|
|
16
17
|
_RAYS_EVAL_AXES,
|
|
17
18
|
_RAYS_EVAL_DESCRIPTIONS,
|
|
19
|
+
_wrap_depth_space_pfm_metrics,
|
|
18
20
|
)
|
|
19
21
|
|
|
20
22
|
# Build base (non-benchmark) axis dicts for testing
|
|
@@ -97,6 +99,105 @@ class TestMetaBlockStructure:
|
|
|
97
99
|
cleaned = _clean_metric_tree(meta)
|
|
98
100
|
assert "eval_params" not in cleaned
|
|
99
101
|
|
|
102
|
+
def test_per_file_metrics_object_preserved_when_metrics_clean_empty(self):
|
|
103
|
+
"""Per-file entries keep a metrics object even if all values are pruned."""
|
|
104
|
+
per_file = {
|
|
105
|
+
"files": [
|
|
106
|
+
{
|
|
107
|
+
"id": "frame_0001",
|
|
108
|
+
"metrics": {
|
|
109
|
+
"rgb": {
|
|
110
|
+
"eval": {
|
|
111
|
+
"image_quality": {
|
|
112
|
+
"psnr": None,
|
|
113
|
+
"ssim": float("nan"),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
assert _clean_metric_tree(per_file) == {"files": [{"id": "frame_0001"}]}
|
|
123
|
+
assert _clean_per_file_metrics(per_file) == {
|
|
124
|
+
"files": [{"id": "frame_0001", "metrics": {}}]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def test_depth_per_file_wrapper_falls_back_to_canonical_metrics(self):
|
|
128
|
+
"""Canonical-only per-file depth metrics still serialize under a space."""
|
|
129
|
+
metrics = {
|
|
130
|
+
"depth": {
|
|
131
|
+
"standard": {
|
|
132
|
+
"absrel": 0.1,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
assert _wrap_depth_space_pfm_metrics(
|
|
138
|
+
metrics,
|
|
139
|
+
metric_root="depth",
|
|
140
|
+
canonical_key="depth",
|
|
141
|
+
canonical_space="native",
|
|
142
|
+
) == {
|
|
143
|
+
"depth": {
|
|
144
|
+
"eval": {
|
|
145
|
+
"native": metrics["depth"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def test_sparse_depth_zero_valid_count_survives_cleaning(self):
|
|
151
|
+
"""Zero-support sparse entries keep a finite support metric in eval.json."""
|
|
152
|
+
metrics = {
|
|
153
|
+
"sparse_depth": {
|
|
154
|
+
"standard": {
|
|
155
|
+
"absrel": None,
|
|
156
|
+
},
|
|
157
|
+
"depth_metrics": {
|
|
158
|
+
"valid_pixel_count": 0,
|
|
159
|
+
"absrel": None,
|
|
160
|
+
"rmse": None,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
wrapped = _wrap_depth_space_pfm_metrics(
|
|
166
|
+
metrics,
|
|
167
|
+
metric_root=_SPARSE_DEPTH_METRIC_ROOT,
|
|
168
|
+
canonical_key="sparse_depth",
|
|
169
|
+
canonical_space="metric",
|
|
170
|
+
)
|
|
171
|
+
cleaned = _clean_per_file_metrics(
|
|
172
|
+
{
|
|
173
|
+
"files": [
|
|
174
|
+
{
|
|
175
|
+
"id": "frame",
|
|
176
|
+
"metrics": wrapped,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
assert cleaned == {
|
|
183
|
+
"files": [
|
|
184
|
+
{
|
|
185
|
+
"id": "frame",
|
|
186
|
+
"metrics": {
|
|
187
|
+
_SPARSE_DEPTH_METRIC_ROOT: {
|
|
188
|
+
"eval": {
|
|
189
|
+
"metric": {
|
|
190
|
+
"depth_metrics": {
|
|
191
|
+
"valid_pixel_count": 0,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
|
|
100
201
|
def test_meta_in_full_save_dict(self):
|
|
101
202
|
"""meta sits alongside metricSet and dataset_info without conflict."""
|
|
102
203
|
save_dict = {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Tests for shared metric utility helpers."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from euler_eval.metrics.utils import get_benchmark_depth_bins
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_benchmark_depth_bins_for_001_to_80m_range():
|
|
9
|
+
range_min = 0.01
|
|
10
|
+
range_max = 80.0
|
|
11
|
+
sqrt_min = np.sqrt(range_min)
|
|
12
|
+
sqrt_max = np.sqrt(range_max)
|
|
13
|
+
step = (sqrt_max - sqrt_min) / 3.0
|
|
14
|
+
near_max = float((sqrt_min + step) ** 2)
|
|
15
|
+
mid_max = float((sqrt_min + 2 * step) ** 2)
|
|
16
|
+
|
|
17
|
+
depth = np.array(
|
|
18
|
+
[
|
|
19
|
+
0.0,
|
|
20
|
+
range_min,
|
|
21
|
+
np.nextafter(near_max, range_min),
|
|
22
|
+
near_max,
|
|
23
|
+
np.nextafter(mid_max, near_max),
|
|
24
|
+
mid_max,
|
|
25
|
+
range_max,
|
|
26
|
+
np.nextafter(range_max, np.inf),
|
|
27
|
+
],
|
|
28
|
+
dtype=np.float64,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
bins = get_benchmark_depth_bins(depth, range_min, range_max)
|
|
32
|
+
|
|
33
|
+
assert bins["boundaries"] == {
|
|
34
|
+
"range": [range_min, range_max],
|
|
35
|
+
"near": [range_min, near_max],
|
|
36
|
+
"mid": [near_max, mid_max],
|
|
37
|
+
"far": [mid_max, range_max],
|
|
38
|
+
}
|
|
39
|
+
assert np.isclose(near_max, 9.290856529333297)
|
|
40
|
+
assert np.isclose(mid_max, 35.95418986266663)
|
|
41
|
+
|
|
42
|
+
np.testing.assert_array_equal(
|
|
43
|
+
bins["all"],
|
|
44
|
+
np.array([False, True, True, True, True, True, True, False]),
|
|
45
|
+
)
|
|
46
|
+
np.testing.assert_array_equal(
|
|
47
|
+
bins["near"],
|
|
48
|
+
np.array([False, True, True, False, False, False, False, False]),
|
|
49
|
+
)
|
|
50
|
+
np.testing.assert_array_equal(
|
|
51
|
+
bins["mid"],
|
|
52
|
+
np.array([False, False, False, True, True, False, False, False]),
|
|
53
|
+
)
|
|
54
|
+
np.testing.assert_array_equal(
|
|
55
|
+
bins["far"],
|
|
56
|
+
np.array([False, False, False, False, False, True, True, False]),
|
|
57
|
+
)
|
|
@@ -214,6 +214,21 @@ class TestSaveResultsZip:
|
|
|
214
214
|
stored = json.loads(zf.read("eval.json"))
|
|
215
215
|
assert stored == results
|
|
216
216
|
|
|
217
|
+
def test_inline_zip_split_writes_eval_json_into_zip(self, tmp_path):
|
|
218
|
+
zp = tmp_path / "dataset.zip"
|
|
219
|
+
with zipfile.ZipFile(zp, "w") as zf:
|
|
220
|
+
zf.writestr("image_001.png", b"\x89PNG")
|
|
221
|
+
|
|
222
|
+
results = {"depth": {"rmse": 0.42}}
|
|
223
|
+
config = {"name": "model", "depth": {"path": f"{zp}:fog_day"}}
|
|
224
|
+
out = save_results(results, config)
|
|
225
|
+
|
|
226
|
+
assert out == zp / "eval.json"
|
|
227
|
+
with zipfile.ZipFile(zp, "r") as zf:
|
|
228
|
+
assert "eval.json" in zf.namelist()
|
|
229
|
+
stored = json.loads(zf.read("eval.json"))
|
|
230
|
+
assert stored == results
|
|
231
|
+
|
|
217
232
|
def test_preserves_existing_zip_contents(self, tmp_path):
|
|
218
233
|
zp = tmp_path / "dataset.zip"
|
|
219
234
|
with zipfile.ZipFile(zp, "w") as zf:
|
|
@@ -93,6 +93,12 @@ class _OneSampleSparseDataset:
|
|
|
93
93
|
return {"gt": {"path": "/gt"}, "pred": {"path": "/pred"}}
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
class _ZeroValidSparseDataset(_OneSampleSparseDataset):
|
|
97
|
+
def __init__(self):
|
|
98
|
+
super().__init__()
|
|
99
|
+
self.pred = np.zeros((3, 3), dtype=np.float32)
|
|
100
|
+
|
|
101
|
+
|
|
96
102
|
def test_sparse_depth_eval_reports_only_pointwise_depth_metrics():
|
|
97
103
|
dataset = _OneSampleSparseDataset()
|
|
98
104
|
|
|
@@ -111,3 +117,20 @@ def test_sparse_depth_eval_reports_only_pointwise_depth_metrics():
|
|
|
111
117
|
np.testing.assert_allclose(metrics["standard"]["pixel_pool"]["absrel"], 0.0)
|
|
112
118
|
np.testing.assert_allclose(metrics["standard"]["pixel_pool"]["delta1"], 1.0)
|
|
113
119
|
np.testing.assert_allclose(metrics["depth_metrics"]["rmse"]["median"], 0.0)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_sparse_depth_per_file_reports_zero_valid_support():
|
|
123
|
+
result = evaluate_sparse_depth_samples(
|
|
124
|
+
_ZeroValidSparseDataset(),
|
|
125
|
+
pred_is_radial=True,
|
|
126
|
+
num_workers=0,
|
|
127
|
+
alignment_mode="none",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert result["dataset_info"]["projected_pixels"] == 2
|
|
131
|
+
assert result["dataset_info"]["evaluated_pixels"] == 0
|
|
132
|
+
|
|
133
|
+
per_file = result["per_file_metrics"]["files"][0]["metrics"]["sparse_depth"]
|
|
134
|
+
assert per_file["depth_metrics"]["valid_pixel_count"] == 0
|
|
135
|
+
assert per_file["depth_metrics"]["absrel"] is None
|
|
136
|
+
assert per_file["depth_metrics"]["rmse"] is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|