fix(raw): pin tone curve ends to eliminate shadow purple artifacts and fix camera matrix WB color cast
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user