euler-preprocess 3.2.0__tar.gz → 3.3.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.2.0/euler_preprocess.egg-info → euler_preprocess-3.3.0}/PKG-INFO +23 -1
  2. euler_preprocess-3.2.0/PKG-INFO → euler_preprocess-3.3.0/README.md +22 -14
  3. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/capture.py +335 -0
  4. euler_preprocess-3.2.0/README.md → euler_preprocess-3.3.0/euler_preprocess.egg-info/PKG-INFO +36 -0
  5. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/pyproject.toml +1 -1
  6. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_fog_aux_outputs.py +89 -0
  7. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/__init__.py +0 -0
  8. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/cli.py +0 -0
  9. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/__init__.py +0 -0
  10. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/dataset.py +0 -0
  11. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/device.py +0 -0
  12. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/intrinsics.py +0 -0
  13. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/io.py +0 -0
  14. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/logging.py +0 -0
  15. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/noise.py +0 -0
  16. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/normalize.py +0 -0
  17. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/output.py +0 -0
  18. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/sampling.py +0 -0
  19. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/common/transform.py +0 -0
  20. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/__init__.py +0 -0
  21. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  22. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
  23. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/augmentations.py +0 -0
  24. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  25. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  26. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  27. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  28. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/foggify.py +0 -0
  29. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/foggify_logging.py +0 -0
  30. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/inference.py +0 -0
  31. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/logging.py +0 -0
  32. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/models.py +0 -0
  33. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/pipeline.py +0 -0
  34. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/fog/transform.py +0 -0
  35. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/radial/__init__.py +0 -0
  36. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/radial/transform.py +0 -0
  37. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  38. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess/sky_depth/transform.py +0 -0
  39. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess.egg-info/SOURCES.txt +0 -0
  40. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  41. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  42. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess.egg-info/requires.txt +0 -0
  43. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  44. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/setup.cfg +0 -0
  45. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_airlight_fallback.py +0 -0
  46. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_cli_sample_selection.py +0 -0
  47. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  48. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_foggify_integration.py +0 -0
  49. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_radial.py +0 -0
  50. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_sky_depth.py +0 -0
  51. {euler_preprocess-3.2.0 → euler_preprocess-3.3.0}/tests/test_source_backed_output.py +0 -0
  52. {euler_preprocess-3.2.0 → euler_preprocess-3.3.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.2.0
3
+ Version: 3.3.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
@@ -323,6 +323,28 @@ noise is fine-grained rather than blocky. `black_noise_floor` with
323
323
  noise in near-clipped black regions, so the strongest visible noise sits in
324
324
  dim-but-readable shadows.
325
325
 
326
+ Set `sensor.noise_adjustment` for relative, scenario-level noise controls on
327
+ top of the selected camera/condition profile. `level: 1.0` leaves the authored
328
+ profile unchanged; lower values suppress read/static/chroma noise and higher
329
+ values amplify it. `static_chroma_bias` ranges from `-1.0` for more fixed
330
+ pattern, row/column, banding, and bad-pixel noise to `1.0` for more
331
+ chromatic/high-ISO-looking shadow noise:
332
+
333
+ ```json
334
+ {
335
+ "capture_overrides": {
336
+ "sensor": {
337
+ "condition_profile": "nominal_gloom",
338
+ "noise_adjustment": {
339
+ "enabled": true,
340
+ "level": 1.25,
341
+ "static_chroma_bias": 0.35
342
+ }
343
+ }
344
+ }
345
+ }
346
+ ```
347
+
326
348
  Any stage can define `condition_profiles` to sample coherent per-image settings
327
349
  before the stage runs. This is useful for exposure states where ISO, exposure
328
350
  gain, read noise, banding, and dark/fog noise modulation should move together:
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: euler-preprocess
3
- Version: 3.2.0
4
- Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
5
- Requires-Python: >=3.9
6
- Description-Content-Type: text/markdown
7
- Requires-Dist: numpy
8
- Requires-Dist: Pillow
9
- Requires-Dist: euler-loading
10
- Provides-Extra: gpu
11
- Requires-Dist: torch; extra == "gpu"
12
- Provides-Extra: progress
13
- Requires-Dist: tqdm; extra == "progress"
14
-
15
1
  # euler-preprocess
16
2
 
17
3
  Physics-based preprocessing transforms for multi-modal RGB+depth datasets. Built on top of [euler-loading](https://github.com/d-rothen/euler-loading) and [ds-crawler](https://github.com/d-rothen/ds-crawler).
@@ -323,6 +309,28 @@ noise is fine-grained rather than blocky. `black_noise_floor` with
323
309
  noise in near-clipped black regions, so the strongest visible noise sits in
324
310
  dim-but-readable shadows.
325
311
 
312
+ Set `sensor.noise_adjustment` for relative, scenario-level noise controls on
313
+ top of the selected camera/condition profile. `level: 1.0` leaves the authored
314
+ profile unchanged; lower values suppress read/static/chroma noise and higher
315
+ values amplify it. `static_chroma_bias` ranges from `-1.0` for more fixed
316
+ pattern, row/column, banding, and bad-pixel noise to `1.0` for more
317
+ chromatic/high-ISO-looking shadow noise:
318
+
319
+ ```json
320
+ {
321
+ "capture_overrides": {
322
+ "sensor": {
323
+ "condition_profile": "nominal_gloom",
324
+ "noise_adjustment": {
325
+ "enabled": true,
326
+ "level": 1.25,
327
+ "static_chroma_bias": 0.35
328
+ }
329
+ }
330
+ }
331
+ }
332
+ ```
333
+
326
334
  Any stage can define `condition_profiles` to sample coherent per-image settings
327
335
  before the stage runs. This is useful for exposure states where ISO, exposure
328
336
  gain, read noise, banding, and dark/fog noise modulation should move together:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from copy import deepcopy
3
4
  import io
4
5
  import math
5
6
  from dataclasses import dataclass, field
@@ -522,6 +523,7 @@ class SensorStage(ConfiguredCaptureStage):
522
523
  rng,
523
524
  manual_exposure_gain=exposure,
524
525
  )
526
+ config = _apply_sensor_noise_adjustment(config, rng)
525
527
  wb = _sample_triplet(config.get("white_balance", [1.0, 1.0, 1.0]), rng)
526
528
  wb_jitter = _sample_float(config, "white_balance_jitter", 0.0, rng)
527
529
  if wb_jitter > 0.0:
@@ -717,6 +719,7 @@ class SensorStage(ConfiguredCaptureStage):
717
719
  rng,
718
720
  manual_exposure_gain=exposure,
719
721
  )
