euler-preprocess 3.4.0__tar.gz → 3.6.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 (58) hide show
  1. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/PKG-INFO +74 -7
  2. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/README.md +73 -6
  3. euler_preprocess-3.6.0/euler_preprocess/common/color.py +50 -0
  4. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/capture.py +1034 -99
  5. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/models.py +193 -33
  6. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/pipeline.py +30 -2
  7. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/transform.py +111 -9
  8. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess.egg-info/PKG-INFO +74 -7
  9. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess.egg-info/SOURCES.txt +6 -0
  10. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/pyproject.toml +1 -1
  11. euler_preprocess-3.6.0/tests/test_dense_gloomy_daylight_config.py +76 -0
  12. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_fog_aux_outputs.py +134 -0
  13. euler_preprocess-3.6.0/tests/test_fog_aware_auto_exposure.py +96 -0
  14. euler_preprocess-3.6.0/tests/test_scene_illumination.py +96 -0
  15. euler_preprocess-3.6.0/tests/test_sensor_identity.py +108 -0
  16. euler_preprocess-3.6.0/tests/test_tone_map_lut.py +37 -0
  17. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/__init__.py +0 -0
  18. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/cli.py +0 -0
  19. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/__init__.py +0 -0
  20. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/dataset.py +0 -0
  21. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/device.py +0 -0
  22. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/intrinsics.py +0 -0
  23. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/io.py +0 -0
  24. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/logging.py +0 -0
  25. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/noise.py +0 -0
  26. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/normalize.py +0 -0
  27. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/output.py +0 -0
  28. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/sampling.py +0 -0
  29. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/common/transform.py +0 -0
  30. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/__init__.py +0 -0
  31. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  32. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
  33. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/augmentations.py +0 -0
  34. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  35. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  36. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  37. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  38. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/foggify.py +0 -0
  39. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/foggify_logging.py +0 -0
  40. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/inference.py +0 -0
  41. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/fog/logging.py +0 -0
  42. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/radial/__init__.py +0 -0
  43. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/radial/transform.py +0 -0
  44. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  45. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess/sky_depth/transform.py +0 -0
  46. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  47. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  48. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess.egg-info/requires.txt +0 -0
  49. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  50. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/setup.cfg +0 -0
  51. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_airlight_fallback.py +0 -0
  52. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_cli_sample_selection.py +0 -0
  53. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  54. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_foggify_integration.py +0 -0
  55. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_radial.py +0 -0
  56. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_sky_depth.py +0 -0
  57. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_source_backed_output.py +0 -0
  58. {euler_preprocess-3.4.0 → euler_preprocess-3.6.0}/tests/test_zip_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-preprocess
3
- Version: 3.4.0
3
+ Version: 3.6.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
@@ -148,6 +148,7 @@ Controls the fog simulation.
148
148
  "depth_scale": 1.0,
149
149
  "resize_depth": true,
150
150
  "contrast_threshold": 0.05,
151
+ "render_input_space": "srgb",
151
152
  "mode": "sample",
152
153
  "device": "cpu",
153
154
  "gpu_batch_size": 4,
@@ -166,6 +167,7 @@ Controls the fog simulation.
166
167
  | `depth_scale` | Multiplier applied to depth values after loading. |
167
168
  | `resize_depth` | Resize the depth map to match the RGB resolution (bilinear). |
168
169
  | `contrast_threshold` | Threshold *C_t* used in the visibility-to-attenuation conversion (default `0.05`). |
170
+ | `render_input_space` | Colour space of the RGB supplied to the fog renderer. Use `"srgb"` for display-encoded dataset images so fog and airlight are mixed in scene-linear RGB; use `"linear"` for legacy configs or already-linear radiance. |
169
171
  | `mode` | Optional scenario mode. Omit it or use `"sample"` for current one-scenario-per-image behavior; use `"progressive"` to render every scenario step for every image. |
170
172
  | `device` | `"cpu"`, `"cuda"`, `"mps"`, or `"gpu"` (alias for cuda). |
171
173
  | `gpu_batch_size` | Batch size when running on GPU. Uniform-model samples are batched; heterogeneous samples are processed individually. |
@@ -310,6 +312,22 @@ the exposure; `resolve_iso` can raise ISO from the metering pressure, dark pixel
310
312
  fraction, and fog opacity. When auto exposure is enabled, `exposure_gain` still
311
313
  applies as scenario-specific exposure compensation.
