Feat: Add optional three-channel sensor bloom model

Add an opt-in, post-processing limited-response model applied to the
finished linear HDR before tone mapping. Each RGB channel is processed
independently and isotropically: overflow above E spreads to the eight
neighbours with a fixed 9-point stencil, while the rest is absorbed or
lost at the image boundary. The synchronous ping-pong update uses a
monotonic bounding box and a row-parallel, deterministic reduction; the
conservative round bound reserves fp guard rounds inside a 4096 hard
limit and fails before touching HDR when exceeded.

Expose --sensor-bloom-limit E and --sensor-bloom-transfer e (both
required together, default disabled), validate them before expensive
initialization, and route every output path through the same hook in
write_frame_outputs: raw FITS first, bloom, tone-mapped PNG/PPM, then the
mesh overlay. The raw --hdr-output FITS therefore stays pre-bloom.

Add a standalone unit test (stencil, boundary loss, cascade reference,
symmetry, thread determinism, convergence limits, validation, allocation
failure), CLI integration and regression coverage, an isolated
sensor-bloom-bench target, and document the model in the design, usage,
README, and build docs.
This commit is contained in:
wyj committed 2026-09-27 04:39:38 -04:00
1 parent 85fce1bdeb
commit 9cd933d1f8
12 files changed
+1280 -7

No files matched your search

+29 -1
View File
@@ -307,6 +307,33 @@ Commands written before this change that omit `--tone-map` actually used
Reinhard; add `--tone-map reinhard` explicitly to reproduce their PNG/PPM
output. Historical FITS/HDR benchmarks are unaffected.
### Sensor bloom (optional)
`--sensor-bloom-limit E --sensor-bloom-transfer e` (both required together,
default disabled) enable an optional post-PSF sensor saturation model applied to
the finished linear HDR framebuffer before the display operator. Each RGB
channel is processed independently and isotropically. A channel value above the
finite response limit \(E\) contributes overflow \(D=\max(H-E,0)\). A fraction
\(e\in[0,1)\) of that overflow is spread to the eight neighbours with a fixed
9-point stencil (four axial weights \(4/20\), four diagonal weights \(1/20\)),
while the remaining \((1-e)D\) is absorbed. Overflow directed outside the image
is lost at the boundary and the stencil is not renormalized there. `e=0` clamps
every over-limit channel to \(E\) without spreading to neighbours.
\(E\) is expressed in the post-exposure linear HDR renderer scale, so it scales
with `--exposure`. The model is a phenomenological limited-response
approximation, not a specific CCD/CMOS/Bayer structure. It is non-conservative:
signal is lost both to the drain \((1-e)D\) and to the image boundary, and \(e\)
controls both the per-round retained fraction and the effective propagation
distance. The mesh overlay is drawn after the model, so the diagnostic lines
neither bloom nor feed back into overflow propagation. Visual multi-scale bloom
is not implemented.
When enabled, each frame prints one `Sensor bloom:` line with the initial
saturated and final clamped channel counts, the actual and conservative round
counts, the peak and maximum initial overflow, absorbed and boundary loss,
residual clamp loss, and elapsed time.
### Fast preview mode
`--fast-mode` replaces the per-event PSF splat with a two-stage approximation:
@@ -413,7 +440,8 @@ ordinary output, replacing its extension with `_HDR.fits`; for example,
framebuffer as a three-plane, 32-bit float FITS image. Values remain linear HDR
at the renderer's arbitrary scale; no tone mapping or per-frame normalization
is applied, and the `--tone-map` operator and `--tone-map-p` value never affect
this file. The ordinary
this file. The `--sensor-bloom-*` model runs only after this file is written, so
enabling bloom never changes the stored FITS. The ordinary
binaries do not contain this option or writer.
The FITS header describes a synthetic 8640-by-5760, 36-by-24 mm full-frame