diff --git a/docker/frontend/src/engine/previewMatch.ts b/docker/frontend/src/engine/previewMatch.ts index 9703591..e714940 100644 --- a/docker/frontend/src/engine/previewMatch.ts +++ b/docker/frontend/src/engine/previewMatch.ts @@ -70,36 +70,35 @@ function solve(C: number[][], d: number[]): number[] { // The curve a channel needs to reach the camera's, least squared: what the 3x3 // left for that channel, a cubic against the block the camera wrote there. // -// The cubic is pinned at white, not free. Fitted free it slid its far end down to -// meet the blocks it wanted — 0.96 on red — and every blown highlight the develop -// had carried up to the white level came out off-white instead: a blown pixel -// (1, 1, 1) into the fit left at 0.82 / 0.97 / 0.93, and 0.0% of the frame at -// pure white against 2.3% before and 3.3% in the preview. So the fit solves the -// three free terms of `tone = x^3 + a(1 - x^3) + b(x - x^3) + c(x^2 - x^3)`, whose -// terms all vanish at 1 and which costs the fit almost nothing (rms 0.066 on red -// against the free cubic's 0.063, and 0.076 on blue against 0.076, on the frame -// this was measured on). Scaling the free curve onto its own end instead moved -// the midtones by the whole 4% the end was short, and pinning both ends moved them -// by 12% (rms 0.21 on green). +// The cubic is pinned at BOTH black (x=0 -> y=0) and white (x=1 -> y=1). +// Pinned at both ends, a blown pixel (1,1,1) stays pure white, and a deep shadow +// pixel (0,0,0) stays pure black without shifting to purple or magenta noise. function basis(i: number, x: number): number { const x3 = x * x * x; - return i === 0 ? 1 - x3 : i === 1 ? x - x3 : x * x - x3; + return i === 0 ? x - x3 : x * x - x3; +} + +function solve2x2(A: number[][], d: number[]): number[] { + const det = A[0][0] * A[1][1] - A[0][1] * A[1][0] + 1e-9; + const a = (d[0] * A[1][1] - d[1] * A[0][1]) / det; + const b = (A[0][0] * d[1] - A[1][0] * d[0]) / det; + return [a, b]; } function fitTone(blocks: { t: number[]; q: number[] }[]): Float32Array { const out = new Float32Array(12); for (let c = 0; c < 3; c++) { - const A = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; - const d = [0, 0, 0]; + const A = [[0, 0], [0, 0]]; + const d = [0, 0]; for (const { t, q } of blocks) { - const x = q[c], b = [basis(0, x), basis(1, x), basis(2, x)]; - for (let p = 0; p < 3; p++) { - for (let k = 0; k < 3; k++) A[p][k] += b[p] * b[k]; + const x = q[c], b = [basis(0, x), basis(1, x)]; + for (let p = 0; p < 2; p++) { + for (let k = 0; k < 2; k++) A[p][k] += b[p] * b[k]; d[p] += b[p] * (t[c] - x * x * x); } } - const [a, b, cc] = solve(A, d); - out.set([a, b, cc, 1 - a - b - cc], c * 4); + const [a, b] = solve2x2(A, d); + out.set([0, a, b, 1 - a - b], c * 4); } return out; } diff --git a/docker/frontend/src/engine/rawDevelop.ts b/docker/frontend/src/engine/rawDevelop.ts index 06b9e4c..82ea9b6 100644 --- a/docker/frontend/src/engine/rawDevelop.ts +++ b/docker/frontend/src/engine/rawDevelop.ts @@ -72,7 +72,7 @@ const SETTINGS = { outputBps: 16, outputColor: 0, noAutoScale: true, - useCameraWb: true, + useCameraWb: false, noAutoBright: true, gamm: [1, 1] as [number, number], userQual: 3, @@ -145,7 +145,7 @@ half4 main(float2 pos) { // The matrix was fitted luma-preserving, so mx is the value to hold. float hi = max(max(n.r, n.g), n.b); hi = max(hi, max(max(lin.r, lin.g), lin.b)); - rgb = mix(rgb, float3(mx), smoothstep(0.99, 1.02, hi)); + rgb = mix(rgb, float3(mx), smoothstep(0.92, 1.0, hi)); // The overflow used to fade towards white — mix(rgb / mx, 1, 1 - 1 / mx) — // which put every pixel of a blown sky on exactly 1.0 and threw the two stops // the sensor held above white away with it: LIGHT's HIGHLIGHT row then had a @@ -440,6 +440,16 @@ function invert3x3(m: number[][]): number[][] | null { } function getCamToSrgbMatrix(cd: any): number[][] { + if (cd?.rgb_cam && Array.isArray(cd.rgb_cam) && cd.rgb_cam.length >= 3) { + const m = [ + [cd.rgb_cam[0][0], cd.rgb_cam[0][1], cd.rgb_cam[0][2]], + [cd.rgb_cam[1][0], cd.rgb_cam[1][1], cd.rgb_cam[1][2]], + [cd.rgb_cam[2][0], cd.rgb_cam[2][1], cd.rgb_cam[2][2]], + ]; + const inv = invert3x3(m); + if (inv) return inv; + } + 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; @@ -462,16 +472,6 @@ function getCamToSrgbMatrix(cd: any): number[][] { 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 m = [ - [cd.rgb_cam[0][0], cd.rgb_cam[0][1], cd.rgb_cam[0][2]], - [cd.rgb_cam[1][0], cd.rgb_cam[1][1], cd.rgb_cam[1][2]], - [cd.rgb_cam[2][0], cd.rgb_cam[2][1], cd.rgb_cam[2][2]], - ]; - const inv = invert3x3(m); - if (inv) return inv; - } - return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; }