use crate::distance_transforms::{cityblock_dt, euclidean_dt};
use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::Array2;
use std::collections::VecDeque;
pub fn zhang_suen_thin(binary: &Array2<bool>) -> Array2<bool> {
let rows = binary.nrows();
let cols = binary.ncols();
let mut img = binary.clone();
loop {
let mut changed = false;
let to_delete_1 = collect_deletable_zs(&img, 1);
for (r, c) in &to_delete_1 {
img[[*r, *c]] = false;
changed = true;
}
let to_delete_2 = collect_deletable_zs(&img, 2);
for (r, c) in &to_delete_2 {
img[[*r, *c]] = false;
changed = true;
}
if !changed {
break;
}
}
img
}
fn collect_deletable_zs(img: &Array2<bool>, step: u8) -> Vec<(usize, usize)> {
let rows = img.nrows();
let cols = img.ncols();
let mut to_delete = Vec::new();
for r in 1..(rows - 1) {
for c in 1..(cols - 1) {
if !img[[r, c]] {
continue;
}
let p2 = img[[r - 1, c]] as u8;
let p3 = img[[r - 1, c + 1]] as u8;
let p4 = img[[r, c + 1]] as u8;
let p5 = img[[r + 1, c + 1]] as u8;
let p6 = img[[r + 1, c]] as u8;
let p7 = img[[r + 1, c - 1]] as u8;
let p8 = img[[r, c - 1]] as u8;
let p9 = img[[r - 1, c - 1]] as u8;
let neighbors = [p2, p3, p4, p5, p6, p7, p8, p9];
let b: u8 = neighbors.iter().sum();
if !(2..=6).contains(&b) {
continue;
}
let seq = [p2, p3, p4, p5, p6, p7, p8, p9, p2]; let a: u8 = seq
.windows(2)
.map(|w| if w[0] == 0 && w[1] == 1 { 1 } else { 0 })
.sum();
if a != 1 {
continue;
}
if step == 1 {
if p2 * p4 * p6 != 0 || p4 * p6 * p8 != 0 {
continue;
}
} else {
if p2 * p4 * p8 != 0 || p2 * p6 * p8 != 0 {
continue;
}
}
to_delete.push((r, c));
}
}
to_delete
}
pub fn lee_thin(binary: &Array2<bool>) -> Array2<bool> {
let rows = binary.nrows();
let cols = binary.ncols();
let mut img = binary.clone();
loop {
let mut changed = false;
for direction in 0..4u8 {
let mut to_delete: Vec<(usize, usize)> = Vec::new();
for r in 1..(rows - 1) {
for c in 1..(cols - 1) {
if !img[[r, c]] {
continue;
}
let is_border = match direction {
0 => !img[[r - 1, c]], 1 => !img[[r + 1, c]], 2 => !img[[r, c - 1]], _ => !img[[r, c + 1]], };
if !is_border {
continue;
}
if is_simple_point(&img, r, c) {
to_delete.push((r, c));
}
}
}
for (r, c) in &to_delete {
img[[*r, *c]] = false;
changed = true;
}
}
if !changed {
break;
}
}
img
}
fn is_simple_point(img: &Array2<bool>, r: usize, c: usize) -> bool {
let fg_cc = count_fg_components(img, r, c);
if fg_cc != 1 {
return false;
}
let bg_cc = count_bg_components(img, r, c);
bg_cc == 1
}
fn count_fg_components(img: &Array2<bool>, r: usize, c: usize) -> usize {
let nbrs: [(isize, isize); 8] = [
(-1, 0),
(-1, 1),
(0, 1),
(1, 1),
(1, 0),
(1, -1),
(0, -1),
(-1, -1),
];
let rows = img.nrows() as isize;
let cols = img.ncols() as isize;
let mut visited = [false; 8];
let mut count = 0;
for start in 0..8 {
let (dr, dc) = nbrs[start];
let nr = r as isize + dr;
let nc = c as isize + dc;
if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
continue;
}
if !img[[nr as usize, nc as usize]] || visited[start] {
continue;
}
count += 1;
let mut queue = VecDeque::new();
queue.push_back(start);
visited[start] = true;
while let Some(cur) = queue.pop_front() {
for other in 0..8 {
if visited[other] {
continue;
}
let (dr2, dc2) = nbrs[other];
let nr2 = r as isize + dr2;
let nc2 = c as isize + dc2;
if nr2 < 0 || nr2 >= rows || nc2 < 0 || nc2 >= cols {
continue;
}
if !img[[nr2 as usize, nc2 as usize]] {
continue;
}
let (dr1, dc1) = nbrs[cur];
if (dr1 - dr2).abs() <= 1 && (dc1 - dc2).abs() <= 1 {
visited[other] = true;
queue.push_back(other);
}
}
}
}
count
}
fn count_bg_components(img: &Array2<bool>, r: usize, c: usize) -> usize {
let rows = img.nrows() as isize;
let cols = img.ncols() as isize;
let nbrs4: [(isize, isize); 4] = [(-1, 0), (0, 1), (1, 0), (0, -1)];
let mut visited = [false; 4];
let mut count = 0;
for start in 0..4 {
let (dr, dc) = nbrs4[start];
let nr = r as isize + dr;
let nc = c as isize + dc;
let is_bg = if nr < 0 || nr >= rows || nc < 0 || nc >= cols {
true
} else {
!img[[nr as usize, nc as usize]]
};
if !is_bg || visited[start] {
continue;
}
count += 1;
visited[start] = true;
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(cur) = queue.pop_front() {
for other in 0..4 {
if visited[other] {
continue;
}
let (dr2, dc2) = nbrs4[other];
let nr2 = r as isize + dr2;
let nc2 = c as isize + dc2;
let is_bg2 = if nr2 < 0 || nr2 >= rows || nc2 < 0 || nc2 >= cols {
true
} else {
!img[[nr2 as usize, nc2 as usize]]
};
if !is_bg2 {
continue;
}
let (dr1, dc1) = nbrs4[cur];
let (dr2c, dc2c) = nbrs4[other];
if (dr1 - dr2c).abs() + (dc1 - dc2c).abs() <= 2 {
visited[other] = true;
queue.push_back(other);
}
}
}
}
count
}
#[derive(Debug, Clone)]
pub struct MedialAxisResult {
pub skeleton: Array2<bool>,
pub distance: Array2<f64>,
}
pub fn medial_axis(binary: &Array2<bool>) -> NdimageResult<MedialAxisResult> {
let rows = binary.nrows();
let cols = binary.ncols();
let distance = euclidean_dt(binary)?;
let mut skeleton_raw = Array2::<bool>::from_elem((rows, cols), false);
for r in 1..(rows.saturating_sub(1)) {
for c in 1..(cols.saturating_sub(1)) {
if !binary[[r, c]] {
continue;
}
let v = distance[[r, c]];
if v <= 0.0 {
continue;
}
let is_local_max = (-1i32..=1).all(|dr| {
(-1i32..=1).all(|dc| {
let nr = (r as i32 + dr) as usize;
let nc = (c as i32 + dc) as usize;
distance[[nr, nc]] <= v
})
});
if is_local_max {
skeleton_raw[[r, c]] = true;
}
}
}
let skeleton = zhang_suen_thin(&skeleton_raw);
Ok(MedialAxisResult { skeleton, distance })
}
pub fn distance_transform_edt(binary: &Array2<bool>) -> NdimageResult<Array2<f64>> {
euclidean_dt(binary)
}
pub fn distance_transform_cdt(binary: &Array2<bool>) -> NdimageResult<Array2<f64>> {
cityblock_dt(binary)
}
pub fn convex_hull_image(binary: &Array2<bool>) -> Array2<bool> {
let rows = binary.nrows();
let cols = binary.ncols();
let points: Vec<(i64, i64)> = binary
.indexed_iter()
.filter_map(|((r, c), &v)| if v { Some((r as i64, c as i64)) } else { None })
.collect();
if points.len() < 3 {
return binary.clone();
}
let hull = graham_scan(points);
let mut result = Array2::<bool>::from_elem((rows, cols), false);
rasterise_convex_hull(&hull, &mut result);
result
}
fn graham_scan(mut points: Vec<(i64, i64)>) -> Vec<(i64, i64)> {
let pivot_idx = points
.iter()
.enumerate()
.min_by_key(|&(_, &(r, c))| (r, c))
.map(|(i, _)| i)
.unwrap_or(0);
points.swap(0, pivot_idx);
let pivot = points[0];
points[1..].sort_by(|&(r1, c1), &(r2, c2)| {
let cross = (r1 - pivot.0) * (c2 - pivot.1) - (r2 - pivot.0) * (c1 - pivot.1);
if cross != 0 {
return cross.cmp(&0).reverse(); }
let d1 = (r1 - pivot.0).pow(2) + (c1 - pivot.1).pow(2);
let d2 = (r2 - pivot.0).pow(2) + (c2 - pivot.1).pow(2);
d1.cmp(&d2)
});
let mut hull: Vec<(i64, i64)> = Vec::with_capacity(points.len());
for p in &points {
while hull.len() >= 2 {
let a = hull[hull.len() - 2];
let b = hull[hull.len() - 1];
let cross = (b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0);
if cross <= 0 {
hull.pop();
} else {
break;
}
}
hull.push(*p);
}
hull
}
fn rasterise_convex_hull(hull: &[(i64, i64)], out: &mut Array2<bool>) {
if hull.is_empty() {
return;
}
let rows = out.nrows() as i64;
let cols = out.ncols() as i64;
let min_r = hull.iter().map(|&(r, _)| r).min().unwrap_or(0).max(0);
let max_r = hull.iter().map(|&(r, _)| r).max().unwrap_or(0).min(rows - 1);
let n = hull.len();
for scan_r in min_r..=max_r {
let mut xs: Vec<i64> = Vec::new();
for i in 0..n {
let (r0, c0) = hull[i];
let (r1, c1) = hull[(i + 1) % n];
let (rmin, rmax) = if r0 <= r1 { (r0, r1) } else { (r1, r0) };
if scan_r < rmin || scan_r > rmax {
continue;
}
let dr = r1 - r0;
if dr == 0 {
xs.push(c0);
xs.push(c1);
} else {
let x = c0 + (scan_r - r0) * (c1 - c0) / dr;
xs.push(x);
}
}
if xs.is_empty() {
continue;
}
xs.sort_unstable();
let x_min = xs[0].max(0).min(cols - 1) as usize;
let x_max = xs[xs.len() - 1].max(0).min(cols - 1) as usize;
for c in x_min..=x_max {
out[[scan_r as usize, c]] = true;
}
}
}
pub fn binary_fill_holes(binary: &Array2<bool>) -> Array2<bool> {
let rows = binary.nrows();
let cols = binary.ncols();
let mut out = binary.clone();
let mut reachable = Array2::<bool>::from_elem((rows, cols), false);
let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
let seed_if_bg = |r: usize, c: usize, img: &Array2<bool>, reach: &mut Array2<bool>, q: &mut VecDeque<(usize, usize)>| {
if !img[[r, c]] && !reach[[r, c]] {
reach[[r, c]] = true;
q.push_back((r, c));
}
};
for c in 0..cols {
seed_if_bg(0, c, binary, &mut reachable, &mut queue);
if rows > 1 {
seed_if_bg(rows - 1, c, binary, &mut reachable, &mut queue);
}
}
for r in 0..rows {
seed_if_bg(r, 0, binary, &mut reachable, &mut queue);
if cols > 1 {
seed_if_bg(r, cols - 1, binary, &mut reachable, &mut queue);
}
}
let dirs: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
while let Some((r, c)) = queue.pop_front() {
for &(dr, dc) in &dirs {
let nr = r as i32 + dr;
let nc = c as i32 + dc;
if nr < 0 || nr >= rows as i32 || nc < 0 || nc >= cols as i32 {
continue;
}
let nr = nr as usize;
let nc = nc as usize;
if !binary[[nr, nc]] && !reachable[[nr, nc]] {
reachable[[nr, nc]] = true;
queue.push_back((nr, nc));
}
}
}
for r in 0..rows {
for c in 0..cols {
if !binary[[r, c]] && !reachable[[r, c]] {
out[[r, c]] = true;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn filled_rect(rows: usize, cols: usize) -> Array2<bool> {
Array2::from_elem((rows, cols), true)
}
fn ring(size: usize) -> Array2<bool> {
Array2::from_shape_fn((size, size), |(r, c)| {
let d = (r as i32 - size as i32 / 2).abs().max((c as i32 - size as i32 / 2).abs());
d == size as i32 / 2 - 1
})
}
#[test]
fn zhang_suen_solid_rect_produces_skeleton() {
let binary = filled_rect(15, 15);
let skel = zhang_suen_thin(&binary);
assert!(
skel.iter().any(|&v| v),
"Expected non-empty skeleton from filled rectangle"
);
let rows = skel.nrows();
let cols = skel.ncols();
for r in 1..(rows - 1) {
for c in 1..(cols - 1) {
if skel[[r, c]] {
let nbr_count: u8 = [
skel[[r - 1, c - 1]] as u8,
skel[[r - 1, c]] as u8,
skel[[r - 1, c + 1]] as u8,
skel[[r, c - 1]] as u8,
skel[[r, c + 1]] as u8,
skel[[r + 1, c - 1]] as u8,
skel[[r + 1, c]] as u8,
skel[[r + 1, c + 1]] as u8,
]
.iter()
.sum();
assert_ne!(nbr_count, 8, "Skeleton pixel ({r},{c}) is fully surrounded");
}
}
}
}
#[test]
fn zhang_suen_single_pixel_unchanged() {
let mut binary = Array2::from_elem((5, 5), false);
binary[[2, 2]] = true;
let skel = zhang_suen_thin(&binary);
assert!(skel[[2, 2]]);
let total: usize = skel.iter().map(|&v| v as usize).sum();
assert_eq!(total, 1);
}
#[test]
fn zhang_suen_empty_image_unchanged() {
let binary = Array2::from_elem((10, 10), false);
let skel = zhang_suen_thin(&binary);
assert!(skel.iter().all(|&v| !v));
}
#[test]
fn zhang_suen_horizontal_line_unchanged() {
let mut binary = Array2::from_elem((10, 30), false);
for c in 0..30 {
binary[[5, c]] = true;
}
let skel = zhang_suen_thin(&binary);
assert!(skel.iter().any(|&v| v));
}
#[test]
fn lee_thin_produces_nonempty_skeleton() {
let binary = filled_rect(12, 12);
let skel = lee_thin(&binary);
assert!(
skel.iter().any(|&v| v),
"Expected non-empty skeleton from Lee thinning"
);
}
#[test]
fn lee_thin_preserves_connectivity() {
let size = 20;
let binary = Array2::from_shape_fn((size, size), |(r, c)| {
let mid = size / 2;
(r >= mid - 2 && r < mid + 2) || (c >= mid - 2 && c < mid + 2)
});
let skel = lee_thin(&binary);
assert!(skel.iter().any(|&v| v));
}
#[test]
fn medial_axis_disk_center() {
let size = 30usize;
let center = size / 2;
let radius = 10i32;
let binary = Array2::from_shape_fn((size, size), |(r, c)| {
let dr = r as i32 - center as i32;
let dc = c as i32 - center as i32;
dr * dr + dc * dc <= radius * radius
});
let result = medial_axis(&binary).expect("medial_axis failed");
assert!(
result.skeleton.iter().any(|&v| v),
"Medial axis of disk should be non-empty"
);
let d = result.distance[[center, center]];
assert!(d > 5.0, "Distance at center ({d}) should be > 5");
}
#[test]
fn distance_transform_edt_all_true_gives_positive_dist() {
let binary = Array2::from_elem((10, 10), true);
let dt = distance_transform_edt(&binary).expect("edt failed");
for r in 0..10 {
for c in 0..10 {
assert!(dt[[r, c]] >= 0.0);
}
}
}
#[test]
fn distance_transform_edt_background_is_zero() {
let binary = Array2::from_elem((10, 10), false);
let dt = distance_transform_edt(&binary).expect("edt failed");
assert!(dt.iter().all(|&v| v == 0.0));
}
#[test]
fn distance_transform_edt_center_of_square() {
let binary = Array2::from_shape_fn((9, 9), |(r, c)| {
r >= 2 && r <= 6 && c >= 2 && c <= 6
});
let dt = distance_transform_edt(&binary).expect("edt ok");
let center_dist = dt[[4, 4]];
assert!(
(center_dist - 2.0).abs() < 0.5,
"Center distance {center_dist} not close to 2.0"
);
assert_eq!(dt[[0, 0]], 0.0);
}
#[test]
fn distance_transform_cdt_matches_l1() {
let binary = Array2::from_shape_fn((7, 7), |(r, c)| r >= 1 && r <= 5 && c >= 1 && c <= 5);
let dt = distance_transform_cdt(&binary).expect("cdt ok");
assert_eq!(dt[[0, 0]], 0.0);
let center = dt[[3, 3]];
assert!(center >= 1.0, "L1 center distance {center}");
}
#[test]
fn convex_hull_of_convex_shape_is_unchanged() {
let size = 20usize;
let center = 10i32;
let r_sq = 8i32;
let binary = Array2::from_shape_fn((size, size), |(r, c)| {
let dr = r as i32 - center;
let dc = c as i32 - center;
dr * dr + dc * dc <= r_sq * r_sq
});
let hull = convex_hull_image(&binary);
for ((r, c), &v) in binary.indexed_iter() {
if v {
assert!(hull[[r, c]], "Disk pixel ({r},{c}) missing from hull");
}
}
}
#[test]
fn convex_hull_fills_concavity() {
let mut binary = Array2::from_elem((10, 10), false);
for r in 0..10 {
binary[[r, 0]] = true;
}
for c in 0..10 {
binary[[9, c]] = true;
}
let hull = convex_hull_image(&binary);
assert!(hull.iter().any(|&v| v));
}
#[test]
fn fill_holes_fills_enclosed_region() {
let size = 9usize;
let mut binary = Array2::from_elem((size, size), false);
for r in 1..(size - 1) {
for c in 1..(size - 1) {
let dr = r as i32 - 4;
let dc = c as i32 - 4;
let d2 = dr * dr + dc * dc;
if d2 >= 4 && d2 <= 16 {
binary[[r, c]] = true;
}
}
}
let filled = binary_fill_holes(&binary);
assert!(
filled[[4, 4]],
"Hole at center should be filled, but it is not"
);
}
#[test]
fn fill_holes_does_not_fill_exterior() {
let binary = Array2::from_shape_fn((9, 9), |(r, c)| {
r >= 2 && r <= 6 && c >= 2 && c <= 6
});
let filled = binary_fill_holes(&binary);
assert!(!filled[[0, 0]]);
assert!(filled[[4, 4]]);
}
#[test]
fn fill_holes_all_background_unchanged() {
let binary = Array2::from_elem((10, 10), false);
let filled = binary_fill_holes(&binary);
assert!(filled.iter().all(|&v| !v));
}
}