euler-preprocess 3.4.0__tar.gz → 3.5.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 (52) hide show
  1. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/PKG-INFO +13 -5
  2. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/README.md +12 -4
  3. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/models.py +21 -18
  4. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/transform.py +111 -9
  5. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess.egg-info/PKG-INFO +13 -5
  6. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/pyproject.toml +1 -1
  7. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_fog_aux_outputs.py +134 -0
  8. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/__init__.py +0 -0
  9. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/cli.py +0 -0
  10. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/__init__.py +0 -0
  11. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/dataset.py +0 -0
  12. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/device.py +0 -0
  13. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/intrinsics.py +0 -0
  14. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/io.py +0 -0
  15. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/logging.py +0 -0
  16. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/noise.py +0 -0
  17. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/normalize.py +0 -0
  18. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/output.py +0 -0
  19. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/sampling.py +0 -0
  20. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/common/transform.py +0 -0
  21. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/__init__.py +0 -0
  22. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  23. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
  24. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/augmentations.py +0 -0
  25. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/capture.py +0 -0
  26. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  27. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  28. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  29. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  30. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/foggify.py +0 -0
  31. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/foggify_logging.py +0 -0
  32. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/inference.py +0 -0
  33. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/logging.py +0 -0
  34. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/fog/pipeline.py +0 -0
  35. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/radial/__init__.py +0 -0
  36. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/radial/transform.py +0 -0
  37. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  38. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess/sky_depth/transform.py +0 -0
  39. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess.egg-info/SOURCES.txt +0 -0
  40. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  41. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  42. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess.egg-info/requires.txt +0 -0
  43. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  44. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/setup.cfg +0 -0
  45. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_airlight_fallback.py +0 -0
  46. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_cli_sample_selection.py +0 -0
  47. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  48. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_foggify_integration.py +0 -0
  49. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_radial.py +0 -0
  50. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_sky_depth.py +0 -0
  51. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_source_backed_output.py +0 -0
  52. {euler_preprocess-3.4.0 → euler_preprocess-3.5.0}/tests/test_zip_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-preprocess
3
- Version: 3.4.0
3
+ Version: 3.5.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
@@ -408,10 +408,14 @@ weights locally.
408
408
 
409
409
  Set top-level `"mode": "progressive"` to emit every configured scenario for
410
410
  every input image instead of sampling one scenario. Each scenario accepts
411
- `"steps"` and `"weight"`; the transform writes steps from weight `0` through
412
- the scenario's configured weight, and weight `1` matches the original scenario.
413
- Fog density is progressed in scattering-coefficient space, while numeric
414
- camera/config values blend from the base config toward the scenario config.
411
+ `"steps"` and `"progressive_weight"` (or `"max_weight"` / `"weight"` as
412
+ aliases); the transform writes steps from weight `0` through the scenario's
413
+ configured weight, and weight `1` matches the original scenario. Fog density is
414
+ progressed in scattering-coefficient space, while numeric camera/config values
415
+ blend from the base config toward the scenario config. Progressive blends clamp
416
+ probability-like values and non-negative physical factors back into valid
417
+ mathematical domains so extrapolated weights above `1` do not create invalid
418
+ render parameters.
415
419
  Source-backed outputs are written as `fog_progression` variants under each
416
420
  source file id.
417
421
 
@@ -493,6 +497,10 @@ Each fog model can override the dampening curve:
493
497
 
494
498
  The factor is:
