fix(raw): open Olympus ORF at the colour of its own preview via rgb_cam

The develop preferred cam_xyz, whose rows are the XYZ of each camera channel and
which nothing normalised: on the ORF this was reported on it left the frame
green and blue (R/G 0.921, B/G 0.886) where the file's own preview sits at
1.013 / 0.841, and saturated reds came back as the dark purple the frame was
reported for — a red pixel's green ran negative through a row that carries
-2.64, so it clipped to 0 while the knee pulled red down with it.

LibRaw hands back dcraw's own rgb_cam, already the camera -> sRGB transform with
rows summing to one, and it is now applied as handed back: R/G 1.020 B/G 0.920,
and the fit against the embedded preview follows to mean 167.5,164.8,138.5
against the preview's 167.0,164.8,138.6 and the camera's own JPEG's 165,162,133.
Dividing rgb_cam by pre_mul — which carries that row normalisation — is what the
frame before this one did instead, and it undoes it.

The cam_xyz chain stays as the fallback for a file with no rgb_cam, with its
rows normalised so a neutral frame opens neutral. invert3x3, unused since the
frame stopped going through the chain, is dropped.
This commit is contained in:
2026-10-05 22:51:10 +07:00
parent cef7868db8
commit 2e36acd18b
2 changed files with 79 additions and 40 deletions
+35 -40
View File
@@ -425,51 +425,46 @@ function previewGrid(jpeg: Uint8Array, w: number, h: number, n = MATCH_GRID): Ui
}
}
function invert3x3(m: number[][]): number[][] | null {
const [a, b, c] = m[0];
const [d, e, f] = m[1];
const [g, h, i] = m[2];
const det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
if (Math.abs(det) < 1e-6) return null;
const invDet = 1.0 / det;
return [
[(e * i - f * h) * invDet, (c * h - b * i) * invDet, (b * f - c * e) * invDet],
[(f * g - d * i) * invDet, (a * i - c * g) * invDet, (c * d - a * f) * invDet],
[(d * h - e * g) * invDet, (b * g - a * h) * invDet, (a * e - b * d) * invDet],
];
}
// The camera -> sRGB matrix, applied to the WB'd sensor triple.
//
// `rgb_cam` is dcraw's own: it is the camera -> sRGB transform already, its rows
// sum to one, and the 1/num dcraw scales it by rides in `pre_mul`. Dividing it by
// pre_mul a second time undoes the normalisation and drags the frame towards
// green. It is what every body LibRaw knows a `cam_xyz` for gets — the Olympus
// ORF this was reported on included, whose `rgb_cam` and `cam_xyz` disagree
// (measured: rgb_cam x cam_mul lands on R/G 1.021 B/G 0.920 against the file's own
// preview's 1.013 / 0.841, the cam_xyz chain this used to prefer on 0.921 / 0.886
// — green, and blue for a scene the camera left neutral).
//
// `cam_xyz` stays as the fallback for a file that has one and no `rgb_cam`: its
// rows are the XYZ of each camera channel, so sRGB = xyz_rgb^-1 . cam_xyz^T . cam,
// with the rows normalised so a neutral camera triple opens neutral, as dcraw's
// cam_xyz_coeff normalises them.
const XYZ_TO_SRGB = [
[3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[0.0556434, -0.2040259, 1.0572252],
];
function getCamToSrgbMatrix(cd: any): number[][] {
if (cd?.rgb_cam && Array.isArray(cd.rgb_cam) && cd.rgb_cam.length >= 3) {
return [0, 1, 2].map((i) => [0, 1, 2].map((j) => cd.rgb_cam[i]?.[j] || 0));
}
if (cd?.cam_xyz && Array.isArray(cd.cam_xyz) && cd.cam_xyz.length >= 3) {
const cx = cd.cam_xyz;
const scale = (Math.abs(cx[0][0]) > 100) ? 10000.0 : 1.0;
const rX = (cx[0][0] || 0) / scale, rY = (cx[0][1] || 0) / scale, rZ = (cx[0][2] || 0) / scale;
const gX = (cx[1][0] || 0) / scale, gY = (cx[1][1] || 0) / scale, gZ = (cx[1][2] || 0) / scale;
const bX = (cx[2][0] || 0) / scale, bY = (cx[2][1] || 0) / scale, bZ = (cx[2][2] || 0) / scale;
const m00 = 3.2404542 * rX - 1.5371385 * rY - 0.4985314 * rZ;
const m01 = 3.2404542 * gX - 1.5371385 * gY - 0.4985314 * gZ;
const m02 = 3.2404542 * bX - 1.5371385 * bY - 0.4985314 * bZ;
const m10 = -0.9692660 * rX + 1.8760108 * rY + 0.0415560 * rZ;
const m11 = -0.9692660 * gX + 1.8760108 * gY + 0.0415560 * gZ;
const m12 = -0.9692660 * bX + 1.8760108 * bY + 0.0415560 * bZ;
const m20 = 0.0556434 * rX - 0.2040259 * rY + 1.0572252 * rZ;
const m21 = 0.0556434 * gX - 0.2040259 * gY + 1.0572252 * gZ;
const m22 = 0.0556434 * bX - 0.2040259 * bY + 1.0572252 * bZ;
return [[m00, m01, m02], [m10, m11, m12], [m20, m21, m22]];
}
if (cd?.rgb_cam && Array.isArray(cd.rgb_cam) && cd.rgb_cam.length >= 3) {
const pm = (cd?.pre_mul && cd.pre_mul.length >= 3) ? cd.pre_mul : [1, 1, 1, 1];
const pmR = pm[0] || 1, pmG = pm[1] || 1, pmB = pm[2] || 1;
return [
[(cd.rgb_cam[0][0] || 0) / pmR, (cd.rgb_cam[0][1] || 0) / pmG, (cd.rgb_cam[0][2] || 0) / pmB],
[(cd.rgb_cam[1][0] || 0) / pmR, (cd.rgb_cam[1][1] || 0) / pmG, (cd.rgb_cam[1][2] || 0) / pmB],
[(cd.rgb_cam[2][0] || 0) / pmR, (cd.rgb_cam[2][1] || 0) / pmG, (cd.rgb_cam[2][2] || 0) / pmB],
];
const m = [0, 1, 2].map((i) =>
[0, 1, 2].map((j) =>
XYZ_TO_SRGB[i][0] * ((cx[j]?.[0] || 0) / scale) +
XYZ_TO_SRGB[i][1] * ((cx[j]?.[1] || 0) / scale) +
XYZ_TO_SRGB[i][2] * ((cx[j]?.[2] || 0) / scale)
)
);
return m.map((row) => {
const num = row[0] + row[1] + row[2];
return Math.abs(num) < 1e-6 ? row : row.map((v) => v / num);
});
}
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]];