pub fn morph_close(mask: &[bool], width: u32, height: u32) -> Vec<bool> {
let radius = adaptive_radius(width, height);
if radius == 0 {
return mask.to_vec();
}
let dilated = dilate(mask, width, height, radius);
erode(&dilated, width, height, radius)
}
fn adaptive_radius(width: u32, height: u32) -> u32 {
(width.max(height) / 200).clamp(2, 15)
}
fn dilate(mask: &[bool], width: u32, height: u32, radius: u32) -> Vec<bool> {
let w = width as usize;
let h = height as usize;
let r = radius as usize;
let mut horiz = vec![false; w * h];
for y in 0..h {
let row_start = y * w;
let mut count = 0usize;
let init_end = r.min(w - 1);
for x in 0..=init_end {
if mask[row_start + x] {
count += 1;
}
}
horiz[row_start] = count > 0;
for x in 1..w {
let add = x + r;
if add < w && mask[row_start + add] {
count += 1;
}
if x > r + 1 {
let rem = x - r - 1;
if mask[row_start + rem] {
count -= 1;
}
} else if x == r + 1 {
let rem = 0;
if mask[row_start + rem] {
count -= 1;
}
}
horiz[row_start + x] = count > 0;
}
}
let mut result = vec![false; w * h];
for x in 0..w {
let mut count = 0usize;
let init_end = r.min(h - 1);
for y in 0..=init_end {
if horiz[y * w + x] {
count += 1;
}
}
result[x] = count > 0;
for y in 1..h {
let add = y + r;
if add < h && horiz[add * w + x] {
count += 1;
}
if y > r + 1 {
let rem = y - r - 1;
if horiz[rem * w + x] {
count -= 1;
}
} else if y == r + 1 {
if horiz[x] {
count -= 1;
}
}
result[y * w + x] = count > 0;
}
}
result
}
fn erode(mask: &[bool], width: u32, height: u32, radius: u32) -> Vec<bool> {
let w = width as usize;
let h = height as usize;
let r = radius as usize;
let mut horiz = vec![false; w * h];
for y in 0..h {
let row_start = y * w;
let mut count = 0usize;
let init_end = r.min(w - 1);
for x in 0..=init_end {
if mask[row_start + x] {
count += 1;
}
}
let effective_size = (r + 1).min(w);
horiz[row_start] = count == effective_size;
for x in 1..w {
let add = x + r;
if add < w && mask[row_start + add] {
count += 1;
}
if x > r + 1 {
let rem = x - r - 1;
if mask[row_start + rem] {
count -= 1;
}
} else if x == r + 1 {
if mask[row_start] {
count -= 1;
}
}
let left = if x > r { x - r } else { 0 };
let right = (x + r).min(w - 1);
let eff = right - left + 1;
horiz[row_start + x] = count == eff;
}
}
let mut result = vec![false; w * h];
for x in 0..w {
let mut count = 0usize;
let init_end = r.min(h - 1);
for y in 0..=init_end {
if horiz[y * w + x] {
count += 1;
}
}
let effective_size = (r + 1).min(h);
result[x] = count == effective_size;
for y in 1..h {
let add = y + r;
if add < h && horiz[add * w + x] {
count += 1;
}
if y > r + 1 {
let rem = y - r - 1;
if horiz[rem * w + x] {
count -= 1;
}
} else if y == r + 1 {
if horiz[x] {
count -= 1;
}
}
let top = if y > r { y - r } else { 0 };
let bottom = (y + r).min(h - 1);
let eff = bottom - top + 1;
result[y * w + x] = count == eff;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dilate_single_pixel() {
let w = 11u32;
let h = 11u32;
let mut mask = vec![false; (w * h) as usize];
mask[5 * 11 + 5] = true;
let dilated = dilate(&mask, w, h, 2);
for y in 0..h {
for x in 0..w {
let expected = x >= 3 && x <= 7 && y >= 3 && y <= 7;
assert_eq!(
dilated[(y * w + x) as usize],
expected,
"mismatch at ({x},{y}): got {}, expected {expected}",
dilated[(y * w + x) as usize]
);
}
}
}
#[test]
fn test_erode_removes_thin_line() {
let w = 5u32;
let h = 5u32;
let mut mask = vec![false; 25];
for x in 0..5 {
mask[2 * 5 + x] = true;
}
let eroded = erode(&mask, w, h, 1);
for i in 0..25 {
assert!(!eroded[i], "pixel {i} should be eroded away");
}
}
#[test]
fn test_close_bridges_small_gap() {
let w = 12u32;
let h = 5u32;
let mut mask = vec![false; (w * h) as usize];
for y in 1..4 {
for x in 1..4 {
mask[(y * w + x) as usize] = true;
}
}
for y in 1..4 {
for x in 6..9 {
mask[(y * w + x) as usize] = true;
}
}
let closed = morph_close(&mask, w, h);
let gap_filled = (1..4).any(|y: u32| (4..6).any(|x: u32| closed[(y * w + x) as usize]));
assert!(gap_filled, "close should bridge the 2px gap");
}
#[test]
fn test_close_preserves_original_blobs() {
let w = 10u32;
let h = 10u32;
let mut mask = vec![false; 100];
for y in 3..7 {
for x in 3..7 {
mask[y * 10 + x] = true;
}
}
let closed = morph_close(&mask, w, h);
for y in 3..7 {
for x in 3..7 {
assert!(
closed[y * 10 + x],
"original pixel ({x},{y}) lost after close"
);
}
}
}
#[test]
fn test_close_doesnt_bridge_large_gap() {
let w = 100u32;
let h = 10u32;
let mut mask = vec![false; (w * h) as usize];
for y in 3..7 {
for x in 5..10 {
mask[(y * w + x) as usize] = true;
}
}
for y in 3..7 {
for x in 60..65 {
mask[(y * w + x) as usize] = true;
}
}
let closed = morph_close(&mask, w, h);
assert!(
!closed[(5 * w + 35) as usize],
"large gap should not be bridged"
);
}
#[test]
fn test_close_is_idempotent() {
let w = 30u32;
let h = 30u32;
let mut mask = vec![false; (w * h) as usize];
for y in 5..15 {
for x in 5..10 {
mask[(y * w + x) as usize] = true;
}
}
for y in 5..15 {
for x in 13..20 {
mask[(y * w + x) as usize] = true;
}
}
let closed_once = morph_close(&mask, w, h);
let closed_twice = morph_close(&closed_once, w, h);
assert_eq!(
closed_once, closed_twice,
"Morphological close should be idempotent"
);
}
#[test]
fn test_close_never_removes_foreground() {
let w = 50u32;
let h = 50u32;
let mut mask = vec![false; (w * h) as usize];
for i in (0..(w * h) as usize).step_by(7) {
mask[i] = true;
}
let closed = morph_close(&mask, w, h);
for i in 0..mask.len() {
if mask[i] {
assert!(
closed[i],
"Closing removed original foreground pixel at index {i}"
);
}
}
}
}