use crate::border::BorderPolicy;
use crate::image::{
Image, ImageRef, ImageRefMut, ImageView, RasterImage, RasterImageMut, SeparableWeights,
};
use crate::pixel::{FromLinear, LinearPixel, ZeroablePixel};
use crate::transform::fold::{
FoldItem, FoldOp, FoldScratch, fold_neighborhood, fold_neighborhood_into,
fold_neighborhood_into_with_scratch,
};
pub(crate) struct HFold<P, Acc> {
_marker: core::marker::PhantomData<(P, Acc)>,
}
impl<P, Acc> HFold<P, Acc> {
#[inline(always)]
pub(crate) fn new() -> Self {
Self {
_marker: core::marker::PhantomData,
}
}
}
impl<P, Acc> FoldOp<P, f32> for HFold<P, Acc>
where
P: Copy + LinearPixel<f32, Accumulator = Acc>,
Acc: Copy + Default + std::ops::Add<Output = Acc>,
{
type Accumulator = Acc;
type Output = Acc;
#[inline(always)]
fn init(&self) -> Acc {
Acc::default()
}
#[inline(always)]
fn accumulate(&self, acc: &mut Acc, item: FoldItem<P, f32>) {
*acc = item.pixel.scale_add(item.weight, *acc);
}
#[inline(always)]
fn finalize(&mut self, acc: Acc) -> Acc {
acc
}
}
pub(crate) struct VFold<Acc, Out> {
_marker: core::marker::PhantomData<(Acc, Out)>,
}
impl<Acc, Out> VFold<Acc, Out> {
#[inline(always)]
pub(crate) fn new() -> Self {
Self {
_marker: core::marker::PhantomData,
}
}
}
impl<Acc, Out> FoldOp<Acc, f32> for VFold<Acc, Out>
where
Acc: Copy + Default + LinearPixel<f32, Accumulator = Acc> + std::ops::Add<Output = Acc>,
Out: FromLinear<Acc>,
{
type Accumulator = Acc;
type Output = Out;
#[inline(always)]
fn init(&self) -> Acc {
Acc::default()
}
#[inline(always)]
fn accumulate(&self, acc: &mut Acc, item: FoldItem<Acc, f32>) {
*acc = item.pixel.scale_add(item.weight, *acc);
}
#[inline(always)]
fn finalize(&mut self, acc: Acc) -> Out {
Out::from_linear(acc)
}
}
pub fn convolve_separable_into<I, B, K, O, P, Acc, Out>(
image: &I,
kernel: &K,
border: &B,
output: &mut O,
) where
I: RasterImage<Pixel = P>,
K: SeparableWeights,
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>>,
O: RasterImageMut<Pixel = Out>,
Out: FromLinear<Acc>,
{
let flipped = kernel.flipped();
let (h, v) = weight_views(&flipped);
correlate_separable_raw_into(
image,
&h,
flipped.h_anchor(),
&v,
flipped.v_anchor(),
border,
output,
);
}
fn weight_views<K: SeparableWeights>(kernel: &K) -> (ImageRef<'_, f32>, ImageRef<'_, f32>) {
let h_weights = kernel.h_weights();
let v_weights = kernel.v_weights();
assert!(
!h_weights.is_empty() && !v_weights.is_empty(),
"SeparableWeights contract violated: h_weights() and v_weights() must be non-empty \
(h has {} taps, v has {})",
h_weights.len(),
v_weights.len(),
);
assert!(
kernel.h_anchor() < h_weights.len() && kernel.v_anchor() < v_weights.len(),
"SeparableWeights contract violated: anchors must index their own axis \
(h_anchor() {} of {} taps, v_anchor() {} of {})",
kernel.h_anchor(),
h_weights.len(),
kernel.v_anchor(),
v_weights.len(),
);
let h = ImageRef::new(h_weights.len(), 1, h_weights).expect("h kernel view: 1 row");
let v = ImageRef::new(1, v_weights.len(), v_weights).expect("v kernel view: 1 column");
(h, v)
}
#[must_use]
pub fn convolve_separable<I, B, K, P, Acc, Out>(image: &I, kernel: &K, border: &B) -> Image<Out>
where
I: RasterImage<Pixel = P>,
K: SeparableWeights,
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>,
{
let flipped = kernel.flipped();
let (h, v) = weight_views(&flipped);
correlate_separable_raw(
image,
&h,
flipped.h_anchor(),
&v,
flipped.v_anchor(),
border,
)
}
#[derive(Debug, Clone)]
pub struct SeparableScratch<Acc> {
intermediate: Vec<Acc>,
fold: FoldScratch<Acc, f32>,
}
impl<Acc> SeparableScratch<Acc> {
#[must_use]
pub const fn new() -> Self {
Self {
intermediate: Vec::new(),
fold: FoldScratch::new(),
}
}
}
impl<Acc> Default for SeparableScratch<Acc> {
fn default() -> Self {
Self::new()
}
}
impl<Acc> SeparableScratch<Acc>
where
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
{
pub fn convolve_separable_into<I, B, K, O, P, Out>(
&mut self,
image: &I,
kernel: &K,
border: &B,
output: &mut O,
) where
I: RasterImage<Pixel = P>,
K: SeparableWeights,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
B: BorderPolicy<I> + for<'r> BorderPolicy<ImageRef<'r, Acc>>,
O: RasterImageMut<Pixel = Out>,
Out: FromLinear<Acc>,
{
let flipped = kernel.flipped();
let (h, v) = weight_views(&flipped);
self.correlate_separable_raw_into(
image,
(&h, flipped.h_anchor()),
(&v, flipped.v_anchor()),
border,
output,
);
}
}
pub(crate) fn correlate_separable_raw_into<I, HW, VW, B, O, P, Acc, Out>(
image: &I,
h_weights: &HW,
h_anchor: usize,
v_weights: &VW,
v_anchor: usize,
border: &B,
output: &mut O,
) where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
HW: ImageView<Pixel = f32>,
VW: ImageView<Pixel = f32>,
B: BorderPolicy<I> + BorderPolicy<Image<Acc>>,
O: RasterImageMut<Pixel = Out>,
Out: FromLinear<Acc>,
{
let intermediate: Image<Acc> = fold_neighborhood(
image,
h_weights,
(h_anchor, 0),
border,
HFold::<P, Acc>::new(),
);
fold_neighborhood_into(
&intermediate,
v_weights,
(0, v_anchor),
border,
output,
VFold::<Acc, Out>::new(),
);
}
impl<Acc> SeparableScratch<Acc>
where
Acc: Copy
+ Default
+ ZeroablePixel
+ LinearPixel<f32, Accumulator = Acc>
+ std::ops::Add<Output = Acc>,
{
pub(crate) fn correlate_separable_raw_into<I, HW, VW, B, O, P, Out>(
&mut self,
image: &I,
h: (&HW, usize),
v: (&VW, usize),
border: &B,
output: &mut O,
) where
I: RasterImage<Pixel = P>,
P: Copy + LinearPixel<f32, Accumulator = Acc>,
HW: ImageView<Pixel = f32>,
VW: ImageView<Pixel = f32>,
B: BorderPolicy<I> + for<'r> BorderPolicy<ImageRef<'r, Acc>>,
O: RasterImageMut<Pixel = Out>,
Out: FromLinear<Acc>,
{
let (h_weights, h_anchor) = h;
let (v_weights, v_anchor) = v;
let mid = <B as BorderPolicy<I>>::output_region(
border,
image.size(),
h_weights.size(),
(h_anchor, 0),
)
.size;
let area = mid
.checked_area()
.expect("intermediate area overflows usize");
let Self { intermediate, fold } = self;
if intermediate.len() < area {
intermediate.resize(area, Acc::default());
}
{
let mut mid_view = ImageRefMut::new(mid.width, mid.height, &mut intermediate[..area])
.expect("intermediate view: len == area");
fold_neighborhood_into_with_scratch(
image,
h_weights,
(h_anchor, 0),
border,
&mut mid_view,
HFold::<P, Acc>::new(),
fold,
);
}
let mid_view = ImageRef::new(mid.width, mid.height, &intermediate[..area])
.expect("intermediate view: len == area");
fold_neighborhood_into_with_scratch(
&mid_view,
v_weights,
(0, v_anchor),
border,
output,
VFold::<Acc, Out>::new(),
fold,
);
}
}
pub(crate) fn correlate_separable_raw<I, HW, VW, B, P, Acc, Out>(
image: &I,
h_weights: &HW,
h_anchor: usize,
v_weights: &VW,
v_anchor: usize,
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>,
HW: ImageView<Pixel = f32>,
VW: ImageView<Pixel = f32>,
B: BorderPolicy<I> + BorderPolicy<Image<Acc>>,
Out: ZeroablePixel + FromLinear<Acc>,
{
let intermediate_region = <B as BorderPolicy<I>>::output_region(
border,
image.size(),
h_weights.size(),
(h_anchor, 0),
);
let output_region = <B as BorderPolicy<Image<Acc>>>::output_region(
border,
intermediate_region.size,
v_weights.size(),
(0, v_anchor),
);
let mut out = Image::<Out>::zero(output_region.size.width, output_region.size.height);
correlate_separable_raw_into(
image, h_weights, h_anchor, v_weights, v_anchor, border, &mut out,
);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::border::{Clamp, Constant, Skip};
use crate::image::{ImageView, Neighborhood, SeparableKernel};
use crate::pixel::{Mono8, MonoF32};
use crate::transform::convolve;
fn make_4x4_monof32() -> Image<MonoF32> {
Image::generate(4, 4, |x, y| MonoF32((x + y * 4) as f32))
}
fn make_6x6_monof32() -> Image<MonoF32> {
Image::generate(6, 6, |x, y| MonoF32((x + y * 6) as f32))
}
#[test]
fn sep_kernel_identity_preserves_image() {
let src = make_4x4_monof32();
let kernel = SeparableKernel::new([1.0], [1.0]);
let result: Image<MonoF32> = convolve_separable(&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})",
);
}
}
}
#[test]
fn sep_kernel_box_blur_3_matches_full() {
let src = make_6x6_monof32();
let full_kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep = SeparableKernel::box_blur_3();
let sep_result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 - sep_result.pixel_at(x, y).0).abs() < 1e-4,
"mismatch at ({x}, {y}): full={}, sep={}",
full_result.pixel_at(x, y).0,
sep_result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn sep_kernel_box_blur_5_matches_full() {
let src = Image::generate(8, 8, |x, y| MonoF32((x * 3 + y * 7) as f32));
let full_kernel = Neighborhood::<f32, 5, 5>::box_blur_5x5();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep = SeparableKernel::box_blur_5();
let sep_result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 - sep_result.pixel_at(x, y).0).abs() < 1e-3,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn sep_kernel_gaussian_3_matches_full() {
let src = make_6x6_monof32();
let full_kernel = Neighborhood::<f32, 3, 3>::gaussian_3x3();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep = SeparableKernel::gaussian_3();
let sep_result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 / 16.0 - sep_result.pixel_at(x, y).0).abs()
< 1e-3,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn sep_kernel_gaussian_5_matches_full() {
let src = Image::generate(10, 10, |x, y| MonoF32((x + y) as f32));
let full_kernel = Neighborhood::<f32, 5, 5>::gaussian_5x5();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let sep = SeparableKernel::gaussian_5();
let sep_result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 / 256.0 - sep_result.pixel_at(x, y).0).abs()
< 1e-3,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn sep_kernel_uniform_stays_uniform() {
let src = Image::fill(8, 8, MonoF32(42.0));
let sep = SeparableKernel::box_blur_3();
let result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 42.0).abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn sep_kernel_u8_round_trip() {
let src = Image::fill(6, 6, Mono8::new(100));
let sep = SeparableKernel::box_blur_3();
let result: Image<Mono8> = convolve_separable(&src, &sep, &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 sep_kernel_into_matches_allocating() {
let src = make_6x6_monof32();
let sep = SeparableKernel::gaussian_3();
let alloc_result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
let mut into_result = Image::<MonoF32>::zero(alloc_result.width(), alloc_result.height());
convolve_separable_into(&src, &sep, &Clamp, &mut into_result);
for y in 0..alloc_result.height() {
for x in 0..alloc_result.width() {
assert!(
(alloc_result.pixel_at(x, y).0 - into_result.pixel_at(x, y).0).abs() < 1e-6,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn sep_kernel_skip_shrinks_output() {
let src = Image::generate(8, 8, |x, y| MonoF32((x + y) as f32));
let sep = SeparableKernel::box_blur_3();
let result: Image<MonoF32> = convolve_separable(&src, &sep, &Skip);
assert!(result.width() <= 8);
assert!(result.height() <= 8);
}
#[test]
fn sep_kernel_constant_border_single_pixel() {
let src = Image::fill(1, 1, MonoF32(9.0));
let border = Constant(MonoF32(0.0));
let sep = SeparableKernel::box_blur_3();
let result: Image<MonoF32> = convolve_separable(&src, &sep, &border);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert!(
(result.pixel_at(0, 0).0 - 1.0).abs() < 1e-4,
"got {}",
result.pixel_at(0, 0).0,
);
}
#[test]
fn sep_kernel_clamp_single_pixel() {
let src = Image::fill(1, 1, MonoF32(7.0));
let sep = SeparableKernel::box_blur_3();
let result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
assert!((result.pixel_at(0, 0).0 - 7.0).abs() < 1e-4);
}
#[test]
fn sep_kernel_large_image_no_panic() {
let src = Image::fill(100, 100, MonoF32(1.0));
let sep = SeparableKernel::gaussian_5();
let result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
assert_eq!(result.width(), 100);
assert_eq!(result.height(), 100);
}
#[test]
fn sep_kernel_matches_raw_api() {
let src = make_6x6_monof32();
let sep = SeparableKernel::gaussian_3();
let sep_result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
let h = Neighborhood::<f32, 3, 1>::gaussian_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::gaussian_1d_3_v();
let raw_result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(sep_result.width(), raw_result.width());
assert_eq!(sep_result.height(), raw_result.height());
for y in 0..sep_result.height() {
for x in 0..sep_result.width() {
assert!(
(sep_result.pixel_at(x, y).0 - raw_result.pixel_at(x, y).0 / 16.0).abs() < 1e-4,
"mismatch at ({x}, {y}): sep={}, raw/16={}",
sep_result.pixel_at(x, y).0,
raw_result.pixel_at(x, y).0 / 16.0,
);
}
}
}
#[test]
fn sep_kernel_asymmetric_weights() {
let src = Image::generate(5, 5, |x, y| MonoF32((x * 10 + y) as f32));
let sep = SeparableKernel::with_anchors([1.0, 0.0, 0.0], 1, [0.0, 0.0, 1.0], 1);
let result: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
let h = Neighborhood::<f32, 3, 1>::with_anchor([0.0, 0.0, 1.0], (1, 0));
let v = Neighborhood::<f32, 1, 3>::with_anchor([1.0, 0.0, 0.0], (0, 1));
let raw: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - raw.pixel_at(x, y).0).abs() < 1e-4,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn scratch_reuse_matches_owned() {
let src = make_6x6_monof32();
let sep = SeparableKernel::gaussian_5();
let expected: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
let mut scratch = SeparableScratch::new();
for round in 0..2 {
let mut actual = Image::<MonoF32>::zero(expected.width(), expected.height());
scratch.convolve_separable_into(&src, &sep, &Clamp, &mut actual);
for y in 0..expected.height() {
for x in 0..expected.width() {
assert!(
(expected.pixel_at(x, y).0 - actual.pixel_at(x, y).0).abs() < 1e-6,
"round {round}: mismatch at ({x}, {y}): owned={}, scratch={}",
expected.pixel_at(x, y).0,
actual.pixel_at(x, y).0,
);
}
}
}
}
#[test]
fn scratch_handles_size_change() {
let sizes = [(6usize, 6usize), (13, 11), (4, 5)];
let sep = SeparableKernel::gaussian_3();
let mut scratch = SeparableScratch::new();
for (w, h) in sizes {
let src = Image::generate(w, h, |x, y| MonoF32((x * 3 + y * 7) as f32));
let expected: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
let mut actual = Image::<MonoF32>::zero(expected.width(), expected.height());
scratch.convolve_separable_into(&src, &sep, &Clamp, &mut actual);
for y in 0..expected.height() {
for x in 0..expected.width() {
assert!(
(expected.pixel_at(x, y).0 - actual.pixel_at(x, y).0).abs() < 1e-4,
"{w}×{h}: mismatch at ({x}, {y}): owned={}, scratch={}",
expected.pixel_at(x, y).0,
actual.pixel_at(x, y).0,
);
}
}
}
}
#[test]
fn scratch_matches_owned_for_shrinking_border() {
let src = Image::generate(9, 7, |x, y| MonoF32((x + y * 9) as f32));
let sep = SeparableKernel::box_blur_5();
let expected: Image<MonoF32> = convolve_separable(&src, &sep, &Skip);
let mut scratch = SeparableScratch::new();
let mut actual = Image::<MonoF32>::zero(expected.width(), expected.height());
scratch.convolve_separable_into(&src, &sep, &Skip, &mut actual);
assert!(expected.width() < src.width() && expected.height() < src.height());
for y in 0..expected.height() {
for x in 0..expected.width() {
assert!(
(expected.pixel_at(x, y).0 - actual.pixel_at(x, y).0).abs() < 1e-4,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn scratch_shared_across_kernels_and_pixel_types() {
let mut scratch = SeparableScratch::<MonoF32>::new();
let f32_src = make_6x6_monof32();
let sep3 = SeparableKernel::gaussian_3();
let f32_expected: Image<MonoF32> = convolve_separable(&f32_src, &sep3, &Clamp);
let mut f32_out = Image::<MonoF32>::zero(6, 6);
scratch.convolve_separable_into(&f32_src, &sep3, &Clamp, &mut f32_out);
let u8_src = Image::fill(8, 8, Mono8::new(100));
let sep5 = SeparableKernel::box_blur_5();
let mut u8_out = Image::<Mono8>::zero(8, 8);
scratch.convolve_separable_into(&u8_src, &sep5, &Clamp, &mut u8_out);
for y in 0..6 {
for x in 0..6 {
assert!((f32_expected.pixel_at(x, y).0 - f32_out.pixel_at(x, y).0).abs() < 1e-6);
}
}
for y in 0..8 {
for x in 0..8 {
assert_eq!(u8_out.pixel_at(x, y), Mono8::new(100));
}
}
}
#[test]
fn scratch_handles_empty_region() {
let src = Image::generate(3, 3, |x, y| MonoF32((x + y) as f32));
let sep = SeparableKernel::box_blur_5();
let expected: Image<MonoF32> = convolve_separable(&src, &sep, &Skip);
assert_eq!(expected.width(), 0);
let mut scratch = SeparableScratch::new();
let mut actual = Image::<MonoF32>::zero(expected.width(), expected.height());
scratch.convolve_separable_into(&src, &sep, &Skip, &mut actual);
assert_eq!(actual.width(), expected.width());
assert_eq!(actual.height(), expected.height());
}
#[test]
fn scratch_default_matches_new() {
let src = make_4x4_monof32();
let sep = SeparableKernel::box_blur_3();
let mut from_new = SeparableScratch::new();
let mut out_new = Image::<MonoF32>::zero(4, 4);
from_new.convolve_separable_into(&src, &sep, &Clamp, &mut out_new);
let mut from_default = SeparableScratch::default();
let mut out_default = Image::<MonoF32>::zero(4, 4);
from_default.convolve_separable_into(&src, &sep, &Clamp, &mut out_default);
for y in 0..4 {
for x in 0..4 {
assert_eq!(out_new.pixel_at(x, y).0, out_default.pixel_at(x, y).0);
}
}
}
#[test]
fn correlate_separable_matches_convolve_for_symmetric() {
let src = Image::generate(7, 7, |x, y| MonoF32((x * 5 + y * 2) as f32));
let sep = SeparableKernel::gaussian_5();
let convolved: Image<MonoF32> = convolve_separable(&src, &sep, &Clamp);
let h = Neighborhood::<f32, 5, 1>::gaussian_1d_5_h();
let v = Neighborhood::<f32, 1, 5>::gaussian_1d_5_v();
let correlated: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(convolved.width(), correlated.width());
assert_eq!(convolved.height(), correlated.height());
for y in 0..convolved.height() {
for x in 0..convolved.width() {
assert!(
(convolved.pixel_at(x, y).0 - correlated.pixel_at(x, y).0 / 256.0).abs() < 1e-3,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn separable_identity_preserves_image() {
let src = make_4x4_monof32();
let h = Neighborhood::<f32, 1, 1>::new([1.0]);
let v = Neighborhood::<f32, 1, 1>::new([1.0]);
let result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&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 separable_box_blur_3x3_matches_full() {
let src = make_6x6_monof32();
let full_kernel = Neighborhood::<f32, 3, 3>::box_blur_3x3();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let h = Neighborhood::<f32, 3, 1>::box_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::box_1d_3_v();
let sep_result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 - sep_result.pixel_at(x, y).0).abs() < 1e-4,
"mismatch at ({x}, {y}): full={}, sep={}",
full_result.pixel_at(x, y).0,
sep_result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn separable_box_blur_5x5_matches_full() {
let src = Image::generate(8, 8, |x, y| MonoF32((x * 3 + y * 7) as f32));
let full_kernel = Neighborhood::<f32, 5, 5>::box_blur_5x5();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let h = Neighborhood::<f32, 5, 1>::box_1d_5_h();
let v = Neighborhood::<f32, 1, 5>::box_1d_5_v();
let sep_result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 - sep_result.pixel_at(x, y).0).abs() < 1e-3,
"mismatch at ({x}, {y}): full={}, sep={}",
full_result.pixel_at(x, y).0,
sep_result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn separable_gaussian_3x3_matches_full() {
let src = make_6x6_monof32();
let full_kernel = Neighborhood::<f32, 3, 3>::gaussian_3x3();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let h = Neighborhood::<f32, 3, 1>::gaussian_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::gaussian_1d_3_v();
let sep_result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 - sep_result.pixel_at(x, y).0).abs() < 1e-3,
"mismatch at ({x}, {y}): full={}, sep={}",
full_result.pixel_at(x, y).0,
sep_result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn separable_gaussian_5x5_matches_full() {
let src = Image::generate(10, 10, |x, y| MonoF32((x + y) as f32));
let full_kernel = Neighborhood::<f32, 5, 5>::gaussian_5x5();
let full_result: Image<MonoF32> = convolve(&src, &full_kernel, &Clamp);
let h = Neighborhood::<f32, 5, 1>::gaussian_1d_5_h();
let v = Neighborhood::<f32, 1, 5>::gaussian_1d_5_v();
let sep_result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(full_result.width(), sep_result.width());
assert_eq!(full_result.height(), sep_result.height());
for y in 0..full_result.height() {
for x in 0..full_result.width() {
assert!(
(full_result.pixel_at(x, y).0 - sep_result.pixel_at(x, y).0).abs() < 1e-2,
"mismatch at ({x}, {y}): full={}, sep={}",
full_result.pixel_at(x, y).0,
sep_result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn separable_box_blur_uniform_image() {
let src = Image::fill(8, 8, MonoF32(42.0));
let h = Neighborhood::<f32, 3, 1>::box_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::box_1d_3_v();
let result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
for y in 0..result.height() {
for x in 0..result.width() {
assert!(
(result.pixel_at(x, y).0 - 42.0).abs() < 1e-4,
"at ({x}, {y}): {}",
result.pixel_at(x, y).0,
);
}
}
}
#[test]
fn separable_box_blur_u8_uniform() {
let src = Image::fill(6, 6, Mono8::new(100));
let h = Neighborhood::<f32, 3, 1>::box_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::box_1d_3_v();
let result: Image<Mono8> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&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 separable_into_matches_allocating() {
let src = make_6x6_monof32();
let h = Neighborhood::<f32, 3, 1>::gaussian_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::gaussian_1d_3_v();
let alloc_result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
let mut into_result = Image::<MonoF32>::zero(alloc_result.width(), alloc_result.height());
correlate_separable_raw_into(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
&mut into_result,
);
for y in 0..alloc_result.height() {
for x in 0..alloc_result.width() {
assert!(
(alloc_result.pixel_at(x, y).0 - into_result.pixel_at(x, y).0).abs() < 1e-6,
"mismatch at ({x}, {y})",
);
}
}
}
#[test]
fn separable_skip_shrinks_output() {
let src = Image::generate(8, 8, |x, y| MonoF32((x + y) as f32));
let h = Neighborhood::<f32, 3, 1>::box_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::box_1d_3_v();
let result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Skip,
);
assert!(result.width() <= 8);
assert!(result.height() <= 8);
}
#[test]
fn separable_constant_border_single_pixel() {
let src = Image::fill(1, 1, MonoF32(9.0));
let border = Constant(MonoF32(0.0));
let h = Neighborhood::<f32, 3, 1>::box_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::box_1d_3_v();
let result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&border,
);
assert_eq!(result.width(), 1);
assert_eq!(result.height(), 1);
assert!(
(result.pixel_at(0, 0).0 - 1.0).abs() < 1e-4,
"got {}",
result.pixel_at(0, 0).0,
);
}
#[test]
fn separable_clamp_single_pixel() {
let src = Image::fill(1, 1, MonoF32(7.0));
let h = Neighborhood::<f32, 3, 1>::box_1d_3_h();
let v = Neighborhood::<f32, 1, 3>::box_1d_3_v();
let result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert!((result.pixel_at(0, 0).0 - 7.0).abs() < 1e-4);
}
#[test]
fn separable_large_image_no_panic() {
let src = Image::fill(100, 100, MonoF32(1.0));
let h = Neighborhood::<f32, 5, 1>::gaussian_1d_5_h();
let v = Neighborhood::<f32, 1, 5>::gaussian_1d_5_v();
let result: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
assert_eq!(result.width(), 100);
assert_eq!(result.height(), 100);
}
#[test]
fn separable_order_h_then_v() {
let src = Image::generate(5, 5, |x, y| MonoF32((x * 10 + y) as f32));
let h = Neighborhood::<f32, 3, 1>::with_anchor([1.0, 0.0, 0.0], (1, 0));
let v = Neighborhood::<f32, 1, 3>::with_anchor([0.0, 0.0, 1.0], (0, 1));
let result_hv: Image<MonoF32> = correlate_separable_raw(
&src,
h.weights(),
h.anchor().0,
v.weights(),
v.anchor().1,
&Clamp,
);
let h2 = Neighborhood::<f32, 3, 1>::with_anchor([0.0, 0.0, 1.0], (1, 0));
let v2 = Neighborhood::<f32, 1, 3>::with_anchor([1.0, 0.0, 0.0], (0, 1));
let result_vh: Image<MonoF32> = correlate_separable_raw(
&src,
h2.weights(),
h2.anchor().0,
v2.weights(),
v2.anchor().1,
&Clamp,
);
let mut differ = false;
for y in 0..result_hv.height() {
for x in 0..result_hv.width() {
if (result_hv.pixel_at(x, y).0 - result_vh.pixel_at(x, y).0).abs() > 1e-4 {
differ = true;
}
}
}
assert!(
differ,
"swapping asymmetric kernels should produce different results"
);
}
struct EmptyAxis;
impl crate::image::SeparableWeights for EmptyAxis {
fn h_weights(&self) -> &[f32] {
&[]
}
fn h_anchor(&self) -> usize {
0
}
fn v_weights(&self) -> &[f32] {
&[1.0]
}
fn v_anchor(&self) -> usize {
0
}
fn flipped(&self) -> Self {
EmptyAxis
}
}
struct WildAnchor;
impl crate::image::SeparableWeights for WildAnchor {
fn h_weights(&self) -> &[f32] {
&[1.0, 1.0, 1.0]
}
fn h_anchor(&self) -> usize {
3
}
fn v_weights(&self) -> &[f32] {
&[1.0]
}
fn v_anchor(&self) -> usize {
0
}
fn flipped(&self) -> Self {
WildAnchor
}
}
#[test]
#[should_panic(expected = "SeparableWeights contract violated")]
fn an_empty_weight_axis_is_reported_by_trait_and_method_name() {
let src: Image<MonoF32> = Image::fill(4, 4, MonoF32::new(1.0));
let _: Image<MonoF32> = convolve_separable(&src, &EmptyAxis, &Clamp);
}
#[test]
#[should_panic(expected = "SeparableWeights contract violated")]
fn an_out_of_bounds_anchor_is_reported_by_trait_and_method_name() {
let src: Image<MonoF32> = Image::fill(4, 4, MonoF32::new(1.0));
let _: Image<MonoF32> = convolve_separable(&src, &WildAnchor, &Clamp);
}
}