312
314
 
315
+ Fog-aware metering modes use `CaptureContext.depth_m`, `k_map`, fog opacity, and
316
+ `attributes.sky_mask` when available. Use `"metering":
317
+ "fog_aware_center_weighted"` or `"sky_aware_center_weighted"` and tune
318
+ `sky_suppression`, `fog_meter_suppression`, `depth_meter_decay_m`, and
319
+ `min_meter_weight` to keep bright sky or dense far-field airlight from dominating
320
+ the exposure meter. Legacy metering modes are unchanged unless these suppression
321
+ keys are present.
322
+
323
+ Set `sensor.sensor_identity.enabled` for persistent sensor structure across
324
+ frames. The identity cache is deterministic for the same `sensor_id`, `seed`,
325
+ image shape, and Bayer pattern. `prnu_sigma` adds multiplicative pixel-response
326
+ non-uniformity before shot noise; `dsnu_sigma`, `persistent_row_sigma`, and
327
+ `persistent_column_sigma` add fixed raw-domain offsets; persistent hot/dead pixel
328
+ probabilities create stable bad-pixel masks that combine with the existing
329
+ per-image bad-pixel probabilities.
330
+
313
331
  Set `sensor.shadow_recovery_noise.enabled` to add extra post-demosaic luma and
314
332
  chroma corruption only where the pre-exposure rendered luminance was low. This
315
333
  is useful for reducing broad global grain while keeping lifted shadows visibly
@@ -361,6 +379,20 @@ gain, read noise, banding, and dark/fog noise modulation should move together:
361
379
  }
362
380
  ```
363
381
 
382
+ `isp.tone_map` supports `"reinhard"`, `"aces"`, `"clip"`, and `"lut"`. The LUT
383
+ mode uses a cheap interpolated 1D camera-response curve:
384
+
385
+ ```json
386
+ {
387
+ "type": "isp",
388
+ "tone_map": "lut",
389
+ "tone_map_strength": 1.0,
390
+ "tone_map_lut": [0.0, 0.006, 0.014, 0.028, 0.052, 0.090, 0.145, 0.220, 0.320, 0.450, 0.610, 0.780, 0.900, 0.965, 0.995, 1.0],
391
+ "tone_map_lut_domain": "linear",
392
+ "gamma": "srgb"
393
+ }
394
+ ```
395
+
364
396
  Top-level `scenario_profiles` sample one latent scene/camera condition before
365
397
  rendering. The selected scenario is merged over the root config, so it can drive
366
398
  fog density, atmospheric light, camera profile, capture-stage overrides, ISP, and
@@ -390,11 +422,30 @@ compression together:
390
422
  "airlight_method": "dcp_heuristic",
391
423
  "models": {
392
424
  "heterogeneous_k_ls": {
393
- "visibility_m": {"dist": "uniform", "min": 18.0, "max": 55.0}
425
+ "visibility_m": {"dist": "uniform", "min": 18.0, "max": 55.0},
426
+ "scene_illumination": {
427
+ "enabled": true,
428
+ "global_ev": {"dist": "uniform", "min": 0.25, "max": 0.85},
429
+ "near_ev": {"dist": "uniform", "min": 0.35, "max": 1.20},
430
+ "near_decay_depth_m": {"dist": "uniform", "min": 10.0, "max": 22.0},
431
+ "fog_coupled_ev": {"dist": "uniform", "min": 0.10, "max": 0.45},
432
+ "sky_weight": 0.0
433
+ }
394
434
  }
395
435
  },
396
436
  "capture_overrides": {
397
- "sensor": {"condition_profile": "underexposed_noisy"},
437
+ "sensor": {
438
+ "condition_profile": "underexposed_noisy",
439
+ "auto_exposure": {
440
+ "enabled": true,
441
+ "metering": "fog_aware_center_weighted",
442
+ "target_luminance": {"dist": "uniform", "min": 0.13, "max": 0.20},
443
+ "highlight_protection": 0.78,
444
+ "manual_gain_weight": 0.0,
445
+ "sky_suppression": 0.85,
446
+ "fog_meter_suppression": 0.65
447
+ }
448
+ },
398
449
  "transport": {"jpeg": {"quality": {"dist": "uniform", "min": 54, "max": 78}}}
399
450
  }
400
451
  }
@@ -408,10 +459,14 @@ weights locally.
408
459
 
409
460
  Set top-level `"mode": "progressive"` to emit every configured scenario for
410
461
  every input image instead of sampling one scenario. Each scenario accepts
411
- `"steps"` and `"weight"`; the transform writes steps from weight `0` through
412
- the scenario's configured weight, and weight `1` matches the original scenario.
413
- Fog density is progressed in scattering-coefficient space, while numeric
414
- camera/config values blend from the base config toward the scenario config.
462
+ `"steps"` and `"progressive_weight"` (or `"max_weight"` / `"weight"` as
463
+ aliases); the transform writes steps from weight `0` through the scenario's
464
+ configured weight, and weight `1` matches the original scenario. Fog density is
465
+ progressed in scattering-coefficient space, while numeric camera/config values
466
+ blend from the base config toward the scenario config. Progressive blends clamp
467
+ probability-like values and non-negative physical factors back into valid
468
+ mathematical domains so extrapolated weights above `1` do not create invalid
469
+ render parameters.
415
470
  Source-backed outputs are written as `fog_progression` variants under each
416
471
  source file id.
417
472
 
@@ -432,6 +487,14 @@ where:
432
487
 
433
488
  Distant objects are attenuated more (`t` approaches 0) and replaced by airlight, just as in real fog.
434
489
 
490
+ For gloomy conditions, add `scene_illumination` inside a fog model config. This
491
+ darkens pre-fog scene radiance `I(x)` before the atmospheric scattering equation,
492
+ so near objects can become plausibly overcast or storm-lit instead of passing
493
+ through unchanged. `global_ev` applies to the whole non-sky scene, `near_ev` adds
494
+ extra near-field darkening with `near_decay_depth_m`, `fog_coupled_ev` adds a
495
+ term proportional to local fog opacity, and `sky_weight: 0.0` preserves sky
496
+ pixels when a sky mask is available.
497
+
435
498
  ### How Each Modality is Used
436
499
 
437
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.
@@ -493,6 +556,10 @@ Each fog model can override the dampening curve:
493
556
 
494
557
  The factor is:
495
558
  `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
