Apply a camera profile's HueSatMap and LookTable after exposure

The second half of D20: a camera_profile scene operation at order 25
that converts working colour into linear ProPhoto, runs the DNG SDK's
HSV lookup through the HueSatMap and then the LookTable, and converts
back. Hue and saturation do not change under the uniform gains before
it, so a 2.5-D HueSatMap gives the same answer as straight after the
matrix, and the look sees the photographer's exposure as it does in
the SDK. Two departures for scene-referred values: value is not
clamped on the way out, and a colour outside ProPhoto passes through.

The operation holds only the switch (on by default) and a look
strength of 0-200 %. It is composed while the switch is on — a new
Operation::composes() separates "does something" from "moved from the
defaults", so an untouched raw renders through its profile and still
writes nothing. The tables come from the source: dr-gpu uploads the
ones DemosaicedImage carries into a storage buffer at @binding(8),
whose two-entry header tells the fragment whether there is anything to
apply, and binds a header of zeros for every other source.

apply_reference is the lookup on the CPU. The GPU test holds the
shader to it over 256 colours, through synthetic tables strong enough
that a wrong index shows, and through the library's real Adobe
Standard tables when the 6D DNG is present.
This commit is contained in:
2026-10-02 22:38:06 -04:00
parent f6a3f3f4e2
commit c02b401a9a
13 changed files with 1117 additions and 51 deletions
+12 -10
View File
@@ -95,13 +95,13 @@ that differed from the screen would be the result.
### 3.1 The two constants
`P = row-normalise(XYZ→sRGB · Bradford(D50→D65) · ProPhoto→XYZ(D50))` takes ProPhoto to the
working space; the fragment uses `P⁻¹` going in and `P` coming out. These are the same constants
`forward_to_srgb` composes for the forward-matrix route, so for a profile with forward matrices the
round trip is exact: the working colour is `P · FM`-space colour and `P⁻¹` recovers the SDK's
ProPhoto. Row normalisation keeps working-space white at ProPhoto white, so a neutral stays at
`s = 0`. For the colour-matrix route, `P⁻¹` gives the ProPhoto rendering of the same colour, which
is what the SDK derives too.
`P⁻¹` is `ColourSpace::ProPhoto.from_linear_srgb()` — the conversion `dr-types` already derives
from the two spaces' chromaticities, adapting D65 to D50 by Bradford, which the export path uses to
write ProPhoto files — and `P` is its inverse. The fragment uses `P⁻¹` going in and `P` coming
out. Each row of both is scaled to sum to one, so working-space white is ProPhoto white exactly and
a neutral reaches the tables at `s = 0`. For a profile with forward matrices this recovers the
SDK's ProPhoto colour to within the difference between that derivation and `forward_to_srgb`'s
published Bradford constants, which is rounding.
## 4. Where a profile comes from
@@ -197,11 +197,13 @@ action sits on that line.
- **The CPU reference matches the SDK's algorithm**: grey passes through; a table of
`(0°, 1, 1)` everywhere is the identity to 1e-6; a uniform `satScale` of 1.2 scales HSV
saturation by 1.2; hue interpolation wraps between the last and first column.
- **The shader agrees with the CPU reference** within 1e-4 on a ramp across hue, saturation and
values up to 16.0, on a device.
- **The shader agrees with the CPU reference** on a device, within two 8-bit codes of the
display-encoded readback (the only readback the adjust pass has), over 256 colours that tables
of tens of degrees and ±30 % saturation move, and over the library's real Adobe Standard tables
(`dr-gpu/tests/camera_profile.rs`).
- **Scene-referred.** A value above 1.0 leaves the stage above 1.0 (`scene_referred_until_the_view`
covers the operation).
- **Neutral.** `apply` off, or no tables, composes a shader whose output equals today's to the bit.
- **Neutral.** `apply` off renders to the bit what a source with no tables renders.
- **Two illuminants.** A HueSatMap at blend weight 0 is Data1, at 1 is Data2.
- **Matching.** An embedded profile beats a directory one; a DCP for "Canon EOS 6D" matches a CR2
whose rawler make/model is "Canon"/"EOS 6D"; no match leaves the matrix.
+1 -1
View File
@@ -459,7 +459,7 @@ FR-DEV-3j's default, for every body. ΔE2000 validation against ColorChecker ref
once DCP support lands.
For item 4: the lookup follows the DNG SDK's on `[0, 1]` and leaves values above 1.0 above it;
grey and an identity table pass through unchanged; the shader agrees with the CPU reference; with
the switch off, or no profile, the render is unchanged to the bit (camera-profiles.md §8).
the switch off the render is to the bit the one with no profile (camera-profiles.md §8).
**FR-DEV-3f — Look emulation.** Support HaldCLUT import, which inherits the existing free film
simulation ecosystem at near-zero implementation cost, plus reading the in-RAF film simulation tag
File diff suppressed because one or more lines are too long