euler-preprocess 3.8.0__tar.gz → 3.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/PKG-INFO +30 -3
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/README.md +29 -2
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/models.py +172 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/pipeline.py +3 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/transform.py +59 -47
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/PKG-INFO +30 -3
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/pyproject.toml +1 -1
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_dense_gloomy_daylight_config.py +8 -1
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_fog_aux_outputs.py +103 -27
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_real_drive_sim_scenario_profiles.py +21 -7
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/MANIFEST.in +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/cli.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/color.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/dataset.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/intrinsics.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/normalize.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/output.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/common/transform.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/augmentations.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/capture.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/foggify.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/foggify_logging.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/inference.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/logging.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/radial/__init__.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/radial/transform.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/sky_depth/__init__.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/sky_depth/transform.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/SOURCES.txt +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/requires.txt +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/setup.cfg +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_cli_sample_selection.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_fog_aware_auto_exposure.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_foggify_integration.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_radial.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_scene_illumination.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_sensor_identity.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_sky_depth.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_tone_map_lut.py +0 -0
- {euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_zip_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.10.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
|
|
@@ -499,12 +499,39 @@ pixels when a sky mask is available.
|
|
|
499
499
|
|
|
500
500
|
**RGB** — The clean scene image. Normalised to float32 in [0, 1]. This is the *I(x)* term in the fog equation -- it gets blended with the airlight according to transmittance.
|
|
501
501
|
|
|
502
|
-
**Depth** — A per-pixel depth map in **metres**. Provides the *d(x)* term in the transmittance calculation `t(x) = exp(-k * d(x))`. Pixels with greater depth receive more fog. Invalid values (NaN, inf, negative) are clamped to zero (treated as infinitely close, receiving no fog).
|
|
502
|
+
**Depth** — A per-pixel depth map in **metres**. Provides the *d(x)* term in the transmittance calculation `t(x) = exp(-k * d(x))`. Pixels with greater depth receive more fog. Invalid values (NaN, inf, negative) are clamped to zero (treated as infinitely close, receiving no fog). By default, depth remains authoritative even where the semantic map says sky. For noisy real-world predictions, `sky_fog_path` can instead derive a density-weighted sky path through a bounded fog layer from the camera intrinsics and assumed orientation.
|
|
503
503
|
|
|
504
|
-
**Semantic Segmentation** — A per-pixel semantic segmentation map from which a boolean sky mask is derived, loaded via euler-loading's dataset-specific `semantic_segmentation` loader. The sky mask is used for airlight estimation when the `airlight` method is `"from_sky"`: the mean RGB of all sky pixels in the clean image is used as the airlight colour *L_s*.
|
|
504
|
+
**Semantic Segmentation** — A per-pixel semantic segmentation map from which a boolean sky mask is derived, loaded via euler-loading's dataset-specific `semantic_segmentation` loader. The sky mask is used for airlight estimation when the `airlight` method is `"from_sky"`: the mean RGB of all sky pixels in the clean image is used as the airlight colour *L_s*. When `sky_fog_path` is configured, semantic sky pixels use the modeled fog-layer path while depth remains authoritative everywhere else.
|
|
505
505
|
|
|
506
506
|
**Intrinsics** *(optional)* — When present, planar (z-buffer) depth is converted to radial (Euclidean) depth before fog is applied.
|
|
507
507
|
|
|
508
|
+
For a bounded valley-fog treatment of sky, add this inside a model config:
|
|
509
|
+
|
|
510
|
+
```json
|
|
511
|
+
"sky_fog_path": {
|
|
512
|
+
"mode": "layer",
|
|
513
|
+
"camera_height_m": 1.6,
|
|
514
|
+
"fog_valley_peak_m": 80.0,
|
|
515
|
+
"camera_pitch_deg": 0.0,
|
|
516
|
+
"camera_roll_deg": 0.0,
|
|
517
|
+
"density_profile": "linear_fade",
|
|
518
|
+
"transition_height_m": 10.0,
|
|
519
|
+
"max_path_m": 1000.0
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Pixel rays are reconstructed from the intrinsics. Near-horizon rays accumulate
|
|
524
|
+
long paths through fog, while upward rays exit the layer sooner. The default
|
|
525
|
+
`linear_fade` profile has full density below the transition and decreases
|
|
526
|
+
linearly to zero at `fog_valley_peak_m`; both `camera_height_m` and
|
|
527
|
+
`fog_valley_peak_m` are elevations in the same local vertical coordinate.
|
|
528
|
+
`uniform` keeps full density up to the top. Positive `camera_pitch_deg` points
|
|
529
|
+
the optical axis upward. `max_path_m` bounds horizon rays. The fog-layer path feeds the same fog
|
|
530
|
+
opacity, scene-illumination, and atmospheric-light-gradient heuristics as
|
|
531
|
+
ordinary depth. The original depth is retained for non-sky pixels and capture
|
|
532
|
+
effects. Enabling this option without intrinsics raises an error rather than
|
|
533
|
+
silently reverting to predicted sky depth.
|
|
534
|
+
|
|
508
535
|
### Airlight Estimation
|
|
509
536
|
|
|
510
537
|
The `airlight` config key selects how the atmospheric light *L_s* is estimated:
|
|
@@ -485,12 +485,39 @@ pixels when a sky mask is available.
|
|
|
485
485
|
|
|
486
486
|
**RGB** — The clean scene image. Normalised to float32 in [0, 1]. This is the *I(x)* term in the fog equation -- it gets blended with the airlight according to transmittance.
|
|
487
487
|
|
|
488
|
-
**Depth** — A per-pixel depth map in **metres**. Provides the *d(x)* term in the transmittance calculation `t(x) = exp(-k * d(x))`. Pixels with greater depth receive more fog. Invalid values (NaN, inf, negative) are clamped to zero (treated as infinitely close, receiving no fog).
|
|
488
|
+
**Depth** — A per-pixel depth map in **metres**. Provides the *d(x)* term in the transmittance calculation `t(x) = exp(-k * d(x))`. Pixels with greater depth receive more fog. Invalid values (NaN, inf, negative) are clamped to zero (treated as infinitely close, receiving no fog). By default, depth remains authoritative even where the semantic map says sky. For noisy real-world predictions, `sky_fog_path` can instead derive a density-weighted sky path through a bounded fog layer from the camera intrinsics and assumed orientation.
|
|
489
489
|
|
|
490
|
-
**Semantic Segmentation** — A per-pixel semantic segmentation map from which a boolean sky mask is derived, loaded via euler-loading's dataset-specific `semantic_segmentation` loader. The sky mask is used for airlight estimation when the `airlight` method is `"from_sky"`: the mean RGB of all sky pixels in the clean image is used as the airlight colour *L_s*.
|
|
490
|
+
**Semantic Segmentation** — A per-pixel semantic segmentation map from which a boolean sky mask is derived, loaded via euler-loading's dataset-specific `semantic_segmentation` loader. The sky mask is used for airlight estimation when the `airlight` method is `"from_sky"`: the mean RGB of all sky pixels in the clean image is used as the airlight colour *L_s*. When `sky_fog_path` is configured, semantic sky pixels use the modeled fog-layer path while depth remains authoritative everywhere else.
|
|
491
491
|
|
|
492
492
|
**Intrinsics** *(optional)* — When present, planar (z-buffer) depth is converted to radial (Euclidean) depth before fog is applied.
|
|
493
493
|
|
|
494
|
+
For a bounded valley-fog treatment of sky, add this inside a model config:
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
"sky_fog_path": {
|
|
498
|
+
"mode": "layer",
|
|
499
|
+
"camera_height_m": 1.6,
|
|
500
|
+
"fog_valley_peak_m": 80.0,
|
|
501
|
+
"camera_pitch_deg": 0.0,
|
|
502
|
+
"camera_roll_deg": 0.0,
|
|
503
|
+
"density_profile": "linear_fade",
|
|
504
|
+
"transition_height_m": 10.0,
|
|
505
|
+
"max_path_m": 1000.0
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Pixel rays are reconstructed from the intrinsics. Near-horizon rays accumulate
|
|
510
|
+
long paths through fog, while upward rays exit the layer sooner. The default
|
|
511
|
+
`linear_fade` profile has full density below the transition and decreases
|
|
512
|
+
linearly to zero at `fog_valley_peak_m`; both `camera_height_m` and
|
|
513
|
+
`fog_valley_peak_m` are elevations in the same local vertical coordinate.
|
|
514
|
+
`uniform` keeps full density up to the top. Positive `camera_pitch_deg` points
|
|
515
|
+
the optical axis upward. `max_path_m` bounds horizon rays. The fog-layer path feeds the same fog
|
|
516
|
+
opacity, scene-illumination, and atmospheric-light-gradient heuristics as
|
|
517
|
+
ordinary depth. The original depth is retained for non-sky pixels and capture
|
|
518
|
+
effects. Enabling this option without intrinsics raises an error rather than
|
|
519
|
+
silently reverting to predicted sky depth.
|
|
520
|
+
|
|
494
521
|
### Airlight Estimation
|
|
495
522
|
|
|
496
523
|
The `airlight` config key selects how the atmospheric light *L_s* is estimated:
|
|
@@ -989,6 +989,170 @@ def _sanitize_depth_torch(depth_m: "torch.Tensor") -> "torch.Tensor":
|
|
|
989
989
|
return torch.clamp(depth, min=0.0)
|
|
990
990
|
|
|
991
991
|
|
|
992
|
+
def _resolve_sky_fog_path_config(
|
|
993
|
+
model_cfg: dict,
|
|
994
|
+
rng: np.random.Generator,
|
|
995
|
+
) -> dict | None:
|
|
996
|
+
raw = model_cfg.get("sky_fog_path")
|
|
997
|
+
if raw is None or raw is False:
|
|
998
|
+
return None
|
|
999
|
+
if not isinstance(raw, dict):
|
|
1000
|
+
raise ValueError("sky_fog_path must be an object or false")
|
|
1001
|
+
enabled = raw.get("enabled", True)
|
|
1002
|
+
if not isinstance(enabled, bool):
|
|
1003
|
+
raise ValueError("sky_fog_path.enabled must be a boolean")
|
|
1004
|
+
if not enabled:
|
|
1005
|
+
return None
|
|
1006
|
+
mode = str(raw.get("mode", "layer")).lower()
|
|
1007
|
+
if mode != "layer":
|
|
1008
|
+
raise ValueError("sky_fog_path.mode must be 'layer'")
|
|
1009
|
+
|
|
1010
|
+
def value(key: str, default: float) -> float:
|
|
1011
|
+
return _sample_float(raw.get(key, default), rng, f"sky_fog_path.{key}")
|
|
1012
|
+
|
|
1013
|
+
camera_height = value("camera_height_m", 1.6)
|
|
1014
|
+
fog_top = value("fog_valley_peak_m", 80.0)
|
|
1015
|
+
transition = value("transition_height_m", 10.0)
|
|
1016
|
+
max_path = value("max_path_m", 1000.0)
|
|
1017
|
+
if transition < 0.0:
|
|
1018
|
+
raise ValueError("sky_fog_path.transition_height_m must be >= 0")
|
|
1019
|
+
if max_path <= 0.0:
|
|
1020
|
+
raise ValueError("sky_fog_path.max_path_m must be > 0")
|
|
1021
|
+
profile = str(raw.get("density_profile", "linear_fade")).lower()
|
|
1022
|
+
if profile not in {"linear_fade", "uniform"}:
|
|
1023
|
+
raise ValueError(
|
|
1024
|
+
"sky_fog_path.density_profile must be 'linear_fade' or 'uniform'"
|
|
1025
|
+
)
|
|
1026
|
+
return {
|
|
1027
|
+
"layer_height_m": max(fog_top - camera_height, 0.0),
|
|
1028
|
+
"transition_height_m": transition,
|
|
1029
|
+
"max_path_m": max_path,
|
|
1030
|
+
"pitch_rad": math.radians(value("camera_pitch_deg", 0.0)),
|
|
1031
|
+
"roll_rad": math.radians(value("camera_roll_deg", 0.0)),
|
|
1032
|
+
"density_profile": profile,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _camera_up_vector(cfg: dict) -> tuple[float, float, float]:
|
|
1037
|
+
pitch = cfg["pitch_rad"]
|
|
1038
|
+
roll = cfg["roll_rad"]
|
|
1039
|
+
return (
|
|
1040
|
+
math.sin(roll) * math.cos(pitch),
|
|
1041
|
+
-math.cos(roll) * math.cos(pitch),
|
|
1042
|
+
math.sin(pitch),
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def apply_sky_fog_path_np(
|
|
1047
|
+
depth_m: np.ndarray,
|
|
1048
|
+
sky_mask: np.ndarray | None,
|
|
1049
|
+
intrinsics: np.ndarray | None,
|
|
1050
|
+
model_cfg: dict,
|
|
1051
|
+
rng: np.random.Generator,
|
|
1052
|
+
) -> np.ndarray:
|
|
1053
|
+
"""Replace sky depth with its density-weighted path through a fog layer."""
|
|
1054
|
+
cfg = _resolve_sky_fog_path_config(model_cfg, rng)
|
|
1055
|
+
if cfg is None or sky_mask is None:
|
|
1056
|
+
return depth_m
|
|
1057
|
+
if intrinsics is None:
|
|
1058
|
+
raise ValueError("sky_fog_path requires camera intrinsics")
|
|
1059
|
+
depth = np.asarray(depth_m, dtype=np.float32)
|
|
1060
|
+
sky = np.asarray(sky_mask, dtype=bool)
|
|
1061
|
+
if sky.shape != depth.shape:
|
|
1062
|
+
raise ValueError(f"sky_mask must have shape {depth.shape}; got {sky.shape}")
|
|
1063
|
+
K = np.asarray(intrinsics, dtype=np.float32)
|
|
1064
|
+
if K.shape != (3, 3):
|
|
1065
|
+
raise ValueError(f"intrinsics must have shape (3, 3); got {K.shape}")
|
|
1066
|
+
|
|
1067
|
+
height, width = depth.shape
|
|
1068
|
+
yy, xx = np.indices((height, width), dtype=np.float32)
|
|
1069
|
+
pixels = np.stack((xx, yy, np.ones_like(xx)), axis=-1)
|
|
1070
|
+
rays = pixels @ np.linalg.inv(K).T
|
|
1071
|
+
rays /= np.maximum(np.linalg.norm(rays, axis=-1, keepdims=True), 1e-8)
|
|
1072
|
+
up = np.maximum(rays @ np.asarray(_camera_up_vector(cfg), dtype=np.float32), 0.0)
|
|
1073
|
+
layer_height = cfg["layer_height_m"]
|
|
1074
|
+
path = np.full_like(depth, cfg["max_path_m"])
|
|
1075
|
+
if layer_height <= 0.0:
|
|
1076
|
+
path.fill(0.0)
|
|
1077
|
+
else:
|
|
1078
|
+
reached_height = np.minimum(up * cfg["max_path_m"], layer_height)
|
|
1079
|
+
if cfg["density_profile"] == "uniform":
|
|
1080
|
+
vertical_optical_depth = reached_height
|
|
1081
|
+
else:
|
|
1082
|
+
transition = min(cfg["transition_height_m"], layer_height)
|
|
1083
|
+
fade_start = layer_height - transition
|
|
1084
|
+
fade_height = np.clip(reached_height - fade_start, 0.0, transition)
|
|
1085
|
+
vertical_optical_depth = np.minimum(reached_height, fade_start)
|
|
1086
|
+
if transition > 0.0:
|
|
1087
|
+
vertical_optical_depth += fade_height - fade_height**2 / (
|
|
1088
|
+
2.0 * transition
|
|
1089
|
+
)
|
|
1090
|
+
np.divide(vertical_optical_depth, up, out=path, where=up > 1e-8)
|
|
1091
|
+
return np.where(sky, path, depth).astype(np.float32, copy=False)
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def apply_sky_fog_path_torch(
|
|
1095
|
+
depth_m: "torch.Tensor",
|
|
1096
|
+
sky_mask: "torch.Tensor | None",
|
|
1097
|
+
intrinsics,
|
|
1098
|
+
model_cfg: dict,
|
|
1099
|
+
rng: np.random.Generator,
|
|
1100
|
+
) -> "torch.Tensor":
|
|
1101
|
+
"""Torch equivalent of :func:`apply_sky_fog_path_np`."""
|
|
1102
|
+
cfg = _resolve_sky_fog_path_config(model_cfg, rng)
|
|
1103
|
+
if cfg is None or sky_mask is None:
|
|
1104
|
+
return depth_m
|
|
1105
|
+
if intrinsics is None:
|
|
1106
|
+
raise ValueError("sky_fog_path requires camera intrinsics")
|
|
1107
|
+
depth = depth_m.to(dtype=torch.float32)
|
|
1108
|
+
sky = torch.as_tensor(sky_mask, device=depth.device, dtype=torch.bool)
|
|
1109
|
+
if tuple(sky.shape) != tuple(depth.shape):
|
|
1110
|
+
raise ValueError(
|
|
1111
|
+
f"sky_mask must have shape {tuple(depth.shape)}; got {tuple(sky.shape)}"
|
|
1112
|
+
)
|
|
1113
|
+
K = torch.as_tensor(intrinsics, device=depth.device, dtype=depth.dtype)
|
|
1114
|
+
if tuple(K.shape) != (3, 3):
|
|
1115
|
+
raise ValueError(f"intrinsics must have shape (3, 3); got {tuple(K.shape)}")
|
|
1116
|
+
|
|
1117
|
+
height, width = int(depth.shape[0]), int(depth.shape[1])
|
|
1118
|
+
yy, xx = torch.meshgrid(
|
|
1119
|
+
torch.arange(height, device=depth.device, dtype=depth.dtype),
|
|
1120
|
+
torch.arange(width, device=depth.device, dtype=depth.dtype),
|
|
1121
|
+
indexing="ij",
|
|
1122
|
+
)
|
|
1123
|
+
pixels = torch.stack((xx, yy, torch.ones_like(xx)), dim=-1)
|
|
1124
|
+
rays = pixels @ torch.linalg.inv(K).T
|
|
1125
|
+
rays = rays / torch.clamp(torch.linalg.vector_norm(rays, dim=-1, keepdim=True), min=1e-8)
|
|
1126
|
+
up_vector = torch.tensor(_camera_up_vector(cfg), device=depth.device, dtype=depth.dtype)
|
|
1127
|
+
up = torch.clamp(rays @ up_vector, min=0.0)
|
|
1128
|
+
layer_height = cfg["layer_height_m"]
|
|
1129
|
+
if layer_height <= 0.0:
|
|
1130
|
+
path = torch.zeros_like(depth)
|
|
1131
|
+
else:
|
|
1132
|
+
reached_height = torch.clamp(
|
|
1133
|
+
up * cfg["max_path_m"], max=layer_height
|
|
1134
|
+
)
|
|
1135
|
+
if cfg["density_profile"] == "uniform":
|
|
1136
|
+
vertical_optical_depth = reached_height
|
|
1137
|
+
else:
|
|
1138
|
+
transition = min(cfg["transition_height_m"], layer_height)
|
|
1139
|
+
fade_start = layer_height - transition
|
|
1140
|
+
fade_height = torch.clamp(
|
|
1141
|
+
reached_height - fade_start, min=0.0, max=transition
|
|
1142
|
+
)
|
|
1143
|
+
vertical_optical_depth = torch.clamp(reached_height, max=fade_start)
|
|
1144
|
+
if transition > 0.0:
|
|
1145
|
+
vertical_optical_depth = vertical_optical_depth + (
|
|
1146
|
+
fade_height - fade_height**2 / (2.0 * transition)
|
|
1147
|
+
)
|
|
1148
|
+
path = torch.where(
|
|
1149
|
+
up > 1e-8,
|
|
1150
|
+
vertical_optical_depth / torch.clamp(up, min=1e-8),
|
|
1151
|
+
torch.full_like(up, cfg["max_path_m"]),
|
|
1152
|
+
)
|
|
1153
|
+
return torch.where(sky, path, depth)
|
|
1154
|
+
|
|
1155
|
+
|
|
992
1156
|
def _resolve_scene_illumination_config(model_cfg: dict) -> dict:
|
|
993
1157
|
raw = model_cfg.get("scene_illumination")
|
|
994
1158
|
if raw is None:
|
|
@@ -1464,6 +1628,7 @@ def apply_model(
|
|
|
1464
1628
|
contrast_threshold_default: float,
|
|
1465
1629
|
estimated_airlight: np.ndarray,
|
|
1466
1630
|
sky_mask: np.ndarray | None = None,
|
|
1631
|
+
intrinsics: np.ndarray | None = None,
|
|
1467
1632
|
) -> tuple[np.ndarray, float, np.ndarray, np.ndarray, np.ndarray]:
|
|
1468
1633
|
"""Apply a fog model to ``rgb``.
|
|
1469
1634
|
|
|
@@ -1493,6 +1658,13 @@ def apply_model(
|
|
|
1493
1658
|
sampled_al = sample_value(al_spec, rng)
|
|
1494
1659
|
raw_ls_base = normalize_atmospheric_light(np.asarray(sampled_al))
|
|
1495
1660
|
|
|
1661
|
+
depth_m = apply_sky_fog_path_np(
|
|
1662
|
+
depth_m,
|
|
1663
|
+
sky_mask,
|
|
1664
|
+
intrinsics,
|
|
1665
|
+
model_cfg,
|
|
1666
|
+
rng,
|
|
1667
|
+
)
|
|
1496
1668
|
height, width = depth_m.shape
|
|
1497
1669
|
ls_base = dampen_airlight(
|
|
1498
1670
|
raw_ls_base,
|
|
@@ -75,6 +75,7 @@ class FogProcessingPipeline:
|
|
|
75
75
|
rng: np.random.Generator,
|
|
76
76
|
sample_id: str | None,
|
|
77
77
|
airlight_method: str | None = None,
|
|
78
|
+
intrinsics: np.ndarray | None = None,
|
|
78
79
|
) -> FogPipelineResult:
|
|
79
80
|
render_input_space = self.render_input_space_for(model_cfg)
|
|
80
81
|
rgb_for_render = srgb_to_linear(rgb) if render_input_space == "srgb" else rgb
|
|
@@ -93,6 +94,7 @@ class FogProcessingPipeline:
|
|
|
93
94
|
self.contrast_threshold_default,
|
|
94
95
|
estimated_airlight,
|
|
95
96
|
sky_mask=sky_mask,
|
|
97
|
+
intrinsics=intrinsics,
|
|
96
98
|
)
|
|
97
99
|
if render_input_space == "srgb":
|
|
98
100
|
foggy = linear_to_srgb(foggy)
|
|
@@ -193,6 +195,7 @@ class FogProcessingPipeline:
|
|
|
193
195
|
rng=rng,
|
|
194
196
|
sample_id=sample_id,
|
|
195
197
|
airlight_method=airlight_method,
|
|
198
|
+
intrinsics=intrinsics,
|
|
196
199
|
)
|
|
197
200
|
return self.apply_capture_np(
|
|
198
201
|
result,
|
|
@@ -45,6 +45,7 @@ from euler_preprocess.fog.models import (
|
|
|
45
45
|
AIRLIGHT_METHODS,
|
|
46
46
|
DEFAULT_CONTRAST_THRESHOLD,
|
|
47
47
|
DEFAULT_MODEL_CONFIGS,
|
|
48
|
+
apply_sky_fog_path_torch,
|
|
48
49
|
apply_ls_gradient_torch,
|
|
49
50
|
modulate_with_noise_torch,
|
|
50
51
|
prepare_noise_field_torch,
|
|
@@ -215,8 +216,7 @@ def _freeze_distribution_specs(value: Any, rng: np.random.Generator) -> Any:
|
|
|
215
216
|
if "dist" in value:
|
|
216
217
|
return sample_value(value, rng)
|
|
217
218
|
return {
|
|
218
|
-
key: _freeze_distribution_specs(child, rng)
|
|
219
|
-
for key, child in value.items()
|
|
219
|
+
key: _freeze_distribution_specs(child, rng) for key, child in value.items()
|
|
220
220
|
}
|
|
221
221
|
if isinstance(value, list):
|
|
222
222
|
return [_freeze_distribution_specs(child, rng) for child in value]
|
|
@@ -333,9 +333,7 @@ class FogTransform(Transform):
|
|
|
333
333
|
self.config_path = Path(config_path)
|
|
334
334
|
if output_backends is not None:
|
|
335
335
|
if "rgb" not in output_backends:
|
|
336
|
-
raise ValueError(
|
|
337
|
-
"output_backends must contain the primary 'rgb' slot"
|
|
338
|
-
)
|
|
336
|
+
raise ValueError("output_backends must contain the primary 'rgb' slot")
|
|
339
337
|
self.output_backends = dict(output_backends)
|
|
340
338
|
self.output_backend = self.output_backends["rgb"]
|
|
341
339
|
else:
|
|
@@ -457,9 +455,7 @@ class FogTransform(Transform):
|
|
|
457
455
|
def _rng_for_batch(self, batch_index: int):
|
|
458
456
|
if self.seed is not None:
|
|
459
457
|
return np.random.default_rng(
|
|
460
|
-
np.random.SeedSequence(
|
|
461
|
-
[int(self.seed), int(batch_index), 1_000_003]
|
|
462
|
-
)
|
|
458
|
+
np.random.SeedSequence([int(self.seed), int(batch_index), 1_000_003])
|
|
463
459
|
)
|
|
464
460
|
return self.base_rng
|
|
465
461
|
|
|
@@ -524,7 +520,9 @@ class FogTransform(Transform):
|
|
|
524
520
|
augmentation: FogAugmentationSpec,
|
|
525
521
|
) -> tuple[str, dict]:
|
|
526
522
|
base_cfg = resolve_model_config(augmentation.model_name, self.models_cfg)
|
|
527
|
-
return augmentation.model_name, deep_merge(
|
|
523
|
+
return augmentation.model_name, deep_merge(
|
|
524
|
+
base_cfg, augmentation.model_overrides
|
|
525
|
+
)
|
|
528
526
|
|
|
529
527
|
def _parse_scenario_profiles(
|
|
530
528
|
self,
|
|
@@ -598,7 +596,9 @@ class FogTransform(Transform):
|
|
|
598
596
|
"a finite non-negative number in progressive mode"
|
|
599
597
|
)
|
|
600
598
|
|
|
601
|
-
scenario_name =
|
|
599
|
+
scenario_name = (
|
|
600
|
+
self._scenario_name(profile) or f"scenario_{scenario_index:03d}"
|
|
601
|
+
)
|
|
602
602
|
scenario_id = _sanitize_variant_identifier(scenario_name)
|
|
603
603
|
for step_index in range(step_count + 1):
|
|
604
604
|
weight = (
|
|
@@ -644,9 +644,7 @@ class FogTransform(Transform):
|
|
|
644
644
|
raw.get("scenario_scope", config.get("gpu_scenario_scope", "sample"))
|
|
645
645
|
).lower()
|
|
646
646
|
if scenario_scope not in _GPU_BATCH_SCOPE_VALUES:
|
|
647
|
-
raise ValueError(
|
|
648
|
-
"gpu_batching.scenario_scope must be 'sample' or 'batch'"
|
|
649
|
-
)
|
|
647
|
+
raise ValueError("gpu_batching.scenario_scope must be 'sample' or 'batch'")
|
|
650
648
|
|
|
651
649
|
if "condition_parameter_scope" in raw:
|
|
652
650
|
condition_scope = str(raw["condition_parameter_scope"]).lower()
|
|
@@ -808,7 +806,7 @@ class FogTransform(Transform):
|
|
|
808
806
|
if freeze_sampled_parameters
|
|
809
807
|
else None
|
|
810
808
|
),
|
|
811
|
-
|
|
809
|
+
)
|
|
812
810
|
model_name = select_model(effective_config, rng)
|
|
813
811
|
model_cfg = resolve_model_config(model_name, models_cfg)
|
|
814
812
|
if freeze_sampled_parameters:
|
|
@@ -831,9 +829,7 @@ class FogTransform(Transform):
|
|
|
831
829
|
if freeze_sampled_parameters:
|
|
832
830
|
effective_config = _freeze_distribution_specs(effective_config, rng)
|
|
833
831
|
models_cfg = (
|
|
834
|
-
effective_config.get("models")
|
|
835
|
-
or effective_config.get("fog_models")
|
|
836
|
-
or {}
|
|
832
|
+
effective_config.get("models") or effective_config.get("fog_models") or {}
|
|
837
833
|
)
|
|
838
834
|
scenario_overrides = self._scenario_model_overrides(payload)
|
|
839
835
|
|
|
@@ -952,10 +948,12 @@ class FogTransform(Transform):
|
|
|
952
948
|
rng,
|
|
953
949
|
self.contrast_threshold_default,
|
|
954
950
|
)
|
|
955
|
-
target_beta, _target_visibility, _target_contrast =
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
951
|
+
target_beta, _target_visibility, _target_contrast = (
|
|
952
|
+
resolve_scattering_coefficient(
|
|
953
|
+
target_cfg,
|
|
954
|
+
rng,
|
|
955
|
+
self.contrast_threshold_default,
|
|
956
|
+
)
|
|
959
957
|
)
|
|
960
958
|
beta = max(0.0, float(base_beta + (target_beta - base_beta) * weight))
|
|
961
959
|
cfg.pop("visibility_m", None)
|
|
@@ -1090,8 +1088,7 @@ class FogTransform(Transform):
|
|
|
1090
1088
|
return self._progressive_neutral_number(key, float(target))
|
|
1091
1089
|
if _is_numeric_sequence(target):
|
|
1092
1090
|
return [
|
|
1093
|
-
self._progressive_neutral_number(key, float(item))
|
|
1094
|
-
for item in target
|
|
1091
|
+
self._progressive_neutral_number(key, float(item)) for item in target
|
|
1095
1092
|
]
|
|
1096
1093
|
if isinstance(target, dict):
|
|
1097
1094
|
return {
|
|
@@ -1135,9 +1132,7 @@ class FogTransform(Transform):
|
|
|
1135
1132
|
rng,
|
|
1136
1133
|
scenario=scenario,
|
|
1137
1134
|
sample_scenario=False,
|
|
1138
|
-
freeze_sampled_parameters=(
|
|
1139
|
-
self.gpu_condition_parameter_scope == "batch"
|
|
1140
|
-
),
|
|
1135
|
+
freeze_sampled_parameters=(self.gpu_condition_parameter_scope == "batch"),
|
|
1141
1136
|
)
|
|
1142
1137
|
|
|
1143
1138
|
def _source_extension(self, sample: dict, backend: Any | None = None) -> str:
|
|
@@ -1158,7 +1153,9 @@ class FogTransform(Transform):
|
|
|
1158
1153
|
return raw if isinstance(raw, str) and raw else None
|
|
1159
1154
|
|
|
1160
1155
|
def _augmentation_hierarchy_separator(self, backend: Any) -> str:
|
|
1161
|
-
separator = getattr(
|
|
1156
|
+
separator = getattr(
|
|
1157
|
+
getattr(backend, "dataset_writer", None), "_separator", None
|
|
1158
|
+
)
|
|
1162
1159
|
if isinstance(separator, str) and separator and separator != "+":
|
|
1163
1160
|
return separator
|
|
1164
1161
|
return ":"
|
|
@@ -1646,6 +1643,13 @@ class FogTransform(Transform):
|
|
|
1646
1643
|
estimated_airlight_t=estimated_airlight_t,
|
|
1647
1644
|
device=self.torch_device,
|
|
1648
1645
|
)
|
|
1646
|
+
render_depth_t = apply_sky_fog_path_torch(
|
|
1647
|
+
depth_t,
|
|
1648
|
+
sky_mask,
|
|
1649
|
+
intrinsics,
|
|
1650
|
+
model_cfg,
|
|
1651
|
+
rng,
|
|
1652
|
+
)
|
|
1649
1653
|
height = int(depth_t.shape[0])
|
|
1650
1654
|
width = int(depth_t.shape[1])
|
|
1651
1655
|
|
|
@@ -1694,7 +1698,7 @@ class FogTransform(Transform):
|
|
|
1694
1698
|
)
|
|
1695
1699
|
ls_field = apply_ls_gradient_torch(
|
|
1696
1700
|
ls_field,
|
|
1697
|
-
|
|
1701
|
+
render_depth_t,
|
|
1698
1702
|
k_field,
|
|
1699
1703
|
model_cfg,
|
|
1700
1704
|
ls_cfg,
|
|
@@ -1706,7 +1710,7 @@ class FogTransform(Transform):
|
|
|
1706
1710
|
|
|
1707
1711
|
foggy, k_map, ls_map = render_fog_fields_torch(
|
|
1708
1712
|
rgb_t,
|
|
1709
|
-
|
|
1713
|
+
render_depth_t,
|
|
1710
1714
|
k_field,
|
|
1711
1715
|
ls_field,
|
|
1712
1716
|
model_cfg,
|
|
@@ -1817,7 +1821,9 @@ class FogTransform(Transform):
|
|
|
1817
1821
|
"rgb": rgb,
|
|
1818
1822
|
"depth": depth,
|
|
1819
1823
|
"intrinsics": intrinsics,
|
|
1820
|
-
"sky_mask": normalize_sky_mask(
|
|
1824
|
+
"sky_mask": normalize_sky_mask(
|
|
1825
|
+
sample["semantic_segmentation"]
|
|
1826
|
+
),
|
|
1821
1827
|
"rng": rng,
|
|
1822
1828
|
"model_name": plan.model_name,
|
|
1823
1829
|
"model_cfg": plan.model_cfg,
|
|
@@ -1903,7 +1909,16 @@ class FogTransform(Transform):
|
|
|
1903
1909
|
rendered = [
|
|
1904
1910
|
render_fog_fields_torch(
|
|
1905
1911
|
rgb_batch[idx],
|
|
1906
|
-
|
|
1912
|
+
apply_sky_fog_path_torch(
|
|
1913
|
+
depth_tensors[idx],
|
|
1914
|
+
torch.from_numpy(item["sky_mask"]).to(
|
|
1915
|
+
device=device,
|
|
1916
|
+
dtype=torch.bool,
|
|
1917
|
+
),
|
|
1918
|
+
item.get("intrinsics"),
|
|
1919
|
+
item["model_cfg"],
|
|
1920
|
+
item["rng"],
|
|
1921
|
+
),
|
|
1907
1922
|
k_means[idx],
|
|
1908
1923
|
ls_base[idx].view(1, 1, 3),
|
|
1909
1924
|
item["model_cfg"],
|
|
@@ -1943,9 +1958,7 @@ class FogTransform(Transform):
|
|
|
1943
1958
|
)
|
|
1944
1959
|
|
|
1945
1960
|
for idx, item in enumerate(uniform_items):
|
|
1946
|
-
foggy_img = (
|
|
1947
|
-
torch.clamp(foggy[idx], 0.0, 1.0).cpu().numpy()
|
|
1948
|
-
)
|
|
1961
|
+
foggy_img = torch.clamp(foggy[idx], 0.0, 1.0).cpu().numpy()
|
|
1949
1962
|
airlight_np = ls_base[idx].detach().cpu().numpy()
|
|
1950
1963
|
sample_ref = {
|
|
1951
1964
|
"id": item["sample_id"],
|
|
@@ -1997,7 +2010,8 @@ class FogTransform(Transform):
|
|
|
1997
2010
|
K_np = item.get("intrinsics")
|
|
1998
2011
|
if K_np is not None:
|
|
1999
2012
|
K_t = torch.from_numpy(K_np).to(
|
|
2000
|
-
device=device,
|
|
2013
|
+
device=device,
|
|
2014
|
+
dtype=torch.float32,
|
|
2001
2015
|
)
|
|
2002
2016
|
depth_t = planar_to_radial_depth_torch(depth_t, K_t)
|
|
2003
2017
|
sky_mask_t = (
|
|
@@ -2405,18 +2419,16 @@ class FogTransform(Transform):
|
|
|
2405
2419
|
variant.file_id_hierarchy_name,
|
|
2406
2420
|
)
|
|
2407
2421
|
output_basename = f"{variant.id}{backend.output_extension or '.npy'}"
|
|
2408
|
-
attributes = (
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
airlight
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
),
|
|
2419
|
-
)
|
|
2422
|
+
attributes = self._output_variant_attributes(
|
|
2423
|
+
sample,
|
|
2424
|
+
variant,
|
|
2425
|
+
model_name=model_name,
|
|
2426
|
+
beta=float(beta) if beta is not None else float(np.mean(value)),
|
|
2427
|
+
airlight=(
|
|
2428
|
+
airlight
|
|
2429
|
+
if airlight is not None
|
|
2430
|
+
else np.asarray([np.nan, np.nan, np.nan], dtype=np.float32)
|
|
2431
|
+
),
|
|
2420
2432
|
)
|
|
2421
2433
|
backend.write(
|
|
2422
2434
|
sample,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.10.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
|
|
@@ -499,12 +499,39 @@ pixels when a sky mask is available.
|
|
|
499
499
|
|
|
500
500
|
**RGB** — The clean scene image. Normalised to float32 in [0, 1]. This is the *I(x)* term in the fog equation -- it gets blended with the airlight according to transmittance.
|
|
501
501
|
|
|
502
|
-
**Depth** — A per-pixel depth map in **metres**. Provides the *d(x)* term in the transmittance calculation `t(x) = exp(-k * d(x))`. Pixels with greater depth receive more fog. Invalid values (NaN, inf, negative) are clamped to zero (treated as infinitely close, receiving no fog).
|
|
502
|
+
**Depth** — A per-pixel depth map in **metres**. Provides the *d(x)* term in the transmittance calculation `t(x) = exp(-k * d(x))`. Pixels with greater depth receive more fog. Invalid values (NaN, inf, negative) are clamped to zero (treated as infinitely close, receiving no fog). By default, depth remains authoritative even where the semantic map says sky. For noisy real-world predictions, `sky_fog_path` can instead derive a density-weighted sky path through a bounded fog layer from the camera intrinsics and assumed orientation.
|
|
503
503
|
|
|
504
|
-
**Semantic Segmentation** — A per-pixel semantic segmentation map from which a boolean sky mask is derived, loaded via euler-loading's dataset-specific `semantic_segmentation` loader. The sky mask is used for airlight estimation when the `airlight` method is `"from_sky"`: the mean RGB of all sky pixels in the clean image is used as the airlight colour *L_s*.
|
|
504
|
+
**Semantic Segmentation** — A per-pixel semantic segmentation map from which a boolean sky mask is derived, loaded via euler-loading's dataset-specific `semantic_segmentation` loader. The sky mask is used for airlight estimation when the `airlight` method is `"from_sky"`: the mean RGB of all sky pixels in the clean image is used as the airlight colour *L_s*. When `sky_fog_path` is configured, semantic sky pixels use the modeled fog-layer path while depth remains authoritative everywhere else.
|
|
505
505
|
|
|
506
506
|
**Intrinsics** *(optional)* — When present, planar (z-buffer) depth is converted to radial (Euclidean) depth before fog is applied.
|
|
507
507
|
|
|
508
|
+
For a bounded valley-fog treatment of sky, add this inside a model config:
|
|
509
|
+
|
|
510
|
+
```json
|
|
511
|
+
"sky_fog_path": {
|
|
512
|
+
"mode": "layer",
|
|
513
|
+
"camera_height_m": 1.6,
|
|
514
|
+
"fog_valley_peak_m": 80.0,
|
|
515
|
+
"camera_pitch_deg": 0.0,
|
|
516
|
+
"camera_roll_deg": 0.0,
|
|
517
|
+
"density_profile": "linear_fade",
|
|
518
|
+
"transition_height_m": 10.0,
|
|
519
|
+
"max_path_m": 1000.0
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Pixel rays are reconstructed from the intrinsics. Near-horizon rays accumulate
|
|
524
|
+
long paths through fog, while upward rays exit the layer sooner. The default
|
|
525
|
+
`linear_fade` profile has full density below the transition and decreases
|
|
526
|
+
linearly to zero at `fog_valley_peak_m`; both `camera_height_m` and
|
|
527
|
+
`fog_valley_peak_m` are elevations in the same local vertical coordinate.
|
|
528
|
+
`uniform` keeps full density up to the top. Positive `camera_pitch_deg` points
|
|
529
|
+
the optical axis upward. `max_path_m` bounds horizon rays. The fog-layer path feeds the same fog
|
|
530
|
+
opacity, scene-illumination, and atmospheric-light-gradient heuristics as
|
|
531
|
+
ordinary depth. The original depth is retained for non-sky pixels and capture
|
|
532
|
+
effects. Enabling this option without intrinsics raises an error rather than
|
|
533
|
+
silently reverting to predicted sky depth.
|
|
534
|
+
|
|
508
535
|
### Airlight Estimation
|
|
509
536
|
|
|
510
537
|
The `airlight` config key selects how the atmospheric light *L_s* is estimated:
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_dense_gloomy_daylight_config.py
RENAMED
|
@@ -54,7 +54,7 @@ def test_dense_gloomy_daylight_config_activates_realism_stack() -> None:
|
|
|
54
54
|
assert profile["isp"]["tone_map_lut_domain"] == "linear"
|
|
55
55
|
assert len(profile["isp"]["tone_map_lut"]) >= 2
|
|
56
56
|
|
|
57
|
-
assert config["scenario_profiles"]
|
|
57
|
+
assert len(config["scenario_profiles"]) == 6
|
|
58
58
|
clear = _profile_by_name(
|
|
59
59
|
config["scenario_profiles"],
|
|
60
60
|
"clear_weather_camera_reference",
|
|
@@ -62,6 +62,13 @@ def test_dense_gloomy_daylight_config_activates_realism_stack() -> None:
|
|
|
62
62
|
assert clear["clear_weather"] is True
|
|
63
63
|
assert clear["weight"] == 0.1
|
|
64
64
|
assert clear["model"] == "uniform"
|
|
65
|
+
high_visibility = _profile_by_name(
|
|
66
|
+
config["scenario_profiles"],
|
|
67
|
+
"clear_high_visibility_daylight_reference",
|
|
68
|
+
)
|
|
69
|
+
assert high_visibility.get("clear_weather", False) is False
|
|
70
|
+
assert high_visibility["weight"] == 0.09
|
|
71
|
+
assert high_visibility["model"] == "heterogeneous_k_ls"
|
|
65
72
|
assert sum(scenario["weight"] for scenario in config["scenario_profiles"]) == 1.0
|
|
66
73
|
underexposed_condition = _profile_by_name(
|
|
67
74
|
profile["sensor"]["condition_profiles"],
|
|
@@ -46,11 +46,13 @@ def _write_rgb_dataset(root: Path) -> None:
|
|
|
46
46
|
meta={"range": [0, 255]},
|
|
47
47
|
euler_loading={"loader": "generic_dense_depth", "function": "rgb"},
|
|
48
48
|
)
|
|
49
|
-
rgb = np.dstack(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
rgb = np.dstack(
|
|
50
|
+
[
|
|
51
|
+
np.full((4, 6), 32, dtype=np.uint8),
|
|
52
|
+
np.full((4, 6), 96, dtype=np.uint8),
|
|
53
|
+
np.full((4, 6), 160, dtype=np.uint8),
|
|
54
|
+
]
|
|
55
|
+
)
|
|
54
56
|
path = writer.get_path("Scene01/Camera_0/00001", "00001.png")
|
|
55
57
|
write_rgb(str(path), rgb)
|
|
56
58
|
writer.save_index()
|
|
@@ -247,7 +249,9 @@ def test_writes_scattering_and_airlight_maps(tmp_path: Path) -> None:
|
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
transform = FogTransform(
|
|
250
|
-
config_path=str(
|
|
252
|
+
config_path=str(
|
|
253
|
+
_write_fog_config(tmp_path / "fog_cfg.json", visibility_m=visibility_m)
|
|
254
|
+
),
|
|
251
255
|
out_path=str(backends["rgb"].root),
|
|
252
256
|
output_backends=backends,
|
|
253
257
|
)
|
|
@@ -318,20 +322,10 @@ def test_stepped_augmentations_write_file_id_layout_and_attributes(
|
|
|
318
322
|
assert path.exists()
|
|
319
323
|
|
|
320
324
|
scattering_path = (
|
|
321
|
-
pipeline_root
|
|
322
|
-
/ "scattering"
|
|
323
|
-
/ "Scene01"
|
|
324
|
-
/ "Camera_0"
|
|
325
|
-
/ "00001"
|
|
326
|
-
/ "mor_10m.npy"
|
|
325
|
+
pipeline_root / "scattering" / "Scene01" / "Camera_0" / "00001" / "mor_10m.npy"
|
|
327
326
|
)
|
|
328
327
|
airlight_path = (
|
|
329
|
-
pipeline_root
|
|
330
|
-
/ "airlight"
|
|
331
|
-
/ "Scene01"
|
|
332
|
-
/ "Camera_0"
|
|
333
|
-
/ "00001"
|
|
334
|
-
/ "mor_10m.npy"
|
|
328
|
+
pipeline_root / "airlight" / "Scene01" / "Camera_0" / "00001" / "mor_10m.npy"
|
|
335
329
|
)
|
|
336
330
|
assert scattering_path.exists()
|
|
337
331
|
assert airlight_path.exists()
|
|
@@ -547,7 +541,9 @@ def test_only_scattering_target_writes_only_scattering(tmp_path: Path) -> None:
|
|
|
547
541
|
)
|
|
548
542
|
transform.run(dataset)
|
|
549
543
|
|
|
550
|
-
assert (
|
|
544
|
+
assert (
|
|
545
|
+
pipeline_root / "scattering" / "Scene01" / "Camera_0" / "00001.npy"
|
|
546
|
+
).exists()
|
|
551
547
|
assert not (pipeline_root / "airlight").exists()
|
|
552
548
|
|
|
553
549
|
|
|
@@ -1913,13 +1909,7 @@ def _deterministic_sensor_pipeline(
|
|
|
1913
1909
|
}
|
|
1914
1910
|
if sensor_overrides:
|
|
1915
1911
|
sensor.update(sensor_overrides)
|
|
1916
|
-
return CaptureArtifactPipeline.from_config(
|
|
1917
|
-
{
|
|
1918
|
-
"capture": {
|
|
1919
|
-
"stages": [sensor]
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
)
|
|
1912
|
+
return CaptureArtifactPipeline.from_config({"capture": {"stages": [sensor]}})
|
|
1923
1913
|
|
|
1924
1914
|
|
|
1925
1915
|
def test_sensor_noise_adjustment_scales_authored_noise_groups() -> None:
|
|
@@ -1980,7 +1970,9 @@ def test_sensor_noise_adjustment_scales_authored_noise_groups() -> None:
|
|
|
1980
1970
|
< chroma["shadow_recovery_noise"]["blue_chroma_gain"]
|
|
1981
1971
|
)
|
|
1982
1972
|
assert static["read_noise_sigma"]["min"] < chroma["read_noise_sigma"]["min"]
|
|
1983
|
-
assert
|
|
1973
|
+
assert (
|
|
1974
|
+
static["noise_modulation"]["max_gain"] == chroma["noise_modulation"]["max_gain"]
|
|
1975
|
+
)
|
|
1984
1976
|
|
|
1985
1977
|
|
|
1986
1978
|
def test_sensor_noise_adjustment_level_changes_output_noise() -> None:
|
|
@@ -2880,3 +2872,87 @@ def test_apply_model_accepts_direct_scattering_coefficient() -> None:
|
|
|
2880
2872
|
|
|
2881
2873
|
assert k_mean == 0.123
|
|
2882
2874
|
np.testing.assert_allclose(k_map, 0.123)
|
|
2875
|
+
|
|
2876
|
+
|
|
2877
|
+
def test_sky_fog_path_uses_ray_angle_and_linear_density_fade() -> None:
|
|
2878
|
+
from euler_preprocess.fog.models import apply_sky_fog_path_np
|
|
2879
|
+
|
|
2880
|
+
depth = np.full((3, 3), 2.0, dtype=np.float32)
|
|
2881
|
+
sky = np.zeros((3, 3), dtype=bool)
|
|
2882
|
+
sky[:, 1] = True
|
|
2883
|
+
intrinsics = np.array(
|
|
2884
|
+
[[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]],
|
|
2885
|
+
dtype=np.float32,
|
|
2886
|
+
)
|
|
2887
|
+
cfg = {
|
|
2888
|
+
"sky_fog_path": {
|
|
2889
|
+
"camera_height_m": 0.0,
|
|
2890
|
+
"fog_valley_peak_m": 10.0,
|
|
2891
|
+
"transition_height_m": 4.0,
|
|
2892
|
+
"max_path_m": 100.0,
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
result = apply_sky_fog_path_np(
|
|
2897
|
+
depth, sky, intrinsics, cfg, np.random.default_rng(0)
|
|
2898
|
+
)
|
|
2899
|
+
|
|
2900
|
+
# The vertical density integral is 6 m full-density + 2 m fade.
|
|
2901
|
+
np.testing.assert_allclose(result[0, 1], 8.0 * np.sqrt(2.0), rtol=1e-6)
|
|
2902
|
+
assert result[1, 1] == 100.0 # level-camera horizon is capped
|
|
2903
|
+
assert result[2, 1] == 100.0 # downward ray cannot leave through the top
|
|
2904
|
+
assert result[0, 0] == 2.0 # non-sky depth remains untouched
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
def test_sky_fog_path_changes_rendering_without_changing_non_sky_depth() -> None:
|
|
2908
|
+
from euler_preprocess.fog.models import apply_model
|
|
2909
|
+
|
|
2910
|
+
rgb = np.full((3, 3, 3), 0.2, dtype=np.float32)
|
|
2911
|
+
depth = np.full((3, 3), 1.0, dtype=np.float32)
|
|
2912
|
+
sky = np.zeros((3, 3), dtype=bool)
|
|
2913
|
+
sky[:2, 1] = True
|
|
2914
|
+
intrinsics = np.array(
|
|
2915
|
+
[[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]],
|
|
2916
|
+
dtype=np.float32,
|
|
2917
|
+
)
|
|
2918
|
+
cfg = {
|
|
2919
|
+
"scattering_coefficient": 0.1,
|
|
2920
|
+
"atmospheric_light": [0.8, 0.8, 0.8],
|
|
2921
|
+
"sky_fog_path": {
|
|
2922
|
+
"camera_height_m": 0.0,
|
|
2923
|
+
"fog_valley_peak_m": 10.0,
|
|
2924
|
+
"transition_height_m": 4.0,
|
|
2925
|
+
"max_path_m": 100.0,
|
|
2926
|
+
},
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
foggy, _, _, _, _ = apply_model(
|
|
2930
|
+
rgb,
|
|
2931
|
+
depth,
|
|
2932
|
+
"uniform",
|
|
2933
|
+
cfg,
|
|
2934
|
+
np.random.default_rng(0),
|
|
2935
|
+
0.05,
|
|
2936
|
+
np.full(3, 0.8, dtype=np.float32),
|
|
2937
|
+
sky_mask=sky,
|
|
2938
|
+
intrinsics=intrinsics,
|
|
2939
|
+
)
|
|
2940
|
+
|
|
2941
|
+
assert foggy[1, 1, 0] > foggy[0, 1, 0] > foggy[2, 1, 0]
|
|
2942
|
+
np.testing.assert_allclose(
|
|
2943
|
+
foggy[2, 1],
|
|
2944
|
+
0.2 * np.exp(-0.1) + 0.8 * (1.0 - np.exp(-0.1)),
|
|
2945
|
+
)
|
|
2946
|
+
|
|
2947
|
+
|
|
2948
|
+
def test_sky_fog_path_requires_intrinsics() -> None:
|
|
2949
|
+
from euler_preprocess.fog.models import apply_sky_fog_path_np
|
|
2950
|
+
|
|
2951
|
+
with pytest.raises(ValueError, match="requires camera intrinsics"):
|
|
2952
|
+
apply_sky_fog_path_np(
|
|
2953
|
+
np.ones((2, 2), dtype=np.float32),
|
|
2954
|
+
np.ones((2, 2), dtype=bool),
|
|
2955
|
+
None,
|
|
2956
|
+
{"sky_fog_path": {"fog_valley_peak_m": 20.0}},
|
|
2957
|
+
np.random.default_rng(0),
|
|
2958
|
+
)
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/tests/test_real_drive_sim_scenario_profiles.py
RENAMED
|
@@ -33,6 +33,7 @@ DEFAULT_RDS_SAMPLE_ZIP = (
|
|
|
33
33
|
RDS_CONFIG_PATH = Path("configs/dense_gloomy_daylight_fog_camera.json")
|
|
34
34
|
SCENARIO_ORDER = (
|
|
35
35
|
"clear_weather_camera_reference",
|
|
36
|
+
"clear_high_visibility_daylight_reference",
|
|
36
37
|
"light_haze_soft_overcast",
|
|
37
38
|
"moderate_gloomy_fog_nominal_camera",
|
|
38
39
|
"underexposed_dense_gloom",
|
|
@@ -137,10 +138,17 @@ def test_dense_gloomy_scenario_profiles_render_real_drive_sim_samples(
|
|
|
137
138
|
for sample in rds_samples:
|
|
138
139
|
metrics = _render_scenario_metrics(transform, sample)
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
]
|
|
143
|
-
assert
|
|
141
|
+
clear_metrics = metrics["clear_weather_camera_reference"]
|
|
142
|
+
assert clear_metrics["beta"] == 0.0
|
|
143
|
+
assert clear_metrics["opacity_mean"] == 0.0
|
|
144
|
+
assert clear_metrics["opacity_p95"] == 0.0
|
|
145
|
+
fog_profiles = SCENARIO_ORDER[1:]
|
|
146
|
+
assert all(metrics[name]["opacity_mean"] > 0.0 for name in fog_profiles)
|
|
147
|
+
# Adjacent fog profiles intentionally have overlapping distributions,
|
|
148
|
+
# so sampled opacity need not be strictly ordered for every image.
|
|
149
|
+
assert metrics["severe_low_contrast_sensor_stress"]["opacity_mean"] > (
|
|
150
|
+
metrics["clear_high_visibility_daylight_reference"]["opacity_mean"]
|
|
151
|
+
)
|
|
144
152
|
assert metrics["severe_low_contrast_sensor_stress"]["opacity_mean"] > (
|
|
145
153
|
metrics["clear_weather_camera_reference"]["opacity_mean"] + 0.25
|
|
146
154
|
)
|
|
@@ -151,7 +159,7 @@ def test_dense_gloomy_scenario_profiles_render_real_drive_sim_samples(
|
|
|
151
159
|
assert metrics["severe_low_contrast_sensor_stress"]["luma_mean"] < (
|
|
152
160
|
metrics["clear_weather_camera_reference"]["luma_mean"] * 0.65
|
|
153
161
|
)
|
|
154
|
-
chromatically_bounded_profiles = SCENARIO_ORDER[:
|
|
162
|
+
chromatically_bounded_profiles = SCENARIO_ORDER[:4]
|
|
155
163
|
assert all(
|
|
156
164
|
0.75 <= metrics[name]["blue_red_ratio"] <= 1.65
|
|
157
165
|
for name in chromatically_bounded_profiles
|
|
@@ -164,7 +172,7 @@ def test_dense_gloomy_scenario_profiles_render_real_drive_sim_samples(
|
|
|
164
172
|
assert metrics["underexposed_dense_gloom"]["blue_red_ratio"] <= 2.5, {
|
|
165
173
|
sample["id"]: metrics
|
|
166
174
|
}
|
|
167
|
-
stress_profiles = SCENARIO_ORDER[
|
|
175
|
+
stress_profiles = SCENARIO_ORDER[4:]
|
|
168
176
|
assert all(
|
|
169
177
|
metrics[name]["blue_minus_red_mean"] <= 0.1 for name in stress_profiles
|
|
170
178
|
), {sample["id"]: metrics}
|
|
@@ -328,7 +336,12 @@ def _render_profile(
|
|
|
328
336
|
profile: dict[str, Any],
|
|
329
337
|
scenario_index: int,
|
|
330
338
|
):
|
|
331
|
-
|
|
339
|
+
# Keep the original five profiles on their historical deterministic seeds
|
|
340
|
+
# when the new clear-weather profile is inserted at the front.
|
|
341
|
+
stable_scenario_index = max(0, scenario_index - 1)
|
|
342
|
+
rng = np.random.default_rng(
|
|
343
|
+
np.random.SeedSequence([1337, stable_scenario_index, 1])
|
|
344
|
+
)
|
|
332
345
|
plan = transform._resolve_render_plan(
|
|
333
346
|
rng,
|
|
334
347
|
scenario=profile,
|
|
@@ -346,6 +359,7 @@ def _render_profile(
|
|
|
346
359
|
intrinsics=sample["intrinsics"]["intrinsics"],
|
|
347
360
|
airlight_method=plan.airlight_method,
|
|
348
361
|
capture_artifacts=plan.capture_artifacts,
|
|
362
|
+
clear_weather=plan.clear_weather,
|
|
349
363
|
)
|
|
350
364
|
|
|
351
365
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/airlight_from_sky.py
RENAMED
|
File without changes
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/atmospheric_light.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/dcp_airlight_torch.py
RENAMED
|
File without changes
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess/fog/dcp_heuristic_airlight.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{euler_preprocess-3.8.0 → euler_preprocess-3.10.0}/euler_preprocess.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|