559
+ `min_factor` and `max_factor` must be finite and non-negative. Values above
560
+ `1.0` are allowed when you intentionally want to brighten estimated airlight;
561
+ the final RGB output is still clamped to the valid image range. `strength`
562
+ must remain finite and non-negative.
496
563
  `reference_beta` is either `reference_scattering_coefficient` /
497
564
  `reference_beta`, or it is derived from `reference_visibility_m` using the
498
565
  model's contrast threshold. The default applies only when `atmospheric_light`
@@ -134,6 +134,7 @@ Controls the fog simulation.
134
134
  "depth_scale": 1.0,
135
135
  "resize_depth": true,
136
136
  "contrast_threshold": 0.05,
137
+ "render_input_space": "srgb",
137
138
  "mode": "sample",
138
139
  "device": "cpu",
139
140
  "gpu_batch_size": 4,
@@ -152,6 +153,7 @@ Controls the fog simulation.
152
153
  | `depth_scale` | Multiplier applied to depth values after loading. |
153
154
  | `resize_depth` | Resize the depth map to match the RGB resolution (bilinear). |
154
155
  | `contrast_threshold` | Threshold *C_t* used in the visibility-to-attenuation conversion (default `0.05`). |
156
+ | `render_input_space` | Colour space of the RGB supplied to the fog renderer. Use `"srgb"` for display-encoded dataset images so fog and airlight are mixed in scene-linear RGB; use `"linear"` for legacy configs or already-linear radiance. |
155
157
  | `mode` | Optional scenario mode. Omit it or use `"sample"` for current one-scenario-per-image behavior; use `"progressive"` to render every scenario step for every image. |
156
158
  | `device` | `"cpu"`, `"cuda"`, `"mps"`, or `"gpu"` (alias for cuda). |
157
159
  | `gpu_batch_size` | Batch size when running on GPU. Uniform-model samples are batched; heterogeneous samples are processed individually. |
@@ -296,6 +298,22 @@ the exposure; `resolve_iso` can raise ISO from the metering pressure, dark pixel
296
298
  fraction, and fog opacity. When auto exposure is enabled, `exposure_gain` still
297
299
  applies as scenario-specific exposure compensation.
298
300
 
