FR-DEV-3f asks for look emulation and proposes HaldCLUT import to inherit
the free film-simulation ecosystem. This takes the other road for the
stocks where the measurements exist: run the physics.
A stock here is its manufacturer's own datasheet -- spectral sensitivity,
characteristic curves, dye densities. Light exposes three emulsion layers,
the layers develop to densities, the densities are dyes that absorb, and
what is left is what reaches the eye. A colour negative comes out orange
and upside down because that is what a colour negative is; it becomes a
photograph when a paper profile prints it, with the enlarger's filtration
solved rather than dialled.
What that buys over a LUT is that the parameters stay physical. Opening up
a stop moves the picture along the film's real characteristic curve,
shoulder and all, instead of scaling a number baked at one exposure. The
data cost runs the other way too: a stock is 17 kB of published
measurements where one HaldCLUT is 800 kB of one person's grade.
It looks like it needs a spectral integration per pixel. It does not, and
that is the whole design:
- Exposure is a 3x3 matrix. The reconstructed scene spectrum is linear
in the sRGB triple, so the integral collapses into nine numbers,
exactly -- no approximation.
- The characteristic curve is three 1D functions, sampled exactly.
- Everything after that -- dye absorption, the print through the
negative, the paper, the viewing illuminant, the adaptation -- takes
exactly three numbers in, so it bakes into one 32^3 lookup.
Per pixel: a matrix multiply, three curve taps, one fetch. Splitting the
curve out of the 3D lookup rather than baking one LUT over exposure is
measured, not assumed: the curve carries the sharp shape and the dye
mixing is smooth, so folding them together would need three times the
resolution for the same error. At 32^3 the worst error is 0.003 in linear
sRGB, under one 8-bit code value, and a test says so.
No wgpu dependency, deliberately, and the same isolation argument dr-lens
makes: the model is plain f32 with a documented layout, so every property
worth asserting is asserted on the CPU. Binding it to a texture is dr-gpu's
job and is not done here yet.
The expected values in tests/ came from a Python prototype running against
a different colour-science stack. Agreement to three decimals is evidence
about the model rather than about one implementation of it -- a transposed
matrix or a mispasted observer row would pass every unit test and fail
that one.
Profiles are converted from spektrafilm by Andrea Volpato, CC BY-SA 4.0.
The converter is in the tree and runnable, so what was changed from
upstream is auditable rather than taken on trust; profiles/CHANGELOG.txt
records it, including the one deliberate deviation -- Mallett & Yuksel's
1 kB basis instead of Hanatos's 4 MB table, which costs accuracy at the
gamut edge and saves four megabytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
3.6 KiB
Plaintext
75 lines
3.6 KiB
Plaintext
Changes made to the spektrafilm profiles shipped in this directory
|
|
=================================================================
|
|
|
|
The profiles here are derived from spektrafilm by Andrea Volpato
|
|
(https://github.com/andreavolpato/spektrafilm), licensed CC BY-SA 4.0. The
|
|
full licence is in LICENSE-PROFILES.txt and is reproduced unchanged.
|
|
|
|
CC BY-SA 4.0 section 3(a)(1)(B) requires that a modified copy say it was
|
|
modified. It was. This file says how, and tools/film-profiles/convert.py
|
|
performs the modification, so it can be re-run against upstream and the result
|
|
compared rather than taken on trust.
|
|
|
|
|
|
What was changed
|
|
----------------
|
|
|
|
1. Format. Upstream ships JSON; these are YAML, so that adding or correcting a
|
|
stock is editing a legible file rather than a minified one. No value is
|
|
altered by the reformat.
|
|
|
|
2. Trimmed to the fields this renderer reads:
|
|
|
|
kept info.*, data.wavelengths (implicitly, as the fixed grid),
|
|
data.log_sensitivity, data.channel_density (renamed
|
|
dye_density), data.base_density, data.log_exposure (kept as its
|
|
two endpoints, since it is uniformly sampled),
|
|
data.density_curves
|
|
|
|
dropped data.density_curves_model - a 3-CDF fit of the curves; the
|
|
sampled curves are shipped
|
|
instead, and reproduce it to
|
|
0.004 density
|
|
data.density_curves_layers - per-sublayer curves, used for
|
|
grain, which is not implemented
|
|
yet. Worth restoring when it is:
|
|
real grain is per sublayer.
|
|
data.hanatos2025_adaptation_* - parameters for a spectral
|
|
upsampling method this renderer
|
|
does not use; see below
|
|
data.midscale_neutral_density - null in every profile shipped
|
|
|
|
Dropping fields loses nothing for the stocks shipped, but it does mean a
|
|
re-run of the converter is needed to pick up an upstream field later.
|
|
|
|
3. Numbers are written at 6 significant figures (5 for the density curves).
|
|
The inputs are digitised datasheet curves, so this is well inside their
|
|
measurement error; it is what takes a profile from 207 kB to 17 kB.
|
|
|
|
4. Nulls made explicit. Upstream uses null where a datasheet has no reading.
|
|
In log_sensitivity that means the layer is blind there, written here as the
|
|
sentinel -9; in the density tables it means no absorption, written as 0.
|
|
|
|
|
|
What was NOT changed
|
|
--------------------
|
|
|
|
No measured value has been rescaled, shifted, smoothed or refitted. The
|
|
renderer's own calibration conventions - mid-grey at 0.184, exposure
|
|
normalised on the green layer - are taken from spektrafilm's reference
|
|
implementation rather than invented, because the profile data is calibrated
|
|
against them.
|
|
|
|
|
|
Known deviation from upstream's rendering
|
|
-----------------------------------------
|
|
|
|
Upstream reconstructs a spectrum from an RGB triple with Hanatos (2025), which
|
|
needs a 4 MB coefficient table. This renderer uses the Mallett & Yuksel (2019)
|
|
sRGB basis instead, which is three curves and about 1 kB, at some cost in how
|
|
faithfully very saturated and out-of-gamut colours are handled. The
|
|
hanatos2025_adaptation_* parameters in the upstream profiles are therefore
|
|
unused here. This is a deliberate trade of accuracy at the gamut edge against
|
|
shipping four megabytes, and it is the first thing to revisit if saturated
|
|
colours look wrong.
|