Skip to main content

rawshift_image/processing/demosaic/
bayer.rs

1//! Bayer-specific demosaicing algorithms.
2//!
3//! This module contains demosaicing algorithms designed for standard 2x2 Bayer
4//! color filter arrays found in most cameras (Sony, Canon, Nikon, DNG, etc.).
5
6use super::{Demosaic, DemosaicError};
7use crate::core::image::{CfaPattern, RawImage};
8use rayon::prelude::*;
9
10// =============================================================================
11// AMaZE — Aliasing Minimization and Zipper Elimination
12// =============================================================================
13
14/// AMaZE (Aliasing Minimization and Zipper Elimination) demosaicing algorithm.
15///
16/// Industry standard for high-detail, low-noise images. Uses directional
17/// interpolation with adaptive selection based on local gradient homogeneity,
18/// followed by color-difference reconstruction for non-green channels.
19///
20/// Key features:
21/// - Gradient-based direction selection (horizontal vs vertical)
22/// - Laplacian second-derivative correction for green interpolation
23/// - Color-difference model for R/B reconstruction
24/// - Homogeneity-driven blending at ambiguous edges
25///
26/// Reference: [RawTherapee AMaZE implementation](https://github.com/RawTherapee/RawTherapee)
27pub struct Amaze;
28
29impl Demosaic for Amaze {
30    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
31        let width = raw.active_area().size.width as usize;
32        let height = raw.active_area().size.height as usize;
33        let x_off = raw.active_area().origin.x as usize;
34        let y_off = raw.active_area().origin.y as usize;
35        let raw_w = raw.width() as usize;
36
37        let expected_size = width * height * 3;
38        if output.len() != expected_size {
39            return Err(DemosaicError::BufferSizeMismatch {
40                expected: expected_size,
41                actual: output.len(),
42            });
43        }
44
45        if width < 6 || height < 6 {
46            return Err(DemosaicError::InvalidDimensions);
47        }
48
49        let white = raw.white_level() as f32;
50
51        // Determine color at each CFA position: 0=Red, 1=Green(R-row), 2=Blue, 3=Green(B-row)
52        let fc = |x: usize, y: usize| -> u8 {
53            let ax = x + x_off;
54            let ay = y + y_off;
55            match raw.cfa_pattern() {
56                CfaPattern::Rggb => match (ax % 2, ay % 2) {
57                    (0, 0) => 0, // R
58                    (1, 0) => 1, // G on R-row
59                    (0, 1) => 3, // G on B-row
60                    _ => 2,      // B
61                },
62                CfaPattern::Grbg => match (ax % 2, ay % 2) {
63                    (0, 0) => 1,
64                    (1, 0) => 0,
65                    (0, 1) => 2,
66                    _ => 3,
67                },
68                CfaPattern::Gbrg => match (ax % 2, ay % 2) {
69                    (0, 0) => 3,
70                    (1, 0) => 2,
71                    (0, 1) => 0,
72                    _ => 1,
73                },
74                CfaPattern::Bggr => match (ax % 2, ay % 2) {
75                    (0, 0) => 2,
76                    (1, 0) => 3,
77                    (0, 1) => 1,
78                    _ => 0,
79                },
80            }
81        };
82
83        // Safe accessor into raw data with mirror-padding at borders
84        let get = |x: isize, y: isize| -> f32 {
85            let cx = x.clamp(0, (width as isize) - 1) as usize;
86            let cy = y.clamp(0, (height as isize) - 1) as usize;
87            raw.data[(cy + y_off) * raw_w + (cx + x_off)] as f32
88        };
89
90        // ── Step 1: Green channel interpolation ──────────────────────
91
92        // Allocate green plane
93        let mut green = vec![0.0f32; width * height];
94
95        // For green pixels, just copy. For R/B pixels, interpolate green.
96        for y in 0..height {
97            for x in 0..width {
98                let color = fc(x, y);
99                let ix = x as isize;
100                let iy = y as isize;
101
102                if color == 1 || color == 3 {
103                    // Green pixel — copy directly
104                    green[y * width + x] = get(ix, iy);
105                } else {
106                    // Red or Blue pixel — interpolate green using directional gradients
107
108                    // Horizontal gradient (Laplacian-weighted)
109                    let dh = (get(ix - 1, iy) - get(ix + 1, iy)).abs()
110                        + (2.0 * get(ix, iy) - get(ix - 2, iy) - get(ix + 2, iy)).abs();
111
112                    // Vertical gradient
113                    let dv = (get(ix, iy - 1) - get(ix, iy + 1)).abs()
114                        + (2.0 * get(ix, iy) - get(ix, iy - 2) - get(ix, iy + 2)).abs();
115
116                    // Horizontal green estimate with 2nd-derivative correction
117                    let gh = (get(ix - 1, iy) + get(ix + 1, iy)) * 0.5
118                        + (2.0 * get(ix, iy) - get(ix - 2, iy) - get(ix + 2, iy)) * 0.25;
119
120                    // Vertical green estimate with 2nd-derivative correction
121                    let gv = (get(ix, iy - 1) + get(ix, iy + 1)) * 0.5
122                        + (2.0 * get(ix, iy) - get(ix, iy - 2) - get(ix, iy + 2)) * 0.25;
123
124                    // Adaptive direction selection
125                    let eps = 1e-5;
126                    if dh < dv * 0.5 {
127                        // Strong horizontal preference
128                        green[y * width + x] = gh;
129                    } else if dv < dh * 0.5 {
130                        // Strong vertical preference
131                        green[y * width + x] = gv;
132                    } else {
133                        // Blend based on gradient ratio
134                        let wh = 1.0 / (dh + eps);
135                        let wv = 1.0 / (dv + eps);
136                        green[y * width + x] = (wh * gh + wv * gv) / (wh + wv);
137                    }
138
139                    // Clamp to valid range
140                    green[y * width + x] = green[y * width + x].max(0.0);
141                }
142            }
143        }
144
145        // ── Step 2: Homogeneity-based green refinement ───────────────
146
147        // Compute horizontal and vertical green estimates for homogeneity test
148        let mut gh_plane = vec![0.0f32; width * height];
149        let mut gv_plane = vec![0.0f32; width * height];
150
151        for y in 0..height {
152            for x in 0..width {
153                let color = fc(x, y);
154                let ix = x as isize;
155                let iy = y as isize;
156
157                if color == 1 || color == 3 {
158                    gh_plane[y * width + x] = get(ix, iy);
159                    gv_plane[y * width + x] = get(ix, iy);
160                } else {
161                    gh_plane[y * width + x] = (get(ix - 1, iy) + get(ix + 1, iy)) * 0.5
162                        + (2.0 * get(ix, iy) - get(ix - 2, iy) - get(ix + 2, iy)) * 0.25;
163                    gv_plane[y * width + x] = (get(ix, iy - 1) + get(ix, iy + 1)) * 0.5
164                        + (2.0 * get(ix, iy) - get(ix, iy - 2) - get(ix, iy + 2)) * 0.25;
165                }
166            }
167        }
168
169        // Compute homogeneity in a 3x3 window around each pixel
170        let mut h_homo = vec![0i32; width * height];
171        let mut v_homo = vec![0i32; width * height];
172
173        let border = 3usize;
174        for y in border..height.saturating_sub(border) {
175            for x in border..width.saturating_sub(border) {
176                let color = fc(x, y);
177                if color == 1 || color == 3 {
178                    continue;
179                }
180
181                let mut hh = 0i32;
182                let mut vh = 0i32;
183
184                for dy in -1i32..=1 {
185                    for dx in -1i32..=1 {
186                        let nx = (x as i32 + dx) as usize;
187                        let ny = (y as i32 + dy) as usize;
188                        let idx = ny * width + nx;
189
190                        // Color-difference homogeneity for horizontal
191                        let cdh = (gh_plane[idx] - get(nx as isize, ny as isize)).abs();
192                        // Color-difference homogeneity for vertical
193                        let cdv = (gv_plane[idx] - get(nx as isize, ny as isize)).abs();
194
195                        // Luminance homogeneity
196                        let lh = (gh_plane[idx] - gh_plane[y * width + x]).abs();
197                        let lv = (gv_plane[idx] - gv_plane[y * width + x]).abs();
198
199                        let eps_h = cdh + lh;
200                        let eps_v = cdv + lv;
201
202                        if eps_h < eps_v {
203                            hh += 1;
204                        } else if eps_v < eps_h {
205                            vh += 1;
206                        }
207                    }
208                }
209
210                h_homo[y * width + x] = hh;
211                v_homo[y * width + x] = vh;
212            }
213        }
214
215        // Refine green using homogeneity
216        for y in border..height.saturating_sub(border) {
217            for x in border..width.saturating_sub(border) {
218                let color = fc(x, y);
219                if color == 1 || color == 3 {
220                    continue;
221                }
222
223                let idx = y * width + x;
224                let hh = h_homo[idx];
225                let vh = v_homo[idx];
226
227                if hh > vh + 1 {
228                    green[idx] = gh_plane[idx];
229                } else if vh > hh + 1 {
230                    green[idx] = gv_plane[idx];
231                } else {
232                    // Blend
233                    let wh = (hh + 1) as f32;
234                    let wv = (vh + 1) as f32;
235                    green[idx] = (wh * gh_plane[idx] + wv * gv_plane[idx]) / (wh + wv);
236                }
237
238                green[idx] = green[idx].max(0.0);
239            }
240        }
241
242        // Free intermediate buffers
243        drop(gh_plane);
244        drop(gv_plane);
245        drop(h_homo);
246        drop(v_homo);
247
248        // ── Step 3: R/B channel reconstruction via color-difference ──
249
250        // Build full R and B planes using color-difference interpolation.
251        // Color differences (R-G) and (B-G) vary more smoothly than raw R/B,
252        // so interpolating them produces fewer artifacts.
253
254        let mut red = vec![0.0f32; width * height];
255        let mut blue = vec![0.0f32; width * height];
256
257        // First pass: fill in known values and compute color differences
258        let mut cd_rg = vec![0.0f32; width * height]; // R - G
259        let mut cd_bg = vec![0.0f32; width * height]; // B - G
260
261        for y in 0..height {
262            for x in 0..width {
263                let idx = y * width + x;
264                let color = fc(x, y);
265                let val = get(x as isize, y as isize);
266
267                match color {
268                    0 => {
269                        // Red pixel
270                        red[idx] = val;
271                        cd_rg[idx] = val - green[idx];
272                    }
273                    2 => {
274                        // Blue pixel
275                        blue[idx] = val;
276                        cd_bg[idx] = val - green[idx];
277                    }
278                    _ => {}
279                }
280            }
281        }
282
283        // Second pass: interpolate color differences at missing positions
284        // For green pixels on red rows: need both R and B via color-difference
285        // For green pixels on blue rows: need both R and B via color-difference
286        for y in 0..height {
287            for x in 0..width {
288                let idx = y * width + x;
289                let color = fc(x, y);
290                let ix = x as isize;
291                let iy = y as isize;
292
293                match color {
294                    0 => {
295                        // Red pixel — need blue. Blue neighbors are at diagonals.
296                        let mut sum = 0.0f32;
297                        let mut count = 0.0f32;
298                        for &(dx, dy) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] {
299                            let nx = ix + dx as isize;
300                            let ny = iy + dy as isize;
301                            if nx >= 0 && nx < width as isize && ny >= 0 && ny < height as isize {
302                                let nidx = ny as usize * width + nx as usize;
303                                if fc(nx as usize, ny as usize) == 2 {
304                                    sum += cd_bg[nidx];
305                                    count += 1.0;
306                                }
307                            }
308                        }
309                        if count > 0.0 {
310                            blue[idx] = green[idx] + sum / count;
311                        } else {
312                            blue[idx] = green[idx];
313                        }
314                    }
315                    2 => {
316                        // Blue pixel — need red. Red neighbors are at diagonals.
317                        let mut sum = 0.0f32;
318                        let mut count = 0.0f32;
319                        for &(dx, dy) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] {
320                            let nx = ix + dx as isize;
321                            let ny = iy + dy as isize;
322                            if nx >= 0 && nx < width as isize && ny >= 0 && ny < height as isize {
323                                let nidx = ny as usize * width + nx as usize;
324                                if fc(nx as usize, ny as usize) == 0 {
325                                    sum += cd_rg[nidx];
326                                    count += 1.0;
327                                }
328                            }
329                        }
330                        if count > 0.0 {
331                            red[idx] = green[idx] + sum / count;
332                        } else {
333                            red[idx] = green[idx];
334                        }
335                    }
336                    1 => {
337                        // Green on R-row — need R (horizontal neighbors) and B (vertical neighbors)
338                        let mut sum_r = 0.0f32;
339                        let mut cnt_r = 0.0f32;
340                        for &dx in &[-1i32, 1] {
341                            let nx = ix + dx as isize;
342                            if nx >= 0 && nx < width as isize {
343                                let nidx = y * width + nx as usize;
344                                if fc(nx as usize, y) == 0 {
345                                    sum_r += cd_rg[nidx];
346                                    cnt_r += 1.0;
347                                }
348                            }
349                        }
350                        red[idx] = green[idx] + if cnt_r > 0.0 { sum_r / cnt_r } else { 0.0 };
351
352                        let mut sum_b = 0.0f32;
353                        let mut cnt_b = 0.0f32;
354                        for &dy in &[-1i32, 1] {
355                            let ny = iy + dy as isize;
356                            if ny >= 0 && ny < height as isize {
357                                let nidx = ny as usize * width + x;
358                                if fc(x, ny as usize) == 2 {
359                                    sum_b += cd_bg[nidx];
360                                    cnt_b += 1.0;
361                                }
362                            }
363                        }
364                        blue[idx] = green[idx] + if cnt_b > 0.0 { sum_b / cnt_b } else { 0.0 };
365                    }
366                    3 => {
367                        // Green on B-row — need B (horizontal neighbors) and R (vertical neighbors)
368                        let mut sum_b = 0.0f32;
369                        let mut cnt_b = 0.0f32;
370                        for &dx in &[-1i32, 1] {
371                            let nx = ix + dx as isize;
372                            if nx >= 0 && nx < width as isize {
373                                let nidx = y * width + nx as usize;
374                                if fc(nx as usize, y) == 2 {
375                                    sum_b += cd_bg[nidx];
376                                    cnt_b += 1.0;
377                                }
378                            }
379                        }
380                        blue[idx] = green[idx] + if cnt_b > 0.0 { sum_b / cnt_b } else { 0.0 };
381
382                        let mut sum_r = 0.0f32;
383                        let mut cnt_r = 0.0f32;
384                        for &dy in &[-1i32, 1] {
385                            let ny = iy + dy as isize;
386                            if ny >= 0 && ny < height as isize {
387                                let nidx = ny as usize * width + x;
388                                if fc(x, ny as usize) == 0 {
389                                    sum_r += cd_rg[nidx];
390                                    cnt_r += 1.0;
391                                }
392                            }
393                        }
394                        red[idx] = green[idx] + if cnt_r > 0.0 { sum_r / cnt_r } else { 0.0 };
395                    }
396                    _ => unreachable!(),
397                }
398            }
399        }
400
401        // ── Step 4: Zipper artifact reduction ────────────────────────
402
403        // Detect and correct zipper artifacts by checking local color-difference
404        // smoothness and replacing outliers with median-filtered values.
405        let mut red_out = red.clone();
406        let mut blue_out = blue.clone();
407
408        for y in 2..height.saturating_sub(2) {
409            for x in 2..width.saturating_sub(2) {
410                let idx = y * width + x;
411                let g = green[idx];
412
413                // Check red-green difference smoothness
414                let cd_r = red[idx] - g;
415                let cd_r_h1 = red[idx.wrapping_sub(1)] - green[idx.wrapping_sub(1)];
416                let cd_r_h2 = red[idx + 1] - green[idx + 1];
417                let cd_r_v1 = red[(y - 1) * width + x] - green[(y - 1) * width + x];
418                let cd_r_v2 = red[(y + 1) * width + x] - green[(y + 1) * width + x];
419
420                // If center color-difference is an outlier, smooth it
421                let avg_cd_r = (cd_r_h1 + cd_r_h2 + cd_r_v1 + cd_r_v2) * 0.25;
422                let var_r = (cd_r_h1 - avg_cd_r).abs()
423                    + (cd_r_h2 - avg_cd_r).abs()
424                    + (cd_r_v1 - avg_cd_r).abs()
425                    + (cd_r_v2 - avg_cd_r).abs();
426
427                if (cd_r - avg_cd_r).abs() > var_r * 1.5 + 1.0 {
428                    red_out[idx] = g + avg_cd_r;
429                }
430
431                // Same for blue-green
432                let cd_b = blue[idx] - g;
433                let cd_b_h1 = blue[idx.wrapping_sub(1)] - green[idx.wrapping_sub(1)];
434                let cd_b_h2 = blue[idx + 1] - green[idx + 1];
435                let cd_b_v1 = blue[(y - 1) * width + x] - green[(y - 1) * width + x];
436                let cd_b_v2 = blue[(y + 1) * width + x] - green[(y + 1) * width + x];
437
438                let avg_cd_b = (cd_b_h1 + cd_b_h2 + cd_b_v1 + cd_b_v2) * 0.25;
439                let var_b = (cd_b_h1 - avg_cd_b).abs()
440                    + (cd_b_h2 - avg_cd_b).abs()
441                    + (cd_b_v1 - avg_cd_b).abs()
442                    + (cd_b_v2 - avg_cd_b).abs();
443
444                if (cd_b - avg_cd_b).abs() > var_b * 1.5 + 1.0 {
445                    blue_out[idx] = g + avg_cd_b;
446                }
447            }
448        }
449
450        // ── Step 5: Write output ─────────────────────────────────────
451
452        // Convert from planar f32 to interleaved u16 output
453        output
454            .par_chunks_mut(width * 3)
455            .enumerate()
456            .for_each(|(y, row)| {
457                for x in 0..width {
458                    let idx = y * width + x;
459                    let out_idx = x * 3;
460                    row[out_idx] = red_out[idx].round().clamp(0.0, white) as u16;
461                    row[out_idx + 1] = green[idx].round().clamp(0.0, white) as u16;
462                    row[out_idx + 2] = blue_out[idx].round().clamp(0.0, white) as u16;
463                }
464            });
465
466        Ok(())
467    }
468}
469
470// =============================================================================
471// LMMSE — Linear Minimum Mean Square Error
472// =============================================================================
473
474/// LMMSE (Linear Minimum Mean Square Error) demosaicing algorithm.
475///
476/// High-ISO specialist that treats noise as a statistical probability.
477/// Particularly effective for images shot at high ISO where noise is prominent.
478pub struct Lmmse;
479
480impl Demosaic for Lmmse {
481    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
482        let width = raw.active_area().size.width as usize;
483        let height = raw.active_area().size.height as usize;
484        let x_off = raw.active_area().origin.x as usize;
485        let y_off = raw.active_area().origin.y as usize;
486        let raw_w = raw.width() as usize;
487
488        let expected_size = width * height * 3;
489        if output.len() != expected_size {
490            return Err(DemosaicError::BufferSizeMismatch {
491                expected: expected_size,
492                actual: output.len(),
493            });
494        }
495
496        if width < 6 || height < 6 {
497            return Err(DemosaicError::InvalidDimensions);
498        }
499
500        let white = raw.white_level() as f32;
501
502        // CFA color at each active-area position
503        let fc = |x: usize, y: usize| -> u8 {
504            let ax = x + x_off;
505            let ay = y + y_off;
506            match raw.cfa_pattern() {
507                CfaPattern::Rggb => match (ax % 2, ay % 2) {
508                    (0, 0) => 0,
509                    (1, 0) => 1,
510                    (0, 1) => 3,
511                    _ => 2,
512                },
513                CfaPattern::Grbg => match (ax % 2, ay % 2) {
514                    (0, 0) => 1,
515                    (1, 0) => 0,
516                    (0, 1) => 2,
517                    _ => 3,
518                },
519                CfaPattern::Gbrg => match (ax % 2, ay % 2) {
520                    (0, 0) => 3,
521                    (1, 0) => 2,
522                    (0, 1) => 0,
523                    _ => 1,
524                },
525                CfaPattern::Bggr => match (ax % 2, ay % 2) {
526                    (0, 0) => 2,
527                    (1, 0) => 3,
528                    (0, 1) => 1,
529                    _ => 0,
530                },
531            }
532        };
533
534        // Mirror-padded accessor into the raw data (active area coordinates)
535        let get = |x: isize, y: isize| -> f32 {
536            let cx = x.clamp(0, (width as isize) - 1) as usize;
537            let cy = y.clamp(0, (height as isize) - 1) as usize;
538            raw.data[(cy + y_off) * raw_w + (cx + x_off)] as f32
539        };
540
541        // ── Step 1: Compute horizontal and vertical green estimates ──
542
543        let mut gh = vec![0.0f32; width * height];
544        let mut gv = vec![0.0f32; width * height];
545
546        for y in 0..height {
547            for x in 0..width {
548                let color = fc(x, y);
549                let ix = x as isize;
550                let iy = y as isize;
551
552                if color == 1 || color == 3 {
553                    // Green pixel — copy to both directional planes
554                    let val = get(ix, iy);
555                    gh[y * width + x] = val;
556                    gv[y * width + x] = val;
557                } else {
558                    // Non-green pixel — LMMSE horizontal estimate
559                    // gh = 0.5*(G[x-1]+G[x+1]) + 0.25*(2*raw[x]-raw[x-2]-raw[x+2])
560                    let est_h = 0.5 * (get(ix - 1, iy) + get(ix + 1, iy))
561                        + 0.25 * (2.0 * get(ix, iy) - get(ix - 2, iy) - get(ix + 2, iy));
562                    gh[y * width + x] = est_h.clamp(0.0, white);
563
564                    // LMMSE vertical estimate
565                    let est_v = 0.5 * (get(ix, iy - 1) + get(ix, iy + 1))
566                        + 0.25 * (2.0 * get(ix, iy) - get(ix, iy - 2) - get(ix, iy + 2));
567                    gv[y * width + x] = est_v.clamp(0.0, white);
568                }
569            }
570        }
571
572        // ── Step 2: Variance-based adaptive blending of green estimates ──
573
574        let mut green = vec![0.0f32; width * height];
575        let eps = 1e-5f32;
576        let half_win = 2usize; // 5-pixel window radius
577
578        for y in 0..height {
579            for x in 0..width {
580                let color = fc(x, y);
581                if color == 1 || color == 3 {
582                    green[y * width + x] = gh[y * width + x]; // already the raw value
583                    continue;
584                }
585
586                // Compute local variance of (raw - green_est) along each direction
587                let mut sum_h = 0.0f32;
588                let mut sum_sq_h = 0.0f32;
589                let mut sum_v = 0.0f32;
590                let mut sum_sq_v = 0.0f32;
591                let mut n = 0.0f32;
592
593                for dy in -(half_win as isize)..=(half_win as isize) {
594                    for dx in -(half_win as isize)..=(half_win as isize) {
595                        let nx = (x as isize + dx).clamp(0, (width as isize) - 1) as usize;
596                        let ny = (y as isize + dy).clamp(0, (height as isize) - 1) as usize;
597                        let nidx = ny * width + nx;
598                        let raw_val = get(nx as isize, ny as isize);
599                        let dh = raw_val - gh[nidx];
600                        let dv = raw_val - gv[nidx];
601                        sum_h += dh;
602                        sum_sq_h += dh * dh;
603                        sum_v += dv;
604                        sum_sq_v += dv * dv;
605                        n += 1.0;
606                    }
607                }
608
609                let var_h = (sum_sq_h - sum_h * sum_h / n) / n;
610                let var_v = (sum_sq_v - sum_v * sum_v / n) / n;
611
612                // Weight inversely proportional to variance
613                let wh = 1.0 / (var_h + eps);
614                let wv = 1.0 / (var_v + eps);
615
616                let idx = y * width + x;
617                green[idx] = ((wh * gh[idx] + wv * gv[idx]) / (wh + wv)).clamp(0.0, white);
618            }
619        }
620
621        drop(gh);
622        drop(gv);
623
624        // ── Step 3: R/B interpolation via color-difference bilinear ──
625
626        // Compute color differences at known positions, then interpolate.
627        let mut cd_rg = vec![0.0f32; width * height]; // R - G at R pixels
628        let mut cd_bg = vec![0.0f32; width * height]; // B - G at B pixels
629
630        for y in 0..height {
631            for x in 0..width {
632                let idx = y * width + x;
633                match fc(x, y) {
634                    0 => cd_rg[idx] = get(x as isize, y as isize) - green[idx],
635                    2 => cd_bg[idx] = get(x as isize, y as isize) - green[idx],
636                    _ => {}
637                }
638            }
639        }
640
641        let mut red = vec![0.0f32; width * height];
642        let mut blue = vec![0.0f32; width * height];
643
644        for y in 0..height {
645            for x in 0..width {
646                let idx = y * width + x;
647                let color = fc(x, y);
648                let ix = x as isize;
649                let iy = y as isize;
650
651                match color {
652                    0 => {
653                        // Red pixel — R is known, need B from diagonal bilinear
654                        red[idx] = get(ix, iy);
655                        let mut sum = 0.0f32;
656                        let mut cnt = 0.0f32;
657                        for &(dx, dy) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] {
658                            let nx = ix + dx as isize;
659                            let ny = iy + dy as isize;
660                            if nx >= 0 && nx < width as isize && ny >= 0 && ny < height as isize {
661                                let nidx = ny as usize * width + nx as usize;
662                                if fc(nx as usize, ny as usize) == 2 {
663                                    sum += cd_bg[nidx];
664                                    cnt += 1.0;
665                                }
666                            }
667                        }
668                        blue[idx] = (green[idx] + if cnt > 0.0 { sum / cnt } else { 0.0 })
669                            .clamp(0.0, white);
670                    }
671                    2 => {
672                        // Blue pixel — B is known, need R from diagonal bilinear
673                        blue[idx] = get(ix, iy);
674                        let mut sum = 0.0f32;
675                        let mut cnt = 0.0f32;
676                        for &(dx, dy) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] {
677                            let nx = ix + dx as isize;
678                            let ny = iy + dy as isize;
679                            if nx >= 0 && nx < width as isize && ny >= 0 && ny < height as isize {
680                                let nidx = ny as usize * width + nx as usize;
681                                if fc(nx as usize, ny as usize) == 0 {
682                                    sum += cd_rg[nidx];
683                                    cnt += 1.0;
684                                }
685                            }
686                        }
687                        red[idx] = (green[idx] + if cnt > 0.0 { sum / cnt } else { 0.0 })
688                            .clamp(0.0, white);
689                    }
690                    1 => {
691                        // Green on R-row: R from horizontal neighbors, B from vertical
692                        let mut sr = 0.0f32;
693                        let mut cr = 0.0f32;
694                        for &dx in &[-1i32, 1] {
695                            let nx = ix + dx as isize;
696                            if nx >= 0 && nx < width as isize {
697                                let nidx = y * width + nx as usize;
698                                if fc(nx as usize, y) == 0 {
699                                    sr += cd_rg[nidx];
700                                    cr += 1.0;
701                                }
702                            }
703                        }
704                        red[idx] =
705                            (green[idx] + if cr > 0.0 { sr / cr } else { 0.0 }).clamp(0.0, white);
706
707                        let mut sb = 0.0f32;
708                        let mut cb = 0.0f32;
709                        for &dy in &[-1i32, 1] {
710                            let ny = iy + dy as isize;
711                            if ny >= 0 && ny < height as isize {
712                                let nidx = ny as usize * width + x;
713                                if fc(x, ny as usize) == 2 {
714                                    sb += cd_bg[nidx];
715                                    cb += 1.0;
716                                }
717                            }
718                        }
719                        blue[idx] =
720                            (green[idx] + if cb > 0.0 { sb / cb } else { 0.0 }).clamp(0.0, white);
721                    }
722                    3 => {
723                        // Green on B-row: B from horizontal neighbors, R from vertical
724                        let mut sb = 0.0f32;
725                        let mut cb = 0.0f32;
726                        for &dx in &[-1i32, 1] {
727                            let nx = ix + dx as isize;
728                            if nx >= 0 && nx < width as isize {
729                                let nidx = y * width + nx as usize;
730                                if fc(nx as usize, y) == 2 {
731                                    sb += cd_bg[nidx];
732                                    cb += 1.0;
733                                }
734                            }
735                        }
736                        blue[idx] =
737                            (green[idx] + if cb > 0.0 { sb / cb } else { 0.0 }).clamp(0.0, white);
738
739                        let mut sr = 0.0f32;
740                        let mut cr = 0.0f32;
741                        for &dy in &[-1i32, 1] {
742                            let ny = iy + dy as isize;
743                            if ny >= 0 && ny < height as isize {
744                                let nidx = ny as usize * width + x;
745                                if fc(x, ny as usize) == 0 {
746                                    sr += cd_rg[nidx];
747                                    cr += 1.0;
748                                }
749                            }
750                        }
751                        red[idx] =
752                            (green[idx] + if cr > 0.0 { sr / cr } else { 0.0 }).clamp(0.0, white);
753                    }
754                    _ => unreachable!(),
755                }
756            }
757        }
758
759        // ── Step 4: Write interleaved RGB output ──────────────────────
760
761        output
762            .par_chunks_mut(width * 3)
763            .enumerate()
764            .for_each(|(y, row)| {
765                for x in 0..width {
766                    let idx = y * width + x;
767                    let out = x * 3;
768                    row[out] = red[idx].round().clamp(0.0, white) as u16;
769                    row[out + 1] = green[idx].round().clamp(0.0, white) as u16;
770                    row[out + 2] = blue[idx].round().clamp(0.0, white) as u16;
771                }
772            });
773
774        Ok(())
775    }
776}
777
778// =============================================================================
779// RCD — Ratio Corrected Demosaicing
780// =============================================================================
781
782/// RCD (Ratio Corrected Demosaicing) algorithm.
783///
784/// Fast, high-quality alternative to AMaZE that's particularly good for
785/// organic shapes and natural textures.
786pub struct Rcd;
787
788impl Demosaic for Rcd {
789    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
790        let width = raw.active_area().size.width as usize;
791        let height = raw.active_area().size.height as usize;
792        let x_off = raw.active_area().origin.x as usize;
793        let y_off = raw.active_area().origin.y as usize;
794        let raw_w = raw.width() as usize;
795
796        let expected_size = width * height * 3;
797        if output.len() != expected_size {
798            return Err(DemosaicError::BufferSizeMismatch {
799                expected: expected_size,
800                actual: output.len(),
801            });
802        }
803
804        if width < 6 || height < 6 {
805            return Err(DemosaicError::InvalidDimensions);
806        }
807
808        let white = raw.white_level() as f32;
809
810        // CFA color at each active-area position
811        let fc = |x: usize, y: usize| -> u8 {
812            let ax = x + x_off;
813            let ay = y + y_off;
814            match raw.cfa_pattern() {
815                CfaPattern::Rggb => match (ax % 2, ay % 2) {
816                    (0, 0) => 0,
817                    (1, 0) => 1,
818                    (0, 1) => 3,
819                    _ => 2,
820                },
821                CfaPattern::Grbg => match (ax % 2, ay % 2) {
822                    (0, 0) => 1,
823                    (1, 0) => 0,
824                    (0, 1) => 2,
825                    _ => 3,
826                },
827                CfaPattern::Gbrg => match (ax % 2, ay % 2) {
828                    (0, 0) => 3,
829                    (1, 0) => 2,
830                    (0, 1) => 0,
831                    _ => 1,
832                },
833                CfaPattern::Bggr => match (ax % 2, ay % 2) {
834                    (0, 0) => 2,
835                    (1, 0) => 3,
836                    (0, 1) => 1,
837                    _ => 0,
838                },
839            }
840        };
841
842        // Mirror-padded accessor
843        let get = |x: isize, y: isize| -> f32 {
844            let cx = x.clamp(0, (width as isize) - 1) as usize;
845            let cy = y.clamp(0, (height as isize) - 1) as usize;
846            raw.data[(cy + y_off) * raw_w + (cx + x_off)] as f32
847        };
848
849        // ── Step 1: Green channel interpolation (adaptive directional) ──
850
851        let mut green = vec![0.0f32; width * height];
852
853        for y in 0..height {
854            for x in 0..width {
855                let color = fc(x, y);
856                let ix = x as isize;
857                let iy = y as isize;
858
859                if color == 1 || color == 3 {
860                    green[y * width + x] = get(ix, iy);
861                } else {
862                    // RCD uses same adaptive directional green interpolation as AMaZE
863                    let dh = (get(ix - 1, iy) - get(ix + 1, iy)).abs()
864                        + (2.0 * get(ix, iy) - get(ix - 2, iy) - get(ix + 2, iy)).abs();
865                    let dv = (get(ix, iy - 1) - get(ix, iy + 1)).abs()
866                        + (2.0 * get(ix, iy) - get(ix, iy - 2) - get(ix, iy + 2)).abs();
867
868                    let gh = (get(ix - 1, iy) + get(ix + 1, iy)) * 0.5
869                        + (2.0 * get(ix, iy) - get(ix - 2, iy) - get(ix + 2, iy)) * 0.25;
870                    let gv = (get(ix, iy - 1) + get(ix, iy + 1)) * 0.5
871                        + (2.0 * get(ix, iy) - get(ix, iy - 2) - get(ix, iy + 2)) * 0.25;
872
873                    let eps = 1e-5f32;
874                    let g = if dh < dv * 0.5 {
875                        gh
876                    } else if dv < dh * 0.5 {
877                        gv
878                    } else {
879                        let wh = 1.0 / (dh + eps);
880                        let wv = 1.0 / (dv + eps);
881                        (wh * gh + wv * gv) / (wh + wv)
882                    };
883                    green[y * width + x] = g.max(0.0);
884                }
885            }
886        }
887
888        // ── Step 2: R/B reconstruction using color ratios ─────────────
889
890        // The RCD insight: R/G and B/G ratios are smoother than R-G differences.
891        // We interpolate ratios and then multiply back by the interpolated green.
892
893        // First build ratio planes at known positions.
894        // ratio_rg[idx] = R[idx] / G[idx] at R pixels (else 1.0 placeholder)
895        // ratio_bg[idx] = B[idx] / G[idx] at B pixels (else 1.0 placeholder)
896        let mut ratio_rg = vec![1.0f32; width * height];
897        let mut ratio_bg = vec![1.0f32; width * height];
898
899        for y in 0..height {
900            for x in 0..width {
901                let idx = y * width + x;
902                let g = green[idx].max(1.0); // avoid division by zero
903                match fc(x, y) {
904                    0 => ratio_rg[idx] = get(x as isize, y as isize) / g,
905                    2 => ratio_bg[idx] = get(x as isize, y as isize) / g,
906                    _ => {}
907                }
908            }
909        }
910
911        let mut red = vec![0.0f32; width * height];
912        let mut blue = vec![0.0f32; width * height];
913
914        for y in 0..height {
915            for x in 0..width {
916                let idx = y * width + x;
917                let color = fc(x, y);
918                let ix = x as isize;
919                let iy = y as isize;
920
921                match color {
922                    0 => {
923                        // R pixel — R is known; B interpolated from diagonal ratio bilinear
924                        red[idx] = get(ix, iy);
925                        let mut sum_ratio = 0.0f32;
926                        let mut cnt = 0.0f32;
927                        for &(dx, dy) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] {
928                            let nx = ix + dx as isize;
929                            let ny = iy + dy as isize;
930                            if nx >= 0
931                                && nx < width as isize
932                                && ny >= 0
933                                && ny < height as isize
934                                && fc(nx as usize, ny as usize) == 2
935                            {
936                                sum_ratio += ratio_bg[ny as usize * width + nx as usize];
937                                cnt += 1.0;
938                            }
939                        }
940                        let r = if cnt > 0.0 { sum_ratio / cnt } else { 1.0 };
941                        blue[idx] = (green[idx] * r).clamp(0.0, white);
942                    }
943                    2 => {
944                        // B pixel — B is known; R interpolated from diagonal ratio bilinear
945                        blue[idx] = get(ix, iy);
946                        let mut sum_ratio = 0.0f32;
947                        let mut cnt = 0.0f32;
948                        for &(dx, dy) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] {
949                            let nx = ix + dx as isize;
950                            let ny = iy + dy as isize;
951                            if nx >= 0
952                                && nx < width as isize
953                                && ny >= 0
954                                && ny < height as isize
955                                && fc(nx as usize, ny as usize) == 0
956                            {
957                                sum_ratio += ratio_rg[ny as usize * width + nx as usize];
958                                cnt += 1.0;
959                            }
960                        }
961                        let r = if cnt > 0.0 { sum_ratio / cnt } else { 1.0 };
962                        red[idx] = (green[idx] * r).clamp(0.0, white);
963                    }
964                    1 => {
965                        // Green on R-row:
966                        // R from horizontal R/G ratio pairs
967                        // B from vertical B/G ratio pairs
968                        let mut sr = 0.0f32;
969                        let mut cr = 0.0f32;
970                        for &dx in &[-1i32, 1] {
971                            let nx = ix + dx as isize;
972                            if nx >= 0 && nx < width as isize && fc(nx as usize, y) == 0 {
973                                sr += ratio_rg[y * width + nx as usize];
974                                cr += 1.0;
975                            }
976                        }
977                        let rr = if cr > 0.0 { sr / cr } else { 1.0 };
978                        red[idx] = (green[idx] * rr).clamp(0.0, white);
979
980                        let mut sb = 0.0f32;
981                        let mut cb = 0.0f32;
982                        for &dy in &[-1i32, 1] {
983                            let ny = iy + dy as isize;
984                            if ny >= 0 && ny < height as isize && fc(x, ny as usize) == 2 {
985                                sb += ratio_bg[ny as usize * width + x];
986                                cb += 1.0;
987                            }
988                        }
989                        let rb = if cb > 0.0 { sb / cb } else { 1.0 };
990                        blue[idx] = (green[idx] * rb).clamp(0.0, white);
991                    }
992                    3 => {
993                        // Green on B-row:
994                        // B from horizontal B/G ratio pairs
995                        // R from vertical R/G ratio pairs
996                        let mut sb = 0.0f32;
997                        let mut cb = 0.0f32;
998                        for &dx in &[-1i32, 1] {
999                            let nx = ix + dx as isize;
1000                            if nx >= 0 && nx < width as isize && fc(nx as usize, y) == 2 {
1001                                sb += ratio_bg[y * width + nx as usize];
1002                                cb += 1.0;
1003                            }
1004                        }
1005                        let rb = if cb > 0.0 { sb / cb } else { 1.0 };
1006                        blue[idx] = (green[idx] * rb).clamp(0.0, white);
1007
1008                        let mut sr = 0.0f32;
1009                        let mut cr = 0.0f32;
1010                        for &dy in &[-1i32, 1] {
1011                            let ny = iy + dy as isize;
1012                            if ny >= 0 && ny < height as isize && fc(x, ny as usize) == 0 {
1013                                sr += ratio_rg[ny as usize * width + x];
1014                                cr += 1.0;
1015                            }
1016                        }
1017                        let rr = if cr > 0.0 { sr / cr } else { 1.0 };
1018                        red[idx] = (green[idx] * rr).clamp(0.0, white);
1019                    }
1020                    _ => unreachable!(),
1021                }
1022            }
1023        }
1024
1025        // ── Step 3: Write interleaved RGB output ──────────────────────
1026
1027        output
1028            .par_chunks_mut(width * 3)
1029            .enumerate()
1030            .for_each(|(y, row)| {
1031                for x in 0..width {
1032                    let idx = y * width + x;
1033                    let out = x * 3;
1034                    row[out] = red[idx].round().clamp(0.0, white) as u16;
1035                    row[out + 1] = green[idx].round().clamp(0.0, white) as u16;
1036                    row[out + 2] = blue[idx].round().clamp(0.0, white) as u16;
1037                }
1038            });
1039
1040        Ok(())
1041    }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046    use super::*;
1047    use crate::core::image::{Point, Rect, Size};
1048
1049    fn create_test_raw(width: u32, height: u32, pattern: CfaPattern, value: u16) -> RawImage {
1050        let size = Size::new(width, height);
1051        let active_area = Rect::new(Point::ORIGIN, size);
1052        RawImage::builder(size, active_area, 14, pattern)
1053            .white_level(16383)
1054            .data(vec![value; (width * height) as usize])
1055            .build()
1056    }
1057
1058    fn create_gradient_raw(width: u32, height: u32, pattern: CfaPattern) -> RawImage {
1059        let size = Size::new(width, height);
1060        let active_area = Rect::new(Point::ORIGIN, size);
1061        let mut data = vec![0u16; (width * height) as usize];
1062        for y in 0..height {
1063            for x in 0..width {
1064                // Smooth gradient — easy for demosaic
1065                let val =
1066                    ((x as f32 / width as f32 + y as f32 / height as f32) * 0.5 * 8000.0) as u16;
1067                data[(y * width + x) as usize] = val;
1068            }
1069        }
1070        RawImage::builder(size, active_area, 14, pattern)
1071            .white_level(16383)
1072            .data(data)
1073            .build()
1074    }
1075
1076    #[test]
1077    fn test_amaze_correct_output_size() {
1078        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1079        let mut output = vec![0u16; 20 * 20 * 3];
1080        assert!(Amaze.demosaic_into(&raw, &mut output).is_ok());
1081    }
1082
1083    #[test]
1084    fn test_amaze_wrong_buffer_size() {
1085        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1086        let mut output = vec![0u16; 50];
1087        assert!(matches!(
1088            Amaze.demosaic_into(&raw, &mut output),
1089            Err(DemosaicError::BufferSizeMismatch { .. })
1090        ));
1091    }
1092
1093    #[test]
1094    fn test_amaze_too_small_image() {
1095        let raw = create_test_raw(4, 4, CfaPattern::Rggb, 5000);
1096        let mut output = vec![0u16; 4 * 4 * 3];
1097        assert!(matches!(
1098            Amaze.demosaic_into(&raw, &mut output),
1099            Err(DemosaicError::InvalidDimensions)
1100        ));
1101    }
1102
1103    #[test]
1104    fn test_amaze_uniform_produces_uniform() {
1105        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1106        let rgb = Amaze.demosaic(&raw);
1107
1108        // Interior pixels should be close to 5000
1109        for y in 4..16 {
1110            for x in 4..16 {
1111                let idx = (y * 20 + x) * 3;
1112                for c in 0..3 {
1113                    let val = rgb.data[idx + c];
1114                    assert!(
1115                        (val as i32 - 5000).abs() < 500,
1116                        "pixel ({},{}) ch {} = {}, expected ~5000",
1117                        x,
1118                        y,
1119                        c,
1120                        val
1121                    );
1122                }
1123            }
1124        }
1125    }
1126
1127    #[test]
1128    fn test_amaze_all_cfa_patterns() {
1129        for pattern in [
1130            CfaPattern::Rggb,
1131            CfaPattern::Grbg,
1132            CfaPattern::Gbrg,
1133            CfaPattern::Bggr,
1134        ] {
1135            let raw = create_test_raw(20, 20, pattern, 3000);
1136            let rgb = Amaze.demosaic(&raw);
1137            assert_eq!(rgb.width(), 20);
1138            assert_eq!(rgb.height(), 20);
1139            assert_eq!(rgb.data.len(), 20 * 20 * 3);
1140
1141            // All values should be non-negative and bounded
1142            for val in &rgb.data {
1143                assert!(
1144                    *val <= 16383,
1145                    "pattern {:?}: value {} too high",
1146                    pattern,
1147                    val
1148                );
1149            }
1150        }
1151    }
1152
1153    #[test]
1154    fn test_amaze_gradient_smooth() {
1155        let raw = create_gradient_raw(40, 40, CfaPattern::Rggb);
1156        let rgb = Amaze.demosaic(&raw);
1157
1158        // Check that output is reasonably smooth (no huge jumps between neighbors)
1159        for y in 5..35 {
1160            for x in 5..35 {
1161                let idx = (y * 40 + x) * 3;
1162                let idx_right = (y * 40 + x + 1) * 3;
1163                let idx_down = ((y + 1) * 40 + x) * 3;
1164
1165                for c in 0..3 {
1166                    let diff_h = (rgb.data[idx + c] as i32 - rgb.data[idx_right + c] as i32).abs();
1167                    let diff_v = (rgb.data[idx + c] as i32 - rgb.data[idx_down + c] as i32).abs();
1168                    assert!(
1169                        diff_h < 1000,
1170                        "horizontal jump at ({},{}) ch {}: {}",
1171                        x,
1172                        y,
1173                        c,
1174                        diff_h
1175                    );
1176                    assert!(
1177                        diff_v < 1000,
1178                        "vertical jump at ({},{}) ch {}: {}",
1179                        x,
1180                        y,
1181                        c,
1182                        diff_v
1183                    );
1184                }
1185            }
1186        }
1187    }
1188
1189    #[test]
1190    fn test_amaze_preserves_known_green() {
1191        // Create image where all values are distinct
1192        let mut raw = create_test_raw(10, 10, CfaPattern::Rggb, 0);
1193        // Set known green pixel at (1,0) which is G in RGGB
1194        raw.data[1] = 7000;
1195        let rgb = Amaze.demosaic(&raw);
1196        // Green channel at (1,0) should be exactly 7000
1197        // pixel (1, 0): row=0, col=1, so index = 1 * 3 + 1 = 4
1198        let g = rgb.data[3 + 1];
1199        assert_eq!(
1200            g, 7000,
1201            "green pixel should be preserved exactly, got {}",
1202            g
1203        );
1204    }
1205
1206    #[test]
1207    fn test_amaze_with_active_area() {
1208        let size = Size::new(30, 30);
1209        let active_area = Rect::from_coords(5, 5, 20, 20);
1210        let raw = RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
1211            .white_level(16383)
1212            .data(vec![4000u16; 30 * 30])
1213            .build();
1214        let rgb = Amaze.demosaic(&raw);
1215        assert_eq!(rgb.width(), 20);
1216        assert_eq!(rgb.height(), 20);
1217        assert_eq!(rgb.data.len(), 20 * 20 * 3);
1218    }
1219
1220    #[test]
1221    fn test_amaze_respects_white_level_clamp() {
1222        // Feed values at white_level — output must never exceed white_level
1223        let white_level: u16 = 16383;
1224        let raw = create_test_raw(20, 20, CfaPattern::Rggb, white_level);
1225        let mut output = vec![0u16; 20 * 20 * 3];
1226        Amaze.demosaic_into(&raw, &mut output).unwrap();
1227        for (i, &v) in output.iter().enumerate() {
1228            assert!(
1229                v <= white_level,
1230                "pixel {} has value {} exceeding white_level {}",
1231                i,
1232                v,
1233                white_level
1234            );
1235        }
1236    }
1237
1238    // ── LMMSE tests ───────────────────────────────────────────────────────────
1239
1240    #[test]
1241    fn test_lmmse_correct_output_size() {
1242        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1243        let mut output = vec![0u16; 20 * 20 * 3];
1244        assert!(Lmmse.demosaic_into(&raw, &mut output).is_ok());
1245    }
1246
1247    #[test]
1248    fn test_lmmse_wrong_buffer_size() {
1249        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1250        let mut output = vec![0u16; 50];
1251        assert!(matches!(
1252            Lmmse.demosaic_into(&raw, &mut output),
1253            Err(DemosaicError::BufferSizeMismatch { .. })
1254        ));
1255    }
1256
1257    #[test]
1258    fn test_lmmse_too_small_image() {
1259        let raw = create_test_raw(4, 4, CfaPattern::Rggb, 5000);
1260        let mut output = vec![0u16; 4 * 4 * 3];
1261        assert!(matches!(
1262            Lmmse.demosaic_into(&raw, &mut output),
1263            Err(DemosaicError::InvalidDimensions)
1264        ));
1265    }
1266
1267    #[test]
1268    fn test_lmmse_uniform_produces_uniform() {
1269        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1270        let rgb = Lmmse.demosaic(&raw);
1271
1272        for y in 4..16 {
1273            for x in 4..16 {
1274                let idx = (y * 20 + x) * 3;
1275                for c in 0..3 {
1276                    let val = rgb.data[idx + c];
1277                    assert!(
1278                        (val as i32 - 5000).abs() < 500,
1279                        "LMMSE pixel ({},{}) ch {} = {}, expected ~5000",
1280                        x,
1281                        y,
1282                        c,
1283                        val
1284                    );
1285                }
1286            }
1287        }
1288    }
1289
1290    #[test]
1291    fn test_lmmse_all_cfa_patterns() {
1292        for pattern in [
1293            CfaPattern::Rggb,
1294            CfaPattern::Grbg,
1295            CfaPattern::Gbrg,
1296            CfaPattern::Bggr,
1297        ] {
1298            let raw = create_test_raw(20, 20, pattern, 3000);
1299            let rgb = Lmmse.demosaic(&raw);
1300            assert_eq!(rgb.width(), 20);
1301            assert_eq!(rgb.height(), 20);
1302            assert_eq!(rgb.data.len(), 20 * 20 * 3);
1303
1304            for val in &rgb.data {
1305                assert!(
1306                    *val <= 16383,
1307                    "LMMSE pattern {:?}: value {} too high",
1308                    pattern,
1309                    val
1310                );
1311            }
1312        }
1313    }
1314
1315    #[test]
1316    fn test_lmmse_gradient_smooth() {
1317        let raw = create_gradient_raw(40, 40, CfaPattern::Rggb);
1318        let rgb = Lmmse.demosaic(&raw);
1319
1320        for y in 5..35 {
1321            for x in 5..35 {
1322                let idx = (y * 40 + x) * 3;
1323                let idx_right = (y * 40 + x + 1) * 3;
1324                let idx_down = ((y + 1) * 40 + x) * 3;
1325
1326                for c in 0..3 {
1327                    let diff_h = (rgb.data[idx + c] as i32 - rgb.data[idx_right + c] as i32).abs();
1328                    let diff_v = (rgb.data[idx + c] as i32 - rgb.data[idx_down + c] as i32).abs();
1329                    assert!(
1330                        diff_h < 1000,
1331                        "LMMSE horizontal jump at ({},{}) ch {}: {}",
1332                        x,
1333                        y,
1334                        c,
1335                        diff_h
1336                    );
1337                    assert!(
1338                        diff_v < 1000,
1339                        "LMMSE vertical jump at ({},{}) ch {}: {}",
1340                        x,
1341                        y,
1342                        c,
1343                        diff_v
1344                    );
1345                }
1346            }
1347        }
1348    }
1349
1350    #[test]
1351    fn test_lmmse_with_active_area() {
1352        let size = Size::new(30, 30);
1353        let active_area = Rect::from_coords(5, 5, 20, 20);
1354        let raw = RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
1355            .white_level(16383)
1356            .data(vec![4000u16; 30 * 30])
1357            .build();
1358        let rgb = Lmmse.demosaic(&raw);
1359        assert_eq!(rgb.width(), 20);
1360        assert_eq!(rgb.height(), 20);
1361        assert_eq!(rgb.data.len(), 20 * 20 * 3);
1362    }
1363
1364    // ── RCD tests ─────────────────────────────────────────────────────────────
1365
1366    #[test]
1367    fn test_rcd_correct_output_size() {
1368        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1369        let mut output = vec![0u16; 20 * 20 * 3];
1370        assert!(Rcd.demosaic_into(&raw, &mut output).is_ok());
1371    }
1372
1373    #[test]
1374    fn test_rcd_wrong_buffer_size() {
1375        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1376        let mut output = vec![0u16; 50];
1377        assert!(matches!(
1378            Rcd.demosaic_into(&raw, &mut output),
1379            Err(DemosaicError::BufferSizeMismatch { .. })
1380        ));
1381    }
1382
1383    #[test]
1384    fn test_rcd_too_small_image() {
1385        let raw = create_test_raw(4, 4, CfaPattern::Rggb, 5000);
1386        let mut output = vec![0u16; 4 * 4 * 3];
1387        assert!(matches!(
1388            Rcd.demosaic_into(&raw, &mut output),
1389            Err(DemosaicError::InvalidDimensions)
1390        ));
1391    }
1392
1393    #[test]
1394    fn test_rcd_uniform_produces_uniform() {
1395        let raw = create_test_raw(20, 20, CfaPattern::Rggb, 5000);
1396        let rgb = Rcd.demosaic(&raw);
1397
1398        for y in 4..16 {
1399            for x in 4..16 {
1400                let idx = (y * 20 + x) * 3;
1401                for c in 0..3 {
1402                    let val = rgb.data[idx + c];
1403                    assert!(
1404                        (val as i32 - 5000).abs() < 500,
1405                        "RCD pixel ({},{}) ch {} = {}, expected ~5000",
1406                        x,
1407                        y,
1408                        c,
1409                        val
1410                    );
1411                }
1412            }
1413        }
1414    }
1415
1416    #[test]
1417    fn test_rcd_all_cfa_patterns() {
1418        for pattern in [
1419            CfaPattern::Rggb,
1420            CfaPattern::Grbg,
1421            CfaPattern::Gbrg,
1422            CfaPattern::Bggr,
1423        ] {
1424            let raw = create_test_raw(20, 20, pattern, 3000);
1425            let rgb = Rcd.demosaic(&raw);
1426            assert_eq!(rgb.width(), 20);
1427            assert_eq!(rgb.height(), 20);
1428            assert_eq!(rgb.data.len(), 20 * 20 * 3);
1429
1430            for val in &rgb.data {
1431                assert!(
1432                    *val <= 16383,
1433                    "RCD pattern {:?}: value {} too high",
1434                    pattern,
1435                    val
1436                );
1437            }
1438        }
1439    }
1440
1441    #[test]
1442    fn test_rcd_gradient_smooth() {
1443        let raw = create_gradient_raw(40, 40, CfaPattern::Rggb);
1444        let rgb = Rcd.demosaic(&raw);
1445
1446        for y in 5..35 {
1447            for x in 5..35 {
1448                let idx = (y * 40 + x) * 3;
1449                let idx_right = (y * 40 + x + 1) * 3;
1450                let idx_down = ((y + 1) * 40 + x) * 3;
1451
1452                for c in 0..3 {
1453                    let diff_h = (rgb.data[idx + c] as i32 - rgb.data[idx_right + c] as i32).abs();
1454                    let diff_v = (rgb.data[idx + c] as i32 - rgb.data[idx_down + c] as i32).abs();
1455                    assert!(
1456                        diff_h < 1000,
1457                        "RCD horizontal jump at ({},{}) ch {}: {}",
1458                        x,
1459                        y,
1460                        c,
1461                        diff_h
1462                    );
1463                    assert!(
1464                        diff_v < 1000,
1465                        "RCD vertical jump at ({},{}) ch {}: {}",
1466                        x,
1467                        y,
1468                        c,
1469                        diff_v
1470                    );
1471                }
1472            }
1473        }
1474    }
1475
1476    #[test]
1477    fn test_rcd_with_active_area() {
1478        let size = Size::new(30, 30);
1479        let active_area = Rect::from_coords(5, 5, 20, 20);
1480        let raw = RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
1481            .white_level(16383)
1482            .data(vec![4000u16; 30 * 30])
1483            .build();
1484        let rgb = Rcd.demosaic(&raw);
1485        assert_eq!(rgb.width(), 20);
1486        assert_eq!(rgb.height(), 20);
1487        assert_eq!(rgb.data.len(), 20 * 20 * 3);
1488    }
1489}