use crate::border::{BorderPolicy, FullFrameBorder};
use crate::image::Kernel;
use crate::image::{Image, ImageView, RasterImage, RasterImageMut};
use crate::pixel::ZeroablePixel;
use crate::transform::combine::{PixelSubtract, combine_images};
use crate::transform::map_neighborhood::MapItem;
use crate::transform::map_neighborhood::{MapOp, map_neighborhood, map_neighborhood_into};
pub(crate) struct ErodeOp;
impl<P: Copy + Ord> MapOp<P> for ErodeOp {
type Accumulator = P;
type Output = P;
#[inline(always)]
fn init(&self, center: P) -> P {
center
}
#[inline(always)]
fn accumulate(&self, acc: &mut P, item: MapItem<P>) {
*acc = (*acc).min(item.pixel);
}
#[inline(always)]
fn finalize(&mut self, acc: P) -> P {
acc
}
}
pub(crate) struct DilateOp;
impl<P: Copy + Ord> MapOp<P> for DilateOp {
type Accumulator = P;
type Output = P;
#[inline(always)]
fn init(&self, center: P) -> P {
center
}
#[inline(always)]
fn accumulate(&self, acc: &mut P, item: MapItem<P>) {
*acc = (*acc).max(item.pixel);
}
#[inline(always)]
fn finalize(&mut self, acc: P) -> P {
acc
}
}
struct MedianOp<P> {
buf: Vec<P>,
}
impl<P: Copy + Ord> MapOp<P> for MedianOp<P> {
type Accumulator = Vec<P>;
type Output = P;
const INVERTIBLE: bool = false;
fn init(&self, _center: P) -> Vec<P> {
Vec::new()
}
fn accumulate(&self, acc: &mut Vec<P>, item: MapItem<P>) {
acc.push(item.pixel);
}
fn finalize(&mut self, mut acc: Vec<P>) -> P {
assert!(
!acc.is_empty(),
"median_filter: mask must have at least one active position"
);
acc.sort_unstable();
acc[acc.len() / 2]
}
fn map<I>(&mut self, _center: P, neighbors: I) -> P
where
I: Iterator<Item = MapItem<P>>,
{
self.buf.clear();
self.buf.extend(neighbors.map(|n| n.pixel));
assert!(
!self.buf.is_empty(),
"median_filter: mask must have at least one active position"
);
self.buf.sort_unstable();
self.buf[self.buf.len() / 2]
}
}
pub fn erode_into<I, K, B, O, P>(image: &I, kernel: &K, border: &B, output: &mut O)
where
I: RasterImage<Pixel = P>,
P: Copy + Ord,
K: Kernel<Weight = bool>,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = P>,
{
map_neighborhood_into(
image,
kernel.weights(),
kernel.anchor(),
border,
output,
ErodeOp,
);
}
#[must_use]
pub fn erode<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I>,
{
map_neighborhood(image, kernel.weights(), kernel.anchor(), border, ErodeOp)
}
pub fn dilate_into<I, K, B, O, P>(image: &I, kernel: &K, border: &B, output: &mut O)
where
I: RasterImage<Pixel = P>,
P: Copy + Ord,
K: Kernel<Weight = bool>,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = P>,
{
map_neighborhood_into(
image,
kernel.weights(),
kernel.anchor(),
border,
output,
DilateOp,
);
}
#[must_use]
pub fn dilate<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I>,
{
map_neighborhood(image, kernel.weights(), kernel.anchor(), border, DilateOp)
}
pub fn opening_into<I, K, B, O, P>(
image: &I,
kernel: &K,
border: &B,
output: &mut O,
scratch: &mut Image<P>,
) where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I> + BorderPolicy<Image<P>>,
O: RasterImageMut<Pixel = P>,
{
erode_into(image, kernel, border, scratch);
dilate_into(scratch, kernel, border, output);
}
pub fn closing_into<I, K, B, O, P>(
image: &I,
kernel: &K,
border: &B,
output: &mut O,
scratch: &mut Image<P>,
) where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I> + BorderPolicy<Image<P>>,
O: RasterImageMut<Pixel = P>,
{
dilate_into(image, kernel, border, scratch);
erode_into(scratch, kernel, border, output);
}
#[must_use]
pub fn opening<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I> + BorderPolicy<Image<P>>,
{
let eroded = erode(image, kernel, border);
dilate(&eroded, kernel, border)
}
#[must_use]
pub fn closing<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I> + BorderPolicy<Image<P>>,
{
let dilated = dilate(image, kernel, border);
erode(&dilated, kernel, border)
}
#[must_use]
pub fn morphological_gradient<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel + core::ops::Sub<Output = P>,
K: Kernel<Weight = bool>,
B: BorderPolicy<I> + BorderPolicy<Image<P>>,
{
let dilated = dilate(image, kernel, border);
let eroded = erode(image, kernel, border);
combine_images(&dilated, &eroded, PixelSubtract)
.expect("internal: dilated and eroded are always produced from the same source image and have matching sizes")
}
#[must_use]
pub fn top_hat<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel + core::ops::Sub<Output = P>,
K: Kernel<Weight = bool>,
B: FullFrameBorder<I> + BorderPolicy<Image<P>>,
{
let opened = opening(image, kernel, border);
combine_images(image, &opened, PixelSubtract)
.expect("internal: opened is produced from image and always has the same size")
}
#[must_use]
pub fn black_hat<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel + core::ops::Sub<Output = P>,
K: Kernel<Weight = bool>,
B: FullFrameBorder<I> + BorderPolicy<Image<P>>,
{
let closed = closing(image, kernel, border);
combine_images(&closed, image, PixelSubtract)
.expect("internal: closed is produced from image and always has the same size")
}
#[must_use]
pub fn median_filter<I, K, B, P>(image: &I, kernel: &K, border: &B) -> Image<P>
where
I: RasterImage<Pixel = P>,
P: Copy + Ord + ZeroablePixel,
K: Kernel<Weight = bool>,
B: BorderPolicy<I>,
{
let mask_size = kernel.weights().size();
let capacity = mask_size.width * mask_size.height;
map_neighborhood(
image,
kernel.weights(),
kernel.anchor(),
border,
MedianOp {
buf: Vec::with_capacity(capacity),
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::border::Clamp;
use crate::image::{ImageViewMut, Neighborhood};
fn make_5x5_gradient() -> Image<u8> {
Image::generate(5, 5, |x, y| (x + y * 5) as u8)
}
#[test]
fn erode_uniform_is_identity() {
let src = Image::fill(6, 6, 42u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = erode(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 42);
}
}
}
#[test]
fn dilate_uniform_is_identity() {
let src = Image::fill(6, 6, 42u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = dilate(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 42);
}
}
}
#[test]
fn erode_gradient_center_pixel() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = erode(&src, &se, &Clamp);
assert_eq!(result.pixel_at(2, 2), 6);
}
#[test]
fn dilate_gradient_center_pixel() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = dilate(&src, &se, &Clamp);
assert_eq!(result.pixel_at(2, 2), 18);
}
#[test]
fn erode_le_original_le_dilate() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let eroded: Image<u8> = erode(&src, &se, &Clamp);
let dilated: Image<u8> = dilate(&src, &se, &Clamp);
for y in 0..src.height() {
for x in 0..src.width() {
assert!(
eroded.pixel_at(x, y) <= src.pixel_at(x, y),
"erode > original at ({x}, {y})",
);
assert!(
src.pixel_at(x, y) <= dilated.pixel_at(x, y),
"original > dilate at ({x}, {y})",
);
}
}
}
#[test]
fn opening_le_original() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let opened: Image<u8> = opening(&src, &se, &Clamp);
for y in 0..src.height() {
for x in 0..src.width() {
assert!(
opened.pixel_at(x, y) <= src.pixel_at(x, y),
"opening > original at ({x}, {y}): {} > {}",
opened.pixel_at(x, y),
src.pixel_at(x, y),
);
}
}
}
#[test]
fn closing_ge_original() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let closed: Image<u8> = closing(&src, &se, &Clamp);
for y in 0..src.height() {
for x in 0..src.width() {
assert!(
closed.pixel_at(x, y) >= src.pixel_at(x, y),
"closing < original at ({x}, {y}): {} < {}",
closed.pixel_at(x, y),
src.pixel_at(x, y),
);
}
}
}
#[test]
fn opening_uniform_is_identity() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = opening(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 50);
}
}
}
#[test]
fn closing_uniform_is_identity() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = closing(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 50);
}
}
}
#[test]
fn morphological_gradient_uniform_is_zero() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = morphological_gradient(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 0);
}
}
}
#[test]
fn morphological_gradient_equals_dilate_minus_erode() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let gradient: Image<u8> = morphological_gradient(&src, &se, &Clamp);
let dilated: Image<u8> = dilate(&src, &se, &Clamp);
let eroded: Image<u8> = erode(&src, &se, &Clamp);
for y in 0..gradient.height() {
for x in 0..gradient.width() {
let expected = dilated.pixel_at(x, y) - eroded.pixel_at(x, y);
assert_eq!(
gradient.pixel_at(x, y),
expected,
"gradient mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn top_hat_uniform_is_zero() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = top_hat(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 0);
}
}
}
#[test]
fn black_hat_uniform_is_zero() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = black_hat(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 0);
}
}
}
#[test]
fn top_hat_equals_original_minus_opening() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let th: Image<u8> = top_hat(&src, &se, &Clamp);
let opened: Image<u8> = opening(&src, &se, &Clamp);
for y in 0..th.height() {
for x in 0..th.width() {
let expected = src.pixel_at(x, y) - opened.pixel_at(x, y);
assert_eq!(
th.pixel_at(x, y),
expected,
"top-hat mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn black_hat_equals_closing_minus_original() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let bh: Image<u8> = black_hat(&src, &se, &Clamp);
let closed: Image<u8> = closing(&src, &se, &Clamp);
for y in 0..bh.height() {
for x in 0..bh.width() {
let expected = closed.pixel_at(x, y) - src.pixel_at(x, y);
assert_eq!(
bh.pixel_at(x, y),
expected,
"black-hat mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn erode_with_cross_se() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::cross_3x3();
let result: Image<u8> = erode(&src, &se, &Clamp);
assert_eq!(result.pixel_at(2, 2), 7);
}
#[test]
fn dilate_with_cross_se() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::cross_3x3();
let result: Image<u8> = dilate(&src, &se, &Clamp);
assert_eq!(result.pixel_at(2, 2), 17);
}
#[test]
fn erode_5x5_full_rect() {
let src = Image::generate(7, 7, |x, y| (x + y * 7) as u8);
let se = Neighborhood::<bool, 5, 5>::full_rect_5x5();
let result: Image<u8> = erode(&src, &se, &Clamp);
assert_eq!(result.pixel_at(3, 3), 8);
}
#[test]
fn dilate_5x5_full_rect() {
let src = Image::generate(7, 7, |x, y| (x + y * 7) as u8);
let se = Neighborhood::<bool, 5, 5>::full_rect_5x5();
let result: Image<u8> = dilate(&src, &se, &Clamp);
assert_eq!(result.pixel_at(3, 3), 40);
}
#[test]
fn erode_into_matches_erode() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let expected: Image<u8> = erode(&src, &se, &Clamp);
let border = Clamp;
let out_region = <Clamp as BorderPolicy<Image<u8>>>::output_region(
&border,
src.size(),
se.weights().size(),
se.anchor(),
);
let mut into_result = Image::<u8>::zero(out_region.size.width, out_region.size.height);
erode_into(&src, &se, &border, &mut into_result);
for y in 0..expected.height() {
for x in 0..expected.width() {
assert_eq!(
expected.pixel_at(x, y),
into_result.pixel_at(x, y),
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn dilate_into_matches_dilate() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let expected: Image<u8> = dilate(&src, &se, &Clamp);
let border = Clamp;
let out_region = <Clamp as BorderPolicy<Image<u8>>>::output_region(
&border,
src.size(),
se.weights().size(),
se.anchor(),
);
let mut into_result = Image::<u8>::zero(out_region.size.width, out_region.size.height);
dilate_into(&src, &se, &border, &mut into_result);
for y in 0..expected.height() {
for x in 0..expected.width() {
assert_eq!(
expected.pixel_at(x, y),
into_result.pixel_at(x, y),
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn erode_single_pixel() {
let src = Image::fill(1, 1, 99u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = erode(&src, &se, &Clamp);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert_eq!(result.pixel_at(0, 0), 99);
}
#[test]
fn dilate_single_pixel() {
let src = Image::fill(1, 1, 99u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = dilate(&src, &se, &Clamp);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert_eq!(result.pixel_at(0, 0), 99);
}
#[test]
fn opening_is_idempotent() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let once: Image<u8> = opening(&src, &se, &Clamp);
let twice: Image<u8> = opening(&once, &se, &Clamp);
for y in 0..once.height() {
for x in 0..once.width() {
assert_eq!(
once.pixel_at(x, y),
twice.pixel_at(x, y),
"opening not idempotent at ({x}, {y})",
);
}
}
}
#[test]
fn closing_is_idempotent() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let once: Image<u8> = closing(&src, &se, &Clamp);
let twice: Image<u8> = closing(&once, &se, &Clamp);
for y in 0..once.height() {
for x in 0..once.width() {
assert_eq!(
once.pixel_at(x, y),
twice.pixel_at(x, y),
"closing not idempotent at ({x}, {y})",
);
}
}
}
#[test]
fn morphological_gradient_nonnegative() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = morphological_gradient(&src, &se, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
let _ = result.pixel_at(x, y);
}
}
}
#[test]
fn opening_removes_single_bright_pixel() {
let mut src = Image::fill(5, 5, 10u8);
*src.pixel_at_mut(2, 2) = 200;
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = opening(&src, &se, &Clamp);
assert!(
result.pixel_at(2, 2) <= 10,
"opening should remove single bright pixel, got {}",
result.pixel_at(2, 2),
);
}
#[test]
fn closing_fills_single_dark_pixel() {
let mut src = Image::fill(5, 5, 200u8);
*src.pixel_at_mut(2, 2) = 10;
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = closing(&src, &se, &Clamp);
assert!(
result.pixel_at(2, 2) >= 200,
"closing should fill single dark pixel, got {}",
result.pixel_at(2, 2),
);
}
#[test]
fn median_filter_uniform_is_identity() {
let src = Image::fill(6, 6, 42u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = median_filter(&src, &se, &Clamp);
assert_eq!(result.width(), 6);
assert_eq!(result.height(), 6);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), 42);
}
}
}
#[test]
fn median_filter_removes_isolated_bright_spike() {
let mut src = Image::fill(5, 5, 50u8);
*src.pixel_at_mut(2, 2) = 200;
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = median_filter(&src, &se, &Clamp);
assert_eq!(result.pixel_at(2, 2), 50);
}
#[test]
fn median_filter_known_3x3_value() {
let src = Image::generate(3, 3, |x, y| (x + 1 + y * 3) as u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
use crate::border::Skip;
let result: Image<u8> = median_filter(&src, &se, &Skip);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert_eq!(result.pixel_at(0, 0), 5);
}
#[test]
fn median_filter_skip_output_size() {
let src = Image::fill(7, 7, 0u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
use crate::border::Skip;
let result: Image<u8> = median_filter(&src, &se, &Skip);
assert_eq!(result.width(), 5);
assert_eq!(result.height(), 5);
}
#[test]
fn median_filter_matches_sort_by_hand() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> = median_filter(&src, &se, &Clamp);
assert_eq!(result.pixel_at(2, 2), 12);
}
#[test]
#[should_panic(expected = "mask must have at least one active position")]
fn median_filter_empty_mask_panics() {
let src = Image::fill(5, 5, 50u8);
let empty_mask = Neighborhood::<bool, 3, 3>::new([false; 9]);
let _: Image<u8> = median_filter(&src, &empty_mask, &Clamp);
}
#[test]
fn opening_into_matches_opening() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let expected: Image<u8> = opening(&src, &se, &Clamp);
let border = Clamp;
let out_region = <Clamp as BorderPolicy<Image<u8>>>::output_region(
&border,
src.size(),
se.weights().size(),
se.anchor(),
);
let mut into_result = Image::<u8>::zero(out_region.size.width, out_region.size.height);
let mut scratch = Image::<u8>::zero(out_region.size.width, out_region.size.height);
opening_into(&src, &se, &border, &mut into_result, &mut scratch);
for y in 0..expected.height() {
for x in 0..expected.width() {
assert_eq!(
expected.pixel_at(x, y),
into_result.pixel_at(x, y),
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn closing_into_matches_closing() {
let src = make_5x5_gradient();
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let expected: Image<u8> = closing(&src, &se, &Clamp);
let border = Clamp;
let out_region = <Clamp as BorderPolicy<Image<u8>>>::output_region(
&border,
src.size(),
se.weights().size(),
se.anchor(),
);
let mut into_result = Image::<u8>::zero(out_region.size.width, out_region.size.height);
let mut scratch = Image::<u8>::zero(out_region.size.width, out_region.size.height);
closing_into(&src, &se, &border, &mut into_result, &mut scratch);
for y in 0..expected.height() {
for x in 0..expected.width() {
assert_eq!(
expected.pixel_at(x, y),
into_result.pixel_at(x, y),
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn opening_into_uniform_is_identity() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let mut output = Image::<u8>::zero(6, 6);
let mut scratch = Image::<u8>::zero(6, 6);
opening_into(&src, &se, &Clamp, &mut output, &mut scratch);
for y in 0..output.height() {
for x in 0..output.width() {
assert_eq!(output.pixel_at(x, y), 50);
}
}
}
#[test]
fn closing_into_uniform_is_identity() {
let src = Image::fill(6, 6, 50u8);
let se = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let mut output = Image::<u8>::zero(6, 6);
let mut scratch = Image::<u8>::zero(6, 6);
closing_into(&src, &se, &Clamp, &mut output, &mut scratch);
for y in 0..output.height() {
for x in 0..output.width() {
assert_eq!(output.pixel_at(x, y), 50);
}
}
}
}