use crate::image::Image;
#[derive(Clone, Debug)]
pub struct SeparableKernel<const HK: usize, const VK: usize> {
h_weights: [f32; HK],
h_anchor: usize,
v_weights: [f32; VK],
v_anchor: usize,
}
impl<const HK: usize, const VK: usize> SeparableKernel<HK, VK> {
const _ASSERT_NONZERO: () = {
assert!(
HK > 0,
"SeparableKernel: horizontal kernel length HK must be > 0"
);
assert!(
VK > 0,
"SeparableKernel: vertical kernel length VK must be > 0"
);
};
pub fn new(h_weights: [f32; HK], v_weights: [f32; VK]) -> Self {
let () = Self::_ASSERT_NONZERO;
Self {
h_weights,
h_anchor: HK / 2,
v_weights,
v_anchor: VK / 2,
}
}
pub fn with_anchors(
h_weights: [f32; HK],
h_anchor: usize,
v_weights: [f32; VK],
v_anchor: usize,
) -> Self {
let () = Self::_ASSERT_NONZERO;
assert!(
h_anchor < HK,
"h_anchor ({h_anchor}) out of bounds for horizontal kernel of size {HK}"
);
assert!(
v_anchor < VK,
"v_anchor ({v_anchor}) out of bounds for vertical kernel of size {VK}"
);
Self {
h_weights,
h_anchor,
v_weights,
v_anchor,
}
}
pub fn h_weights(&self) -> &[f32; HK] {
&self.h_weights
}
pub fn v_weights(&self) -> &[f32; VK] {
&self.v_weights
}
pub fn h_anchor(&self) -> usize {
self.h_anchor
}
pub fn v_anchor(&self) -> usize {
self.v_anchor
}
pub fn flipped(&self) -> Self {
let mut h = self.h_weights;
h.reverse();
let mut v = self.v_weights;
v.reverse();
Self {
h_weights: h,
h_anchor: HK - 1 - self.h_anchor,
v_weights: v,
v_anchor: VK - 1 - self.v_anchor,
}
}
pub(crate) fn to_h_image(&self) -> Image<f32> {
Image::generate(HK, 1, |x, _y| self.h_weights[x])
}
pub(crate) fn to_v_image(&self) -> Image<f32> {
Image::generate(1, VK, |_x, y| self.v_weights[y])
}
}
impl<const K: usize> SeparableKernel<K, K> {
pub fn symmetric(weights: [f32; K]) -> Self {
let () = Self::_ASSERT_NONZERO;
Self {
h_weights: weights,
h_anchor: K / 2,
v_weights: weights,
v_anchor: K / 2,
}
}
}
impl SeparableKernel<3, 3> {
pub fn gaussian_3() -> Self {
Self::symmetric([1.0, 2.0, 1.0])
}
pub fn box_blur_3() -> Self {
let w = 1.0 / 3.0;
Self::symmetric([w, w, w])
}
}
impl SeparableKernel<5, 5> {
pub fn gaussian_5() -> Self {
Self::symmetric([1.0, 4.0, 6.0, 4.0, 1.0])
}
pub fn box_blur_5() -> Self {
let w = 1.0 / 5.0;
Self::symmetric([w, w, w, w, w])
}
}
impl<const HK: usize, const VK: usize> PartialEq for SeparableKernel<HK, VK> {
fn eq(&self, other: &Self) -> bool {
self.h_anchor == other.h_anchor
&& self.v_anchor == other.v_anchor
&& self.h_weights == other.h_weights
&& self.v_weights == other.v_weights
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::ImageView;
#[test]
fn new_centered_anchors() {
let k = SeparableKernel::new([1.0, 2.0, 1.0], [1.0, 4.0, 6.0, 4.0, 1.0]);
assert_eq!(k.h_anchor(), 1); assert_eq!(k.v_anchor(), 2); assert_eq!(k.h_weights(), &[1.0, 2.0, 1.0]);
assert_eq!(k.v_weights(), &[1.0, 4.0, 6.0, 4.0, 1.0]);
}
#[test]
fn new_even_sizes_center_left() {
let k = SeparableKernel::new([1.0; 4], [1.0; 2]);
assert_eq!(k.h_anchor(), 2); assert_eq!(k.v_anchor(), 1); }
#[test]
fn with_anchors_explicit() {
let k = SeparableKernel::with_anchors([1.0, 2.0, 3.0], 0, [4.0, 5.0], 1);
assert_eq!(k.h_anchor(), 0);
assert_eq!(k.v_anchor(), 1);
assert_eq!(k.h_weights(), &[1.0, 2.0, 3.0]);
assert_eq!(k.v_weights(), &[4.0, 5.0]);
}
#[test]
#[should_panic(expected = "h_anchor")]
fn with_anchors_h_out_of_bounds() {
SeparableKernel::with_anchors([1.0, 2.0, 3.0], 3, [1.0], 0);
}
#[test]
#[should_panic(expected = "v_anchor")]
fn with_anchors_v_out_of_bounds() {
SeparableKernel::with_anchors([1.0], 0, [1.0, 2.0], 2);
}
#[test]
fn symmetric_constructor() {
let k = SeparableKernel::symmetric([1.0, 2.0, 1.0]);
assert_eq!(k.h_weights(), k.v_weights());
assert_eq!(k.h_anchor(), k.v_anchor());
assert_eq!(k.h_anchor(), 1);
}
#[test]
fn flipped_reverses_weights() {
let k = SeparableKernel::new([1.0, 2.0, 3.0], [4.0, 5.0]);
let f = k.flipped();
assert_eq!(f.h_weights(), &[3.0, 2.0, 1.0]);
assert_eq!(f.v_weights(), &[5.0, 4.0]);
}
#[test]
fn flipped_mirrors_anchors() {
let k = SeparableKernel::with_anchors([1.0, 2.0, 3.0], 0, [4.0, 5.0, 6.0], 2);
let f = k.flipped();
assert_eq!(f.h_anchor(), 2); assert_eq!(f.v_anchor(), 0); }
#[test]
fn flipped_centered_anchor_stays_centered() {
let k = SeparableKernel::gaussian_3();
let f = k.flipped();
assert_eq!(f.h_anchor(), 1);
assert_eq!(f.v_anchor(), 1);
}
#[test]
fn flipped_involution() {
let k = SeparableKernel::with_anchors([1.0, 2.0, 3.0], 0, [4.0, 5.0], 1);
let ff = k.flipped().flipped();
assert_eq!(k, ff);
}
#[test]
fn flipped_symmetric_kernel_unchanged() {
let k = SeparableKernel::box_blur_3();
let f = k.flipped();
assert_eq!(k, f);
}
#[test]
fn flipped_gaussian_5_symmetric() {
let k = SeparableKernel::gaussian_5();
let f = k.flipped();
assert_eq!(k, f);
}
#[test]
fn gaussian_3_weights() {
let k = SeparableKernel::gaussian_3();
assert_eq!(k.h_weights(), &[1.0, 2.0, 1.0]);
assert_eq!(k.v_weights(), &[1.0, 2.0, 1.0]);
assert_eq!(k.h_anchor(), 1);
assert_eq!(k.v_anchor(), 1);
}
#[test]
fn gaussian_5_weights() {
let k = SeparableKernel::gaussian_5();
assert_eq!(k.h_weights(), &[1.0, 4.0, 6.0, 4.0, 1.0]);
assert_eq!(k.v_weights(), &[1.0, 4.0, 6.0, 4.0, 1.0]);
assert_eq!(k.h_anchor(), 2);
assert_eq!(k.v_anchor(), 2);
}
#[test]
fn box_blur_3_weights() {
let k = SeparableKernel::box_blur_3();
let third = 1.0f32 / 3.0;
for &w in k.h_weights() {
assert!((w - third).abs() < 1e-7);
}
for &w in k.v_weights() {
assert!((w - third).abs() < 1e-7);
}
}
#[test]
fn box_blur_5_weights() {
let k = SeparableKernel::box_blur_5();
let fifth = 1.0f32 / 5.0;
for &w in k.h_weights() {
assert!((w - fifth).abs() < 1e-7);
}
for &w in k.v_weights() {
assert!((w - fifth).abs() < 1e-7);
}
}
#[test]
fn gaussian_3_outer_product_matches_neighborhood() {
let sep = SeparableKernel::gaussian_3();
let full = crate::image::Neighborhood::<f32, 3, 3>::gaussian_3x3();
for y in 0..3 {
for x in 0..3 {
let outer = sep.h_weights()[x] * sep.v_weights()[y];
let expected = full.weights().pixel_at(x, y);
assert!(
(outer - expected).abs() < 1e-6,
"mismatch at ({x}, {y}): outer={outer}, expected={expected}"
);
}
}
}
#[test]
fn gaussian_5_outer_product_matches_neighborhood() {
let sep = SeparableKernel::gaussian_5();
let full = crate::image::Neighborhood::<f32, 5, 5>::gaussian_5x5();
for y in 0..5 {
for x in 0..5 {
let outer = sep.h_weights()[x] * sep.v_weights()[y];
let expected = full.weights().pixel_at(x, y);
assert!(
(outer - expected).abs() < 1e-4,
"mismatch at ({x}, {y}): outer={outer}, expected={expected}"
);
}
}
}
#[test]
fn box_blur_3_outer_product_matches_neighborhood() {
let sep = SeparableKernel::box_blur_3();
let full = crate::image::Neighborhood::<f32, 3, 3>::box_blur_3x3();
for y in 0..3 {
for x in 0..3 {
let outer = sep.h_weights()[x] * sep.v_weights()[y];
let expected = full.weights().pixel_at(x, y);
assert!(
(outer - expected).abs() < 1e-6,
"mismatch at ({x}, {y}): outer={outer}, expected={expected}"
);
}
}
}
#[test]
fn box_blur_5_outer_product_matches_neighborhood() {
let sep = SeparableKernel::box_blur_5();
let full = crate::image::Neighborhood::<f32, 5, 5>::box_blur_5x5();
for y in 0..5 {
for x in 0..5 {
let outer = sep.h_weights()[x] * sep.v_weights()[y];
let expected = full.weights().pixel_at(x, y);
assert!(
(outer - expected).abs() < 1e-6,
"mismatch at ({x}, {y}): outer={outer}, expected={expected}"
);
}
}
}
#[test]
fn to_h_image_shape_and_content() {
let k = SeparableKernel::gaussian_3();
let arr = k.to_h_image();
assert_eq!(arr.width(), 3);
assert_eq!(arr.height(), 1);
assert_eq!(arr.pixel_at(0, 0), 1.0);
assert_eq!(arr.pixel_at(1, 0), 2.0);
assert_eq!(arr.pixel_at(2, 0), 1.0);
}
#[test]
fn to_v_image_shape_and_content() {
let k = SeparableKernel::gaussian_3();
let arr = k.to_v_image();
assert_eq!(arr.width(), 1);
assert_eq!(arr.height(), 3);
assert_eq!(arr.pixel_at(0, 0), 1.0);
assert_eq!(arr.pixel_at(0, 1), 2.0);
assert_eq!(arr.pixel_at(0, 2), 1.0);
}
#[test]
fn to_h_image_5() {
let k = SeparableKernel::gaussian_5();
let arr = k.to_h_image();
assert_eq!(arr.width(), 5);
assert_eq!(arr.height(), 1);
assert_eq!(arr.pixel_at(0, 0), 1.0);
assert_eq!(arr.pixel_at(1, 0), 4.0);
assert_eq!(arr.pixel_at(2, 0), 6.0);
assert_eq!(arr.pixel_at(3, 0), 4.0);
assert_eq!(arr.pixel_at(4, 0), 1.0);
}
#[test]
fn to_v_image_5() {
let k = SeparableKernel::gaussian_5();
let arr = k.to_v_image();
assert_eq!(arr.width(), 1);
assert_eq!(arr.height(), 5);
assert_eq!(arr.pixel_at(0, 0), 1.0);
assert_eq!(arr.pixel_at(0, 1), 4.0);
assert_eq!(arr.pixel_at(0, 2), 6.0);
assert_eq!(arr.pixel_at(0, 3), 4.0);
assert_eq!(arr.pixel_at(0, 4), 1.0);
}
#[test]
fn clone_produces_equal_kernel() {
let k = SeparableKernel::with_anchors([1.0, 2.0, 3.0], 0, [4.0, 5.0], 1);
let c = k.clone();
assert_eq!(k, c);
}
#[test]
fn debug_format_contains_weights() {
let k = SeparableKernel::new([1.0, 2.0], [3.0]);
let dbg = format!("{k:?}");
assert!(dbg.contains("SeparableKernel"));
assert!(dbg.contains("h_weights"));
assert!(dbg.contains("v_weights"));
}
#[test]
fn partial_eq_different_weights() {
let a = SeparableKernel::new([1.0, 2.0, 3.0], [1.0]);
let b = SeparableKernel::new([3.0, 2.0, 1.0], [1.0]);
assert_ne!(a, b);
}
#[test]
fn partial_eq_different_anchors() {
let a = SeparableKernel::with_anchors([1.0, 2.0, 3.0], 0, [1.0], 0);
let b = SeparableKernel::with_anchors([1.0, 2.0, 3.0], 2, [1.0], 0);
assert_ne!(a, b);
}
#[test]
fn asymmetric_3x5() {
let k = SeparableKernel::new([1.0, 2.0, 1.0], [1.0, 4.0, 6.0, 4.0, 1.0]);
assert_eq!(k.h_anchor(), 1);
assert_eq!(k.v_anchor(), 2);
let f = k.flipped();
assert_eq!(f.h_weights(), &[1.0, 2.0, 1.0]);
assert_eq!(f.v_weights(), &[1.0, 4.0, 6.0, 4.0, 1.0]);
}
#[test]
fn asymmetric_weights_flip() {
let k = SeparableKernel::new([1.0, 0.0, 0.0], [0.0, 1.0]);
let f = k.flipped();
assert_eq!(f.h_weights(), &[0.0, 0.0, 1.0]);
assert_eq!(f.v_weights(), &[1.0, 0.0]);
}
#[test]
fn identity_1x1() {
let k = SeparableKernel::new([1.0], [1.0]);
assert_eq!(k.h_anchor(), 0);
assert_eq!(k.v_anchor(), 0);
let f = k.flipped();
assert_eq!(f, k);
}
}