euler-preprocess 3.16.0__tar.gz → 3.17.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_preprocess-3.16.0 → euler_preprocess-3.17.0}/PKG-INFO +13 -3
- euler_preprocess-3.16.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.17.0/README.md +11 -21
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/cli.py +8 -1
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/dataset.py +30 -18
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/normalize.py +14 -1
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/transform.py +5 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/atmospheric_light.py +2 -2
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/inference.py +4 -3
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/logging.py +25 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/models.py +23 -5
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/transform.py +39 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/sky_depth/transform.py +2 -2
- euler_preprocess-3.16.0/README.md → euler_preprocess-3.17.0/euler_preprocess.egg-info/PKG-INFO +31 -1
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/SOURCES.txt +1 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/requires.txt +1 -1
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/pyproject.toml +2 -2
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_fog_aux_outputs.py +2 -0
- euler_preprocess-3.17.0/tests/test_fog_input_normalization.py +259 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_sky_depth.py +11 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/CHANGELOG.md +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/LICENSE +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/MANIFEST.in +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/docs/spatial.md +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/color.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/intrinsics.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/output.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/augmentations.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/capture.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/pipeline.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/radial/__init__.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/radial/transform.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/sky_depth/__init__.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/spatial.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/examples/spatial-five-field.json +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/setup.cfg +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_cli_sample_selection.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_fog_aware_auto_exposure.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_fog_integration.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_modality_roles.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_radial.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_scene_illumination.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_sensor_identity.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_spatial.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_tone_map_lut.py +0 -0
- {euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_zip_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.17.0
|
|
4
4
|
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
5
|
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: Pillow
|
|
12
|
-
Requires-Dist: euler-loading>=2.
|
|
12
|
+
Requires-Dist: euler-loading>=2.29.0
|
|
13
13
|
Requires-Dist: ds-crawler>=2.11.0
|
|
14
14
|
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
15
15
|
Provides-Extra: gpu
|
|
@@ -119,6 +119,15 @@ loader.
|
|
|
119
119
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
120
120
|
| `radial` | `depth`, `intrinsics` | — |
|
|
121
121
|
|
|
122
|
+
`fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
|
|
123
|
+
it with the `sky_mask` function of the euler-loading module that the index's
|
|
124
|
+
dataset head names, so a `class_segmentation` index works as is: the sky is the
|
|
125
|
+
head's `skyclass` (Synscapes `[23, 23, 23]`, Real Drive Sim `[29, 0, 0]`, VKITTI2
|
|
126
|
+
`[90, 200, 255]`), and the log names the loader and the sky definition in use.
|
|
127
|
+
Set `"function"` in the modality entry to use another loader function, or
|
|
128
|
+
`"function": null` for the one the head declares. A non-binary mask raises an
|
|
129
|
+
error rather than marking every non-zero class as sky.
|
|
130
|
+
|
|
122
131
|
**Per-sample vs. hierarchical modalities.** You don't declare a modality's role:
|
|
123
132
|
it is inferred from the ds-crawler indexes. A modality whose qualified IDs
|
|
124
133
|
(hierarchy path plus file ID) match the transform's per-sample inputs is joined per
|
|
@@ -667,7 +676,8 @@ where sky depth is encoded as zero or infinity and needs a large finite value.
|
|
|
667
676
|
}
|
|
668
677
|
```
|
|
669
678
|
|
|
670
|
-
`sky_depth_value` defaults to `1000.0`.
|
|
679
|
+
`sky_depth_value` defaults to `1000.0`. The sky mask comes from the dataset head as
|
|
680
|
+
described under [Dataset Config](#dataset-config).
|
|
671
681
|
|
|
672
682
|
CLI runs write a source-backed depth dataset mirroring the input depth modality's paths,
|
|
673
683
|
filenames, extensions, and metadata. Standalone `SkyDepthTransform(...)` usage keeps the
|
euler_preprocess-3.16.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.17.0/README.md
RENAMED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: euler-preprocess
|
|
3
|
-
Version: 3.16.0
|
|
4
|
-
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
|
-
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.9
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: numpy
|
|
11
|
-
Requires-Dist: Pillow
|
|
12
|
-
Requires-Dist: euler-loading>=2.24.0
|
|
13
|
-
Requires-Dist: ds-crawler>=2.11.0
|
|
14
|
-
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
15
|
-
Provides-Extra: gpu
|
|
16
|
-
Requires-Dist: torch; extra == "gpu"
|
|
17
|
-
Provides-Extra: progress
|
|
18
|
-
Requires-Dist: tqdm; extra == "progress"
|
|
19
|
-
Dynamic: license-file
|
|
20
|
-
|
|
21
1
|
<!-- euler header — shared across the euler packages.
|
|
22
2
|
Per package, change only: the <h1>, the tagline, and the badge URLs. -->
|
|
23
3
|
<p align="center">
|
|
@@ -119,6 +99,15 @@ loader.
|
|
|
119
99
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
120
100
|
| `radial` | `depth`, `intrinsics` | — |
|
|
121
101
|
|
|
102
|
+
`fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
|
|
103
|
+
it with the `sky_mask` function of the euler-loading module that the index's
|
|
104
|
+
dataset head names, so a `class_segmentation` index works as is: the sky is the
|
|
105
|
+
head's `skyclass` (Synscapes `[23, 23, 23]`, Real Drive Sim `[29, 0, 0]`, VKITTI2
|
|
106
|
+
`[90, 200, 255]`), and the log names the loader and the sky definition in use.
|
|
107
|
+
Set `"function"` in the modality entry to use another loader function, or
|
|
108
|
+
`"function": null` for the one the head declares. A non-binary mask raises an
|
|
109
|
+
error rather than marking every non-zero class as sky.
|
|
110
|
+
|
|
122
111
|
**Per-sample vs. hierarchical modalities.** You don't declare a modality's role:
|
|
123
112
|
it is inferred from the ds-crawler indexes. A modality whose qualified IDs
|
|
124
113
|
(hierarchy path plus file ID) match the transform's per-sample inputs is joined per
|
|
@@ -667,7 +656,8 @@ where sky depth is encoded as zero or infinity and needs a large finite value.
|
|
|
667
656
|
}
|
|
668
657
|
```
|
|
669
658
|
|
|
670
|
-
`sky_depth_value` defaults to `1000.0`.
|
|
659
|
+
`sky_depth_value` defaults to `1000.0`. The sky mask comes from the dataset head as
|
|
660
|
+
described under [Dataset Config](#dataset-config).
|
|
671
661
|
|
|
672
662
|
CLI runs write a source-backed depth dataset mirroring the input depth modality's paths,
|
|
673
663
|
filenames, extensions, and metadata. Standalone `SkyDepthTransform(...)` usage keeps the
|
|
@@ -178,7 +178,14 @@ def _run_transform(args: argparse.Namespace, transform_class: type) -> int:
|
|
|
178
178
|
|
|
179
179
|
required_modalities = transform_class.REQUIRED_MODALITIES
|
|
180
180
|
required_hierarchical = transform_class.REQUIRED_HIERARCHICAL_MODALITIES or None
|
|
181
|
-
|
|
181
|
+
# euler-loading logs which loader (and sky definition) each modality uses.
|
|
182
|
+
get_logger("euler_loading")
|
|
183
|
+
dataset = build_dataset(
|
|
184
|
+
config,
|
|
185
|
+
required_modalities,
|
|
186
|
+
required_hierarchical,
|
|
187
|
+
transform_class.MODALITY_FUNCTIONS,
|
|
188
|
+
)
|
|
182
189
|
output_backends = prepare_output_backends(config, dataset, transform_class)
|
|
183
190
|
primary_backend = next(iter(output_backends.values()))
|
|
184
191
|
samples = _select_configured_samples(config, dataset, logger)
|
|
@@ -5,26 +5,27 @@ from collections import Counter
|
|
|
5
5
|
from euler_preprocess.common.logging import get_logger
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def _make_modality(entry: str | dict):
|
|
8
|
+
def _make_modality(entry: str | dict, function: str | None = None):
|
|
9
|
+
"""Build a ``Modality``; an entry's own ``function`` replaces *function*."""
|
|
9
10
|
from euler_loading import Modality
|
|
10
11
|
|
|
11
12
|
if isinstance(entry, str):
|
|
12
|
-
return Modality(entry)
|
|
13
|
-
|
|
14
|
-
entry[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
13
|
+
return Modality(entry, function=function)
|
|
14
|
+
options = {
|
|
15
|
+
key: entry[key]
|
|
16
|
+
for key in (
|
|
17
|
+
"split",
|
|
18
|
+
"metadata_scope",
|
|
19
|
+
"collapse_single",
|
|
20
|
+
"cache",
|
|
21
|
+
"applies_to",
|
|
22
|
+
"hierarchy_scope",
|
|
23
|
+
"function",
|
|
24
|
+
)
|
|
25
|
+
if key in entry
|
|
26
|
+
}
|
|
27
|
+
options.setdefault("function", function)
|
|
28
|
+
return Modality(entry["path"], **options)
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def _load_index_tree(name: str, modality) -> dict:
|
|
@@ -151,6 +152,7 @@ def build_dataset(
|
|
|
151
152
|
config: dict,
|
|
152
153
|
required_modalities: set[str],
|
|
153
154
|
required_hierarchical: set[str] | None = None,
|
|
155
|
+
modality_functions: dict[str, str] | None = None,
|
|
154
156
|
):
|
|
155
157
|
"""Build a ``MultiModalDataset`` from a config dict.
|
|
156
158
|
|
|
@@ -169,6 +171,12 @@ def build_dataset(
|
|
|
169
171
|
same way. *required_modalities* are per-sample inputs and anchor that
|
|
170
172
|
inference; *required_hierarchical* names further required modalities,
|
|
171
173
|
whose role is inferred like any other.
|
|
174
|
+
|
|
175
|
+
*modality_functions* maps a modality to the loader function to use from
|
|
176
|
+
the module its dataset head names (a transform's ``MODALITY_FUNCTIONS``),
|
|
177
|
+
e.g. ``sky_mask`` for a ``class_segmentation`` index; the sky then comes
|
|
178
|
+
from the head's ``skyclass``. A modality entry's own ``function`` key
|
|
179
|
+
overrides it, and ``"function": null`` keeps the head's function.
|
|
172
180
|
"""
|
|
173
181
|
from euler_loading import MultiModalDataset, SamplePreprocessor
|
|
174
182
|
|
|
@@ -191,7 +199,11 @@ def build_dataset(
|
|
|
191
199
|
f"'modalities' must contain at least: {', '.join(sorted(required))}"
|
|
192
200
|
)
|
|
193
201
|
|
|
194
|
-
|
|
202
|
+
functions = modality_functions or {}
|
|
203
|
+
modalities = {
|
|
204
|
+
name: _make_modality(entry, functions.get(name))
|
|
205
|
+
for name, entry in configured.items()
|
|
206
|
+
}
|
|
195
207
|
regular, hierarchical = _resolve_roles(modalities, set(required_modalities))
|
|
196
208
|
logger = get_logger()
|
|
197
209
|
for name in regular:
|
|
@@ -98,7 +98,9 @@ def normalize_sky_mask(sky_mask) -> np.ndarray:
|
|
|
98
98
|
"""Normalise a sky mask to a 2-D boolean numpy array ``(H, W)``.
|
|
99
99
|
|
|
100
100
|
Handles torch tensors (GPU loaders) and the ``(1, H, W)`` channel-first
|
|
101
|
-
layout produced by euler-loading's GPU loaders.
|
|
101
|
+
layout produced by euler-loading's GPU loaders. A non-boolean input may
|
|
102
|
+
only hold 0/1: casting class IDs to bool would mark every non-zero class
|
|
103
|
+
as sky.
|
|
102
104
|
"""
|
|
103
105
|
mask = _to_numpy(sky_mask)
|
|
104
106
|
# CHW → take first channel: (C, H, W) → (H, W)
|
|
@@ -109,4 +111,15 @@ def normalize_sky_mask(sky_mask) -> np.ndarray:
|
|
|
109
111
|
mask = mask[0]
|
|
110
112
|
if mask.ndim == 3 and mask.shape[-1] == 1:
|
|
111
113
|
mask = mask[..., 0]
|
|
114
|
+
if mask.dtype != np.bool_ and np.any((mask != 0) & (mask != 1)):
|
|
115
|
+
values = np.unique(mask)
|
|
116
|
+
shown = ", ".join(str(value) for value in values[:8])
|
|
117
|
+
if values.size > 8:
|
|
118
|
+
shown += ", ..."
|
|
119
|
+
raise ValueError(
|
|
120
|
+
"semantic_segmentation must be a boolean sky mask, got a "
|
|
121
|
+
f"{mask.dtype} array with values [{shown}]. Load it with the "
|
|
122
|
+
"dataset's euler-loading sky_mask loader (the CLI selects it "
|
|
123
|
+
"automatically) or pass a boolean mask."
|
|
124
|
+
)
|
|
112
125
|
return mask.astype(bool)
|
|
@@ -20,6 +20,10 @@ class Transform(ABC):
|
|
|
20
20
|
store per sample or once per hierarchy level; their role is inferred
|
|
21
21
|
from the ds-crawler indexes (see
|
|
22
22
|
:func:`~euler_preprocess.common.dataset.build_dataset`).
|
|
23
|
+
:attr:`MODALITY_FUNCTIONS` maps a modality to the euler-loading loader
|
|
24
|
+
function the transform needs, taken from the loader module its
|
|
25
|
+
dataset head names (``{"semantic_segmentation": "sky_mask"}`` reads a
|
|
26
|
+
class-segmentation index as sky masks).
|
|
23
27
|
|
|
24
28
|
Output slots:
|
|
25
29
|
Most transforms produce a single output (the *primary* slot, declared
|
|
@@ -32,6 +36,7 @@ class Transform(ABC):
|
|
|
32
36
|
|
|
33
37
|
REQUIRED_MODALITIES: ClassVar[set[str]] = set()
|
|
34
38
|
REQUIRED_HIERARCHICAL_MODALITIES: ClassVar[set[str]] = set()
|
|
39
|
+
MODALITY_FUNCTIONS: ClassVar[dict[str, str]] = {}
|
|
35
40
|
SOURCE_MODALITY: ClassVar[str | None] = None
|
|
36
41
|
OUTPUT_SLOT: ClassVar[str | None] = None
|
|
37
42
|
OUTPUT_SLOTS: ClassVar[tuple[str, ...]] = ()
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/atmospheric_light.py
RENAMED
|
@@ -124,7 +124,7 @@ class AtmosphericLightResolver:
|
|
|
124
124
|
airlight_is_estimated = uses_estimated_airlight(al_spec)
|
|
125
125
|
if airlight_is_estimated:
|
|
126
126
|
raw_ls_base = normalize_atmospheric_light_torch(
|
|
127
|
-
estimated_airlight_t
|
|
127
|
+
estimated_airlight_t, allow_8bit=False
|
|
128
128
|
).squeeze(0)
|
|
129
129
|
else:
|
|
130
130
|
sampled_al = sample_value(al_spec, rng)
|
|
@@ -156,7 +156,7 @@ class AtmosphericLightResolver:
|
|
|
156
156
|
airlight_is_estimated = uses_estimated_airlight(al_spec)
|
|
157
157
|
if airlight_is_estimated:
|
|
158
158
|
ls_base = self._estimate_batch_torch(rgb_batch, items, device, method)
|
|
159
|
-
ls_base = normalize_atmospheric_light_torch(ls_base)
|
|
159
|
+
ls_base = normalize_atmospheric_light_torch(ls_base, allow_8bit=False)
|
|
160
160
|
else:
|
|
161
161
|
ls_values = []
|
|
162
162
|
for item in items:
|
|
@@ -58,7 +58,7 @@ def render_fog_image(
|
|
|
58
58
|
mode: FogInferenceMode = "cpu",
|
|
59
59
|
sample_id: str | None = None,
|
|
60
60
|
sample_index: int = 0,
|
|
61
|
-
sky_class: Sequence[int | float] | None =
|
|
61
|
+
sky_class: Sequence[int | float] | None = None,
|
|
62
62
|
) -> np.ndarray:
|
|
63
63
|
"""Render and return only the RGB image for one in-memory sample."""
|
|
64
64
|
|
|
@@ -89,14 +89,15 @@ def render_fog_sample(
|
|
|
89
89
|
mode: FogInferenceMode = "cpu",
|
|
90
90
|
sample_id: str | None = None,
|
|
91
91
|
sample_index: int = 0,
|
|
92
|
-
sky_class: Sequence[int | float] | None =
|
|
92
|
+
sky_class: Sequence[int | float] | None = None,
|
|
93
93
|
) -> FogInferenceResult:
|
|
94
94
|
"""Render fog/camera artifacts for a single in-memory sample.
|
|
95
95
|
|
|
96
96
|
The call accepts the four modalities used by :class:`FogTransform` without
|
|
97
97
|
requiring a dataset reader or output backend. ``semantic_segmentation`` may
|
|
98
98
|
be either a boolean sky mask or an RGB semantic label map; RGB maps are
|
|
99
|
-
converted using ``sky_class
|
|
99
|
+
converted using ``sky_class``, the map's sky colour as its dataset head
|
|
100
|
+
declares it in ``skyclass`` (e.g. ``[29, 0, 0]`` for Real Drive Sim).
|
|
100
101
|
"""
|
|
101
102
|
|
|
102
103
|
if mode not in ("cpu", "gpu"):
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from typing import Any
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def log_config(
|
|
@@ -18,7 +20,14 @@ def log_config(
|
|
|
18
20
|
resize_depth: bool,
|
|
19
21
|
seed,
|
|
20
22
|
contrast_threshold_default: float,
|
|
23
|
+
scenarios: Sequence[tuple[str, Any, Any]] = (),
|
|
24
|
+
scenario_scope: str = "sample",
|
|
21
25
|
) -> None:
|
|
26
|
+
"""Log the resolved run settings.
|
|
27
|
+
|
|
28
|
+
*scenarios* holds ``(name, weight, model)`` per scenario profile, with
|
|
29
|
+
``model`` ``None`` when the profile defers to ``selection``.
|
|
30
|
+
"""
|
|
22
31
|
logger.info("Loaded config from %s -> output %s", config_path, out_path)
|
|
23
32
|
logger.info(
|
|
24
33
|
"Device: requested=%s active=%s use_gpu=%s torch=%s",
|
|
@@ -36,6 +45,22 @@ def log_config(
|
|
|
36
45
|
seed,
|
|
37
46
|
contrast_threshold_default,
|
|
38
47
|
)
|
|
48
|
+
if scenarios:
|
|
49
|
+
logger.info(
|
|
50
|
+
"Model selection: %d scenario_profiles (scope=%s) choose the model; "
|
|
51
|
+
"'selection' only applies to profiles without one",
|
|
52
|
+
len(scenarios),
|
|
53
|
+
scenario_scope,
|
|
54
|
+
)
|
|
55
|
+
for name, weight, model in scenarios:
|
|
56
|
+
logger.info(
|
|
57
|
+
"Scenario %s: weight=%s model=%s",
|
|
58
|
+
name,
|
|
59
|
+
weight,
|
|
60
|
+
"<selection>" if model is None else model,
|
|
61
|
+
)
|
|
62
|
+
if all(model is not None for _name, _weight, model in scenarios):
|
|
63
|
+
return
|
|
39
64
|
selection = config.get("selection")
|
|
40
65
|
if selection:
|
|
41
66
|
mode = selection.get("mode", "fixed")
|
|
@@ -160,18 +160,34 @@ def resolve_scattering_coefficient(
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def normalize_atmospheric_light(
|
|
163
|
+
def normalize_atmospheric_light(
|
|
164
|
+
value: np.ndarray, *, allow_8bit: bool = True
|
|
165
|
+
) -> np.ndarray:
|
|
166
|
+
"""Return ``value`` as an RGB triple clipped to ``[0, 1]``.
|
|
167
|
+
|
|
168
|
+
With *allow_8bit*, values above 1 are read as an 8-bit colour and divided
|
|
169
|
+
by 255. That only suits configured colours: estimated airlight comes from
|
|
170
|
+
``[0, 1]`` RGB and may overshoot 1 slightly after the estimators' luminance
|
|
171
|
+
rescaling, so estimate callers pass ``allow_8bit=False`` to clip instead.
|
|
172
|
+
"""
|
|
164
173
|
value = np.asarray(value, dtype=np.float32)
|
|
165
174
|
if value.ndim == 0:
|
|
166
175
|
value = np.array([value, value, value], dtype=np.float32)
|
|
167
176
|
if value.ndim != 1 or value.shape[0] != 3:
|
|
168
177
|
raise ValueError("atmospheric_light must be scalar or length-3 list")
|
|
169
|
-
if value.max() > 1.0:
|
|
178
|
+
if allow_8bit and value.max() > 1.0:
|
|
170
179
|
value = value / 255.0
|
|
171
180
|
return np.clip(value, 0.0, 1.0)
|
|
172
181
|
|
|
173
182
|
|
|
174
|
-
def normalize_atmospheric_light_torch(
|
|
183
|
+
def normalize_atmospheric_light_torch(
|
|
184
|
+
value: "torch.Tensor", *, allow_8bit: bool = True
|
|
185
|
+
) -> "torch.Tensor":
|
|
186
|
+
"""Torch counterpart of :func:`normalize_atmospheric_light`.
|
|
187
|
+
|
|
188
|
+
The 8-bit check uses the maximum over every row, so stacked estimates must
|
|
189
|
+
pass ``allow_8bit=False`` or one overshooting sample rescales the batch.
|
|
190
|
+
"""
|
|
175
191
|
value_t = value.to(torch.float32)
|
|
176
192
|
if value_t.ndim == 0:
|
|
177
193
|
value_t = value_t.repeat(3)
|
|
@@ -180,7 +196,7 @@ def normalize_atmospheric_light_torch(value: "torch.Tensor") -> "torch.Tensor":
|
|
|
180
196
|
if value_t.ndim != 2 or value_t.shape[-1] != 3:
|
|
181
197
|
raise ValueError("atmospheric_light must be scalar or length-3 list")
|
|
182
198
|
max_val = float(value_t.max().item()) if value_t.numel() else 0.0
|
|
183
|
-
if max_val > 1.0:
|
|
199
|
+
if allow_8bit and max_val > 1.0:
|
|
184
200
|
value_t = value_t / 255.0
|
|
185
201
|
return torch.clamp(value_t, 0.0, 1.0)
|
|
186
202
|
|
|
@@ -1653,7 +1669,9 @@ def apply_model(
|
|
|
1653
1669
|
al_spec = model_cfg.get("atmospheric_light", "from_sky")
|
|
1654
1670
|
airlight_is_estimated = uses_estimated_airlight(al_spec)
|
|
1655
1671
|
if airlight_is_estimated:
|
|
1656
|
-
raw_ls_base = normalize_atmospheric_light(
|
|
1672
|
+
raw_ls_base = normalize_atmospheric_light(
|
|
1673
|
+
estimated_airlight, allow_8bit=False
|
|
1674
|
+
)
|
|
1657
1675
|
else:
|
|
1658
1676
|
sampled_al = sample_value(al_spec, rng)
|
|
1659
1677
|
raw_ls_base = normalize_atmospheric_light(np.asarray(sampled_al))
|
|
@@ -301,6 +301,7 @@ class FogTransform(Transform):
|
|
|
301
301
|
|
|
302
302
|
REQUIRED_MODALITIES: ClassVar[set[str]] = {"rgb", "depth", "semantic_segmentation"}
|
|
303
303
|
REQUIRED_HIERARCHICAL_MODALITIES: ClassVar[set[str]] = set()
|
|
304
|
+
MODALITY_FUNCTIONS: ClassVar[dict[str, str]] = {"semantic_segmentation": "sky_mask"}
|
|
304
305
|
SOURCE_MODALITY: ClassVar[str] = "rgb"
|
|
305
306
|
OUTPUT_SLOT: ClassVar[str] = "rgb"
|
|
306
307
|
OUTPUT_SLOTS: ClassVar[tuple[str, ...]] = (
|
|
@@ -399,6 +400,8 @@ class FogTransform(Transform):
|
|
|
399
400
|
self.resize_depth_flag,
|
|
400
401
|
self.seed,
|
|
401
402
|
self.contrast_threshold_default,
|
|
403
|
+
scenarios=self._scenario_log_entries(),
|
|
404
|
+
scenario_scope=self._scenario_scope(),
|
|
402
405
|
)
|
|
403
406
|
|
|
404
407
|
airlight_method = self.config.get("airlight")
|
|
@@ -664,6 +667,42 @@ class FogTransform(Transform):
|
|
|
664
667
|
name = config.get("name", config.get("id"))
|
|
665
668
|
return None if name is None else str(name)
|
|
666
669
|
|
|
670
|
+
def _scenario_log_entries(self) -> list[tuple[str, Any, Any]]:
|
|
671
|
+
"""Return ``(name, weight, model)`` per scenario profile for the config log."""
|
|
672
|
+
entries: list[tuple[str, Any, Any]] = []
|
|
673
|
+
for index, profile in enumerate(self.scenario_profiles):
|
|
674
|
+
payload = self._scenario_payload(profile)
|
|
675
|
+
model = next(
|
|
676
|
+
(
|
|
677
|
+
payload[key]
|
|
678
|
+
for key in ("model", "model_name", "fog_model")
|
|
679
|
+
if key in payload
|
|
680
|
+
),
|
|
681
|
+
None,
|
|
682
|
+
)
|
|
683
|
+
weight = profile.get(
|
|
684
|
+
"weight",
|
|
685
|
+
profile.get("profile_weight", profile.get("probability", 1.0)),
|
|
686
|
+
)
|
|
687
|
+
name = self._scenario_name(profile) or f"scenario_{index:03d}"
|
|
688
|
+
entries.append((name, weight, model))
|
|
689
|
+
return entries
|
|
690
|
+
|
|
691
|
+
def _scenario_scope(self) -> str:
|
|
692
|
+
"""Describe how often a scenario profile is drawn, for the config log."""
|
|
693
|
+
if self.mode == "progressive":
|
|
694
|
+
return "progressive"
|
|
695
|
+
if (
|
|
696
|
+
self.use_gpu
|
|
697
|
+
and not self.augmentation_specs
|
|
698
|
+
and self.gpu_scenario_scope == "batch"
|
|
699
|
+
):
|
|
700
|
+
return (
|
|
701
|
+
"batch, condition parameters per "
|
|
702
|
+
f"{self.gpu_condition_parameter_scope}"
|
|
703
|
+
)
|
|
704
|
+
return "sample"
|
|
705
|
+
|
|
667
706
|
def _scenario_config_override(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
668
707
|
return {
|
|
669
708
|
key: value
|
|
@@ -31,6 +31,7 @@ class SkyDepthTransform(Transform):
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
REQUIRED_MODALITIES: ClassVar[set[str]] = {"depth", "semantic_segmentation"}
|
|
34
|
+
MODALITY_FUNCTIONS: ClassVar[dict[str, str]] = {"semantic_segmentation": "sky_mask"}
|
|
34
35
|
SOURCE_MODALITY: ClassVar[str] = "depth"
|
|
35
36
|
OUTPUT_SLOT: ClassVar[str] = "depth"
|
|
36
37
|
|
|
@@ -257,8 +258,7 @@ class SkyDepthTransform(Transform):
|
|
|
257
258
|
log.warning(
|
|
258
259
|
" SANITY CHECK: %.2f%% of pixels are marked as sky in sample "
|
|
259
260
|
"'%s'. That is suspiciously high — check that the mask is not "
|
|
260
|
-
"inverted
|
|
261
|
-
"every non-zero class id as sky.",
|
|
261
|
+
"inverted and that the dataset head's `skyclass` names the sky.",
|
|
262
262
|
pct,
|
|
263
263
|
sample_id,
|
|
264
264
|
)
|
euler_preprocess-3.16.0/README.md → euler_preprocess-3.17.0/euler_preprocess.egg-info/PKG-INFO
RENAMED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: euler-preprocess
|
|
3
|
+
Version: 3.17.0
|
|
4
|
+
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
|
+
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: Pillow
|
|
12
|
+
Requires-Dist: euler-loading>=2.29.0
|
|
13
|
+
Requires-Dist: ds-crawler>=2.11.0
|
|
14
|
+
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
15
|
+
Provides-Extra: gpu
|
|
16
|
+
Requires-Dist: torch; extra == "gpu"
|
|
17
|
+
Provides-Extra: progress
|
|
18
|
+
Requires-Dist: tqdm; extra == "progress"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
1
21
|
<!-- euler header — shared across the euler packages.
|
|
2
22
|
Per package, change only: the <h1>, the tagline, and the badge URLs. -->
|
|
3
23
|
<p align="center">
|
|
@@ -99,6 +119,15 @@ loader.
|
|
|
99
119
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
100
120
|
| `radial` | `depth`, `intrinsics` | — |
|
|
101
121
|
|
|
122
|
+
`fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
|
|
123
|
+
it with the `sky_mask` function of the euler-loading module that the index's
|
|
124
|
+
dataset head names, so a `class_segmentation` index works as is: the sky is the
|
|
125
|
+
head's `skyclass` (Synscapes `[23, 23, 23]`, Real Drive Sim `[29, 0, 0]`, VKITTI2
|
|
126
|
+
`[90, 200, 255]`), and the log names the loader and the sky definition in use.
|
|
127
|
+
Set `"function"` in the modality entry to use another loader function, or
|
|
128
|
+
`"function": null` for the one the head declares. A non-binary mask raises an
|
|
129
|
+
error rather than marking every non-zero class as sky.
|
|
130
|
+
|
|
102
131
|
**Per-sample vs. hierarchical modalities.** You don't declare a modality's role:
|
|
103
132
|
it is inferred from the ds-crawler indexes. A modality whose qualified IDs
|
|
104
133
|
(hierarchy path plus file ID) match the transform's per-sample inputs is joined per
|
|
@@ -647,7 +676,8 @@ where sky depth is encoded as zero or infinity and needs a large finite value.
|
|
|
647
676
|
}
|
|
648
677
|
```
|
|
649
678
|
|
|
650
|
-
`sky_depth_value` defaults to `1000.0`.
|
|
679
|
+
`sky_depth_value` defaults to `1000.0`. The sky mask comes from the dataset head as
|
|
680
|
+
described under [Dataset Config](#dataset-config).
|
|
651
681
|
|
|
652
682
|
CLI runs write a source-backed depth dataset mirroring the input depth modality's paths,
|
|
653
683
|
filenames, extensions, and metadata. Standalone `SkyDepthTransform(...)` usage keeps the
|
|
@@ -51,6 +51,7 @@ tests/test_dense_gloomy_daylight_config.py
|
|
|
51
51
|
tests/test_fog_aux_outputs.py
|
|
52
52
|
tests/test_fog_aware_auto_exposure.py
|
|
53
53
|
tests/test_fog_cpu_gpu_parity.py
|
|
54
|
+
tests/test_fog_input_normalization.py
|
|
54
55
|
tests/test_fog_integration.py
|
|
55
56
|
tests/test_modality_roles.py
|
|
56
57
|
tests/test_radial.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "euler-preprocess"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.17.0"
|
|
8
8
|
description = "Physics-based preprocessing (fog, etc.) for RGB+depth datasets"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -16,7 +16,7 @@ authors = [
|
|
|
16
16
|
dependencies = [
|
|
17
17
|
"numpy",
|
|
18
18
|
"Pillow",
|
|
19
|
-
"euler-loading>=2.
|
|
19
|
+
"euler-loading>=2.29.0",
|
|
20
20
|
"ds-crawler>=2.11.0",
|
|
21
21
|
"euler-dataset-contract>=0.5.0",
|
|
22
22
|
]
|
|
@@ -1753,6 +1753,7 @@ def test_render_fog_sample_accepts_modalities_and_named_scenario() -> None:
|
|
|
1753
1753
|
config=config,
|
|
1754
1754
|
scenario_profile_name="dark_reference",
|
|
1755
1755
|
mode="cpu",
|
|
1756
|
+
sky_class=(29, 0, 0),
|
|
1756
1757
|
)
|
|
1757
1758
|
image = render_fog_image(
|
|
1758
1759
|
rgb=rgb,
|
|
@@ -1767,6 +1768,7 @@ def test_render_fog_sample_accepts_modalities_and_named_scenario() -> None:
|
|
|
1767
1768
|
config=config,
|
|
1768
1769
|
scenario_profile_name="dark_reference",
|
|
1769
1770
|
mode="cpu",
|
|
1771
|
+
sky_class=(29, 0, 0),
|
|
1770
1772
|
)
|
|
1771
1773
|
|
|
1772
1774
|
assert result.scenario_name == "dark_reference"
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Regression tests for sky-mask and estimated-airlight normalisation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
from ds_crawler import DatasetWriter
|
|
8
|
+
from euler_loading.loaders.cpu import real_drive_sim as rds
|
|
9
|
+
from euler_loading.loaders.cpu.generic_dense_depth import write_depth
|
|
10
|
+
|
|
11
|
+
from euler_preprocess.common.dataset import build_dataset
|
|
12
|
+
from euler_preprocess.common.logging import get_logger
|
|
13
|
+
from euler_preprocess.common.normalize import normalize_sky_mask
|
|
14
|
+
from euler_preprocess.fog.atmospheric_light import AtmosphericLightResolver
|
|
15
|
+
from euler_preprocess.fog.models import (
|
|
16
|
+
apply_model,
|
|
17
|
+
normalize_atmospheric_light,
|
|
18
|
+
resolve_model_config,
|
|
19
|
+
)
|
|
20
|
+
from euler_preprocess.fog.transform import FogTransform
|
|
21
|
+
from euler_preprocess.sky_depth.transform import SkyDepthTransform
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import torch
|
|
25
|
+
except ImportError:
|
|
26
|
+
torch = None
|
|
27
|
+
|
|
28
|
+
SKY = 23
|
|
29
|
+
# Cityscapes-style label map: every pixel has a non-zero class ID.
|
|
30
|
+
LABELS = np.array(
|
|
31
|
+
[
|
|
32
|
+
[SKY, SKY, SKY, SKY],
|
|
33
|
+
[SKY, 7, 7, 11],
|
|
34
|
+
[7, 7, 11, 11],
|
|
35
|
+
[7, 7, 11, 11],
|
|
36
|
+
],
|
|
37
|
+
dtype=np.int64,
|
|
38
|
+
)
|
|
39
|
+
UNIFORM_CFG = {
|
|
40
|
+
"visibility_m": 100.0,
|
|
41
|
+
"atmospheric_light": "from_sky",
|
|
42
|
+
"airlight_dampening": False,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestNormalizeSkyMask:
|
|
47
|
+
def test_class_map_raises(self):
|
|
48
|
+
with pytest.raises(ValueError, match="sky_mask loader") as excinfo:
|
|
49
|
+
normalize_sky_mask(LABELS[None])
|
|
50
|
+
assert "7, 11, 23" in str(excinfo.value)
|
|
51
|
+
|
|
52
|
+
def test_rgb_class_map_raises(self):
|
|
53
|
+
# VKITTI2 GPU layout (3, H, W); vegetation (90, 240, 0) shares sky's red.
|
|
54
|
+
colours = np.zeros((3, 6, 6), dtype=np.int64)
|
|
55
|
+
colours[:, :2] = np.array([90, 200, 255])[:, None, None]
|
|
56
|
+
colours[:, 2:] = np.array([90, 240, 0])[:, None, None]
|
|
57
|
+
with pytest.raises(ValueError, match="boolean sky mask"):
|
|
58
|
+
normalize_sky_mask(colours)
|
|
59
|
+
|
|
60
|
+
def test_boolean_and_binary_masks_pass(self):
|
|
61
|
+
mask = LABELS == SKY
|
|
62
|
+
np.testing.assert_array_equal(normalize_sky_mask(mask), mask)
|
|
63
|
+
binary = mask.astype(np.uint8)[..., None]
|
|
64
|
+
np.testing.assert_array_equal(normalize_sky_mask(binary), mask)
|
|
65
|
+
|
|
66
|
+
@pytest.mark.skipif(torch is None, reason="torch not installed")
|
|
67
|
+
def test_gpu_loader_mask_passes(self):
|
|
68
|
+
mask = torch.from_numpy(LABELS == SKY)[None]
|
|
69
|
+
np.testing.assert_array_equal(normalize_sky_mask(mask), LABELS == SKY)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_fog_transform_rejects_class_maps(tmp_path):
|
|
73
|
+
cfg = {
|
|
74
|
+
"airlight": "from_sky",
|
|
75
|
+
"device": "cpu",
|
|
76
|
+
"models": {"uniform": UNIFORM_CFG},
|
|
77
|
+
"selection": {"mode": "fixed", "model": "uniform"},
|
|
78
|
+
}
|
|
79
|
+
path = tmp_path / "fog.json"
|
|
80
|
+
path.write_text(json.dumps(cfg))
|
|
81
|
+
transform = FogTransform(config_path=str(path), out_path=str(tmp_path / "out"))
|
|
82
|
+
sample = {
|
|
83
|
+
"rgb": np.full(LABELS.shape + (3,), 0.5, dtype=np.float32),
|
|
84
|
+
"depth": np.full(LABELS.shape, 20.0, dtype=np.float32),
|
|
85
|
+
"semantic_segmentation": LABELS,
|
|
86
|
+
"id": "frame",
|
|
87
|
+
}
|
|
88
|
+
with pytest.raises(ValueError, match="sky_mask loader"):
|
|
89
|
+
transform.run([sample])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Real Drive Sim class IDs: 29 is sky, 31 the class a relabelled head declares.
|
|
93
|
+
RDS_CLASSES = np.array([[29, 29, 7], [31, 7, 7]], dtype=np.uint8)
|
|
94
|
+
FRAMES = ("Scene01/Camera_0/0000", "Scene01/Camera_0/0001")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rds_dataset_config(tmp_path, skyclass, segmentation_entry=None) -> dict:
|
|
98
|
+
"""Depth plus an RDS ``class_segmentation`` index whose head declares *skyclass*."""
|
|
99
|
+
depth = DatasetWriter(
|
|
100
|
+
tmp_path / "depth",
|
|
101
|
+
name="depth",
|
|
102
|
+
type="depth",
|
|
103
|
+
separator=None,
|
|
104
|
+
meta={"radial_depth": False, "scale_to_meters": 1.0, "range": [0, 1000]},
|
|
105
|
+
euler_loading={"loader": "generic_dense_depth", "function": "depth"},
|
|
106
|
+
)
|
|
107
|
+
segmentation = DatasetWriter(
|
|
108
|
+
tmp_path / "segmentation",
|
|
109
|
+
name="segmentation",
|
|
110
|
+
type="semantic_segmentation",
|
|
111
|
+
separator=None,
|
|
112
|
+
meta={"skyclass": skyclass},
|
|
113
|
+
euler_loading={"loader": "real_drive_sim", "function": "class_segmentation"},
|
|
114
|
+
)
|
|
115
|
+
for full_id in FRAMES:
|
|
116
|
+
name = full_id.rsplit("/", 1)[-1]
|
|
117
|
+
write_depth(str(depth.get_path(full_id, f"{name}.npy")), np.full((2, 3), 5.0, dtype=np.float32))
|
|
118
|
+
rds.write_class_segmentation(str(segmentation.get_path(full_id, f"{name}.png")), RDS_CLASSES)
|
|
119
|
+
depth.save_index()
|
|
120
|
+
segmentation.save_index()
|
|
121
|
+
entry = str(tmp_path / "segmentation")
|
|
122
|
+
if segmentation_entry is not None:
|
|
123
|
+
entry = {"path": entry, **segmentation_entry}
|
|
124
|
+
return {
|
|
125
|
+
"modalities": {"depth": str(tmp_path / "depth"), "semantic_segmentation": entry}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _run_sky_depth(tmp_path, config) -> np.ndarray:
|
|
130
|
+
dataset = build_dataset(
|
|
131
|
+
config,
|
|
132
|
+
SkyDepthTransform.REQUIRED_MODALITIES,
|
|
133
|
+
None,
|
|
134
|
+
SkyDepthTransform.MODALITY_FUNCTIONS,
|
|
135
|
+
)
|
|
136
|
+
cfg_path = tmp_path / "sky_depth.json"
|
|
137
|
+
cfg_path.write_text(json.dumps({"sky_depth_value": 900.0, "sanity_check_samples": 0}))
|
|
138
|
+
transform = SkyDepthTransform(str(cfg_path), str(tmp_path / "out"))
|
|
139
|
+
return np.load(transform.run(dataset)[0])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestSkyMaskFromDatasetHead:
|
|
143
|
+
"""The transforms read class-segmentation indexes through euler-loading's ``sky_mask``."""
|
|
144
|
+
|
|
145
|
+
def test_sky_comes_from_the_head_skyclass(self, tmp_path):
|
|
146
|
+
config = _rds_dataset_config(tmp_path, [29, 0, 0])
|
|
147
|
+
np.testing.assert_array_equal(
|
|
148
|
+
_run_sky_depth(tmp_path, config), np.where(RDS_CLASSES == 29, 900.0, 5.0)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def test_relabelled_head_moves_the_sky(self, tmp_path):
|
|
152
|
+
config = _rds_dataset_config(tmp_path, [31, 0, 0])
|
|
153
|
+
np.testing.assert_array_equal(
|
|
154
|
+
_run_sky_depth(tmp_path, config), np.where(RDS_CLASSES == 31, 900.0, 5.0)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def test_function_null_keeps_the_class_map_and_fails_loudly(self, tmp_path):
|
|
158
|
+
config = _rds_dataset_config(tmp_path, [29, 0, 0], {"function": None})
|
|
159
|
+
with pytest.raises(ValueError, match="boolean sky mask"):
|
|
160
|
+
_run_sky_depth(tmp_path, config)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TestEstimatedAirlightRange:
|
|
164
|
+
# A DCP-heuristic estimate whose luminance rescale overshoots one channel.
|
|
165
|
+
ESTIMATE = [0.9, 1.03, 0.98]
|
|
166
|
+
CLIPPED = [0.9, 1.0, 0.98]
|
|
167
|
+
|
|
168
|
+
def test_estimate_overshoot_is_clipped_not_rescaled(self):
|
|
169
|
+
result = normalize_atmospheric_light(np.array(self.ESTIMATE), allow_8bit=False)
|
|
170
|
+
np.testing.assert_allclose(result, self.CLIPPED, rtol=1e-6)
|
|
171
|
+
|
|
172
|
+
def test_configured_8bit_colour_is_still_rescaled(self):
|
|
173
|
+
result = normalize_atmospheric_light(np.array([200, 210, 220]))
|
|
174
|
+
np.testing.assert_allclose(result, np.array([200, 210, 220]) / 255.0, rtol=1e-6)
|
|
175
|
+
|
|
176
|
+
def test_cpu_model_keeps_overshooting_estimate(self):
|
|
177
|
+
_foggy, _beta, ls_base, _k_map, _ls_map = apply_model(
|
|
178
|
+
np.full((4, 4, 3), 0.5, dtype=np.float32),
|
|
179
|
+
np.full((4, 4), 20.0, dtype=np.float32),
|
|
180
|
+
"uniform",
|
|
181
|
+
resolve_model_config("uniform", {"uniform": UNIFORM_CFG}),
|
|
182
|
+
np.random.default_rng(0),
|
|
183
|
+
0.05,
|
|
184
|
+
np.array(self.ESTIMATE, dtype=np.float32),
|
|
185
|
+
)
|
|
186
|
+
np.testing.assert_allclose(ls_base, self.CLIPPED, rtol=1e-6)
|
|
187
|
+
|
|
188
|
+
@pytest.mark.skipif(torch is None, reason="torch not installed")
|
|
189
|
+
def test_torch_sample_keeps_overshooting_estimate(self):
|
|
190
|
+
_k_mean, ls_base = AtmosphericLightResolver("from_sky").resolve_model_torch(
|
|
191
|
+
model_cfg=resolve_model_config("uniform", {"uniform": UNIFORM_CFG}),
|
|
192
|
+
rng=np.random.default_rng(0),
|
|
193
|
+
contrast_threshold_default=0.05,
|
|
194
|
+
estimated_airlight_t=torch.tensor(self.ESTIMATE),
|
|
195
|
+
device=torch.device("cpu"),
|
|
196
|
+
)
|
|
197
|
+
torch.testing.assert_close(ls_base, torch.tensor(self.CLIPPED))
|
|
198
|
+
|
|
199
|
+
@pytest.mark.skipif(torch is None, reason="torch not installed")
|
|
200
|
+
def test_torch_batch_overshoot_leaves_other_samples_unchanged(self, monkeypatch):
|
|
201
|
+
resolver = AtmosphericLightResolver("from_sky")
|
|
202
|
+
estimates = torch.tensor([[0.5, 0.6, 0.7], self.ESTIMATE])
|
|
203
|
+
monkeypatch.setattr(resolver, "_estimate_batch_torch", lambda *a, **k: estimates)
|
|
204
|
+
model_cfg = resolve_model_config("uniform", {"uniform": UNIFORM_CFG})
|
|
205
|
+
items = [
|
|
206
|
+
{"model_cfg": model_cfg, "rng": np.random.default_rng(index)}
|
|
207
|
+
for index in range(2)
|
|
208
|
+
]
|
|
209
|
+
_k_means, ls_base = resolver.resolve_uniform_batch_torch(
|
|
210
|
+
rgb_batch=torch.zeros(2, 4, 4, 3),
|
|
211
|
+
items=items,
|
|
212
|
+
device=torch.device("cpu"),
|
|
213
|
+
contrast_threshold_default=0.05,
|
|
214
|
+
)
|
|
215
|
+
torch.testing.assert_close(
|
|
216
|
+
ls_base, torch.tensor([[0.5, 0.6, 0.7], self.CLIPPED])
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestScenarioSelectionLog:
|
|
221
|
+
def _log_messages(self, tmp_path, caplog, scenario_profiles):
|
|
222
|
+
cfg = {
|
|
223
|
+
"airlight": "from_sky",
|
|
224
|
+
"device": "cpu",
|
|
225
|
+
"models": {"uniform": UNIFORM_CFG},
|
|
226
|
+
"selection": {"mode": "fixed", "model": "heterogeneous_k_ls"},
|
|
227
|
+
"scenario_profiles": scenario_profiles,
|
|
228
|
+
}
|
|
229
|
+
path = tmp_path / "scenarios.json"
|
|
230
|
+
path.write_text(json.dumps(cfg))
|
|
231
|
+
logger = get_logger()
|
|
232
|
+
logger.addHandler(caplog.handler)
|
|
233
|
+
try:
|
|
234
|
+
FogTransform(config_path=str(path), out_path=str(tmp_path / "out"))
|
|
235
|
+
finally:
|
|
236
|
+
logger.removeHandler(caplog.handler)
|
|
237
|
+
return caplog.messages
|
|
238
|
+
|
|
239
|
+
def test_profiles_with_models_do_not_log_fixed_model(self, tmp_path, caplog):
|
|
240
|
+
messages = self._log_messages(
|
|
241
|
+
tmp_path,
|
|
242
|
+
caplog,
|
|
243
|
+
[
|
|
244
|
+
{"name": "haze", "weight": 0.7, "model": "uniform"},
|
|
245
|
+
{"name": "clear", "weight": 0.3, "model": "uniform", "clear_weather": True},
|
|
246
|
+
],
|
|
247
|
+
)
|
|
248
|
+
assert any("2 scenario_profiles (scope=sample)" in m for m in messages)
|
|
249
|
+
assert "Scenario haze: weight=0.7 model=uniform" in messages
|
|
250
|
+
assert not any("Model fixed" in m for m in messages)
|
|
251
|
+
|
|
252
|
+
def test_profile_without_model_defers_to_selection(self, tmp_path, caplog):
|
|
253
|
+
messages = self._log_messages(
|
|
254
|
+
tmp_path,
|
|
255
|
+
caplog,
|
|
256
|
+
[{"name": "default_model", "weight": 1.0}],
|
|
257
|
+
)
|
|
258
|
+
assert "Scenario default_model: weight=1.0 model=<selection>" in messages
|
|
259
|
+
assert "Model fixed: heterogeneous_k_ls" in messages
|
|
@@ -181,3 +181,14 @@ class TestSkyDepthTransform:
|
|
|
181
181
|
transform = SkyDepthTransform(str(cfg_path), str(out_path), strict=False)
|
|
182
182
|
paths = transform.run(samples)
|
|
183
183
|
assert len(paths) == 2
|
|
184
|
+
|
|
185
|
+
def test_class_map_raises(self, tmp_path, sky_depth_config):
|
|
186
|
+
"""A class-ID map must not be cast to bool (every non-zero class -> sky)."""
|
|
187
|
+
depth = np.ones((4, 4), dtype=np.float32)
|
|
188
|
+
labels = np.full((4, 4), 7, dtype=np.int64)
|
|
189
|
+
labels[0] = 23
|
|
190
|
+
samples = [{"id": "labels", "depth": depth, "semantic_segmentation": labels}]
|
|
191
|
+
|
|
192
|
+
transform = SkyDepthTransform(str(sky_depth_config), str(tmp_path / "output"))
|
|
193
|
+
with pytest.raises(ValueError, match="sky_mask loader"):
|
|
194
|
+
transform.run(samples)
|
|
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
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/airlight_from_sky.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/dcp_airlight_torch.py
RENAMED
|
File without changes
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess/fog/dcp_heuristic_airlight.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/euler_preprocess.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_dense_gloomy_daylight_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.16.0 → euler_preprocess-3.17.0}/tests/test_real_drive_sim_scenario_profiles.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|