euler-eval 2.9.0__tar.gz → 2.11.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.9.0 → euler_eval-2.11.0}/PKG-INFO +5 -3
- {euler_eval-2.9.0 → euler_eval-2.11.0}/README.md +4 -2
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/cli.py +23 -4
- euler_eval-2.11.0/euler_eval/config_paths.py +125 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/data.py +9 -8
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval.egg-info/PKG-INFO +5 -3
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval.egg-info/SOURCES.txt +1 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/pyproject.toml +1 -1
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_config.py +44 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_data_builders.py +34 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_save_results.py +30 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/__init__.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/evaluate.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/__init__.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/absrel.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/daniel_error.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/depth_binned_error.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/depth_standard.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/fid_kid.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/high_freq_energy.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/lpips_metric.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/normal_consistency.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/psnr.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/rgb_lpips.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/rho_a.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/rmse.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/ssim.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/tail_errors.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/metrics/utils.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/sanity_checker.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval/utils/hierarchy_parser.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval.egg-info/dependency_links.txt +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval.egg-info/entry_points.txt +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval.egg-info/requires.txt +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/euler_eval.egg-info/top_level.txt +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/init_cache.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/setup.cfg +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_alignment.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_cli_device.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_data.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_depth_alignment_output.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_depth_standard.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_evaluate_helpers.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_init_cache.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_integration.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_meta_output.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_metrics_utils.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_rgb_fid_output.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_rho_a.py +0 -0
- {euler_eval-2.9.0 → euler_eval-2.11.0}/tests/test_sparse_depth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-eval
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
4
4
|
Summary: Depth map evaluation toolkit with comprehensive metrics
|
|
5
5
|
Author: Depth Eval Contributors
|
|
6
6
|
License: MIT
|
|
@@ -239,7 +239,7 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
|
|
|
239
239
|
}
|
|
240
240
|
```
|
|
241
241
|
|
|
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" }`).
|
|
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`.
|
|
243
243
|
|
|
244
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.
|
|
245
245
|
|
|
@@ -293,7 +293,7 @@ Dataset metadata (e.g. `radial_depth`, `rgb_range`, sparse point columns, and co
|
|
|
293
293
|
|
|
294
294
|
### Dataset Metadata
|
|
295
295
|
|
|
296
|
-
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/`:
|
|
297
297
|
|
|
298
298
|
```text
|
|
299
299
|
dataset-root/
|
|
@@ -303,6 +303,8 @@ dataset-root/
|
|
|
303
303
|
index.json
|
|
304
304
|
```
|
|
305
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
|
+
|
|
306
308
|
GT and prediction datasets are matched by hierarchy path and file ID through `MultiModalDataset`.
|
|
307
309
|
|
|
308
310
|
### `metrics_config.json`
|
|
@@ -201,7 +201,7 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
|
|
|
201
201
|
}
|
|
202
202
|
```
|
|
203
203
|
|
|
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" }`).
|
|
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`.
|
|
205
205
|
|
|
206
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.
|
|
207
207
|
|
|
@@ -255,7 +255,7 @@ Dataset metadata (e.g. `radial_depth`, `rgb_range`, sparse point columns, and co
|
|
|
255
255
|
|
|
256
256
|
### Dataset Metadata
|
|
257
257
|
|
|
258
|
-
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/`:
|
|
259
259
|
|
|
260
260
|
```text
|
|
261
261
|
dataset-root/
|
|
@@ -265,6 +265,8 @@ dataset-root/
|
|
|
265
265
|
index.json
|
|
266
266
|
```
|
|
267
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
|
+
|
|
268
270
|
GT and prediction datasets are matched by hierarchy path and file ID through `MultiModalDataset`.
|
|
269
271
|
|
|
270
272
|
### `metrics_config.json`
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
import torch
|
|
16
16
|
|
|
17
|
+
from .config_paths import normalize_modality_path
|
|
17
18
|
from .data import (
|
|
18
19
|
build_depth_eval_dataset,
|
|
19
20
|
build_rays_eval_dataset,
|
|
@@ -599,7 +600,10 @@ def validate_gt_config(gt: dict) -> None:
|
|
|
599
600
|
"camera_extrinsics",
|
|
600
601
|
):
|
|
601
602
|
if modality in gt and "path" in gt[modality]:
|
|
602
|
-
p =
|
|
603
|
+
p = normalize_modality_path(
|
|
604
|
+
gt[modality]["path"],
|
|
605
|
+
split=gt[modality].get("split"),
|
|
606
|
+
)
|
|
603
607
|
if not p.exists():
|
|
604
608
|
raise ValueError(f"gt.{modality}.path does not exist: {p}")
|
|
605
609
|
|
|
@@ -624,7 +628,10 @@ def validate_dataset_entry(entry: dict, index: int) -> None:
|
|
|
624
628
|
|
|
625
629
|
for modality in ("rgb", "depth", "rays"):
|
|
626
630
|
if modality in entry and "path" in entry[modality]:
|
|
627
|
-
p =
|
|
631
|
+
p = normalize_modality_path(
|
|
632
|
+
entry[modality]["path"],
|
|
633
|
+
split=entry[modality].get("split"),
|
|
634
|
+
)
|
|
628
635
|
if not p.exists():
|
|
629
636
|
raise ValueError(f"{label}.{modality}.path does not exist: {p}")
|
|
630
637
|
|
|
@@ -749,12 +756,24 @@ def save_results(
|
|
|
749
756
|
and modality in dataset_config
|
|
750
757
|
and "path" in dataset_config[modality]
|
|
751
758
|
):
|
|
752
|
-
output_file =
|
|
759
|
+
output_file = (
|
|
760
|
+
normalize_modality_path(
|
|
761
|
+
dataset_config[modality]["path"],
|
|
762
|
+
split=dataset_config[modality].get("split"),
|
|
763
|
+
)
|
|
764
|
+
/ "eval.json"
|
|
765
|
+
)
|
|
753
766
|
else:
|
|
754
767
|
# Default: save alongside first available modality path
|
|
755
768
|
for mod in ("depth", "rgb", "rays"):
|
|
756
769
|
if mod in dataset_config and "path" in dataset_config[mod]:
|
|
757
|
-
output_file =
|
|
770
|
+
output_file = (
|
|
771
|
+
normalize_modality_path(
|
|
772
|
+
dataset_config[mod]["path"],
|
|
773
|
+
split=dataset_config[mod].get("split"),
|
|
774
|
+
)
|
|
775
|
+
/ "eval.json"
|
|
776
|
+
)
|
|
758
777
|
break
|
|
759
778
|
if output_file is None:
|
|
760
779
|
output_file = Path("eval.json")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Helpers for euler-loading modality path selectors in config files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import parse_qsl
|
|
8
|
+
|
|
9
|
+
from ds_crawler import validate_metadata_scope
|
|
10
|
+
from ds_crawler.zip_utils import validate_split_name
|
|
11
|
+
from euler_loading import Modality
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ModalityPath:
|
|
16
|
+
path: str
|
|
17
|
+
split: str | None = None
|
|
18
|
+
metadata_scope: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_inline_scope(path: str) -> tuple[str, str | None]:
|
|
22
|
+
candidate_path, sep, fragment = path.rpartition("#")
|
|
23
|
+
if not sep:
|
|
24
|
+
return path, None
|
|
25
|
+
|
|
26
|
+
params = parse_qsl(fragment, keep_blank_values=True)
|
|
27
|
+
scope_values = [value for key, value in params if key == "scope"]
|
|
28
|
+
if not scope_values:
|
|
29
|
+
return path, None
|
|
30
|
+
|
|
31
|
+
unsupported = [key for key, _ in params if key != "scope"]
|
|
32
|
+
if unsupported:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"Unsupported modality path fragment parameter(s): "
|
|
35
|
+
f"{', '.join(sorted(set(unsupported)))}. "
|
|
36
|
+
"Only '#scope=<metadata_scope>' is supported."
|
|
37
|
+
)
|
|
38
|
+
if len(scope_values) > 1:
|
|
39
|
+
raise ValueError("Modality path may contain at most one '#scope=' selector.")
|
|
40
|
+
|
|
41
|
+
return candidate_path, validate_metadata_scope(scope_values[0])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_inline_split(path: str) -> tuple[str, str | None]:
|
|
45
|
+
colon_pos = path.rfind(":")
|
|
46
|
+
if colon_pos <= 1:
|
|
47
|
+
return path, None
|
|
48
|
+
|
|
49
|
+
candidate_split = path[colon_pos + 1 :]
|
|
50
|
+
candidate_path = path[:colon_pos]
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
split = validate_split_name(candidate_split)
|
|
54
|
+
except ValueError:
|
|
55
|
+
return path, None
|
|
56
|
+
|
|
57
|
+
return candidate_path, split
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_modality_path(
|
|
61
|
+
path: str | Path,
|
|
62
|
+
*,
|
|
63
|
+
split: str | None = None,
|
|
64
|
+
metadata_scope: str | None = None,
|
|
65
|
+
) -> ModalityPath:
|
|
66
|
+
"""Parse euler-loading inline split/scope selectors from a config path."""
|
|
67
|
+
raw_path = str(path)
|
|
68
|
+
parsed_path, inline_scope = _parse_inline_scope(raw_path)
|
|
69
|
+
parsed_path, inline_split = _parse_inline_split(parsed_path)
|
|
70
|
+
|
|
71
|
+
if inline_split is not None and split is not None:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Modality path {raw_path!r} contains an inline split "
|
|
74
|
+
f"({inline_split!r}) but an explicit split={split!r} was also "
|
|
75
|
+
"provided. Use one or the other, not both."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
normalized_scope = (
|
|
79
|
+
validate_metadata_scope(metadata_scope)
|
|
80
|
+
if metadata_scope is not None
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
83
|
+
if inline_scope is not None:
|
|
84
|
+
if normalized_scope is not None and normalized_scope != inline_scope:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Modality path {raw_path!r} contains an inline metadata scope "
|
|
87
|
+
f"({inline_scope!r}) but an explicit "
|
|
88
|
+
f"metadata_scope={metadata_scope!r} was also provided. Use one "
|
|
89
|
+
"metadata scope selector."
|
|
90
|
+
)
|
|
91
|
+
normalized_scope = inline_scope
|
|
92
|
+
|
|
93
|
+
return ModalityPath(
|
|
94
|
+
path=parsed_path,
|
|
95
|
+
split=split if split is not None else inline_split,
|
|
96
|
+
metadata_scope=normalized_scope,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def normalize_modality_path(path: str | Path, *, split: str | None = None) -> Path:
|
|
101
|
+
"""Return the filesystem/archive root for a config modality path."""
|
|
102
|
+
return Path(parse_modality_path(path, split=split).path)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_modality(
|
|
106
|
+
*,
|
|
107
|
+
path: str | Path,
|
|
108
|
+
modality_key: str,
|
|
109
|
+
split: str | None = None,
|
|
110
|
+
loader=None,
|
|
111
|
+
used_as: str | None = None,
|
|
112
|
+
) -> Modality:
|
|
113
|
+
"""Create a Modality after parsing inline config selectors locally."""
|
|
114
|
+
parsed = parse_modality_path(
|
|
115
|
+
path,
|
|
116
|
+
split=split,
|
|
117
|
+
)
|
|
118
|
+
return Modality(
|
|
119
|
+
path=parsed.path,
|
|
120
|
+
loader=loader,
|
|
121
|
+
used_as=used_as,
|
|
122
|
+
modality_type=modality_key,
|
|
123
|
+
metadata_scope=parsed.metadata_scope or modality_key,
|
|
124
|
+
split=parsed.split,
|
|
125
|
+
)
|
|
@@ -13,6 +13,7 @@ import torch
|
|
|
13
13
|
from ds_crawler import get_dataset_contract, index_dataset_from_path
|
|
14
14
|
from euler_loading import Modality, MultiModalDataset, resolve_loader_module
|
|
15
15
|
|
|
16
|
+
from .config_paths import build_modality
|
|
16
17
|
from .metrics.utils import convert_planar_to_radial
|
|
17
18
|
|
|
18
19
|
# ---------------------------------------------------------------------------
|
|
@@ -525,13 +526,14 @@ def _resolve_sky_mask_loader(segmentation_path: str) -> Callable[..., Any]:
|
|
|
525
526
|
its ``sky_mask`` function so that raw class-segmentation data is
|
|
526
527
|
converted to a boolean sky mask at load time.
|
|
527
528
|
"""
|
|
529
|
+
modality = _modality(path=segmentation_path, modality_key="segmentation")
|
|
528
530
|
try:
|
|
529
531
|
index = index_dataset_from_path(
|
|
530
|
-
|
|
531
|
-
metadata_scope=
|
|
532
|
+
modality.path,
|
|
533
|
+
metadata_scope=modality.metadata_scope,
|
|
532
534
|
)
|
|
533
535
|
except FileNotFoundError:
|
|
534
|
-
index = index_dataset_from_path(
|
|
536
|
+
index = index_dataset_from_path(modality.path)
|
|
535
537
|
# Resolve euler_loading metadata the same way euler-loading does:
|
|
536
538
|
# try the contract addon API first (handles addons.euler_loading),
|
|
537
539
|
# then fall back to top-level euler_loading for legacy indices.
|
|
@@ -546,14 +548,14 @@ def _resolve_sky_mask_loader(segmentation_path: str) -> Callable[..., Any]:
|
|
|
546
548
|
loader_name = euler_meta.get("loader")
|
|
547
549
|
if loader_name is None:
|
|
548
550
|
raise ValueError(
|
|
549
|
-
f"Segmentation dataset at {
|
|
551
|
+
f"Segmentation dataset at {modality.path!r} does not declare "
|
|
550
552
|
f"an 'euler_loading.loader' in its ds-crawler index."
|
|
551
553
|
)
|
|
552
554
|
module = resolve_loader_module(loader_name)
|
|
553
555
|
sky_fn = getattr(module, "sky_mask", None)
|
|
554
556
|
if sky_fn is None or not callable(sky_fn):
|
|
555
557
|
raise ValueError(
|
|
556
|
-
f"Loader module {loader_name!r} (resolved from {
|
|
558
|
+
f"Loader module {loader_name!r} (resolved from {modality.path!r}) "
|
|
557
559
|
f"does not expose a 'sky_mask' function."
|
|
558
560
|
)
|
|
559
561
|
return sky_fn
|
|
@@ -573,12 +575,11 @@ def _modality(
|
|
|
573
575
|
used_as: Optional[str] = None,
|
|
574
576
|
) -> Modality:
|
|
575
577
|
"""Create an euler-loading modality with an explicit metadata selector."""
|
|
576
|
-
return
|
|
578
|
+
return build_modality(
|
|
577
579
|
path=path,
|
|
578
580
|
loader=loader,
|
|
579
581
|
used_as=used_as,
|
|
580
|
-
|
|
581
|
-
metadata_scope=modality_key,
|
|
582
|
+
modality_key=modality_key,
|
|
582
583
|
split=split,
|
|
583
584
|
)
|
|
584
585
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-eval
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
4
4
|
Summary: Depth map evaluation toolkit with comprehensive metrics
|
|
5
5
|
Author: Depth Eval Contributors
|
|
6
6
|
License: MIT
|
|
@@ -239,7 +239,7 @@ Defines GT modalities, prediction datasets to evaluate, and optional euler_train
|
|
|
239
239
|
}
|
|
240
240
|
```
|
|
241
241
|
|
|
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" }`).
|
|
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`.
|
|
243
243
|
|
|
244
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.
|
|
245
245
|
|
|
@@ -293,7 +293,7 @@ Dataset metadata (e.g. `radial_depth`, `rgb_range`, sparse point columns, and co
|
|
|
293
293
|
|
|
294
294
|
### Dataset Metadata
|
|
295
295
|
|
|
296
|
-
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/`:
|
|
297
297
|
|
|
298
298
|
```text
|
|
299
299
|
dataset-root/
|
|
@@ -303,6 +303,8 @@ dataset-root/
|
|
|
303
303
|
index.json
|
|
304
304
|
```
|
|
305
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
|
+
|
|
306
308
|
GT and prediction datasets are matched by hierarchy path and file ID through `MultiModalDataset`.
|
|
307
309
|
|
|
308
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,39 @@ 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
|
+
|
|
68
|
+
def test_inline_scope_intrinsics_gt_path_is_valid(self, tmp_path):
|
|
69
|
+
depth_path = tmp_path / "depth"
|
|
70
|
+
depth_path.mkdir()
|
|
71
|
+
intrinsics_path = tmp_path / "frame_camera_trainvaltest.zip"
|
|
72
|
+
with zipfile.ZipFile(intrinsics_path, "w") as zf:
|
|
73
|
+
zf.writestr("dummy.txt", "x")
|
|
74
|
+
gt = {
|
|
75
|
+
"depth": {"path": str(depth_path)},
|
|
76
|
+
"intrinsics": {"path": f"{intrinsics_path}#scope=intrinsics"},
|
|
77
|
+
}
|
|
78
|
+
validate_gt_config(gt)
|
|
79
|
+
|
|
80
|
+
def test_inline_split_and_scope_gt_path_is_valid(self, tmp_path):
|
|
81
|
+
rgb_path = tmp_path / "reference.zip"
|
|
82
|
+
with zipfile.ZipFile(rgb_path, "w") as zf:
|
|
83
|
+
zf.writestr("dummy.txt", "x")
|
|
84
|
+
gt = {"rgb": {"path": f"{rgb_path}:fog_day#scope=rgb"}}
|
|
85
|
+
validate_gt_config(gt)
|
|
86
|
+
|
|
53
87
|
def test_sparse_depth_requires_projection_modalities(self, tmp_path):
|
|
54
88
|
sparse_path = tmp_path / "sparse_depth"
|
|
55
89
|
sparse_path.mkdir()
|
|
@@ -132,6 +166,16 @@ class TestValidateDatasetEntry:
|
|
|
132
166
|
with pytest.raises(ValueError, match="does not exist"):
|
|
133
167
|
validate_dataset_entry(entry, 0)
|
|
134
168
|
|
|
169
|
+
def test_inline_zip_split_dataset_path_is_valid(self, tmp_path):
|
|
170
|
+
rgb_path = tmp_path / "prediction.zip"
|
|
171
|
+
with zipfile.ZipFile(rgb_path, "w") as zf:
|
|
172
|
+
zf.writestr("dummy.txt", "x")
|
|
173
|
+
entry = {
|
|
174
|
+
"name": "model_a",
|
|
175
|
+
"rgb": {"path": f"{rgb_path}:fog_day"},
|
|
176
|
+
}
|
|
177
|
+
validate_dataset_entry(entry, 0)
|
|
178
|
+
|
|
135
179
|
def test_index_in_error_message(self, tmp_path):
|
|
136
180
|
entry = {"name": "test"}
|
|
137
181
|
with pytest.raises(ValueError, match=r"datasets\[3\]"):
|
|
@@ -169,3 +169,37 @@ 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
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_modality_parses_inline_split_and_scope():
|
|
198
|
+
modality = data._modality(
|
|
199
|
+
path="/datasets/frame_camera_trainvaltest.zip:fog_day#scope=intrinsics",
|
|
200
|
+
modality_key="intrinsics",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
assert modality.path == "/datasets/frame_camera_trainvaltest.zip"
|
|
204
|
+
assert modality.split == "fog_day"
|
|
205
|
+
assert modality.metadata_scope == "intrinsics"
|
|
@@ -214,6 +214,36 @@ 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
|
+
|
|
232
|
+
def test_inline_zip_scope_writes_eval_json_into_zip(self, tmp_path):
|
|
233
|
+
zp = tmp_path / "dataset.zip"
|
|
234
|
+
with zipfile.ZipFile(zp, "w") as zf:
|
|
235
|
+
zf.writestr("image_001.png", b"\x89PNG")
|
|
236
|
+
|
|
237
|
+
results = {"rgb": {"psnr": 30.0}}
|
|
238
|
+
config = {"name": "model", "rgb": {"path": f"{zp}#scope=rgb"}}
|
|
239
|
+
out = save_results(results, config, modality="rgb")
|
|
240
|
+
|
|
241
|
+
assert out == zp / "eval.json"
|
|
242
|
+
with zipfile.ZipFile(zp, "r") as zf:
|
|
243
|
+
assert "eval.json" in zf.namelist()
|
|
244
|
+
stored = json.loads(zf.read("eval.json"))
|
|
245
|
+
assert stored == results
|
|
246
|
+
|
|
217
247
|
def test_preserves_existing_zip_contents(self, tmp_path):
|
|
218
248
|
zp = tmp_path / "dataset.zip"
|
|
219
249
|
with zipfile.ZipFile(zp, "w") as zf:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|