Read the true white level, and clamp the sensor stage at both ends

Two corrections to the sensor stage, found while chasing magenta highlights.
Neither is the cause of that — see below — but both are wrong on their own
terms.

`white_level` took the *first* of rawler's per-channel saturation points. On a
Canon 6D that reports 15070 while the data reaches 16383, so every sample
above it was treated as brighter than white. It takes the maximum now.

The normalisation clamped its floor and not its ceiling, so those over-white
samples passed through as values above 1.0. Clamped at both ends.

**This does not fix the pink.** Measured on _MG_8596.CR2, exported and looked
at: the subject renders correctly and only the blown sky is magenta. A fully
clipped pixel is (1,1,1) in raw, the as-shot balance multiplies it to
(1.93, 1.00, 1.68), and the camera matrix turns that into R 2.88, G 0.51,
B 2.03 — red and blue clip at one, green does not, and the result is magenta.
It is correct white balance applied to already-saturated data, which is the
classic highlight-clipping cast and needs highlight desaturation to fix: a
pixel at saturation carries no colour information and must be rendered
neutral, not balanced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 13:13:17 +02:00
co-authored by Claude Opus 5
parent 7d0fb710a6
commit 31e20399c8
2 changed files with 24 additions and 3 deletions
+8 -2
View File
@@ -481,11 +481,17 @@ pub fn decode(bytes: &[u8]) -> Result<RawImage, DecodeError> {
data,
cfa_pattern: cfa,
black_level,
// The **maximum** of the per-channel saturation points, not the
// first. Taking one channel's value under-reports the others', and
// every sample above the level it is normalised against becomes a
// highlight the pipeline treats as brighter than white.
white_level: image
.whitelevel
.0
.first()
.map(|v| *v as u16)
.iter()
.copied()
.max()
.map(|v| v as u16)
.unwrap_or(u16::MAX),
wb_coeffs,
color_matrix,