use crate::border::{BorderPolicy, Mirror, compute_interior_region};
use crate::error::Error;
use crate::image::{Image, ImageView, ImageViewMut};
use crate::pixel::{
FromLinear, LinearPixel, MonoF32, RgbF32, ZeroablePixel,
bayer::{BayerPixel, CfaColor},
};
use crate::{Coordinate, Offset, Size};
pub trait DemosaicMethod<B: BayerPixel> {
const RADIUS: usize;
fn interpolate<S>(&self, at: Coordinate, sample: S) -> RgbF32
where
S: Fn(Offset) -> f32;
}
#[must_use]
pub fn demosaic<I, B, M>(image: &I, method: M) -> Image<B::RgbOutput>
where
I: ImageView<Pixel = B>,
B: BayerPixel + LinearPixel<f32, Accumulator = MonoF32>,
B::RgbOutput: ZeroablePixel + FromLinear<RgbF32>,
M: DemosaicMethod<B>,
{
let mut output = Image::<B::RgbOutput>::zero(image.width(), image.height());
demosaic_into(image, &mut output, method);
output
}
pub fn demosaic_into<I, B, O, M>(image: &I, output: &mut O, method: M)
where
I: ImageView<Pixel = B>,
B: BayerPixel + LinearPixel<f32, Accumulator = MonoF32>,
O: ImageViewMut<Pixel = B::RgbOutput>,
B::RgbOutput: FromLinear<RgbF32>,
M: DemosaicMethod<B>,
{
assert_eq!(
image.size(),
output.size(),
"demosaic_into: input size {:?} does not match output size {:?}",
image.size(),
output.size(),
);
let radius = M::RADIUS;
let window = 2 * radius + 1;
let interior =
compute_interior_region(image.size(), Size::new(window, window), (radius, radius));
if let Some(interior) = interior {
for y in interior.top()..interior.bottom() {
for x in interior.left()..interior.right() {
let site = Coordinate::new(x, y);
let rgb = method.interpolate(site, |tap| {
let sx = (x as isize + tap.dx as isize) as usize;
let sy = (y as isize + tap.dy as isize) as usize;
image.pixel_at(sx, sy).to_accumulator().0
});
*output.pixel_at_mut(x, y) = FromLinear::from_linear(rgb);
}
}
}
for y in 0..image.height() {
for x in 0..image.width() {
if let Some(ref interior) = interior {
if x >= interior.left()
&& x < interior.right()
&& y >= interior.top()
&& y < interior.bottom()
{
continue;
}
}
let rgb = method.interpolate(Coordinate::new(x, y), |tap| {
Mirror
.pixel_at(
image,
x as isize + tap.dx as isize,
y as isize + tap.dy as isize,
)
.to_accumulator()
.0
});
*output.pixel_at_mut(x, y) = FromLinear::from_linear(rgb);
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BayerBilinear;
impl<B: BayerPixel> DemosaicMethod<B> for BayerBilinear {
const RADIUS: usize = 1;
#[inline]
fn interpolate<S>(&self, at: Coordinate, sample: S) -> RgbF32
where
S: Fn(Offset) -> f32,
{
let center = sample(Offset::ZERO);
match B::PATTERN.color_at(at.x, at.y) {
color @ (CfaColor::Red | CfaColor::Blue) => {
let green = (sample(Offset::new(-1, 0))
+ sample(Offset::new(1, 0))
+ sample(Offset::new(0, -1))
+ sample(Offset::new(0, 1)))
* 0.25;
let other = (sample(Offset::new(-1, -1))
+ sample(Offset::new(1, -1))
+ sample(Offset::new(-1, 1))
+ sample(Offset::new(1, 1)))
* 0.25;
if color == CfaColor::Red {
RgbF32::new(center, green, other)
} else {
RgbF32::new(other, green, center)
}
}
CfaColor::Green => {
let along_row = (sample(Offset::new(-1, 0)) + sample(Offset::new(1, 0))) * 0.5;
let along_column = (sample(Offset::new(0, -1)) + sample(Offset::new(0, 1))) * 0.5;
if B::PATTERN.color_at(at.x + 1, at.y) == CfaColor::Red {
RgbF32::new(along_row, center, along_column)
} else {
RgbF32::new(along_column, center, along_row)
}
}
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MalvarHeCutler;
impl MalvarHeCutler {
#[inline(always)]
fn green_at_red_or_blue<S>(sample: &S) -> f32
where
S: Fn(Offset) -> f32,
{
let adjacent = sample(Offset::new(-1, 0))
+ sample(Offset::new(1, 0))
+ sample(Offset::new(0, -1))
+ sample(Offset::new(0, 1));
let outer = sample(Offset::new(-2, 0))
+ sample(Offset::new(2, 0))
+ sample(Offset::new(0, -2))
+ sample(Offset::new(0, 2));
(4.0 * sample(Offset::ZERO) + 2.0 * adjacent - outer) * 0.125
}
#[inline(always)]
fn along_row<S>(sample: &S) -> f32
where
S: Fn(Offset) -> f32,
{
let row_adjacent = sample(Offset::new(-1, 0)) + sample(Offset::new(1, 0));
let row_outer = sample(Offset::new(-2, 0)) + sample(Offset::new(2, 0));
let diagonal = sample(Offset::new(-1, -1))
+ sample(Offset::new(1, -1))
+ sample(Offset::new(-1, 1))
+ sample(Offset::new(1, 1));
let column_outer = sample(Offset::new(0, -2)) + sample(Offset::new(0, 2));
(5.0 * sample(Offset::ZERO) + 4.0 * row_adjacent - row_outer - diagonal
+ 0.5 * column_outer)
* 0.125
}
#[inline(always)]
fn along_column<S>(sample: &S) -> f32
where
S: Fn(Offset) -> f32,
{
let column_adjacent = sample(Offset::new(0, -1)) + sample(Offset::new(0, 1));
let column_outer = sample(Offset::new(0, -2)) + sample(Offset::new(0, 2));
let diagonal = sample(Offset::new(-1, -1))
+ sample(Offset::new(1, -1))
+ sample(Offset::new(-1, 1))
+ sample(Offset::new(1, 1));
let row_outer = sample(Offset::new(-2, 0)) + sample(Offset::new(2, 0));
(5.0 * sample(Offset::ZERO) + 4.0 * column_adjacent - column_outer - diagonal
+ 0.5 * row_outer)
* 0.125
}
#[inline(always)]
fn diagonal<S>(sample: &S) -> f32
where
S: Fn(Offset) -> f32,
{
let diagonal = sample(Offset::new(-1, -1))
+ sample(Offset::new(1, -1))
+ sample(Offset::new(-1, 1))
+ sample(Offset::new(1, 1));
let outer = sample(Offset::new(-2, 0))
+ sample(Offset::new(2, 0))
+ sample(Offset::new(0, -2))
+ sample(Offset::new(0, 2));
(6.0 * sample(Offset::ZERO) + 2.0 * diagonal - 1.5 * outer) * 0.125
}
}
impl<B: BayerPixel> DemosaicMethod<B> for MalvarHeCutler {
const RADIUS: usize = 2;
#[inline]
fn interpolate<S>(&self, at: Coordinate, sample: S) -> RgbF32
where
S: Fn(Offset) -> f32,
{
let center = sample(Offset::ZERO);
match B::PATTERN.color_at(at.x, at.y) {
CfaColor::Red => RgbF32::new(
center,
Self::green_at_red_or_blue(&sample),
Self::diagonal(&sample),
),
CfaColor::Blue => RgbF32::new(
Self::diagonal(&sample),
Self::green_at_red_or_blue(&sample),
center,
),
CfaColor::Green => {
if B::PATTERN.color_at(at.x + 1, at.y) == CfaColor::Red {
RgbF32::new(
Self::along_row(&sample),
center,
Self::along_column(&sample),
)
} else {
RgbF32::new(
Self::along_column(&sample),
center,
Self::along_row(&sample),
)
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BayerGains {
red: f32,
green: f32,
blue: f32,
}
impl BayerGains {
#[must_use]
pub const fn new(red: f32, green: f32, blue: f32) -> Option<Self> {
if !(red.is_finite() && green.is_finite() && blue.is_finite()) {
return None;
}
if red < 0.0 || green < 0.0 || blue < 0.0 {
return None;
}
Some(Self { red, green, blue })
}
pub fn try_new(red: f32, green: f32, blue: f32) -> Result<Self, Error> {
let all = [red, green, blue];
if all.iter().all(|g| g.is_finite() && *g >= 0.0) {
Ok(Self { red, green, blue })
} else {
Err(Error::InvalidParameter(format!(
"BayerGains must be finite and non-negative, got \
red = {red}, green = {green}, blue = {blue}"
)))
}
}
#[must_use]
pub const fn unity() -> Self {
Self {
red: 1.0,
green: 1.0,
blue: 1.0,
}
}
#[must_use]
#[inline(always)]
pub const fn gain(self, color: CfaColor) -> f32 {
match color {
CfaColor::Red => self.red,
CfaColor::Green => self.green,
CfaColor::Blue => self.blue,
}
}
#[must_use]
pub const fn red(self) -> f32 {
self.red
}
#[must_use]
pub const fn green(self) -> f32 {
self.green
}
#[must_use]
pub const fn blue(self) -> f32 {
self.blue
}
}
#[must_use]
pub fn white_balance<I, B>(image: &I, gains: BayerGains) -> Image<B>
where
I: ImageView<Pixel = B>,
B: BayerPixel + LinearPixel<f32, Accumulator = MonoF32> + FromLinear<MonoF32> + ZeroablePixel,
{
let mut output = Image::<B>::zero(image.width(), image.height());
white_balance_into(image, &mut output, gains);
output
}
pub fn white_balance_into<I, B, O>(image: &I, output: &mut O, gains: BayerGains)
where
I: ImageView<Pixel = B>,
B: BayerPixel + LinearPixel<f32, Accumulator = MonoF32> + FromLinear<MonoF32>,
O: ImageViewMut<Pixel = B>,
{
assert_eq!(
image.size(),
output.size(),
"white_balance_into: input size {:?} does not match output size {:?}",
image.size(),
output.size(),
);
for y in 0..image.height() {
for x in 0..image.width() {
let gain = gains.gain(B::PATTERN.color_at(x, y));
let scaled = image.pixel_at(x, y).scale(gain);
*output.pixel_at_mut(x, y) = FromLinear::from_linear(scaled);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pixel::bayer::{
BayerBggr8, BayerGbrg8, BayerGrbg8, BayerRggb8, BayerRggb12, BayerRggb16,
};
use crate::pixel::{Rgb8, Rgb12, Rgb16};
fn mosaic<B>(rgb: &Image<Rgb8>) -> Image<B>
where
B: BayerPixel + From<u8>,
{
Image::generate(rgb.width(), rgb.height(), |x, y| {
let p = rgb.pixel_at(x, y);
B::from(match B::PATTERN.color_at(x, y) {
CfaColor::Red => p.r.0,
CfaColor::Green => p.g.0,
CfaColor::Blue => p.b.0,
})
})
}
fn channel(rgb: Rgb8, color: CfaColor) -> u8 {
match color {
CfaColor::Red => rgb.r.0,
CfaColor::Green => rgb.g.0,
CfaColor::Blue => rgb.b.0,
}
}
fn mean_abs_error(reference: &Image<Rgb8>, result: &Image<Rgb8>) -> f64 {
let mut sum = 0.0;
for y in 0..reference.height() {
for x in 0..reference.width() {
let a = reference.pixel_at(x, y);
let b = result.pixel_at(x, y);
sum += (a.r.0 as f64 - b.r.0 as f64).abs()
+ (a.g.0 as f64 - b.g.0 as f64).abs()
+ (a.b.0 as f64 - b.b.0 as f64).abs();
}
}
sum / (3 * reference.width() * reference.height()) as f64
}
fn scene(width: usize, height: usize) -> Image<Rgb8> {
Image::generate(width, height, |x, y| {
let (cx, cy) = (width as f64 / 2.0, height as f64 / 2.0);
let radius = ((x as f64 - cx).powi(2) + (y as f64 - cy).powi(2)).sqrt();
let disc = if radius < width as f64 / 5.0 {
90.0
} else {
0.0
};
let step = if x > 2 * width / 3 { 60.0 } else { 0.0 };
let ramp = 40.0 + 100.0 * x as f64 / width as f64;
let l = ramp + disc + step;
Rgb8::new(
(l * 1.0).min(255.0) as u8,
(l * 0.8).min(255.0) as u8,
(l * 0.6).min(255.0) as u8,
)
})
}
#[test]
fn bilinear_reproduces_every_sampled_site_exactly() {
let reference = scene(24, 20);
let raw: Image<BayerRggb8> = mosaic(&reference);
let out: Image<Rgb8> = demosaic(&raw, BayerBilinear);
for y in 0..reference.height() {
for x in 0..reference.width() {
let color = BayerRggb8::PATTERN.color_at(x, y);
assert_eq!(
channel(out.pixel_at(x, y), color),
channel(reference.pixel_at(x, y), color),
"sampled {color:?} channel changed at ({x}, {y})",
);
}
}
}
#[test]
fn malvar_reproduces_every_sampled_site_exactly() {
let reference = scene(24, 20);
let raw: Image<BayerRggb8> = mosaic(&reference);
let out: Image<Rgb8> = demosaic(&raw, MalvarHeCutler);
for y in 0..reference.height() {
for x in 0..reference.width() {
let color = BayerRggb8::PATTERN.color_at(x, y);
assert_eq!(
channel(out.pixel_at(x, y), color),
channel(reference.pixel_at(x, y), color),
"sampled {color:?} channel changed at ({x}, {y})",
);
}
}
}
fn flat_round_trip<B>(method: impl DemosaicMethod<B> + Copy, label: &str)
where
B: BayerPixel<RgbOutput = Rgb8> + From<u8> + LinearPixel<f32, Accumulator = MonoF32>,
{
let color = Rgb8::new(200, 120, 60);
let reference = Image::fill(9, 7, color);
let raw: Image<B> = mosaic(&reference);
let out: Image<Rgb8> = demosaic(&raw, method);
for y in 0..out.height() {
for x in 0..out.width() {
assert_eq!(
out.pixel_at(x, y),
color,
"{label} {:?}: flat colour not recovered at ({x}, {y})",
B::PATTERN,
);
}
}
}
#[test]
fn bilinear_recovers_a_flat_colour_in_all_four_patterns() {
flat_round_trip::<BayerRggb8>(BayerBilinear, "bilinear");
flat_round_trip::<BayerBggr8>(BayerBilinear, "bilinear");
flat_round_trip::<BayerGrbg8>(BayerBilinear, "bilinear");
flat_round_trip::<BayerGbrg8>(BayerBilinear, "bilinear");
}
#[test]
fn malvar_recovers_a_flat_colour_in_all_four_patterns() {
flat_round_trip::<BayerRggb8>(MalvarHeCutler, "malvar");
flat_round_trip::<BayerBggr8>(MalvarHeCutler, "malvar");
flat_round_trip::<BayerGrbg8>(MalvarHeCutler, "malvar");
flat_round_trip::<BayerGbrg8>(MalvarHeCutler, "malvar");
}
#[test]
fn both_methods_are_exact_on_a_linear_ramp_in_the_interior() {
let reference = Image::generate(16, 12, |x, _| {
let v = (4 * x + 20) as u8;
Rgb8::new(v, v, v)
});
let raw: Image<BayerRggb8> = mosaic(&reference);
let bilinear: Image<Rgb8> = demosaic(&raw, BayerBilinear);
let malvar: Image<Rgb8> = demosaic(&raw, MalvarHeCutler);
for y in 2..10 {
for x in 2..14 {
assert_eq!(
bilinear.pixel_at(x, y),
reference.pixel_at(x, y),
"bilinear ({x}, {y})"
);
assert_eq!(
malvar.pixel_at(x, y),
reference.pixel_at(x, y),
"malvar ({x}, {y})"
);
}
}
}
#[test]
fn malvar_beats_bilinear_on_an_edge_scene() {
let reference = scene(48, 40);
let raw: Image<BayerRggb8> = mosaic(&reference);
let bilinear_error = mean_abs_error(&reference, &demosaic(&raw, BayerBilinear));
let malvar_error = mean_abs_error(&reference, &demosaic(&raw, MalvarHeCutler));
assert!(
malvar_error < bilinear_error,
"expected Malvar-He-Cutler to beat bilinear: \
malvar = {malvar_error:.4}, bilinear = {bilinear_error:.4}",
);
}
#[test]
fn malvar_lowers_the_fringe_peak_and_widens_the_band() {
let reference = Image::generate(16, 12, |x, _| {
let v = if x < 8 { 30u8 } else { 220 };
Rgb8::new(v, v, v)
});
let raw: Image<BayerRggb8> = mosaic(&reference);
let fringe = |out: &Image<Rgb8>| -> (i32, usize) {
let mut worst = 0;
let mut count = 0;
for y in 0..out.height() {
for x in 0..out.width() {
let p = out.pixel_at(x, y);
let channels = [p.r.0 as i32, p.g.0 as i32, p.b.0 as i32];
let spread = channels.iter().max().unwrap() - channels.iter().min().unwrap();
worst = worst.max(spread);
count += usize::from(spread > 0);
}
}
(worst, count)
};
let bilinear: Image<Rgb8> = demosaic(&raw, BayerBilinear);
let malvar: Image<Rgb8> = demosaic(&raw, MalvarHeCutler);
let (bilinear_worst, bilinear_count) = fringe(&bilinear);
let (malvar_worst, malvar_count) = fringe(&malvar);
assert!(
malvar_worst * 3 < bilinear_worst * 2,
"expected Malvar-He-Cutler to cut the fringe peak by a third: \
malvar = {malvar_worst}, bilinear = {bilinear_worst}",
);
assert_eq!(
(bilinear_count, malvar_count),
(2 * 12, 4 * 12),
"the fringed band should be exactly as wide as each method's window",
);
for y in 0..12 {
for x in (0..16).filter(|x| !(6..=9).contains(x)) {
let p = malvar.pixel_at(x, y);
assert_eq!(
(p.r.0, p.g.0, p.b.0),
(p.r.0, p.r.0, p.r.0),
"colour fringe away from the step at ({x}, {y}): {p:?}",
);
}
}
}
#[test]
fn output_size_matches_the_input_including_odd_dimensions() {
let raw = Image::fill(7, 5, BayerRggb8::new(80));
let out: Image<Rgb8> = demosaic(&raw, MalvarHeCutler);
assert_eq!(out.size(), Size::new(7, 5));
}
#[test]
fn images_smaller_than_the_window_are_all_border() {
for (w, h) in [(1, 1), (2, 2), (3, 3), (4, 1), (1, 4)] {
let raw = Image::fill(w, h, BayerRggb8::new(120));
let out: Image<Rgb8> = demosaic(&raw, MalvarHeCutler);
assert_eq!(out.size(), Size::new(w, h));
assert_eq!(out.pixel_at(0, 0), Rgb8::new(120, 120, 120));
}
}
#[test]
fn depth_is_preserved_across_the_family() {
let raw12 = Image::fill(8, 8, BayerRggb12::new(3000));
let out12: Image<Rgb12> = demosaic(&raw12, MalvarHeCutler);
assert_eq!(out12.pixel_at(4, 4), Rgb12::new(3000, 3000, 3000));
let raw16 = Image::fill(8, 8, BayerRggb16::new(60000));
let out16: Image<Rgb16> = demosaic(&raw16, BayerBilinear);
assert_eq!(out16.pixel_at(4, 4), Rgb16::new(60000, 60000, 60000));
}
#[test]
fn malvar_overshoot_clamps_at_the_sample_depth() {
let raw = Image::generate(16, 12, |x, _| {
BayerRggb12::new(if x < 8 { 0 } else { 4095 })
});
let out: Image<Rgb12> = demosaic(&raw, MalvarHeCutler);
for y in 0..12 {
for x in 0..16 {
let p = out.pixel_at(x, y);
for value in [p.r.value(), p.g.value(), p.b.value()] {
assert!(value <= 4095, "value {value} exceeds 12 bits at ({x}, {y})");
}
}
}
}
#[test]
fn demosaic_accepts_a_phase_aligned_sub_view() {
use crate::Rectangle;
use crate::image::BayerSubView;
let reference = scene(16, 16);
let raw: Image<BayerRggb8> = mosaic(&reference);
let whole: Image<Rgb8> = demosaic(&raw, BayerBilinear);
let roi = raw
.aligned_bayer_roi(Rectangle::new((4, 4), (8, 8)))
.expect("even origin inside bounds");
let cropped: Image<Rgb8> = demosaic(&roi, BayerBilinear);
for y in 1..7 {
for x in 1..7 {
assert_eq!(
cropped.pixel_at(x, y),
whole.pixel_at(x + 4, y + 4),
"sub-view disagrees with the parent at ({x}, {y})",
);
}
}
}
#[test]
#[should_panic(expected = "does not match output size")]
fn demosaic_into_rejects_a_mismatched_output() {
let raw = Image::fill(8, 8, BayerRggb8::new(10));
let mut out = Image::<Rgb8>::zero(4, 4);
demosaic_into(&raw, &mut out, BayerBilinear);
}
#[test]
fn demosaic_and_demosaic_into_agree() {
let raw: Image<BayerRggb8> = mosaic(&scene(12, 10));
let allocated: Image<Rgb8> = demosaic(&raw, MalvarHeCutler);
let mut written = Image::<Rgb8>::zero(12, 10);
demosaic_into(&raw, &mut written, MalvarHeCutler);
for y in 0..10 {
for x in 0..12 {
assert_eq!(allocated.pixel_at(x, y), written.pixel_at(x, y));
}
}
}
#[test]
fn gains_apply_per_cfa_colour() {
let raw = Image::fill(6, 6, BayerRggb12::new(1000));
let out = white_balance(&raw, BayerGains::new(1.5, 1.0, 0.5).unwrap());
for y in 0..6 {
for x in 0..6 {
let expected = match BayerRggb12::PATTERN.color_at(x, y) {
CfaColor::Red => 1500,
CfaColor::Green => 1000,
CfaColor::Blue => 500,
};
assert_eq!(out.pixel_at(x, y).value(), expected, "at ({x}, {y})");
}
}
}
#[test]
fn every_pattern_balances_its_own_tile() {
let rggb = white_balance(
&Image::fill(4, 4, BayerRggb8::new(100)),
BayerGains::new(2.0, 1.0, 1.0).unwrap(),
);
let bggr = white_balance(
&Image::fill(4, 4, BayerBggr8::new(100)),
BayerGains::new(2.0, 1.0, 1.0).unwrap(),
);
assert_eq!(rggb.pixel_at(0, 0).value(), 200); assert_eq!(bggr.pixel_at(0, 0).value(), 100); assert_eq!(bggr.pixel_at(1, 1).value(), 200); }
#[test]
fn unity_gains_are_the_identity() {
let raw: Image<BayerRggb12> = Image::generate(8, 8, |x, y| {
BayerRggb12::new(((x * 37 + y * 11) % 4096) as u16)
});
let out = white_balance(&raw, BayerGains::unity());
for y in 0..8 {
for x in 0..8 {
assert_eq!(out.pixel_at(x, y), raw.pixel_at(x, y));
}
}
}
#[test]
fn gains_clip_at_the_sample_depth() {
let raw = Image::fill(4, 4, BayerRggb12::new(3000));
let out = white_balance(&raw, BayerGains::new(4.0, 1.0, 1.0).unwrap());
assert_eq!(out.pixel_at(0, 0).value(), 4095);
}
#[test]
fn a_zero_gain_empties_its_colour() {
let raw = Image::fill(4, 4, BayerRggb8::new(200));
let out = white_balance(&raw, BayerGains::new(0.0, 1.0, 1.0).unwrap());
assert_eq!(out.pixel_at(0, 0).value(), 0);
assert_eq!(out.pixel_at(1, 0).value(), 200);
}
#[test]
fn try_new_rejects_unusable_gains() {
assert!(BayerGains::try_new(1.0, 1.0, 1.0).is_ok());
assert!(BayerGains::try_new(0.0, 0.0, 0.0).is_ok());
assert!(BayerGains::try_new(-1.0, 1.0, 1.0).is_err());
assert!(BayerGains::try_new(1.0, f32::NAN, 1.0).is_err());
assert!(BayerGains::try_new(1.0, 1.0, f32::INFINITY).is_err());
}
#[test]
fn new_rejects_a_negative_gain() {
assert!(BayerGains::new(1.0, -0.5, 1.0).is_none());
assert!(BayerGains::new(f32::NAN, 1.0, 1.0).is_none());
}
#[test]
fn accessors_report_what_was_given() {
let gains = BayerGains::new(1.9, 1.0, 1.6).unwrap();
assert_eq!(gains.red(), 1.9);
assert_eq!(gains.green(), 1.0);
assert_eq!(gains.blue(), 1.6);
assert_eq!(gains.gain(CfaColor::Red), 1.9);
assert_eq!(gains.gain(CfaColor::Green), 1.0);
assert_eq!(gains.gain(CfaColor::Blue), 1.6);
}
#[test]
fn white_balance_into_agrees_with_the_allocating_form() {
let raw = Image::fill(6, 4, BayerGrbg8::new(90));
let gains = BayerGains::new(1.4, 1.0, 1.2).unwrap();
let allocated = white_balance(&raw, gains);
let mut written = Image::<BayerGrbg8>::zero(6, 4);
white_balance_into(&raw, &mut written, gains);
for y in 0..4 {
for x in 0..6 {
assert_eq!(allocated.pixel_at(x, y), written.pixel_at(x, y));
}
}
}
#[test]
#[should_panic(expected = "does not match output size")]
fn white_balance_into_rejects_a_mismatched_output() {
let raw = Image::fill(8, 8, BayerRggb8::new(10));
let mut out = Image::<BayerRggb8>::zero(8, 4);
white_balance_into(&raw, &mut out, BayerGains::unity());
}
#[test]
fn balancing_before_or_after_a_bilinear_demosaic_agrees() {
let reference = scene(20, 16);
let raw: Image<BayerRggb8> = mosaic(&reference);
let gains = BayerGains::new(1.25, 1.0, 0.75).unwrap();
let before: Image<Rgb8> = demosaic(&white_balance(&raw, gains), BayerBilinear);
let after: Image<Rgb8> = demosaic(&raw, BayerBilinear);
for y in 0..16 {
for x in 0..20 {
let b = before.pixel_at(x, y);
let a = after.pixel_at(x, y);
let scaled = [
(a.r.0 as f32 * gains.red()).round().min(255.0) as i32,
(a.g.0 as f32 * gains.green()).round().min(255.0) as i32,
(a.b.0 as f32 * gains.blue()).round().min(255.0) as i32,
];
let got = [b.r.0 as i32, b.g.0 as i32, b.b.0 as i32];
for (channel, (g, s)) in got.iter().zip(scaled.iter()).enumerate() {
assert!(
(g - s).abs() <= 1,
"channel {channel} at ({x}, {y}): {g} vs {s}",
);
}
}
}
}
#[test]
fn the_pinned_reflection_preserves_coordinate_parity() {
use crate::pixel::Mono8;
for len in 2..12usize {
let row: Image<Mono8> = Image::generate(len, 1, |x, _| Mono8::new(x as u8));
for coord in -6isize..(len as isize + 6) {
let reflected = Mirror.pixel_at(&row, coord, 0).value() as isize;
assert_eq!(
reflected.rem_euclid(2),
coord.rem_euclid(2),
"len {len}: reflecting {coord} to {reflected} flipped parity",
);
}
}
}
}