#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderMode {
Replicate,
Reflect,
Constant(f32),
}
impl Default for BorderMode {
fn default() -> Self {
Self::Replicate
}
}
#[derive(Debug, Clone, Copy)]
pub struct MorphologyOptions {
pub kernel_size: usize,
pub iterations: u32,
pub border_mode: BorderMode,
}
impl Default for MorphologyOptions {
fn default() -> Self {
Self {
kernel_size: 3,
iterations: 1,
border_mode: BorderMode::default(),
}
}
}
impl MorphologyOptions {
pub fn new(kernel_size: usize) -> Self {
Self {
kernel_size,
..Self::default()
}
}
pub fn with_iterations(mut self, n: u32) -> Self {
self.iterations = n;
self
}
pub fn with_border_mode(mut self, m: BorderMode) -> Self {
self.border_mode = m;
self
}
}
#[inline]
fn sample(
raster: &[f32],
width: usize,
height: usize,
x: i64,
y: i64,
mode: BorderMode,
) -> Option<f32> {
let w = width as i64;
let h = height as i64;
let (sx, sy) = match mode {
BorderMode::Replicate => (x.clamp(0, w - 1), y.clamp(0, h - 1)),
BorderMode::Reflect => {
let rx = reflect_index(x, w);
let ry = reflect_index(y, h);
(rx, ry)
}
BorderMode::Constant(c) => {
if x < 0 || x >= w || y < 0 || y >= h {
return Some(c);
}
(x, y)
}
};
let ux = sx as usize;
let uy = sy as usize;
Some(raster[uy * width + ux])
}
#[inline]
fn reflect_index(i: i64, n: i64) -> i64 {
if n <= 1 {
return 0;
}
let period = 2 * (n - 1);
let mut m = i % period;
if m < 0 {
m += period;
}
if m >= n { period - m } else { m }
}
pub fn dilate_slice(
raster: &[f32],
width: usize,
height: usize,
kernel_size: usize,
mode: BorderMode,
) -> Vec<f32> {
let ks = kernel_size.max(1);
let half = (ks / 2) as i64;
let mut out = vec![0.0f32; width * height];
for y in 0..height {
for x in 0..width {
let mut max_val = f32::NEG_INFINITY;
for dy in -half..=half {
for dx in -half..=half {
if let Some(v) =
sample(raster, width, height, x as i64 + dx, y as i64 + dy, mode)
{
if v.is_finite() && v > max_val {
max_val = v;
}
}
}
}
out[y * width + x] = if max_val.is_finite() {
max_val
} else {
raster[y * width + x]
};
}
}
out
}
pub fn erode_slice(
raster: &[f32],
width: usize,
height: usize,
kernel_size: usize,
mode: BorderMode,
) -> Vec<f32> {
let ks = kernel_size.max(1);
let half = (ks / 2) as i64;
let mut out = vec![0.0f32; width * height];
for y in 0..height {
for x in 0..width {
let mut min_val = f32::INFINITY;
for dy in -half..=half {
for dx in -half..=half {
if let Some(v) =
sample(raster, width, height, x as i64 + dx, y as i64 + dy, mode)
{
if v.is_finite() && v < min_val {
min_val = v;
}
}
}
}
out[y * width + x] = if min_val.is_finite() {
min_val
} else {
raster[y * width + x]
};
}
}
out
}
pub fn opening(raster: &[f32], width: usize, height: usize, kernel_size: usize) -> Vec<f32> {
let mode = BorderMode::default();
let eroded = erode_slice(raster, width, height, kernel_size, mode);
dilate_slice(&eroded, width, height, kernel_size, mode)
}
pub fn closing(raster: &[f32], width: usize, height: usize, kernel_size: usize) -> Vec<f32> {
let mode = BorderMode::default();
let dilated = dilate_slice(raster, width, height, kernel_size, mode);
erode_slice(&dilated, width, height, kernel_size, mode)
}
pub fn top_hat(raster: &[f32], width: usize, height: usize, kernel_size: usize) -> Vec<f32> {
let opened = opening(raster, width, height, kernel_size);
raster
.iter()
.zip(opened.iter())
.map(|(a, b)| a - b)
.collect()
}
pub fn black_hat(raster: &[f32], width: usize, height: usize, kernel_size: usize) -> Vec<f32> {
let closed = closing(raster, width, height, kernel_size);
closed
.iter()
.zip(raster.iter())
.map(|(a, b)| a - b)
.collect()
}
pub fn opening_with(
raster: &[f32],
width: usize,
height: usize,
opts: &MorphologyOptions,
) -> Vec<f32> {
let mut current = raster.to_vec();
for _ in 0..opts.iterations {
let eroded = erode_slice(¤t, width, height, opts.kernel_size, opts.border_mode);
current = dilate_slice(&eroded, width, height, opts.kernel_size, opts.border_mode);
}
current
}
pub fn closing_with(
raster: &[f32],
width: usize,
height: usize,
opts: &MorphologyOptions,
) -> Vec<f32> {
let mut current = raster.to_vec();
for _ in 0..opts.iterations {
let dilated = dilate_slice(¤t, width, height, opts.kernel_size, opts.border_mode);
current = erode_slice(&dilated, width, height, opts.kernel_size, opts.border_mode);
}
current
}
pub fn top_hat_with(
raster: &[f32],
width: usize,
height: usize,
opts: &MorphologyOptions,
) -> Vec<f32> {
let opened = opening_with(raster, width, height, opts);
raster
.iter()
.zip(opened.iter())
.map(|(a, b)| a - b)
.collect()
}
pub fn black_hat_with(
raster: &[f32],
width: usize,
height: usize,
opts: &MorphologyOptions,
) -> Vec<f32> {
let closed = closing_with(raster, width, height, opts);
closed
.iter()
.zip(raster.iter())
.map(|(a, b)| a - b)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reflect_index_basic() {
assert_eq!(reflect_index(-1, 5), 1);
assert_eq!(reflect_index(-2, 5), 2);
assert_eq!(reflect_index(5, 5), 3);
assert_eq!(reflect_index(6, 5), 2);
}
#[test]
fn test_default_morphology_options() {
let opts = MorphologyOptions::default();
assert_eq!(opts.kernel_size, 3);
assert_eq!(opts.iterations, 1);
matches!(opts.border_mode, BorderMode::Replicate);
}
#[test]
fn test_morphology_options_builder() {
let opts = MorphologyOptions::new(5)
.with_iterations(2)
.with_border_mode(BorderMode::Constant(0.0));
assert_eq!(opts.kernel_size, 5);
assert_eq!(opts.iterations, 2);
matches!(opts.border_mode, BorderMode::Constant(_));
}
#[test]
fn test_dilate_slice_constant_raster() {
let r = vec![5.0f32; 16];
let out = dilate_slice(&r, 4, 4, 3, BorderMode::Replicate);
assert!(out.iter().all(|&v| (v - 5.0).abs() < 1e-6));
}
#[test]
fn test_erode_slice_constant_raster() {
let r = vec![5.0f32; 16];
let out = erode_slice(&r, 4, 4, 3, BorderMode::Replicate);
assert!(out.iter().all(|&v| (v - 5.0).abs() < 1e-6));
}
}