Feat: Add soft-clip tone mapping with legacy Reinhard

Replace the per-channel Reinhard display transform with a parameterized
soft clip T_p(x) = tanh(x^p)^(1/p), default softclip p=2, exposed through
--tone-map and --tone-map-p. Keep --tone-map reinhard bit-compatible with
the previous x/(1+x) curve for existing images and reject combining it
with an explicit --tone-map-p.

Route the primary image and the mesh overlay through the same
ToneMapSettings; the linear HDR FITS writer stays pre-tone-map. Add a
focused optics-linked tone-map test target, CLI success/error coverage,
and document the display operator versus the Moffat effective PSF.
This commit is contained in:
wyj committed 2026-09-27 00:44:19 -04:00
1 parent 7399eb6904
commit e31e1ed27e
10 files changed
+359 -30

No files matched your search

+42 -1
View File
@@ -267,6 +267,46 @@ at the existing cache radius. Images above `F` retain the direct fallback.
`--max-magnification M` (default unlimited) caps the per-triangle rendering
magnification before flux is formed; it is an explicit preview approximation.
### Tone mapping and display
`--exposure` is a linear multiplier applied while the HDR framebuffer and its
PSF splats are accumulated. The Moffat profile is the empirical effective
optical PSF, absorbing seeing, jitter, and residual aberrations; it is not
changed by any display choice.
The tone map is a later camera/display response applied to the finished linear
HDR image, so it does not alter the linear HDR PSF. The default operator is a
parameterized, per-channel soft clip
\[
T_p(x) = \left[\tanh(x^p)\right]^{1/p}, \qquad p \ge 1,
\]
selected with `--tone-map softclip --tone-map-p 2`, i.e.
\[
T_2(x) = \sqrt{\tanh(x^2)}.
\]
It is close to the identity in the dark, then rolls smoothly into a saturated
shoulder above \(x \sim 1\). Because it is applied independently to each linear
RGB channel, a bright star transitions from its colored Moffat wings into a
white saturated core. Larger `p` approaches \(\min(x,1)\). This per-channel
response is what gives the current simplified preview pipeline the crisp white
cores seen in real astrophotography, rather than the gray, soft cores a
Reinhard curve leaves on bright stars.
`--tone-map reinhard` selects the historical \(x/(1+x)\) display transform and
is retained only to reproduce output rendered before the soft-clip operator was
introduced. FITS output always remains the pre-tone-map linear RGB framebuffer,
independent of operator and `p`; use it to measure physical flux, peak values,
and FWHM. Tone-mapped PNG/PPM data are display-referred and must not be used for
those measurements.
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.
### Fast preview mode
`--fast-mode` replaces the per-event PSF splat with a two-stage approximation:
@@ -372,7 +412,8 @@ ordinary output, replacing its extension with `_HDR.fits`; for example,
`output/imgs/ring_HDR.fits`. It writes the pre-tone-mapping RGB
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. The ordinary
is applied, and the `--tone-map` operator and `--tone-map-p` value never affect
this file. 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