diff --git a/docker/frontend/scripts/raw-develop-check.mjs b/docker/frontend/scripts/raw-develop-check.mjs index 2b150eb..8cc11e8 100644 --- a/docker/frontend/scripts/raw-develop-check.mjs +++ b/docker/frontend/scripts/raw-develop-check.mjs @@ -119,4 +119,48 @@ assert.match(src, /^ *uniforms\[21\] = f\[0\]; uniforms\[22\] = f\[1\]; uniforms // 6020x4024, and every one of the eight bodies checked doubled its frame. assert.match(src, /^ *halfSize: false,$/m, 'the develop is half-size again'); +// The matrix the develop applies is the camera->sRGB one LibRaw already hands +// back, not one derived from `cam_xyz` beside it. They disagree on the Olympus +// ORF this was reported on: 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 on +// 0.921 / 0.886 — green, and blue for a scene the camera left neutral, which is +// the cast the frame opened with. And the matrix must not be divided by `pre_mul` +// again: that undoes the row-sum-to-one normalisation dcraw built into rgb_cam. +{ + const body = src.match(/const XYZ_TO_SRGB = \[[\s\S]*?\n\}\n/)?.[0]; + assert.ok(body, 'getCamToSrgbMatrix not found'); + // The file is TypeScript; the two annotations in this block are all that stop + // `new Function` from reading it. + const js = body.replace(/: any/g, '').replace(/: number\[\]\[\]/g, ''); + const getCamToSrgbMatrix = new Function(`${js}; return getCamToSrgbMatrix;`)(); + const rgb_cam = [ + [1.631760597229004, -0.39896339178085327, -0.23279713094234467], + [-0.298429936170578, 1.7056032419204712, -0.4071732759475708], + [0.02502557262778282, -0.45518237352371216, 1.4301568269729614], + ]; + const cam_xyz = [ + [0.9422, -0.3258, -0.0711], + [-0.2655, 1.0898, 0.2015], + [-0.0512, 0.1354, 0.5512], + ]; + const pre_mul = [2.0312116146087646, 0.9462084174156189, 1.4558180570602417]; + assert.deepEqual( + getCamToSrgbMatrix({ rgb_cam, cam_xyz, pre_mul }), + rgb_cam.map((row) => row.slice(0, 3)), + 'rgb_cam is no longer applied as LibRaw handed it back' + ); + // A neutral camera triple stays neutral through what is left: every row of the + // matrix has to sum to one, or the frame opens with a cast of its own. + for (const [name, cd] of [ + ['rgb_cam', { rgb_cam }], + ['cam_xyz', { cam_xyz }], + ['nothing', {}], + ]) { + const rows = getCamToSrgbMatrix(cd); + for (const row of rows) { + assert.ok(Math.abs(row[0] + row[1] + row[2] - 1) < 1e-6, `${name}: row sums to what white is not`); + } + } +} + console.log('raw-develop-check ok'); diff --git a/docker/frontend/src/engine/rawDevelop.ts b/docker/frontend/src/engine/rawDevelop.ts index c182a83..0464cc7 100644 --- a/docker/frontend/src/engine/rawDevelop.ts +++ b/docker/frontend/src/engine/rawDevelop.ts @@ -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]];