use super::*;
use crate::{
BayerPattern, ColorSpace, DemosaicMethod, DynamicImageOwned, DynamicImageRef, GenericImageRef,
ImageProps, ImageRef, PixelData, PixelStor, PixelType,
};
fn sample16(n: usize) -> Vec<u16> {
(0..n).map(|i| ((i * 7 + 3) % 251) as u16).collect()
}
fn bayer_frame(w: usize, h: usize, pat: BayerPattern, data: &mut [u16]) -> DynamicImageRef<'_> {
DynamicImageRef::from(ImageRef::new(data, w, h, ColorSpace::Bayer(pat)).unwrap())
}
#[test]
fn growing_convert_in_place_matches_reference() {
let (w, h) = (7, 5);
let src = sample16(w * h);
let expected: Vec<f32> = src.iter().map(|&v| v.cast_f32()).collect();
let spec = ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16);
let mut runner = Pipeline::new()
.convert(PixelType::F32)
.compile(spec, Strategy::Sequential)
.unwrap();
let mut frame = src.clone();
let img = DynamicImageRef::from(ImageRef::new(&mut frame, w, h, ColorSpace::Gray).unwrap());
let out = runner.run(&img).unwrap();
assert_eq!(
bytemuck::cast_slice::<u8, f32>(out.as_raw_u8()),
&expected[..]
);
}
fn gray16(w: usize, h: usize, data: &mut [u16]) -> DynamicImageRef<'_> {
DynamicImageRef::from(ImageRef::new(data, w, h, ColorSpace::Gray).unwrap())
}
#[test]
fn sub_container_depth_flows_through_pipeline() {
let (w, h) = (6usize, 4usize);
let mut data = sample16(w * h);
let img = DynamicImageRef::from(
ImageRef::new(&mut data, w, h, ColorSpace::Gray)
.unwrap()
.with_bit_depth(12u8),
);
assert_eq!(img.pixel_type(), PixelType::U12);
let doubled = Pipeline::new().scale_by(2.0).apply(&img).unwrap();
assert_eq!(doubled.pixel_type(), PixelType::U12);
let got: &[u16] = bytemuck::cast_slice(doubled.as_raw_u8());
let want: Vec<u16> = sample16(w * h)
.iter()
.map(|&v| v.saturating_mul(2))
.collect();
assert_eq!(got, &want[..]);
let as_u8 = Pipeline::new().convert(PixelType::U8).apply(&img).unwrap();
assert_eq!(as_u8.pixel_type(), PixelType::U8);
assert!(matches!(
Pipeline::new()
.convert(PixelType::U12)
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U12),
Strategy::Sequential
)
.unwrap_err(),
PipelineError::ConvertTargetNotStorage(PixelType::U12)
));
}
#[test]
fn sub_container_depth_scales_against_true_range() {
let (w, h) = (2usize, 1usize);
let mut data = [4095u16, 0];
let img = DynamicImageRef::from(
ImageRef::new(&mut data, w, h, ColorSpace::Gray)
.unwrap()
.with_bit_depth(12u8),
);
let as_u8 = Pipeline::new().convert(PixelType::U8).apply(&img).unwrap();
assert_eq!(as_u8.as_raw_u8(), &[255, 0]);
let as_u16 = Pipeline::new().convert(PixelType::U16).apply(&img).unwrap();
let got: &[u16] = bytemuck::cast_slice(as_u16.as_raw_u8());
assert_eq!(got, &[u16::MAX, 0]);
assert_eq!(as_u16.pixel_type(), PixelType::U16);
let mut data2 = [3000u16, 0];
let img2 = DynamicImageRef::from(
ImageRef::new(&mut data2, w, h, ColorSpace::Gray)
.unwrap()
.with_bit_depth(12u8),
);
let scaled = Pipeline::new().scale_rational(2, 1).apply(&img2).unwrap();
let got: &[u16] = bytemuck::cast_slice(scaled.as_raw_u8());
assert_eq!(got, &[4095, 0]);
}
#[test]
fn geo_ops_sequential_reference() {
let (w, h) = (8usize, 6usize);
let src: Vec<u16> = (0..(w * h) as u16).collect();
let at = |d: &[u16], c: usize, r: usize, rw: usize| d[r * rw + c];
let run = |p: Pipeline| {
let mut runner = p
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let mut f = src.clone();
let out = runner.run(&gray16(w, h, &mut f)).unwrap();
(
out.width(),
out.height(),
bytemuck::cast_slice::<u8, u16>(out.as_raw_u8()).to_vec(),
)
};
let (ow, oh, d) = run(Pipeline::new().flip_horizontal());
assert_eq!((ow, oh), (w, h));
for r in 0..h {
for c in 0..w {
assert_eq!(at(&d, c, r, w), at(&src, w - 1 - c, r, w));
}
}
let (_, _, d) = run(Pipeline::new().flip_vertical());
for r in 0..h {
for c in 0..w {
assert_eq!(at(&d, c, r, w), at(&src, c, h - 1 - r, w));
}
}
let (_, _, d) = run(Pipeline::new().rotate_180());
for r in 0..h {
for c in 0..w {
assert_eq!(at(&d, c, r, w), at(&src, w - 1 - c, h - 1 - r, w));
}
}
let (ow, oh, d) = run(Pipeline::new().rotate_90());
assert_eq!((ow, oh), (h, w));
for r in 0..w {
for c in 0..h {
assert_eq!(at(&d, c, r, h), at(&src, r, h - 1 - c, w));
}
}
let (ow, oh, d) = run(Pipeline::new().rotate_270());
assert_eq!((ow, oh), (h, w));
for r in 0..w {
for c in 0..h {
assert_eq!(at(&d, c, r, h), at(&src, w - 1 - r, c, w));
}
}
let (ow, oh, d) = run(Pipeline::new().crop(2, 1, 4, 3));
assert_eq!((ow, oh), (4, 3));
for r in 0..3 {
for c in 0..4 {
assert_eq!(at(&d, c, r, 4), at(&src, 2 + c, 1 + r, w));
}
}
}
#[test]
fn geo_with_tiling_matches_sequential() {
let (w, h) = (32usize, 24usize);
let pat = BayerPattern::Rggb;
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let chains: Vec<(&str, Pipeline)> = vec![
(
"trailing flip_v",
Pipeline::new()
.debayer(DemosaicMethod::Linear)
.to_luma()
.convert(PixelType::U8)
.flip_vertical(),
),
(
"trailing rotate_90",
Pipeline::new()
.debayer(DemosaicMethod::Cubic)
.to_luma()
.rotate_90(),
),
(
"trailing rotate_270 + crop",
Pipeline::new()
.debayer(DemosaicMethod::Nearest)
.to_luma()
.rotate_270()
.crop(1, 2, 10, 20),
),
(
"leading crop",
Pipeline::new()
.crop(4, 2, 20, 16)
.debayer(DemosaicMethod::Linear)
.to_luma()
.convert(PixelType::U8),
),
(
"leading crop + trailing rot180",
Pipeline::new()
.crop(2, 4, 24, 16)
.debayer(DemosaicMethod::Cubic)
.to_luma()
.convert(PixelType::U8)
.rotate_180(),
),
];
for (name, p) in chains {
let mut seq = p.clone().compile(spec(), Strategy::Sequential).unwrap();
let mut f0 = sample16(w * h);
let want = seq
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
for strat in [
Strategy::tiled(4),
Strategy::tiled_parallel(5),
Strategy::tiled_2d(6, 8),
] {
let mut r = p.clone().compile(spec(), strat).unwrap();
let mut f = sample16(w * h);
let got = r
.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want, "{name} / {strat:?}");
}
}
}
#[test]
fn geo_validation_errors() {
let bayer = ImageSpec::new(8, 8, ColorSpace::Bayer(BayerPattern::Rggb), PixelType::U16);
assert!(matches!(
Pipeline::new()
.rotate_90()
.compile(bayer.clone(), Strategy::Sequential)
.unwrap_err(),
PipelineError::RotateOnBayer
));
assert!(matches!(
Pipeline::new()
.crop(4, 4, 6, 2)
.compile(bayer.clone(), Strategy::Sequential)
.unwrap_err(),
PipelineError::CropOutOfBounds { .. }
));
assert!(matches!(
Pipeline::new()
.roi(8, 0, 2, 2)
.compile(bayer, Strategy::Sequential)
.unwrap_err(),
PipelineError::RoiOutOfBounds { .. }
));
}
#[test]
fn roi_matches_select_roi_reference() {
let mut data: Vec<u16> = vec![0, 1, 2, 3, 4, 6, 5, 7, 8, 9]; let img = gray16(5, 2, &mut data);
let out = Pipeline::new().roi(1, 0, 2, 3).apply(&img).unwrap();
let got: &[u16] = bytemuck::cast_slice(out.as_raw_u8());
assert_eq!(got, &[1, 2, 5, 7, 0, 0]);
assert_eq!((out.width(), out.height()), (2, 3));
}
#[test]
fn run_into_reproduces_copy_to() {
let spec = ImageSpec::new(5, 2, ColorSpace::Gray, PixelType::U16);
let mut r = Pipeline::new()
.roi(1, 0, 2, 3)
.compile(spec, Strategy::Sequential)
.unwrap();
let mut dest = DynamicImageOwned::from(
crate::ImageOwned::from_owned(vec![0u16; 6], 2, 3, ColorSpace::Gray).unwrap(),
);
let mut data: Vec<u16> = vec![0, 1, 2, 3, 4, 6, 5, 7, 8, 9];
r.run_into(&gray16(5, 2, &mut data), &mut dest).unwrap();
assert_eq!(
dest.as_raw_u8_checked()
.map(bytemuck::cast_slice::<u8, u16>),
Some(&[1, 2, 5, 7, 0, 0][..])
);
let mut dest2 = DynamicImageOwned::from(
crate::ImageOwned::from_owned(vec![9u16; 4], 2, 2, ColorSpace::Gray).unwrap(),
);
let mut data2: Vec<u16> = vec![0, 1, 2, 3, 4, 6, 5, 7, 8, 9];
r.run_into(&gray16(5, 2, &mut data2), &mut dest2).unwrap();
assert_eq!((dest2.width(), dest2.height()), (2, 3));
}
#[test]
fn apply_preserves_metadata() {
use std::time::Duration;
let mut data: Vec<u16> = (0..24).collect();
let dynref = gray16(4, 6, &mut data);
let ts = chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap();
let mut rgen = GenericImageRef::new(ts, Duration::from_millis(50), dynref);
rgen.insert_key("CAMERA", "test-cam").unwrap();
let out = Pipeline::new().convert(PixelType::U8).apply(&rgen).unwrap();
assert_eq!(out.metadata(), rgen.metadata());
assert_eq!(out.timestamp(), ts);
assert_eq!(out.exposure(), Duration::from_millis(50));
assert_eq!(out.pixel_type(), PixelType::U8);
}
#[test]
fn tiled_matches_sequential_debayer_chain() {
let (w, h) = (24, 20);
let build = |m| Pipeline::new().debayer(m).to_luma().convert(PixelType::U8);
for pat in [
BayerPattern::Rggb,
BayerPattern::Bggr,
BayerPattern::Grbg,
BayerPattern::Gbrg,
] {
for method in [
DemosaicMethod::None,
DemosaicMethod::Nearest,
DemosaicMethod::Linear,
DemosaicMethod::Cubic,
] {
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let mut seq = build(method).compile(spec(), Strategy::Sequential).unwrap();
let mut f0 = sample16(w * h);
let want = seq
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
let strategies = [
Strategy::tiled(1),
Strategy::tiled(3),
Strategy::tiled(7),
Strategy::tiled_parallel(4),
Strategy::tiled_2d(4, 6),
Strategy::tiled_2d(3, 8),
Strategy::tiled_2d_parallel(5, 7),
Strategy::tiled_2d(1, 1),
];
for strat in strategies {
let mut r = build(method).compile(spec(), strat).unwrap();
let mut f = sample16(w * h);
let got = r
.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want, "pat={pat:?} method={method:?} strat={strat:?}");
}
}
}
}
#[test]
fn tiled_matches_sequential_row_local_chain() {
let (w, h) = (10, 40);
let build = || Pipeline::new().to_luma().convert(PixelType::U8);
let spec = || ImageSpec::new(w, h, ColorSpace::Rgb, PixelType::U16);
let mut seq = build().compile(spec(), Strategy::Sequential).unwrap();
let mut f0 = sample16(w * h * 3);
let img0 = DynamicImageRef::from(ImageRef::new(&mut f0, w, h, ColorSpace::Rgb).unwrap());
let want = seq.run(&img0).unwrap().as_raw_u8().to_vec();
for strat in [
Strategy::tiled_parallel(6),
Strategy::tiled_2d(6, 4),
Strategy::tiled(1),
] {
let mut r = build().compile(spec(), strat).unwrap();
let mut f = sample16(w * h * 3);
let img = DynamicImageRef::from(ImageRef::new(&mut f, w, h, ColorSpace::Rgb).unwrap());
assert_eq!(r.run(&img).unwrap().as_raw_u8(), want.as_slice());
}
}
fn mk_strat(tile_rows: usize, tile_cols: usize, parallel: bool) -> Strategy {
match (tile_cols, parallel) {
(0, false) => Strategy::tiled(tile_rows),
(0, true) => Strategy::tiled_parallel(tile_rows),
(c, false) => Strategy::tiled_2d(tile_rows, c),
(c, true) => Strategy::tiled_2d_parallel(tile_rows, c),
}
}
#[test]
fn tiled_matches_sequential_ragged_dims() {
let dims = [(53, 71), (53, 64), (64, 53), (47, 47)];
let tilings = [(16, 0), (19, 0), (11, 17), (23, 29), (13, 0)];
let mut tiled = 0usize;
let mut check = |strat: Strategy, is_tiled: bool, got: &[u8], want: &[u8], ctx: &str| {
assert!(is_tiled, "expected genuine tiling for {ctx} {strat:?}");
assert_eq!(got, want, "{ctx} {strat:?}");
tiled += 1;
};
for &(w, h) in &dims {
{
let build = || Pipeline::new().to_luma().convert(PixelType::U8);
let spec = || ImageSpec::new(w, h, ColorSpace::Rgb, PixelType::U16);
let mut f0 = sample16(w * h * 3);
let want = build()
.compile(spec(), Strategy::Sequential)
.unwrap()
.run(&DynamicImageRef::from(
ImageRef::new(&mut f0, w, h, ColorSpace::Rgb).unwrap(),
))
.unwrap()
.as_raw_u8()
.to_vec();
for &(tr, tc) in &tilings {
for parallel in [false, true] {
let strat = mk_strat(tr, tc, parallel);
let mut r = build().compile(spec(), strat).unwrap();
let is_tiled = r.is_tiled();
let mut f = sample16(w * h * 3);
let got = r
.run(&DynamicImageRef::from(
ImageRef::new(&mut f, w, h, ColorSpace::Rgb).unwrap(),
))
.unwrap()
.as_raw_u8()
.to_vec();
check(strat, is_tiled, &got, &want, &format!("row-local {w}x{h}"));
}
}
}
for pat in [BayerPattern::Rggb, BayerPattern::Gbrg] {
for method in [DemosaicMethod::Linear, DemosaicMethod::Cubic] {
let build = || {
Pipeline::new()
.debayer(method)
.to_luma()
.convert(PixelType::U8)
};
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let mut f0 = sample16(w * h);
let want = build()
.compile(spec(), Strategy::Sequential)
.unwrap()
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
for &(tr, tc) in &tilings {
for parallel in [false, true] {
let strat = mk_strat(tr, tc, parallel);
let mut r = build().compile(spec(), strat).unwrap();
let is_tiled = r.is_tiled();
let mut f = sample16(w * h);
let got = r
.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec();
check(
strat,
is_tiled,
&got,
&want,
&format!("debayer {w}x{h} {pat:?} {method:?}"),
);
}
}
}
}
}
assert_eq!(tiled, dims.len() * (5 * 2 + 2 * 2 * 5 * 2));
}
#[test]
fn tiling_falls_back_to_sequential_when_it_cannot_help() {
let build = || Pipeline::new().debayer(DemosaicMethod::Cubic).to_luma();
let spec = ImageSpec::new(40, 8, ColorSpace::Bayer(BayerPattern::Rggb), PixelType::U16);
for strat in [
Strategy::tiled(2),
Strategy::tiled_parallel(2),
Strategy::tiled_2d(2, 4),
] {
let mut r = build().compile(spec.clone(), strat).unwrap();
assert!(!r.is_tiled(), "{strat:?} should have fallen back");
let mut f = sample16(40 * 8);
let mut s = build().compile(spec.clone(), Strategy::Sequential).unwrap();
let mut f2 = sample16(40 * 8);
assert_eq!(
r.run(&bayer_frame(40, 8, BayerPattern::Rggb, &mut f))
.unwrap()
.as_raw_u8(),
s.run(&bayer_frame(40, 8, BayerPattern::Rggb, &mut f2))
.unwrap()
.as_raw_u8(),
);
}
let tall = ImageSpec::new(20, 100, ColorSpace::Rgb, PixelType::U16);
let r = Pipeline::new()
.to_luma()
.compile(tall, Strategy::tiled(200))
.unwrap();
assert!(!r.is_tiled());
}
#[test]
#[ignore = "multi-megapixel sweep; slow in debug"]
fn tiled_matches_sequential_full_sensor_frame() {
let (w, h) = (1944usize, 1472usize);
let tilings = [
(0, 0), (256, 0), (200, 0), (64, 0), (256, 512), (180, 300), (64, 8), (64, 64), ];
{
let build = || Pipeline::new().to_luma().convert(PixelType::U8);
let spec = || ImageSpec::new(w, h, ColorSpace::Rgb, PixelType::U16);
let mut f0 = sample16(w * h * 3);
let want = build()
.compile(spec(), Strategy::Sequential)
.unwrap()
.run(&DynamicImageRef::from(
ImageRef::new(&mut f0, w, h, ColorSpace::Rgb).unwrap(),
))
.unwrap()
.as_raw_u8()
.to_vec();
for &(tr, tc) in &tilings {
for parallel in [false, true] {
let strat = mk_strat(tr, tc, parallel);
let mut r = build().compile(spec(), strat).unwrap();
assert!(r.is_tiled(), "row-local {strat:?}");
let mut f = sample16(w * h * 3);
let got = r
.run(&DynamicImageRef::from(
ImageRef::new(&mut f, w, h, ColorSpace::Rgb).unwrap(),
))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want, "row-local {strat:?}");
}
}
}
{
let pat = BayerPattern::Rggb;
let build = || {
Pipeline::new()
.debayer(DemosaicMethod::Linear)
.to_luma()
.convert(PixelType::U8)
};
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let mut f0 = sample16(w * h);
let want = build()
.compile(spec(), Strategy::Sequential)
.unwrap()
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
for &(tr, tc) in &tilings {
for parallel in [false, true] {
let strat = mk_strat(tr, tc, parallel);
let mut r = build().compile(spec(), strat).unwrap();
assert!(r.is_tiled(), "debayer {strat:?}");
let mut f = sample16(w * h);
let got = r
.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want, "debayer {strat:?}");
}
}
}
}
#[test]
fn independent_buffer_sizing() {
let spec = ImageSpec::new(
100,
100,
ColorSpace::Bayer(BayerPattern::Rggb),
PixelType::U16,
);
let runner = Pipeline::new()
.debayer(DemosaicMethod::Nearest)
.to_luma()
.convert(PixelType::U8)
.compile(spec, Strategy::Sequential)
.unwrap();
assert!(
runner.scratch_bytes() <= 80_000,
"scratch {} B, expected <= 80000",
runner.scratch_bytes()
);
}
#[test]
fn no_swap_chain_uses_one_buffer() {
let spec = ImageSpec::new(64, 64, ColorSpace::Rgb, PixelType::U16);
let runner = Pipeline::new()
.scale(1.5, 0.0)
.to_luma()
.convert(PixelType::U8)
.compile(spec, Strategy::Sequential)
.unwrap();
assert!(
runner.scratch_bytes() <= 24_576 + 16,
"scratch {} B",
runner.scratch_bytes()
);
}
#[test]
fn scale_tiles_identically() {
let (w, h) = (16, 12);
let pat = BayerPattern::Rggb;
let build = || {
Pipeline::new()
.debayer(DemosaicMethod::Linear)
.scale(0.5, 10.0)
.to_luma()
.convert(PixelType::U8)
};
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let mut seq = build().compile(spec(), Strategy::Sequential).unwrap();
let mut f0 = sample16(w * h);
let want = seq
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
for strat in [
Strategy::tiled(3),
Strategy::tiled_parallel(4),
Strategy::tiled_2d(4, 6),
] {
let mut r = build().compile(spec(), strat).unwrap();
let mut f = sample16(w * h);
let got = r
.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want, "strat={strat:?}");
}
}
#[test]
fn scale_saturates_out_of_range() {
let (w, h) = (8, 4);
let mut data: Vec<u16> = (0..(w * h) as u16).map(|v| v * 1000).collect();
let mut r = Pipeline::new()
.scale(4.0, -5000.0)
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let got: Vec<u16> = {
let out = r.run(&gray16(w, h, &mut data)).unwrap();
bytemuck::cast_slice::<u8, u16>(out.as_raw_u8()).to_vec()
};
for (i, &g) in got.iter().enumerate() {
let want = u16::from_f64((i as f64 * 1000.0) * 4.0 - 5000.0);
assert_eq!(g, want);
}
assert_eq!(got[0], 0);
assert_eq!(*got.last().unwrap(), u16::MAX);
}
#[test]
fn scale_pixels_rational_is_exact() {
let (w, h) = (8, 4);
let mut data: Vec<u16> = (0..(w * h) as u16).map(|v| v * 137).collect();
let src = data.clone();
let mut r = Pipeline::new()
.scale_rational(65535, 4095)
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let got: Vec<u16> = {
let out = r.run(&gray16(w, h, &mut data)).unwrap();
bytemuck::cast_slice::<u8, u16>(out.as_raw_u8()).to_vec()
};
for (&s, &g) in src.iter().zip(&got) {
let want = ((s as i128 * 65535 + 2047) / 4095).min(u16::MAX as i128) as u16;
assert_eq!(g, want);
}
}
#[test]
fn scale_pixels_float_matches_and_saturates() {
let (w, h) = (8, 4);
let mut a: Vec<u16> = (0..(w * h) as u16)
.map(|v| v.saturating_mul(3000))
.collect();
let mut b = a.clone();
let run = |p: Pipeline, buf: &mut [u16]| -> Vec<u16> {
let mut r = p
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let out = r.run(&gray16(w, h, buf)).unwrap();
bytemuck::cast_slice::<u8, u16>(out.as_raw_u8()).to_vec()
};
let rat = run(Pipeline::new().scale_rational(2, 1), &mut a);
let flt = run(Pipeline::new().scale_by(2.0), &mut b);
assert_eq!(rat, flt);
assert_eq!(*rat.last().unwrap(), u16::MAX); }
#[test]
fn scale_pixels_zero_denominator_errors() {
let err = Pipeline::new().scale_rational(1, 0).compile(
ImageSpec::new(4, 4, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
);
assert!(matches!(err, Err(PipelineError::BadScaleFactor)));
}
#[test]
fn scale_pixels_tiles_identically() {
let (w, h) = (16, 12);
let pat = BayerPattern::Rggb;
let build = || {
Pipeline::new()
.debayer(DemosaicMethod::Linear)
.scale_rational(3, 2)
.scale_by(1.1)
.to_luma()
.convert(PixelType::U8)
};
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let mut seq = build().compile(spec(), Strategy::Sequential).unwrap();
let mut f0 = sample16(w * h);
let want = seq
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
for strat in [
Strategy::tiled(3),
Strategy::tiled_parallel(4),
Strategy::tiled_2d(4, 6),
] {
let mut r = build().compile(spec(), strat).unwrap();
let mut f = sample16(w * h);
let got = r
.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want, "strat={strat:?}");
}
}
#[test]
fn recompile_adapts_to_new_shape() {
let pat = BayerPattern::Rggb;
let mut runner = Pipeline::new()
.debayer(DemosaicMethod::Linear)
.to_luma()
.compile(
ImageSpec::new(16, 16, ColorSpace::Bayer(pat), PixelType::U16),
Strategy::tiled(4),
)
.unwrap();
let mut f16 = sample16(16 * 16);
assert!(runner.run(&bayer_frame(16, 16, pat, &mut f16)).is_ok());
runner
.recompile(ImageSpec::new(
32,
24,
ColorSpace::Bayer(pat),
PixelType::U16,
))
.unwrap();
assert_eq!(runner.input_spec().width, 32);
let mut f32 = sample16(32 * 24);
let out = runner.run(&bayer_frame(32, 24, pat, &mut f32)).unwrap();
assert_eq!((out.width(), out.height()), (32, 24));
}
#[test]
fn pipeline_round_trips_through_json() {
let p = Pipeline::new()
.crop(1, 1, 6, 6)
.debayer(DemosaicMethod::Cubic)
.scale(1.25, -3.0)
.to_luma_custom(vec![0.2, 0.7, 0.1])
.convert(PixelType::U8)
.rotate_90();
let json = serde_json::to_string(&p).unwrap();
let back: Pipeline = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
let spec = ImageSpec::new(8, 8, ColorSpace::Bayer(BayerPattern::Rggb), PixelType::U16);
assert!(back.compile(spec, Strategy::Sequential).is_ok());
}
#[test]
#[cfg(not(feature = "grow"))]
fn rejects_wrong_input_shape() {
let spec = ImageSpec::new(8, 8, ColorSpace::Bayer(BayerPattern::Rggb), PixelType::U16);
let mut runner = Pipeline::new()
.debayer(DemosaicMethod::Nearest)
.compile(spec, Strategy::Sequential)
.unwrap();
let mut frame = vec![0u16; 36];
let img = bayer_frame(6, 6, BayerPattern::Rggb, &mut frame);
assert!(matches!(
runner.run(&img),
Err(PipelineError::InputMismatch { .. })
));
}
#[test]
#[cfg(feature = "grow")]
fn grow_feature_auto_recompiles() {
let pat = BayerPattern::Rggb;
let mut runner = Pipeline::new()
.debayer(DemosaicMethod::Nearest)
.compile(
ImageSpec::new(8, 8, ColorSpace::Bayer(pat), PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let mut frame = sample16(6 * 6);
let out = runner.run(&bayer_frame(6, 6, pat, &mut frame)).unwrap();
assert_eq!((out.width(), out.height()), (6, 6));
assert_eq!(runner.input_spec().width, 6);
}
#[test]
fn tiny_frame_falls_back_to_sequential() {
let spec = ImageSpec::new(8, 8, ColorSpace::Bayer(BayerPattern::Rggb), PixelType::U16);
let runner = Pipeline::new()
.debayer(DemosaicMethod::Cubic)
.compile(spec, Strategy::tiled(2))
.unwrap();
assert!(!runner.is_tiled());
}
#[test]
fn rejects_invalid_chain_at_compile() {
let spec = ImageSpec::new(8, 8, ColorSpace::Gray, PixelType::U8);
let err = Pipeline::new()
.debayer(DemosaicMethod::Nearest)
.compile(spec, Strategy::Sequential)
.unwrap_err();
assert!(matches!(err, PipelineError::NotBayer));
}
const FILTERS: [ResizeFilter; 3] = [
ResizeFilter::Bilinear,
ResizeFilter::Bicubic,
ResizeFilter::Lanczos3,
];
#[test]
fn resize_to_fit_computes_bounded_aspect_dims() {
let cases = [
(100, 40, 50, 50, 50, 20), (40, 100, 50, 50, 20, 50), (30, 30, 50, 80, 50, 50), (200, 100, 200, 100, 200, 100), (7, 3, 4, 4, 4, 2), ];
for (sw, sh, bw, bh, ww, wh) in cases {
let runner = Pipeline::new()
.resize_to_fit(bw, bh, ResizeFilter::Bilinear)
.compile(
ImageSpec::new(sw, sh, ColorSpace::Gray, PixelType::U8),
Strategy::Sequential,
)
.unwrap();
let out = runner.output_spec();
assert_eq!(
(out.width, out.height),
(ww, wh),
"{sw}x{sh} -> box {bw}x{bh}"
);
assert!(out.width <= bw && out.height <= bh);
}
}
#[test]
fn resize_to_fit_scale_one_is_identity() {
let (w, h) = (9, 7);
for cs in [ColorSpace::Gray, ColorSpace::Rgb] {
let ch = if cs == ColorSpace::Rgb { 3 } else { 1 };
for filter in FILTERS {
let src = sample16(w * h * ch);
let mut frame = src.clone();
let img = DynamicImageRef::from(ImageRef::new(&mut frame, w, h, cs.clone()).unwrap());
let mut runner = Pipeline::new()
.resize_to_fit(w, h, filter)
.compile(
ImageSpec::new(w, h, cs.clone(), PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let out = runner.run(&img).unwrap();
assert_eq!(
bytemuck::cast_slice::<u8, u16>(out.as_raw_u8()),
&src[..],
"{cs:?} / {filter:?}"
);
}
}
}
#[test]
fn resize_to_fit_preserves_constant_image() {
let (w, h) = (37, 29);
for (bw, bh) in [(20, 16), (80, 62), (37, 29)] {
for filter in FILTERS {
let mut frame = vec![1234u16; w * h];
let img = gray16(w, h, &mut frame);
let mut runner = Pipeline::new()
.resize_to_fit(bw, bh, filter)
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let out = runner.run(&img).unwrap();
let px = bytemuck::cast_slice::<u8, u16>(out.as_raw_u8());
assert!(
px.iter().all(|&v| v == 1234),
"box {bw}x{bh} / {filter:?}: {:?}",
&px[..px.len().min(8)]
);
}
}
}
#[test]
fn resize_to_fit_downscale_gradient() {
let src: Vec<f32> = (0..16).map(|i| i as f32 / 15.0).collect();
let mut frame = src.clone();
let img = DynamicImageRef::from(ImageRef::new(&mut frame, 16, 1, ColorSpace::Gray).unwrap());
let mut runner = Pipeline::new()
.resize_to_fit(4, 1, ResizeFilter::Bilinear)
.compile(
ImageSpec::new(16, 1, ColorSpace::Gray, PixelType::F32),
Strategy::Sequential,
)
.unwrap();
let out = runner.run(&img).unwrap();
let px = bytemuck::cast_slice::<u8, f32>(out.as_raw_u8());
assert_eq!(px.len(), 4);
assert!(px.windows(2).all(|w| w[0] < w[1]), "monotonic: {px:?}");
assert!(px.iter().all(|&v| (0.0..=1.0).contains(&v)));
assert!((px[0] + px[3] - 1.0).abs() < 1e-3, "antisymmetric: {px:?}");
}
#[test]
fn resize_to_fit_is_deterministic() {
let (w, h) = (300usize, 220usize);
let src = sample16(w * h * 3);
let build = || {
Pipeline::new()
.resize_to_fit(97, 71, ResizeFilter::Lanczos3)
.compile(
ImageSpec::new(w, h, ColorSpace::Rgb, PixelType::U16),
Strategy::Sequential,
)
.unwrap()
};
let mut first = build();
let mut f0 = src.clone();
let want = first
.run(&DynamicImageRef::from(
ImageRef::new(&mut f0, w, h, ColorSpace::Rgb).unwrap(),
))
.unwrap()
.as_raw_u8()
.to_vec();
for _ in 0..8 {
let mut r = build();
let mut f = src.clone();
let got = r
.run(&DynamicImageRef::from(
ImageRef::new(&mut f, w, h, ColorSpace::Rgb).unwrap(),
))
.unwrap()
.as_raw_u8()
.to_vec();
assert_eq!(got, want);
}
}
#[test]
fn resize_to_fit_composes_with_geometry() {
let (w, h) = (40usize, 24usize);
let src: Vec<u16> = (0..(w * h) as u16).collect();
let run = |p: Pipeline| {
let mut r = p
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let mut f = src.clone();
let out = r.run(&gray16(w, h, &mut f)).unwrap();
(out.width(), out.height())
};
assert_eq!(
run(Pipeline::new()
.resize_to_fit(20, 20, ResizeFilter::Bicubic)
.rotate_90()),
(12, 20)
);
assert_eq!(
run(Pipeline::new().flip_vertical().rotate_90().resize_to_fit(
10,
30,
ResizeFilter::Bilinear
)),
(10, 17) );
assert_eq!(
run(Pipeline::new()
.crop(4, 0, 32, 24)
.resize_to_fit(16, 16, ResizeFilter::Lanczos3)
.flip_horizontal()),
(16, 12)
);
}
#[test]
fn resize_to_fit_rejects_bayer() {
let bayer = ImageSpec::new(
16,
16,
ColorSpace::Bayer(BayerPattern::Rggb),
PixelType::U16,
);
let err = Pipeline::new()
.resize_to_fit(8, 8, ResizeFilter::Bicubic)
.compile(bayer.clone(), Strategy::Sequential)
.unwrap_err();
assert!(matches!(err, PipelineError::ResizeOnBayer));
assert!(
Pipeline::new()
.debayer(DemosaicMethod::Linear)
.resize_to_fit(8, 8, ResizeFilter::Bicubic)
.compile(bayer, Strategy::Sequential)
.is_ok()
);
}
#[test]
fn resize_to_fit_runs_after_tiled_body() {
let (w, h) = (64, 48);
let mut frame = sample16(w * h);
let img = bayer_frame(w, h, BayerPattern::Rggb, &mut frame);
let mut runner = Pipeline::new()
.debayer(DemosaicMethod::Linear)
.to_luma()
.resize_to_fit(20, 20, ResizeFilter::Lanczos3)
.convert(PixelType::U8)
.compile(
ImageSpec::new(w, h, ColorSpace::Bayer(BayerPattern::Rggb), PixelType::U16),
Strategy::tiled_parallel(16),
)
.unwrap();
assert!(runner.is_tiled());
let out = runner.run(&img).unwrap();
assert_eq!((out.width(), out.height()), (20, 15));
assert_eq!(out.color_space(), ColorSpace::Gray);
assert_eq!(out.pixel_type(), PixelType::U8);
}
#[test]
fn resize_op_round_trips_through_serde() {
let p = Pipeline::new().resize_to_fit(320, 240, ResizeFilter::Lanczos3);
let json = serde_json::to_string(&p).unwrap();
assert_eq!(p, serde_json::from_str::<Pipeline>(&json).unwrap());
}
#[test]
fn geometry_splits_the_chain_into_multiple_tiled_passes() {
let (w, h) = (96usize, 72usize);
let spec = || ImageSpec::new(w, h, ColorSpace::Rgb, PixelType::U16);
let r = Pipeline::new()
.to_luma()
.convert(PixelType::U8)
.compile(spec(), Strategy::tiled(8))
.unwrap();
assert_eq!(r.tiled_pass_count(), 1);
let r = Pipeline::new()
.scale(1.1, 0.0)
.resize_to_fit(60, 60, ResizeFilter::Bilinear)
.to_luma()
.convert(PixelType::U8)
.compile(spec(), Strategy::tiled(8))
.unwrap();
assert_eq!(r.tiled_pass_count(), 2);
let r = Pipeline::new()
.scale(1.0, 1.0)
.flip_vertical()
.to_luma()
.rotate_180()
.convert(PixelType::U8)
.compile(spec(), Strategy::tiled(8))
.unwrap();
assert_eq!(r.tiled_pass_count(), 3);
let r = Pipeline::new()
.resize_to_fit(60, 60, ResizeFilter::Bilinear)
.to_luma()
.compile(spec(), Strategy::Sequential)
.unwrap();
assert_eq!(r.tiled_pass_count(), 0);
}
#[test]
fn segmented_tiling_matches_sequential() {
let (w, h) = (64usize, 48usize);
let pat = BayerPattern::Rggb;
let spec = || ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let chains: Vec<(&str, Pipeline)> = vec![
(
"debayer luma | flip_v | scale convert",
Pipeline::new()
.debayer(DemosaicMethod::Linear)
.to_luma()
.flip_vertical()
.scale(1.3, 2.0)
.convert(PixelType::U8),
),
(
"debayer luma | resize | scale convert",
Pipeline::new()
.debayer(DemosaicMethod::Cubic)
.to_luma()
.resize_to_fit(40, 40, ResizeFilter::Lanczos3)
.scale(0.9, 1.0)
.convert(PixelType::U8),
),
(
"debayer | rotate_90 | luma convert | crop",
Pipeline::new()
.debayer(DemosaicMethod::Nearest)
.rotate_90()
.to_luma()
.convert(PixelType::U8)
.crop(2, 3, 30, 40),
),
(
"crop | debayer luma | resize | convert | rotate_180",
Pipeline::new()
.crop(4, 2, 52, 40)
.debayer(DemosaicMethod::Linear)
.to_luma()
.resize_to_fit(24, 24, ResizeFilter::Bicubic)
.convert(PixelType::U8)
.rotate_180(),
),
];
for (name, p) in chains {
let mut seq = p.clone().compile(spec(), Strategy::Sequential).unwrap();
let mut f0 = sample16(w * h);
let want = seq
.run(&bayer_frame(w, h, pat, &mut f0))
.unwrap()
.as_raw_u8()
.to_vec();
for strat in [
Strategy::tiled(6),
Strategy::tiled(13),
Strategy::tiled_parallel(8),
Strategy::tiled_2d(10, 20),
Strategy::tiled_2d_parallel(7, 15),
] {
let mut r = p.clone().compile(spec(), strat).unwrap();
let got = {
let mut f = sample16(w * h);
r.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec()
};
assert_eq!(got, want, "{name} / {strat:?}");
}
}
}
fn run_gray_u16(p: &Pipeline, w: usize, h: usize, src: &[u16]) -> (usize, usize, Vec<u16>) {
let mut runner = p
.clone()
.compile(
ImageSpec::new(w, h, ColorSpace::Gray, PixelType::U16),
Strategy::Sequential,
)
.unwrap();
let mut f = src.to_vec();
let out = runner.run(&gray16(w, h, &mut f)).unwrap();
(
out.width(),
out.height(),
bytemuck::cast_slice::<u8, u16>(out.as_raw_u8()).to_vec(),
)
}
#[test]
fn optimize_dihedral_runs_preserve_output() {
let dih = [
Op::FlipHorizontal,
Op::FlipVertical,
Op::Rotate90,
Op::Rotate180,
Op::Rotate270,
];
let (w, h) = (5usize, 3usize);
let src: Vec<u16> = (0..(w * h) as u16).collect();
let mut chains: Vec<Vec<Op>> = vec![vec![]];
for _ in 0..3 {
let mut next = chains.clone();
for c in &chains {
if c.len() == 3 {
continue;
}
for op in &dih {
let mut nc = c.clone();
nc.push(op.clone());
next.push(nc);
}
}
chains = next;
}
for chain in &chains {
let base = chain.iter().cloned().fold(Pipeline::new(), Pipeline::push);
let opt = base.clone().optimize();
assert!(opt.ops().len() <= 2, "{chain:?} -> {:?}", opt.ops());
assert!(!opt.ops().iter().any(|o| matches!(o, Op::Nop)), "{chain:?}");
assert_eq!(opt.clone().optimize().ops(), opt.ops(), "{chain:?}");
let want = run_gray_u16(&base, w, h, &src);
let got = run_gray_u16(&opt, w, h, &src);
assert_eq!(got, want, "{chain:?} -> {:?}", opt.ops());
}
}
#[test]
fn optimize_folds_known_identities() {
let f = |p: Pipeline| p.optimize().ops().to_vec();
assert_eq!(
f(Pipeline::new().flip_horizontal().flip_horizontal()),
vec![]
);
assert_eq!(f(Pipeline::new().rotate_90().rotate_270()), vec![]);
assert_eq!(
f(Pipeline::new().rotate_90().rotate_90()),
vec![Op::Rotate180]
);
assert_eq!(
f(Pipeline::new().rotate_90().rotate_90().rotate_90()),
vec![Op::Rotate270]
);
assert_eq!(
f(Pipeline::new().flip_horizontal().flip_vertical()),
vec![Op::Rotate180]
);
assert_eq!(f(Pipeline::new().rotate_90()), vec![Op::Rotate90]);
}
#[test]
fn optimize_merges_nested_crops() {
let p = Pipeline::new()
.crop(4, 4, 20, 20)
.crop(2, 3, 8, 6)
.crop(1, 1, 4, 4)
.optimize();
assert_eq!(
p.ops(),
&[Op::Crop {
x: 7,
y: 8,
width: 4,
height: 4,
}]
);
let (w, h) = (40usize, 40usize);
let src: Vec<u16> = (0..(w * h) as u16).collect();
let base = Pipeline::new()
.crop(4, 4, 20, 20)
.crop(2, 3, 8, 6)
.crop(1, 1, 4, 4);
assert_eq!(
run_gray_u16(&p, w, h, &src),
run_gray_u16(&base, w, h, &src)
);
}
#[test]
fn optimize_drops_nops_and_redundant_stages() {
let p = Pipeline::new()
.push(Op::Nop)
.to_luma()
.to_luma_custom([0.2, 0.7, 0.1])
.push(Op::Nop)
.convert(PixelType::U8)
.convert(PixelType::U8)
.optimize();
assert_eq!(p.ops(), &[Op::ToLuma, Op::Convert(PixelType::U8)]);
}
#[test]
fn optimize_dihedral_preserves_bayer_output() {
let (w, h) = (16usize, 12usize);
let pat = BayerPattern::Grbg;
let base = Pipeline::new()
.flip_horizontal()
.rotate_180()
.flip_vertical()
.flip_horizontal()
.debayer(DemosaicMethod::Linear)
.to_luma();
let opt = base.clone().optimize();
assert_eq!(opt.ops()[0], Op::FlipHorizontal);
let spec = ImageSpec::new(w, h, ColorSpace::Bayer(pat), PixelType::U16);
let run = |p: &Pipeline| {
let mut r = p
.clone()
.compile(spec.clone(), Strategy::Sequential)
.unwrap();
let mut f = sample16(w * h);
r.run(&bayer_frame(w, h, pat, &mut f))
.unwrap()
.as_raw_u8()
.to_vec()
};
assert_eq!(run(&opt), run(&base));
}
#[test]
fn optimize_keeps_arithmetic_stages_unfused() {
let p = Pipeline::new().scale_by(2.0).scale_by(0.5).optimize();
assert_eq!(
p.ops(),
&[
Op::ScalePixels(ScaleFactor::Float(2.0)),
Op::ScalePixels(ScaleFactor::Float(0.5)),
]
);
}