722
+ config = _apply_sensor_noise_adjustment(config, rng)
720
723
  wb = _sample_triplet(config.get("white_balance", [1.0, 1.0, 1.0]), rng)
721
724
  wb_jitter = _sample_float(config, "white_balance_jitter", 0.0, rng)
722
725
  if wb_jitter > 0.0:
@@ -2152,6 +2155,338 @@ def _resolve_electron_capacity_torch(
2152
2155
  )
2153
2156
 
2154
2157
 
2158
+ def _apply_sensor_noise_adjustment(
2159
+ config: Mapping[str, Any],
2160
+ rng: np.random.Generator,
2161
+ ) -> Mapping[str, Any]:
2162
+ cfg = _config_block(config, "noise_adjustment")
2163
+ if not cfg or not _bool_value(cfg.get("enabled", True)):
2164
+ return config
2165
+
2166
+ level = max(
2167
+ _sample_float_from_keys(
2168
+ cfg,
2169
+ ("level", "amount", "factor", "noise_level"),
2170
+ 1.0,
2171
+ rng,
2172
+ ),
2173
+ 0.0,
2174
+ )
2175
+ bias = float(
2176
+ np.clip(
2177
+ _sample_float_from_keys(
2178
+ cfg,
2179
+ ("static_chroma_bias", "character_bias", "chroma_bias"),
2180
+ 0.0,
2181
+ rng,
2182
+ ),
2183
+ -1.0,
2184
+ 1.0,
2185
+ )
2186
+ )
2187
+ groups = _config_block(cfg, "groups")
2188
+ limits = _config_block(cfg, "limits")
2189
+
2190
+ min_factor = max(_sample_float(limits, "min_factor", 0.0, rng), 0.0)
2191
+ max_factor = max(_sample_float(limits, "max_factor", 8.0, rng), min_factor)
2192
+
2193
+ def group_factor(name: str, base: float) -> float:
2194
+ factor = base * max(_sample_float(groups, name, 1.0, rng), 0.0)
2195
+ return float(np.clip(factor, min_factor, max_factor))
2196
+
2197
+ read_factor = group_factor("read", level * math.pow(2.0, 0.35 * bias))
2198
+ static_factor = group_factor("static", level * math.pow(2.0, -bias))
2199
+ banding_factor = group_factor("banding", static_factor)
2200
+ bad_pixel_factor = group_factor("bad_pixels", static_factor)
2201
+ chroma_factor = group_factor("chroma", level * math.pow(2.0, bias))
2202
+ shadow_luma_factor = group_factor("shadow_luma", level)
2203
+ modulation_factor = group_factor("modulation", 1.0 + (level - 1.0) * 0.75)
2204
+
2205
+ if (
2206
+ abs(read_factor - 1.0) < 1e-9
2207
+ and abs(static_factor - 1.0) < 1e-9
2208
+ and abs(banding_factor - 1.0) < 1e-9
2209
+ and abs(bad_pixel_factor - 1.0) < 1e-9
2210
+ and abs(chroma_factor - 1.0) < 1e-9
2211
+ and abs(shadow_luma_factor - 1.0) < 1e-9
2212
+ and abs(modulation_factor - 1.0) < 1e-9
2213
+ ):
2214
+ return config
2215
+
2216
+ adjusted = deepcopy(dict(config))
2217
+
2218
+ _scale_config_path(adjusted, ("read_noise_electrons",), read_factor, min_value=0.0)
2219
+ _scale_config_path(adjusted, ("read_noise_sigma",), read_factor, min_value=0.0)
2220
+
2221
+ _scale_config_path(adjusted, ("fixed_pattern_sigma",), static_factor, min_value=0.0)
2222
+ _scale_config_path(adjusted, ("row_noise_sigma",), banding_factor, min_value=0.0)
2223
+ _scale_config_path(adjusted, ("column_noise_sigma",), banding_factor, min_value=0.0)
2224
+ _scale_config_path(
2225
+ adjusted,
2226
+ ("banding_modulation",),
2227
+ banding_factor,
2228
+ min_value=0.0,
2229
+ max_value=1.0,
2230
+ )
2231
+
2232
+ max_bad_pixel_probability = _sample_float(
2233
+ limits,
2234
+ "max_bad_pixel_probability",
2235
+ 0.05,
2236
+ rng,
2237
+ )
2238
+ _scale_config_path(
2239
+ adjusted,
2240
+ ("hot_pixel_probability",),
2241
+ bad_pixel_factor,
2242
+ min_value=0.0,
2243
+ max_value=max_bad_pixel_probability,
2244
+ )
2245
+ _scale_config_path(
2246
+ adjusted,
2247
+ ("dead_pixel_probability",),
2248
+ bad_pixel_factor,
2249
+ min_value=0.0,
2250
+ max_value=max_bad_pixel_probability,
2251
+ )
2252
+
2253
+ for key in ("dark_gain", "depth_gain", "fog_gain"):
2254
+ _scale_config_path(
2255
+ adjusted,
2256
+ ("noise_modulation", key),
2257
+ modulation_factor,
2258
+ min_value=0.0,
2259
+ )
2260
+ _scale_config_path(
2261
+ adjusted,
2262
+ ("noise_modulation", "max_gain"),
2263
+ modulation_factor,
2264
+ anchor=1.0,
2265
+ min_value=1.0,
2266
+ )
2267
+
2268
+ _scale_config_path(
2269
+ adjusted,
2270
+ ("shadow_recovery_noise", "luma_sigma"),
2271
+ shadow_luma_factor,
2272
+ min_value=0.0,
2273
+ )
2274
+ _scale_config_path(
2275
+ adjusted,
2276
+ ("shadow_recovery_noise", "chroma_sigma"),
2277
+ chroma_factor,
2278
+ min_value=0.0,
2279
+ )
2280
+ _scale_config_path(
2281
+ adjusted,
2282
+ ("shadow_recovery_noise", "blotch_sigma"),
2283
+ chroma_factor,
2284
+ min_value=0.0,
2285
+ )
2286
+ _scale_config_path(
2287
+ adjusted,
2288
+ ("shadow_recovery_noise", "red_chroma_gain"),
2289
+ chroma_factor,
2290
+ anchor=1.0,
2291
+ min_value=0.0,
2292
+ )
2293
+ _scale_config_path(
2294
+ adjusted,
2295
+ ("shadow_recovery_noise", "blue_chroma_gain"),
2296
+ chroma_factor,
2297
+ anchor=1.0,
2298
+ min_value=0.0,
2299
+ )
2300
+ _scale_config_path(
2301
+ adjusted,
2302
+ ("shadow_recovery_noise", "chroma_axis_correlation"),
2303
+ chroma_factor,
2304
+ min_value=-0.95,
2305
+ max_value=0.95,
2306
+ )
2307
+ return adjusted
2308
+
2309
+
2310
+ def _sample_float_from_keys(
2311
+ config: Mapping[str, Any],
2312
+ keys: tuple[str, ...],
2313
+ default: float,
2314
+ rng: np.random.Generator,
2315
+ ) -> float:
2316
+ for key in keys:
2317
+ if config.get(key) is not None:
2318
+ return _sample_float(config, key, default, rng)
2319
+ return float(default)
2320
+
2321
+
2322
+ def _scale_config_path(
2323
+ config: dict[str, Any],
2324
+ path: tuple[str, ...],
2325
+ factor: float,
2326
+ *,
2327
+ anchor: float = 0.0,
2328
+ min_value: float | None = None,
2329
+ max_value: float | None = None,
2330
+ ) -> None:
2331
+ parent: dict[str, Any] = config
2332
+ for key in path[:-1]:
2333
+ value = parent.get(key)
2334
+ if not isinstance(value, dict):
2335
+ return
2336
+ parent = value
2337
+ key = path[-1]
2338
+ if key not in parent or parent[key] is None:
2339
+ return
2340
+ parent[key] = _scale_numeric_spec(
2341
+ parent[key],
2342
+ factor,
2343
+ anchor=anchor,
2344
+ min_value=min_value,
2345
+ max_value=max_value,
2346
+ )
2347
+
2348
+
2349
+ def _scale_numeric_spec(
2350
+ spec: Any,
2351
+ factor: float,
2352
+ *,
2353
+ anchor: float = 0.0,
2354
+ min_value: float | None = None,
2355
+ max_value: float | None = None,
2356
+ ) -> Any:
2357
+ if isinstance(spec, bool) or spec is None:
2358
+ return spec
2359
+ if isinstance(spec, (int, float)):
2360
+ return _clamp_scaled_number(
2361
+ anchor + (float(spec) - anchor) * factor,
2362
+ min_value,
2363
+ max_value,
2364
+ )
2365
+ if isinstance(spec, list):
2366
+ return [
2367
+ _scale_numeric_spec(
2368
+ value,
2369
+ factor,
2370
+ anchor=anchor,
2371
+ min_value=min_value,
2372
+ max_value=max_value,
2373
+ )
2374
+ for value in spec
2375
+ ]
2376
+ if isinstance(spec, tuple):
2377
+ return tuple(
2378
+ _scale_numeric_spec(
2379
+ value,
2380
+ factor,
2381
+ anchor=anchor,
2382
+ min_value=min_value,
2383
+ max_value=max_value,
2384
+ )
2385
+ for value in spec
2386
+ )
2387
+ if not isinstance(spec, dict):
2388
+ return spec
2389
+
2390
+ scaled = dict(spec)
2391
+ dist = scaled.get("dist")
2392
+ if dist is None:
2393
+ if "value" in scaled:
2394
+ scaled["value"] = _scale_numeric_spec(
2395
+ scaled["value"],
2396
+ factor,
2397
+ anchor=anchor,
2398
+ min_value=min_value,
2399
+ max_value=max_value,
2400
+ )
2401
+ return scaled
2402
+
2403
+ if dist == "constant":
2404
+ scaled["value"] = _scale_numeric_spec(
2405
+ scaled.get("value", 0.0),
2406
+ factor,
2407
+ anchor=anchor,
2408
+ min_value=min_value,
2409
+ max_value=max_value,
2410
+ )
2411
+ elif dist == "uniform":
2412
+ scaled["min"] = _scale_numeric_spec(
2413
+ scaled["min"],
2414
+ factor,
2415
+ anchor=anchor,
2416
+ min_value=min_value,
2417
+ max_value=max_value,
2418
+ )
2419
+ scaled["max"] = _scale_numeric_spec(
2420
+ scaled["max"],
2421
+ factor,
2422
+ anchor=anchor,
2423
+ min_value=min_value,
2424
+ max_value=max_value,
2425
+ )
2426
+ if scaled["min"] > scaled["max"]:
2427
+ scaled["min"], scaled["max"] = scaled["max"], scaled["min"]
2428
+ elif dist == "normal":
2429
+ scaled["mean"] = _scale_numeric_spec(
2430
+ scaled["mean"],
2431
+ factor,
2432
+ anchor=anchor,
2433
+ min_value=min_value,
2434
+ max_value=max_value,
2435
+ )
2436
+ scaled["std"] = max(float(scaled.get("std", 0.0)) * abs(factor), 0.0)
2437
+ for key in ("min", "max"):
2438
+ if key in scaled and scaled[key] is not None:
2439
+ scaled[key] = _scale_numeric_spec(
2440
+ scaled[key],
2441
+ factor,
2442
+ anchor=anchor,
2443
+ min_value=min_value,
2444
+ max_value=max_value,
2445
+ )
2446
+ if scaled.get("min") is not None and scaled.get("max") is not None:
2447
+ if scaled["min"] > scaled["max"]:
2448
+ scaled["min"], scaled["max"] = scaled["max"], scaled["min"]
2449
+ elif dist == "lognormal":
2450
+ if anchor == 0.0 and factor > 0.0:
2451
+ scaled["mean"] = float(scaled.get("mean", 0.0)) + math.log(factor)
2452
+ for key in ("min", "max"):
2453
+ if key in scaled and scaled[key] is not None:
2454
+ scaled[key] = _scale_numeric_spec(
2455
+ scaled[key],
2456
+ factor,
2457
+ anchor=anchor,
2458
+ min_value=min_value,
2459
+ max_value=max_value,
2460
+ )
2461
+ if scaled.get("min") is not None and scaled.get("max") is not None:
2462
+ if scaled["min"] > scaled["max"]:
2463
+ scaled["min"], scaled["max"] = scaled["max"], scaled["min"]
2464
+ elif dist == "choice":
2465
+ scaled["values"] = [
2466
+ _scale_numeric_spec(
2467
+ value,
2468
+ factor,
2469
+ anchor=anchor,
2470
+ min_value=min_value,
2471
+ max_value=max_value,
2472
+ )
2473
+ for value in scaled.get("values", [])
2474
+ ]
2475
+ return scaled
2476
+
2477
+
2478
+ def _clamp_scaled_number(
2479
+ value: float,
2480
+ min_value: float | None,
2481
+ max_value: float | None,
2482
+ ) -> float:
2483
+ if min_value is not None:
2484
+ value = max(float(min_value), value)
2485
+ if max_value is not None:
2486
+ value = min(float(max_value), value)
2487
+ return float(value)
2488
+
2489
+
2155
2490
  def _sensor_noise_modulation(
2156
2491
  signal: np.ndarray,
2157
2492
  context: CaptureContext,
@@ -1,3 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: euler-preprocess
3
+ Version: 3.3.0
4
+ Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: numpy
8
+ Requires-Dist: Pillow
9
+ Requires-Dist: euler-loading
10
+ Provides-Extra: gpu
11
+ Requires-Dist: torch; extra == "gpu"
12
+ Provides-Extra: progress
13
+ Requires-Dist: tqdm; extra == "progress"
14
+
1
15
  # euler-preprocess
2
16
 
3
17
  Physics-based preprocessing transforms for multi-modal RGB+depth datasets. Built on top of [euler-loading](https://github.com/d-rothen/euler-loading) and [ds-crawler](https://github.com/d-rothen/ds-crawler).
@@ -309,6 +323,28 @@ noise is fine-grained rather than blocky. `black_noise_floor` with
309
323
  noise in near-clipped black regions, so the strongest visible noise sits in
310
324
  dim-but-readable shadows.
311
325
 
326
+ Set `sensor.noise_adjustment` for relative, scenario-level noise controls on
327
+ top of the selected camera/condition profile. `level: 1.0` leaves the authored
328
+ profile unchanged; lower values suppress read/static/chroma noise and higher
329
+ values amplify it. `static_chroma_bias` ranges from `-1.0` for more fixed
330
+ pattern, row/column, banding, and bad-pixel noise to `1.0` for more
331
+ chromatic/high-ISO-looking shadow noise:
332
+
333
+ ```json
334
+ {
335
+ "capture_overrides": {
336
+ "sensor": {
337
+ "condition_profile": "nominal_gloom",
338
+ "noise_adjustment": {
339
+ "enabled": true,
340
+ "level": 1.25,
341
+ "static_chroma_bias": 0.35
342
+ }
343
+ }
344
+ }
345
+ }
346
+ ```
347
+
312
348
  Any stage can define `condition_profiles` to sample coherent per-image settings
313
349
  before the stage runs. This is useful for exposure states where ISO, exposure
314
350
  gain, read noise, banding, and dark/fog noise modulation should move together:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "euler-preprocess"
3
- version = "3.2.0"
3
+ version = "3.3.0"
4
4
  description = "Physics-based preprocessing (fog, etc.) for RGB+depth datasets"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -1652,6 +1652,95 @@ def _deterministic_sensor_pipeline(
1652
1652
  )
1653
1653
 
1654
1654
 
1655
+ def test_sensor_noise_adjustment_scales_authored_noise_groups() -> None:
1656
+ config = {
1657
+ "read_noise_sigma": {"dist": "uniform", "min": 0.01, "max": 0.02},
1658
+ "fixed_pattern_sigma": 0.001,
1659
+ "row_noise_sigma": 0.002,
1660
+ "column_noise_sigma": 0.0015,
1661
+ "banding_modulation": 0.2,
1662
+ "noise_modulation": {
1663
+ "dark_gain": 1.0,
1664
+ "depth_gain": 0.4,
1665
+ "fog_gain": 0.6,
1666
+ "max_gain": 2.0,
1667
+ },
1668
+ "shadow_recovery_noise": {
1669
+ "luma_sigma": 0.003,
1670
+ "chroma_sigma": 0.004,
1671
+ "red_chroma_gain": 0.8,
1672
+ "blue_chroma_gain": 2.0,
1673
+ "chroma_axis_correlation": 0.2,
1674
+ "blotch_sigma": 0.005,
1675
+ },
1676
+ "hot_pixel_probability": 0.0002,
1677
+ "dead_pixel_probability": 0.0001,
1678
+ }
1679
+
1680
+ static = capture_module._apply_sensor_noise_adjustment(
1681
+ {
1682
+ **config,
1683
+ "noise_adjustment": {
1684
+ "level": 1.0,
1685
+ "static_chroma_bias": -1.0,
1686
+ },
1687
+ },
1688
+ np.random.default_rng(1),
1689
+ )
1690
+ chroma = capture_module._apply_sensor_noise_adjustment(
1691
+ {
1692
+ **config,
1693
+ "noise_adjustment": {
1694
+ "level": 1.0,
1695
+ "static_chroma_bias": 1.0,
1696
+ },
1697
+ },
1698
+ np.random.default_rng(1),
1699
+ )
1700
+
1701
+ assert static["row_noise_sigma"] > chroma["row_noise_sigma"]
1702
+ assert static["fixed_pattern_sigma"] > chroma["fixed_pattern_sigma"]
1703
+ assert static["hot_pixel_probability"] > chroma["hot_pixel_probability"]
1704
+ assert (
1705
+ static["shadow_recovery_noise"]["chroma_sigma"]
1706
+ < chroma["shadow_recovery_noise"]["chroma_sigma"]
1707
+ )
1708
+ assert (
1709
+ static["shadow_recovery_noise"]["blue_chroma_gain"]
1710
+ < chroma["shadow_recovery_noise"]["blue_chroma_gain"]
1711
+ )
1712
+ assert static["read_noise_sigma"]["min"] < chroma["read_noise_sigma"]["min"]
1713
+ assert static["noise_modulation"]["max_gain"] == chroma["noise_modulation"]["max_gain"]
1714
+
1715
+
1716
+ def test_sensor_noise_adjustment_level_changes_output_noise() -> None:
1717
+ base_overrides = {
1718
+ "demosaic": False,
1719
+ "read_noise_sigma": 0.01,
1720
+ }
1721
+ low = _deterministic_sensor_pipeline(
1722
+ auto_exposure={"enabled": False},
1723
+ sensor_overrides={
1724
+ **base_overrides,
1725
+ "noise_adjustment": {"level": 0.0, "static_chroma_bias": 0.0},
1726
+ },
1727
+ )
1728
+ high = _deterministic_sensor_pipeline(
1729
+ auto_exposure={"enabled": False},
1730
+ sensor_overrides={
1731
+ **base_overrides,
1732
+ "noise_adjustment": {"level": 2.0, "static_chroma_bias": 0.0},
1733
+ },
1734
+ )
1735
+ image = np.full((48, 64, 3), 0.5, dtype=np.float32)
1736
+
1737
+ low_result = low.apply_np(image, CaptureContext(rng=np.random.default_rng(7)))
1738
+ high_result = high.apply_np(image, CaptureContext(rng=np.random.default_rng(7)))
1739
+
1740
+ assert float((low_result - image).std()) < 1e-6
1741
+ assert float((high_result - image).std()) > 0.012
1742
+
1743
+
1655
1744
  def test_sensor_auto_exposure_meters_rendered_luminance() -> None:
1656
1745
  pipeline = _deterministic_sensor_pipeline(
1657
1746
  auto_exposure={