euler-preprocess 3.20.0__tar.gz → 3.22.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.20.0 → euler_preprocess-3.22.0}/CHANGELOG.md +21 -0
- {euler_preprocess-3.20.0/euler_preprocess.egg-info → euler_preprocess-3.22.0}/PKG-INFO +32 -10
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/README.md +31 -9
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/docs/plans/coherent-cropping.md +3 -1
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/cli.py +10 -1
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/dataset.py +48 -1
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/transform.py +6 -1
- euler_preprocess-3.22.0/euler_preprocess/fog/airlight_from_sky.py +81 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/atmospheric_light.py +14 -9
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/models.py +16 -6
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/transform.py +122 -29
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0/euler_preprocess.egg-info}/PKG-INFO +32 -10
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/SOURCES.txt +1 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/pyproject.toml +1 -1
- euler_preprocess-3.22.0/tests/test_fog_optional_sky_and_radial_depth.py +442 -0
- euler_preprocess-3.20.0/euler_preprocess/fog/airlight_from_sky.py +0 -49
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/LICENSE +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/MANIFEST.in +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/docs/spatial.md +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/color.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/intrinsics.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/normalize.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/output.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/calibration.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/files.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/label_files.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/roles.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/run.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/spec.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/verify.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/augmentations.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/capture.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/inference.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/logging.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/pipeline.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/lidar_motion/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/lidar_motion/compensation.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/lidar_motion/transform.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/radial/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/radial/transform.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/sky_depth/__init__.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/sky_depth/transform.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/spatial.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/requires.txt +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/examples/spatial-five-field.json +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/setup.cfg +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_cli_sample_selection.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_labels.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_muses.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_roles.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_spec.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_aux_outputs.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_aware_auto_exposure.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_input_normalization.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_integration.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_intrinsics.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_lidar_motion.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_modality_roles.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_radial.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_scene_illumination.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_sensor_identity.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_sky_depth.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_spatial.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_tone_map_lut.py +0 -0
- {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_zip_output.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- `fog` no longer requires `semantic_segmentation`. Without a sky mask no pixel
|
|
6
|
+
is sky: `from_sky` airlight falls back to white, sky-only options have no
|
|
7
|
+
effect, and one warning per run says so. A configured mask is joined and
|
|
8
|
+
used exactly as before. For indoor datasets such as Hypersim.
|
|
9
|
+
- The white `from_sky` fallback for samples without sky pixels is logged as a
|
|
10
|
+
warning once per run, then at debug level, instead of once per sample and
|
|
11
|
+
fog variant.
|
|
12
|
+
- `fog` reads the depth head's `radial_depth` (the CLI logs what it found):
|
|
13
|
+
- `true`: the depth is used as the distance along the ray, and intrinsics
|
|
14
|
+
never convert it again (before, a radial depth plus intrinsics was
|
|
15
|
+
converted twice, up to 1.9× too far for Hypersim's tilt-shift cameras).
|
|
16
|
+
- `false` without intrinsics: an error, instead of silently using planar
|
|
17
|
+
depth as distance. `"planar_depth_as_distance": true` in the fog config
|
|
18
|
+
accepts it with one warning.
|
|
19
|
+
- Undeclared: unchanged (converted when intrinsics are present).
|
|
20
|
+
- With a `function` override, the loader function's own `radial_depth`
|
|
21
|
+
annotation decides, e.g. Hypersim's `depth` index read with `radial_distance`.
|
|
22
|
+
- Standalone `FogTransform(...)` takes it as `depth_radial`.
|
|
23
|
+
- Transforms can declare `OPTIONAL_MODALITIES`: per-sample inputs that anchor
|
|
24
|
+
the role inference like required ones when configured.
|
|
25
|
+
|
|
5
26
|
- `crop` crops label rasters in the stored file: semantic segmentation (the
|
|
6
27
|
ground truth that also holds the sky), instance and panoptic segmentation,
|
|
7
28
|
and sky masks.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.22.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
|
|
@@ -120,13 +120,17 @@ loader.
|
|
|
120
120
|
|
|
121
121
|
| Transform | Required | Optional |
|
|
122
122
|
|---|---|---|
|
|
123
|
-
| `fog` | `rgb`, `depth
|
|
123
|
+
| `fog` | `rgb`, `depth` | `semantic_segmentation` (sky mask; without it no pixel is sky); `intrinsics`, used to convert planar depth to the distance along the ray and for camera-profile optics |
|
|
124
124
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
125
125
|
| `radial` | `depth`, `intrinsics` | — |
|
|
126
126
|
| `lidar-motion` | `sparse_depth`, `gnss`, `sensor_timing`, `lidar_to_gnss` | — |
|
|
127
127
|
|
|
128
|
-
`
|
|
129
|
-
it
|
|
128
|
+
`sky-depth` needs `semantic_segmentation` as a boolean sky mask, and `fog` uses one
|
|
129
|
+
when it is configured. Without it, `fog` treats no pixel as sky, e.g. for indoor
|
|
130
|
+
datasets: `from_sky` airlight falls back to white `[1, 1, 1]`, sky-only options
|
|
131
|
+
(`sky_fog_path`, `sky_weight`, sky-aware metering) have no effect, and one warning
|
|
132
|
+
per run says so. Both transforms load the mask
|
|
133
|
+
with the `sky_mask` function of the euler-loading module that the index's
|
|
130
134
|
dataset head names, so a `class_segmentation` index works as is: the sky is the
|
|
131
135
|
head's `skyclass` (Synscapes `[23, 23, 23]`, Real Drive Sim `[29, 0, 0]`, VKITTI2
|
|
132
136
|
`[90, 200, 255]`), and the log names the loader and the sky definition in use.
|
|
@@ -208,6 +212,7 @@ whole dataset.
|
|
|
208
212
|
| `seed` | Random seed for reproducibility; `null` for non-deterministic. |
|
|
209
213
|
| `depth_scale` | Multiplier applied to depth values after loading. |
|
|
210
214
|
| `resize_depth` | Bilinearly resize the depth map to the RGB resolution. |
|
|
215
|
+
| `planar_depth_as_distance` | `true` accepts planar depth without intrinsics as the distance along the ray, with one warning, instead of failing (see [How Each Modality Is Used](#how-each-modality-is-used)). Default `false`. |
|
|
211
216
|
| `contrast_threshold` | Threshold *C_t* in the visibility-to-attenuation conversion (default `0.05`). |
|
|
212
217
|
| `render_input_space` | Colour space of the input RGB. `"srgb"` for display-encoded images (fog is mixed in scene-linear RGB); `"linear"` for already-linear radiance. |
|
|
213
218
|
| `mode` | `"sample"` (default) renders one sampled scenario per image; `"progressive"` renders every scenario step for every image. |
|
|
@@ -251,12 +256,28 @@ inf, negative) are clamped to zero, i.e. treated as infinitely close and receivi
|
|
|
251
256
|
fog. Depth stays authoritative even where the semantic map says sky, unless
|
|
252
257
|
`sky_fog_path` is configured.
|
|
253
258
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
Fog attenuates with the distance along each pixel's ray, so *d(x)* must be radial
|
|
260
|
+
(Euclidean) distance. The depth's dataset head says which kind it holds in
|
|
261
|
+
`modality.meta.radial_depth`, and the CLI logs what it found:
|
|
257
262
|
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
| Head declares | With intrinsics | Without intrinsics |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `radial_depth: true` | Used as it is; the intrinsics never convert it | Used as it is |
|
|
266
|
+
| `radial_depth: false` | Converted: `d * ‖K⁻¹ [u, v, 1]‖` (see [Radial Transform](#radial-transform)) | **Error**, unless the fog config sets `planar_depth_as_distance: true` (then one warning) |
|
|
267
|
+
| nothing (the dataset contract requires it for the `depth` key, so e.g. older heads or other keys) | Converted | Used as it is |
|
|
268
|
+
|
|
269
|
+
When the modality entry selects another loader `function` than the head names (e.g.
|
|
270
|
+
Hypersim's planar `depth` index read with `radial_distance`), that function's own
|
|
271
|
+
`radial_depth` annotation decides instead. Standalone `FogTransform(...)` use takes it
|
|
272
|
+
as `depth_radial=True/False/None`; `None`, the default, keeps the last row.
|
|
273
|
+
|
|
274
|
+
**Semantic Segmentation** *(optional)* — a per-pixel semantic map from which a boolean
|
|
275
|
+
sky mask is derived. Used for airlight estimation when `airlight` is `"from_sky"`: the
|
|
276
|
+
mean RGB of all sky pixels becomes *L_s*. Without it no pixel is sky (see
|
|
277
|
+
[Dataset Config](#dataset-config)).
|
|
278
|
+
|
|
279
|
+
**Intrinsics** *(optional)* — convert planar (z-buffer) depth to radial distance before
|
|
280
|
+
fog is applied, as in the table above, and place camera-profile optics.
|
|
260
281
|
|
|
261
282
|
For a bounded valley-fog treatment of sky, add `sky_fog_path` inside a model config:
|
|
262
283
|
|
|
@@ -285,7 +306,7 @@ this without intrinsics raises an error rather than silently falling back.
|
|
|
285
306
|
|
|
286
307
|
| Method | Description |
|
|
287
308
|
|---|---|
|
|
288
|
-
| `from_sky` | Mean RGB of sky pixels. Falls back to white `[1, 1, 1]` when no sky pixels exist. |
|
|
309
|
+
| `from_sky` | Mean RGB of sky pixels. Falls back to white `[1, 1, 1]` when no sky pixels exist; the first such sample is logged as a warning, later ones at debug level only. |
|
|
289
310
|
| `dcp` | Dark Channel Prior — brightest pixel (by channel sum) among the top 0.1% darkest-channel pixels. |
|
|
290
311
|
| `dcp_heuristic` | Robust DCP — pools the brighter half of the top 0.1% darkest-channel pixels; when sky pixels exist their brightest colours act as a chromaticity prior while DCP-derived luminance is preserved. |
|
|
291
312
|
|
|
@@ -801,6 +822,7 @@ The crop config (`configs/crop_config.json`) describes the box relative to the
|
|
|
801
822
|
|
|
802
823
|
| Key | Meaning |
|
|
803
824
|
|---|---|
|
|
825
|
+
| `reference` | The per-sample raster the box is relative to (default `rgb`). Any image-shaped modality works, so `"reference": "semantic_segmentation"` with only that modality in the dataset config crops the labels alone; other rasters must have its size |
|
|
804
826
|
| `crop.remove` | What to cut off each edge (`top`, `bottom`, `left`, `right`) |
|
|
805
827
|
| `crop.keep` + `crop.anchor` | The kept `height` / `width`, placed at `center` (default), `top`, `bottom`, `left`, `right`, `top_left`, `top_right`, `bottom_left` or `bottom_right` |
|
|
806
828
|
| `crop.unit` | `fraction` (default) or `pixel`. Fractions lie in `[0, 1)` (`keep`: `(0, 1]`) and round half up per edge: `floor(f · size + 0.5)` |
|
|
@@ -100,13 +100,17 @@ loader.
|
|
|
100
100
|
|
|
101
101
|
| Transform | Required | Optional |
|
|
102
102
|
|---|---|---|
|
|
103
|
-
| `fog` | `rgb`, `depth
|
|
103
|
+
| `fog` | `rgb`, `depth` | `semantic_segmentation` (sky mask; without it no pixel is sky); `intrinsics`, used to convert planar depth to the distance along the ray and for camera-profile optics |
|
|
104
104
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
105
105
|
| `radial` | `depth`, `intrinsics` | — |
|
|
106
106
|
| `lidar-motion` | `sparse_depth`, `gnss`, `sensor_timing`, `lidar_to_gnss` | — |
|
|
107
107
|
|
|
108
|
-
`
|
|
109
|
-
it
|
|
108
|
+
`sky-depth` needs `semantic_segmentation` as a boolean sky mask, and `fog` uses one
|
|
109
|
+
when it is configured. Without it, `fog` treats no pixel as sky, e.g. for indoor
|
|
110
|
+
datasets: `from_sky` airlight falls back to white `[1, 1, 1]`, sky-only options
|
|
111
|
+
(`sky_fog_path`, `sky_weight`, sky-aware metering) have no effect, and one warning
|
|
112
|
+
per run says so. Both transforms load the mask
|
|
113
|
+
with the `sky_mask` function of the euler-loading module that the index's
|
|
110
114
|
dataset head names, so a `class_segmentation` index works as is: the sky is the
|
|
111
115
|
head's `skyclass` (Synscapes `[23, 23, 23]`, Real Drive Sim `[29, 0, 0]`, VKITTI2
|
|
112
116
|
`[90, 200, 255]`), and the log names the loader and the sky definition in use.
|
|
@@ -188,6 +192,7 @@ whole dataset.
|
|
|
188
192
|
| `seed` | Random seed for reproducibility; `null` for non-deterministic. |
|
|
189
193
|
| `depth_scale` | Multiplier applied to depth values after loading. |
|
|
190
194
|
| `resize_depth` | Bilinearly resize the depth map to the RGB resolution. |
|
|
195
|
+
| `planar_depth_as_distance` | `true` accepts planar depth without intrinsics as the distance along the ray, with one warning, instead of failing (see [How Each Modality Is Used](#how-each-modality-is-used)). Default `false`. |
|
|
191
196
|
| `contrast_threshold` | Threshold *C_t* in the visibility-to-attenuation conversion (default `0.05`). |
|
|
192
197
|
| `render_input_space` | Colour space of the input RGB. `"srgb"` for display-encoded images (fog is mixed in scene-linear RGB); `"linear"` for already-linear radiance. |
|
|
193
198
|
| `mode` | `"sample"` (default) renders one sampled scenario per image; `"progressive"` renders every scenario step for every image. |
|
|
@@ -231,12 +236,28 @@ inf, negative) are clamped to zero, i.e. treated as infinitely close and receivi
|
|
|
231
236
|
fog. Depth stays authoritative even where the semantic map says sky, unless
|
|
232
237
|
`sky_fog_path` is configured.
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
Fog attenuates with the distance along each pixel's ray, so *d(x)* must be radial
|
|
240
|
+
(Euclidean) distance. The depth's dataset head says which kind it holds in
|
|
241
|
+
`modality.meta.radial_depth`, and the CLI logs what it found:
|
|
237
242
|
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
| Head declares | With intrinsics | Without intrinsics |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `radial_depth: true` | Used as it is; the intrinsics never convert it | Used as it is |
|
|
246
|
+
| `radial_depth: false` | Converted: `d * ‖K⁻¹ [u, v, 1]‖` (see [Radial Transform](#radial-transform)) | **Error**, unless the fog config sets `planar_depth_as_distance: true` (then one warning) |
|
|
247
|
+
| nothing (the dataset contract requires it for the `depth` key, so e.g. older heads or other keys) | Converted | Used as it is |
|
|
248
|
+
|
|
249
|
+
When the modality entry selects another loader `function` than the head names (e.g.
|
|
250
|
+
Hypersim's planar `depth` index read with `radial_distance`), that function's own
|
|
251
|
+
`radial_depth` annotation decides instead. Standalone `FogTransform(...)` use takes it
|
|
252
|
+
as `depth_radial=True/False/None`; `None`, the default, keeps the last row.
|
|
253
|
+
|
|
254
|
+
**Semantic Segmentation** *(optional)* — a per-pixel semantic map from which a boolean
|
|
255
|
+
sky mask is derived. Used for airlight estimation when `airlight` is `"from_sky"`: the
|
|
256
|
+
mean RGB of all sky pixels becomes *L_s*. Without it no pixel is sky (see
|
|
257
|
+
[Dataset Config](#dataset-config)).
|
|
258
|
+
|
|
259
|
+
**Intrinsics** *(optional)* — convert planar (z-buffer) depth to radial distance before
|
|
260
|
+
fog is applied, as in the table above, and place camera-profile optics.
|
|
240
261
|
|
|
241
262
|
For a bounded valley-fog treatment of sky, add `sky_fog_path` inside a model config:
|
|
242
263
|
|
|
@@ -265,7 +286,7 @@ this without intrinsics raises an error rather than silently falling back.
|
|
|
265
286
|
|
|
266
287
|
| Method | Description |
|
|
267
288
|
|---|---|
|
|
268
|
-
| `from_sky` | Mean RGB of sky pixels. Falls back to white `[1, 1, 1]` when no sky pixels exist. |
|
|
289
|
+
| `from_sky` | Mean RGB of sky pixels. Falls back to white `[1, 1, 1]` when no sky pixels exist; the first such sample is logged as a warning, later ones at debug level only. |
|
|
269
290
|
| `dcp` | Dark Channel Prior — brightest pixel (by channel sum) among the top 0.1% darkest-channel pixels. |
|
|
270
291
|
| `dcp_heuristic` | Robust DCP — pools the brighter half of the top 0.1% darkest-channel pixels; when sky pixels exist their brightest colours act as a chromaticity prior while DCP-derived luminance is preserved. |
|
|
271
292
|
|
|
@@ -781,6 +802,7 @@ The crop config (`configs/crop_config.json`) describes the box relative to the
|
|
|
781
802
|
|
|
782
803
|
| Key | Meaning |
|
|
783
804
|
|---|---|
|
|
805
|
+
| `reference` | The per-sample raster the box is relative to (default `rgb`). Any image-shaped modality works, so `"reference": "semantic_segmentation"` with only that modality in the dataset config crops the labels alone; other rasters must have its size |
|
|
784
806
|
| `crop.remove` | What to cut off each edge (`top`, `bottom`, `left`, `right`) |
|
|
785
807
|
| `crop.keep` + `crop.anchor` | The kept `height` / `width`, placed at `center` (default), `top`, `bottom`, `left`, `right`, `top_left`, `top_right`, `bottom_left` or `bottom_right` |
|
|
786
808
|
| `crop.unit` | `fraction` (default) or `pixel`. Fractions lie in `[0, 1)` (`keep`: `(0, 1]`) and round half up per edge: `floor(f · size + 0.5)` |
|
|
@@ -394,7 +394,9 @@ ground truth for every class, the sky included:
|
|
|
394
394
|
- Labels are never interpolated.
|
|
395
395
|
|
|
396
396
|
euler-view's crop addon offers `semantic_segmentation` instead of `sky_mask`,
|
|
397
|
-
which is only a model output.
|
|
397
|
+
which is only a model output. RGB is optional in the addon: the crop's
|
|
398
|
+
`reference` is the first included of RGB, depth and semantic segmentation, so
|
|
399
|
+
a single modality (e.g. the semantic GT) can be cropped on its own. Depth PNGs (§7.3) could use the same path later.
|
|
398
400
|
|
|
399
401
|
**MUSES lidar needs motion compensation.** The sensor is not a spinning lidar.
|
|
400
402
|
It scans a ~124° × 25° forward window top to bottom in 84.5 ms: point
|
|
@@ -13,7 +13,7 @@ from collections.abc import Iterable, Iterator, Sequence
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
-
from euler_preprocess.common.dataset import build_dataset
|
|
16
|
+
from euler_preprocess.common.dataset import build_dataset, declared_radial_depth
|
|
17
17
|
from euler_preprocess.common.logging import get_logger, log_dataset_info
|
|
18
18
|
from euler_preprocess.common.output import prepare_output_backends
|
|
19
19
|
|
|
@@ -186,6 +186,7 @@ def _run_transform(args: argparse.Namespace, transform_class: type) -> int:
|
|
|
186
186
|
required_modalities,
|
|
187
187
|
required_hierarchical,
|
|
188
188
|
transform_class.MODALITY_FUNCTIONS,
|
|
189
|
+
optional_modalities=getattr(transform_class, "OPTIONAL_MODALITIES", None),
|
|
189
190
|
)
|
|
190
191
|
output_backends = prepare_output_backends(config, dataset, transform_class)
|
|
191
192
|
primary_backend = next(iter(output_backends.values()))
|
|
@@ -222,6 +223,14 @@ def _run_transform(args: argparse.Namespace, transform_class: type) -> int:
|
|
|
222
223
|
transform_class.__name__,
|
|
223
224
|
extra,
|
|
224
225
|
)
|
|
226
|
+
if "depth_radial" in init_params and "depth" in dataset.modality_paths():
|
|
227
|
+
depth_radial, source = declared_radial_depth(dataset, "depth")
|
|
228
|
+
logger.info(
|
|
229
|
+
"Depth: radial_depth=%s (from the %s)",
|
|
230
|
+
"undeclared" if depth_radial is None else str(depth_radial).lower(),
|
|
231
|
+
source,
|
|
232
|
+
)
|
|
233
|
+
transform_kwargs["depth_radial"] = depth_radial
|
|
225
234
|
if "strict" in init_params:
|
|
226
235
|
transform_kwargs["strict"] = bool(getattr(args, "strict", False))
|
|
227
236
|
elif getattr(args, "strict", False):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections import Counter
|
|
4
|
+
from collections.abc import Mapping
|
|
4
5
|
|
|
5
6
|
from euler_preprocess.common.logging import get_logger
|
|
6
7
|
|
|
@@ -153,6 +154,7 @@ def build_dataset(
|
|
|
153
154
|
required_modalities: set[str],
|
|
154
155
|
required_hierarchical: set[str] | None = None,
|
|
155
156
|
modality_functions: dict[str, str] | None = None,
|
|
157
|
+
optional_modalities: set[str] | None = None,
|
|
156
158
|
):
|
|
157
159
|
"""Build a ``MultiModalDataset`` from a config dict.
|
|
158
160
|
|
|
@@ -177,6 +179,10 @@ def build_dataset(
|
|
|
177
179
|
e.g. ``sky_mask`` for a ``class_segmentation`` index; the sky then comes
|
|
178
180
|
from the head's ``skyclass``. A modality entry's own ``function`` key
|
|
179
181
|
overrides it, and ``"function": null`` keeps the head's function.
|
|
182
|
+
|
|
183
|
+
*optional_modalities* (a transform's ``OPTIONAL_MODALITIES``) are
|
|
184
|
+
per-sample inputs the transform can do without. When configured, they
|
|
185
|
+
anchor the role inference exactly like required ones.
|
|
180
186
|
"""
|
|
181
187
|
from euler_loading import MultiModalDataset, SamplePreprocessor
|
|
182
188
|
|
|
@@ -204,7 +210,10 @@ def build_dataset(
|
|
|
204
210
|
name: _make_modality(entry, functions.get(name))
|
|
205
211
|
for name, entry in configured.items()
|
|
206
212
|
}
|
|
207
|
-
|
|
213
|
+
per_sample = set(required_modalities) | (
|
|
214
|
+
set(optional_modalities or ()) & configured.keys()
|
|
215
|
+
)
|
|
216
|
+
regular, hierarchical = _resolve_roles(modalities, per_sample)
|
|
208
217
|
logger = get_logger()
|
|
209
218
|
for name in regular:
|
|
210
219
|
if name in raw_hierarchical:
|
|
@@ -222,3 +231,41 @@ def build_dataset(
|
|
|
222
231
|
if config.get("preprocessing")
|
|
223
232
|
else None,
|
|
224
233
|
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _head_loader_function(index: dict) -> str | None:
|
|
237
|
+
"""The loader function a ds-crawler index's dataset head names, if any."""
|
|
238
|
+
try:
|
|
239
|
+
from euler_loading._resolution import _get_euler_loading_meta
|
|
240
|
+
|
|
241
|
+
addon = _get_euler_loading_meta(index)
|
|
242
|
+
except Exception: # pragma: no cover - depends on the euler-loading version
|
|
243
|
+
addon = index.get("euler_loading") if isinstance(index, Mapping) else None
|
|
244
|
+
function = addon.get("function") if isinstance(addon, Mapping) else None
|
|
245
|
+
return function if isinstance(function, str) and function else None
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def declared_radial_depth(dataset, name: str = "depth") -> tuple[bool | None, str]:
|
|
249
|
+
"""Whether modality *name* holds distances along the ray, as declared.
|
|
250
|
+
|
|
251
|
+
Returns ``(radial, source)``. *radial* is the dataset head's
|
|
252
|
+
``modality.meta.radial_depth``: ``True`` for the Euclidean distance to
|
|
253
|
+
the camera centre, ``False`` for planar z-depth, ``None`` when the head
|
|
254
|
+
does not declare it. When the modality is read with another loader
|
|
255
|
+
function than the head names (a ``function`` override, e.g. Hypersim's
|
|
256
|
+
planar ``depth`` index read with ``radial_distance``), the head describes the
|
|
257
|
+
files but not the loaded values, so that function's own
|
|
258
|
+
``radial_depth`` annotation is used if it has one. *source* says where
|
|
259
|
+
the answer came from, for logging.
|
|
260
|
+
"""
|
|
261
|
+
head_value = dataset.get_modality_metadata(name).get("radial_depth")
|
|
262
|
+
loader_name = getattr(dataset.get_loader(name), "__name__", None)
|
|
263
|
+
head_function = _head_loader_function(dataset.get_modality_index(name))
|
|
264
|
+
if head_function is not None and loader_name not in (None, head_function):
|
|
265
|
+
annotation = (dataset.get_loader_meta(name) or {}).get("meta") or {}
|
|
266
|
+
loader_value = annotation.get("radial_depth")
|
|
267
|
+
if isinstance(loader_value, bool):
|
|
268
|
+
return loader_value, f"loader function '{loader_name}'"
|
|
269
|
+
if isinstance(head_value, bool):
|
|
270
|
+
return head_value, "dataset head"
|
|
271
|
+
return None, "dataset head"
|
|
@@ -15,7 +15,11 @@ class Transform(ABC):
|
|
|
15
15
|
Required modalities:
|
|
16
16
|
:attr:`REQUIRED_MODALITIES` are per-sample inputs (RGB, depth, masks);
|
|
17
17
|
they are always joined per sample and anchor the role inference of
|
|
18
|
-
every other modality. :attr:`
|
|
18
|
+
every other modality. :attr:`OPTIONAL_MODALITIES` are per-sample
|
|
19
|
+
inputs the transform can do without (fog's sky mask): a configured
|
|
20
|
+
one is joined and anchors the role inference like a required one;
|
|
21
|
+
otherwise the samples simply lack it.
|
|
22
|
+
:attr:`REQUIRED_HIERARCHICAL_MODALITIES` are
|
|
19
23
|
further required inputs, such as camera intrinsics, that a dataset may
|
|
20
24
|
store per sample or once per hierarchy level; their role is inferred
|
|
21
25
|
from the ds-crawler indexes (see
|
|
@@ -35,6 +39,7 @@ class Transform(ABC):
|
|
|
35
39
|
"""
|
|
36
40
|
|
|
37
41
|
REQUIRED_MODALITIES: ClassVar[set[str]] = set()
|
|
42
|
+
OPTIONAL_MODALITIES: ClassVar[set[str]] = set()
|
|
38
43
|
REQUIRED_HIERARCHICAL_MODALITIES: ClassVar[set[str]] = set()
|
|
39
44
|
MODALITY_FUNCTIONS: ClassVar[dict[str, str]] = {}
|
|
40
45
|
SOURCE_MODALITY: ClassVar[str | None] = None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger("euler-preprocess.fog")
|
|
8
|
+
|
|
9
|
+
# Default fallback when no sky pixels are available (white, standard
|
|
10
|
+
# assumption in the Koschmieder atmospheric scattering model).
|
|
11
|
+
DEFAULT_AIRLIGHT_FALLBACK = np.array([1.0, 1.0, 1.0], dtype=np.float32)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoSkyWarning:
|
|
15
|
+
"""Report the white-airlight fallback for samples without sky pixels.
|
|
16
|
+
|
|
17
|
+
The first sample is logged as a warning, every later one at debug level
|
|
18
|
+
only, so a run over an indoor dataset (no sky anywhere) logs one line
|
|
19
|
+
instead of one per sample and fog variant. :attr:`count` holds the number
|
|
20
|
+
of fallbacks so far.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, log: logging.Logger | None = None) -> None:
|
|
24
|
+
self.log = log or logger
|
|
25
|
+
self.count = 0
|
|
26
|
+
|
|
27
|
+
def __call__(self, sample_id: str | None = None) -> None:
|
|
28
|
+
self.count += 1
|
|
29
|
+
id_str = f" (sample {sample_id})" if sample_id else ""
|
|
30
|
+
if self.count == 1:
|
|
31
|
+
self.log.warning(
|
|
32
|
+
"No sky pixels in segmentation mask%s; using default airlight "
|
|
33
|
+
"fallback %s. Later samples without sky pixels fall back the "
|
|
34
|
+
"same way and are logged at debug level only.",
|
|
35
|
+
id_str,
|
|
36
|
+
DEFAULT_AIRLIGHT_FALLBACK.tolist(),
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
self.log.debug(
|
|
40
|
+
"No sky pixels in segmentation mask%s; using default airlight "
|
|
41
|
+
"fallback %s",
|
|
42
|
+
id_str,
|
|
43
|
+
DEFAULT_AIRLIGHT_FALLBACK.tolist(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AirlightFromSky:
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
sky_depth_threshold: float,
|
|
51
|
+
no_sky_warning: NoSkyWarning | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
self.sky_depth_threshold = sky_depth_threshold
|
|
54
|
+
self.no_sky_warning = no_sky_warning or NoSkyWarning()
|
|
55
|
+
|
|
56
|
+
def estimate_airlight(
|
|
57
|
+
self,
|
|
58
|
+
image: np.ndarray,
|
|
59
|
+
sky_mask: np.ndarray,
|
|
60
|
+
sample_id: str | None = None,
|
|
61
|
+
) -> np.ndarray:
|
|
62
|
+
n_sky = int(sky_mask.sum())
|
|
63
|
+
if n_sky == 0:
|
|
64
|
+
self.no_sky_warning(sample_id)
|
|
65
|
+
return DEFAULT_AIRLIGHT_FALLBACK.copy()
|
|
66
|
+
|
|
67
|
+
airlight_pixels = image[sky_mask]
|
|
68
|
+
airlight = np.mean(airlight_pixels, axis=0)
|
|
69
|
+
|
|
70
|
+
if not np.all(np.isfinite(airlight)):
|
|
71
|
+
id_str = f" (sample {sample_id})" if sample_id else ""
|
|
72
|
+
logger.warning(
|
|
73
|
+
"Airlight estimated from sky pixels contains non-finite values "
|
|
74
|
+
"(%s)%s; using default airlight fallback %s",
|
|
75
|
+
airlight,
|
|
76
|
+
id_str,
|
|
77
|
+
DEFAULT_AIRLIGHT_FALLBACK.tolist(),
|
|
78
|
+
)
|
|
79
|
+
return DEFAULT_AIRLIGHT_FALLBACK.copy()
|
|
80
|
+
|
|
81
|
+
return airlight
|
{euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/atmospheric_light.py
RENAMED
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
8
|
from euler_preprocess.common.sampling import sample_value
|
|
9
|
-
from euler_preprocess.fog.airlight_from_sky import AirlightFromSky
|
|
9
|
+
from euler_preprocess.fog.airlight_from_sky import AirlightFromSky, NoSkyWarning
|
|
10
10
|
from euler_preprocess.fog.dcp_airlight import DCPAirlight
|
|
11
11
|
from euler_preprocess.fog.dcp_heuristic_airlight import DCPHeuristicAirlight
|
|
12
12
|
from euler_preprocess.fog.models import (
|
|
@@ -41,6 +41,9 @@ class AtmosphericLightResolver:
|
|
|
41
41
|
)
|
|
42
42
|
self.method = method
|
|
43
43
|
self.logger = logger or logging.getLogger("euler-preprocess.fog")
|
|
44
|
+
# One report for the whole run: the white fallback of every sample
|
|
45
|
+
# without sky pixels after the first is logged at debug level only.
|
|
46
|
+
self.no_sky_warning = NoSkyWarning()
|
|
44
47
|
dcp_heuristic_config = (
|
|
45
48
|
{} if dcp_heuristic_config is None else dcp_heuristic_config
|
|
46
49
|
)
|
|
@@ -90,7 +93,12 @@ class AtmosphericLightResolver:
|
|
|
90
93
|
) -> "torch.Tensor":
|
|
91
94
|
resolved_method = method or self.method
|
|
92
95
|
if resolved_method == "from_sky":
|
|
93
|
-
return estimate_airlight_torch(
|
|
96
|
+
return estimate_airlight_torch(
|
|
97
|
+
rgb_t,
|
|
98
|
+
sky_mask_t,
|
|
99
|
+
sample_id=sample_id,
|
|
100
|
+
no_sky_warning=self.no_sky_warning,
|
|
101
|
+
)
|
|
94
102
|
estimator = self._get_estimator_torch(resolved_method)
|
|
95
103
|
if estimator is None:
|
|
96
104
|
raise RuntimeError(
|
|
@@ -254,12 +262,7 @@ class AtmosphericLightResolver:
|
|
|
254
262
|
]
|
|
255
263
|
if no_sky.any():
|
|
256
264
|
for idx_ns in no_sky.nonzero(as_tuple=False):
|
|
257
|
-
|
|
258
|
-
self.logger.warning(
|
|
259
|
-
"No sky pixels in segmentation mask (sample %s); using "
|
|
260
|
-
"default airlight fallback [1.0, 1.0, 1.0]",
|
|
261
|
-
items[i]["sample_id"],
|
|
262
|
-
)
|
|
265
|
+
self.no_sky_warning(items[int(idx_ns.item())]["sample_id"])
|
|
263
266
|
airlight[no_sky] = 1.0
|
|
264
267
|
return airlight
|
|
265
268
|
|
|
@@ -268,7 +271,9 @@ class AtmosphericLightResolver:
|
|
|
268
271
|
if estimator is not None:
|
|
269
272
|
return estimator
|
|
270
273
|
if method == "from_sky":
|
|
271
|
-
estimator = AirlightFromSky(
|
|
274
|
+
estimator = AirlightFromSky(
|
|
275
|
+
sky_depth_threshold=0.0, no_sky_warning=self.no_sky_warning
|
|
276
|
+
)
|
|
272
277
|
elif method == "dcp":
|
|
273
278
|
estimator = DCPAirlight()
|
|
274
279
|
elif method == "dcp_heuristic":
|
|
@@ -399,14 +399,24 @@ def estimate_airlight_torch(
|
|
|
399
399
|
image: "torch.Tensor",
|
|
400
400
|
sky_mask: "torch.Tensor",
|
|
401
401
|
sample_id: str | None = None,
|
|
402
|
+
*,
|
|
403
|
+
no_sky_warning=None,
|
|
402
404
|
) -> "torch.Tensor":
|
|
405
|
+
"""Mean colour of the sky pixels, white ``[1, 1, 1]`` without any.
|
|
406
|
+
|
|
407
|
+
*no_sky_warning* (a :class:`~euler_preprocess.fog.airlight_from_sky.NoSkyWarning`)
|
|
408
|
+
reports the fallback; without one, every fallback logs a warning.
|
|
409
|
+
"""
|
|
403
410
|
if sky_mask.sum() == 0:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
"
|
|
408
|
-
|
|
409
|
-
|
|
411
|
+
if no_sky_warning is not None:
|
|
412
|
+
no_sky_warning(sample_id)
|
|
413
|
+
else:
|
|
414
|
+
id_str = f" (sample {sample_id})" if sample_id else ""
|
|
415
|
+
_logger.warning(
|
|
416
|
+
"No sky pixels in segmentation mask%s; "
|
|
417
|
+
"using default airlight fallback [1.0, 1.0, 1.0]",
|
|
418
|
+
id_str,
|
|
419
|
+
)
|
|
410
420
|
return torch.ones(3, device=image.device, dtype=image.dtype)
|
|
411
421
|
airlight_pixels = image[sky_mask]
|
|
412
422
|
airlight = airlight_pixels.mean(dim=0)
|