use crate::error::{Result, VisionError};
use scirs2_core::ndarray::Array2;
pub fn disk_kernel(radius: usize) -> Array2<u8> {
let size = 2 * radius + 1;
let mut k = Array2::<u8>::zeros((size, size));
let cx = radius as isize;
let cy = radius as isize;
let r2 = radius as isize;
for y in 0..size {
for x in 0..size {
let dx = x as isize - cx;
let dy = y as isize - cy;
if dx * dx + dy * dy <= r2 * r2 {
k[[y, x]] = 1;
}
}
}
k
}
pub fn rect_kernel(rows: usize, cols: usize) -> Array2<u8> {
Array2::<u8>::ones((rows, cols))
}
pub fn cross_kernel(size: usize) -> Array2<u8> {
let sz = if size == 0 {
1
} else if size.is_multiple_of(2) {
size + 1
} else {
size
};
let mut k = Array2::<u8>::zeros((sz, sz));
let mid = sz / 2;
for x in 0..sz {
k[[mid, x]] = 1;
}
for y in 0..sz {
k[[y, mid]] = 1;
}
k
}
pub fn erode(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
validate_kernel(image, kernel)?;
let (h, w) = image.dim();
let (kh, kw) = kernel.dim();
let ry = kh / 2;
let rx = kw / 2;
let mut out = Array2::<u8>::zeros((h, w));
for y in 0..h {
for x in 0..w {
let mut min_val = 255u8;
for ky in 0..kh {
for kx in 0..kw {
if kernel[[ky, kx]] == 0 {
continue;
}
let iy = y as isize + ky as isize - ry as isize;
let ix = x as isize + kx as isize - rx as isize;
let pix = if iy >= 0 && iy < h as isize && ix >= 0 && ix < w as isize {
image[[iy as usize, ix as usize]]
} else {
255 };
if pix < min_val {
min_val = pix;
}
}
}
out[[y, x]] = min_val;
}
}
Ok(out)
}
pub fn dilate(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
validate_kernel(image, kernel)?;
let (h, w) = image.dim();
let (kh, kw) = kernel.dim();
let ry = kh / 2;
let rx = kw / 2;
let mut out = Array2::<u8>::zeros((h, w));
for y in 0..h {
for x in 0..w {
let mut max_val = 0u8;
for ky in 0..kh {
for kx in 0..kw {
if kernel[[ky, kx]] == 0 {
continue;
}
let iy = y as isize + ky as isize - ry as isize;
let ix = x as isize + kx as isize - rx as isize;
let pix = if iy >= 0 && iy < h as isize && ix >= 0 && ix < w as isize {
image[[iy as usize, ix as usize]]
} else {
0
};
if pix > max_val {
max_val = pix;
}
}
}
out[[y, x]] = max_val;
}
}
Ok(out)
}
pub fn opening(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
let eroded = erode(image, kernel)?;
dilate(&eroded, kernel)
}
pub fn closing(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
let dilated = dilate(image, kernel)?;
erode(&dilated, kernel)
}
pub fn morphological_gradient(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
let dilated = dilate(image, kernel)?;
let eroded = erode(image, kernel)?;
let (h, w) = image.dim();
let mut out = Array2::<u8>::zeros((h, w));
for y in 0..h {
for x in 0..w {
out[[y, x]] = dilated[[y, x]].saturating_sub(eroded[[y, x]]);
}
}
Ok(out)
}
pub fn top_hat(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
let opened = opening(image, kernel)?;
let (h, w) = image.dim();
let mut out = Array2::<u8>::zeros((h, w));
for y in 0..h {
for x in 0..w {
out[[y, x]] = image[[y, x]].saturating_sub(opened[[y, x]]);
}
}
Ok(out)
}
pub fn black_hat(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<Array2<u8>> {
let closed = closing(image, kernel)?;
let (h, w) = image.dim();
let mut out = Array2::<u8>::zeros((h, w));
for y in 0..h {
for x in 0..w {
out[[y, x]] = closed[[y, x]].saturating_sub(image[[y, x]]);
}
}
Ok(out)
}
fn validate_kernel(image: &Array2<u8>, kernel: &Array2<u8>) -> Result<()> {
let (kh, kw) = kernel.dim();
if kh == 0 || kw == 0 {
return Err(VisionError::InvalidParameter(
"Structuring element must have non-zero dimensions".to_string(),
));
}
let (h, w) = image.dim();
if h == 0 || w == 0 {
return Err(VisionError::InvalidParameter(
"Image must have non-zero dimensions".to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn disk_kernel_size() {
let k = disk_kernel(0);
assert_eq!(k.dim(), (1, 1));
assert_eq!(k[[0, 0]], 1);
let k3 = disk_kernel(3);
assert_eq!(k3.dim(), (7, 7));
assert_eq!(k3[[3, 3]], 1); assert_eq!(k3[[0, 0]], 0);
assert_eq!(k3[[0, 6]], 0);
assert_eq!(k3[[6, 0]], 0);
assert_eq!(k3[[6, 6]], 0);
assert_eq!(k3[[0, 3]], 1);
assert_eq!(k3[[3, 0]], 1);
assert_eq!(k3[[6, 3]], 1);
assert_eq!(k3[[3, 6]], 1);
}
#[test]
fn rect_kernel_all_ones() {
let k = rect_kernel(4, 6);
assert_eq!(k.dim(), (4, 6));
assert!(k.iter().all(|&v| v == 1));
}
#[test]
fn cross_kernel_shape() {
let k = cross_kernel(5);
assert_eq!(k.dim(), (5, 5));
for i in 0..5 {
assert_eq!(k[[2, i]], 1, "middle row at col {i}");
assert_eq!(k[[i, 2]], 1, "middle col at row {i}");
}
assert_eq!(k[[0, 0]], 0);
assert_eq!(k[[4, 4]], 0);
}
#[test]
fn cross_kernel_even_rounds_up() {
let k = cross_kernel(4);
assert_eq!(k.dim(), (5, 5));
}
fn bright_square(size: usize, sq_start: usize, sq_end: usize) -> Array2<u8> {
let mut img = Array2::<u8>::zeros((size, size));
for y in sq_start..sq_end {
for x in sq_start..sq_end {
img[[y, x]] = 200;
}
}
img
}
#[test]
fn erode_shrinks_bright_region() {
let img = bright_square(20, 4, 16); let k = rect_kernel(3, 3);
let eroded = erode(&img, &k).expect("erode should succeed");
assert_eq!(eroded[[9, 9]], 200, "Interior should survive erosion");
assert_eq!(eroded[[4, 4]], 0, "Border of square should be eroded");
}
#[test]
fn dilate_grows_bright_region() {
let mut img = Array2::<u8>::zeros((20, 20));
img[[10, 10]] = 255;
let k = rect_kernel(3, 3);
let dilated = dilate(&img, &k).expect("dilate should succeed");
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let y = (10i32 + dy) as usize;
let x = (10i32 + dx) as usize;
assert_eq!(dilated[[y, x]], 255, "({y},{x}) should be dilated");
}
}
assert_eq!(dilated[[0, 0]], 0);
}
#[test]
fn dilate_then_erode_identity_for_large_region() {
let img = bright_square(30, 5, 25); let k = rect_kernel(3, 3);
let dilated = dilate(&img, &k).expect("dilate");
let back = erode(&dilated, &k).expect("erode");
for y in 8..22 {
for x in 8..22 {
assert_eq!(
back[[y, x]],
img[[y, x]],
"Interior pixel ({y},{x}) should be preserved by dil+erode"
);
}
}
}
#[test]
fn erode_uniform_image_unchanged() {
let img = Array2::<u8>::from_elem((16, 16), 128);
let k = disk_kernel(2);
let eroded = erode(&img, &k).expect("erode uniform");
assert!(
eroded.iter().all(|&v| v == 128),
"Uniform image should be unchanged by erosion"
);
}
#[test]
fn dilate_uniform_image_unchanged() {
let img = Array2::<u8>::from_elem((16, 16), 128);
let k = disk_kernel(2);
let dilated = dilate(&img, &k).expect("dilate uniform");
assert!(
dilated.iter().all(|&v| v == 128),
"Uniform image should be unchanged by dilation"
);
}
#[test]
fn opening_removes_small_blobs() {
let mut img = bright_square(20, 4, 16);
img[[1, 1]] = 255;
img[[1, 18]] = 255;
let k = disk_kernel(2); let opened = opening(&img, &k).expect("opening");
assert_eq!(opened[[1, 1]], 0, "Small blob should be removed by opening");
assert_eq!(
opened[[1, 18]],
0,
"Small blob should be removed by opening"
);
assert!(opened[[9, 9]] > 0, "Large region should survive opening");
}
#[test]
fn closing_fills_small_holes() {
let mut img = Array2::<u8>::from_elem((20, 20), 200u8);
img[[10, 10]] = 0;
let k = rect_kernel(3, 3);
let closed = closing(&img, &k).expect("closing");
assert!(
closed[[10, 10]] > 0,
"Small hole should be filled by closing"
);
}
#[test]
fn morphological_gradient_highlights_edges() {
let img = bright_square(20, 4, 16);
let k = rect_kernel(3, 3);
let grad = morphological_gradient(&img, &k).expect("gradient");
assert_eq!(grad[[9, 9]], 0, "Interior should have zero gradient");
assert!(grad[[4, 4]] > 0, "Edge should have non-zero gradient");
}
#[test]
fn top_hat_extracts_small_bright_feature() {
let mut img = Array2::<u8>::from_elem((16, 16), 100u8);
img[[8, 8]] = 255;
let k = disk_kernel(3); let th = top_hat(&img, &k).expect("top_hat");
assert!(
th[[8, 8]] > 0,
"Small bright feature should appear in top-hat result"
);
}
#[test]
fn black_hat_extracts_small_dark_feature() {
let mut img = Array2::<u8>::from_elem((16, 16), 200u8);
img[[8, 8]] = 0;
let k = disk_kernel(3);
let bh = black_hat(&img, &k).expect("black_hat");
assert!(
bh[[8, 8]] > 0,
"Small dark feature should appear in black-hat result"
);
}
#[test]
fn all_ops_preserve_dimensions() {
let img = bright_square(24, 4, 20);
let k = rect_kernel(3, 3);
#[allow(clippy::type_complexity)]
let ops: Vec<(&str, Box<dyn Fn() -> Result<Array2<u8>>>)> = vec![
("erode", Box::new(|| erode(&img, &k))),
("dilate", Box::new(|| dilate(&img, &k))),
("opening", Box::new(|| opening(&img, &k))),
("closing", Box::new(|| closing(&img, &k))),
("gradient", Box::new(|| morphological_gradient(&img, &k))),
("top_hat", Box::new(|| top_hat(&img, &k))),
("black_hat", Box::new(|| black_hat(&img, &k))),
];
for (name, op) in &ops {
let result = op().unwrap_or_else(|e| panic!("{name} failed: {e}"));
assert_eq!(
result.dim(),
img.dim(),
"{name} should preserve image dimensions"
);
}
}
#[test]
fn zero_dimension_kernel_returns_error() {
let img = Array2::<u8>::zeros((8, 8));
let k = Array2::<u8>::zeros((0, 3));
assert!(
erode(&img, &k).is_err(),
"Zero-height kernel should return error"
);
let k2 = Array2::<u8>::zeros((3, 0));
assert!(
dilate(&img, &k2).is_err(),
"Zero-width kernel should return error"
);
}
#[test]
fn zero_dimension_image_returns_error() {
let img = Array2::<u8>::zeros((0, 8));
let k = rect_kernel(3, 3);
assert!(erode(&img, &k).is_err());
assert!(dilate(&img, &k).is_err());
}
#[test]
fn cross_dilation_cardinal_directions() {
let mut img = Array2::<u8>::zeros((11, 11));
img[[5, 5]] = 255;
let k = cross_kernel(5); let dilated = dilate(&img, &k).expect("dilate with cross");
assert_eq!(dilated[[5, 5]], 255, "Centre");
assert_eq!(dilated[[5, 3]], 255, "Left 2");
assert_eq!(dilated[[5, 7]], 255, "Right 2");
assert_eq!(dilated[[3, 5]], 255, "Up 2");
assert_eq!(dilated[[7, 5]], 255, "Down 2");
assert_eq!(dilated[[3, 3]], 0, "Upper-left diagonal should stay dark");
assert_eq!(dilated[[7, 7]], 0, "Lower-right diagonal should stay dark");
}
#[test]
fn disk_kernel_contains_full_ring() {
let k = disk_kernel(4);
for y in 0..9usize {
for x in 0..9usize {
let dy = y as isize - 4;
let dx = x as isize - 4;
let r2 = dy * dy + dx * dx;
let expected: u8 = if r2 <= 16 { 1 } else { 0 };
assert_eq!(
k[[y, x]],
expected,
"disk_kernel(4)[[{y},{x}]] should be {expected}, r²={r2}"
);
}
}
}
}