use crate::error::{VisionError, VisionResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructuringElement {
pub width: usize,
pub height: usize,
pub anchor_x: usize,
pub anchor_y: usize,
pub mask: Vec<bool>,
}
impl StructuringElement {
pub fn new(width: usize, height: usize, mask: Vec<bool>) -> VisionResult<Self> {
let se = Self {
width,
height,
anchor_x: width / 2,
anchor_y: height / 2,
mask,
};
validate_se(&se)?;
Ok(se)
}
pub fn rect(width: usize, height: usize) -> VisionResult<Self> {
if width == 0 || height == 0 {
return Err(VisionError::EmptyInput("structuring element"));
}
Ok(Self {
width,
height,
anchor_x: width / 2,
anchor_y: height / 2,
mask: vec![true; width * height],
})
}
pub fn square(size: usize) -> VisionResult<Self> {
Self::rect(size, size)
}
#[must_use]
pub fn cross(radius: usize) -> Self {
let size = 2 * radius + 1;
let mut mask = vec![false; size * size];
for i in 0..size {
mask[radius * size + i] = true;
mask[i * size + radius] = true;
}
Self {
width: size,
height: size,
anchor_x: radius,
anchor_y: radius,
mask,
}
}
fn active_offsets(&self) -> Vec<(isize, isize)> {
let mut offsets = Vec::new();
for row in 0..self.height {
for col in 0..self.width {
if self.mask[row * self.width + col] {
offsets.push((
row as isize - self.anchor_y as isize,
col as isize - self.anchor_x as isize,
));
}
}
}
offsets
}
}
fn validate_se(se: &StructuringElement) -> VisionResult<()> {
if se.width == 0 || se.height == 0 {
return Err(VisionError::EmptyInput("structuring element"));
}
if se.mask.len() != se.width * se.height {
return Err(VisionError::DimensionMismatch {
expected: se.width * se.height,
got: se.mask.len(),
});
}
if se.anchor_x >= se.width || se.anchor_y >= se.height {
return Err(VisionError::Internal(format!(
"structuring-element anchor ({}, {}) out of bounds for {}×{}",
se.anchor_y, se.anchor_x, se.height, se.width
)));
}
if !se.mask.iter().any(|&m| m) {
return Err(VisionError::EmptyInput("structuring element"));
}
Ok(())
}
#[inline]
fn validate_gray(img: &[f32], h: usize, w: usize) -> VisionResult<()> {
if h == 0 || w == 0 {
return Err(VisionError::InvalidImageSize {
height: h,
width: w,
channels: 1,
});
}
if img.len() != h * w {
return Err(VisionError::DimensionMismatch {
expected: h * w,
got: img.len(),
});
}
Ok(())
}
fn validate_op(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<()> {
validate_gray(img, h, w)?;
validate_se(se)?;
if se.height > h || se.width > w {
return Err(VisionError::Internal(format!(
"structuring element {}×{} larger than image {}×{}",
se.height, se.width, h, w
)));
}
Ok(())
}
#[inline]
fn clamp_idx(v: isize, n: usize) -> usize {
if v < 0 {
0
} else if v as usize >= n {
n - 1
} else {
v as usize
}
}
pub fn erode(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
validate_op(img, h, w, se)?;
let offsets = se.active_offsets();
let mut out = vec![0.0_f32; h * w];
for y in 0..h {
for x in 0..w {
let mut acc = f32::INFINITY;
for &(dy, dx) in &offsets {
let sy = clamp_idx(y as isize + dy, h);
let sx = clamp_idx(x as isize + dx, w);
let v = img[sy * w + sx];
if v < acc {
acc = v;
}
}
out[y * w + x] = acc;
}
}
Ok(out)
}
pub fn dilate(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
validate_op(img, h, w, se)?;
let offsets = se.active_offsets();
let mut out = vec![0.0_f32; h * w];
for y in 0..h {
for x in 0..w {
let mut acc = f32::NEG_INFINITY;
for &(dy, dx) in &offsets {
let sy = clamp_idx(y as isize - dy, h);
let sx = clamp_idx(x as isize - dx, w);
let v = img[sy * w + sx];
if v > acc {
acc = v;
}
}
out[y * w + x] = acc;
}
}
Ok(out)
}
pub fn open(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
let eroded = erode(img, h, w, se)?;
dilate(&eroded, h, w, se)
}
pub fn close(img: &[f32], h: usize, w: usize, se: &StructuringElement) -> VisionResult<Vec<f32>> {
let dilated = dilate(img, h, w, se)?;
erode(&dilated, h, w, se)
}
pub fn morphological_gradient(
img: &[f32],
h: usize,
w: usize,
se: &StructuringElement,
) -> VisionResult<Vec<f32>> {
let dilated = dilate(img, h, w, se)?;
let eroded = erode(img, h, w, se)?;
let out = dilated
.iter()
.zip(eroded.iter())
.map(|(&d, &e)| d - e)
.collect();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn single_pixel(h: usize, w: usize, y: usize, x: usize) -> Vec<f32> {
let mut img = vec![0.0_f32; h * w];
img[y * w + x] = 1.0;
img
}
fn block(h: usize, w: usize, r0: usize, r1: usize, c0: usize, c1: usize) -> Vec<f32> {
let mut img = vec![0.0_f32; h * w];
for y in r0..r1 {
for x in c0..c1 {
img[y * w + x] = 1.0;
}
}
img
}
#[test]
fn erode_removes_isolated_pixel() {
let img = single_pixel(7, 7, 3, 3);
let se = StructuringElement::square(3).expect("se");
let out = erode(&img, 7, 7, &se).expect("erode");
let total: f32 = out.iter().sum();
assert_eq!(total, 0.0, "erosion must remove an isolated pixel");
}
#[test]
fn dilate_grows_single_pixel_to_se() {
let img = single_pixel(5, 5, 2, 2);
let se = StructuringElement::square(3).expect("se");
let out = dilate(&img, 5, 5, &se).expect("dilate");
for y in 0..5 {
for x in 0..5 {
let expected = if (1..=3).contains(&y) && (1..=3).contains(&x) {
1.0
} else {
0.0
};
assert_eq!(
out[y * 5 + x],
expected,
"dilation of a point must reproduce the 3×3 element at ({y},{x})"
);
}
}
}
#[test]
fn dilate_constant_image_unchanged() {
let img = vec![0.37_f32; 6 * 6];
let se = StructuringElement::square(3).expect("se");
let out = dilate(&img, 6, 6, &se).expect("dilate");
assert!(out.iter().all(|&v| (v - 0.37).abs() < 1e-6));
}
#[test]
fn erode_constant_image_unchanged() {
let img = vec![0.42_f32; 6 * 6];
let se = StructuringElement::square(3).expect("se");
let out = erode(&img, 6, 6, &se).expect("erode");
assert!(out.iter().all(|&v| (v - 0.42).abs() < 1e-6));
}
#[test]
fn cross_se_dilation_shape() {
let img = single_pixel(5, 5, 2, 2);
let se = StructuringElement::cross(1);
let out = dilate(&img, 5, 5, &se).expect("dilate");
let total: f32 = out.iter().sum();
assert_eq!(total, 5.0, "plus-shaped element has 5 members");
for &(y, x) in &[(2usize, 2usize), (1, 2), (3, 2), (2, 1), (2, 3)] {
assert_eq!(out[y * 5 + x], 1.0);
}
assert_eq!(out[3 * 5 + 3], 0.0);
}
#[test]
fn opening_removes_noise_keeps_blob() {
let mut img = block(8, 8, 2, 6, 2, 6);
img[0] = 1.0; let se = StructuringElement::square(3).expect("se");
let out = open(&img, 8, 8, &se).expect("open");
assert_eq!(out[0], 0.0, "opening must remove the isolated noise pixel");
let total: f32 = out.iter().sum();
assert_eq!(total, 16.0, "opening must preserve the 4×4 blob");
for y in 2..6 {
for x in 2..6 {
assert_eq!(out[y * 8 + x], 1.0);
}
}
}
#[test]
fn closing_fills_small_hole() {
let mut img = block(8, 8, 2, 6, 2, 6);
img[3 * 8 + 3] = 0.0; let se = StructuringElement::square(3).expect("se");
let out = close(&img, 8, 8, &se).expect("close");
assert_eq!(out[3 * 8 + 3], 1.0, "closing must fill the interior hole");
let total: f32 = out.iter().sum();
assert_eq!(total, 16.0);
}
#[test]
fn opening_is_idempotent() {
let mut img = block(12, 12, 5, 9, 5, 9);
img[3 * 12 + 3] = 1.0; let se = StructuringElement::square(3).expect("se");
let once = open(&img, 12, 12, &se).expect("open");
let twice = open(&once, 12, 12, &se).expect("open");
for (a, b) in once.iter().zip(twice.iter()) {
assert!((a - b).abs() < 1e-6, "opening must be idempotent");
}
}
#[test]
fn closing_is_idempotent() {
let mut img = block(12, 12, 4, 9, 4, 9);
img[6 * 12 + 6] = 0.0; let se = StructuringElement::square(3).expect("se");
let once = close(&img, 12, 12, &se).expect("close");
let twice = close(&once, 12, 12, &se).expect("close");
for (a, b) in once.iter().zip(twice.iter()) {
assert!((a - b).abs() < 1e-6, "closing must be idempotent");
}
}
#[test]
fn morph_gradient_outlines_point() {
let img = single_pixel(5, 5, 2, 2);
let se = StructuringElement::square(3).expect("se");
let grad = morphological_gradient(&img, 5, 5, &se).expect("gradient");
let total: f32 = grad.iter().sum();
assert_eq!(total, 9.0);
assert!(grad.iter().all(|&v| v >= 0.0));
}
#[test]
fn se_larger_than_image_errors() {
let img = vec![0.0_f32; 4 * 4];
let se = StructuringElement::square(7).expect("se");
assert!(matches!(
erode(&img, 4, 4, &se),
Err(VisionError::Internal(_))
));
assert!(matches!(
dilate(&img, 4, 4, &se),
Err(VisionError::Internal(_))
));
}
#[test]
fn empty_se_errors() {
let r = StructuringElement::new(3, 3, vec![false; 9]);
assert!(matches!(r, Err(VisionError::EmptyInput(_))));
assert!(matches!(
StructuringElement::rect(0, 3),
Err(VisionError::EmptyInput(_))
));
}
#[test]
fn se_mask_length_mismatch_errors() {
let r = StructuringElement::new(3, 3, vec![true; 8]);
assert!(matches!(r, Err(VisionError::DimensionMismatch { .. })));
}
#[test]
fn wrong_image_size_errors() {
let img = vec![0.0_f32; 10];
let se = StructuringElement::square(3).expect("se");
assert!(matches!(
erode(&img, 8, 8, &se),
Err(VisionError::DimensionMismatch { .. })
));
}
#[test]
fn zero_dim_image_errors() {
let img: Vec<f32> = vec![];
let se = StructuringElement::square(3).expect("se");
assert!(matches!(
dilate(&img, 0, 8, &se),
Err(VisionError::InvalidImageSize { .. })
));
}
#[test]
fn erode_is_anti_extensive_dilate_extensive() {
let img = block(8, 8, 2, 6, 1, 5);
let se = StructuringElement::square(3).expect("se");
let er = erode(&img, 8, 8, &se).expect("erode");
let di = dilate(&img, 8, 8, &se).expect("dilate");
for ((&e, &f), &d) in er.iter().zip(img.iter()).zip(di.iter()) {
assert!(e <= f + 1e-6, "erosion must be anti-extensive");
assert!(d + 1e-6 >= f, "dilation must be extensive");
}
}
}