Skip to main content

rawshift_image/processing/demosaic/
bilinear.rs

1use super::{Demosaic, DemosaicError};
2use crate::core::image::{CfaPattern, RawImage};
3use rayon::prelude::*;
4
5/// Bilinear interpolation demosaicing algorithm.
6///
7/// A fast, simple demosaicing algorithm that interpolates missing color
8/// values using the average of neighboring pixels. This produces acceptable
9/// results for most images but may show color fringing on high-contrast edges.
10///
11/// This implementation uses rayon for parallel row processing on multi-core systems.
12pub struct Bilinear;
13
14impl Demosaic for Bilinear {
15    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
16        let width = raw.active_area().size.width;
17        let height = raw.active_area().size.height;
18        let x_offset = raw.active_area().origin.x;
19        let y_offset = raw.active_area().origin.y;
20
21        let expected_size = (width as usize) * (height as usize) * 3;
22        if output.len() != expected_size {
23            return Err(DemosaicError::BufferSizeMismatch {
24                expected: expected_size,
25                actual: output.len(),
26            });
27        }
28
29        let raw_width = raw.width();
30        let raw_height = raw.height();
31        let raw_data = &raw.data;
32        let cfa_pattern = raw.cfa_pattern();
33        let row_stride = (width as usize) * 3;
34
35        // Get raw pixel with bounds checking (closure captures raw data)
36        let get_raw = |x: u32, y: u32| -> u16 {
37            if x < raw_width && y < raw_height {
38                raw_data[(y as usize) * (raw_width as usize) + (x as usize)]
39            } else {
40                0
41            }
42        };
43
44        // Process rows in parallel
45        output
46            .par_chunks_mut(row_stride)
47            .enumerate()
48            .for_each(|(y, row_output)| {
49                let abs_y = (y as u32) + y_offset;
50
51                for x in 0..width {
52                    let abs_x = x + x_offset;
53                    let (r, g, b) = demosaic_pixel_bilinear(abs_x, abs_y, cfa_pattern, &get_raw);
54
55                    let idx = (x as usize) * 3;
56                    row_output[idx] = r;
57                    row_output[idx + 1] = g;
58                    row_output[idx + 2] = b;
59                }
60            });
61
62        Ok(())
63    }
64}
65
66/// Interpolates a single pixel using bilinear interpolation.
67/// `get_raw`: (x: u32, y: u32) -> <raw pixel value>: u16
68fn demosaic_pixel_bilinear<F>(x: u32, y: u32, pattern: CfaPattern, get_raw: &F) -> (u16, u16, u16)
69where
70    F: Fn(u32, u32) -> u16,
71{
72    // Determine the color of the current pixel location
73    let (is_red, is_blue) = match pattern {
74        CfaPattern::Rggb => (
75            x.is_multiple_of(2) && y.is_multiple_of(2),
76            (x % 2 == 1) && (y % 2 == 1),
77        ),
78        CfaPattern::Grbg => (
79            (x % 2 == 1) && y.is_multiple_of(2),
80            x.is_multiple_of(2) && (y % 2 == 1),
81        ),
82        CfaPattern::Gbrg => (
83            x.is_multiple_of(2) && (y % 2 == 1),
84            (x % 2 == 1) && y.is_multiple_of(2),
85        ),
86        CfaPattern::Bggr => (
87            (x % 2 == 1) && (y % 2 == 1),
88            x.is_multiple_of(2) && y.is_multiple_of(2),
89        ),
90    };
91
92    // Green is true if it's neither red nor blue
93    let is_green = !is_red && !is_blue;
94
95    if is_green {
96        // Current pixel is Green.
97        // We need to interpolate Red and Blue.
98        // One of them will be vertical neighbors, the other horizontal neighbors.
99
100        let g = get_raw(x, y);
101
102        let (r, b) = match pattern {
103            // For RGGB:
104            // R G R G
105            // G B G B
106            // If we are at (0,1) [Green], Left/Right is B, Up/Down is R
107            // If we are at (1,0) [Green], Left/Right is R, Up/Down is B
108            CfaPattern::Rggb | CfaPattern::Bggr => {
109                if y.is_multiple_of(2) {
110                    // Even row
111                    // R G R G (RGGB case) -> (x%2==1) -> Horizontal is R, Vertical is B
112                    // B G B G (BGGR case) -> (x%2==1) -> Horizontal is B, Vertical is R
113                    // checking pattern specifically:
114                    if matches!(pattern, CfaPattern::Rggb) {
115                        // Row 0: R G R G. We are at G. Neighbors (left/right) are R. Vertical are B.
116                        (
117                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
118                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
119                        )
120                    } else {
121                        // BGGR
122                        // Row 0: B G B G. We are at G. Neighbors (left/right) are B. Vertical are R.
123                        (
124                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
125                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
126                        )
127                    }
128                } else {
129                    // Odd row
130                    // G B G B (RGGB case). We are at G. Left/Right B, Vert R.
131                    // G R G R (BGGR case). We are at G. Left/Right R, Vert B.
132                    if matches!(pattern, CfaPattern::Rggb) {
133                        (
134                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
135                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
136                        )
137                    } else {
138                        (
139                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
140                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
141                        )
142                    }
143                }
144            }
145            CfaPattern::Grbg | CfaPattern::Gbrg => {
146                // Similar logic...
147                // GRBG:
148                // G R G R
149                // B G B G
150                if y.is_multiple_of(2) {
151                    // Even row G R G R. At G. Horizontal R, Vert B.
152                    if matches!(pattern, CfaPattern::Grbg) {
153                        (
154                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
155                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
156                        )
157                    } else {
158                        // GBRG: G B G B. At G. Horizontal B, Vert R.
159                        (
160                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
161                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
162                        )
163                    }
164                } else {
165                    // Odd row
166                    // GRBG: B G B G. At G. Horizontal B, Vert R.
167                    if matches!(pattern, CfaPattern::Grbg) {
168                        (
169                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
170                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
171                        )
172                    } else {
173                        // GBRG: R G R G. At G. Horizontal R, Vert B.
174                        (
175                            avg2(get_raw(x.saturating_sub(1), y), get_raw(x + 1, y)),
176                            avg2(get_raw(x, y.saturating_sub(1)), get_raw(x, y + 1)),
177                        )
178                    }
179                }
180            }
181        };
182        (r, g, b)
183    } else if is_red {
184        // Current is Red.
185        let r = get_raw(x, y);
186        // Green is average of 4 cross neighbors (up, down, left, right)
187        let g = avg4(
188            get_raw(x, y.saturating_sub(1)),
189            get_raw(x, y + 1),
190            get_raw(x.saturating_sub(1), y),
191            get_raw(x + 1, y),
192        );
193        // Blue is average of 4 diagonal neighbors
194        let b = avg4(
195            get_raw(x.saturating_sub(1), y.saturating_sub(1)),
196            get_raw(x + 1, y.saturating_sub(1)),
197            get_raw(x.saturating_sub(1), y + 1),
198            get_raw(x + 1, y + 1),
199        );
200        (r, g, b)
201    } else {
202        // Current is Blue.
203        let b = get_raw(x, y);
204        // Green is average of 4 cross neighbors
205        let g = avg4(
206            get_raw(x, y.saturating_sub(1)),
207            get_raw(x, y + 1),
208            get_raw(x.saturating_sub(1), y),
209            get_raw(x + 1, y),
210        );
211        // Red is average of 4 diagonal neighbors
212        let r = avg4(
213            get_raw(x.saturating_sub(1), y.saturating_sub(1)),
214            get_raw(x + 1, y.saturating_sub(1)),
215            get_raw(x.saturating_sub(1), y + 1),
216            get_raw(x + 1, y + 1),
217        );
218        (r, g, b)
219    }
220}
221
222#[inline(always)]
223fn avg2(a: u16, b: u16) -> u16 {
224    ((a as u32 + b as u32) / 2) as u16
225}
226
227#[inline(always)]
228fn avg4(a: u16, b: u16, c: u16, d: u16) -> u16 {
229    ((a as u32 + b as u32 + c as u32 + d as u32) / 4) as u16
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::core::image::{Point, Rect, Size};
236
237    /// Create a test raw image with given dimensions and CFA pattern.
238    fn create_test_raw(width: u32, height: u32, pattern: CfaPattern, value: u16) -> RawImage {
239        let size = Size::new(width, height);
240        let active_area = Rect::new(Point::ORIGIN, size);
241        let pixel_count = (width * height) as usize;
242        RawImage::builder(size, active_area, 14, pattern)
243            .white_level(16383)
244            .data(vec![value; pixel_count])
245            .build()
246    }
247
248    /// Create a raw image with a 2x2 Bayer pattern.
249    fn create_bayer_2x2(pattern: CfaPattern) -> RawImage {
250        // Create a 2x2 pattern with distinct values for each cell
251        let mut raw = create_test_raw(2, 2, pattern, 0);
252        // Set values for each pixel position
253        raw.data[0] = 1000; // (0, 0)
254        raw.data[1] = 2000; // (1, 0)
255        raw.data[2] = 3000; // (0, 1)
256        raw.data[3] = 4000; // (1, 1)
257        raw
258    }
259
260    #[test]
261    fn test_demosaic_into_correct_size() {
262        let raw = create_test_raw(10, 10, CfaPattern::Rggb, 1000);
263        let mut output = vec![0u16; 10 * 10 * 3]; // Correct size
264
265        let result = Bilinear.demosaic_into(&raw, &mut output);
266        assert!(result.is_ok());
267    }
268
269    #[test]
270    fn test_demosaic_into_wrong_size() {
271        let raw = create_test_raw(10, 10, CfaPattern::Rggb, 1000);
272        let mut output = vec![0u16; 50]; // Too small
273
274        let result = Bilinear.demosaic_into(&raw, &mut output);
275        assert!(matches!(
276            result,
277            Err(DemosaicError::BufferSizeMismatch { .. })
278        ));
279    }
280
281    #[test]
282    fn test_demosaic_solid_color() {
283        // A uniform input should produce roughly uniform output
284        let raw = create_test_raw(10, 10, CfaPattern::Rggb, 5000);
285        let demosaic = Bilinear;
286        let rgb = demosaic.demosaic(&raw);
287
288        assert_eq!(rgb.width(), 10);
289        assert_eq!(rgb.height(), 10);
290        assert_eq!(rgb.data.len(), 10 * 10 * 3);
291
292        // Interior pixels (not on edge) should be close to input value
293        // Edge pixels may have lower values due to boundary handling
294        for y in 1..9 {
295            for x in 1..9 {
296                let idx = ((y * 10 + x) * 3) as usize;
297                for c in 0..3 {
298                    let pixel = rgb.data[idx + c];
299                    assert!(
300                        (4000..=5500).contains(&pixel),
301                        "Interior pixel at ({},{}) channel {} value {} out of range",
302                        x,
303                        y,
304                        c,
305                        pixel
306                    );
307                }
308            }
309        }
310    }
311
312    #[test]
313    fn test_demosaic_all_cfa_patterns() {
314        let patterns = [
315            CfaPattern::Rggb,
316            CfaPattern::Grbg,
317            CfaPattern::Gbrg,
318            CfaPattern::Bggr,
319        ];
320
321        for pattern in patterns {
322            let raw = create_test_raw(8, 8, pattern, 2000);
323            let rgb = Bilinear.demosaic(&raw);
324
325            assert_eq!(rgb.width(), 8);
326            assert_eq!(rgb.height(), 8);
327            assert_eq!(rgb.data.len(), 8 * 8 * 3);
328
329            // Verify all pixels have reasonable values
330            for pixel in &rgb.data {
331                assert!(
332                    *pixel <= 3000,
333                    "Pattern {:?}: pixel value {} too high",
334                    pattern,
335                    pixel
336                );
337            }
338        }
339    }
340
341    #[test]
342    fn test_demosaic_rggb_pattern() {
343        // Test that RGGB pattern is correctly interpreted
344        let raw = create_bayer_2x2(CfaPattern::Rggb);
345        let rgb = Bilinear.demosaic(&raw);
346
347        // For RGGB, position (0,0) is Red
348        // The output should have interpolated values
349        assert_eq!(rgb.width(), 2);
350        assert_eq!(rgb.height(), 2);
351
352        // First pixel (0,0) - this is a Red position in RGGB
353        let r = rgb.data[0];
354        let g = rgb.data[1];
355        let _b = rgb.data[2];
356
357        // Red should be the original value (1000)
358        assert_eq!(r, 1000, "Red at RGGB position (0,0) should be 1000");
359
360        // Green should be interpolated from neighbors
361        // At (0,0), only (1,0) and (0,1) are in bounds, both at edges
362        assert!(g > 0, "Green should be interpolated");
363    }
364
365    #[test]
366    fn test_demosaic_with_active_area() {
367        // Test that active_area is respected
368        let raw = {
369            let size = Size::new(10, 10);
370            let active_area = Rect::from_coords(3, 3, 4, 4);
371            RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
372                .white_level(16383)
373                .data(vec![1000u16; 100])
374                .build()
375        };
376
377        let rgb = Bilinear.demosaic(&raw);
378
379        // Output dimensions should match active area
380        assert_eq!(rgb.width(), 4);
381        assert_eq!(rgb.height(), 4);
382        assert_eq!(rgb.data.len(), 4 * 4 * 3);
383    }
384
385    #[test]
386    fn test_avg2() {
387        assert_eq!(avg2(0, 0), 0);
388        assert_eq!(avg2(100, 100), 100);
389        assert_eq!(avg2(100, 200), 150);
390        assert_eq!(avg2(0, 65535), 32767);
391    }
392
393    #[test]
394    fn test_avg4() {
395        assert_eq!(avg4(0, 0, 0, 0), 0);
396        assert_eq!(avg4(100, 100, 100, 100), 100);
397        assert_eq!(avg4(0, 100, 200, 300), 150);
398        assert_eq!(avg4(0, 0, 0, 65535), 16383);
399    }
400
401    #[test]
402    fn test_bilinear_all_cfa_patterns() {
403        // All 4 CFA patterns should produce valid RGB output with correct dimensions
404        let patterns = [
405            CfaPattern::Rggb,
406            CfaPattern::Grbg,
407            CfaPattern::Gbrg,
408            CfaPattern::Bggr,
409        ];
410
411        for pattern in patterns {
412            let raw = create_test_raw(6, 6, pattern, 8000);
413            let rgb = Bilinear.demosaic(&raw);
414
415            assert_eq!(rgb.width(), 6, "width for {:?}", pattern);
416            assert_eq!(rgb.height(), 6, "height for {:?}", pattern);
417            assert_eq!(rgb.data.len(), 6 * 6 * 3, "data length for {:?}", pattern);
418
419            // All output pixels must be in valid u16 range (which they always are,
420            // but also check that at least some pixels are non-zero for a non-zero input)
421            let non_zero = rgb.data.iter().any(|&v| v > 0);
422            assert!(
423                non_zero,
424                "Output for {:?} should have non-zero pixels",
425                pattern
426            );
427        }
428    }
429
430    #[test]
431    fn test_bilinear_with_active_area() {
432        // Test various active area offsets
433        let raw = {
434            let size = Size::new(12, 12);
435            let active_area = Rect::from_coords(2, 4, 6, 6);
436            RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
437                .white_level(16383)
438                .data(vec![5000u16; 144])
439                .build()
440        };
441
442        let rgb = Bilinear.demosaic(&raw);
443
444        assert_eq!(
445            rgb.width(),
446            6,
447            "output width should match active area width"
448        );
449        assert_eq!(
450            rgb.height(),
451            6,
452            "output height should match active area height"
453        );
454        assert_eq!(
455            rgb.data.len(),
456            6 * 6 * 3,
457            "output should have correct data length"
458        );
459
460        // Output values are u16, so always in [0, 65535] by definition
461        assert!(!rgb.data.is_empty(), "output should have pixel data");
462    }
463
464    #[test]
465    fn test_bilinear_gradient_smooth() {
466        // A horizontally-varying input should produce a smooth (monotonically varying)
467        // green channel in the output, since bilinear averages neighbors.
468        let width = 8u32;
469        let height = 4u32;
470        let size = Size::new(width, height);
471        let active_area = Rect::new(Point::ORIGIN, size);
472
473        // Fill with a horizontal gradient: pixel value increases with x
474        let mut data = vec![0u16; (width * height) as usize];
475        for y in 0..height as usize {
476            for x in 0..width as usize {
477                data[y * width as usize + x] = (x as u16) * 1000;
478            }
479        }
480
481        let raw = RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
482            .white_level(16383)
483            .data(data)
484            .build();
485
486        let rgb = Bilinear.demosaic(&raw);
487
488        // Check that the green channel (index 1) generally increases left to right
489        // for a middle row. Allow for some non-monotonicity at edges.
490        let mid_row = 2usize;
491        let row_start = mid_row * width as usize * 3;
492
493        // Compare interior pixels: pixel at x+1 should have green >= pixel at x
494        // (with tolerance for boundary effects)
495        for x in 1..(width as usize - 2) {
496            let g_left = rgb.data[row_start + (x - 1) * 3 + 1];
497            let g_right = rgb.data[row_start + (x + 1) * 3 + 1];
498            assert!(
499                g_right >= g_left || (g_right as i32 - g_left as i32).abs() < 2000,
500                "gradient smoothness: g[{}]={} should not greatly exceed g[{}]={} in row {}",
501                x - 1,
502                g_left,
503                x + 1,
504                g_right,
505                mid_row
506            );
507        }
508    }
509}