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.
Files changed (88) hide show
  1. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/CHANGELOG.md +21 -0
  2. {euler_preprocess-3.20.0/euler_preprocess.egg-info → euler_preprocess-3.22.0}/PKG-INFO +32 -10
  3. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/README.md +31 -9
  4. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/docs/plans/coherent-cropping.md +3 -1
  5. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/cli.py +10 -1
  6. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/dataset.py +48 -1
  7. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/transform.py +6 -1
  8. euler_preprocess-3.22.0/euler_preprocess/fog/airlight_from_sky.py +81 -0
  9. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/atmospheric_light.py +14 -9
  10. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/models.py +16 -6
  11. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/transform.py +122 -29
  12. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0/euler_preprocess.egg-info}/PKG-INFO +32 -10
  13. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/SOURCES.txt +1 -0
  14. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/pyproject.toml +1 -1
  15. euler_preprocess-3.22.0/tests/test_fog_optional_sky_and_radial_depth.py +442 -0
  16. euler_preprocess-3.20.0/euler_preprocess/fog/airlight_from_sky.py +0 -49
  17. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/LICENSE +0 -0
  18. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/MANIFEST.in +0 -0
  19. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/docs/spatial.md +0 -0
  20. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/__init__.py +0 -0
  21. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/__init__.py +0 -0
  22. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/color.py +0 -0
  23. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/device.py +0 -0
  24. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/intrinsics.py +0 -0
  25. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/io.py +0 -0
  26. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/logging.py +0 -0
  27. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/noise.py +0 -0
  28. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/normalize.py +0 -0
  29. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/output.py +0 -0
  30. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/common/sampling.py +0 -0
  31. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/__init__.py +0 -0
  32. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/calibration.py +0 -0
  33. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/files.py +0 -0
  34. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/label_files.py +0 -0
  35. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/roles.py +0 -0
  36. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/run.py +0 -0
  37. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/spec.py +0 -0
  38. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/crop/verify.py +0 -0
  39. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/__init__.py +0 -0
  40. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/augmentations.py +0 -0
  41. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/capture.py +0 -0
  42. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  43. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  44. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  45. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  46. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/inference.py +0 -0
  47. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/logging.py +0 -0
  48. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/fog/pipeline.py +0 -0
  49. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/lidar_motion/__init__.py +0 -0
  50. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/lidar_motion/compensation.py +0 -0
  51. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/lidar_motion/transform.py +0 -0
  52. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/radial/__init__.py +0 -0
  53. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/radial/transform.py +0 -0
  54. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  55. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/sky_depth/transform.py +0 -0
  56. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess/spatial.py +0 -0
  57. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  58. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  59. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/requires.txt +0 -0
  60. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  61. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/examples/spatial-five-field.json +0 -0
  62. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/setup.cfg +0 -0
  63. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_airlight_fallback.py +0 -0
  64. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_cli_sample_selection.py +0 -0
  65. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop.py +0 -0
  66. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_labels.py +0 -0
  67. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_muses.py +0 -0
  68. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_roles.py +0 -0
  69. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_crop_spec.py +0 -0
  70. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  71. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
  72. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_aux_outputs.py +0 -0
  73. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_aware_auto_exposure.py +0 -0
  74. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
  75. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_input_normalization.py +0 -0
  76. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_fog_integration.py +0 -0
  77. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_intrinsics.py +0 -0
  78. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_lidar_motion.py +0 -0
  79. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_modality_roles.py +0 -0
  80. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_radial.py +0 -0
  81. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
  82. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_scene_illumination.py +0 -0
  83. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_sensor_identity.py +0 -0
  84. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_sky_depth.py +0 -0
  85. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_source_backed_output.py +0 -0
  86. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_spatial.py +0 -0
  87. {euler_preprocess-3.20.0 → euler_preprocess-3.22.0}/tests/test_tone_map_lut.py +0 -0
  88. {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.20.0
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`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
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
- `fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
129
- it with the `sky_mask` function of the euler-loading module that the index's
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
- **Semantic Segmentation** — a per-pixel semantic map from which a boolean sky mask is
255
- derived. Used for airlight estimation when `airlight` is `"from_sky"`: the mean RGB of
256
- all sky pixels becomes *L_s*.
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
- **Intrinsics** *(optional)* — when present, planar (z-buffer) depth is converted to
259
- radial (Euclidean) depth before fog is applied.
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`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
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
- `fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
109
- it with the `sky_mask` function of the euler-loading module that the index's
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
- **Semantic Segmentation** — a per-pixel semantic map from which a boolean sky mask is
235
- derived. Used for airlight estimation when `airlight` is `"from_sky"`: the mean RGB of
236
- all sky pixels becomes *L_s*.
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
- **Intrinsics** *(optional)* — when present, planar (z-buffer) depth is converted to
239
- radial (Euclidean) depth before fog is applied.
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. Depth PNGs (§7.3) could use the same path later.
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
- regular, hierarchical = _resolve_roles(modalities, set(required_modalities))
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:`REQUIRED_HIERARCHICAL_MODALITIES` are
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
@@ -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(rgb_t, sky_mask_t, sample_id=sample_id)
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
- i = int(idx_ns.item())
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(sky_depth_threshold=0.0)
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
- id_str = f" (sample {sample_id})" if sample_id else ""
405
- _logger.warning(
406
- "No sky pixels in segmentation mask%s; "
407
- "using default airlight fallback [1.0, 1.0, 1.0]",
408
- id_str,
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)