use crate::border::BorderPolicy;
use crate::image::{Image, Neighborhood, RasterImage, SeparableKernel};
use crate::pixel::{FromLinear, LinearPixel, ZeroablePixel};
use crate::transform::convolve::convolve;
use crate::transform::convolve_separable::convolve_separable;
#[must_use]
pub fn box_blur_3x3<I, B, P, Acc, Out>(image: &I, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
B: BorderPolicy<I> + BorderPolicy<Image<Acc>>,
Out: ZeroablePixel + FromLinear<Acc>,
{
convolve_separable(image, &SeparableKernel::box_blur_3(), border)
}
#[must_use]
pub fn box_blur_5x5<I, B, P, Acc, Out>(image: &I, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
B: BorderPolicy<I> + BorderPolicy<Image<Acc>>,
Out: ZeroablePixel + FromLinear<Acc>,
{
convolve_separable(image, &SeparableKernel::box_blur_5(), border)
}
#[must_use]
pub fn gaussian_blur_3x3<I, B, P, Acc, Out>(image: &I, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
B: BorderPolicy<I> + BorderPolicy<Image<Acc>>,
Out: ZeroablePixel + FromLinear<Acc>,
{
convolve_separable(image, &SeparableKernel::gaussian_3(), border)
}
#[must_use]
pub fn gaussian_blur_5x5<I, B, P, Acc, Out>(image: &I, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
B: BorderPolicy<I> + BorderPolicy<Image<Acc>>,
Out: ZeroablePixel + FromLinear<Acc>,
{
convolve_separable(image, &SeparableKernel::gaussian_5(), border)
}
#[must_use]
pub fn sobel_x<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::sobel_y(), border)
}
#[must_use]
pub fn sobel_y<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::sobel_x(), border)
}
#[must_use]
pub fn scharr_x<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::scharr_y(), border)
}
#[must_use]
pub fn scharr_y<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::scharr_x(), border)
}
#[must_use]
pub fn prewitt_x<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::prewitt_y(), border)
}
#[must_use]
pub fn prewitt_y<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::prewitt_x(), border)
}
#[must_use]
pub fn laplacian<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::laplacian(), border)
}
#[must_use]
pub fn laplacian_8<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::laplacian_8(), border)
}
#[must_use]
pub fn sharpen<I, B, P, Out>(image: &I, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default,
B: BorderPolicy<I>,
Out: ZeroablePixel + FromLinear<<P as LinearPixel<f32>>::Accumulator>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::sharpen(), border)
}
#[must_use]
pub fn emboss<I, B, P>(image: &I, border: &B) -> Image<<P as LinearPixel<f32>>::Accumulator>
where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32>,
<P as LinearPixel<f32>>::Accumulator: Default + ZeroablePixel,
B: BorderPolicy<I>,
{
convolve(image, &Neighborhood::<f32, 3, 3>::emboss(), border)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::border::{Clamp, Skip};
use crate::image::{ImageView, ImageViewMut};
use crate::pixel::{Mono8, MonoF32};
use crate::transform::convolve;
fn make_gradient_8x8() -> Image<MonoF32> {
Image::generate(8, 8, |x, y| MonoF32::new((x + y * 8) as f32))
}
#[test]
fn box_blur_3x3_uniform_f32() {
let src = Image::fill(8, 8, MonoF32::new(7.0));
let result: Image<MonoF32> = box_blur_3x3(&src, &Clamp);
assert_eq!(result.width(), 8);
assert_eq!(result.height(), 8);
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 box_blur_3x3_uniform_u8() {
let src = Image::fill(8, 8, Mono8::new(100));
let result: Image<Mono8> = box_blur_3x3(&src, &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 box_blur_5x5_uniform_f32() {
let src = Image::fill(10, 10, MonoF32::new(3.0));
let result: Image<MonoF32> = box_blur_5x5(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!((result.pixel_at(x, y).0 - 3.0).abs() < 1e-4);
}
}
}
#[test]
fn box_blur_5x5_uniform_u8() {
let src = Image::fill(10, 10, Mono8::new(200));
let result: Image<Mono8> = box_blur_5x5(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert_eq!(result.pixel_at(x, y), Mono8::new(200));
}
}
}
#[test]
fn box_blur_3x3_matches_full_convolution() {
let src = make_gradient_8x8();
let full_kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let full: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep: Image<MonoF32> = box_blur_3x3(&src, &Clamp);
assert_eq!(full.width(), sep.width());
assert_eq!(full.height(), sep.height());
for y in 0..full.height() {
for x in 0..full.width() {
assert!(
(full.pixel_at(x, y).0 - sep.pixel_at(x, y).0).abs() < 1e-3,
"mismatch at ({x}, {y}): full={}, sep={}",
full.pixel_at(x, y).0,
sep.pixel_at(x, y).0,
);
}
}
}
#[test]
fn gaussian_blur_3x3_uniform_f32() {
let src = Image::fill(8, 8, MonoF32::new(1.0));
let result: Image<MonoF32> = gaussian_blur_3x3(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 16.0).abs() < 1e-3,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn gaussian_blur_3x3_matches_full_convolution() {
let src = make_gradient_8x8();
let full_kernel = Neighborhood::<f32, 3, 3>::gaussian_3x3();
let full: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep: Image<MonoF32> = gaussian_blur_3x3(&src, &Clamp);
for y in 0..full.height() {
for x in 0..full.width() {
assert!(
(full.pixel_at(x, y).0 - sep.pixel_at(x, y).0).abs() < 1e-2,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn gaussian_blur_5x5_uniform_f32() {
let src = Image::fill(10, 10, MonoF32::new(1.0));
let result: Image<MonoF32> = gaussian_blur_5x5(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!((result.pixel_at(x, y).0 - 256.0).abs() < 1e-1);
}
}
}
#[test]
fn gaussian_blur_5x5_matches_full_convolution() {
let src = make_gradient_8x8();
let full_kernel = Neighborhood::<f32, 5, 5>::gaussian_5x5();
let full: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep: Image<MonoF32> = gaussian_blur_5x5(&src, &Clamp);
for y in 0..full.height() {
for x in 0..full.width() {
assert!(
(full.pixel_at(x, y).0 - sep.pixel_at(x, y).0).abs() < 1e-1,
"mismatch at ({x}, {y}): full={}, sep={}",
full.pixel_at(x, y).0,
sep.pixel_at(x, y).0,
);
}
}
}
#[test]
fn sobel_x_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(50));
let result: Image<crate::pixel::MonoF32> = sobel_x(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn sobel_y_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(50));
let result: Image<crate::pixel::MonoF32> = sobel_y(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn sobel_x_on_horizontal_gradient() {
let src = Image::generate(8, 8, |x, _y| MonoF32::new(x as f32));
let result: Image<MonoF32> = sobel_x(&src, &Skip);
let first = result.pixel_at(0, 0);
assert!(first.0.abs() > 0.1, "expected non-zero response");
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - first.0).abs() < 1e-4,
"at ({x}, {y}): got {}, expected {}",
result.pixel_at(x, y).0,
first.0,
);
}
}
}
#[test]
fn sobel_y_on_vertical_gradient() {
let src = Image::generate(8, 8, |_x, y| MonoF32::new(y as f32));
let result: Image<MonoF32> = sobel_y(&src, &Skip);
let first = result.pixel_at(0, 0);
assert!(first.0.abs() > 0.1, "expected non-zero response");
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - first.0).abs() < 1e-4,
"at ({x}, {y}): got {}, expected {}",
result.pixel_at(x, y).0,
first.0,
);
}
}
}
#[test]
fn scharr_x_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(50));
let result: Image<crate::pixel::MonoF32> = scharr_x(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn scharr_y_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(50));
let result: Image<crate::pixel::MonoF32> = scharr_y(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn prewitt_x_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(50));
let result: Image<crate::pixel::MonoF32> = prewitt_x(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn prewitt_y_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(50));
let result: Image<crate::pixel::MonoF32> = prewitt_y(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn laplacian_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(10));
let result: Image<crate::pixel::MonoF32> = laplacian(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn laplacian_8_uniform_is_zero() {
let src = Image::fill(8, 8, Mono8::new(10));
let result: Image<crate::pixel::MonoF32> = laplacian_8(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(result.pixel_at(x, y).abs().0 < 1e-4);
}
}
}
#[test]
fn laplacian_matches_full_convolution() {
let src = make_gradient_8x8();
let full_kernel = Neighborhood::<f32, 3, 3>::laplacian();
let full: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let convenience: Image<MonoF32> = laplacian(&src, &Clamp);
for y in 0..full.height() {
for x in 0..full.width() {
assert!((full.pixel_at(x, y).0 - convenience.pixel_at(x, y).0).abs() < 1e-4);
}
}
}
#[test]
fn sharpen_uniform_is_identity() {
let src = Image::fill(8, 8, Mono8::new(100));
let result: Image<Mono8> = sharpen(&src, &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 sharpen_f32_uniform_is_identity() {
let src = Image::fill(8, 8, MonoF32::new(3.5));
let result: Image<MonoF32> = sharpen(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 3.5).abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn sharpen_matches_full_convolution() {
let src = make_gradient_8x8();
let full_kernel = Neighborhood::<f32, 3, 3>::sharpen();
let full: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let convenience: Image<MonoF32> = sharpen(&src, &Clamp);
for y in 0..full.height() {
for x in 0..full.width() {
assert!((full.pixel_at(x, y).0 - convenience.pixel_at(x, y).0).abs() < 1e-4);
}
}
}
#[test]
fn emboss_uniform_is_original() {
let src = Image::fill(8, 8, MonoF32::new(25.0));
let result: Image<MonoF32> = emboss(&src, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 25.0).abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn emboss_matches_full_convolution() {
let src = make_gradient_8x8();
let full_kernel = Neighborhood::<f32, 3, 3>::emboss();
let full: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let convenience: Image<MonoF32> = emboss(&src, &Clamp);
for y in 0..full.height() {
for x in 0..full.width() {
assert!((full.pixel_at(x, y).0 - convenience.pixel_at(x, y).0).abs() < 1e-4);
}
}
}
#[test]
fn sobel_detects_step_edge() {
let src = Image::generate(10, 10, |x, _y| {
MonoF32::new(if x < 5 { 0.0 } else { 100.0 })
});
let result: Image<MonoF32> = sobel_x(&src, &Clamp);
let edge_val = result.pixel_at(4, 5).0.abs();
let flat_val = result.pixel_at(1, 5).0.abs();
assert!(
edge_val > flat_val * 5.0,
"edge response ({edge_val}) should be much larger than flat ({flat_val})",
);
}
#[test]
fn laplacian_detects_blob() {
let mut src = Image::fill(7, 7, MonoF32::new(0.0));
*src.pixel_at_mut(3, 3) = MonoF32::new(100.0);
let result: Image<MonoF32> = laplacian(&src, &Clamp);
assert!(
result.pixel_at(3, 3).0 > 200.0,
"center Laplacian response should be large, got {}",
result.pixel_at(3, 3).0,
);
}
#[test]
fn all_filters_handle_single_pixel() {
let src_f32 = Image::fill(1, 1, MonoF32::new(42.0));
let src_u8 = Image::fill(1, 1, Mono8::new(42));
let _: Image<MonoF32> = box_blur_3x3(&src_f32, &Clamp);
let _: Image<MonoF32> = box_blur_5x5(&src_f32, &Clamp);
let _: Image<MonoF32> = gaussian_blur_3x3(&src_f32, &Clamp);
let _: Image<MonoF32> = gaussian_blur_5x5(&src_f32, &Clamp);
let _: Image<MonoF32> = sobel_x(&src_f32, &Clamp);
let _: Image<MonoF32> = sobel_y(&src_f32, &Clamp);
let _: Image<MonoF32> = scharr_x(&src_f32, &Clamp);
let _: Image<MonoF32> = scharr_y(&src_f32, &Clamp);
let _: Image<MonoF32> = prewitt_x(&src_f32, &Clamp);
let _: Image<MonoF32> = prewitt_y(&src_f32, &Clamp);
let _: Image<MonoF32> = laplacian(&src_f32, &Clamp);
let _: Image<MonoF32> = laplacian_8(&src_f32, &Clamp);
let _: Image<MonoF32> = sharpen(&src_f32, &Clamp);
let _: Image<MonoF32> = emboss(&src_f32, &Clamp);
let _: Image<crate::pixel::MonoF32> = sobel_x(&src_u8, &Clamp);
let _: Image<crate::pixel::MonoF32> = sobel_y(&src_u8, &Clamp);
}
}