301
+ Fog-aware metering modes use `CaptureContext.depth_m`, `k_map`, fog opacity, and
302
+ `attributes.sky_mask` when available. Use `"metering":
303
+ "fog_aware_center_weighted"` or `"sky_aware_center_weighted"` and tune
304
+ `sky_suppression`, `fog_meter_suppression`, `depth_meter_decay_m`, and
305
+ `min_meter_weight` to keep bright sky or dense far-field airlight from dominating
306
+ the exposure meter. Legacy metering modes are unchanged unless these suppression
307
+ keys are present.
308
+
309
+ Set `sensor.sensor_identity.enabled` for persistent sensor structure across
310
+ frames. The identity cache is deterministic for the same `sensor_id`, `seed`,
311
+ image shape, and Bayer pattern. `prnu_sigma` adds multiplicative pixel-response
312
+ non-uniformity before shot noise; `dsnu_sigma`, `persistent_row_sigma`, and
313
+ `persistent_column_sigma` add fixed raw-domain offsets; persistent hot/dead pixel
314
+ probabilities create stable bad-pixel masks that combine with the existing
315
+ per-image bad-pixel probabilities.
316
+
299
317
  Set `sensor.shadow_recovery_noise.enabled` to add extra post-demosaic luma and
300
318
  chroma corruption only where the pre-exposure rendered luminance was low. This
301
319
  is useful for reducing broad global grain while keeping lifted shadows visibly
@@ -347,6 +365,20 @@ gain, read noise, banding, and dark/fog noise modulation should move together:
347
365
  }
