use crate::error::Error;
use crate::image::{Image, ImageView};
use crate::{CoordinateF64, PixelDistance, Sigma, Size};
pub trait PyramidLevel {
type Pixel: Copy;
fn as_image(&self) -> &Image<Self::Pixel>;
}
impl<P: Copy> PyramidLevel for Image<P> {
type Pixel = P;
#[inline]
fn as_image(&self) -> &Image<P> {
self
}
}
#[derive(Clone, Debug)]
pub struct Pyramid<L: PyramidLevel> {
levels: Vec<L>,
}
impl<L: PyramidLevel> Pyramid<L> {
pub fn try_from_levels(levels: Vec<L>) -> Result<Self, Error> {
if levels.is_empty() {
return Err(Error::EmptyPyramid);
}
for (index, pair) in levels.windows(2).enumerate() {
let previous = pair[0].as_image().size();
let current = pair[1].as_image().size();
if current.width > previous.width || current.height > previous.height {
return Err(Error::PyramidLevelOrder {
index: index + 1,
previous,
current,
});
}
}
Ok(Self { levels })
}
pub fn depth(&self) -> usize {
self.levels.len()
}
pub fn level(&self, index: usize) -> &L {
&self.levels[index]
}
pub fn get(&self, index: usize) -> Option<&L> {
self.levels.get(index)
}
pub fn finest(&self) -> &L {
&self.levels[0]
}
pub fn coarsest(&self) -> &L {
self.levels.last().expect("pyramid is empty")
}
pub fn iter(&self) -> impl Iterator<Item = &L> {
self.levels.iter()
}
}
pub type GaussianPyramid<P> = Pyramid<Image<P>>;
pub trait Decimated: PyramidLevel {
fn pixel_distance(&self) -> PixelDistance;
fn origin_offset(&self) -> CoordinateF64;
fn to_base(&self, local: CoordinateF64) -> CoordinateF64 {
let d = self.pixel_distance().get();
let o = self.origin_offset();
CoordinateF64::new(o.x + d * local.x, o.y + d * local.y)
}
fn to_local(&self, base: CoordinateF64) -> CoordinateF64 {
let d = self.pixel_distance().get();
let o = self.origin_offset();
CoordinateF64::new((base.x - o.x) / d, (base.y - o.y) / d)
}
}
pub trait ScaleLevel: PyramidLevel {
fn sigma(&self) -> Sigma;
}
#[derive(Clone, Debug)]
pub struct ScaledImage<P: Copy> {
image: Image<P>,
pixel_distance: PixelDistance,
origin_offset: OriginOffset,
sigma: Sigma,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct OriginOffset(CoordinateF64);
impl OriginOffset {
pub const ZERO: Self = Self(CoordinateF64 { x: 0.0, y: 0.0 });
#[must_use]
pub const fn new(x: f64, y: f64) -> Option<Self> {
if x.is_finite() && y.is_finite() {
Some(Self(CoordinateF64 { x, y }))
} else {
None
}
}
pub fn try_new(x: f64, y: f64) -> Result<Self, Error> {
Self::new(x, y).ok_or_else(|| {
Error::InvalidParameter(format!(
"origin offset must be finite along both axes, got ({x}, {y})"
))
})
}
#[inline]
#[must_use]
pub const fn get(self) -> CoordinateF64 {
self.0
}
}
impl<P: Copy> ScaledImage<P> {
pub fn new(
image: Image<P>,
pixel_distance: PixelDistance,
origin_offset: OriginOffset,
sigma: Sigma,
) -> Self {
Self {
image,
pixel_distance,
origin_offset,
sigma,
}
}
pub fn image(&self) -> &Image<P> {
&self.image
}
pub fn size(&self) -> Size {
self.image.size()
}
pub fn into_image(self) -> Image<P> {
self.image
}
}
impl<P: Copy> PyramidLevel for ScaledImage<P> {
type Pixel = P;
#[inline]
fn as_image(&self) -> &Image<P> {
&self.image
}
}
impl<P: Copy> Decimated for ScaledImage<P> {
#[inline]
fn pixel_distance(&self) -> PixelDistance {
self.pixel_distance
}
#[inline]
fn origin_offset(&self) -> CoordinateF64 {
self.origin_offset.get()
}
}
impl<P: Copy> ScaleLevel for ScaledImage<P> {
#[inline]
fn sigma(&self) -> Sigma {
self.sigma
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pixel::{Mono8, MonoF32};
use crate::{pixel_distance, sigma};
fn two_level_pyramid() -> Pyramid<Image<Mono8>> {
Pyramid::try_from_levels(vec![
Image::fill(8, 6, Mono8::new(10)),
Image::fill(4, 3, Mono8::new(20)),
])
.unwrap()
}
#[test]
fn image_is_its_own_level() {
let img: Image<Mono8> = Image::fill(5, 4, Mono8::new(7));
let view: &Image<Mono8> = img.as_image();
assert_eq!(view.size(), Size::new(5, 4));
assert_eq!(view.pixel_at(0, 0), Mono8::new(7));
}
#[test]
fn from_levels_and_depth() {
let p = two_level_pyramid();
assert_eq!(p.depth(), 2);
}
#[test]
fn try_from_levels_empty_is_error() {
let result = Pyramid::<Image<Mono8>>::try_from_levels(vec![]);
assert_eq!(result.err().unwrap(), Error::EmptyPyramid);
}
#[test]
fn try_from_levels_rejects_growing_levels() {
let result = Pyramid::try_from_levels(vec![
Image::fill(4, 3, Mono8::new(0)),
Image::fill(8, 6, Mono8::new(0)),
]);
assert_eq!(
result.err().unwrap(),
Error::PyramidLevelOrder {
index: 1,
previous: Size::new(4, 3),
current: Size::new(8, 6),
}
);
}
#[test]
fn try_from_levels_rejects_single_growing_axis() {
let result = Pyramid::try_from_levels(vec![
Image::fill(8, 6, Mono8::new(0)),
Image::fill(4, 7, Mono8::new(0)),
]);
assert_eq!(
result.err().unwrap(),
Error::PyramidLevelOrder {
index: 1,
previous: Size::new(8, 6),
current: Size::new(4, 7),
}
);
}
#[test]
fn try_from_levels_allows_equal_sizes() {
let p = Pyramid::try_from_levels(vec![
Image::fill(8, 8, Mono8::new(1)),
Image::fill(8, 8, Mono8::new(2)),
Image::fill(4, 4, Mono8::new(3)),
])
.unwrap();
assert_eq!(p.depth(), 3);
}
#[test]
fn level_returns_by_index() {
let p = two_level_pyramid();
assert_eq!(p.level(0).size(), Size::new(8, 6));
assert_eq!(p.level(1).size(), Size::new(4, 3));
}
#[test]
#[should_panic]
fn level_out_of_bounds_panics() {
let p = two_level_pyramid();
let _ = p.level(2);
}
#[test]
fn get_returns_option() {
let p = two_level_pyramid();
assert!(p.get(0).is_some());
assert!(p.get(1).is_some());
assert!(p.get(2).is_none());
}
#[test]
fn finest_and_coarsest() {
let p = two_level_pyramid();
assert_eq!(p.finest().size(), Size::new(8, 6));
assert_eq!(p.coarsest().size(), Size::new(4, 3));
}
#[test]
fn finest_equals_coarsest_for_single_level() {
let p = Pyramid::try_from_levels(vec![Image::fill(3, 3, Mono8::new(1))]).unwrap();
assert_eq!(p.finest().size(), p.coarsest().size());
assert_eq!(p.depth(), 1);
}
#[test]
fn iter_goes_finest_to_coarsest() {
let p = two_level_pyramid();
let sizes: Vec<Size> = p.iter().map(|l| l.size()).collect();
assert_eq!(sizes, [Size::new(8, 6), Size::new(4, 3)]);
}
#[test]
fn pyramid_is_clone() {
let p = two_level_pyramid();
let q = p.clone();
assert_eq!(q.depth(), p.depth());
assert_eq!(q.level(1).pixel_at(0, 0), Mono8::new(20));
}
#[test]
fn gaussian_pyramid_alias_is_plain_pyramid() {
let p: GaussianPyramid<Mono8> = two_level_pyramid();
assert_eq!(p.depth(), 2);
}
#[test]
fn scaled_image_accessors() {
let level = ScaledImage::new(
Image::fill(4, 4, MonoF32::new(0.5)),
pixel_distance!(2.0),
OriginOffset::ZERO,
sigma!(1.0),
);
assert_eq!(level.size(), Size::new(4, 4));
assert_eq!(level.image().pixel_at(1, 1), MonoF32::new(0.5));
assert_eq!(level.pixel_distance(), pixel_distance!(2.0));
assert_eq!(level.origin_offset(), CoordinateF64::new(0.0, 0.0));
assert_eq!(level.sigma(), sigma!(1.0));
let img = level.into_image();
assert_eq!(img.size(), Size::new(4, 4));
}
#[test]
fn to_base_even_sample_convention() {
let level = ScaledImage::new(
Image::<MonoF32>::zero(4, 4),
pixel_distance!(2.0),
OriginOffset::ZERO,
sigma!(1.0),
);
assert_eq!(
level.to_base(CoordinateF64::new(0.0, 0.0)),
CoordinateF64::new(0.0, 0.0)
);
assert_eq!(
level.to_base(CoordinateF64::new(1.5, 3.0)),
CoordinateF64::new(3.0, 6.0)
);
}
#[test]
fn to_base_area_average_convention() {
let level = ScaledImage::new(
Image::<MonoF32>::zero(4, 4),
pixel_distance!(2.0),
OriginOffset::new(0.5, 0.5).unwrap(),
sigma!(1.0),
);
assert_eq!(
level.to_base(CoordinateF64::new(0.0, 0.0)),
CoordinateF64::new(0.5, 0.5)
);
assert_eq!(
level.to_base(CoordinateF64::new(2.0, 1.0)),
CoordinateF64::new(4.5, 2.5)
);
}
#[test]
fn to_base_upsampled_level() {
let level = ScaledImage::new(
Image::<MonoF32>::zero(16, 16),
pixel_distance!(0.5),
OriginOffset::ZERO,
sigma!(0.8),
);
assert_eq!(
level.to_base(CoordinateF64::new(6.0, 10.0)),
CoordinateF64::new(3.0, 5.0)
);
}
#[test]
fn to_local_inverts_to_base() {
let level = ScaledImage::new(
Image::<MonoF32>::zero(4, 4),
pixel_distance!(2.0),
OriginOffset::new(0.5, 0.5).unwrap(),
sigma!(1.0),
);
for local in [
CoordinateF64::new(0.0, 0.0),
CoordinateF64::new(1.5, 3.0),
CoordinateF64::new(3.25, 0.75),
] {
assert_eq!(level.to_local(level.to_base(local)), local);
}
}
#[test]
fn to_local_projects_base_coordinates_into_the_level() {
let level = ScaledImage::new(
Image::<MonoF32>::zero(4, 4),
pixel_distance!(2.0),
OriginOffset::ZERO,
sigma!(1.0),
);
assert_eq!(
level.to_local(CoordinateF64::new(6.0, 8.0)),
CoordinateF64::new(3.0, 4.0)
);
assert_eq!(
level.to_local(CoordinateF64::new(3.0, 1.0)),
CoordinateF64::new(1.5, 0.5)
);
}
#[test]
fn to_local_on_an_upsampled_level() {
let level = ScaledImage::new(
Image::<MonoF32>::zero(16, 16),
pixel_distance!(0.5),
OriginOffset::ZERO,
sigma!(0.8),
);
assert_eq!(
level.to_local(CoordinateF64::new(3.0, 5.0)),
CoordinateF64::new(6.0, 10.0)
);
}
#[test]
fn scaled_pyramid_composes() {
let levels = vec![
ScaledImage::new(
Image::<MonoF32>::zero(8, 8),
pixel_distance!(1.0),
OriginOffset::ZERO,
sigma!(0.5),
),
ScaledImage::new(
Image::<MonoF32>::zero(4, 4),
pixel_distance!(2.0),
OriginOffset::ZERO,
sigma!(1.0),
),
];
let p = Pyramid::try_from_levels(levels).unwrap();
assert_eq!(p.depth(), 2);
assert_eq!(p.level(1).pixel_distance(), pixel_distance!(2.0));
assert_eq!(p.level(1).sigma(), sigma!(1.0));
}
}