use super::{BorderMode, ColorSpace, CvError, Image};
use crate::array::Array;
use crate::error::NumRs2Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StructuringElementShape {
Rectangular,
Cross,
Elliptical,
}
#[derive(Debug, Clone)]
pub struct StructuringElement {
data: Vec<Vec<bool>>,
height: usize,
width: usize,
anchor_row: usize,
anchor_col: usize,
}
impl StructuringElement {
pub fn new(
shape: StructuringElementShape,
height: usize,
width: usize,
) -> Result<Self, NumRs2Error> {
if height == 0 || width == 0 {
return Err(CvError::InvalidKernelSize(0).into());
}
if height.is_multiple_of(2) || width.is_multiple_of(2) {
return Err(CvError::InvalidParameter(
"Structuring element dimensions must be odd".to_string(),
)
.into());
}
let anchor_row = height / 2;
let anchor_col = width / 2;
let data = match shape {
StructuringElementShape::Rectangular => {
vec![vec![true; width]; height]
}
StructuringElementShape::Cross => {
let mut d = vec![vec![false; width]; height];
for i in 0..height {
d[i][anchor_col] = true;
}
for j in 0..width {
d[anchor_row][j] = true;
}
d
}
StructuringElementShape::Elliptical => {
let mut d = vec![vec![false; width]; height];
let a = anchor_col as f64; let b = anchor_row as f64; for i in 0..height {
for j in 0..width {
let dy = (i as f64 - b) / b.max(1.0);
let dx = (j as f64 - a) / a.max(1.0);
if dx * dx + dy * dy <= 1.0 + 1e-10 {
d[i][j] = true;
}
}
}
d
}
};
Ok(Self {
data,
height,
width,
anchor_row,
anchor_col,
})
}
pub fn square(shape: StructuringElementShape, size: usize) -> Result<Self, NumRs2Error> {
Self::new(shape, size, size)
}
pub fn is_active(&self, row: usize, col: usize) -> bool {
if row < self.height && col < self.width {
self.data[row][col]
} else {
false
}
}
pub fn height(&self) -> usize {
self.height
}
pub fn width(&self) -> usize {
self.width
}
pub fn anchor_row(&self) -> usize {
self.anchor_row
}
pub fn anchor_col(&self) -> usize {
self.anchor_col
}
pub fn active_count(&self) -> usize {
self.data
.iter()
.flat_map(|row| row.iter())
.filter(|&&v| v)
.count()
}
}
fn fetch_morph_pixel(img: &Image, row: isize, col: isize, default_val: f64) -> f64 {
let h = img.height() as isize;
let w = img.width() as isize;
if row < 0 || row >= h || col < 0 || col >= w {
default_val
} else {
img.get_pixel(row as usize, col as usize, 0)
.unwrap_or(default_val)
}
}
pub fn erosion(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
if img.color_space() != ColorSpace::Grayscale {
return Err(CvError::RequiresGrayscale.into());
}
let h = img.height();
let w = img.width();
let mut result = Image::zeros_grayscale(w, h);
for row in 0..h {
for col in 0..w {
let mut min_val = f64::INFINITY;
for si in 0..se.height() {
for sj in 0..se.width() {
if se.is_active(si, sj) {
let img_row = row as isize + si as isize - se.anchor_row() as isize;
let img_col = col as isize + sj as isize - se.anchor_col() as isize;
let pixel = fetch_morph_pixel(img, img_row, img_col, f64::INFINITY);
if pixel < min_val {
min_val = pixel;
}
}
}
}
if min_val.is_infinite() {
min_val = img.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Erosion pixel read: {}", e))
})?;
}
result
.set_pixel(row, col, 0, min_val)
.map_err(|e| NumRs2Error::ComputationError(format!("Erosion pixel set: {}", e)))?;
}
}
Ok(result)
}
pub fn dilation(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
if img.color_space() != ColorSpace::Grayscale {
return Err(CvError::RequiresGrayscale.into());
}
let h = img.height();
let w = img.width();
let mut result = Image::zeros_grayscale(w, h);
for row in 0..h {
for col in 0..w {
let mut max_val = f64::NEG_INFINITY;
for si in 0..se.height() {
for sj in 0..se.width() {
if se.is_active(si, sj) {
let img_row = row as isize + si as isize - se.anchor_row() as isize;
let img_col = col as isize + sj as isize - se.anchor_col() as isize;
let pixel = fetch_morph_pixel(img, img_row, img_col, f64::NEG_INFINITY);
if pixel > max_val {
max_val = pixel;
}
}
}
}
if max_val.is_infinite() && max_val < 0.0 {
max_val = img.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Dilation pixel read: {}", e))
})?;
}
result
.set_pixel(row, col, 0, max_val)
.map_err(|e| NumRs2Error::ComputationError(format!("Dilation pixel set: {}", e)))?;
}
}
Ok(result)
}
pub fn opening(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
let eroded = erosion(img, se)?;
dilation(&eroded, se)
}
pub fn closing(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
let dilated = dilation(img, se)?;
erosion(&dilated, se)
}
pub fn morphological_gradient(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
let dilated = dilation(img, se)?;
let eroded = erosion(img, se)?;
let h = img.height();
let w = img.width();
let mut result = Image::zeros_grayscale(w, h);
for row in 0..h {
for col in 0..w {
let d = dilated.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Gradient dilation read: {}", e))
})?;
let e = eroded.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Gradient erosion read: {}", e))
})?;
result
.set_pixel(row, col, 0, d - e)
.map_err(|err| NumRs2Error::ComputationError(format!("Gradient set: {}", err)))?;
}
}
Ok(result)
}
pub fn top_hat(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
let opened = opening(img, se)?;
let h = img.height();
let w = img.width();
let mut result = Image::zeros_grayscale(w, h);
for row in 0..h {
for col in 0..w {
let orig = img.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Top hat original read: {}", e))
})?;
let op = opened.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Top hat opened read: {}", e))
})?;
result
.set_pixel(row, col, 0, (orig - op).max(0.0))
.map_err(|e| NumRs2Error::ComputationError(format!("Top hat set: {}", e)))?;
}
}
Ok(result)
}
pub fn black_hat(img: &Image, se: &StructuringElement) -> Result<Image, NumRs2Error> {
let closed = closing(img, se)?;
let h = img.height();
let w = img.width();
let mut result = Image::zeros_grayscale(w, h);
for row in 0..h {
for col in 0..w {
let cl = closed.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Black hat closed read: {}", e))
})?;
let orig = img.get_pixel(row, col, 0).map_err(|e| {
NumRs2Error::ComputationError(format!("Black hat original read: {}", e))
})?;
result
.set_pixel(row, col, 0, (cl - orig).max(0.0))
.map_err(|e| NumRs2Error::ComputationError(format!("Black hat set: {}", e)))?;
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_binary_image(size: usize) -> Image {
let mut data = vec![0.0; size * size];
let margin = size / 4;
for row in margin..(size - margin) {
for col in margin..(size - margin) {
data[row * size + col] = 1.0;
}
}
Image::from_grayscale(size, size, &data).expect("test: image creation should succeed")
}
#[test]
fn test_structuring_element_rectangular() {
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
assert_eq!(se.height(), 3);
assert_eq!(se.width(), 3);
assert_eq!(se.active_count(), 9);
assert!(se.is_active(0, 0));
assert!(se.is_active(1, 1));
assert!(se.is_active(2, 2));
}
#[test]
fn test_structuring_element_cross() {
let se = StructuringElement::square(StructuringElementShape::Cross, 3)
.expect("test: SE creation should succeed");
assert_eq!(se.active_count(), 5);
assert!(se.is_active(1, 0));
assert!(se.is_active(1, 1));
assert!(se.is_active(1, 2));
assert!(se.is_active(0, 1));
assert!(se.is_active(2, 1));
assert!(!se.is_active(0, 0));
assert!(!se.is_active(0, 2));
assert!(!se.is_active(2, 0));
assert!(!se.is_active(2, 2));
}
#[test]
fn test_structuring_element_elliptical() {
let se = StructuringElement::square(StructuringElementShape::Elliptical, 5)
.expect("test: SE creation should succeed");
assert!(se.is_active(2, 2)); assert!(se.active_count() < 25);
assert!(se.active_count() > 5);
}
#[test]
fn test_structuring_element_invalid_size() {
let result = StructuringElement::square(StructuringElementShape::Rectangular, 4);
assert!(result.is_err());
let result = StructuringElement::square(StructuringElementShape::Rectangular, 0);
assert!(result.is_err());
}
#[test]
fn test_erosion_shrinks_bright_region() {
let img = make_binary_image(16);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let eroded = erosion(&img, &se).expect("test: erosion should succeed");
let original_bright: usize = img.to_vec().iter().filter(|&&v| v > 0.5).count();
let eroded_bright: usize = eroded.to_vec().iter().filter(|&&v| v > 0.5).count();
assert!(
eroded_bright < original_bright,
"Erosion should shrink the bright region: {} vs {}",
eroded_bright,
original_bright
);
}
#[test]
fn test_dilation_expands_bright_region() {
let img = make_binary_image(16);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let dilated = dilation(&img, &se).expect("test: dilation should succeed");
let original_bright: usize = img.to_vec().iter().filter(|&&v| v > 0.5).count();
let dilated_bright: usize = dilated.to_vec().iter().filter(|&&v| v > 0.5).count();
assert!(
dilated_bright > original_bright,
"Dilation should expand the bright region: {} vs {}",
dilated_bright,
original_bright
);
}
#[test]
fn test_opening_idempotent() {
let img = make_binary_image(16);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let opened_once = opening(&img, &se).expect("test: opening should succeed");
let opened_twice = opening(&opened_once, &se).expect("test: opening should succeed");
let data_once = opened_once.to_vec();
let data_twice = opened_twice.to_vec();
for (v1, v2) in data_once.iter().zip(data_twice.iter()) {
assert!((v1 - v2).abs() < 1e-10, "Opening should be idempotent");
}
}
#[test]
fn test_closing_idempotent() {
let img = make_binary_image(16);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let closed_once = closing(&img, &se).expect("test: closing should succeed");
let closed_twice = closing(&closed_once, &se).expect("test: closing should succeed");
let data_once = closed_once.to_vec();
let data_twice = closed_twice.to_vec();
for (v1, v2) in data_once.iter().zip(data_twice.iter()) {
assert!((v1 - v2).abs() < 1e-10, "Closing should be idempotent");
}
}
#[test]
fn test_morphological_gradient() {
let img = make_binary_image(16);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let gradient =
morphological_gradient(&img, &se).expect("test: morphological gradient should succeed");
let center = gradient
.get_pixel(8, 8, 0)
.expect("test: pixel read should succeed");
assert!(
center.abs() < 1e-10,
"Gradient should be zero in constant interior: got {}",
center
);
let gradient_data = gradient.to_vec();
let max_gradient = gradient_data.iter().cloned().fold(0.0_f64, f64::max);
assert!(
max_gradient > 0.0,
"Gradient should have positive values at edges"
);
}
#[test]
fn test_top_hat_on_constant() {
let data = vec![0.5; 16 * 16];
let img =
Image::from_grayscale(16, 16, &data).expect("test: image creation should succeed");
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let th = top_hat(&img, &se).expect("test: top hat should succeed");
for row in 1..15 {
for col in 1..15 {
let v = th
.get_pixel(row, col, 0)
.expect("test: pixel read should succeed");
assert!(
v.abs() < 1e-8,
"Top hat of constant should be zero at ({}, {}): got {}",
row,
col,
v
);
}
}
}
#[test]
fn test_black_hat_on_constant() {
let data = vec![0.5; 16 * 16];
let img =
Image::from_grayscale(16, 16, &data).expect("test: image creation should succeed");
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let bh = black_hat(&img, &se).expect("test: black hat should succeed");
for row in 1..15 {
for col in 1..15 {
let v = bh
.get_pixel(row, col, 0)
.expect("test: pixel read should succeed");
assert!(
v.abs() < 1e-8,
"Black hat of constant should be zero at ({}, {}): got {}",
row,
col,
v
);
}
}
}
#[test]
fn test_erosion_then_dilation_preserves_shape() {
let img = make_binary_image(32);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let opened = opening(&img, &se).expect("test: opening should succeed");
let center = opened
.get_pixel(16, 16, 0)
.expect("test: pixel read should succeed");
assert!(
center > 0.9,
"Opening should preserve center of large bright region: got {}",
center
);
}
#[test]
fn test_erosion_on_all_zeros() {
let img = Image::zeros_grayscale(8, 8);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let eroded = erosion(&img, &se).expect("test: erosion should succeed");
for v in eroded.to_vec() {
assert!(
v.abs() < 1e-10,
"Erosion of all-zero image should remain zero"
);
}
}
#[test]
fn test_dilation_on_all_zeros() {
let img = Image::zeros_grayscale(8, 8);
let se = StructuringElement::square(StructuringElementShape::Rectangular, 3)
.expect("test: SE creation should succeed");
let dilated = dilation(&img, &se).expect("test: dilation should succeed");
for v in dilated.to_vec() {
assert!(
v.abs() < 1e-10,
"Dilation of all-zero image should remain zero"
);
}
}
}