use crate::border::{BorderPolicy, compute_interior_region};
use crate::image::{Image, ImageView, RasterImage, RasterImageMut};
use crate::pixel::ZeroablePixel;
#[cfg(test)]
use crate::pixel::MonoF32;
pub trait FoldOp<P, W> {
type Accumulator;
type Output;
const INVERTIBLE: bool = true;
fn init(&self) -> Self::Accumulator;
fn accumulate(&self, acc: &mut Self::Accumulator, item: FoldItem<P, W>);
fn finalize(&mut self, acc: Self::Accumulator) -> Self::Output;
fn fold<I>(&mut self, neighbors: I) -> Self::Output
where
I: Iterator<Item = FoldItem<P, W>>,
{
let mut acc = self.init();
for item in neighbors {
self.accumulate(&mut acc, item);
}
self.finalize(acc)
}
}
pub struct ClosureFold<F>(pub F);
impl<P, W, Out, F> FoldOp<P, W> for ClosureFold<F>
where
P: Copy,
W: Copy,
F: FnMut(&mut dyn Iterator<Item = FoldItem<P, W>>) -> Out,
{
type Accumulator = Vec<FoldItem<P, W>>;
type Output = Out;
const INVERTIBLE: bool = false;
#[inline(always)]
fn init(&self) -> Vec<FoldItem<P, W>> {
Vec::new()
}
#[inline(always)]
fn accumulate(&self, acc: &mut Vec<FoldItem<P, W>>, item: FoldItem<P, W>) {
acc.push(item);
}
#[inline(always)]
fn finalize(&mut self, acc: Vec<FoldItem<P, W>>) -> Out {
let mut iter = acc.into_iter();
(self.0)(&mut iter)
}
#[inline(always)]
fn fold<I>(&mut self, mut neighbors: I) -> Out
where
I: Iterator<Item = FoldItem<P, W>>,
{
(self.0)(&mut neighbors)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FoldItem<P, W> {
pub pixel: P,
pub weight: W,
}
pub fn fold_neighborhood_into<I, WI, B, O, F, P, W, Out>(
image: &I,
weights: &WI,
anchor: (usize, usize),
border: &B,
output: &mut O,
mut f: F,
) where
I: RasterImage<Pixel = P>,
P: Copy,
WI: ImageView<Pixel = W>,
W: Copy,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = Out>,
F: FoldOp<P, W, Output = Out>,
{
let kernel_size = weights.size();
let output_region = border.output_region(image.size(), kernel_size, anchor);
let interior = compute_interior_region(image.size(), kernel_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 kernel_positions: Vec<(isize, isize, W)> = {
let mut positions = Vec::with_capacity(kernel_size.width * kernel_size.height);
for ky in 0..kernel_size.height {
for kx in 0..kernel_size.width {
let dx = kx as isize - anchor.0 as isize;
let dy = ky as isize - anchor.1 as isize;
let w = weights.pixel_at(kx, ky);
positions.push((dx, dy, w));
}
}
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 F::INVERTIBLE {
let mut acc_row: Vec<F::Accumulator> = (0..int_width).map(|_| f.init()).collect();
for cy in int_top..int_bottom {
for &(dx, dy, w) in &kernel_positions {
let src_row = image.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 {
f.accumulate(
&mut acc_row[i],
FoldItem {
pixel: src_slice[i],
weight: w,
},
);
}
}
let out_row = &mut output.row_mut(cy - oy)[int_left - ox..int_right - ox];
for i in 0..int_width {
let new_init = f.init();
let acc = std::mem::replace(&mut acc_row[i], new_init);
out_row[i] = f.finalize(acc);
}
}
} else {
for cy in int_top..int_bottom {
for cx in int_left..int_right {
let iter = kernel_positions.iter().map(|&(dx, dy, w)| {
let sx = (cx as isize + dx) as usize;
let sy = (cy as isize + dy) as usize;
FoldItem {
pixel: image.pixel_at(sx, sy),
weight: w,
}
});
let result = f.fold(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 iter = kernel_positions.iter().map(|&(dx, dy, w)| {
let pixel = border.pixel_at(image, cx as isize + dx, cy as isize + dy);
FoldItem { pixel, weight: w }
});
let result = f.fold(iter);
*output.pixel_at_mut(cx - ox, cy - oy) = result;
}
}
}
#[must_use]
pub fn fold_neighborhood<I, WI, B, F, P, W, Out>(
image: &I,
weights: &WI,
anchor: (usize, usize),
border: &B,
f: F,
) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy,
WI: ImageView<Pixel = W>,
W: Copy,
B: BorderPolicy<I>,
Out: ZeroablePixel,
F: FoldOp<P, W, Output = Out>,
{
let output_region = border.output_region(image.size(), weights.size(), anchor);
let mut out = Image::<Out>::zero(output_region.size.width, output_region.size.height);
fold_neighborhood_into(image, weights, anchor, border, &mut out, f);
out
}
pub fn fold_neighborhood_fn_into<I, WI, B, O, F, P, W, Out>(
image: &I,
weights: &WI,
anchor: (usize, usize),
border: &B,
output: &mut O,
f: F,
) where
I: RasterImage<Pixel = P>,
P: Copy,
WI: ImageView<Pixel = W>,
W: Copy,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = Out>,
F: FnMut(&mut dyn Iterator<Item = FoldItem<P, W>>) -> Out,
{
fold_neighborhood_into(image, weights, anchor, border, output, ClosureFold(f));
}
#[must_use]
pub fn fold_neighborhood_fn<I, WI, B, F, P, W, Out>(
image: &I,
weights: &WI,
anchor: (usize, usize),
border: &B,
f: F,
) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy,
WI: ImageView<Pixel = W>,
W: Copy,
B: BorderPolicy<I>,
Out: ZeroablePixel,
F: FnMut(&mut dyn Iterator<Item = FoldItem<P, W>>) -> Out,
{
fold_neighborhood(image, weights, anchor, border, ClosureFold(f))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::border::{Clamp, Constant, Mirror, Skip, Wrap};
use crate::image::{ImageArray, Neighborhood};
fn make_4x4() -> Image<u8> {
Image::generate(4, 4, |x, y| (x + y * 4) as u8)
}
fn make_5x5() -> Image<u8> {
Image::generate(5, 5, |x, y| (x + y * 5) as u8)
}
struct SumFold;
impl FoldOp<u8, f32> for SumFold {
type Accumulator = f32;
type Output = MonoF32;
#[inline(always)]
fn init(&self) -> f32 {
0.0
}
#[inline(always)]
fn accumulate(&self, acc: &mut f32, item: FoldItem<u8, f32>) {
*acc += item.pixel as f32 * item.weight;
}
#[inline(always)]
fn finalize(&mut self, acc: f32) -> MonoF32 {
MonoF32(acc)
}
}
impl FoldOp<f32, f32> for SumFold {
type Accumulator = f32;
type Output = MonoF32;
#[inline(always)]
fn init(&self) -> f32 {
0.0
}
#[inline(always)]
fn accumulate(&self, acc: &mut f32, item: FoldItem<f32, f32>) {
*acc += item.pixel * item.weight;
}
#[inline(always)]
fn finalize(&mut self, acc: f32) -> MonoF32 {
MonoF32(acc)
}
}
fn identity_fold() -> SumFold {
SumFold
}
fn sum_fold() -> SumFold {
SumFold
}
#[test]
fn fold_item_fields() {
let item = FoldItem {
pixel: 100u8,
weight: 0.5f32,
};
assert_eq!(item.pixel, 100);
assert_eq!(item.weight, 0.5);
}
#[test]
fn fold_item_is_copy() {
let item = FoldItem {
pixel: 1u8,
weight: 1.0f32,
};
let item2 = item; assert_eq!(item, item2);
}
#[test]
fn fold_item_debug() {
let item = FoldItem {
pixel: 0u8,
weight: 0.0f32,
};
let dbg = format!("{:?}", item);
assert!(dbg.contains("FoldItem"));
}
#[test]
fn identity_3x3_skip_preserves_interior() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Skip,
identity_fold(),
);
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),
MonoF32(src.pixel_at(x + 1, y + 1) as f32),
"mismatch at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn identity_3x3_clamp_preserves_all() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
identity_fold(),
);
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),
MonoF32(src.pixel_at(x, y) as f32),
"mismatch at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn identity_1x1_all_policies() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 1, 1>::new([1.0]);
for policy_name in &["skip", "clamp", "mirror", "wrap", "constant"] {
let result: Image<MonoF32> = match *policy_name {
"skip" => fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Skip,
identity_fold(),
),
"clamp" => fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
identity_fold(),
),
"mirror" => fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Mirror,
identity_fold(),
),
"wrap" => fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Wrap,
identity_fold(),
),
"constant" => fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Constant(0u8),
identity_fold(),
),
_ => unreachable!(),
};
assert_eq!(result.width(), 4, "policy={}", policy_name);
assert_eq!(result.height(), 4, "policy={}", policy_name);
for y in 0..4 {
for x in 0..4 {
assert_eq!(
result.pixel_at(x, y),
MonoF32(src.pixel_at(x, y) as f32),
"policy={} at ({}, {})",
policy_name,
x,
y,
);
}
}
}
}
#[test]
fn box_blur_3x3_uniform_image() {
let src = Image::fill(6, 6, 10u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
assert_eq!(result.width(), 6);
assert_eq!(result.height(), 6);
for y in 0..6 {
for x in 0..6 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 10.0).abs() < 1e-4,
"expected ~10.0, got {} at ({}, {})",
v.0,
x,
y,
);
}
}
}
#[test]
fn box_blur_3x3_skip_interior_sum() {
let src = Image::fill(3, 3, 1u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
let v = result.pixel_at(0, 0);
assert!((v.0 - 1.0).abs() < 1e-4, "expected ~1.0, got {}", v.0,);
}
#[test]
fn known_3x3_sum_center_pixel() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
for y in 0..4 {
for x in 0..4 {
assert_eq!(
result.pixel_at(x, y),
MonoF32(src.pixel_at(x, y) as f32 * 2.0),
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn horizontal_gradient_kernel() {
let src = Image::generate(5, 1, |x, _| x as u8); let kernel = Neighborhood::<f32, 3, 1>::new([-1.0, 0.0, 1.0]);
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 1);
for x in 0..3 {
assert_eq!(result.pixel_at(x, 0), MonoF32(2.0), "at x={}", x,);
}
}
#[test]
fn fold_into_writes_correct_output() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let border = Clamp;
let out_region = BorderPolicy::<Image<u8>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut out = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
fold_neighborhood_into(
&src,
kernel.weights(),
kernel.anchor(),
&border,
&mut out,
identity_fold(),
);
for y in 0..4 {
for x in 0..4 {
assert_eq!(
out.pixel_at(x, y),
MonoF32(src.pixel_at(x, y) as f32),
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn fold_into_skip_smaller_output() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0; 9]);
let border = Skip;
let out_region = BorderPolicy::<Image<u8>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut out = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
assert_eq!(out.width(), 3);
assert_eq!(out.height(), 3);
fold_neighborhood_into(
&src,
kernel.weights(),
kernel.anchor(),
&border,
&mut out,
ClosureFold(|_: &mut dyn Iterator<Item = FoldItem<u8, f32>>| MonoF32(42.0)),
);
for y in 0..3 {
for x in 0..3 {
assert_eq!(out.pixel_at(x, y), MonoF32(42.0));
}
}
}
#[test]
fn constant_border_zero_padding() {
let src = Image::fill(3, 3, 1u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Constant(0u8),
sum_fold(),
);
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
let center = result.pixel_at(1, 1);
assert!(
(center.0 - 1.0).abs() < 1e-4,
"center: expected 1.0, got {}",
center.0,
);
let corner = result.pixel_at(0, 0);
assert!(
(corner.0 - 4.0 / 9.0).abs() < 1e-4,
"corner: expected {}, got {}",
4.0 / 9.0,
corner.0,
);
let edge = result.pixel_at(1, 0);
assert!(
(edge.0 - 6.0 / 9.0).abs() < 1e-4,
"edge: expected {}, got {}",
6.0 / 9.0,
edge.0,
);
}
#[test]
fn clamp_border_uniform_image() {
let src = Image::fill(4, 4, 7u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
for y in 0..4 {
for x in 0..4 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 7.0).abs() < 1e-4,
"at ({}, {}): expected 7.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn mirror_border_uniform_image() {
let src = Image::fill(4, 4, 3u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result =
fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Mirror, sum_fold());
for y in 0..4 {
for x in 0..4 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 3.0).abs() < 1e-4,
"at ({}, {}): expected 3.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn wrap_border_uniform_image() {
let src = Image::fill(4, 4, 5u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Wrap, sum_fold());
for y in 0..4 {
for x in 0..4 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 5.0).abs() < 1e-4,
"at ({}, {}): expected 5.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn horizontal_1d_kernel() {
let src = Image::generate(5, 1, |x, _| x as u8); let kernel = Neighborhood::<f32, 3, 1>::new([1.0, 2.0, 1.0]);
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 1);
assert_eq!(result.pixel_at(0, 0), MonoF32(4.0));
assert_eq!(result.pixel_at(1, 0), MonoF32(8.0));
assert_eq!(result.pixel_at(2, 0), MonoF32(12.0));
}
#[test]
fn vertical_1d_kernel() {
let src = Image::generate(1, 5, |_, y| y as u8); let kernel = Neighborhood::<f32, 1, 3>::new([1.0, 2.0, 1.0]);
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 3);
assert_eq!(result.pixel_at(0, 0), MonoF32(4.0)); assert_eq!(result.pixel_at(0, 1), MonoF32(8.0)); assert_eq!(result.pixel_at(0, 2), MonoF32(12.0)); }
#[test]
fn custom_anchor_top_left() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::with_anchor(
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
(0, 0),
);
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
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),
MonoF32(src.pixel_at(x, y) as f32),
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn box_blur_5x5_uniform() {
let src = Image::fill(8, 8, 4u8);
let kernel = Neighborhood::<f32, 5, 5>::box_blur_5x5();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
assert_eq!(result.width(), 8);
assert_eq!(result.height(), 8);
for y in 0..8 {
for x in 0..8 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 4.0).abs() < 1e-4,
"at ({}, {}): expected 4.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn works_with_image_array_source() {
let src: ImageArray<u8, 4, 4> = ImageArray::generate(|x, y| (x + y * 4) as u8);
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
identity_fold(),
);
assert_eq!(result.width(), 4);
assert_eq!(result.height(), 4);
for y in 0..4 {
for x in 0..4 {
assert_eq!(result.pixel_at(x, y), MonoF32(src.pixel_at(x, y) as f32),);
}
}
}
#[test]
fn single_pixel_image_clamp() {
let src = Image::fill(1, 1, 42u8);
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
let v = result.pixel_at(0, 0);
assert!((v.0 - 42.0).abs() < 1e-4, "expected 42.0, got {}", v.0,);
}
#[test]
fn single_pixel_image_constant() {
let src = Image::fill(1, 1, 9u8);
let kernel = Neighborhood::<f32, 3, 3>::new([1.0; 9]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Constant(0u8),
sum_fold(),
);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
let v = result.pixel_at(0, 0);
assert_eq!(v, MonoF32(9.0));
}
#[test]
fn kernel_larger_than_image_skip() {
let src = Image::fill(2, 2, 1u8);
let kernel = Neighborhood::<f32, 5, 5>::box_blur_5x5();
let border = Skip;
let out_region = BorderPolicy::<Image<u8>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
assert_eq!(out_region.area(), 0);
}
#[test]
fn fold_into_oversized_output() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let mut out = Image::<MonoF32>::fill(6, 6, MonoF32(-1.0));
fold_neighborhood_into(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
&mut out,
identity_fold(),
);
for y in 0..4 {
for x in 0..4 {
assert_eq!(
out.pixel_at(x, y),
MonoF32(src.pixel_at(x, y) as f32),
"at ({}, {})",
x,
y,
);
}
}
for x in 4..6 {
assert_eq!(out.pixel_at(x, 0), MonoF32(-1.0));
}
for y in 4..6 {
assert_eq!(out.pixel_at(0, y), MonoF32(-1.0));
}
}
#[test]
fn sobel_y_on_horizontal_gradient() {
let src = Image::generate(5, 5, |x, _| x as u8);
let kernel = Neighborhood::<f32, 3, 3>::sobel_y();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
for y in 0..3 {
for x in 0..3 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 8.0).abs() < 1e-4,
"at ({}, {}): expected 8.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn sobel_x_on_vertical_gradient() {
let src = Image::generate(5, 5, |_, y| y as u8);
let kernel = Neighborhood::<f32, 3, 3>::sobel_x();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Skip, sum_fold());
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
for y in 0..3 {
for x in 0..3 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 8.0).abs() < 1e-4,
"at ({}, {}): expected 8.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn sobel_x_on_uniform_is_zero() {
let src = Image::fill(5, 5, 100u8);
let kernel = Neighborhood::<f32, 3, 3>::sobel_x();
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
for y in 0..5 {
for x in 0..5 {
let v = result.pixel_at(x, y);
assert!(
v.0.abs() < 1e-4,
"at ({}, {}): expected ~0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn max_filter_3x3() {
let src = Image::generate(5, 5, |x, y| (x + y * 5) as u8);
let kernel = Neighborhood::<f32, 3, 3>::new([1.0; 9]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Skip,
ClosureFold(|neighbors: &mut dyn Iterator<Item = FoldItem<u8, f32>>| {
neighbors.map(|item| item.pixel).max().unwrap_or(0)
}),
);
assert_eq!(result.pixel_at(0, 0), 12);
assert_eq!(result.pixel_at(1, 1), 18);
}
#[test]
fn min_filter_3x3() {
let src = Image::generate(5, 5, |x, y| (x + y * 5) as u8);
let kernel = Neighborhood::<f32, 3, 3>::new([1.0; 9]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Skip,
ClosureFold(|neighbors: &mut dyn Iterator<Item = FoldItem<u8, f32>>| {
neighbors.map(|item| item.pixel).min().unwrap_or(255)
}),
);
assert_eq!(result.pixel_at(0, 0), 0);
assert_eq!(result.pixel_at(2, 2), 12);
}
#[test]
fn all_boundary_small_image_large_kernel() {
let src = Image::generate(3, 3, |x, y| (x + y * 3) as u8);
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
identity_fold(),
);
for y in 0..3 {
for x in 0..3 {
assert_eq!(
result.pixel_at(x, y),
MonoF32(src.pixel_at(x, y) as f32),
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn fold_and_fold_into_produce_same_result() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let border = Clamp;
let result1 =
fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &border, sum_fold());
let out_region = BorderPolicy::<Image<u8>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut result2 = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
fold_neighborhood_into(
&src,
kernel.weights(),
kernel.anchor(),
&border,
&mut result2,
sum_fold(),
);
for y in 0..result1.height() {
for x in 0..result1.width() {
assert_eq!(
result1.pixel_at(x, y),
result2.pixel_at(x, y),
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn large_image_runs_without_error() {
let src = Image::fill(100, 100, 1u8);
let kernel = Neighborhood::<f32, 5, 5>::box_blur_5x5();
let result =
fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Mirror, sum_fold());
assert_eq!(result.width(), 100);
assert_eq!(result.height(), 100);
for y in 0..100 {
for x in 0..100 {
let v = result.pixel_at(x, y);
assert!(
(v.0 - 1.0).abs() < 1e-4,
"at ({}, {}): expected 1.0, got {}",
x,
y,
v.0,
);
}
}
}
#[test]
fn f32_image_identity() {
let src = Image::generate(4, 4, |x, y| x as f32 + y as f32 * 0.1);
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood(&src, kernel.weights(), kernel.anchor(), &Clamp, sum_fold());
for y in 0..4 {
for x in 0..4 {
assert!(
(result.pixel_at(x, y).0 - src.pixel_at(x, y)).abs() < 1e-6,
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn fold_neighborhood_fn_identity_skip() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let result = fold_neighborhood_fn(
&src,
kernel.weights(),
kernel.anchor(),
&Skip,
|neighbors| {
let mut sum = 0.0f32;
for item in neighbors {
sum += item.pixel as f32 * item.weight;
}
MonoF32(sum)
},
);
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),
MonoF32(src.pixel_at(x + 1, y + 1) as f32),
"mismatch at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn fold_neighborhood_fn_into_identity_clamp() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 3, 3>::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
let border = Clamp;
let out_region = BorderPolicy::<Image<u8>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut out = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
fold_neighborhood_fn_into(
&src,
kernel.weights(),
kernel.anchor(),
&border,
&mut out,
|neighbors| {
let mut sum = 0.0f32;
for item in neighbors {
sum += item.pixel as f32 * item.weight;
}
MonoF32(sum)
},
);
assert_eq!(out.width(), 4);
assert_eq!(out.height(), 4);
for y in 0..4 {
for x in 0..4 {
assert_eq!(
out.pixel_at(x, y),
MonoF32(src.pixel_at(x, y) as f32),
"at ({}, {})",
x,
y,
);
}
}
}
#[test]
fn fold_fn_matches_fold_with_closure_fold() {
let src = make_5x5();
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let via_fn = fold_neighborhood_fn(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
|neighbors| {
let mut sum = 0.0f32;
for item in neighbors {
sum += item.pixel as f32 * item.weight;
}
MonoF32(sum)
},
);
let via_closure_fold = fold_neighborhood(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
ClosureFold(|neighbors: &mut dyn Iterator<Item = FoldItem<u8, f32>>| {
let mut sum = 0.0f32;
for item in neighbors {
sum += item.pixel as f32 * item.weight;
}
MonoF32(sum)
}),
);
assert_eq!(via_fn.width(), via_closure_fold.width());
assert_eq!(via_fn.height(), via_closure_fold.height());
for y in 0..via_fn.height() {
for x in 0..via_fn.width() {
assert!(
(via_fn.pixel_at(x, y).0 - via_closure_fold.pixel_at(x, y).0).abs() < 1e-6,
"mismatch at ({}, {}): fn={}, closure_fold={}",
x,
y,
via_fn.pixel_at(x, y).0,
via_closure_fold.pixel_at(x, y).0,
);
}
}
}
#[test]
fn fold_fn_into_matches_fold_into_with_closure_fold() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let border = Clamp;
let out_region = BorderPolicy::<Image<u8>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut out_fn = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
fold_neighborhood_fn_into(
&src,
kernel.weights(),
kernel.anchor(),
&border,
&mut out_fn,
|neighbors| {
let mut sum = 0.0f32;
for item in neighbors {
sum += item.pixel as f32 * item.weight;
}
MonoF32(sum)
},
);
let mut out_cf = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
fold_neighborhood_into(
&src,
kernel.weights(),
kernel.anchor(),
&border,
&mut out_cf,
ClosureFold(|neighbors: &mut dyn Iterator<Item = FoldItem<u8, f32>>| {
let mut sum = 0.0f32;
for item in neighbors {
sum += item.pixel as f32 * item.weight;
}
MonoF32(sum)
}),
);
assert_eq!(out_fn.width(), out_cf.width());
assert_eq!(out_fn.height(), out_cf.height());
for y in 0..out_fn.height() {
for x in 0..out_fn.width() {
assert!(
(out_fn.pixel_at(x, y).0 - out_cf.pixel_at(x, y).0).abs() < 1e-6,
"mismatch at ({}, {})",
x,
y,
);
}
}
}
#[test]
#[should_panic(expected = "too small")]
fn fold_into_panics_on_undersized_output() {
let src = make_4x4();
let kernel = Neighborhood::<f32, 3, 3>::new([1.0; 9]);
let mut out = Image::<MonoF32>::zero(2, 2);
fold_neighborhood_into(
&src,
kernel.weights(),
kernel.anchor(),
&Clamp,
&mut out,
ClosureFold(|_: &mut dyn Iterator<Item = FoldItem<u8, f32>>| MonoF32(0.0)),
);
}
}