495
499
  `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
500
+ `min_factor` and `max_factor` must be finite and non-negative. Values above
501
+ `1.0` are allowed when you intentionally want to brighten estimated airlight;
502
+ the final RGB output is still clamped to the valid image range. `strength`
503
+ must remain finite and non-negative.
496
504
  `reference_beta` is either `reference_scattering_coefficient` /
497
505
  `reference_beta`, or it is derived from `reference_visibility_m` using the
498
506
  model's contrast threshold. The default applies only when `atmospheric_light`
@@ -394,10 +394,14 @@ weights locally.
394
394
 
395
395
  Set top-level `"mode": "progressive"` to emit every configured scenario for
396
396
  every input image instead of sampling one scenario. Each scenario accepts
397
- `"steps"` and `"weight"`; the transform writes steps from weight `0` through
398
- the scenario's configured weight, and weight `1` matches the original scenario.
399
- Fog density is progressed in scattering-coefficient space, while numeric
400
- camera/config values blend from the base config toward the scenario config.
397
+ `"steps"` and `"progressive_weight"` (or `"max_weight"` / `"weight"` as
398
+ aliases); the transform writes steps from weight `0` through the scenario's
399
+ configured weight, and weight `1` matches the original scenario. Fog density is
400
+ progressed in scattering-coefficient space, while numeric camera/config values
401
+ blend from the base config toward the scenario config. Progressive blends clamp
402
+ probability-like values and non-negative physical factors back into valid
403
+ mathematical domains so extrapolated weights above `1` do not create invalid
404
+ render parameters.
401
405
  Source-backed outputs are written as `fog_progression` variants under each
402
406
  source file id.
403
407
 
@@ -479,6 +483,10 @@ Each fog model can override the dampening curve:
479
483
 
480
484
  The factor is:
481
485
  `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
486
+ `min_factor` and `max_factor` must be finite and non-negative. Values above
487
+ `1.0` are allowed when you intentionally want to brighten estimated airlight;
488
+ the final RGB output is still clamped to the valid image range. `strength`
489
+ must remain finite and non-negative.
482
490
  `reference_beta` is either `reference_scattering_coefficient` /
483
491
  `reference_beta`, or it is derived from `reference_visibility_m` using the
484
492
  model's contrast threshold. The default applies only when `atmospheric_light`
