| Message ID | 20261115175436.8673-1-firassharary3@gmail.com |
|---|---|
| State | New |
| Headers | show |
| Series |
|
| Related | show |
+Cc Ray. Quoting Firas Sharary (2026-11-15 17:54:36) > When the software ISP scales with the GPU debayer, the fragment shader > samples the Bayer texture at the interpolated position and derives the > Bayer phase of that position from floor(center.zw). The two can disagree: > the sampler resolves nearest filtering in fixed point, so a position just > below a texel boundary may return the next texel, while floor() still > selects the previous one. The pixel is then demosaiced with the phase of > one texel and the data of its neighbour. > > With a colour correction matrix loaded this shows up as isolated magenta > columns and rows, one pixel wide, whose position depends on the scaling > ratio. On an OV5693 read at 2592x1944 through an Intel IPU6 (Tiger Lake, > Iris Xe, Mesa 25.0.7) a 1920x1080 stream has them at x=731, x=1211 and > y=731, and a 2560x1440 stream at y=1051. In those lines the green channel > is zero. > > Compute the texel from floor(center.zw) once and sample at its centre, so > the phase and the data always come from the same texel. > > While at it, handle the neighbours that fall outside the image. They were > clamped to the edge texel, which has a different Bayer colour than the > pixel asked for and tinted the first row and column. Use the neighbour at > the same distance on the other side instead. > Is this the same as / related to: libcamera: software_isp: debayer_egl: Fix texture sampling steps https://patchwork.libcamera.org/patch/28702/ ? -- Kieran > Signed-off-by: Firas Sharary <firassharary3@gmail.com> > --- > > Tested with v0.8.0 on a Surface Pro 7+; bayer_unpacked.frag is unchanged > between v0.8.0 and master. Captures at 1280x720, 1920x1080 and 2560x1440 > from the 2592x1944 mode no longer show the lines or the tinted border. > > bayer_1x_packed.frag derives its coordinates differently and I could not > test it: this machine's IPU6 does not deliver packed frames at all (see > "media: ipu6: packed raw capture never completes a frame on Tiger Lake" > on linux-media). > src/libcamera/shaders/bayer_unpacked.frag | 60 +++++++++++++++++------ > 1 file changed, 46 insertions(+), 14 deletions(-) > > diff --git a/src/libcamera/shaders/bayer_unpacked.frag b/src/libcamera/shaders/bayer_unpacked.frag > index be2f89d..f06022b 100644 > --- a/src/libcamera/shaders/bayer_unpacked.frag > +++ b/src/libcamera/shaders/bayer_unpacked.frag > @@ -24,6 +24,10 @@ uniform sampler2D tex_y; > varying vec4 center; > varying vec4 yCoord; > varying vec4 xCoord; > +uniform vec2 tex_size; > +uniform vec2 tex_step; > +uniform vec2 tex_bayer_first_red; > +uniform float stride_factor; > uniform vec3 awb; > uniform mat3 ccm; > uniform vec3 blacklevel; > @@ -56,17 +60,45 @@ void main(void) { > #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r > #endif > > - float C = fetch(center.x, center.y); // ( 0, 0) > + /* > + * Sample at the centre of the source pixel that floor(center.zw) selects, > + * not at the interpolated position. When the output is scaled the > + * interpolated position can lie so close to a texel boundary that the > + * sampler, which resolves nearest filtering in fixed point, returns the > + * neighbouring texel. The pixel is then demosaiced with the Bayer phase > + * of one texel and the data of another, which shows up as isolated > + * magenta rows and columns in the output. > + */ > + vec2 pos = floor(center.zw); > + vec2 snapped = (pos - tex_bayer_first_red + 0.5) / tex_size; > + vec2 c = vec2(snapped.x * stride_factor, snapped.y); > + vec4 dx = vec4(-2.0, -1.0, 1.0, 2.0) * tex_step.x; > + vec4 dy = vec4(-2.0, -1.0, 1.0, 2.0) * tex_step.y; > + vec4 xC = c.x + dx; > + vec4 yC = c.y + dy; > + > + /* > + * Neighbours beyond the edge of the image would be clamped to the edge > + * texel, which has a different Bayer colour than the pixel asked for and > + * tints the outermost row and column. Use the neighbour at the same > + * distance on the other side instead, which has the right colour. > + */ > + xC = mix(xC, c.x - dx, vec4(lessThan(xC, vec4(0.0))) + > + vec4(greaterThan(xC, vec4(stride_factor)))); > + yC = mix(yC, c.y - dy, vec4(lessThan(yC, vec4(0.0))) + > + vec4(greaterThan(yC, vec4(1.0)))); > + > + float C = fetch(c.x, c.y); // ( 0, 0) > const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0; > > // Determine which of four types of pixels we are on. > - vec2 alternate = mod(floor(center.zw), 2.0); > + vec2 alternate = mod(pos, 2.0); > > vec4 Dvec = vec4( > - fetch(xCoord[1], yCoord[1]), // (-1,-1) > - fetch(xCoord[1], yCoord[2]), // (-1, 1) > - fetch(xCoord[2], yCoord[1]), // ( 1,-1) > - fetch(xCoord[2], yCoord[2])); // ( 1, 1) > + fetch(xC[1], yC[1]), // (-1,-1) > + fetch(xC[1], yC[2]), // (-1, 1) > + fetch(xC[2], yC[1]), // ( 1,-1) > + fetch(xC[2], yC[2])); // ( 1, 1) > > vec4 PATTERN = (kC.xyz * C).xyzz; > > @@ -77,16 +109,16 @@ void main(void) { > Dvec.x += Dvec.y; > > vec4 value = vec4( > - fetch(center.x, yCoord[0]), // ( 0,-2) > - fetch(center.x, yCoord[1]), // ( 0,-1) > - fetch(xCoord[0], center.y), // (-2, 0) > - fetch(xCoord[1], center.y)); // (-1, 0) > + fetch(c.x, yC[0]), // ( 0,-2) > + fetch(c.x, yC[1]), // ( 0,-1) > + fetch(xC[0], c.y), // (-2, 0) > + fetch(xC[1], c.y)); // (-1, 0) > > vec4 temp = vec4( > - fetch(center.x, yCoord[3]), // ( 0, 2) > - fetch(center.x, yCoord[2]), // ( 0, 1) > - fetch(xCoord[3], center.y), // ( 2, 0) > - fetch(xCoord[2], center.y)); // ( 1, 0) > + fetch(c.x, yC[3]), // ( 0, 2) > + fetch(c.x, yC[2]), // ( 0, 1) > + fetch(xC[3], c.y), // ( 2, 0) > + fetch(xC[2], c.y)); // ( 1, 0) > > // Even the simplest compilers should be able to constant-fold these to > // avoid the division. > -- > 2.47.3
diff --git a/src/libcamera/shaders/bayer_unpacked.frag b/src/libcamera/shaders/bayer_unpacked.frag index be2f89d..f06022b 100644 --- a/src/libcamera/shaders/bayer_unpacked.frag +++ b/src/libcamera/shaders/bayer_unpacked.frag @@ -24,6 +24,10 @@ uniform sampler2D tex_y; varying vec4 center; varying vec4 yCoord; varying vec4 xCoord; +uniform vec2 tex_size; +uniform vec2 tex_step; +uniform vec2 tex_bayer_first_red; +uniform float stride_factor; uniform vec3 awb; uniform mat3 ccm; uniform vec3 blacklevel; @@ -56,17 +60,45 @@ void main(void) { #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r #endif - float C = fetch(center.x, center.y); // ( 0, 0) + /* + * Sample at the centre of the source pixel that floor(center.zw) selects, + * not at the interpolated position. When the output is scaled the + * interpolated position can lie so close to a texel boundary that the + * sampler, which resolves nearest filtering in fixed point, returns the + * neighbouring texel. The pixel is then demosaiced with the Bayer phase + * of one texel and the data of another, which shows up as isolated + * magenta rows and columns in the output. + */ + vec2 pos = floor(center.zw); + vec2 snapped = (pos - tex_bayer_first_red + 0.5) / tex_size; + vec2 c = vec2(snapped.x * stride_factor, snapped.y); + vec4 dx = vec4(-2.0, -1.0, 1.0, 2.0) * tex_step.x; + vec4 dy = vec4(-2.0, -1.0, 1.0, 2.0) * tex_step.y; + vec4 xC = c.x + dx; + vec4 yC = c.y + dy; + + /* + * Neighbours beyond the edge of the image would be clamped to the edge + * texel, which has a different Bayer colour than the pixel asked for and + * tints the outermost row and column. Use the neighbour at the same + * distance on the other side instead, which has the right colour. + */ + xC = mix(xC, c.x - dx, vec4(lessThan(xC, vec4(0.0))) + + vec4(greaterThan(xC, vec4(stride_factor)))); + yC = mix(yC, c.y - dy, vec4(lessThan(yC, vec4(0.0))) + + vec4(greaterThan(yC, vec4(1.0)))); + + float C = fetch(c.x, c.y); // ( 0, 0) const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0; // Determine which of four types of pixels we are on. - vec2 alternate = mod(floor(center.zw), 2.0); + vec2 alternate = mod(pos, 2.0); vec4 Dvec = vec4( - fetch(xCoord[1], yCoord[1]), // (-1,-1) - fetch(xCoord[1], yCoord[2]), // (-1, 1) - fetch(xCoord[2], yCoord[1]), // ( 1,-1) - fetch(xCoord[2], yCoord[2])); // ( 1, 1) + fetch(xC[1], yC[1]), // (-1,-1) + fetch(xC[1], yC[2]), // (-1, 1) + fetch(xC[2], yC[1]), // ( 1,-1) + fetch(xC[2], yC[2])); // ( 1, 1) vec4 PATTERN = (kC.xyz * C).xyzz; @@ -77,16 +109,16 @@ void main(void) { Dvec.x += Dvec.y; vec4 value = vec4( - fetch(center.x, yCoord[0]), // ( 0,-2) - fetch(center.x, yCoord[1]), // ( 0,-1) - fetch(xCoord[0], center.y), // (-2, 0) - fetch(xCoord[1], center.y)); // (-1, 0) + fetch(c.x, yC[0]), // ( 0,-2) + fetch(c.x, yC[1]), // ( 0,-1) + fetch(xC[0], c.y), // (-2, 0) + fetch(xC[1], c.y)); // (-1, 0) vec4 temp = vec4( - fetch(center.x, yCoord[3]), // ( 0, 2) - fetch(center.x, yCoord[2]), // ( 0, 1) - fetch(xCoord[3], center.y), // ( 2, 0) - fetch(xCoord[2], center.y)); // ( 1, 0) + fetch(c.x, yC[3]), // ( 0, 2) + fetch(c.x, yC[2]), // ( 0, 1) + fetch(xC[3], c.y), // ( 2, 0) + fetch(xC[2], c.y)); // ( 1, 0) // Even the simplest compilers should be able to constant-fold these to // avoid the division.
When the software ISP scales with the GPU debayer, the fragment shader samples the Bayer texture at the interpolated position and derives the Bayer phase of that position from floor(center.zw). The two can disagree: the sampler resolves nearest filtering in fixed point, so a position just below a texel boundary may return the next texel, while floor() still selects the previous one. The pixel is then demosaiced with the phase of one texel and the data of its neighbour. With a colour correction matrix loaded this shows up as isolated magenta columns and rows, one pixel wide, whose position depends on the scaling ratio. On an OV5693 read at 2592x1944 through an Intel IPU6 (Tiger Lake, Iris Xe, Mesa 25.0.7) a 1920x1080 stream has them at x=731, x=1211 and y=731, and a 2560x1440 stream at y=1051. In those lines the green channel is zero. Compute the texel from floor(center.zw) once and sample at its centre, so the phase and the data always come from the same texel. While at it, handle the neighbours that fall outside the image. They were clamped to the edge texel, which has a different Bayer colour than the pixel asked for and tinted the first row and column. Use the neighbour at the same distance on the other side instead. Signed-off-by: Firas Sharary <firassharary3@gmail.com> --- Tested with v0.8.0 on a Surface Pro 7+; bayer_unpacked.frag is unchanged between v0.8.0 and master. Captures at 1280x720, 1920x1080 and 2560x1440 from the 2592x1944 mode no longer show the lines or the tinted border. bayer_1x_packed.frag derives its coordinates differently and I could not test it: this machine's IPU6 does not deliver packed frames at all (see "media: ipu6: packed raw capture never completes a frame on Tiger Lake" on linux-media). src/libcamera/shaders/bayer_unpacked.frag | 60 +++++++++++++++++------ 1 file changed, 46 insertions(+), 14 deletions(-)