348
366
  ```
349
367
 
368
+ `isp.tone_map` supports `"reinhard"`, `"aces"`, `"clip"`, and `"lut"`. The LUT
369
+ mode uses a cheap interpolated 1D camera-response curve:
370
+
371
+ ```json
372
+ {
373
+ "type": "isp",
374
+ "tone_map": "lut",
375
+ "tone_map_strength": 1.0,
376
+ "tone_map_lut": [0.0, 0.006, 0.014, 0.028, 0.052, 0.090, 0.145, 0.220, 0.320, 0.450, 0.610, 0.780, 0.900, 0.965, 0.995, 1.0],
377
+ "tone_map_lut_domain": "linear",
378
+ "gamma": "srgb"
379
+ }
380
+ ```
381
+
350
382
  Top-level `scenario_profiles` sample one latent scene/camera condition before
351
383
  rendering. The selected scenario is merged over the root config, so it can drive
352
384
  fog density, atmospheric light, camera profile, capture-stage overrides, ISP, and
@@ -376,11 +408,30 @@ compression together:
376
408
  "airlight_method": "dcp_heuristic",
377
409
  "models": {
378
410
  "heterogeneous_k_ls": {
379
- "visibility_m": {"dist": "uniform", "min": 18.0, "max": 55.0}
411
+ "visibility_m": {"dist": "uniform", "min": 18.0, "max": 55.0},
412
+ "scene_illumination": {
413
+ "enabled": true,
414
+ "global_ev": {"dist": "uniform", "min": 0.25, "max": 0.85},
415
+ "near_ev": {"dist": "uniform", "min": 0.35, "max": 1.20},
416
+ "near_decay_depth_m": {"dist": "uniform", "min": 10.0, "max": 22.0},
417
+ "fog_coupled_ev": {"dist": "uniform", "min": 0.10, "max": 0.45},
418
+ "sky_weight": 0.0
419
+ }
380
420
  }
381
421
  },
382
422
  "capture_overrides": {
383
- "sensor": {"condition_profile": "underexposed_noisy"},
423
+ "sensor": {
424
+ "condition_profile": "underexposed_noisy",
425
+ "auto_exposure": {
426
+ "enabled": true,
427
+ "metering": "fog_aware_center_weighted",
428
+ "target_luminance": {"dist": "uniform", "min": 0.13, "max": 0.20},
429
+ "highlight_protection": 0.78,
430
+ "manual_gain_weight": 0.0,
431
+ "sky_suppression": 0.85,
432
+ "fog_meter_suppression": 0.65
433
+ }
434
+ },
384
435
  "transport": {"jpeg": {"quality": {"dist": "uniform", "min": 54, "max": 78}}}
385
436
  }
386
437
  }
@@ -394,10 +445,14 @@ weights locally.
394
445
 
395
446
  Set top-level `"mode": "progressive"` to emit every configured scenario for
396
447
  every input image instead of sampling one scenario. Each scenario accepts
397
- `"steps"` and `"weight"`; the transform writes steps from weight `0` through
398
- the scenario's configured weight, and weight `1` matches the original scenario.
399
- Fog density is progressed in scattering-coefficient space, while numeric
400
- camera/config values blend from the base config toward the scenario config.
448
+ `"steps"` and `"progressive_weight"` (or `"max_weight"` / `"weight"` as
449
+ aliases); the transform writes steps from weight `0` through the scenario's
450
+ configured weight, and weight `1` matches the original scenario. Fog density is
451
+ progressed in scattering-coefficient space, while numeric camera/config values
452
+ blend from the base config toward the scenario config. Progressive blends clamp
453
+ probability-like values and non-negative physical factors back into valid
454
+ mathematical domains so extrapolated weights above `1` do not create invalid
455
+ render parameters.
401
456
  Source-backed outputs are written as `fog_progression` variants under each
402
457
  source file id.
403
458
 
@@ -418,6 +473,14 @@ where:
418
473
 
419
474
  Distant objects are attenuated more (`t` approaches 0) and replaced by airlight, just as in real fog.
420
475
 
476
+ For gloomy conditions, add `scene_illumination` inside a fog model config. This
477
+ darkens pre-fog scene radiance `I(x)` before the atmospheric scattering equation,
478
+ so near objects can become plausibly overcast or storm-lit instead of passing
479
+ through unchanged. `global_ev` applies to the whole non-sky scene, `near_ev` adds
480
+ extra near-field darkening with `near_decay_depth_m`, `fog_coupled_ev` adds a
481
+ term proportional to local fog opacity, and `sky_weight: 0.0` preserves sky
482
+ pixels when a sky mask is available.
483
+
421
484
  ### How Each Modality is Used
422
485
 
423
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.
@@ -479,6 +542,10 @@ Each fog model can override the dampening curve:
479
542
 
480
543
  The factor is:
481
544
  `min_factor + (max_factor - min_factor) / (1 + strength * beta / reference_beta)`.
545
+ `min_factor` and `max_factor` must be finite and non-negative. Values above
546
+ `1.0` are allowed when you intentionally want to brighten estimated airlight;
547
+ the final RGB output is still clamped to the valid image range. `strength`
548
+ must remain finite and non-negative.
482
549
  `reference_beta` is either `reference_scattering_coefficient` /
483
550
  `reference_beta`, or it is derived from `reference_visibility_m` using the
484
551
  model's contrast threshold. The default applies only when `atmospheric_light`
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ try:
6
+ import torch
7
+ except ImportError: # pragma: no cover - torch is optional
8
+ torch = None
9
+
10
+
11
+ def srgb_to_linear(image: np.ndarray) -> np.ndarray:
12
+ """Convert display-encoded sRGB values in ``[0, 1]`` to scene-linear RGB."""
13
+ img = np.clip(np.asarray(image, dtype=np.float32), 0.0, 1.0)
14
+ return np.where(
15
+ img <= 0.04045,
16
+ img / 12.92,
17
+ ((img + 0.055) / 1.055) ** 2.4,
18
+ ).astype(np.float32, copy=False)
19
+
20
+
21
+ def linear_to_srgb(image: np.ndarray) -> np.ndarray:
22
+ """Convert scene-linear RGB values in ``[0, 1]`` to display sRGB."""
23
+ img = np.clip(np.asarray(image, dtype=np.float32), 0.0, 1.0)
24
+ return np.where(
25
+ img <= 0.0031308,
26
+ img * 12.92,
27
+ 1.055 * np.power(img, 1.0 / 2.4) - 0.055,
28
+ ).astype(np.float32, copy=False)
29
+
30
+
31
+ def srgb_to_linear_torch(image):
32
+ if torch is None: # pragma: no cover - defensive
33
+ raise RuntimeError("Torch color conversion requested but torch is unavailable")
34
+ img = torch.clamp(image, 0.0, 1.0)
35
+ return torch.where(
36
+ img <= 0.04045,
37
+ img / 12.92,
38
+ torch.pow((img + 0.055) / 1.055, 2.4),
39
+ )
40
+
41
+
42
+ def linear_to_srgb_torch(image):
43
+ if torch is None: # pragma: no cover - defensive
44
+ raise RuntimeError("Torch color conversion requested but torch is unavailable")
45
+ img = torch.clamp(image, 0.0, 1.0)
46
+ return torch.where(
47
+ img <= 0.0031308,
48
+ img * 12.92,
49
+ 1.055 * torch.pow(img, 1.0 / 2.4) - 0.055,
50
+ )