Skip to main content

rawshift_image/processing/demosaic/
xtrans.rs

1//! X-Trans-specific demosaicing algorithms.
2//!
3//! This module contains demosaicing algorithms designed for Fujifilm's 6x6
4//! X-Trans color filter array pattern.
5
6use super::{Demosaic, DemosaicError};
7use crate::core::image::{RawImage, XTransPattern};
8
9/// Markesteijn demosaicing algorithm for X-Trans sensors.
10///
11/// This is a single-pass weighted-interpolation implementation.  Green pixels
12/// are copied directly; at every non-green site green is estimated with an
13/// inverse-distance² weighted average of all greens in a 5×5 window.  Red
14/// and Blue planes are then built in the "color-difference" domain: the
15/// difference (channel − green) is interpolated from the known samples in a
16/// 7×7 window, and the final value is recovered as G + interpolated_diff.
17///
18/// While not as sophisticated as the multi-pass LibRaw variant, this produces
19/// reasonable colour for Fujifilm X-Trans sensors and passes all correctness
20/// tests.
21pub struct Markesteijn;
22
23impl Demosaic for Markesteijn {
24    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
25        let width = raw.active_area().size.width as usize;
26        let height = raw.active_area().size.height as usize;
27        let x_off = raw.active_area().origin.x as usize;
28        let y_off = raw.active_area().origin.y as usize;
29        let raw_w = raw.width() as usize;
30
31        let expected = width * height * 3;
32        if output.len() != expected {
33            return Err(DemosaicError::BufferSizeMismatch {
34                expected,
35                actual: output.len(),
36            });
37        }
38
39        if width < 6 || height < 6 {
40            return Err(DemosaicError::InvalidDimensions);
41        }
42
43        // Use caller-supplied pattern or fall back to the standard Fujifilm one.
44        let pattern = raw
45            .xtrans_pattern()
46            .copied()
47            .unwrap_or_else(XTransPattern::standard);
48
49        // Color at active-area-relative position (x, y).
50        let fc = |x: usize, y: usize| -> u8 { pattern.color_at(x + x_off, y + y_off) };
51
52        // Border-clamped raw pixel accessor (active-area coordinates).
53        let get = |x: isize, y: isize| -> f32 {
54            let cx = x.clamp(0, (width as isize) - 1) as usize;
55            let cy = y.clamp(0, (height as isize) - 1) as usize;
56            raw.data[(cy + y_off) * raw_w + (cx + x_off)] as f32
57        };
58
59        // ── Step 1: Build full-resolution green plane ─────────────────────────
60        let mut green = vec![0.0f32; width * height];
61        for y in 0..height {
62            for x in 0..width {
63                if fc(x, y) == 1 {
64                    // Known green – copy directly.
65                    green[y * width + x] = get(x as isize, y as isize);
66                } else {
67                    // Weighted interpolation from surrounding green pixels (5×5 window).
68                    let mut sum = 0.0f32;
69                    let mut weight = 0.0f32;
70                    for dy in -2i32..=2 {
71                        for dx in -2i32..=2 {
72                            let nx = x as i32 + dx;
73                            let ny = y as i32 + dy;
74                            if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
75                                continue;
76                            }
77                            if fc(nx as usize, ny as usize) == 1 {
78                                // Inverse-distance² weight (offset by 0.1 to avoid division by zero at distance 0).
79                                let dist2 = (dx * dx + dy * dy) as f32;
80                                let w = 1.0 / (dist2 + 0.1);
81                                sum += get(nx as isize, ny as isize) * w;
82                                weight += w;
83                            }
84                        }
85                    }
86                    green[y * width + x] = if weight > 0.0 {
87                        sum / weight
88                    } else {
89                        get(x as isize, y as isize)
90                    };
91                }
92            }
93        }
94
95        // ── Step 2: Build colour-difference planes (channel − green) ──────────
96        // At known R/B sites we record the actual difference; elsewhere 0.
97        let mut r_diff = vec![0.0f32; width * height];
98        let mut b_diff = vec![0.0f32; width * height];
99
100        for y in 0..height {
101            for x in 0..width {
102                let g = green[y * width + x];
103                match fc(x, y) {
104                    0 => r_diff[y * width + x] = get(x as isize, y as isize) - g,
105                    2 => b_diff[y * width + x] = get(x as isize, y as isize) - g,
106                    _ => {}
107                }
108            }
109        }
110
111        // ── Step 3: Interpolate colour differences at every pixel (7×7 window) ─
112        let mut r_diff_interp = vec![0.0f32; width * height];
113        let mut b_diff_interp = vec![0.0f32; width * height];
114
115        for y in 0..height {
116            for x in 0..width {
117                // Red difference
118                if fc(x, y) == 0 {
119                    r_diff_interp[y * width + x] = r_diff[y * width + x]; // already known
120                } else {
121                    let mut sum = 0.0f32;
122                    let mut w_total = 0.0f32;
123                    for dy in -3i32..=3 {
124                        for dx in -3i32..=3 {
125                            let nx = x as i32 + dx;
126                            let ny = y as i32 + dy;
127                            if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
128                                continue;
129                            }
130                            if fc(nx as usize, ny as usize) == 0 {
131                                let dist2 = (dx * dx + dy * dy) as f32;
132                                let w = 1.0 / (dist2 + 0.1);
133                                sum += r_diff[(ny as usize) * width + (nx as usize)] * w;
134                                w_total += w;
135                            }
136                        }
137                    }
138                    r_diff_interp[y * width + x] = if w_total > 0.0 { sum / w_total } else { 0.0 };
139                }
140
141                // Blue difference
142                if fc(x, y) == 2 {
143                    b_diff_interp[y * width + x] = b_diff[y * width + x]; // already known
144                } else {
145                    let mut sum = 0.0f32;
146                    let mut w_total = 0.0f32;
147                    for dy in -3i32..=3 {
148                        for dx in -3i32..=3 {
149                            let nx = x as i32 + dx;
150                            let ny = y as i32 + dy;
151                            if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
152                                continue;
153                            }
154                            if fc(nx as usize, ny as usize) == 2 {
155                                let dist2 = (dx * dx + dy * dy) as f32;
156                                let w = 1.0 / (dist2 + 0.1);
157                                sum += b_diff[(ny as usize) * width + (nx as usize)] * w;
158                                w_total += w;
159                            }
160                        }
161                    }
162                    b_diff_interp[y * width + x] = if w_total > 0.0 { sum / w_total } else { 0.0 };
163                }
164            }
165        }
166
167        // ── Step 4: Compose and write output ─────────────────────────────────
168        let wl = raw.white_level() as f32;
169        for y in 0..height {
170            for x in 0..width {
171                let idx = (y * width + x) * 3;
172                let g = green[y * width + x];
173                let r = (g + r_diff_interp[y * width + x]).clamp(0.0, wl);
174                let b = (g + b_diff_interp[y * width + x]).clamp(0.0, wl);
175                let g = g.clamp(0.0, wl);
176                output[idx] = r as u16;
177                output[idx + 1] = g as u16;
178                output[idx + 2] = b as u16;
179            }
180        }
181
182        Ok(())
183    }
184}
185
186/// Markesteijn 3-pass demosaicing algorithm for X-Trans sensors.
187///
188/// Extends the single-pass Markesteijn with two refinement passes:
189/// - Pass 1: Standard green + colour-difference interpolation.
190/// - Pass 2: Refine green at R/B sites using the colour-difference
191///   estimates from pass 1 as a cross-channel consistency check.
192/// - Pass 3: Re-interpolate colour differences with the refined green.
193///
194/// The extra passes smooth colour-fringing at high-contrast edges.
195pub struct Markesteijn3Pass;
196
197impl Demosaic for Markesteijn3Pass {
198    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
199        let width = raw.active_area().size.width as usize;
200        let height = raw.active_area().size.height as usize;
201        let x_off = raw.active_area().origin.x as usize;
202        let y_off = raw.active_area().origin.y as usize;
203        let raw_w = raw.width() as usize;
204
205        let expected = width * height * 3;
206        if output.len() != expected {
207            return Err(DemosaicError::BufferSizeMismatch {
208                expected,
209                actual: output.len(),
210            });
211        }
212        if width < 6 || height < 6 {
213            return Err(DemosaicError::InvalidDimensions);
214        }
215
216        let pattern = raw
217            .xtrans_pattern()
218            .copied()
219            .unwrap_or_else(XTransPattern::standard);
220        let fc = |x: usize, y: usize| -> u8 { pattern.color_at(x + x_off, y + y_off) };
221        let get = |x: isize, y: isize| -> f32 {
222            let cx = x.clamp(0, (width as isize) - 1) as usize;
223            let cy = y.clamp(0, (height as isize) - 1) as usize;
224            raw.data[(cy + y_off) * raw_w + (cx + x_off)] as f32
225        };
226
227        // Inverse-distance² weighted interpolation of `plane` at (x,y) using
228        // only pixels whose sensor colour equals `color`.
229        let interp_diff = |plane: &[f32], x: usize, y: usize, color: u8, radius: i32| -> f32 {
230            if fc(x, y) == color {
231                return plane[y * width + x];
232            }
233            let mut sum = 0.0f32;
234            let mut w_total = 0.0f32;
235            for dy in -radius..=radius {
236                for dx in -radius..=radius {
237                    let nx = x as i32 + dx;
238                    let ny = y as i32 + dy;
239                    if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
240                        continue;
241                    }
242                    if fc(nx as usize, ny as usize) == color {
243                        let dist2 = (dx * dx + dy * dy) as f32;
244                        let w = 1.0 / (dist2 + 0.1);
245                        sum += plane[(ny as usize) * width + (nx as usize)] * w;
246                        w_total += w;
247                    }
248                }
249            }
250            if w_total > 0.0 { sum / w_total } else { 0.0 }
251        };
252
253        // ── Pass 1: green interpolation (5×5 IDW from known green sites) ─────
254        let mut green = vec![0.0f32; width * height];
255        for y in 0..height {
256            for x in 0..width {
257                if fc(x, y) == 1 {
258                    green[y * width + x] = get(x as isize, y as isize);
259                } else {
260                    let mut sum = 0.0f32;
261                    let mut w_total = 0.0f32;
262                    for dy in -2i32..=2 {
263                        for dx in -2i32..=2 {
264                            let nx = x as i32 + dx;
265                            let ny = y as i32 + dy;
266                            if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
267                                continue;
268                            }
269                            if fc(nx as usize, ny as usize) == 1 {
270                                let dist2 = (dx * dx + dy * dy) as f32;
271                                let w = 1.0 / (dist2 + 0.1);
272                                sum += get(nx as isize, ny as isize) * w;
273                                w_total += w;
274                            }
275                        }
276                    }
277                    green[y * width + x] = if w_total > 0.0 {
278                        sum / w_total
279                    } else {
280                        get(x as isize, y as isize)
281                    };
282                }
283            }
284        }
285
286        // Compute colour-difference planes from pass-1 green.
287        let build_diffs = |green: &[f32]| -> (Vec<f32>, Vec<f32>) {
288            let mut r_diff = vec![0.0f32; width * height];
289            let mut b_diff = vec![0.0f32; width * height];
290            for y in 0..height {
291                for x in 0..width {
292                    let g = green[y * width + x];
293                    match fc(x, y) {
294                        0 => r_diff[y * width + x] = get(x as isize, y as isize) - g,
295                        2 => b_diff[y * width + x] = get(x as isize, y as isize) - g,
296                        _ => {}
297                    }
298                }
299            }
300            (r_diff, b_diff)
301        };
302
303        let (mut r_diff, mut b_diff) = build_diffs(&green);
304
305        let interp_plane = |diff: &[f32], color: u8| -> Vec<f32> {
306            (0..width * height)
307                .map(|i| interp_diff(diff, i % width, i / width, color, 3))
308                .collect()
309        };
310
311        let mut r_diff_i = interp_plane(&r_diff, 0);
312        let mut b_diff_i = interp_plane(&b_diff, 2);
313
314        // ── Pass 2: refine green at R/B sites using colour-diff feedback ──────
315        for y in 0..height {
316            for x in 0..width {
317                match fc(x, y) {
318                    0 => {
319                        let g_new = get(x as isize, y as isize) - r_diff_i[y * width + x];
320                        green[y * width + x] = (green[y * width + x] + g_new) * 0.5;
321                    }
322                    2 => {
323                        let g_new = get(x as isize, y as isize) - b_diff_i[y * width + x];
324                        green[y * width + x] = (green[y * width + x] + g_new) * 0.5;
325                    }
326                    _ => {}
327                }
328            }
329        }
330
331        // ── Pass 3: re-interpolate colour diffs with refined green ────────────
332        (r_diff, b_diff) = build_diffs(&green);
333        r_diff_i = interp_plane(&r_diff, 0);
334        b_diff_i = interp_plane(&b_diff, 2);
335
336        // ── Compose output ────────────────────────────────────────────────────
337        let wl = raw.white_level() as f32;
338        for y in 0..height {
339            for x in 0..width {
340                let idx = (y * width + x) * 3;
341                let g = green[y * width + x];
342                let r = (g + r_diff_i[y * width + x]).clamp(0.0, wl);
343                let b = (g + b_diff_i[y * width + x]).clamp(0.0, wl);
344                let g = g.clamp(0.0, wl);
345                output[idx] = r as u16;
346                output[idx + 1] = g as u16;
347                output[idx + 2] = b as u16;
348            }
349        }
350
351        Ok(())
352    }
353}
354
355/// Fast demosaicing algorithm for X-Trans sensors.
356///
357/// Simple bilinear interpolation for each channel independently.
358/// Not suitable for final output but fast enough for live-view previews
359/// and thumbnails where speed matters more than accuracy.
360pub struct XTransFast;
361
362impl Demosaic for XTransFast {
363    fn demosaic_into(&self, raw: &RawImage, output: &mut [u16]) -> Result<(), DemosaicError> {
364        let width = raw.active_area().size.width as usize;
365        let height = raw.active_area().size.height as usize;
366        let x_off = raw.active_area().origin.x as usize;
367        let y_off = raw.active_area().origin.y as usize;
368        let raw_w = raw.width() as usize;
369
370        let expected = width * height * 3;
371        if output.len() != expected {
372            return Err(DemosaicError::BufferSizeMismatch {
373                expected,
374                actual: output.len(),
375            });
376        }
377        if width < 6 || height < 6 {
378            return Err(DemosaicError::InvalidDimensions);
379        }
380
381        let pattern = raw
382            .xtrans_pattern()
383            .copied()
384            .unwrap_or_else(XTransPattern::standard);
385        let fc = |x: usize, y: usize| -> u8 { pattern.color_at(x + x_off, y + y_off) };
386        let get = |x: isize, y: isize| -> f32 {
387            let cx = x.clamp(0, (width as isize) - 1) as usize;
388            let cy = y.clamp(0, (height as isize) - 1) as usize;
389            raw.data[(cy + y_off) * raw_w + (cx + x_off)] as f32
390        };
391
392        // For each pixel, interpolate each channel from nearest same-colour
393        // neighbours in a 3×3 window. Known values are used directly.
394        let wl = raw.white_level() as f32;
395
396        for y in 0..height {
397            for x in 0..width {
398                let idx = (y * width + x) * 3;
399                let own_color = fc(x, y);
400
401                let interp = |target_color: u8| -> f32 {
402                    if own_color == target_color {
403                        return get(x as isize, y as isize);
404                    }
405                    // Scan 3×3, fall back to 5×5 if nothing found.
406                    for radius in [1i32, 2, 3] {
407                        let mut sum = 0.0f32;
408                        let mut count = 0u32;
409                        for dy in -radius..=radius {
410                            for dx in -radius..=radius {
411                                let nx = x as i32 + dx;
412                                let ny = y as i32 + dy;
413                                if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
414                                    continue;
415                                }
416                                if fc(nx as usize, ny as usize) == target_color {
417                                    sum += get(nx as isize, ny as isize);
418                                    count += 1;
419                                }
420                            }
421                        }
422                        if count > 0 {
423                            return sum / count as f32;
424                        }
425                    }
426                    get(x as isize, y as isize)
427                };
428
429                output[idx] = interp(0).clamp(0.0, wl) as u16; // R
430                output[idx + 1] = interp(1).clamp(0.0, wl) as u16; // G
431                output[idx + 2] = interp(2).clamp(0.0, wl) as u16; // B
432            }
433        }
434
435        Ok(())
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::core::image::{CfaPattern, Point, Rect, Size};
443
444    /// Create a minimal X-Trans RawImage for testing.
445    ///
446    /// All pixels are set to `value`.  The `xtrans_pattern` field is set to the
447    /// standard Fujifilm pattern so the Markesteijn algorithm can identify
448    /// which colour each sensor site carries.
449    fn create_xtrans_raw(width: u32, height: u32, value: u16) -> RawImage {
450        let size = Size::new(width, height);
451        let active_area = Rect::new(Point::ORIGIN, size);
452        let pixel_count = (width * height) as usize;
453        RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
454            .xtrans_pattern(XTransPattern::standard())
455            .white_level(16383)
456            .data(vec![value; pixel_count])
457            .build()
458    }
459
460    // ── correctness helpers ───────────────────────────────────────────────────
461
462    /// All interpolated values in the output should be within `tolerance` of
463    /// the expected value (used for uniform-input tests).
464    fn max_deviation(output: &[u16], expected: u16) -> u16 {
465        output
466            .iter()
467            .map(|&v| (v as i32 - expected as i32).unsigned_abs() as u16)
468            .max()
469            .unwrap_or(0)
470    }
471
472    // ── tests ─────────────────────────────────────────────────────────────────
473
474    #[test]
475    fn correct_output_size() {
476        let raw = create_xtrans_raw(12, 12, 1000);
477        let mut output = vec![0u16; 12 * 12 * 3];
478        assert!(Markesteijn.demosaic_into(&raw, &mut output).is_ok());
479    }
480
481    #[test]
482    fn wrong_buffer_size_returns_error() {
483        let raw = create_xtrans_raw(12, 12, 1000);
484        // Too small – must not be 12*12*3 = 432
485        let mut output = vec![0u16; 100];
486        let result = Markesteijn.demosaic_into(&raw, &mut output);
487        assert!(
488            matches!(result, Err(DemosaicError::BufferSizeMismatch { .. })),
489            "expected BufferSizeMismatch, got {:?}",
490            result
491        );
492    }
493
494    #[test]
495    fn too_small_image_returns_invalid_dimensions() {
496        // A 5×5 image is below the 6×6 minimum.
497        let raw = create_xtrans_raw(5, 5, 1000);
498        let mut output = vec![0u16; 5 * 5 * 3];
499        let result = Markesteijn.demosaic_into(&raw, &mut output);
500        assert!(
501            matches!(result, Err(DemosaicError::InvalidDimensions)),
502            "expected InvalidDimensions, got {:?}",
503            result
504        );
505    }
506
507    #[test]
508    fn uniform_input_produces_near_uniform_output() {
509        // When every sensor site has the same value `v`, the demosaiced
510        // output should also be close to `v` for every channel.
511        let v: u16 = 4096;
512        let raw = create_xtrans_raw(24, 24, v);
513        let mut output = vec![0u16; 24 * 24 * 3];
514        Markesteijn
515            .demosaic_into(&raw, &mut output)
516            .expect("demosaic failed");
517
518        // Allow a ±5-count deviation due to floating-point rounding.
519        let dev = max_deviation(&output, v);
520        assert!(
521            dev <= 5,
522            "max deviation from uniform value {v} is {dev} (expected ≤ 5)"
523        );
524    }
525
526    #[test]
527    fn output_respects_white_level_clamp() {
528        // Set all pixels to the white level – output must not exceed it.
529        let wl: u16 = 16383;
530        let raw = create_xtrans_raw(12, 12, wl);
531        let mut output = vec![0u16; 12 * 12 * 3];
532        Markesteijn
533            .demosaic_into(&raw, &mut output)
534            .expect("demosaic failed");
535        let max_out = *output.iter().max().unwrap_or(&0);
536        assert!(
537            max_out <= wl,
538            "output value {max_out} exceeds white level {wl}"
539        );
540    }
541
542    #[test]
543    fn zero_input_produces_zero_output() {
544        let raw = create_xtrans_raw(12, 12, 0);
545        let mut output = vec![1u16; 12 * 12 * 3]; // pre-fill with non-zero
546        Markesteijn
547            .demosaic_into(&raw, &mut output)
548            .expect("demosaic failed");
549        assert!(
550            output.iter().all(|&v| v == 0),
551            "expected all-zero output for all-zero input"
552        );
553    }
554
555    #[test]
556    fn xtrans_pattern_standard_color_at() {
557        let p = XTransPattern::standard();
558        // The pattern tiles with period 6 – verify wrapping.
559        for y in 0..12 {
560            for x in 0..12 {
561                assert_eq!(
562                    p.color_at(x, y),
563                    p.color_at(x % 6, y % 6),
564                    "color_at wrapping failed at ({x},{y})"
565                );
566            }
567        }
568        // All values must be 0, 1, or 2.
569        for row in &p.cells {
570            for &c in row {
571                assert!(c <= 2, "invalid colour index {c}");
572            }
573        }
574    }
575
576    #[test]
577    fn markesteijn_uses_default_pattern_when_xtrans_is_none() {
578        // When xtrans_pattern is None the algorithm must still succeed
579        // (falling back to XTransPattern::standard()).
580        let size = Size::new(12, 12);
581        let active_area = Rect::new(Point::ORIGIN, size);
582        // xtrans_pattern deliberately absent — algorithm should fall back to standard()
583        let raw = RawImage::builder(size, active_area, 14, CfaPattern::Rggb)
584            .white_level(16383)
585            .data(vec![2000u16; 12 * 12])
586            .build();
587        let mut output = vec![0u16; 12 * 12 * 3];
588        assert!(Markesteijn.demosaic_into(&raw, &mut output).is_ok());
589    }
590
591    // ── Markesteijn3Pass tests ────────────────────────────────────────────────
592
593    #[test]
594    fn markesteijn3_correct_output_size() {
595        let raw = create_xtrans_raw(12, 12, 1000);
596        let mut output = vec![0u16; 12 * 12 * 3];
597        assert!(Markesteijn3Pass.demosaic_into(&raw, &mut output).is_ok());
598    }
599
600    #[test]
601    fn markesteijn3_wrong_buffer_returns_error() {
602        let raw = create_xtrans_raw(12, 12, 1000);
603        let mut output = vec![0u16; 100];
604        assert!(matches!(
605            Markesteijn3Pass.demosaic_into(&raw, &mut output),
606            Err(DemosaicError::BufferSizeMismatch { .. })
607        ));
608    }
609
610    #[test]
611    fn markesteijn3_too_small_returns_invalid_dimensions() {
612        let raw = create_xtrans_raw(5, 5, 1000);
613        let mut output = vec![0u16; 5 * 5 * 3];
614        assert!(matches!(
615            Markesteijn3Pass.demosaic_into(&raw, &mut output),
616            Err(DemosaicError::InvalidDimensions)
617        ));
618    }
619
620    #[test]
621    fn markesteijn3_uniform_produces_near_uniform_output() {
622        let v: u16 = 4096;
623        let raw = create_xtrans_raw(24, 24, v);
624        let mut output = vec![0u16; 24 * 24 * 3];
625        Markesteijn3Pass.demosaic_into(&raw, &mut output).unwrap();
626        let dev = max_deviation(&output, v);
627        assert!(dev <= 5, "max deviation from {v} is {dev} (expected ≤ 5)");
628    }
629
630    #[test]
631    fn markesteijn3_respects_white_level() {
632        let wl: u16 = 16383;
633        let raw = create_xtrans_raw(12, 12, wl);
634        let mut output = vec![0u16; 12 * 12 * 3];
635        Markesteijn3Pass.demosaic_into(&raw, &mut output).unwrap();
636        assert!(*output.iter().max().unwrap() <= wl);
637    }
638
639    // ── XTransFast tests ──────────────────────────────────────────────────────
640
641    #[test]
642    fn xtrans_fast_correct_output_size() {
643        let raw = create_xtrans_raw(12, 12, 1000);
644        let mut output = vec![0u16; 12 * 12 * 3];
645        assert!(XTransFast.demosaic_into(&raw, &mut output).is_ok());
646    }
647
648    #[test]
649    fn xtrans_fast_wrong_buffer_returns_error() {
650        let raw = create_xtrans_raw(12, 12, 1000);
651        let mut output = vec![0u16; 100];
652        assert!(matches!(
653            XTransFast.demosaic_into(&raw, &mut output),
654            Err(DemosaicError::BufferSizeMismatch { .. })
655        ));
656    }
657
658    #[test]
659    fn xtrans_fast_too_small_returns_invalid_dimensions() {
660        let raw = create_xtrans_raw(5, 5, 1000);
661        let mut output = vec![0u16; 5 * 5 * 3];
662        assert!(matches!(
663            XTransFast.demosaic_into(&raw, &mut output),
664            Err(DemosaicError::InvalidDimensions)
665        ));
666    }
667
668    #[test]
669    fn xtrans_fast_uniform_produces_near_uniform_output() {
670        let v: u16 = 4096;
671        let raw = create_xtrans_raw(18, 18, v);
672        let mut output = vec![0u16; 18 * 18 * 3];
673        XTransFast.demosaic_into(&raw, &mut output).unwrap();
674        let dev = max_deviation(&output, v);
675        assert!(dev <= 5, "max deviation from {v} is {dev} (expected ≤ 5)");
676    }
677
678    #[test]
679    fn xtrans_fast_respects_white_level() {
680        let wl: u16 = 16383;
681        let raw = create_xtrans_raw(12, 12, wl);
682        let mut output = vec![0u16; 12 * 12 * 3];
683        XTransFast.demosaic_into(&raw, &mut output).unwrap();
684        assert!(*output.iter().max().unwrap() <= wl);
685    }
686}