use crate::Size;
use crate::border::Mirror;
use crate::error::Error;
use crate::image::{Image, ImageView, ImageViewMut, Pyramid, RasterImage, SeparableKernel};
use crate::pixel::{FromLinear, LinearPixel, LinearSpace, ZeroablePixel};
use crate::transform::convolve_separable::convolve_separable;
const PYR_UP_WEIGHTS: [f32; 5] = [0.125, 0.5, 0.75, 0.5, 0.125];
const PYR_UP_WEIGHTS_UNDOUBLED: [f32; 5] = [0.0625, 0.25, 0.375, 0.25, 0.0625];
#[must_use]
pub fn pyr_down<I, P, Acc>(image: &I) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: LinearPixel<f32, Accumulator = Acc> + LinearSpace + ZeroablePixel + FromLinear<Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
{
let blurred: Image<P> = convolve_separable(image, &SeparableKernel::gaussian_5(), &Mirror);
let out_width = image.width().div_ceil(2);
let out_height = image.height().div_ceil(2);
Image::generate(out_width, out_height, |x, y| blurred.pixel_at(2 * x, 2 * y))
}
pub fn pyr_up<I, P, Acc>(image: &I, target: Size) -> Result<Image<P>, Error>
where
I: RasterImage<Pixel = P>,
P: LinearPixel<f32, Accumulator = Acc> + LinearSpace + ZeroablePixel + FromLinear<Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
{
fn doubles(target: usize, dim: usize) -> bool {
match dim.checked_mul(2) {
Some(two) => target == two || target.checked_add(1) == Some(two),
None => false,
}
}
let (w, h) = (image.width(), image.height());
if !doubles(target.width, w) || !doubles(target.height, h) {
return Err(Error::InvalidPyrUpTarget {
source: image.size(),
target,
});
}
let mut upsampled = Image::<P>::zero(target.width, target.height);
for y in 0..h {
let row = image.row(y);
for (x, &pixel) in row.iter().enumerate() {
*upsampled.pixel_at_mut(2 * x, 2 * y) = pixel;
}
}
let h_weights = if target.width == w {
PYR_UP_WEIGHTS_UNDOUBLED
} else {
PYR_UP_WEIGHTS
};
let v_weights = if target.height == h {
PYR_UP_WEIGHTS_UNDOUBLED
} else {
PYR_UP_WEIGHTS
};
let kernel = SeparableKernel::new(h_weights, v_weights);
Ok(convolve_separable(&upsampled, &kernel, &Mirror))
}
pub trait PyramidMethod<P: Copy> {
type Level: crate::image::PyramidLevel;
fn build<I>(&self, image: &I, max_depth: usize) -> Pyramid<Self::Level>
where
I: RasterImage<Pixel = P>;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Gaussian;
impl<P, Acc> PyramidMethod<P> for Gaussian
where
P: LinearPixel<f32, Accumulator = Acc> + LinearSpace + ZeroablePixel + FromLinear<Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
{
type Level = Image<P>;
fn build<I>(&self, image: &I, max_depth: usize) -> Pyramid<Image<P>>
where
I: RasterImage<Pixel = P>,
{
let resolved = max_depth.max(1);
let base = {
let mut data = Vec::with_capacity(image.width() * image.height());
for y in 0..image.height() {
data.extend_from_slice(image.row(y));
}
Image::from_vec(image.width(), image.height(), data)
.expect("rows fill width * height exactly")
};
let mut levels = vec![base];
while levels.len() < resolved {
let prev = levels.last().expect("levels start non-empty");
let Size { width, height } = prev.size();
if width <= 1 && height <= 1 || width == 0 || height == 0 {
break;
}
let next = pyr_down(prev);
levels.push(next);
}
Pyramid::try_from_levels(levels)
.expect("Gaussian::build produces non-empty, strictly shrinking levels")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CoordinateF64;
use crate::image::{Decimated, OriginOffset, PyramidLevel, ScaledImage};
use crate::pixel::{Mono8, MonoF32};
use crate::{pixel_distance, sigma};
#[test]
fn pyr_down_even_dimensions_halve() {
let src = Image::fill(8, 6, MonoF32::new(0.0));
let out: Image<MonoF32> = pyr_down(&src);
assert_eq!(out.size(), Size::new(4, 3));
}
#[test]
fn pyr_down_odd_dimensions_use_ceiling_division() {
let src = Image::fill(7, 5, MonoF32::new(0.0));
let out: Image<MonoF32> = pyr_down(&src);
assert_eq!(out.size(), Size::new(4, 3));
}
#[test]
fn pyr_down_one_pixel_image_stays_one_pixel() {
let src = Image::fill(1, 1, MonoF32::new(0.3));
let out: Image<MonoF32> = pyr_down(&src);
assert_eq!(out.size(), Size::new(1, 1));
assert!((out.pixel_at(0, 0).0 - 0.3).abs() < 1e-6);
}
#[test]
fn pyr_down_single_row_and_column() {
let row = Image::fill(9, 1, MonoF32::new(0.5));
let out: Image<MonoF32> = pyr_down(&row);
assert_eq!(out.size(), Size::new(5, 1));
let col = Image::fill(1, 8, MonoF32::new(0.5));
let out: Image<MonoF32> = pyr_down(&col);
assert_eq!(out.size(), Size::new(1, 4));
}
#[test]
fn pyr_down_flat_image_preserves_brightness() {
let src = Image::fill(10, 10, MonoF32::new(0.7));
let out: Image<MonoF32> = pyr_down(&src);
for y in 0..out.height() {
for x in 0..out.width() {
assert!(
(out.pixel_at(x, y).0 - 0.7).abs() < 1e-6,
"flat value drifted at ({x}, {y}): {}",
out.pixel_at(x, y).0
);
}
}
}
#[test]
fn pyr_down_flat_mono8_preserves_brightness() {
let src = Image::fill(12, 8, Mono8::new(100));
let out: Image<Mono8> = pyr_down(&src);
for y in 0..out.height() {
for x in 0..out.width() {
assert_eq!(out.pixel_at(x, y), Mono8::new(100));
}
}
}
#[test]
fn pyr_down_keeps_even_samples() {
let src = Image::generate(16, 16, |x, _| MonoF32::new(x as f32));
let out: Image<MonoF32> = pyr_down(&src);
for y in 2..out.height() - 2 {
for x in 2..out.width() - 2 {
assert!(
(out.pixel_at(x, y).0 - 2.0 * x as f32).abs() < 1e-4,
"expected even sample 2·{x} at ({x}, {y}), got {}",
out.pixel_at(x, y).0
);
}
}
}
#[test]
fn pyr_down_impulse_center_weight() {
let src = Image::generate(9, 9, |x, y| {
if x == 4 && y == 4 {
MonoF32::new(1.0)
} else {
MonoF32::new(0.0)
}
});
let out: Image<MonoF32> = pyr_down(&src);
assert!((out.pixel_at(2, 2).0 - 0.140625).abs() < 1e-6);
}
#[test]
fn pyr_up_accepts_both_valid_widths() {
let src = Image::fill(4, 4, MonoF32::new(0.5));
let a: Image<MonoF32> = pyr_up(&src, Size::new(8, 8)).unwrap();
assert_eq!(a.size(), Size::new(8, 8));
let b: Image<MonoF32> = pyr_up(&src, Size::new(7, 7)).unwrap();
assert_eq!(b.size(), Size::new(7, 7));
}
#[test]
fn pyr_up_handles_one_pixel_sources() {
let dot = Image::fill(1, 1, MonoF32::new(0.5));
let up: Image<MonoF32> = pyr_up(&dot, Size::new(2, 2)).unwrap();
assert_eq!(up.size(), Size::new(2, 2));
let same: Image<MonoF32> = pyr_up(&dot, Size::new(1, 1)).unwrap();
assert_eq!(same.size(), Size::new(1, 1));
let bar = Image::fill(1, 4, MonoF32::new(0.5));
let up: Image<MonoF32> = pyr_up(&bar, Size::new(2, 8)).unwrap();
assert_eq!(up.size(), Size::new(2, 8));
let odd: Image<MonoF32> = pyr_up(&bar, Size::new(1, 7)).unwrap();
assert_eq!(odd.size(), Size::new(1, 7));
for y in 0..odd.height() {
let v = odd.pixel_at(0, y).0;
assert!((0.0..=0.5 + 1e-6).contains(&v), "({y}) = {v}");
}
}
#[test]
fn pyr_up_rejects_extreme_sizes_without_overflowing() {
let wide: Image<MonoF32> = Image::zero(usize::MAX, 0);
let result: Result<Image<MonoF32>, Error> = pyr_up(&wide, Size::new(4, 4));
assert!(result.is_err());
let src = Image::fill(4, 4, MonoF32::new(0.5));
let result: Result<Image<MonoF32>, Error> = pyr_up(&src, Size::new(usize::MAX, usize::MAX));
assert!(result.is_err());
}
#[test]
fn pyr_up_rejects_invalid_targets() {
let src = Image::fill(4, 4, MonoF32::new(0.5));
for target in [Size::new(9, 8), Size::new(6, 8), Size::new(8, 10)] {
let result: Result<Image<MonoF32>, Error> = pyr_up(&src, target);
assert_eq!(
result.unwrap_err(),
Error::InvalidPyrUpTarget {
source: Size::new(4, 4),
target,
},
"target {target:?} must be rejected"
);
}
}
#[test]
fn pyr_up_round_trips_odd_sizes() {
let src = Image::fill(9, 7, MonoF32::new(0.25));
let half: Image<MonoF32> = pyr_down(&src);
assert_eq!(half.size(), Size::new(5, 4));
let restored: Image<MonoF32> = pyr_up(&half, src.size()).unwrap();
assert_eq!(restored.size(), src.size());
}
#[test]
fn pyr_up_flat_image_preserves_brightness() {
let src = Image::fill(5, 4, MonoF32::new(0.6));
for target in [Size::new(10, 8), Size::new(9, 7)] {
let out: Image<MonoF32> = pyr_up(&src, target).unwrap();
for y in 0..out.height() {
for x in 0..out.width() {
assert!(
(out.pixel_at(x, y).0 - 0.6).abs() < 1e-6,
"flat value drifted at ({x}, {y}) for target {target:?}: {}",
out.pixel_at(x, y).0
);
}
}
}
}
#[test]
fn pyr_up_impulse_spreads_interpolation_weights() {
let src = Image::generate(3, 3, |x, y| {
if x == 1 && y == 1 {
MonoF32::new(1.0)
} else {
MonoF32::new(0.0)
}
});
let out: Image<MonoF32> = pyr_up(&src, Size::new(6, 6)).unwrap();
assert!((out.pixel_at(2, 2).0 - 0.5625).abs() < 1e-6);
assert!((out.pixel_at(3, 2).0 - 0.375).abs() < 1e-6);
assert!((out.pixel_at(3, 3).0 - 0.25).abs() < 1e-6);
}
#[test]
fn pyr_up_mono8_flat() {
let src = Image::fill(6, 6, Mono8::new(80));
let out: Image<Mono8> = pyr_up(&src, Size::new(12, 12)).unwrap();
for y in 0..out.height() {
for x in 0..out.width() {
assert_eq!(out.pixel_at(x, y), Mono8::new(80));
}
}
}
#[test]
fn gaussian_build_level_zero_is_the_input() {
let src = Image::generate(8, 8, |x, y| MonoF32::new((x + y) as f32));
let pyramid = Gaussian.build(&src, 3);
let level0 = pyramid.finest();
for y in 0..src.height() {
for x in 0..src.width() {
assert_eq!(level0.pixel_at(x, y), src.pixel_at(x, y));
}
}
}
#[test]
fn gaussian_build_halves_each_level() {
let src = Image::fill(20, 12, MonoF32::new(0.5));
let pyramid = Gaussian.build(&src, 3);
let sizes: Vec<Size> = pyramid.iter().map(|l| l.size()).collect();
assert_eq!(
sizes,
[Size::new(20, 12), Size::new(10, 6), Size::new(5, 3)]
);
}
#[test]
fn gaussian_build_clamps_depth_on_small_images() {
let src = Image::fill(4, 4, MonoF32::new(0.5));
let pyramid = Gaussian.build(&src, 100);
assert_eq!(pyramid.depth(), 3);
assert_eq!(pyramid.coarsest().size(), Size::new(1, 1));
}
#[test]
fn gaussian_build_max_depth_zero_yields_base_level() {
let src = Image::fill(8, 8, MonoF32::new(0.5));
let pyramid = Gaussian.build(&src, 0);
assert_eq!(pyramid.depth(), 1);
assert_eq!(pyramid.finest().size(), Size::new(8, 8));
}
#[test]
fn gaussian_build_respects_requested_depth() {
let src = Image::fill(64, 64, MonoF32::new(0.5));
let pyramid = Gaussian.build(&src, 3);
assert_eq!(pyramid.depth(), 3);
}
#[test]
fn gaussian_build_flat_stays_flat_at_every_level() {
let src = Image::fill(16, 16, MonoF32::new(0.4));
let pyramid = Gaussian.build(&src, 5);
for (i, level) in pyramid.iter().enumerate() {
for y in 0..level.height() {
for x in 0..level.width() {
assert!(
(level.pixel_at(x, y).0 - 0.4).abs() < 1e-5,
"level {i} drifted at ({x}, {y})"
);
}
}
}
}
#[test]
fn gaussian_build_mono8() {
let src = Image::fill(16, 12, Mono8::new(200));
let pyramid = Gaussian.build(&src, 3);
assert_eq!(pyramid.depth(), 3);
assert_eq!(pyramid.coarsest().size(), Size::new(4, 3));
assert_eq!(pyramid.coarsest().pixel_at(0, 0), Mono8::new(200));
}
#[test]
fn decimated_lift_recovers_base_position() {
let (cx, cy) = (12.0f32, 8.0f32);
let src = Image::generate(33, 25, |x, y| {
let dx = x as f32 - cx;
let dy = y as f32 - cy;
MonoF32::new((-(dx * dx + dy * dy) / 18.0).exp())
});
let mut level: Image<MonoF32> = pyr_down(&src);
level = pyr_down(&level);
let scaled = ScaledImage::new(
level,
pixel_distance!(4.0),
OriginOffset::ZERO,
sigma!(2.236),
);
let img = scaled.as_image();
let mut best = (0usize, 0usize, f32::MIN);
for y in 0..img.height() {
for x in 0..img.width() {
let v = img.pixel_at(x, y).0;
if v > best.2 {
best = (x, y, v);
}
}
}
let lifted = scaled.to_base(CoordinateF64::new(best.0 as f64, best.1 as f64));
assert_eq!(lifted, CoordinateF64::new(f64::from(cx), f64::from(cy)));
}
}