From c6cfb2a02ad71ed9eff9826df014c7ca626c9935 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 19 Sep 2026 22:05:39 +0200 Subject: [PATCH] Put the -1 on the greens along the chroma axis, not across it The Malvar "R at green in R row" kernel weights the two greens two sites away along the row at -1 and the pair up and down the column at +1/2. The shader had the two swapped, in the comment as well as the code, so the transcription checked against itself. Both sum to zero and reconstruct a flat patch exactly, which is all the tests fed it. On an edge the correction at green sites is half strength and the false colour doubles: 0.375 against 0.19 on a grey step, and a blue/yellow zipper around every clipped highlight at 1:1. The other three kernels and the CFA tables were right. A grey vertical step now runs through the pass; the transposed kernel fails it at 0.375. --- core/dr-gpu/src/demosaic.rs | 47 +++++++++++++++++++++++++++ core/dr-gpu/src/shaders/demosaic.wgsl | 14 +++++--- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/core/dr-gpu/src/demosaic.rs b/core/dr-gpu/src/demosaic.rs index cc36dfe..f4cc2a6 100644 --- a/core/dr-gpu/src/demosaic.rs +++ b/core/dr-gpu/src/demosaic.rs @@ -1347,6 +1347,53 @@ mod tests { } } + #[test] + fn a_grey_step_edge_stays_grey() { + // A flat patch cannot tell the Malvar kernels from any other set of + // weights that sum to zero. An edge can. A grey vertical step, so + // every photosite records the same profile, must come back with the + // three channels close together on both sides; any spread is false + // colour from interpolating across the edge. + // + // The bound is set by the paper's kernels, which peak at 0.19 here. + // With the ±2 terms of the green-site kernels transposed — the bug + // this test was written against — the peak is 0.375. + let Some(ctx) = ctx() else { return }; + let d = Demosaicer::new(&ctx).expect("demosaicer"); + + let size = 32u32; + let white = 16383u16; + let mut raw = flat_cfa(CfaPattern::Rggb, size, [0, 0, 0], 0, white); + for y in 0..size { + for x in size / 2..size { + raw.data[(y * size + x) as usize] = white; + } + } + + let img = d.run(&raw).expect("demosaic"); + let px = read_rgba(&ctx, &img); + let (w, _) = img.size(); + + let mut worst = (0.0f32, 0u32, 0u32); + for y in 2..size - 2 { + for x in 2..size - 2 { + let p = px[(y * w + x) as usize]; + let spread = (p[0] - p[1]).abs().max((p[2] - p[1]).abs()); + if spread > worst.0 { + worst = (spread, x, y); + } + } + } + assert!( + worst.0 < 0.25, + "false colour of {} at ({}, {}) on a grey edge — the green-site \ + kernels are interpolating across the edge", + worst.0, + worst.1, + worst.2 + ); + } + #[test] fn output_is_free_of_nan_and_negatives() { // f16 NaN propagates silently through every later stage; a negative diff --git a/core/dr-gpu/src/shaders/demosaic.wgsl b/core/dr-gpu/src/shaders/demosaic.wgsl index f86b0f9..7776d0b 100644 --- a/core/dr-gpu/src/shaders/demosaic.wgsl +++ b/core/dr-gpu/src/shaders/demosaic.wgsl @@ -160,12 +160,18 @@ fn main(@builtin(global_invocation_id) gid: vec3) { // Green is measured. Red and blue are interpolated from their own // axis, with a correction from the green Laplacian. // - // Malvar "G at R/B locations" kernels, transposed per axis: - // chroma along the row: (5c + 4(w1+e1) - (nw+ne+sw+se) - (n2+s2) + 0.5(w2+e2)) / 8 + // Malvar "R at green in R row" kernel, and its transpose: + // chroma along the row: (5c + 4(w1+e1) - (nw+ne+sw+se) - (w2+e2) + 0.5(n2+s2)) / 8 + // + // The -1 goes on the two greens *along* the chroma axis and the +0.5 + // on the pair across it. Transposed, both kernels still sum to zero + // and reconstruct a flat patch exactly, but on an edge the correction + // at green sites is half strength and the false colour doubles: a + // blue/yellow zipper around every clipped highlight. let along_row = - (5.0 * c + 4.0 * (w1 + e1) - diag1 - vert2 + 0.5 * horiz2) * 0.125; + (5.0 * c + 4.0 * (w1 + e1) - diag1 - horiz2 + 0.5 * vert2) * 0.125; let along_col = - (5.0 * c + 4.0 * (n1 + s1) - diag1 - horiz2 + 0.5 * vert2) * 0.125; + (5.0 * c + 4.0 * (n1 + s1) - diag1 - vert2 + 0.5 * horiz2) * 0.125; let red_horizontal = red_is_horizontal(gid.x, gid.y); let r = select(along_col, along_row, red_horizontal);