use crate::error::Error;
use crate::image::{Image, ImageView, RasterImage};
use crate::{CoordinateF64, PixelDistance, Sigma, Size};
pub trait PyramidLevel: RasterImage {
fn as_image(&self) -> &Image<Self::Pixel>;
}
impl<P: Copy> PyramidLevel for Image<P> {
#[inline]
fn as_image(&self) -> &Image<P> {
self
}
}
pub trait Pyramid {
type Level: PyramidLevel;
fn depth(&self) -> usize;
fn get(&self, index: usize) -> Option<&Self::Level>;
fn level(&self, index: usize) -> &Self::Level {
let depth = self.depth();
self.get(index).unwrap_or_else(|| {
panic!("no pyramid level {index} at depth {depth} (contract clause 2: agreement)")
})
}
fn finest(&self) -> &Self::Level {
self.get(0)
.expect("pyramid is empty (contract clause 1: non-empty)")
}
fn coarsest(&self) -> &Self::Level {
let last = self
.depth()
.checked_sub(1)
.expect("pyramid is empty (contract clause 1: non-empty)");
self.get(last)
.expect("coarsest index is below depth (contract clause 2: agreement)")
}
fn iter(&self) -> impl Iterator<Item = &Self::Level> {
(0..self.depth()).map(move |index| self.level(index))
}
}
#[derive(Clone, Debug)]
pub struct LevelChain<L: PyramidLevel> {
levels: Vec<L>,
}
impl<L: PyramidLevel> LevelChain<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 into_levels(self) -> Vec<L> {
self.levels
}
}
impl<L: PyramidLevel> IntoIterator for LevelChain<L> {
type Item = L;
type IntoIter = std::vec::IntoIter<L>;
fn into_iter(self) -> Self::IntoIter {
self.levels.into_iter()
}
}
impl<L: PyramidLevel> Pyramid for LevelChain<L> {
type Level = L;
fn depth(&self) -> usize {
self.levels.len()
}
fn get(&self, index: usize) -> Option<&L> {
self.levels.get(index)
}
fn iter(&self) -> impl Iterator<Item = &L> {
self.levels.iter()
}
}
#[derive(Clone, Debug)]
pub struct Dyadic<C: Pyramid>(C);
impl<C: Pyramid> Dyadic<C> {
pub fn try_new(inner: C) -> Result<Self, Error> {
for index in 1..inner.depth() {
let parent = inner.level(index - 1).as_image().size();
let child = inner.level(index).as_image().size();
if child.width != halved(parent.width) || child.height != halved(parent.height) {
return Err(Error::NotDyadic {
index,
parent,
child,
});
}
}
Ok(Self(inner))
}
pub(crate) fn new_unchecked(inner: C) -> Self {
Self(inner)
}
pub fn into_inner(self) -> C {
self.0
}
}
impl<C: Pyramid> Pyramid for Dyadic<C> {
type Level = C::Level;
fn depth(&self) -> usize {
self.0.depth()
}
fn get(&self, index: usize) -> Option<&C::Level> {
self.0.get(index)
}
fn iter(&self) -> impl Iterator<Item = &C::Level> {
self.0.iter()
}
}
impl<C: Pyramid + IntoIterator> IntoIterator for Dyadic<C> {
type Item = C::Item;
type IntoIter = C::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
fn halved(dim: usize) -> usize {
dim / 2 + dim % 2
}
pub type PlacedPyramid<P> = Dyadic<LevelChain<PlacedImage<P>>>;
pub type ScaledPyramid<P> = Dyadic<LevelChain<ScaledImage<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 into_image(self) -> Image<P> {
self.image
}
}
impl<P: Copy> ImageView for ScaledImage<P> {
type Pixel = P;
#[inline]
fn size(&self) -> Size {
self.image.size()
}
#[inline]
fn pixel_at(&self, x: usize, y: usize) -> P {
self.image.pixel_at(x, y)
}
}
impl<P: Copy> RasterImage for ScaledImage<P> {
#[inline]
fn row(&self, y: usize) -> &[P] {
self.image.row(y)
}
}
impl<P: Copy> PyramidLevel for ScaledImage<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
}
}
#[derive(Clone, Debug)]
pub struct PlacedImage<P: Copy> {
image: Image<P>,
pixel_distance: PixelDistance,
origin_offset: OriginOffset,
}
impl<P: Copy> PlacedImage<P> {
pub fn new(
image: Image<P>,
pixel_distance: PixelDistance,
origin_offset: OriginOffset,
) -> Self {
Self {
image,
pixel_distance,
origin_offset,
}
}
pub fn image(&self) -> &Image<P> {
&self.image
}
pub fn into_image(self) -> Image<P> {
self.image
}
pub fn with_sigma(self, sigma: Sigma) -> ScaledImage<P> {
ScaledImage::new(self.image, self.pixel_distance, self.origin_offset, sigma)
}
}
impl<P: Copy> ImageView for PlacedImage<P> {
type Pixel = P;
#[inline]
fn size(&self) -> Size {
self.image.size()
}
#[inline]
fn pixel_at(&self, x: usize, y: usize) -> P {
self.image.pixel_at(x, y)
}
}
impl<P: Copy> RasterImage for PlacedImage<P> {
#[inline]
fn row(&self, y: usize) -> &[P] {
self.image.row(y)
}
}
impl<P: Copy> PyramidLevel for PlacedImage<P> {
#[inline]
fn as_image(&self) -> &Image<P> {
&self.image
}
}
impl<P: Copy> Decimated for PlacedImage<P> {
#[inline]
fn pixel_distance(&self) -> PixelDistance {
self.pixel_distance
}
#[inline]
fn origin_offset(&self) -> CoordinateF64 {
self.origin_offset.get()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pixel::{Mono8, MonoF32};
use crate::{pixel_distance, sigma};
fn two_level_pyramid() -> LevelChain<Image<Mono8>> {
LevelChain::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 = LevelChain::<Image<Mono8>>::try_from_levels(vec![]);
assert_eq!(result.err().unwrap(), Error::EmptyPyramid);
}
#[test]
fn try_from_levels_rejects_growing_levels() {
let result = LevelChain::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 = LevelChain::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 = LevelChain::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(expected = "no pyramid level 2 at depth 2")]
fn level_out_of_bounds_panics() {
let p = two_level_pyramid();
let _ = p.level(2);
}
struct NoLevels;
impl Pyramid for NoLevels {
type Level = Image<Mono8>;
fn depth(&self) -> usize {
0
}
fn get(&self, _index: usize) -> Option<&Image<Mono8>> {
None
}
}
#[test]
#[should_panic(expected = "contract clause 1")]
fn finest_names_the_clause_an_empty_implementor_broke() {
let _ = NoLevels.finest();
}
#[test]
#[should_panic(expected = "contract clause 1")]
fn coarsest_names_the_clause_an_empty_implementor_broke() {
let _ = NoLevels.coarsest();
}
struct MinimalPyramid(Vec<Image<Mono8>>);
impl Pyramid for MinimalPyramid {
type Level = Image<Mono8>;
fn depth(&self) -> usize {
self.0.len()
}
fn get(&self, index: usize) -> Option<&Image<Mono8>> {
self.0.get(index)
}
}
#[test]
fn the_provided_accessors_derive_from_depth_and_get() {
let p = MinimalPyramid(vec![
Image::fill(8, 6, Mono8::new(10)),
Image::fill(4, 3, Mono8::new(20)),
]);
assert_eq!(p.level(1).size(), Size::new(4, 3));
assert_eq!(p.finest().size(), Size::new(8, 6));
assert_eq!(p.coarsest().size(), Size::new(4, 3));
let widths: Vec<usize> = p.iter().map(|l| l.size().width).collect();
assert_eq!(widths, [8, 4]);
}
#[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 = LevelChain::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 the_capability_aliases_name_dyadic_chains() {
let placed: PlacedPyramid<Mono8> = Dyadic::try_new(
LevelChain::try_from_levels(vec![
PlacedImage::new(
Image::fill(8, 6, Mono8::new(10)),
pixel_distance!(1.0),
OriginOffset::ZERO,
),
PlacedImage::new(
Image::fill(4, 3, Mono8::new(20)),
pixel_distance!(2.0),
OriginOffset::ZERO,
),
])
.unwrap(),
)
.unwrap();
assert_eq!(placed.depth(), 2);
assert_eq!(placed.level(1).pixel_distance().get(), 2.0);
let scaled: ScaledPyramid<Mono8> = Dyadic::try_new(
LevelChain::try_from_levels(vec![ScaledImage::new(
Image::fill(8, 6, Mono8::new(10)),
pixel_distance!(1.0),
OriginOffset::ZERO,
sigma!(0.5),
)])
.unwrap(),
)
.unwrap();
assert_eq!(scaled.finest().sigma().get(), 0.5);
}
#[test]
fn into_levels_hands_out_the_levels() {
let levels = two_level_pyramid().into_levels();
assert_eq!(levels.len(), 2);
assert_eq!(levels[0].size(), Size::new(8, 6));
assert_eq!(levels[1].pixel_at(0, 0), Mono8::new(20));
}
#[test]
fn a_chain_can_be_consumed_by_a_for_loop() {
let mut widths = Vec::new();
for level in two_level_pyramid() {
widths.push(level.size().width);
}
assert_eq!(widths, [8, 4]);
}
#[test]
fn a_dyadic_pyramid_can_be_consumed_by_a_for_loop() {
let pyramid = Dyadic::try_new(two_level_pyramid()).unwrap();
let widths: Vec<usize> = pyramid.into_iter().map(|l| l.size().width).collect();
assert_eq!(widths, [8, 4]);
}
#[test]
fn a_level_survives_the_chain_it_came_from() {
let base = two_level_pyramid()
.into_levels()
.into_iter()
.next()
.expect("a chain is never empty");
assert_eq!(base.size(), Size::new(8, 6));
}
#[test]
fn try_new_accepts_a_halving_chain() {
let pyramid = Dyadic::try_new(two_level_pyramid()).unwrap();
assert_eq!(pyramid.depth(), 2);
assert_eq!(pyramid.level(1).size(), Size::new(4, 3));
}
#[test]
fn try_new_accepts_an_odd_parent() {
let chain = LevelChain::try_from_levels(vec![
Image::fill(9, 7, Mono8::new(1)),
Image::fill(5, 4, Mono8::new(1)),
Image::fill(3, 2, Mono8::new(1)),
])
.unwrap();
assert!(Dyadic::try_new(chain).is_ok());
}
#[test]
fn try_new_accepts_a_single_level() {
let chain = LevelChain::try_from_levels(vec![Image::fill(7, 5, Mono8::new(1))]).unwrap();
assert!(Dyadic::try_new(chain).is_ok());
}
#[test]
fn try_new_rejects_equal_size_neighbours() {
let chain = LevelChain::try_from_levels(vec![
Image::fill(8, 6, Mono8::new(1)),
Image::fill(8, 6, Mono8::new(1)),
])
.unwrap();
assert_eq!(
Dyadic::try_new(chain).err().unwrap(),
Error::NotDyadic {
index: 1,
parent: Size::new(8, 6),
child: Size::new(8, 6),
}
);
}
#[test]
fn try_new_rejects_a_chain_that_shrinks_without_halving() {
let chain = LevelChain::try_from_levels(vec![
Image::fill(100, 68, Mono8::new(1)),
Image::fill(30, 34, Mono8::new(1)),
])
.unwrap();
assert!(matches!(
Dyadic::try_new(chain),
Err(Error::NotDyadic { index: 1, .. })
));
}
#[test]
fn try_new_names_the_first_offending_index() {
let chain = LevelChain::try_from_levels(vec![
Image::fill(16, 16, Mono8::new(1)),
Image::fill(8, 8, Mono8::new(1)),
Image::fill(3, 4, Mono8::new(1)),
])
.unwrap();
assert!(matches!(
Dyadic::try_new(chain),
Err(Error::NotDyadic { index: 2, .. })
));
}
#[test]
fn dyadic_forwards_the_pyramid_accessors() {
let pyramid = Dyadic::try_new(two_level_pyramid()).unwrap();
assert_eq!(pyramid.finest().size(), Size::new(8, 6));
assert_eq!(pyramid.coarsest().size(), Size::new(4, 3));
assert!(pyramid.get(2).is_none());
let widths: Vec<usize> = pyramid.iter().map(|l| l.size().width).collect();
assert_eq!(widths, [8, 4]);
}
#[test]
fn into_inner_returns_the_wrapped_container() {
let pyramid = Dyadic::try_new(two_level_pyramid()).unwrap();
let chain: LevelChain<Image<Mono8>> = pyramid.into_inner();
assert_eq!(chain.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 = LevelChain::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));
}
fn placed_level() -> PlacedImage<MonoF32> {
PlacedImage::new(
Image::generate(4, 3, |x, y| MonoF32::new((y * 4 + x) as f32)),
pixel_distance!(2.0),
OriginOffset::ZERO,
)
}
#[test]
fn placed_image_carries_its_grid_and_no_sigma() {
let level = placed_level();
assert_eq!(level.pixel_distance(), pixel_distance!(2.0));
assert_eq!(level.origin_offset(), CoordinateF64::new(0.0, 0.0));
assert_eq!(
level.to_base(CoordinateF64::new(3.0, 4.0)),
CoordinateF64::new(6.0, 8.0)
);
assert_eq!(
level.to_local(CoordinateF64::new(6.0, 8.0)),
CoordinateF64::new(3.0, 4.0)
);
}
#[test]
fn placed_image_answers_as_an_image() {
let level = placed_level();
assert_eq!(level.size(), Size::new(4, 3));
assert_eq!(level.width(), 4);
assert_eq!(level.pixel_at(2, 1), MonoF32::new(6.0));
assert_eq!(level.row(1)[2], MonoF32::new(6.0));
assert_eq!(level.as_image().size(), Size::new(4, 3));
}
#[test]
fn scaled_image_answers_as_an_image_too() {
let level = ScaledImage::new(
Image::generate(4, 3, |x, y| MonoF32::new((y * 4 + x) as f32)),
pixel_distance!(2.0),
OriginOffset::ZERO,
sigma!(1.0),
);
assert_eq!(level.size(), Size::new(4, 3));
assert_eq!(level.pixel_at(2, 1), MonoF32::new(6.0));
assert_eq!(level.row(1)[2], MonoF32::new(6.0));
}
#[test]
fn with_sigma_carries_the_geometry_over() {
let scaled = placed_level().with_sigma(sigma!(1.5));
assert_eq!(scaled.pixel_distance(), pixel_distance!(2.0));
assert_eq!(scaled.origin_offset(), CoordinateF64::new(0.0, 0.0));
assert_eq!(scaled.sigma(), sigma!(1.5));
assert_eq!(scaled.size(), Size::new(4, 3));
}
#[test]
fn into_image_drops_the_geometry() {
let image = placed_level().into_image();
assert_eq!(image.size(), Size::new(4, 3));
}
}