@@ -100,8 +100,12 @@ DEFAULT_MODEL_CONFIGS = {
100
100
 
101
101
 
102
102
  def visibility_to_k(visibility_m: float, contrast_threshold: float) -> float:
103
- if visibility_m <= 0:
103
+ if not math.isfinite(visibility_m) or visibility_m <= 0:
104
104
  raise ValueError(f"Visibility must be > 0, got {visibility_m}")
105
+ if not math.isfinite(contrast_threshold) or not 0.0 < contrast_threshold < 1.0:
106
+ raise ValueError(
107
+ f"Contrast threshold must be in (0, 1), got {contrast_threshold}"
108
+ )
105
109
  return -math.log(contrast_threshold) / visibility_m
106
110
 
107
111
 
@@ -132,7 +136,7 @@ def resolve_scattering_coefficient(
132
136
  beta_spec = model_cfg.get("scattering_coefficient", model_cfg.get("beta"))
133
137
  if beta_spec is not None:
134
138
  beta = float(sample_value(beta_spec, rng))
135
- if beta < 0:
139
+ if not math.isfinite(beta) or beta < 0:
136
140
  raise ValueError(f"Scattering coefficient must be >= 0, got {beta}")
137
141
  return beta, None, contrast_threshold
138
142
 
@@ -222,13 +226,13 @@ def resolve_airlight_dampening_config(
222
226
  rng,
223
227
  "airlight_dampening.strength",
224
228
  )
225
- if not 0.0 <= min_factor <= 1.0:
229
+ if min_factor < 0.0:
226
230
  raise ValueError(
227
- f"airlight_dampening.min_factor must be in [0, 1], got {min_factor}"
231
+ f"airlight_dampening.min_factor must be >= 0, got {min_factor}"
228
232
  )
229
- if not 0.0 <= max_factor <= 1.0:
233
+ if max_factor < 0.0:
230
234
  raise ValueError(
231
- f"airlight_dampening.max_factor must be in [0, 1], got {max_factor}"
235
+ f"airlight_dampening.max_factor must be >= 0, got {max_factor}"
232
236
  )
233
237
  if min_factor > max_factor:
234
238
  raise ValueError("airlight_dampening.min_factor must be <= max_factor")
@@ -504,9 +508,12 @@ def _scale_pixels(value, rng: np.random.Generator, name: str) -> int:
504
508
  def _sample_float(value, rng: np.random.Generator, name: str) -> float:
505
509
  sampled = sample_value(value, rng)
506
510
  try:
507
- return float(sampled)
511
+ resolved = float(sampled)
508
512
  except (TypeError, ValueError) as exc:
509
513
  raise ValueError(f"{name} must resolve to a number, got {sampled!r}") from exc
514
+ if not math.isfinite(resolved):
515
+ raise ValueError(f"{name} must be finite, got {resolved}")
516
+ return resolved
510
517
 
511
518
 
512
519
  def _unique_positive_scales(scales: list[int]) -> list[int]:
@@ -801,9 +808,11 @@ def _gradient_enabled(
801
808
  rng,
802
809
  f"{name}.probability",
803
810
  )
804
- if not 0.0 <= probability <= 1.0:
805
- raise ValueError(f"{name}.probability must be in [0, 1], got {probability}")
806
- return probability >= 1.0 or bool(rng.random() < probability)
811
+ if probability <= 0.0:
812
+ return False
813
+ if probability >= 1.0:
814
+ return True
815
+ return bool(rng.random() < probability)
807
816
 
808
817
 
809
818
  def _ls_gradient_factors_np(
@@ -885,10 +894,7 @@ def _weight_ls_gradient_by_opacity_np(
885
894
  rng,
886
895
  "ls_gradient.fog_opacity_weight",
887
896
  )
888
- if not 0.0 <= weight <= 1.0:
889
- raise ValueError(
890
- f"ls_gradient.fog_opacity_weight must be in [0, 1], got {weight}"
891
- )
897
+ weight = float(np.clip(weight, 0.0, 1.0))
892
898
  if weight <= 0.0:
893
899
  return factors
894
900
  gamma = _sample_float(
@@ -917,10 +923,7 @@ def _weight_ls_gradient_by_opacity_torch(
917
923
  rng,
918
924
  "ls_gradient.fog_opacity_weight",
919
925
  )
920
- if not 0.0 <= weight <= 1.0:
921
- raise ValueError(
922
- f"ls_gradient.fog_opacity_weight must be in [0, 1], got {weight}"
923
- )
926
+ weight = float(np.clip(weight, 0.0, 1.0))
924
927
  if weight <= 0.0:
925
928
  return factors
926
929
  gamma = _sample_float(
@@ -129,6 +129,53 @@ _GPU_BATCH_SCOPE_VALUES = {"sample", "batch"}
129
129
  _CONFIG_MODE_VALUES = {"sample", "sampled", "random", "default", "legacy"}
130
130
  _PROGRESSIVE_ATTRIBUTE_KEY = "fog_progression"
131
131
  _PROGRESSIVE_FILE_ID_HIERARCHY_NAME = "file_id"
132
+ _PROGRESSIVE_EPSILON = 1e-6
133
+ _PROGRESSIVE_UNIT_INTERVAL_KEYS = {
134
+ "probability",
135
+ "fog_opacity_weight",
136
+ "highlight_protection",
137
+ "center_weight",
138
+ "black_noise_floor",
139
+ }
140
+ _PROGRESSIVE_NON_NEGATIVE_KEYS = {
141
+ "beta",
142
+ "scattering_coefficient",
143
+ "strength",
144
+ "min_factor",
145
+ "max_factor",
146
+ "top_factor",
147
+ "bottom_factor",
148
+ "contrast",
149
+ "noise_contrast",
150
+ "smooth_sigma",
151
+ "smoothing_sigma",
152
+ "blur_sigma",
153
+ "smooth_sigma_fraction",
154
+ "smoothing_sigma_fraction",
155
+ "blur_sigma_fraction",
156
+ "read_noise_electrons",
157
+ "read_noise_sigma",
158
+ "fixed_pattern_sigma",
159
+ "row_noise_sigma",
160
+ "column_noise_sigma",
161
+ "hot_pixel_probability",
162
+ "dead_pixel_probability",
163
+ }
164
+ _PROGRESSIVE_POSITIVE_KEYS = {
165
+ "visibility_m",
166
+ "reference_visibility_m",
167
+ "reference_beta",
168
+ "reference_scattering_coefficient",
169
+ "gamma",
170
+ "fog_opacity_gamma",
171
+ "correlation_length_fraction",
172
+ "min_scale_fraction",
173
+ "max_scale_fraction",
174
+ "iso",
175
+ "base_iso",
176
+ "resize_scale",
177
+ }
178
+ _PROGRESSIVE_MIN_MAX_PAIRS = (("min_factor", "max_factor"),)
132
179
 
133
180
 
134
181
  @dataclass(frozen=True)
@@ -826,16 +873,20 @@ class FogTransform(Transform):
826
873
  base_model_cfg = _freeze_distribution_specs(base_model_cfg, rng)
827
874
  target_model_cfg = _freeze_distribution_specs(target_model_cfg, rng)
828
875
 
829
- step_effective_config = self._progressive_blend_value(
830
- base_effective_config,
831
- target_effective_config,
832
- step.weight,
876
+ step_effective_config = self._sanitize_progressive_blended_config(
877
+ self._progressive_blend_value(
878
+ base_effective_config,
879
+ target_effective_config,
880
+ step.weight,
881
+ )
833
882
  )
834
- model_cfg = self._progressive_model_config(
835
- base_model_cfg,
836
- target_model_cfg,
837
- step.weight,
838
- rng,
883
+ model_cfg = self._sanitize_progressive_blended_config(
884
+ self._progressive_model_config(
885
+ base_model_cfg,
886
+ target_model_cfg,
887
+ step.weight,
888
+ rng,
889
+ )
839
890
  )
840
891
  airlight_method = (
841
892
  None
@@ -953,6 +1004,57 @@ class FogTransform(Transform):
953
1004
 
954
1005
  return copy.deepcopy(target)
955
1006
 
1007
+ def _sanitize_progressive_blended_config(
1008
+ self,
1009
+ value: Any,
1010
+ *,
1011
+ key: str | None = None,
1012
+ ) -> Any:
1013
+ if isinstance(value, dict):
1014
+ sanitized = {
1015
+ child_key: self._sanitize_progressive_blended_config(
1016
+ child,
1017
+ key=child_key,
1018
+ )
1019
+ for child_key, child in value.items()
1020
+ }
1021
+ for min_key, max_key in _PROGRESSIVE_MIN_MAX_PAIRS:
1022
+ min_value = sanitized.get(min_key)
1023
+ max_value = sanitized.get(max_key)
1024
+ if (
1025
+ _is_progressive_number(min_value)
1026
+ and _is_progressive_number(max_value)
1027
+ and float(max_value) < float(min_value)
1028
+ ):
1029
+ sanitized[max_key] = float(min_value)
1030
+ return sanitized
1031
+
1032
+ if isinstance(value, list):
1033
+ return [
1034
+ self._sanitize_progressive_blended_config(item, key=key)
1035
+ for item in value
1036
+ ]
1037
+ if isinstance(value, tuple):
1038
+ return tuple(
1039
+ self._sanitize_progressive_blended_config(item, key=key)
1040
+ for item in value
1041
+ )
1042
+
1043
+ if not _is_progressive_number(value):
1044
+ return copy.deepcopy(value)
1045
+
1046
+ numeric = float(value)
1047
+ if not np.isfinite(numeric):
1048
+ label = key or "value"
1049
+ raise ValueError(f"progressive blended {label} must be finite")
1050
+ if key in _PROGRESSIVE_UNIT_INTERVAL_KEYS:
1051
+ return float(np.clip(numeric, 0.0, 1.0))
1052
+ if key in _PROGRESSIVE_POSITIVE_KEYS:
1053
+ return max(numeric, _PROGRESSIVE_EPSILON)
1054
+ if key in _PROGRESSIVE_NON_NEGATIVE_KEYS:
1055
+ return max(numeric, 0.0)
1056
+ return copy.deepcopy(value)
1057
+
956
1058
  def _progressive_neutral_value(self, key: str | None, target: Any) -> Any:
957
1059
  if _is_progressive_number(target):
958
1060
  return self._progressive_neutral_number(key, float(target))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-preprocess
3
- Version: 3.4.0
3
+ Version: 3.5.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
@@ -408,10 +408,14 @@ weights locally.
408
408
 
409
409
  Set top-level `"mode": "progressive"` to emit every configured scenario for
410
410
  every input image instead of sampling one scenario. Each scenario accepts
411
- `"steps"` and `"weight"`; the transform writes steps from weight `0` through
412
- the scenario's configured weight, and weight `1` matches the original scenario.
413
- Fog density is progressed in scattering-coefficient space, while numeric
414
- camera/config values blend from the base config toward the scenario config.
411
+ `"steps"` and `"progressive_weight"` (or `"max_weight"` / `"weight"` as
412
+ aliases); the transform writes steps from weight `0` through the scenario's
413
+ configured weight, and weight `1` matches the original scenario. Fog density is
414
+ progressed in scattering-coefficient space, while numeric camera/config values
415
+ blend from the base config toward the scenario config. Progressive blends clamp
416
+ probability-like values and non-negative physical factors back into valid
417
+ mathematical domains so extrapolated weights above `1` do not create invalid
418
+ render parameters.
415
419
  Source-backed outputs are written as `fog_progression` variants under each
416
420
  source file id.
417
421
 
@@ -493,6 +497,10 @@ Each fog model can override the dampening curve:
493
497
 
494
498
  The factor is:
495
499
  `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
500
+ `min_factor` and `max_factor` must be finite and non-negative. Values above
501
+ `1.0` are allowed when you intentionally want to brighten estimated airlight;
502
+ the final RGB output is still clamped to the valid image range. `strength`
503
+ must remain finite and non-negative.
496
504
  `reference_beta` is either `reference_scattering_coefficient` /
497
505
  `reference_beta`, or it is derived from `reference_visibility_m` using the
498
506
  model's contrast threshold. The default applies only when `atmospheric_light`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "euler-preprocess"
3
- version = "3.4.0"
3
+ version = "3.5.0"
4
4
  description = "Physics-based preprocessing (fog, etc.) for RGB+depth datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -462,6 +462,70 @@ def test_progressive_scenario_profiles_write_steps_and_attributes(
462
462
  }
463
463
 
464
464
 
465
+ def test_progressive_scenario_profiles_accept_extrapolated_airlight_dampening(
466
+ tmp_path: Path,
467
+ ) -> None:
468
+ dataset = _make_dataset(tmp_path)
469
+ pipeline_root = tmp_path / "pipeline_root_progressive_airlight"
470
+ manifest_path = pipeline_root / ".euler_pipeline" / "pipeline_outputs.json"
471
+ config = _build_pipeline_config(
472
+ pipeline_root,
473
+ manifest_path,
474
+ include_scattering=False,
475
+ include_airlight=True,
476
+ )
477
+ fog_config_path = tmp_path / "fog_progressive_airlight.json"
478
+ fog_config_path.write_text(
479
+ json.dumps(
480
+ {
481
+ "mode": "progressive",
482
+ "airlight": "from_sky",
483
+ "device": "cpu",
484
+ "seed": 7,
485
+ "contrast_threshold": 0.05,
486
+ "scenario_profiles": [
487
+ {
488
+ "name": "bright-airlight",
489
+ "steps": 1,
490
+ "progressive_weight": 1.5,
491
+ "model": "uniform",
492
+ "models": {
493
+ "uniform": {
494
+ "visibility_m": 20.0,
495
+ "atmospheric_light": "from_sky",
496
+ "airlight_dampening": {
497
+ "min_factor": 0.8,
498
+ "max_factor": 1.1,
499
+ "strength": 0.0,
500
+ },
501
+ }
502
+ },
503
+ }
504
+ ],
505
+ }
506
+ )
507
+ )
508
+
509
+ backends = prepare_output_backends(config, dataset, FogTransform)
510
+ transform = FogTransform(
511
+ config_path=str(fog_config_path),
512
+ out_path=str(backends["rgb"].root),
513
+ output_backends=backends,
514
+ )
515
+
516
+ saved_paths = transform.run(dataset)
517
+
518
+ assert saved_paths[-1] == (
519
+ pipeline_root
520
+ / "foggy_rgb"
521
+ / "Scene01"
522
+ / "Camera_0"
523
+ / "00001"
524
+ / "bright-airlight_w1.5.png"
525
+ )
526
+ assert saved_paths[-1].exists()
527
+
528
+
465
529
  def test_only_scattering_target_writes_only_scattering(tmp_path: Path) -> None:
466
530
  dataset = _make_dataset(tmp_path)
467
531
  pipeline_root = tmp_path / "pipeline_root_scat_only"
@@ -2424,6 +2488,76 @@ def test_airlight_dampening_alias_can_disable_default() -> None:
2424
2488
  np.testing.assert_allclose(airlight, estimated, atol=1e-6)
2425
2489
 
2426
2490
 
2491
+ def test_airlight_dampening_accepts_brightening_factors() -> None:
2492
+ from euler_preprocess.fog.models import apply_model
2493
+
2494
+ rgb = np.full((4, 5, 3), 0.35, dtype=np.float32)
2495
+ depth = np.full((4, 5), 30.0, dtype=np.float32)
2496
+ estimated = np.array([0.35, 0.4, 0.45], dtype=np.float32)
2497
+ cfg = {
2498
+ "scattering_coefficient": 0.1,
2499
+ "atmospheric_light": "from_sky",
2500
+ "airlight_dampening": {
2501
+ "min_factor": 1.1,
2502
+ "max_factor": 1.25,
2503
+ "strength": 0.0,
2504
+ },
2505
+ }
2506
+
2507
+ _, _, airlight, _, _ = apply_model(
2508
+ rgb,
2509
+ depth,
2510
+ "uniform",
2511
+ cfg,
2512
+ np.random.default_rng(0),
2513
+ contrast_threshold_default=0.05,
2514
+ estimated_airlight=estimated,
2515
+ )
2516
+
2517
+ np.testing.assert_allclose(airlight, estimated * 1.25, atol=1e-6)
2518
+
2519
+
2520
+ def test_ls_gradient_probability_and_opacity_weight_saturate() -> None:
2521
+ from euler_preprocess.fog.models import apply_model
2522
+
2523
+ rng = np.random.default_rng(123)
2524
+ rgb = np.zeros((20, 16, 3), dtype=np.float32)
2525
+ depth = np.full((20, 16), 80.0, dtype=np.float32)
2526
+ estimated = np.array([0.6, 0.65, 0.7], dtype=np.float32)
2527
+ cfg = {
2528
+ "scattering_coefficient": 0.08,
2529
+ "atmospheric_light": "from_sky",
2530
+ "airlight_dampening": {"enabled": False},
2531
+ "ls_hetero": {
2532
+ "scales": [4],
2533
+ "min_factor": 1.0,
2534
+ "max_factor": 1.0,
2535
+ "normalize_to_mean": False,
2536
+ "ls_gradient": {
2537
+ "enabled": True,
2538
+ "probability": 1.4,
2539
+ "top_factor": 1.18,
2540
+ "bottom_factor": 0.82,
2541
+ "gamma": 1.0,
2542
+ "normalize_to_mean": False,
2543
+ "fog_opacity_weight": 1.5,
2544
+ },
2545
+ },
2546
+ }
2547
+
2548
+ _, _, _, _, ls_map = apply_model(
2549
+ rgb,
2550
+ depth,
2551
+ "heterogeneous_ls",
2552
+ cfg,
2553
+ rng,
2554
+ contrast_threshold_default=0.05,
2555
+ estimated_airlight=estimated,
2556
+ )
2557
+
2558
+ assert np.isfinite(ls_map).all()
2559
+
2560
+
2427
2561
  def test_heterogeneous_ls_uses_dampened_airlight_base() -> None:
2428
2562
  """Perlin L_s modes should modulate around the already dampened base airlight."""
2429
2563
  from euler_preprocess.fog.models import apply_model