use crate::border::{BorderPolicy, compute_interior_region};
use crate::image::{Image, ImageView, RasterImage, RasterImageMut};
use crate::pixel::ZeroablePixel;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MapItem<P> {
pub pixel: P,
pub dx: isize,
pub dy: isize,
}
pub trait MapOp<P> {
type Accumulator;
type Output;
const INVERTIBLE: bool = true;
fn init(&self, center: P) -> Self::Accumulator;
fn accumulate(&self, acc: &mut Self::Accumulator, item: MapItem<P>);
fn finalize(&mut self, acc: Self::Accumulator) -> Self::Output;
fn map<I>(&mut self, center: P, neighbors: I) -> Self::Output
where
I: Iterator<Item = MapItem<P>>,
{
let mut acc = self.init(center);
for item in neighbors {
self.accumulate(&mut acc, item);
}
self.finalize(acc)
}
}
pub struct ClosureMap<F>(pub F);
impl<P, Out, F> MapOp<P> for ClosureMap<F>
where
F: FnMut(P, &mut dyn Iterator<Item = MapItem<P>>) -> Out,
{
type Accumulator = ();
type Output = Out;
const INVERTIBLE: bool = false;
#[inline(always)]
fn init(&self, _center: P) {}
#[inline(always)]
fn accumulate(&self, _acc: &mut (), _item: MapItem<P>) {}
#[inline(always)]
fn finalize(&mut self, _acc: ()) -> Out {
unreachable!("ClosureMap uses map() override, not init/accumulate/finalize")
}
#[inline(always)]
fn map<I>(&mut self, center: P, mut neighbors: I) -> Out
where
I: Iterator<Item = MapItem<P>>,
{
(self.0)(center, &mut neighbors)
}
}
pub fn map_neighborhood_into<I, MI, B, O, M, P>(
src: &I,
mask_weights: &MI,
anchor: (usize, usize),
border: &B,
output: &mut O,
mut op: M,
) where
I: RasterImage<Pixel = P>,
P: Copy,
MI: ImageView<Pixel = bool>,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = M::Output>,
M: MapOp<P>,
M::Output: Copy,
{
let mask_size = mask_weights.size();
let output_region = border.output_region(src.size(), mask_size, anchor);
let interior = compute_interior_region(src.size(), mask_size, anchor);
assert!(
output.width() >= output_region.size.width && output.height() >= output_region.size.height,
"output image {}×{} is too small for the output region {}×{}",
output.width(),
output.height(),
output_region.size.width,
output_region.size.height,
);
let mask_positions: Vec<(isize, isize)> = {
let mut positions = Vec::with_capacity(mask_size.width * mask_size.height);
for ky in 0..mask_size.height {
for kx in 0..mask_size.width {
if mask_weights.pixel_at(kx, ky) {
let dx = kx as isize - anchor.0 as isize;
let dy = ky as isize - anchor.1 as isize;
positions.push((dx, dy));
}
}
}
positions
};
let ox = output_region.left();
let oy = output_region.top();
if let Some(interior) = interior {
let int_left = interior.left().max(ox);
let int_top = interior.top().max(oy);
let int_right = interior.right().min(output_region.right());
let int_bottom = interior.bottom().min(output_region.bottom());
let int_width = int_right.saturating_sub(int_left);
if int_width > 0 && int_top < int_bottom {
if M::INVERTIBLE {
let first_center_row = src.row(int_top);
let mut acc_row: Vec<M::Accumulator> = (int_left..int_right)
.map(|cx| op.init(first_center_row[cx]))
.collect();
for cy in int_top..int_bottom {
let center_row = src.row(cy);
if cy > int_top {
let center_slice = ¢er_row[int_left..int_right];
for i in 0..int_width {
acc_row[i] = op.init(center_slice[i]);
}
}
for &(dx, dy) in &mask_positions {
let src_row = src.row((cy as isize + dy) as usize);
let start = (int_left as isize + dx) as usize;
let src_slice = &src_row[start..start + int_width];
for i in 0..int_width {
op.accumulate(
&mut acc_row[i],
MapItem {
pixel: src_slice[i],
dx,
dy,
},
);
}
}
let out_row = &mut output.row_mut(cy - oy)[int_left - ox..int_right - ox];
for i in 0..int_width {
let new_init = op.init(center_row[int_left + i]);
let acc = std::mem::replace(&mut acc_row[i], new_init);
out_row[i] = op.finalize(acc);
}
}
} else {
for cy in int_top..int_bottom {
for cx in int_left..int_right {
let center = src.pixel_at(cx, cy);
let iter = mask_positions.iter().map(|&(dx, dy)| {
let sx = (cx as isize + dx) as usize;
let sy = (cy as isize + dy) as usize;
MapItem {
pixel: src.pixel_at(sx, sy),
dx,
dy,
}
});
let result = op.map(center, iter);
*output.pixel_at_mut(cx - ox, cy - oy) = result;
}
}
}
}
}
for cy in output_region.top()..output_region.bottom() {
for cx in output_region.left()..output_region.right() {
if let Some(ref interior) = interior {
if cx >= interior.left()
&& cx < interior.right()
&& cy >= interior.top()
&& cy < interior.bottom()
{
continue;
}
}
let center = src.pixel_at(cx, cy);
let iter = mask_positions.iter().map(|&(dx, dy)| {
let pixel = border.pixel_at(src, cx as isize + dx, cy as isize + dy);
MapItem { pixel, dx, dy }
});
let result = op.map(center, iter);
*output.pixel_at_mut(cx - ox, cy - oy) = result;
}
}
}
#[must_use]
pub fn map_neighborhood<I, MI, B, M, P>(
src: &I,
mask_weights: &MI,
anchor: (usize, usize),
border: &B,
op: M,
) -> Image<M::Output>
where
I: RasterImage<Pixel = P>,
P: Copy,
MI: ImageView<Pixel = bool>,
B: BorderPolicy<I>,
M: MapOp<P>,
M::Output: Copy + ZeroablePixel,
{
let output_region = border.output_region(src.size(), mask_weights.size(), anchor);
let mut out = Image::<M::Output>::zero(output_region.size.width, output_region.size.height);
map_neighborhood_into(src, mask_weights, anchor, border, &mut out, op);
out
}
pub fn map_neighborhood_fn_into<I, MI, B, O, F, P, Out>(
src: &I,
mask_weights: &MI,
anchor: (usize, usize),
border: &B,
output: &mut O,
f: F,
) where
I: RasterImage<Pixel = P>,
P: Copy,
MI: ImageView<Pixel = bool>,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = Out>,
F: FnMut(P, &mut dyn Iterator<Item = MapItem<P>>) -> Out,
Out: Copy,
{
map_neighborhood_into(src, mask_weights, anchor, border, output, ClosureMap(f));
}
#[must_use]
pub fn map_neighborhood_fn<I, MI, B, F, P, Out>(
src: &I,
mask_weights: &MI,
anchor: (usize, usize),
border: &B,
f: F,
) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy,
MI: ImageView<Pixel = bool>,
B: BorderPolicy<I>,
Out: Copy + ZeroablePixel,
F: FnMut(P, &mut dyn Iterator<Item = MapItem<P>>) -> Out,
{
map_neighborhood(src, mask_weights, anchor, border, ClosureMap(f))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::border::{Clamp, Constant, Mirror, Skip, Wrap};
use crate::image::{Image, ImageView, Neighborhood};
#[test]
fn map_item_fields() {
let item = MapItem {
pixel: 42u8,
dx: -1,
dy: 2,
};
assert_eq!(item.pixel, 42);
assert_eq!(item.dx, -1);
assert_eq!(item.dy, 2);
}
#[test]
fn map_item_is_copy() {
let item = MapItem {
pixel: 7u8,
dx: 0,
dy: 0,
};
let item2 = item; assert_eq!(item, item2);
}
#[test]
fn map_item_debug() {
let item = MapItem {
pixel: 0u8,
dx: 0,
dy: 0,
};
let dbg = format!("{:?}", item);
assert!(dbg.contains("MapItem"));
}
struct IdentityMap;
impl MapOp<u8> for IdentityMap {
type Accumulator = u8;
type Output = u8;
#[inline(always)]
fn init(&self, center: u8) -> u8 {
center
}
#[inline(always)]
fn accumulate(&self, _acc: &mut u8, _item: MapItem<u8>) {}
#[inline(always)]
fn finalize(&mut self, acc: u8) -> u8 {
acc
}
}
struct CountNeighbors;
impl MapOp<u8> for CountNeighbors {
type Accumulator = u8;
type Output = u8;
fn init(&self, _center: u8) -> u8 {
0
}
fn accumulate(&self, acc: &mut u8, _item: MapItem<u8>) {
*acc += 1;
}
fn finalize(&mut self, acc: u8) -> u8 {
acc
}
}
struct MinMap;
impl MapOp<u8> for MinMap {
type Accumulator = u8;
type Output = u8;
#[inline(always)]
fn init(&self, center: u8) -> u8 {
center
}
#[inline(always)]
fn accumulate(&self, acc: &mut u8, item: MapItem<u8>) {
*acc = (*acc).min(item.pixel);
}
#[inline(always)]
fn finalize(&mut self, acc: u8) -> u8 {
acc
}
}
fn make_5x5_gradient() -> Image<u8> {
Image::generate(5, 5, |x, y| (x + y * 5) as u8)
}
fn make_4x4() -> Image<u8> {
Image::generate(4, 4, |x, y| (x + y * 4) as u8)
}
#[test]
fn identity_3x3_skip_preserves_interior() {
let src = make_5x5_gradient();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> =
map_neighborhood(&src, mask.weights(), mask.anchor(), &Skip, IdentityMap);
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
for y in 0..3 {
for x in 0..3 {
assert_eq!(
result.pixel_at(x, y),
src.pixel_at(x + 1, y + 1),
"mismatch at output ({x},{y})"
);
}
}
}
#[test]
fn identity_3x3_clamp_preserves_all() {
let src = make_5x5_gradient();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> =
map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, IdentityMap);
assert_eq!(result.width(), 5);
assert_eq!(result.height(), 5);
for y in 0..5 {
for x in 0..5 {
assert_eq!(result.pixel_at(x, y), src.pixel_at(x, y));
}
}
}
#[test]
fn all_border_policies_identity_agree_on_interior() {
let src = make_5x5_gradient();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let r_clamp = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, IdentityMap);
let r_mirror = map_neighborhood(&src, mask.weights(), mask.anchor(), &Mirror, IdentityMap);
let r_wrap = map_neighborhood(&src, mask.weights(), mask.anchor(), &Wrap, IdentityMap);
let r_const = map_neighborhood(
&src,
mask.weights(),
mask.anchor(),
&Constant(0u8),
IdentityMap,
);
assert_eq!(r_clamp.width(), 5);
assert_eq!(r_mirror.width(), 5);
assert_eq!(r_wrap.width(), 5);
assert_eq!(r_const.width(), 5);
for y in 1..4 {
for x in 1..4 {
let expected = src.pixel_at(x, y);
assert_eq!(r_clamp.pixel_at(x, y), expected);
assert_eq!(r_mirror.pixel_at(x, y), expected);
assert_eq!(r_wrap.pixel_at(x, y), expected);
assert_eq!(r_const.pixel_at(x, y), expected);
}
}
}
#[test]
fn full_rect_mask_yields_9_neighbors_per_pixel() {
let src = Image::fill(5, 5, 1u8);
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, CountNeighbors);
for y in 0..5 {
for x in 0..5 {
assert_eq!(result.pixel_at(x, y), 9, "expected 9 at ({x},{y})");
}
}
}
#[test]
fn cross_mask_yields_5_neighbors_for_interior_pixel() {
let src = Image::fill(5, 5, 1u8);
let mask = Neighborhood::<bool, 3, 3>::cross_3x3();
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, CountNeighbors);
assert_eq!(result.pixel_at(2, 2), 5);
}
#[test]
fn anchor_only_mask_yields_1_neighbor_per_pixel() {
#[rustfmt::skip]
let mask = Neighborhood::<bool, 3, 3>::new([
false, false, false,
false, true, false,
false, false, false,
]);
let src = Image::fill(5, 5, 1u8);
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, CountNeighbors);
for y in 0..5 {
for x in 0..5 {
assert_eq!(result.pixel_at(x, y), 1);
}
}
}
#[test]
fn anchor_excluded_from_iterator_when_mask_anchor_false() {
#[rustfmt::skip]
let mask = Neighborhood::<bool, 3, 3>::new([
false, true, false,
true, false, true,
false, true, false,
]);
let src = Image::fill(5, 5, 0u8);
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, CountNeighbors);
assert_eq!(result.pixel_at(2, 2), 4);
}
#[test]
fn right_neighbor_only_mask_reads_correct_source_pixel() {
let src = make_5x5_gradient();
#[rustfmt::skip]
let mask = Neighborhood::<bool, 3, 3>::new([
false, false, false,
false, false, true, false, false, false,
]);
let result = map_neighborhood_fn(
&src,
mask.weights(),
mask.anchor(),
&Skip,
|_center: u8, neighbors: &mut dyn Iterator<Item = MapItem<u8>>| {
neighbors.next().map(|n| n.pixel).unwrap_or(0)
},
);
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
for oy in 0..3 {
for ox in 0..3 {
assert_eq!(
result.pixel_at(ox, oy),
src.pixel_at(ox + 2, oy + 1),
"mismatch at output ({ox},{oy})"
);
}
}
}
#[test]
fn bottom_right_neighbor_has_correct_dx_dy() {
#[rustfmt::skip]
let mask = Neighborhood::<bool, 3, 3>::new([
false, false, false,
false, false, false,
false, false, true, ]);
let src = Image::fill(5, 5, 0u8);
let mut last_dx = 99isize;
let mut last_dy = 99isize;
let _ = map_neighborhood_fn(
&src,
mask.weights(),
mask.anchor(),
&Skip,
|_center: u8, neighbors: &mut dyn Iterator<Item = MapItem<u8>>| {
for n in neighbors {
last_dx = n.dx;
last_dy = n.dy;
}
0u8
},
);
assert_eq!(last_dx, 1, "expected dx = 1");
assert_eq!(last_dy, 1, "expected dy = 1");
}
#[test]
fn top_left_neighbor_has_correct_dx_dy() {
#[rustfmt::skip]
let mask = Neighborhood::<bool, 3, 3>::new([
true, false, false, false, false, false,
false, false, false,
]);
let src = Image::fill(5, 5, 0u8);
let mut last_dx = 99isize;
let mut last_dy = 99isize;
let _ = map_neighborhood_fn(
&src,
mask.weights(),
mask.anchor(),
&Skip,
|_center: u8, neighbors: &mut dyn Iterator<Item = MapItem<u8>>| {
for n in neighbors {
last_dx = n.dx;
last_dy = n.dy;
}
0u8
},
);
assert_eq!(last_dx, -1, "expected dx = -1");
assert_eq!(last_dy, -1, "expected dy = -1");
}
#[test]
fn map_into_produces_same_result_as_map() {
let src = make_4x4();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let alloc = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, IdentityMap);
let mut into_result = Image::zero(4, 4);
map_neighborhood_into(
&src,
mask.weights(),
mask.anchor(),
&Clamp,
&mut into_result,
IdentityMap,
);
for y in 0..4 {
for x in 0..4 {
assert_eq!(
alloc.pixel_at(x, y),
into_result.pixel_at(x, y),
"mismatch at ({x},{y})"
);
}
}
}
#[test]
fn map_fn_into_produces_same_result_as_map_fn() {
let src = make_4x4();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let alloc = map_neighborhood_fn(
&src,
mask.weights(),
mask.anchor(),
&Clamp,
|center: u8, _: &mut dyn Iterator<Item = MapItem<u8>>| center,
);
let mut into_result = Image::zero(4, 4);
map_neighborhood_fn_into(
&src,
mask.weights(),
mask.anchor(),
&Clamp,
&mut into_result,
|center: u8, _: &mut dyn Iterator<Item = MapItem<u8>>| center,
);
for y in 0..4 {
for x in 0..4 {
assert_eq!(alloc.pixel_at(x, y), into_result.pixel_at(x, y));
}
}
}
#[test]
fn closure_map_produces_same_result_as_struct() {
let src = make_5x5_gradient();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let struct_result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, MinMap);
let closure_result = map_neighborhood_fn(
&src,
mask.weights(),
mask.anchor(),
&Clamp,
|center: u8, neighbors: &mut dyn Iterator<Item = MapItem<u8>>| {
neighbors.map(|n| n.pixel).fold(center, u8::min)
},
);
for y in 0..5 {
for x in 0..5 {
assert_eq!(
struct_result.pixel_at(x, y),
closure_result.pixel_at(x, y),
"mismatch at ({x},{y})"
);
}
}
}
#[test]
fn min_filter_3x3_known_values() {
let src = make_5x5_gradient();
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Skip, MinMap);
assert_eq!(result.pixel_at(0, 0), 0);
assert_eq!(result.pixel_at(1, 1), 6);
assert_eq!(result.pixel_at(2, 2), 12);
}
#[test]
fn single_pixel_image_clamp() {
let src = Image::fill(1, 1, 42u8);
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, IdentityMap);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert_eq!(result.pixel_at(0, 0), 42);
}
#[test]
fn single_pixel_image_constant_border_reduces_to_min() {
let src = Image::fill(1, 1, 10u8);
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Constant(0u8), MinMap);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert_eq!(result.pixel_at(0, 0), 0);
}
#[test]
fn kernel_larger_than_image_skip_produces_empty_output() {
let src = Image::fill(2, 2, 0u8);
let mask = Neighborhood::<bool, 5, 5>::full_rect_5x5();
let result: Image<u8> =
map_neighborhood(&src, mask.weights(), mask.anchor(), &Skip, IdentityMap);
assert_eq!(result.width(), 0);
assert_eq!(result.height(), 0);
}
#[test]
#[should_panic(expected = "too small")]
fn map_into_panics_on_undersized_output() {
let src = Image::fill(5, 5, 0u8);
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let mut out = Image::<u8>::zero(3, 3);
map_neighborhood_into(
&src,
mask.weights(),
mask.anchor(),
&Clamp,
&mut out,
IdentityMap,
);
}
#[test]
fn works_with_f32_pixels() {
use crate::pixel::MonoF32;
let src = Image::fill(5, 5, MonoF32::new(1.0));
#[rustfmt::skip]
let mask = Neighborhood::<bool, 3, 3>::new([
false, false, false,
false, true, false,
false, false, false,
]);
struct IdentityF32;
impl MapOp<MonoF32> for IdentityF32 {
type Accumulator = MonoF32;
type Output = MonoF32;
fn init(&self, center: MonoF32) -> MonoF32 {
center
}
fn accumulate(&self, _acc: &mut MonoF32, _item: MapItem<MonoF32>) {}
fn finalize(&mut self, acc: MonoF32) -> MonoF32 {
acc
}
}
let result = map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, IdentityF32);
for y in 0..5 {
for x in 0..5 {
assert!((result.pixel_at(x, y).0 - 1.0f32).abs() < 1e-6);
}
}
}
#[test]
fn large_image_runs_without_error() {
let src = Image::generate(100, 100, |x, y| ((x + y * 3) % 256) as u8);
let mask = Neighborhood::<bool, 3, 3>::full_rect_3x3();
let result: Image<u8> =
map_neighborhood(&src, mask.weights(), mask.anchor(), &Clamp, IdentityMap);
assert_eq!(result.width(), 100);
assert_eq!(result.height(), 100);
}
}