use super::*;
fn solid_rggb8(width: u32, height: u32, r: u8, g: u8, b: u8) -> std::vec::Vec<u8> {
let w = width as usize;
let h = height as usize;
let mut data = std::vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
data[y * w + x] = match (y & 1, x & 1) {
(0, 0) => r,
(0, 1) => g,
(1, 0) => g,
(1, 1) => b,
_ => unreachable!(),
};
}
}
data
}
fn solid_rggb12(width: u32, height: u32, r: u16, g: u16, b: u16) -> std::vec::Vec<u16> {
let w = width as usize;
let h = height as usize;
let mut data = std::vec![0u16; w * h];
for y in 0..h {
for x in 0..w {
let v = match (y & 1, x & 1) {
(0, 0) => r,
(0, 1) => g,
(1, 0) => g,
(1, 1) => b,
_ => unreachable!(),
};
data[y * w + x] = v;
}
}
data
}
#[test]
fn bayer_mixed_sinker_with_rgb_red_interior() {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb8(w, h, 255, 0, 0);
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let mut rgb = std::vec![0u8; (w * h * 3) as usize];
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_rgb(&mut rgb)
.unwrap();
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let wu = w as usize;
for y in 0..(h as usize) {
for x in 0..wu {
let i = (y * wu + x) * 3;
assert_eq!(rgb[i], 255, "px ({x},{y}) R");
assert_eq!(rgb[i + 1], 0, "px ({x},{y}) G");
assert_eq!(rgb[i + 2], 0, "px ({x},{y}) B");
}
}
}
#[test]
fn bayer_mixed_sinker_with_luma_uniform_byte() {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = std::vec![200u8; (w * h) as usize];
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let mut luma = std::vec![0u8; (w * h) as usize];
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_luma(&mut luma)
.unwrap();
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
for &y in &luma {
assert!((y as i32 - 200).abs() <= 1, "luma got {y}");
}
}
#[test]
fn bayer_mixed_sinker_with_hsv_solid_red_interior() {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb8(w, h, 255, 0, 0);
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let mut hh = std::vec![0u8; (w * h) as usize];
let mut ss = std::vec![0u8; (w * h) as usize];
let mut vv = std::vec![0u8; (w * h) as usize];
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_hsv(&mut hh, &mut ss, &mut vv)
.unwrap();
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let wu = w as usize;
for y in 0..(h as usize) {
for x in 0..wu {
let i = y * wu + x;
assert_eq!(hh[i], 0, "px ({x},{y}) H");
assert_eq!(ss[i], 255, "px ({x},{y}) S");
assert_eq!(vv[i], 255, "px ({x},{y}) V");
}
}
}
#[test]
fn bayer16_mixed_sinker_with_rgb_red_interior() {
use crate::{
frame::Bayer12Frame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer16_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb12(w, h, 4095, 0, 0);
let frame = Bayer12Frame::try_new(&raw, w, h, w).unwrap();
let mut rgb = std::vec![0u8; (w * h * 3) as usize];
let mut sinker = MixedSinker::<Bayer16<12>>::new(w as usize, h as usize)
.with_rgb(&mut rgb)
.unwrap();
bayer16_to::<12, _>(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let wu = w as usize;
for y in 0..(h as usize) {
for x in 0..wu {
let i = (y * wu + x) * 3;
assert_eq!(rgb[i], 255, "px ({x},{y}) R");
assert_eq!(rgb[i + 1], 0, "px ({x},{y}) G");
assert_eq!(rgb[i + 2], 0, "px ({x},{y}) B");
}
}
}
#[test]
fn bayer16_mixed_sinker_with_rgb_u16_red_interior() {
use crate::{
frame::Bayer12Frame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer16_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb12(w, h, 4095, 0, 0);
let frame = Bayer12Frame::try_new(&raw, w, h, w).unwrap();
let mut rgb = std::vec![0u16; (w * h * 3) as usize];
let mut sinker = MixedSinker::<Bayer16<12>>::new(w as usize, h as usize)
.with_rgb_u16(&mut rgb)
.unwrap();
bayer16_to::<12, _>(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let wu = w as usize;
for y in 0..(h as usize) {
for x in 0..wu {
let i = (y * wu + x) * 3;
assert_eq!(rgb[i], 4095, "px ({x},{y}) R");
assert_eq!(rgb[i + 1], 0, "px ({x},{y}) G");
assert_eq!(rgb[i + 2], 0, "px ({x},{y}) B");
}
}
}
#[test]
fn bayer16_mixed_sinker_dual_rgb_and_rgb_u16() {
use crate::{
frame::Bayer12Frame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer16_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb12(w, h, 4095, 0, 0);
let frame = Bayer12Frame::try_new(&raw, w, h, w).unwrap();
let mut rgb_u8 = std::vec![0u8; (w * h * 3) as usize];
let mut rgb_u16 = std::vec![0u16; (w * h * 3) as usize];
let mut sinker = MixedSinker::<Bayer16<12>>::new(w as usize, h as usize)
.with_rgb(&mut rgb_u8)
.unwrap()
.with_rgb_u16(&mut rgb_u16)
.unwrap();
bayer16_to::<12, _>(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let wu = w as usize;
for y in 0..(h as usize) {
for x in 0..wu {
let i = (y * wu + x) * 3;
assert_eq!(rgb_u8[i], 255);
assert_eq!(rgb_u16[i], 4095);
}
}
}
#[test]
fn bayer_mixed_sinker_returns_row_shape_mismatch_on_bad_above() {
use crate::raw::{BayerDemosaic, BayerPattern, BayerRow};
let mut rgb = std::vec![0u8; 8 * 6 * 3];
let mut sinker = MixedSinker::<Bayer>::new(8, 6).with_rgb(&mut rgb).unwrap();
sinker.begin_frame(8, 6).unwrap();
let mid = std::vec![0u8; 8];
let below = std::vec![0u8; 8];
let bad_above = std::vec![0u8; 7]; let m = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let row = BayerRow::new(
&bad_above,
&mid,
&below,
0,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
m,
);
let err = sinker.process(row).unwrap_err();
assert!(matches!(
&err,
MixedSinkerError::RowShapeMismatch(e)
if matches!(e.which(), RowSlice::BayerAbove) && e.expected() == 8 && e.actual() == 7
));
}
#[test]
fn bayer16_mixed_sinker_returns_row_shape_mismatch_on_bad_mid() {
use crate::raw::{BayerDemosaic, BayerPattern, BayerRow16};
let mut rgb = std::vec![0u8; 8 * 6 * 3];
let mut sinker = MixedSinker::<Bayer16<12>>::new(8, 6)
.with_rgb(&mut rgb)
.unwrap();
sinker.begin_frame(8, 6).unwrap();
let above = std::vec![0u16; 8];
let bad_mid = std::vec![0u16; 7]; let below = std::vec![0u16; 8];
let m = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let row = BayerRow16::<12>::new(
&above,
&bad_mid,
&below,
0,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
m,
);
let err = sinker.process(row).unwrap_err();
assert!(matches!(
&err,
MixedSinkerError::RowShapeMismatch(e)
if matches!(e.which(), RowSlice::Bayer16Mid) && e.expected() == 8 && e.actual() == 7
));
}
fn bayer8_solid_red_luma(coeffs: LumaCoefficients) -> u8 {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb8(w, h, 255, 0, 0);
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let mut luma = std::vec![0u8; (w * h) as usize];
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_luma(&mut luma)
.unwrap()
.with_luma_coefficients(coeffs);
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let center = luma[(h as usize / 2) * (w as usize) + (w as usize / 2)];
for (i, &y) in luma.iter().enumerate() {
assert_eq!(
y, center,
"luma not uniform at idx {i}: {y} vs center {center}"
);
}
center
}
#[test]
fn bayer_with_luma_coefficients_solid_red_differs_by_preset() {
let bt709 = bayer8_solid_red_luma(LumaCoefficients::Bt709);
let bt2020 = bayer8_solid_red_luma(LumaCoefficients::Bt2020);
let bt601 = bayer8_solid_red_luma(LumaCoefficients::Bt601);
let dcip3 = bayer8_solid_red_luma(LumaCoefficients::DciP3);
let aces = bayer8_solid_red_luma(LumaCoefficients::AcesAp1);
assert_eq!(bt709, 54, "BT.709 red luma");
assert_eq!(bt2020, 67, "BT.2020 red luma");
assert_eq!(bt601, 77, "BT.601 red luma");
assert_eq!(dcip3, 59, "DCI-P3 red luma");
assert_eq!(aces, 70, "ACES AP1 red luma");
let mut all = std::vec![bt709, bt2020, bt601, dcip3, aces];
all.sort_unstable();
all.dedup();
assert_eq!(all.len(), 5, "presets collapsed to fewer values: {all:?}");
}
#[test]
fn bayer_with_luma_coefficients_custom_round_trips_to_q8() {
let custom = LumaCoefficients::try_custom(1.0, 0.0, 0.0).unwrap();
let red = bayer8_solid_red_luma(custom);
assert_eq!(red, 255, "Custom (1.0, 0.0, 0.0) on red 255 → 255");
}
#[test]
fn bayer_with_luma_coefficients_default_is_bt709() {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb8(w, h, 255, 0, 0);
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let mut luma = std::vec![0u8; (w * h) as usize];
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_luma(&mut luma)
.unwrap();
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
for (i, &y) in luma.iter().enumerate() {
assert_eq!(y, 54, "default red luma at idx {i}");
}
assert_eq!(LumaCoefficients::default(), LumaCoefficients::Bt709);
}
#[test]
fn bayer_with_luma_coefficients_uniform_gray_invariant() {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = std::vec![200u8; (w * h) as usize];
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let presets = [
LumaCoefficients::Bt709,
LumaCoefficients::Bt2020,
LumaCoefficients::Bt601,
LumaCoefficients::DciP3,
LumaCoefficients::AcesAp1,
];
for preset in presets {
let mut luma = std::vec![0u8; (w * h) as usize];
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_luma(&mut luma)
.unwrap()
.with_luma_coefficients(preset);
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
for &y in &luma {
assert!(
(y as i32 - 200).abs() <= 1,
"{preset:?} on gray 200 → {y} (expected ~200)"
);
}
}
}
#[test]
fn bayer16_with_luma_coefficients_solid_red_differs_by_preset() {
use crate::{
frame::Bayer12Frame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer16_to},
};
let (w, h) = (8u32, 6u32);
let raw = solid_rggb12(w, h, 4095, 0, 0);
let frame = Bayer12Frame::try_new(&raw, w, h, w).unwrap();
let run = |coeffs: LumaCoefficients| -> u8 {
let mut luma = std::vec![0u8; (w * h) as usize];
let mut sinker = MixedSinker::<Bayer16<12>>::new(w as usize, h as usize)
.with_luma(&mut luma)
.unwrap()
.with_luma_coefficients(coeffs);
bayer16_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
let center = luma[(h as usize / 2) * (w as usize) + (w as usize / 2)];
for (i, &y) in luma.iter().enumerate() {
assert_eq!(y, center, "luma not uniform at idx {i}");
}
center
};
let bt709 = run(LumaCoefficients::Bt709);
let bt2020 = run(LumaCoefficients::Bt2020);
let bt601 = run(LumaCoefficients::Bt601);
let dcip3 = run(LumaCoefficients::DciP3);
let aces = run(LumaCoefficients::AcesAp1);
assert_eq!(bt709, 54, "BT.709 red luma (Bayer16<12>)");
assert_eq!(bt2020, 67, "BT.2020 red luma (Bayer16<12>)");
assert_eq!(bt601, 77, "BT.601 red luma (Bayer16<12>)");
assert_eq!(dcip3, 59, "DCI-P3 red luma (Bayer16<12>)");
assert_eq!(aces, 70, "ACES AP1 red luma (Bayer16<12>)");
let mut all = std::vec![bt709, bt2020, bt601, dcip3, aces];
all.sort_unstable();
all.dedup();
assert_eq!(all.len(), 5, "Bayer16 presets collapsed: {all:?}");
}
#[test]
fn luma_coefficients_to_q8_presets_sum_to_256() {
for preset in [
LumaCoefficients::Bt709,
LumaCoefficients::Bt2020,
LumaCoefficients::Bt601,
LumaCoefficients::DciP3,
LumaCoefficients::AcesAp1,
] {
let (cr, cg, cb) = preset.to_q8();
assert_eq!(cr + cg + cb, 256, "{preset:?} Q8 weights don't sum to 256");
}
}
#[test]
fn custom_luma_coefficients_accepts_valid_weights() {
let c = CustomLumaCoefficients::try_new(0.2126, 0.7152, 0.0722).unwrap();
assert_eq!(c.r(), 0.2126);
assert_eq!(c.g(), 0.7152);
assert_eq!(c.b(), 0.0722);
let z = CustomLumaCoefficients::try_new(0.0, 1.0, 0.0).unwrap();
assert_eq!(z.r(), 0.0);
let edge =
CustomLumaCoefficients::try_new(CustomLumaCoefficients::MAX_COEFFICIENT, 0.0, 0.0).unwrap();
assert_eq!(edge.r(), CustomLumaCoefficients::MAX_COEFFICIENT);
}
#[test]
fn custom_luma_coefficients_rejects_nan() {
for (channel, r, g, b) in [
(LumaChannel::R, f32::NAN, 1.0, 0.0),
(LumaChannel::G, 0.0, f32::NAN, 0.0),
(LumaChannel::B, 0.5, 0.5, f32::NAN),
] {
let err = CustomLumaCoefficients::try_new(r, g, b).unwrap_err();
assert!(
matches!(err, LumaCoefficientsError::NonFinite { channel: ch, .. } if ch == channel),
"expected NonFinite for {channel:?}, got {err:?}"
);
}
}
#[test]
fn custom_luma_coefficients_rejects_infinity() {
for inf in [f32::INFINITY, f32::NEG_INFINITY] {
let err_r = CustomLumaCoefficients::try_new(inf, 0.0, 0.0).unwrap_err();
let err_g = CustomLumaCoefficients::try_new(0.0, inf, 0.0).unwrap_err();
let err_b = CustomLumaCoefficients::try_new(0.0, 0.0, inf).unwrap_err();
for (err, channel) in [
(err_r, LumaChannel::R),
(err_g, LumaChannel::G),
(err_b, LumaChannel::B),
] {
assert!(
matches!(err, LumaCoefficientsError::NonFinite { channel: ch, .. } if ch == channel),
"expected NonFinite for {channel:?} with inf={inf}, got {err:?}"
);
}
}
}
#[test]
fn custom_luma_coefficients_rejects_negative() {
for (channel, r, g, b) in [
(LumaChannel::R, -0.001, 1.0, 0.0),
(LumaChannel::G, 0.0, -1.0, 0.0),
(LumaChannel::B, 0.5, 0.5, -42.0),
] {
let err = CustomLumaCoefficients::try_new(r, g, b).unwrap_err();
assert!(
matches!(err, LumaCoefficientsError::Negative { channel: ch, .. } if ch == channel),
"expected Negative for {channel:?}, got {err:?}"
);
}
}
#[test]
fn custom_luma_coefficients_rejects_oversized() {
let over = CustomLumaCoefficients::MAX_COEFFICIENT + 1.0;
for (channel, r, g, b) in [
(LumaChannel::R, over, 0.0, 0.0),
(LumaChannel::G, 0.0, over, 0.0),
(LumaChannel::B, 0.0, 0.0, over),
] {
let err = CustomLumaCoefficients::try_new(r, g, b).unwrap_err();
assert!(
matches!(
err,
LumaCoefficientsError::OutOfBounds { channel: ch, .. } if ch == channel
),
"expected OutOfBounds for {channel:?}, got {err:?}"
);
}
let err = CustomLumaCoefficients::try_new(1.0e9, 0.0, 0.0).unwrap_err();
assert!(matches!(err, LumaCoefficientsError::OutOfBounds { .. }));
}
#[test]
fn luma_coefficients_try_custom_routes_through_validation() {
let ok = LumaCoefficients::try_custom(0.5, 0.4, 0.1).unwrap();
assert!(ok.is_custom());
let err = LumaCoefficients::try_custom(f32::NAN, 0.0, 0.0).unwrap_err();
assert!(matches!(err, LumaCoefficientsError::NonFinite { .. }));
}
#[test]
#[should_panic(expected = "invalid CustomLumaCoefficients")]
fn custom_luma_coefficients_new_panics_on_invalid() {
let _ = CustomLumaCoefficients::new(f32::NAN, 0.0, 0.0);
}
#[test]
fn custom_luma_coefficients_at_max_does_not_overflow_kernel() {
use crate::{
frame::BayerFrame,
raw::{BayerDemosaic, BayerPattern, ColorCorrectionMatrix, WhiteBalance, bayer_to},
};
let (w, h) = (8u32, 6u32);
let raw = std::vec![255u8; (w * h) as usize];
let frame = BayerFrame::try_new(&raw, w, h, w).unwrap();
let mut luma = std::vec![0u8; (w * h) as usize];
let max = CustomLumaCoefficients::MAX_COEFFICIENT;
let mut sinker = MixedSinker::<Bayer>::new(w as usize, h as usize)
.with_luma(&mut luma)
.unwrap()
.with_luma_coefficients(LumaCoefficients::try_custom(max, max, max).unwrap());
bayer_to(
&frame,
BayerPattern::Rggb,
BayerDemosaic::Bilinear,
WhiteBalance::neutral(),
ColorCorrectionMatrix::identity(),
&mut sinker,
)
.unwrap();
for &y in &luma {
assert_eq!(
y, 255,
"max-weight saturated luma should clamp to 255, got {y}"
);
}
}