euler-preprocess 3.6.0__tar.gz → 3.9.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 (61) hide show
  1. euler_preprocess-3.9.0/MANIFEST.in +1 -0
  2. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/PKG-INFO +1 -1
  3. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/inference.py +5 -9
  4. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/models.py +260 -5
  5. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/pipeline.py +87 -23
  6. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/transform.py +198 -115
  7. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess.egg-info/PKG-INFO +1 -1
  8. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess.egg-info/SOURCES.txt +3 -0
  9. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/pyproject.toml +1 -1
  10. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_dense_gloomy_daylight_config.py +42 -5
  11. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_fog_aux_outputs.py +87 -0
  12. euler_preprocess-3.9.0/tests/test_fog_cpu_gpu_parity.py +522 -0
  13. euler_preprocess-3.9.0/tests/test_real_drive_sim_scenario_profiles.py +425 -0
  14. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/README.md +0 -0
  15. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/__init__.py +0 -0
  16. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/cli.py +0 -0
  17. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/__init__.py +0 -0
  18. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/color.py +0 -0
  19. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/dataset.py +0 -0
  20. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/device.py +0 -0
  21. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/intrinsics.py +0 -0
  22. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/io.py +0 -0
  23. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/logging.py +0 -0
  24. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/noise.py +0 -0
  25. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/normalize.py +0 -0
  26. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/output.py +0 -0
  27. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/sampling.py +0 -0
  28. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/common/transform.py +0 -0
  29. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/__init__.py +0 -0
  30. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  31. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
  32. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/augmentations.py +0 -0
  33. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/capture.py +0 -0
  34. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  35. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  36. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  37. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  38. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/foggify.py +0 -0
  39. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/foggify_logging.py +0 -0
  40. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/fog/logging.py +0 -0
  41. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/radial/__init__.py +0 -0
  42. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/radial/transform.py +0 -0
  43. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  44. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess/sky_depth/transform.py +0 -0
  45. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  46. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  47. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess.egg-info/requires.txt +0 -0
  48. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  49. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/setup.cfg +0 -0
  50. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_airlight_fallback.py +0 -0
  51. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_cli_sample_selection.py +0 -0
  52. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  53. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_fog_aware_auto_exposure.py +0 -0
  54. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_foggify_integration.py +0 -0
  55. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_radial.py +0 -0
  56. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_scene_illumination.py +0 -0
  57. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_sensor_identity.py +0 -0
  58. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_sky_depth.py +0 -0
  59. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_source_backed_output.py +0 -0
  60. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_tone_map_lut.py +0 -0
  61. {euler_preprocess-3.6.0 → euler_preprocess-3.9.0}/tests/test_zip_output.py +0 -0
@@ -0,0 +1 @@
1
+ recursive-exclude tests/fixtures *.zip
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-preprocess
3
- Version: 3.6.0
3
+ Version: 3.9.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
@@ -264,6 +264,7 @@ def _render_cpu(
264
264
  intrinsics=intrinsics_np,
265
265
  airlight_method=plan.airlight_method,
266
266
  capture_artifacts=plan.capture_artifacts,
267
+ clear_weather=plan.clear_weather,
267
268
  )
