euler-preprocess 2.3.0__tar.gz → 3.0.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 (51) hide show
  1. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/PKG-INFO +198 -9
  2. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/README.md +197 -8
  3. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/output.py +22 -4
  4. euler_preprocess-3.0.0/euler_preprocess/fog/atmospheric_light.py +306 -0
  5. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/augmentations.py +4 -0
  6. euler_preprocess-3.0.0/euler_preprocess/fog/capture.py +1806 -0
  7. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/models.py +502 -4
  8. euler_preprocess-3.0.0/euler_preprocess/fog/pipeline.py +151 -0
  9. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/transform.py +210 -284
  10. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess.egg-info/PKG-INFO +198 -9
  11. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess.egg-info/SOURCES.txt +3 -0
  12. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/pyproject.toml +1 -1
  13. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_dcp_heuristic_airlight.py +2 -0
  14. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_fog_aux_outputs.py +610 -3
  15. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_source_backed_output.py +11 -4
  16. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/__init__.py +0 -0
  17. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/cli.py +0 -0
  18. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/__init__.py +0 -0
  19. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/dataset.py +0 -0
  20. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/device.py +0 -0
  21. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/intrinsics.py +0 -0
  22. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/io.py +0 -0
  23. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/logging.py +0 -0
  24. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/noise.py +0 -0
  25. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/normalize.py +0 -0
  26. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/sampling.py +0 -0
  27. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/common/transform.py +0 -0
  28. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/__init__.py +0 -0
  29. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  30. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  31. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  32. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  33. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  34. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/foggify.py +0 -0
  35. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/foggify_logging.py +0 -0
  36. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/fog/logging.py +0 -0
  37. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/radial/__init__.py +0 -0
  38. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/radial/transform.py +0 -0
  39. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  40. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess/sky_depth/transform.py +0 -0
  41. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  42. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  43. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess.egg-info/requires.txt +0 -0
  44. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  45. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/setup.cfg +0 -0
  46. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_airlight_fallback.py +0 -0
  47. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_cli_sample_selection.py +0 -0
  48. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_foggify_integration.py +0 -0
  49. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_radial.py +0 -0
  50. {euler_preprocess-2.3.0 → euler_preprocess-3.0.0}/tests/test_sky_depth.py +0 -0
  51. {euler_preprocess-2.3.0 → euler_preprocess-3.0.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: 2.3.0
3
+ Version: 3.0.0
4
4
  Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -98,7 +98,7 @@ starting at index 10 with stride 5.
98
98
 
99
99
  | Transform | `modalities` | `hierarchical_modalities` |
100
100
  |---|---|---|
101
- | `fog` | `rgb`, `depth`, `semantic_segmentation` | — (intrinsics optional) |
101
+ | `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics` when available; used for radial depth conversion and camera-profile optics |
102
102
  | `sky-depth` | `depth`, `semantic_segmentation` | — |
103
103
  | `radial` | `depth` | `intrinsics` |
104
104
 
@@ -150,6 +150,8 @@ Controls the fog simulation.
150
150
  "contrast_threshold": 0.05,
151
151
  "device": "cpu",
152
152
  "gpu_batch_size": 4,
153
+ "capture": { "preset": "camera" },
154
+ "camera_profile": "dashcam",
153
155
  "augmentations": { ... },
154
156
  "selection": { ... },
155
157
  "models": { ... }
@@ -165,8 +167,130 @@ Controls the fog simulation.
165
167
  | `contrast_threshold` | Threshold *C_t* used in the visibility-to-attenuation conversion (default `0.05`). |
166
168
  | `device` | `"cpu"`, `"cuda"`, `"mps"`, or `"gpu"` (alias for cuda). |
167
169
  | `gpu_batch_size` | Batch size when running on GPU. Uniform-model samples are batched; heterogeneous samples are processed individually. |
170
+ | `capture` / `capture_artifacts` | Optional post-fog camera artifact pipeline. Omit it or set `{"stages": []}` for the legacy no-op path. Set `true`, `{"preset": "camera"}`, or a custom `stages` list to enable optics, raw sensor, ISP, and compression artifacts. |
171
+ | `camera_profile` | Optional named or inline camera profile whose stage defaults are merged into the capture stack before per-stage overrides. Built-ins are `"default"`, `"generic"`, `"dashcam"`, and `"low_light_fog"`. |
172
+ | `camera_profiles` | Optional map of project-specific named profiles. Use this for calibrated lens, sensor, ISP, and transport settings. |
168
173
  | `augmentations` | Optional stepped augmentation set. When present, every input sample produces every configured augmentation and uses the file-id hierarchy output layout described below. |
169
174
 
175
+ ### Processing Pipeline
176
+
177
+ Fog generation is split into two phases:
178
+
179
+ 1. Ideal scene rendering: physics-based fog and auxiliary `scattering_coefficient`
180
+ / `atmospheric_light` maps are computed.
181
+ 2. Capture artifacts: camera-specific effects are applied to the rendered RGB
182
+ only. Physical fog maps stay stable while the RGB output can receive
183
+ exposure shifts, lens blur, vignetting, raw sensor noise, Bayer/demosaicing
184
+ artifacts, ISP tone/gamma/sharpening, and JPEG/resize/quantization effects.
185
+
186
+ This keeps physical fog maps stable while making the RGB output extensible for
187
+ real-camera simulation.
188
+
189
+ ### Capture Artifact Stack
190
+
191
+ Enable the recommended camera stack with:
192
+
193
+ ```json
194
+ "capture": { "preset": "camera" }
195
+ ```
196
+
197
+ or equivalently:
198
+
199
+ ```json
200
+ "capture": true
201
+ ```
202
+
203
+ For tighter control, provide explicit stages in camera order:
204
+
205
+ ```json
206
+ "camera_profiles": {
207
+ "real_drive_front": {
208
+ "optics": {
209
+ "lens_distortion": -0.012,
210
+ "vignetting_strength": 0.18,
211
+ "windshield_haze": {"enabled": true, "probability": 0.55}
212
+ },
213
+ "sensor": {
214
+ "bayer_pattern": "RGGB",
215
+ "iso": {"dist": "choice", "values": [200, 400, 800]},
216
+ "base_iso": 100,
217
+ "full_well_electrons": [14000, 12000, 13000],
218
+ "read_noise_electrons": {"dist": "uniform", "min": 2.0, "max": 6.0},
219
+ "black_level": [0.003, 0.0035, 0.003],
220
+ "white_level": [1.0, 0.995, 1.0],
221
+ "adc_bit_depth": 12,
222
+ "post_demosaic_bit_depth": 12
223
+ },
224
+ "isp": {
225
+ "tone_map": "reinhard",
226
+ "gamma": "srgb",
227
+ "denoise_sigma": 0.2,
228
+ "sharpen_amount": 0.2,
229
+ "saturation": 0.9
230
+ },
231
+ "transport": {
232
+ "jpeg": {"enabled": true, "quality": {"dist": "uniform", "min": 65, "max": 92}},
233
+ "bit_depth": 8
234
+ }
235
+ }
236
+ },
237
+ "camera_profile": "real_drive_front",
238
+ "capture": {
239
+ "stages": [
240
+ {
241
+ "type": "optics",
242
+ "blur_sigma": {"dist": "uniform", "min": 0.2, "max": 0.8},
243
+ "vignetting_strength": 0.15,
244
+ "windshield_haze": {"enabled": true, "probability": 0.4},
245
+ "droplets": {"enabled": false}
246
+ },
247
+ {
248
+ "type": "sensor",
249
+ "input_space": "srgb",
250
+ "exposure_gain": {"dist": "uniform", "min": 0.85, "max": 1.2},
251
+ "row_noise_sigma": 0.003
252
+ },
253
+ {
254
+ "type": "isp",
255
+ "tone_map": "reinhard",
256
+ "gamma": "srgb",
257
+ "denoise_sigma": 0.2,
258
+ "sharpen_amount": 0.2,
259
+ "saturation": 0.9
260
+ },
261
+ {
262
+ "type": "transport",
263
+ "jpeg": {"enabled": true, "quality": {"dist": "uniform", "min": 65, "max": 92}},
264
+ "bit_depth": 8
265
+ }
266
+ ]
267
+ }
268
+ ```
269
+
270
+ Supported stage types:
271
+
272
+ | Stage | Main effects |
273
+ |---|---|
274
+ | `optics` | Defocus/MTF blur, motion blur, bloom, veiling glare, vignetting, chromatic aberration, lens distortion, windshield haze, optional droplets. |
275
+ | `sensor` | Exposure, white balance, camera matrix, Bayer mosaic, shot/read noise, fixed-pattern noise, row/column banding, hot/dead pixels, bilinear demosaic. |
276
+ | `isp` | Denoising, color correction, tone mapping, sRGB/gamma, local contrast, sharpening halos, saturation shifts. |
277
+ | `transport` | Crop/resize, bit-depth quantization, JPEG round-trip compression. |
278
+ | `exposure` | Lightweight standalone exposure and white-balance stage for simple custom chains. |
279
+
280
+ Any stage can define `condition_profiles` to sample coherent per-image settings
281
+ before the stage runs. This is useful for exposure states where ISO, exposure
282
+ gain, read noise, banding, and dark/fog noise modulation should move together:
283
+
284
+ ```json
285
+ {
286
+ "type": "sensor",
287
+ "condition_profiles": [
288
+ {"name": "clean_daylight", "weight": 0.25, "exposure_gain": 1.0, "iso": 100},
289
+ {"name": "underexposed_noisy", "weight": 0.25, "exposure_gain": 0.65, "iso": 1600}
290
+ ]
291
+ }
292
+ ```
293
+
170
294
  ### Fog Model
171
295
 
172
296
  The core equation is the **Koschmieder model** (atmospheric scattering):
@@ -224,6 +348,39 @@ When `airlight` is `"dcp_heuristic"`, you can optionally add:
224
348
  - `white_bias + cool_bias` must be `<= 1`.
225
349
  - The tint bias preserves the estimated airlight luminance, so it shifts colour without silently changing fog density.
226
350
 
351
+ ### Airlight Intensity Dampening
352
+
353
+ Estimated airlight is dampened by default as fog density increases. This keeps
354
+ strong fog closer to the low, grey lighting seen in real in-car fog footage
355
+ instead of letting DCP-style estimates wash dense fog toward white.
356
+
357
+ Each fog model can override the dampening curve:
358
+
359
+ ```json
360
+ "airlight_dampening": {
361
+ "enabled": true,
362
+ "apply_to": "estimated",
363
+ "reference_visibility_m": 80.0,
364
+ "min_factor": 0.45,
365
+ "max_factor": 1.0,
366
+ "strength": 1.0
367
+ }
368
+ ```
369
+
370
+ The factor is:
371
+ `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
372
+ `reference_beta` is either `reference_scattering_coefficient` /
373
+ `reference_beta`, or it is derived from `reference_visibility_m` using the
374
+ model's contrast threshold. The default applies only when `atmospheric_light`
375
+ uses an estimated airlight method (`"from_sky"`, `"dcp"`, or
376
+ `"dcp_heuristic"`); literal RGB `atmospheric_light` values stay exact unless
377
+ `apply_to` is set to `"all"`. Set `"enabled": false` or `apply_to: "none"` to
378
+ preserve the previous undampened behavior.
379
+
380
+ For `heterogeneous_ls` and `heterogeneous_k_ls`, the Perlin atmospheric-light
381
+ field is sampled around the dampened base airlight, so the spatial variation
382
+ does not reintroduce the old over-illuminated look.
383
+
227
384
  ### Model Selection
228
385
 
229
386
  Each image is assigned a fog model via the `selection` block:
@@ -264,7 +421,10 @@ Each model specifies a `visibility_m` distribution from which a visibility dista
264
421
  | `lognormal` | `mean`, `sigma`, optional `min`/`max` | Log-normal. |
265
422
  | `choice` | `values`, optional `weights` | Discrete weighted choice. |
266
423
 
267
- The sampled visibility *V* is converted to the attenuation coefficient: **k = -ln(C_t) / V**.
424
+ The sampled visibility *V* is converted to the attenuation coefficient:
425
+ **k = -ln(C_t) / V**. This happens once per output image. For
426
+ `heterogeneous_k` and `heterogeneous_k_ls`, that sampled value is the base
427
+ coefficient that is then spatially modulated by the noise field.
268
428
 
269
429
  ### Stepped Augmentations
270
430
 
@@ -355,7 +515,31 @@ The noise field (values in [0, 1]) is mapped to a factor field:
355
515
  `contrast < 1` compresses the noise around 0.5 before this mapping, avoiding
356
516
  extreme local fog density. When `normalize_to_mean` is `true`, the factor field
357
517
  is rescaled so its spatial mean equals 1.0, preserving the overall fog density
358
- while introducing spatial variation.
518
+ while introducing spatial variation. In other words, with heterogeneous `k`:
519
+ `k(x) = k_sampled * factor(x)`. If `visibility_m` / MOR was sampled from a
520
+ distribution, `k_sampled` is the coefficient derived from that one sampled MOR.
521
+ With `normalize_to_mean: true`, the arithmetic mean of the per-pixel `k` map
522
+ equals `k_sampled`; the median is not forced to match. With
523
+ `normalize_to_mean: false`, the map mean shifts by the mean of the factor field.
524
+
525
+ For `heterogeneous_ls` / `heterogeneous_k_ls`, `ls_hetero` can also include a
526
+ weak view-direction illumination prior. This modulates the atmospheric-light
527
+ field itself, so the rendered effect is still gated by fog transmittance:
528
+
529
+ ```json
530
+ "ls_hetero": {
531
+ "ls_gradient": {
532
+ "enabled": true,
533
+ "probability": 0.65,
534
+ "axis": "vertical",
535
+ "top_factor": {"dist": "uniform", "min": 1.03, "max": 1.14},
536
+ "bottom_factor": {"dist": "uniform", "min": 0.88, "max": 0.99},
537
+ "gamma": {"dist": "uniform", "min": 0.85, "max": 1.6},
538
+ "normalize_to_mean": true,
539
+ "fog_opacity_weight": 0.65
540
+ }
541
+ }
542
+ ```
359
543
 
360
544
  | Parameter | Effect |
361
545
  |---|---|
@@ -366,16 +550,19 @@ while introducing spatial variation.
366
550
  | `octaves` / `lacunarity` / `max_scale` | Control how many increasingly broad Perlin components are mixed. |
367
551
  | `contrast` | Compress or expand the Perlin range before mapping to factors. Values below 1 are recommended. |
368
552
  | `smooth_sigma` / `smooth_sigma_fraction` | Optional final Gaussian blur in pixels or as a fraction of the shorter image side. |
553
+ | `ls_gradient` | Optional `L_s` top-to-bottom or left-to-right factor field. Keep it weak and probabilistic to avoid a deterministic image-position shortcut. |
369
554
 
370
555
  ### Fog Output
371
556
 
372
557
  CLI runs write a source-backed RGB dataset. The output keeps the source RGB
373
- dataset's relative paths, basenames, extensions, and `output.json` metadata so
374
- the result stays loadable by `euler-loading`:
558
+ dataset's relative paths, basenames, extensions, and ds-crawler metadata so the
559
+ result stays loadable by `euler-loading`:
375
560
 
376
561
  ```
377
562
  <output_path>/
378
- .ds_crawler/output.json
563
+ .ds_crawler/dataset-head.json
564
+ .ds_crawler/ds-crawler.json
565
+ .ds_crawler/index.json
379
566
  Scene01/
380
567
  Camera_0/
381
568
  00000.png
@@ -390,7 +577,9 @@ the source file id instead:
390
577
 
391
578
  ```
392
579
  <output_path>/
393
- .ds_crawler/output.json
580
+ .ds_crawler/dataset-head.json
581
+ .ds_crawler/ds-crawler.json
582
+ .ds_crawler/index.json
394
583
  Scene01/
395
584
  Camera_0/
396
585
  00000/
@@ -446,6 +635,6 @@ No special parameters are required. The transform reads intrinsics from the `int
446
635
  ### Radial Output
447
636
 
448
637
  CLI runs write a source-backed depth dataset mirroring the input depth
449
- modality's layout and writer metadata. The emitted `output.json` also flips
638
+ modality's layout and writer metadata. The emitted `index.json` also flips
450
639
  `meta.radial_depth` to `true`. Standalone/direct `RadialTransform(...)` usage
451
640
  keeps the legacy `.npy` output behavior.
@@ -84,7 +84,7 @@ starting at index 10 with stride 5.
84
84
 
85
85
  | Transform | `modalities` | `hierarchical_modalities` |
86
86
  |---|---|---|
87
- | `fog` | `rgb`, `depth`, `semantic_segmentation` | — (intrinsics optional) |
87
+ | `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics` when available; used for radial depth conversion and camera-profile optics |
88
88
  | `sky-depth` | `depth`, `semantic_segmentation` | — |
89
89
  | `radial` | `depth` | `intrinsics` |
90
90
 
@@ -136,6 +136,8 @@ Controls the fog simulation.
136
136
  "contrast_threshold": 0.05,
137
137
  "device": "cpu",
138
138
  "gpu_batch_size": 4,
139
+ "capture": { "preset": "camera" },
140
+ "camera_profile": "dashcam",
139
141
  "augmentations": { ... },
140
142
  "selection": { ... },
141
143
  "models": { ... }
@@ -151,8 +153,130 @@ Controls the fog simulation.
151
153
  | `contrast_threshold` | Threshold *C_t* used in the visibility-to-attenuation conversion (default `0.05`). |
152
154
  | `device` | `"cpu"`, `"cuda"`, `"mps"`, or `"gpu"` (alias for cuda). |
153
155
  | `gpu_batch_size` | Batch size when running on GPU. Uniform-model samples are batched; heterogeneous samples are processed individually. |
156
+ | `capture` / `capture_artifacts` | Optional post-fog camera artifact pipeline. Omit it or set `{"stages": []}` for the legacy no-op path. Set `true`, `{"preset": "camera"}`, or a custom `stages` list to enable optics, raw sensor, ISP, and compression artifacts. |
157
+ | `camera_profile` | Optional named or inline camera profile whose stage defaults are merged into the capture stack before per-stage overrides. Built-ins are `"default"`, `"generic"`, `"dashcam"`, and `"low_light_fog"`. |
158
+ | `camera_profiles` | Optional map of project-specific named profiles. Use this for calibrated lens, sensor, ISP, and transport settings. |
154
159
  | `augmentations` | Optional stepped augmentation set. When present, every input sample produces every configured augmentation and uses the file-id hierarchy output layout described below. |
155
160
 
161
+ ### Processing Pipeline
162
+
163
+ Fog generation is split into two phases:
164
+
165
+ 1. Ideal scene rendering: physics-based fog and auxiliary `scattering_coefficient`
166
+ / `atmospheric_light` maps are computed.
167
+ 2. Capture artifacts: camera-specific effects are applied to the rendered RGB
168
+ only. Physical fog maps stay stable while the RGB output can receive
169
+ exposure shifts, lens blur, vignetting, raw sensor noise, Bayer/demosaicing
170
+ artifacts, ISP tone/gamma/sharpening, and JPEG/resize/quantization effects.
171
+
172
+ This keeps physical fog maps stable while making the RGB output extensible for
173
+ real-camera simulation.
174
+
175
+ ### Capture Artifact Stack
176
+
177
+ Enable the recommended camera stack with:
178
+
179
+ ```json
180
+ "capture": { "preset": "camera" }
181
+ ```
182
+
183
+ or equivalently:
184
+
185
+ ```json
186
+ "capture": true
187
+ ```
188
+
189
+ For tighter control, provide explicit stages in camera order:
190
+
191
+ ```json
192
+ "camera_profiles": {
193
+ "real_drive_front": {
194
+ "optics": {
195
+ "lens_distortion": -0.012,
196
+ "vignetting_strength": 0.18,
197
+ "windshield_haze": {"enabled": true, "probability": 0.55}
198
+ },
199
+ "sensor": {
200
+ "bayer_pattern": "RGGB",
201
+ "iso": {"dist": "choice", "values": [200, 400, 800]},
202
+ "base_iso": 100,
203
+ "full_well_electrons": [14000, 12000, 13000],
204
+ "read_noise_electrons": {"dist": "uniform", "min": 2.0, "max": 6.0},
205
+ "black_level": [0.003, 0.0035, 0.003],
206
+ "white_level": [1.0, 0.995, 1.0],
207
+ "adc_bit_depth": 12,
208
+ "post_demosaic_bit_depth": 12
209
+ },
210
+ "isp": {
211
+ "tone_map": "reinhard",
212
+ "gamma": "srgb",
213
+ "denoise_sigma": 0.2,
214
+ "sharpen_amount": 0.2,
215
+ "saturation": 0.9
216
+ },
217
+ "transport": {
218
+ "jpeg": {"enabled": true, "quality": {"dist": "uniform", "min": 65, "max": 92}},
219
+ "bit_depth": 8
220
+ }
221
+ }
222
+ },
223
+ "camera_profile": "real_drive_front",
224
+ "capture": {
225
+ "stages": [
226
+ {
227
+ "type": "optics",
228
+ "blur_sigma": {"dist": "uniform", "min": 0.2, "max": 0.8},
229
+ "vignetting_strength": 0.15,
230
+ "windshield_haze": {"enabled": true, "probability": 0.4},
231
+ "droplets": {"enabled": false}
232
+ },
233
+ {
234
+ "type": "sensor",
235
+ "input_space": "srgb",
236
+ "exposure_gain": {"dist": "uniform", "min": 0.85, "max": 1.2},
237
+ "row_noise_sigma": 0.003
238
+ },
239
+ {
240
+ "type": "isp",
241
+ "tone_map": "reinhard",
242
+ "gamma": "srgb",
243
+ "denoise_sigma": 0.2,
244
+ "sharpen_amount": 0.2,
245
+ "saturation": 0.9
246
+ },
247
+ {
248
+ "type": "transport",
249
+ "jpeg": {"enabled": true, "quality": {"dist": "uniform", "min": 65, "max": 92}},
250
+ "bit_depth": 8
251
+ }
252
+ ]
253
+ }
254
+ ```
255
+
256
+ Supported stage types:
257
+
258
+ | Stage | Main effects |
259
+ |---|---|
260
+ | `optics` | Defocus/MTF blur, motion blur, bloom, veiling glare, vignetting, chromatic aberration, lens distortion, windshield haze, optional droplets. |
261
+ | `sensor` | Exposure, white balance, camera matrix, Bayer mosaic, shot/read noise, fixed-pattern noise, row/column banding, hot/dead pixels, bilinear demosaic. |
262
+ | `isp` | Denoising, color correction, tone mapping, sRGB/gamma, local contrast, sharpening halos, saturation shifts. |
263
+ | `transport` | Crop/resize, bit-depth quantization, JPEG round-trip compression. |
264
+ | `exposure` | Lightweight standalone exposure and white-balance stage for simple custom chains. |
265
+
266
+ Any stage can define `condition_profiles` to sample coherent per-image settings
267
+ before the stage runs. This is useful for exposure states where ISO, exposure
268
+ gain, read noise, banding, and dark/fog noise modulation should move together:
269
+
270
+ ```json
271
+ {
272
+ "type": "sensor",
273
+ "condition_profiles": [
274
+ {"name": "clean_daylight", "weight": 0.25, "exposure_gain": 1.0, "iso": 100},
275
+ {"name": "underexposed_noisy", "weight": 0.25, "exposure_gain": 0.65, "iso": 1600}
276
+ ]
277
+ }
278
+ ```
279
+
156
280
  ### Fog Model
157
281
 
158
282
  The core equation is the **Koschmieder model** (atmospheric scattering):
@@ -210,6 +334,39 @@ When `airlight` is `"dcp_heuristic"`, you can optionally add:
210
334
  - `white_bias + cool_bias` must be `<= 1`.
211
335
  - The tint bias preserves the estimated airlight luminance, so it shifts colour without silently changing fog density.
212
336
 
337
+ ### Airlight Intensity Dampening
338
+
339
+ Estimated airlight is dampened by default as fog density increases. This keeps
340
+ strong fog closer to the low, grey lighting seen in real in-car fog footage
341
+ instead of letting DCP-style estimates wash dense fog toward white.
342
+
343
+ Each fog model can override the dampening curve:
344
+
345
+ ```json
346
+ "airlight_dampening": {
347
+ "enabled": true,
348
+ "apply_to": "estimated",
349
+ "reference_visibility_m": 80.0,
350
+ "min_factor": 0.45,
351
+ "max_factor": 1.0,
352
+ "strength": 1.0
353
+ }
354
+ ```
355
+
356
+ The factor is:
357
+ `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
358
+ `reference_beta` is either `reference_scattering_coefficient` /
359
+ `reference_beta`, or it is derived from `reference_visibility_m` using the
360
+ model's contrast threshold. The default applies only when `atmospheric_light`
361
+ uses an estimated airlight method (`"from_sky"`, `"dcp"`, or
362
+ `"dcp_heuristic"`); literal RGB `atmospheric_light` values stay exact unless
363
+ `apply_to` is set to `"all"`. Set `"enabled": false` or `apply_to: "none"` to
364
+ preserve the previous undampened behavior.
365
+
366
+ For `heterogeneous_ls` and `heterogeneous_k_ls`, the Perlin atmospheric-light
367
+ field is sampled around the dampened base airlight, so the spatial variation
368
+ does not reintroduce the old over-illuminated look.
369
+
213
370
  ### Model Selection
214
371
 
215
372
  Each image is assigned a fog model via the `selection` block:
@@ -250,7 +407,10 @@ Each model specifies a `visibility_m` distribution from which a visibility dista
250
407
  | `lognormal` | `mean`, `sigma`, optional `min`/`max` | Log-normal. |
251
408
  | `choice` | `values`, optional `weights` | Discrete weighted choice. |
252
409
 
253
- The sampled visibility *V* is converted to the attenuation coefficient: **k = -ln(C_t) / V**.
410
+ The sampled visibility *V* is converted to the attenuation coefficient:
411
+ **k = -ln(C_t) / V**. This happens once per output image. For
412
+ `heterogeneous_k` and `heterogeneous_k_ls`, that sampled value is the base
413
+ coefficient that is then spatially modulated by the noise field.
254
414
 
255
415
  ### Stepped Augmentations
256
416
 
@@ -341,7 +501,31 @@ The noise field (values in [0, 1]) is mapped to a factor field:
341
501
  `contrast < 1` compresses the noise around 0.5 before this mapping, avoiding
342
502
  extreme local fog density. When `normalize_to_mean` is `true`, the factor field
343
503
  is rescaled so its spatial mean equals 1.0, preserving the overall fog density
344
- while introducing spatial variation.
504
+ while introducing spatial variation. In other words, with heterogeneous `k`:
505
+ `k(x) = k_sampled * factor(x)`. If `visibility_m` / MOR was sampled from a
506
+ distribution, `k_sampled` is the coefficient derived from that one sampled MOR.
507
+ With `normalize_to_mean: true`, the arithmetic mean of the per-pixel `k` map
508
+ equals `k_sampled`; the median is not forced to match. With
509
+ `normalize_to_mean: false`, the map mean shifts by the mean of the factor field.
510
+
511
+ For `heterogeneous_ls` / `heterogeneous_k_ls`, `ls_hetero` can also include a
512
+ weak view-direction illumination prior. This modulates the atmospheric-light
513
+ field itself, so the rendered effect is still gated by fog transmittance:
514
+
515
+ ```json
516
+ "ls_hetero": {
517
+ "ls_gradient": {
518
+ "enabled": true,
519
+ "probability": 0.65,
520
+ "axis": "vertical",
521
+ "top_factor": {"dist": "uniform", "min": 1.03, "max": 1.14},
522
+ "bottom_factor": {"dist": "uniform", "min": 0.88, "max": 0.99},
523
+ "gamma": {"dist": "uniform", "min": 0.85, "max": 1.6},
524
+ "normalize_to_mean": true,
525
+ "fog_opacity_weight": 0.65
526
+ }
527
+ }
528
+ ```
345
529
 
346
530
  | Parameter | Effect |
347
531
  |---|---|
@@ -352,16 +536,19 @@ while introducing spatial variation.
352
536
  | `octaves` / `lacunarity` / `max_scale` | Control how many increasingly broad Perlin components are mixed. |
353
537
  | `contrast` | Compress or expand the Perlin range before mapping to factors. Values below 1 are recommended. |
354
538
  | `smooth_sigma` / `smooth_sigma_fraction` | Optional final Gaussian blur in pixels or as a fraction of the shorter image side. |
539
+ | `ls_gradient` | Optional `L_s` top-to-bottom or left-to-right factor field. Keep it weak and probabilistic to avoid a deterministic image-position shortcut. |
355
540
 
356
541
  ### Fog Output
357
542
 
358
543
  CLI runs write a source-backed RGB dataset. The output keeps the source RGB
359
- dataset's relative paths, basenames, extensions, and `output.json` metadata so
360
- the result stays loadable by `euler-loading`:
544
+ dataset's relative paths, basenames, extensions, and ds-crawler metadata so the
545
+ result stays loadable by `euler-loading`:
361
546
 
362
547
  ```
363
548
  <output_path>/
364
- .ds_crawler/output.json
549
+ .ds_crawler/dataset-head.json
550
+ .ds_crawler/ds-crawler.json
551
+ .ds_crawler/index.json
365
552
  Scene01/
366
553
  Camera_0/
367
554
  00000.png
@@ -376,7 +563,9 @@ the source file id instead:
376
563
 
377
564
  ```
378
565
  <output_path>/
379
- .ds_crawler/output.json
566
+ .ds_crawler/dataset-head.json
567
+ .ds_crawler/ds-crawler.json
568
+ .ds_crawler/index.json
380
569
  Scene01/
381
570
  Camera_0/
382
571
  00000/
@@ -432,6 +621,6 @@ No special parameters are required. The transform reads intrinsics from the `int
432
621
  ### Radial Output
433
622
 
434
623
  CLI runs write a source-backed depth dataset mirroring the input depth
435
- modality's layout and writer metadata. The emitted `output.json` also flips
624
+ modality's layout and writer metadata. The emitted `index.json` also flips
436
625
  `meta.radial_depth` to `true`. Standalone/direct `RadialTransform(...)` usage
437
626
  keeps the legacy `.npy` output behavior.
@@ -9,6 +9,11 @@ from pathlib import Path
9
9
  from typing import Any
10
10
 
11
11
  from ds_crawler import DatasetWriter, ZipDatasetWriter
12
+ from ds_crawler.zip_utils import (
13
+ METADATA_DIR,
14
+ OUTPUT_FILENAME as DS_CRAWLER_INDEX_FILENAME,
15
+ get_metadata_entry_name,
16
+ )
12
17
  from euler_loading import MultiModalDataset
13
18
  from euler_loading.dataset import create_dataset_writer_from_index
14
19
  from euler_loading.loaders._writer_utils import supports_stream_target
@@ -216,12 +221,18 @@ def _merge_top_level_mapping(target: dict[str, Any], overlay: dict[str, Any]) ->
216
221
  def _patch_output_index(
217
222
  dataset_writer: DatasetWriter | ZipDatasetWriter,
218
223
  overrides: dict[str, Any],
224
+ *,
225
+ filename: str = DS_CRAWLER_INDEX_FILENAME,
219
226
  ) -> None:
220
227
  if not overrides:
221
228
  return
222
229
 
230
+ metadata_scope = getattr(dataset_writer, "_metadata_scope", None)
223
231
  if isinstance(dataset_writer, ZipDatasetWriter):
224
- entry_name = ".ds_crawler/output.json"
232
+ entry_name = get_metadata_entry_name(
233
+ filename,
234
+ metadata_scope=metadata_scope,
235
+ )
225
236
  archive_path = Path(dataset_writer.root)
226
237
  replacement_name = archive_path.with_suffix(archive_path.suffix + ".tmp")
227
238
  with zipfile.ZipFile(archive_path, "r") as source_zip:
@@ -238,7 +249,10 @@ def _patch_output_index(
238
249
  replacement_name.replace(archive_path)
239
250
  return
240
251
 
241
- output_path = Path(dataset_writer.root) / ".ds_crawler" / "output.json"
252
+ output_path = Path(dataset_writer.root) / METADATA_DIR
253
+ if metadata_scope is not None:
254
+ output_path = output_path / metadata_scope
255
+ output_path = output_path / filename
242
256
  payload = json.loads(output_path.read_text(encoding="utf-8"))
243
257
  _merge_top_level_mapping(payload, overrides)
244
258
  output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
@@ -508,8 +522,12 @@ class SourceBackedOutputBackend:
508
522
  )
509
523
 
510
524
  def finalize(self) -> None:
511
- self.dataset_writer.save_index()
512
- _patch_output_index(self.dataset_writer, self.index_overrides)
525
+ self.dataset_writer.save_index(filename=DS_CRAWLER_INDEX_FILENAME)
526
+ _patch_output_index(
527
+ self.dataset_writer,
528
+ self.index_overrides,
529
+ filename=DS_CRAWLER_INDEX_FILENAME,
530
+ )
513
531
  if self.pipeline_manifest_path and self.pipeline_manifest_targets:
514
532
  _write_pipeline_outputs_manifest(
515
533
  self.pipeline_manifest_path,