use crate::border::BorderPolicy;
use crate::image::Kernel;
use crate::image::{Image, RasterImage, RasterImageMut};
use crate::pixel::{FromLinear, LinearPixel, ZeroablePixel};
use crate::transform::fold::{FoldItem, FoldOp, fold_neighborhood, fold_neighborhood_into};
pub(crate) struct ConvolveFold<P, Out> {
_marker: core::marker::PhantomData<(P, Out)>,
}
impl<P, Out> ConvolveFold<P, Out> {
#[inline(always)]
pub(crate) fn new() -> Self {
Self {
_marker: core::marker::PhantomData,
}
}
}
impl<P, Out> FoldOp<P, f32> for ConvolveFold<P, Out>
where
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default,
Out: FromLinear<<P as LinearPixel<f32>>::Accumulator>,
{
type Accumulator = <P as LinearPixel<f32>>::Accumulator;
type Output = Out;
#[inline(always)]
fn init(&self) -> Self::Accumulator {
<P as LinearPixel<f32>>::Accumulator::default()
}
#[inline(always)]
fn accumulate(&self, acc: &mut Self::Accumulator, item: FoldItem<P, f32>) {
*acc = item.pixel.scale_add(item.weight, *acc);
}
#[inline(always)]
fn finalize(&mut self, acc: Self::Accumulator) -> Out {
Out::from_linear(acc)
}
}
pub fn convolve_into<I, K, B, O, P, Out>(image: &I, kernel: &K, border: &B, output: &mut O)
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default,
K: Kernel<Weight = f32>,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = Out>,
Out: FromLinear<<P as LinearPixel<f32>>::Accumulator>,
{
let flipped = kernel.flipped();
fold_neighborhood_into(
image,
flipped.weights(),
flipped.anchor(),
border,
output,
ConvolveFold::<P, Out>::new(),
);
}
#[must_use]
pub fn convolve<I, K, B, P, Out>(image: &I, kernel: &K, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default,
K: Kernel<Weight = f32>,
B: BorderPolicy<I>,
Out: ZeroablePixel + FromLinear<<P as LinearPixel<f32>>::Accumulator>,
{
let flipped = kernel.flipped();
fold_neighborhood(
image,
flipped.weights(),
flipped.anchor(),
border,
ConvolveFold::<P, Out>::new(),
)
}
pub fn correlate_into<I, K, B, O, P, Out>(image: &I, kernel: &K, border: &B, output: &mut O)
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default,
K: Kernel<Weight = f32>,
B: BorderPolicy<I>,
O: RasterImageMut<Pixel = Out>,
Out: FromLinear<<P as LinearPixel<f32>>::Accumulator>,
{
fold_neighborhood_into(
image,
kernel.weights(),
kernel.anchor(),
border,
output,
ConvolveFold::<P, Out>::new(),
);
}
#[must_use]
pub fn correlate<I, K, B, P, Out>(image: &I, kernel: &K, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default,
K: Kernel<Weight = f32>,
B: BorderPolicy<I>,
Out: ZeroablePixel + FromLinear<<P as LinearPixel<f32>>::Accumulator>,
{
fold_neighborhood(
image,
kernel.weights(),
kernel.anchor(),
border,
ConvolveFold::<P, Out>::new(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::border::{Clamp, Constant, Skip};
use crate::image::{ImageView, Neighborhood};
use crate::pixel::{Mono8, MonoF32};
fn make_4x4_monof32() -> Image<MonoF32> {
Image::generate(4, 4, |x, y| MonoF32((x + y * 4) as f32))
}
#[test]
fn convolve_identity_preserves_image() {
let src = make_4x4_monof32();
let kernel = Neighborhood::<f32, 3, 3>::identity_3x3();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
assert_eq!(result.width(), 4);
assert_eq!(result.height(), 4);
for y in 0..4 {
for x in 0..4 {
assert!(
(result.pixel_at(x, y).0 - src.pixel_at(x, y).0).abs() < 1e-6,
"mismatch at ({x}, {y}): got {}, expected {}",
result.pixel_at(x, y).0,
src.pixel_at(x, y).0,
);
}
}
}
#[test]
fn correlate_identity_preserves_image() {
let src = make_4x4_monof32();
let kernel = Neighborhood::<f32, 3, 3>::identity_3x3();
let result: Image<MonoF32> = correlate(&src, &kernel, &Clamp);
for y in 0..4 {
for x in 0..4 {
assert!((result.pixel_at(x, y).0 - src.pixel_at(x, y).0).abs() < 1e-6,);
}
}
}
#[test]
fn symmetric_kernel_convolve_equals_correlate() {
let src = make_4x4_monof32();
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let conv: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
let corr: Image<MonoF32> = correlate(&src, &kernel, &Clamp);
assert_eq!(conv.width(), corr.width());
assert_eq!(conv.height(), corr.height());
for y in 0..conv.height() {
for x in 0..conv.width() {
assert!(
(conv.pixel_at(x, y).0 - corr.pixel_at(x, y).0).abs() < 1e-6,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn box_blur_uniform_image() {
let src = Image::fill(6, 6, MonoF32(5.0));
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 5.0).abs() < 1e-5,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn asymmetric_kernel_convolve_differs_from_correlate() {
let src = Image::generate(6, 6, |x, _y| MonoF32(x as f32));
let kernel = Neighborhood::<f32, 3, 3>::sobel_y();
let conv: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
let corr: Image<MonoF32> = correlate(&src, &kernel, &Clamp);
let mut found_nonzero = false;
for y in 0..conv.height() {
for x in 0..conv.width() {
let sum = conv.pixel_at(x, y).0 + corr.pixel_at(x, y).0;
assert!(
sum.abs() < 1e-4,
"conv + corr should be ~0 at ({x}, {y}): conv={}, corr={}, sum={}",
conv.pixel_at(x, y).0,
corr.pixel_at(x, y).0,
sum,
);
if conv.pixel_at(x, y).0.abs() > 0.1 {
found_nonzero = true;
}
}
}
assert!(
found_nonzero,
"expected non-zero response on gradient image"
);
}
#[test]
fn convolve_into_writes_correct_output() {
let src = Image::fill(4, 4, MonoF32(3.0));
let kernel = Neighborhood::<f32, 3, 3>::identity_3x3();
let border = Clamp;
let out_region = BorderPolicy::<Image<MonoF32>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut out = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
convolve_into(&src, &kernel, &border, &mut out);
for y in 0..out.height() {
for x in 0..out.width() {
assert!((out.pixel_at(x, y).0 - 3.0).abs() < 1e-6);
}
}
}
#[test]
fn convolve_with_skip_shrinks_output() {
let src = Image::generate(6, 6, |x, y| MonoF32((x + y) as f32));
let kernel = Neighborhood::<f32, 3, 3>::identity_3x3();
let result: Image<MonoF32> = convolve(&src, &kernel, &Skip);
assert_eq!(result.width(), 4);
assert_eq!(result.height(), 4);
for y in 0..4 {
for x in 0..4 {
assert!((result.pixel_at(x, y).0 - src.pixel_at(x + 1, y + 1).0).abs() < 1e-6,);
}
}
}
#[test]
fn convolve_constant_border_zero_padding() {
let src = Image::fill(3, 3, MonoF32(1.0));
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let border = Constant(MonoF32(0.0));
let result: Image<MonoF32> = convolve(&src, &kernel, &border);
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
assert!((result.pixel_at(1, 1).0 - 1.0).abs() < 1e-5);
assert!((result.pixel_at(0, 0).0 - 4.0 / 9.0).abs() < 1e-5);
assert!((result.pixel_at(1, 0).0 - 6.0 / 9.0).abs() < 1e-5);
}
#[test]
fn convolve_u8_identity() {
let src = Image::generate(5, 5, |x, y| Mono8::new(((x + y * 5) % 256) as u8));
let kernel = Neighborhood::<f32, 3, 3>::identity_3x3();
let result: Image<Mono8> = convolve(&src, &kernel, &Clamp);
for y in 0..5 {
for x in 0..5 {
assert_eq!(result.pixel_at(x, y), src.pixel_at(x, y));
}
}
}
#[test]
fn convolve_u8_box_blur_uniform() {
let src = Image::fill(4, 4, Mono8::new(100));
let kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let result: Image<Mono8> = convolve(&src, &kernel, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), Mono8::new(100));
}
}
}
#[test]
fn convolve_5x5_box_blur_uniform() {
let src = Image::fill(8, 8, MonoF32(7.0));
let kernel = Neighborhood::<f32, 5, 5>::box_blur_5x5();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 7.0).abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn convolve_and_convolve_into_match() {
let src = make_4x4_monof32();
let kernel = Neighborhood::<f32, 3, 3>::gaussian_3x3();
let result_alloc: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
let border = Clamp;
let out_region = BorderPolicy::<Image<MonoF32>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut result_into = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
convolve_into(&src, &kernel, &border, &mut result_into);
for y in 0..result_alloc.height() {
for x in 0..result_alloc.width() {
assert!(
(result_alloc.pixel_at(x, y).0 - result_into.pixel_at(x, y).0).abs() < 1e-6,
);
}
}
}
#[test]
fn correlate_and_correlate_into_match() {
let src = make_4x4_monof32();
let kernel = Neighborhood::<f32, 3, 3>::sobel_x();
let result_alloc: Image<MonoF32> = correlate(&src, &kernel, &Clamp);
let border = Clamp;
let out_region = BorderPolicy::<Image<MonoF32>>::output_region(
&border,
src.size(),
kernel.weights().size(),
kernel.anchor(),
);
let mut result_into = Image::<MonoF32>::zero(out_region.size.width, out_region.size.height);
correlate_into(&src, &kernel, &border, &mut result_into);
for y in 0..result_alloc.height() {
for x in 0..result_alloc.width() {
assert!(
(result_alloc.pixel_at(x, y).0 - result_into.pixel_at(x, y).0).abs() < 1e-6,
);
}
}
}
#[test]
fn sobel_y_on_horizontal_gradient() {
let src = Image::generate(5, 5, |x, _y| MonoF32(x as f32));
let kernel = Neighborhood::<f32, 3, 3>::sobel_y();
let result: Image<MonoF32> = convolve(&src, &kernel, &Skip);
assert_eq!(result.width(), 3);
assert_eq!(result.height(), 3);
let expected = result.pixel_at(0, 0).0;
for y in 0..3 {
for x in 0..3 {
assert!(
(result.pixel_at(x, y).0 - expected).abs() < 1e-4,
"at ({x}, {y}): got {}, expected {expected}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn gaussian_3x3_unnormalised_on_uniform() {
let src = Image::fill(5, 5, MonoF32(1.0));
let kernel = Neighborhood::<f32, 3, 3>::gaussian_3x3();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 16.0).abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn convolve_single_pixel_clamp() {
let src = Image::fill(1, 1, MonoF32(42.0));
let kernel = Neighborhood::<f32, 3, 3>::identity_3x3();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert!((result.pixel_at(0, 0).0 - 42.0).abs() < 1e-6);
}
#[test]
fn laplacian_on_uniform_is_zero() {
let src = Image::fill(6, 6, MonoF32(10.0));
let kernel = Neighborhood::<f32, 3, 3>::laplacian();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
result.pixel_at(x, y).0.abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn laplacian_8_on_uniform_is_zero() {
let src = Image::fill(6, 6, MonoF32(7.0));
let kernel = Neighborhood::<f32, 3, 3>::laplacian_8();
let result: Image<MonoF32> = convolve(&src, &kernel, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
result.pixel_at(x, y).0.abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn prewitt_on_uniform_is_zero() {
let src = Image::fill(5, 5, MonoF32(3.0));
let kx = Neighborhood::<f32, 3, 3>::prewitt_x();
let ky = Neighborhood::<f32, 3, 3>::prewitt_y();
let rx: Image<MonoF32> = convolve(&src, &kx, &Clamp);
let ry: Image<MonoF32> = convolve(&src, &ky, &Clamp);
for y in 0..rx.height() {
for x in 0..rx.width() {
assert!(rx.pixel_at(x, y).0.abs() < 1e-4);
assert!(ry.pixel_at(x, y).0.abs() < 1e-4);
}
}
}
#[test]
fn scharr_on_uniform_is_zero() {
let src = Image::fill(5, 5, MonoF32(3.0));
let kx = Neighborhood::<f32, 3, 3>::scharr_x();
let ky = Neighborhood::<f32, 3, 3>::scharr_y();
let rx: Image<MonoF32> = convolve(&src, &kx, &Clamp);
let ry: Image<MonoF32> = convolve(&src, &ky, &Clamp);
for y in 0..rx.height() {
for x in 0..rx.width() {
assert!(rx.pixel_at(x, y).0.abs() < 1e-4);
assert!(ry.pixel_at(x, y).0.abs() < 1e-4);
}
}
}
}