268
269
  return FogInferenceResult(
269
270
  rgb=np.asarray(result.rgb, dtype=np.float32),
@@ -300,30 +301,25 @@ def _render_gpu(
300
301
  depth_t = planar_to_radial_depth_torch(depth_t, K_t)
301
302
 
302
303
  sky_mask_t = torch.from_numpy(sky_mask).to(device=device, dtype=torch.bool)
303
- estimated_airlight = transform._estimate_airlight_torch(
304
- rgb_t,
305
- sky_mask_t,
306
- sample_id=sample_id,
307
- method=plan.airlight_method,
308
- )
309
304
  torch_gen = torch_generator_for_index(
310
305
  transform.torch_device,
311
306
  transform.seed,
312
307
  transform.base_rng,
313
308
  sample_index,
314
309
  )
315
- foggy_t, beta, airlight_t, k_map_t, ls_map_t = transform._apply_model_torch(
310
+ foggy_t, beta, airlight_t, k_map_t, ls_map_t = transform._process_torch_pipeline(
316
311
  rgb_t,
317
312
  depth_t,
313
+ sky_mask_t,
318
314
  plan.model_name,
319
315
  plan.model_cfg,
320
316
  rng,
321
- estimated_airlight,
322
317
  torch_gen,
323
318
  sample_id=sample_id,
324
319
  intrinsics=intrinsics_np,
325
- depth_m=depth_t,
320
+ airlight_method=plan.airlight_method,
326
321
  capture_artifacts=plan.capture_artifacts,
322
+ clear_weather=plan.clear_weather,
327
323
  )
328
324
  return FogInferenceResult(
329
325
  rgb=torch.clamp(foggy_t, 0.0, 1.0).detach().cpu().numpy().astype(np.float32),
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import math
5
+ from typing import Any
5
6
 
6
7
  import numpy as np
7
8
 
@@ -1088,6 +1089,115 @@ def apply_scene_illumination_np(
1088
1089
  return (radiance * scale[..., None]).astype(np.float32, copy=False), ev_map
1089
1090
 
1090
1091
 
1092
+ def apply_scene_illumination_torch(
1093
+ rgb_lin: "torch.Tensor",
1094
+ depth_m: "torch.Tensor",
1095
+ k_field,
1096
+ model_cfg: dict,
1097
+ rng: np.random.Generator,
1098
+ sky_mask: "torch.Tensor | None" = None,
1099
+ ) -> tuple["torch.Tensor", "torch.Tensor"]:
1100
+ """Torch equivalent of :func:`apply_scene_illumination_np`."""
1101
+ height, width = int(depth_m.shape[0]), int(depth_m.shape[1])
1102
+ cfg = _resolve_scene_illumination_config(model_cfg)
1103
+ enabled = cfg.get("enabled", False)
1104
+ if not isinstance(enabled, bool):
1105
+ raise ValueError("scene_illumination.enabled must be a boolean")
1106
+ if not enabled:
1107
+ return rgb_lin.to(dtype=torch.float32), torch.zeros(
1108
+ (height, width),
1109
+ device=rgb_lin.device,
1110
+ dtype=torch.float32,
1111
+ )
1112
+
1113
+ global_ev = _sample_float(
1114
+ cfg.get("global_ev", 0.0),
1115
+ rng,
1116
+ "scene_illumination.global_ev",
1117
+ )
1118
+ near_ev = _sample_float(
1119
+ cfg.get("near_ev", 0.0),
1120
+ rng,
1121
+ "scene_illumination.near_ev",
1122
+ )
1123
+ decay_depth = max(
1124
+ _sample_float(
1125
+ cfg.get("near_decay_depth_m", 15.0),
1126
+ rng,
1127
+ "scene_illumination.near_decay_depth_m",
1128
+ ),
1129
+ 1e-6,
1130
+ )
1131
+ fog_ev = _sample_float(
1132
+ cfg.get("fog_coupled_ev", 0.0),
1133
+ rng,
1134
+ "scene_illumination.fog_coupled_ev",
1135
+ )
1136
+
1137
+ depth = _sanitize_depth_torch(depth_m).to(
1138
+ device=rgb_lin.device,
1139
+ dtype=rgb_lin.dtype,
1140
+ )
1141
+ k_map = torch.clamp(
1142
+ broadcast_k_field_torch(
1143
+ k_field,
1144
+ height,
1145
+ width,
1146
+ device=rgb_lin.device,
1147
+ dtype=rgb_lin.dtype,
1148
+ ),
1149
+ min=0.0,
1150
+ )
1151
+ near_weight = torch.exp(-depth / decay_depth)
1152
+ fog_opacity = 1.0 - torch.exp(-k_map * depth)
1153
+ ev_map = (
1154
+ global_ev + near_ev * near_weight + fog_ev * torch.clamp(fog_opacity, 0.0, 1.0)
1155
+ )
1156
+
1157
+ if sky_mask is not None:
1158
+ sky = torch.as_tensor(
1159
+ sky_mask,
1160
+ device=rgb_lin.device,
1161
+ dtype=rgb_lin.dtype,
1162
+ )
1163
+ if tuple(sky.shape) != (height, width):
1164
+ raise ValueError(
1165
+ f"sky_mask must have shape ({height}, {width}); got {tuple(sky.shape)}"
1166
+ )
1167
+ sky_weight = float(
1168
+ np.clip(
1169
+ _sample_float(
1170
+ cfg.get("sky_weight", 0.0),
1171
+ rng,
1172
+ "scene_illumination.sky_weight",
1173
+ ),
1174
+ 0.0,
1175
+ 1.0,
1176
+ )
1177
+ )
1178
+ ev_map = ev_map * (1.0 - torch.clamp(sky, 0.0, 1.0) * (1.0 - sky_weight))
1179
+
1180
+ min_scale = max(
1181
+ _sample_float(
1182
+ cfg.get("min_radiance_scale", 0.08),
1183
+ rng,
1184
+ "scene_illumination.min_radiance_scale",
1185
+ ),
1186
+ 0.0,
1187
+ )
1188
+ scale = torch.maximum(
1189
+ torch.exp2(-ev_map),
1190
+ torch.full_like(ev_map, min_scale),
1191
+ )
1192
+ radiance = torch.nan_to_num(
1193
+ rgb_lin.to(dtype=torch.float32),
1194
+ nan=0.0,
1195
+ posinf=1.0,
1196
+ neginf=0.0,
1197
+ )
1198
+ return radiance * scale[..., None], ev_map
1199
+
1200
+
1091
1201
  def _apply_scene_airlight_dampening_np(
1092
1202
  ls_field: np.ndarray,
1093
1203
  ev_map: np.ndarray,
@@ -1109,6 +1219,33 @@ def _apply_scene_airlight_dampening_np(
1109
1219
  return np.clip(ls_map * scale[..., None], 0.0, 1.0).astype(np.float32, copy=False)
1110
1220
 
1111
1221
 
1222
+ def _apply_scene_airlight_dampening_torch(
1223
+ ls_field: "torch.Tensor",
1224
+ ev_map: "torch.Tensor",
1225
+ model_cfg: dict,
1226
+ rng: np.random.Generator,
1227
+ ) -> "torch.Tensor":
1228
+ cfg = _resolve_scene_illumination_config(model_cfg)
1229
+ if not bool(cfg.get("enabled", False)):
1230
+ return ls_field.to(dtype=torch.float32)
1231
+ ratio = _sample_float(
1232
+ cfg.get("airlight_ev_ratio", 0.0),
1233
+ rng,
1234
+ "scene_illumination.airlight_ev_ratio",
1235
+ )
1236
+ if ratio <= 0.0:
1237
+ return ls_field.to(dtype=torch.float32)
1238
+ scale = torch.exp2(-torch.clamp(ev_map, min=0.0) * ratio)
1239
+ ls_map = broadcast_ls_field_torch(
1240
+ ls_field,
1241
+ int(ev_map.shape[0]),
1242
+ int(ev_map.shape[1]),
1243
+ device=ev_map.device,
1244
+ dtype=ev_map.dtype,
1245
+ )
1246
+ return torch.clamp(ls_map * scale[..., None], 0.0, 1.0)
1247
+
1248
+
1112
1249
  def apply_fog(
1113
1250
  rgb: np.ndarray, depth_m: np.ndarray, k_field: np.ndarray, ls_field: np.ndarray
1114
1251
  ) -> np.ndarray:
@@ -1140,6 +1277,127 @@ def apply_fog_torch(
1140
1277
  return rgb * t + ls_field * (1.0 - t)
1141
1278
 
1142
1279
 
1280
+ def broadcast_k_field_torch(
1281
+ k_field,
1282
+ height: int,
1283
+ width: int,
1284
+ *,
1285
+ device,
1286
+ dtype,
1287
+ ) -> "torch.Tensor":
1288
+ """Return ``k_field`` as a Torch ``(H, W)`` map."""
1289
+ field = torch.as_tensor(k_field, device=device, dtype=dtype)
1290
+ if field.ndim == 0:
1291
+ return field.expand(height, width)
1292
+ if tuple(field.shape) == (height, width):
1293
+ return field
1294
+ raise ValueError(
1295
+ f"k_field must be scalar or shape ({height}, {width}); got {tuple(field.shape)}"
1296
+ )
1297
+
1298
+
1299
+ def broadcast_ls_field_torch(
1300
+ ls_field,
1301
+ height: int,
1302
+ width: int,
1303
+ *,
1304
+ device,
1305
+ dtype,
1306
+ ) -> "torch.Tensor":
1307
+ """Return ``ls_field`` as a Torch ``(H, W, 3)`` map."""
1308
+ field = torch.as_tensor(ls_field, device=device, dtype=dtype)
1309
+ if tuple(field.shape) == (3,):
1310
+ return field.view(1, 1, 3).expand(height, width, 3)
1311
+ if tuple(field.shape) == (1, 1, 3):
1312
+ return field.expand(height, width, 3)
1313
+ if tuple(field.shape) == (height, width, 3):
1314
+ return field
1315
+ raise ValueError(
1316
+ f"ls_field must have shape (3,), (1, 1, 3), or "
1317
+ f"({height}, {width}, 3); got {tuple(field.shape)}"
1318
+ )
1319
+
1320
+
1321
+ def render_fog_fields_np(
1322
+ rgb: np.ndarray,
1323
+ depth_m: np.ndarray,
1324
+ k_field,
1325
+ ls_field: np.ndarray,
1326
+ model_cfg: dict,
1327
+ rng: np.random.Generator,
1328
+ *,
1329
+ sky_mask: np.ndarray | None = None,
1330
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
1331
+ """Render resolved fog fields with the shared CPU scene semantics."""
1332
+ height, width = depth_m.shape
1333
+ scene_rgb, ev_map = apply_scene_illumination_np(
1334
+ rgb,
1335
+ depth_m,
1336
+ k_field,
1337
+ model_cfg,
1338
+ rng,
1339
+ sky_mask=sky_mask,
1340
+ )
1341
+ rendered_ls_field = _apply_scene_airlight_dampening_np(
1342
+ ls_field,
1343
+ ev_map,
1344
+ model_cfg,
1345
+ rng,
1346
+ )
1347
+ foggy = apply_fog(scene_rgb, depth_m, k_field, rendered_ls_field)
1348
+ return (
1349
+ foggy,
1350
+ broadcast_k_field(k_field, height, width),
1351
+ broadcast_ls_field(rendered_ls_field, height, width),
1352
+ )
1353
+
1354
+
1355
+ def render_fog_fields_torch(
1356
+ rgb: "torch.Tensor",
1357
+ depth_m: "torch.Tensor",
1358
+ k_field,
1359
+ ls_field: "torch.Tensor",
1360
+ model_cfg: dict,
1361
+ rng: np.random.Generator,
1362
+ *,
1363
+ sky_mask: "torch.Tensor | None" = None,
1364
+ ) -> tuple["torch.Tensor", "torch.Tensor", "torch.Tensor"]:
1365
+ """Render resolved fog fields with CPU-equivalent Torch semantics."""
1366
+ height, width = int(depth_m.shape[0]), int(depth_m.shape[1])
1367
+ scene_rgb, ev_map = apply_scene_illumination_torch(
1368
+ rgb,
1369
+ depth_m,
1370
+ k_field,
1371
+ model_cfg,
1372
+ rng,
1373
+ sky_mask=sky_mask,
1374
+ )
1375
+ rendered_ls_field = _apply_scene_airlight_dampening_torch(
1376
+ ls_field,
1377
+ ev_map,
1378
+ model_cfg,
1379
+ rng,
1380
+ )
1381
+ foggy = apply_fog_torch(scene_rgb, depth_m, k_field, rendered_ls_field)
1382
+ return (
1383
+ foggy,
1384
+ broadcast_k_field_torch(
1385
+ k_field,
1386
+ height,
1387
+ width,
1388
+ device=rgb.device,
1389
+ dtype=rgb.dtype,
1390
+ ),
1391
+ broadcast_ls_field_torch(
1392
+ rendered_ls_field,
1393
+ height,
1394
+ width,
1395
+ device=rgb.device,
1396
+ dtype=rgb.dtype,
1397
+ ),
1398
+ )
1399
+
1400
+
1143
1401
  def select_model(config: dict, rng: np.random.Generator) -> str:
1144
1402
  selection = config.get("selection")
1145
1403
  if selection is None:
@@ -1288,16 +1546,13 @@ def apply_model(
1288
1546
  else:
1289
1547
  ls_field = ls_base.reshape(1, 1, 3)
1290
1548
 
1291
- scene_rgb, ev_map = apply_scene_illumination_np(
1549
+ foggy, k_map, ls_map = render_fog_fields_np(
1292
1550
  rgb,
1293
1551
  depth_m,
1294
1552
  k_field,
1553
+ ls_field,
1295
1554
  model_cfg,
1296
1555
  rng,
1297
1556
  sky_mask=sky_mask,
1298
1557
  )
1299
- ls_field = _apply_scene_airlight_dampening_np(ls_field, ev_map, model_cfg, rng)
1300
- foggy = apply_fog(scene_rgb, depth_m, k_field, ls_field)
1301
- k_map = broadcast_k_field(k_field, height, width)
1302
- ls_map = broadcast_ls_field(ls_field, height, width)
1303
1558
  return foggy, k_mean, ls_base, k_map, ls_map
@@ -5,7 +5,12 @@ from typing import Any
5
5
 
6
6
  import numpy as np
7
7
 
8
- from euler_preprocess.common.color import linear_to_srgb, srgb_to_linear
8
+ from euler_preprocess.common.color import (
9
+ linear_to_srgb,
10
+ linear_to_srgb_torch,
11
+ srgb_to_linear,
12
+ srgb_to_linear_torch,
13
+ )
9
14
  from euler_preprocess.fog.atmospheric_light import AtmosphericLightResolver
10
15
  from euler_preprocess.fog.capture import CaptureArtifactPipeline, CaptureContext
11
16
  from euler_preprocess.fog.models import apply_model
@@ -71,9 +76,7 @@ class FogProcessingPipeline:
71
76
  sample_id: str | None,
72
77
  airlight_method: str | None = None,
73
78
  ) -> FogPipelineResult:
74
- render_input_space = _normalize_render_input_space(
75
- model_cfg.get("render_input_space", self.render_input_space)
76
- )
79
+ render_input_space = self.render_input_space_for(model_cfg)
77
80
  rgb_for_render = srgb_to_linear(rgb) if render_input_space == "srgb" else rgb
78
81
  estimated_airlight = self.atmospheric_light.estimate_np(
79
82
  rgb_for_render,
@@ -101,6 +104,61 @@ class FogProcessingPipeline:
101
104
  ls_map=ls_map,
102
105
  )
103
106
 
107
+ def render_input_space_for(self, model_cfg: dict) -> str:
108
+ """Resolve the input encoding used by both scene-rendering backends."""
109
+ return _normalize_render_input_space(
110
+ model_cfg.get("render_input_space", self.render_input_space)
111
+ )
112
+
113
+ def prepare_render_rgb_torch(self, rgb, model_cfg: dict):
114
+ """Convert display RGB to the scene-linear domain when configured."""
115
+ if self.render_input_space_for(model_cfg) == "srgb":
116
+ return srgb_to_linear_torch(rgb)
117
+ return rgb
118
+
119
+ def restore_render_rgb_torch(
120
+ self,
121
+ rgb,
122
+ model_cfg: dict,
123
+ *,
124
+ clear_weather: bool = False,
125
+ ):
126
+ """Return a rendered Torch image to the CPU pathway's output domain."""
127
+ if not clear_weather and self.render_input_space_for(model_cfg) == "srgb":
128
+ return linear_to_srgb_torch(rgb)
129
+ return rgb
130
+
131
+ def capture_context(
132
+ self,
133
+ *,
134
+ sample_id: str | None,
135
+ rng: np.random.Generator,
136
+ model_cfg: dict,
137
+ sky_mask,
138
+ airlight,
139
+ intrinsics=None,
140
+ depth_m=None,
141
+ k_map=None,
142
+ device=None,
143
+ ) -> CaptureContext:
144
+ """Build the complete capture context shared by CPU and Torch routes."""
145
+ return CaptureContext(
146
+ sample_id=sample_id,
147
+ rng=rng,
148
+ device=device,
149
+ intrinsics=intrinsics,
150
+ depth_m=depth_m,
151
+ k_map=k_map,
152
+ attributes={
153
+ "sky_mask": sky_mask,
154
+ "airlight": airlight,
155
+ "render_input_space": model_cfg.get(
156
+ "render_input_space",
157
+ self.render_input_space,
158
+ ),
159
+ },
160
+ )
161
+
104
162
  def process_np(
105
163
  self,
106
164
  *,
@@ -114,33 +172,39 @@ class FogProcessingPipeline:
114
172
  intrinsics: np.ndarray | None = None,
115
173
  airlight_method: str | None = None,
116
174
  capture_artifacts: CaptureArtifactPipeline | None = None,
175
+ clear_weather: bool = False,
117
176
  ) -> FogPipelineResult:
118
- result = self.render_scene_np(
119
- rgb=rgb,
120
- depth_m=depth_m,
121
- sky_mask=sky_mask,
122
- model_name=model_name,
123
- model_cfg=model_cfg,
124
- rng=rng,
125
- sample_id=sample_id,
126
- airlight_method=airlight_method,
127
- )
177
+ if clear_weather:
178
+ height, width = depth_m.shape
179
+ result = FogPipelineResult(
180
+ rgb=rgb,
181
+ beta=0.0,
182
+ airlight=np.zeros(3, dtype=np.float32),
183
+ k_map=np.zeros((height, width), dtype=np.float32),
184
+ ls_map=np.zeros((height, width, 3), dtype=np.float32),
185
+ )
186
+ else:
187
+ result = self.render_scene_np(
188
+ rgb=rgb,
189
+ depth_m=depth_m,
190
+ sky_mask=sky_mask,
191
+ model_name=model_name,
192
+ model_cfg=model_cfg,
193
+ rng=rng,
194
+ sample_id=sample_id,
195
+ airlight_method=airlight_method,
196
+ )
128
197
  return self.apply_capture_np(
129
198
  result,
130
- context=CaptureContext(
199
+ context=self.capture_context(
131
200
  sample_id=sample_id,
132
201
  rng=rng,
202
+ model_cfg=model_cfg,
203
+ sky_mask=sky_mask,
204
+ airlight=result.airlight,
133
205
  intrinsics=intrinsics,
134
206
  depth_m=depth_m,
135
207
  k_map=result.k_map,
136
- attributes={
137
- "sky_mask": sky_mask,
138
- "airlight": result.airlight,
139
- "render_input_space": model_cfg.get(
140
- "render_input_space",
141
- self.render_input_space,
142
- ),
143
- },
144
208
  ),
145
209
  capture_artifacts=capture_artifacts,
146
210
  )