use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::Array2;
use std::cmp::Ordering;
use std::collections::BinaryHeap;
pub fn euclidean_dt(binary: &Array2<bool>) -> NdimageResult<Array2<f64>> {
let rows = binary.nrows();
let cols = binary.ncols();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let inf = (rows + cols) as f64;
let mut g = Array2::<f64>::from_elem((rows, cols), 0.0);
for c in 0..cols {
if binary[[0, c]] {
g[[0, c]] = inf;
}
for r in 1..rows {
if binary[[r, c]] {
g[[r, c]] = g[[r - 1, c]] + 1.0;
} else {
g[[r, c]] = 0.0;
}
}
for r in (0..rows.saturating_sub(1)).rev() {
let candidate = g[[r + 1, c]] + 1.0;
if candidate < g[[r, c]] {
g[[r, c]] = candidate;
}
}
}
let mut dt = Array2::<f64>::zeros((rows, cols));
let mut s = vec![0i64; cols]; let mut t = vec![0.0f64; cols];
for r in 0..rows {
let mut k = 0usize; s[0] = 0;
t[0] = f64::NEG_INFINITY;
for q in 1..cols {
let fq = g[[r, q]];
let fq2 = fq * fq;
loop {
let sq = s[k] as f64;
let fsq = g[[r, s[k] as usize]];
let fsq2 = fsq * fsq;
let sep =
((q as f64 * q as f64 - sq * sq) + (fq2 - fsq2)) / (2.0 * (q as f64 - sq));
if sep > t[k] {
k += 1;
s[k] = q as i64;
t[k] = sep;
break;
}
if k == 0 {
s[0] = q as i64;
break;
}
k -= 1;
}
}
let mut k_scan = k;
for c in (0..cols).rev() {
while k_scan > 0 && t[k_scan] > c as f64 {
k_scan -= 1;
}
let sq = s[k_scan] as usize;
let dx = c as f64 - sq as f64;
let gy = g[[r, sq]];
dt[[r, c]] = (dx * dx + gy * gy).sqrt();
}
}
Ok(dt)
}
pub fn cityblock_dt(binary: &Array2<bool>) -> NdimageResult<Array2<f64>> {
let rows = binary.nrows();
let cols = binary.ncols();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let inf = (rows + cols) as f64;
let mut dist = Array2::<f64>::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
if binary[[r, c]] {
dist[[r, c]] = inf;
}
}
}
for r in 0..rows {
for c in 0..cols {
if !binary[[r, c]] {
continue;
}
if r > 0 {
let candidate = dist[[r - 1, c]] + 1.0;
if candidate < dist[[r, c]] {
dist[[r, c]] = candidate;
}
}
if c > 0 {
let candidate = dist[[r, c - 1]] + 1.0;
if candidate < dist[[r, c]] {
dist[[r, c]] = candidate;
}
}
}
}
for r in (0..rows).rev() {
for c in (0..cols).rev() {
if !binary[[r, c]] {
continue;
}
if r + 1 < rows {
let candidate = dist[[r + 1, c]] + 1.0;
if candidate < dist[[r, c]] {
dist[[r, c]] = candidate;
}
}
if c + 1 < cols {
let candidate = dist[[r, c + 1]] + 1.0;
if candidate < dist[[r, c]] {
dist[[r, c]] = candidate;
}
}
}
}
Ok(dist)
}
pub fn chessboard_dt(binary: &Array2<bool>) -> NdimageResult<Array2<f64>> {
let rows = binary.nrows();
let cols = binary.ncols();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let inf = (rows + cols) as f64;
let mut dist = Array2::<f64>::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
if binary[[r, c]] {
dist[[r, c]] = inf;
}
}
}
for r in 0..rows {
for c in 0..cols {
if !binary[[r, c]] {
continue;
}
let candidates = [
if r > 0 { dist[[r - 1, c]] + 1.0 } else { inf },
if c > 0 { dist[[r, c - 1]] + 1.0 } else { inf },
if r > 0 && c > 0 {
dist[[r - 1, c - 1]] + 1.0
} else {
inf
},
if r > 0 && c + 1 < cols {
dist[[r - 1, c + 1]] + 1.0
} else {
inf
},
];
for &cand in &candidates {
if cand < dist[[r, c]] {
dist[[r, c]] = cand;
}
}
}
}
for r in (0..rows).rev() {
for c in (0..cols).rev() {
if !binary[[r, c]] {
continue;
}
let candidates = [
if r + 1 < rows {
dist[[r + 1, c]] + 1.0
} else {
inf
},
if c + 1 < cols {
dist[[r, c + 1]] + 1.0
} else {
inf
},
if r + 1 < rows && c + 1 < cols {
dist[[r + 1, c + 1]] + 1.0
} else {
inf
},
if r + 1 < rows && c > 0 {
dist[[r + 1, c - 1]] + 1.0
} else {
inf
},
];
for &cand in &candidates {
if cand < dist[[r, c]] {
dist[[r, c]] = cand;
}
}
}
}
Ok(dist)
}
#[derive(Clone, Debug)]
struct GeoEntry {
row: usize,
col: usize,
dist: f64,
}
impl PartialEq for GeoEntry {
fn eq(&self, other: &Self) -> bool {
self.row == other.row && self.col == other.col
}
}
impl Eq for GeoEntry {}
impl PartialOrd for GeoEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for GeoEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.dist
.partial_cmp(&self.dist)
.unwrap_or(Ordering::Equal)
}
}
pub fn geodesic_dt(mask: &Array2<bool>, seeds: &Array2<bool>) -> NdimageResult<Array2<f64>> {
if mask.shape() != seeds.shape() {
return Err(NdimageError::DimensionError(
"Mask and seeds must have the same shape".to_string(),
));
}
let rows = mask.nrows();
let cols = mask.ncols();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let sqrt2 = std::f64::consts::SQRT_2;
let neighbors: [(isize, isize, f64); 8] = [
(-1, -1, sqrt2),
(-1, 0, 1.0),
(-1, 1, sqrt2),
(0, -1, 1.0),
(0, 1, 1.0),
(1, -1, sqrt2),
(1, 0, 1.0),
(1, 1, sqrt2),
];
let mut dist = Array2::<f64>::from_elem((rows, cols), f64::INFINITY);
let mut visited = Array2::<bool>::from_elem((rows, cols), false);
let mut queue = BinaryHeap::new();
for r in 0..rows {
for c in 0..cols {
if seeds[[r, c]] && mask[[r, c]] {
dist[[r, c]] = 0.0;
queue.push(GeoEntry {
row: r,
col: c,
dist: 0.0,
});
}
}
}
while let Some(entry) = queue.pop() {
let r = entry.row;
let c = entry.col;
if visited[[r, c]] {
continue;
}
visited[[r, c]] = true;
for &(dr, dc, step_cost) in &neighbors {
let nr = r as isize + dr;
let nc = c as isize + dc;
if nr < 0 || nr >= rows as isize || nc < 0 || nc >= cols as isize {
continue;
}
let nr = nr as usize;
let nc = nc as usize;
if !mask[[nr, nc]] || visited[[nr, nc]] {
continue;
}
let new_dist = entry.dist + step_cost;
if new_dist < dist[[nr, nc]] {
dist[[nr, nc]] = new_dist;
queue.push(GeoEntry {
row: nr,
col: nc,
dist: new_dist,
});
}
}
}
Ok(dist)
}
pub fn signed_distance_function(binary: &Array2<bool>) -> NdimageResult<Array2<f64>> {
let rows = binary.nrows();
let cols = binary.ncols();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let has_foreground = binary.iter().any(|&v| v);
let has_background = binary.iter().any(|&v| !v);
if !has_foreground {
return Ok(Array2::zeros((rows, cols)));
}
if !has_background {
return Ok(Array2::zeros((rows, cols)));
}
let dist_inside = euclidean_dt(binary)?;
let complement: Array2<bool> = binary.mapv(|v| !v);
let dist_outside = euclidean_dt(&complement)?;
let mut sdf = Array2::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
if binary[[r, c]] {
sdf[[r, c]] = -dist_inside[[r, c]];
} else {
sdf[[r, c]] = dist_outside[[r, c]];
}
}
}
Ok(sdf)
}
pub fn nearest_background(binary: &Array2<bool>) -> NdimageResult<(Array2<usize>, Array2<usize>)> {
let rows = binary.nrows();
let cols = binary.ncols();
if rows == 0 || cols == 0 {
return Ok((Array2::zeros((rows, cols)), Array2::zeros((rows, cols))));
}
let inf_dist = ((rows * rows + cols * cols) as f64).sqrt() + 1.0;
let mut dist = Array2::<f64>::from_elem((rows, cols), inf_dist);
let mut nearest_r = Array2::<usize>::zeros((rows, cols));
let mut nearest_c = Array2::<usize>::zeros((rows, cols));
for r in 0..rows {
for c in 0..cols {
if !binary[[r, c]] {
dist[[r, c]] = 0.0;
nearest_r[[r, c]] = r;
nearest_c[[r, c]] = c;
}
}
}
for r in 0..rows {
for c in 0..cols {
if !binary[[r, c]] {
continue;
}
if r > 0 {
let nr = nearest_r[[r - 1, c]];
let nc = nearest_c[[r - 1, c]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
if c > 0 {
let nr = nearest_r[[r, c - 1]];
let nc = nearest_c[[r, c - 1]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
if r > 0 && c > 0 {
let nr = nearest_r[[r - 1, c - 1]];
let nc = nearest_c[[r - 1, c - 1]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
if r > 0 && c + 1 < cols {
let nr = nearest_r[[r - 1, c + 1]];
let nc = nearest_c[[r - 1, c + 1]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
}
}
for r in (0..rows).rev() {
for c in (0..cols).rev() {
if !binary[[r, c]] {
continue;
}
if r + 1 < rows {
let nr = nearest_r[[r + 1, c]];
let nc = nearest_c[[r + 1, c]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
if c + 1 < cols {
let nr = nearest_r[[r, c + 1]];
let nc = nearest_c[[r, c + 1]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
if r + 1 < rows && c + 1 < cols {
let nr = nearest_r[[r + 1, c + 1]];
let nc = nearest_c[[r + 1, c + 1]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
if r + 1 < rows && c > 0 {
let nr = nearest_r[[r + 1, c - 1]];
let nc = nearest_c[[r + 1, c - 1]];
let d =
(((r as f64 - nr as f64).powi(2)) + ((c as f64 - nc as f64).powi(2))).sqrt();
if d < dist[[r, c]] {
dist[[r, c]] = d;
nearest_r[[r, c]] = nr;
nearest_c[[r, c]] = nc;
}
}
}
}
Ok((nearest_r, nearest_c))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_euclidean_dt_empty() {
let binary: Array2<bool> = Array2::from_elem((0, 0), false);
let dist = euclidean_dt(&binary).expect("should succeed");
assert_eq!(dist.len(), 0);
}
#[test]
fn test_euclidean_dt_all_background() {
let binary = Array2::from_elem((5, 5), false);
let dist = euclidean_dt(&binary).expect("should succeed");
for &v in dist.iter() {
assert_abs_diff_eq!(v, 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_euclidean_dt_all_foreground() {
let binary = Array2::from_elem((5, 5), true);
let dist = euclidean_dt(&binary).expect("should succeed");
for &v in dist.iter() {
assert!(v > 0.0);
}
}
#[test]
fn test_euclidean_dt_single_background() {
let mut binary = Array2::from_elem((5, 5), true);
binary[[2, 2]] = false;
let dist = euclidean_dt(&binary).expect("should succeed");
assert_abs_diff_eq!(dist[[2, 2]], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[2, 3]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[1, 1]], std::f64::consts::SQRT_2, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[0, 2]], 2.0, epsilon = 1e-10);
}
#[test]
fn test_euclidean_dt_border() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let dist = euclidean_dt(&binary).expect("should succeed");
assert_abs_diff_eq!(dist[[1, 1]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[2, 2]], 2.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[0, 0]], 0.0, epsilon = 1e-10);
}
#[test]
fn test_euclidean_dt_asymmetric() {
let binary = array![
[false, false, false, false, false, false, false],
[false, true, true, true, true, true, false],
[false, true, true, true, true, true, false],
[false, false, false, false, false, false, false],
];
let dist = euclidean_dt(&binary).expect("should succeed");
assert_abs_diff_eq!(dist[[1, 1]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[1, 3]], 1.0, epsilon = 1e-10);
}
#[test]
fn test_cityblock_dt_basic() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let dist = cityblock_dt(&binary).expect("should succeed");
assert_abs_diff_eq!(dist[[0, 0]], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[1, 1]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[2, 2]], 2.0, epsilon = 1e-10);
}
#[test]
fn test_cityblock_dt_single_pixel() {
let mut binary = Array2::from_elem((5, 5), false);
binary[[2, 2]] = true;
let dist = cityblock_dt(&binary).expect("should succeed");
assert_abs_diff_eq!(dist[[2, 2]], 1.0, epsilon = 1e-10);
}
#[test]
fn test_cityblock_dt_empty() {
let binary: Array2<bool> = Array2::from_elem((0, 0), false);
let dist = cityblock_dt(&binary).expect("should succeed");
assert_eq!(dist.len(), 0);
}
#[test]
fn test_chessboard_dt_basic() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let dist = chessboard_dt(&binary).expect("should succeed");
assert_abs_diff_eq!(dist[[0, 0]], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[1, 1]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[2, 2]], 2.0, epsilon = 1e-10);
}
#[test]
fn test_chessboard_dt_vs_cityblock() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let chess = chessboard_dt(&binary).expect("chessboard");
let city = cityblock_dt(&binary).expect("cityblock");
for r in 0..5 {
for c in 0..5 {
assert!(
chess[[r, c]] <= city[[r, c]] + 1e-10,
"Chessboard should be <= cityblock at ({}, {})",
r,
c
);
}
}
}
#[test]
fn test_geodesic_dt_basic() {
let mask = array![
[true, true, true, true, true],
[true, false, false, false, true],
[true, false, false, false, true],
[true, false, false, false, true],
[true, true, true, true, true],
];
let mut seeds = Array2::from_elem((5, 5), false);
seeds[[0, 0]] = true;
let dist = geodesic_dt(&mask, &seeds).expect("should succeed");
assert_abs_diff_eq!(dist[[0, 0]], 0.0, epsilon = 1e-10);
assert!(dist[[2, 2]].is_infinite());
assert!(dist[[0, 4]].is_finite());
assert_abs_diff_eq!(dist[[0, 4]], 4.0, epsilon = 1e-10);
assert!(dist[[4, 0]].is_finite());
assert_abs_diff_eq!(dist[[4, 0]], 4.0, epsilon = 1e-10);
}
#[test]
fn test_geodesic_dt_shape_mismatch() {
let mask = Array2::from_elem((3, 3), true);
let seeds = Array2::from_elem((4, 4), false);
assert!(geodesic_dt(&mask, &seeds).is_err());
}
#[test]
fn test_geodesic_dt_empty() {
let mask: Array2<bool> = Array2::from_elem((0, 0), false);
let seeds: Array2<bool> = Array2::from_elem((0, 0), false);
let dist = geodesic_dt(&mask, &seeds).expect("should succeed");
assert_eq!(dist.len(), 0);
}
#[test]
fn test_geodesic_dt_full_mask() {
let mask = Array2::from_elem((5, 5), true);
let mut seeds = Array2::from_elem((5, 5), false);
seeds[[2, 2]] = true;
let dist = geodesic_dt(&mask, &seeds).expect("should succeed");
assert_abs_diff_eq!(dist[[2, 2]], 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[2, 3]], 1.0, epsilon = 1e-10);
assert_abs_diff_eq!(dist[[1, 1]], std::f64::consts::SQRT_2, epsilon = 1e-10);
}
#[test]
fn test_sdf_basic() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let sdf = signed_distance_function(&binary).expect("should succeed");
assert!(sdf[[0, 0]] > 0.0);
assert!(sdf[[2, 2]] < 0.0);
assert_abs_diff_eq!(sdf[[1, 2]], -1.0, epsilon = 1e-10);
assert_abs_diff_eq!(sdf[[2, 2]], -2.0, epsilon = 1e-10);
}
#[test]
fn test_sdf_empty() {
let binary: Array2<bool> = Array2::from_elem((0, 0), false);
let sdf = signed_distance_function(&binary).expect("should succeed");
assert_eq!(sdf.len(), 0);
}
#[test]
fn test_sdf_all_foreground() {
let binary = Array2::from_elem((5, 5), true);
let sdf = signed_distance_function(&binary).expect("should succeed");
for &v in sdf.iter() {
assert!(v <= 0.0);
}
}
#[test]
fn test_sdf_all_background() {
let binary = Array2::from_elem((5, 5), false);
let sdf = signed_distance_function(&binary).expect("should succeed");
for &v in sdf.iter() {
assert_abs_diff_eq!(v, 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_nearest_background_basic() {
let binary = array![
[false, false, false],
[false, true, false],
[false, false, false],
];
let (nr, nc) = nearest_background(&binary).expect("should succeed");
let dist_r = (1.0 - nr[[1, 1]] as f64).abs();
let dist_c = (1.0 - nc[[1, 1]] as f64).abs();
let total_dist = (dist_r * dist_r + dist_c * dist_c).sqrt();
assert!(total_dist <= 1.5, "Should map to adjacent pixel");
}
#[test]
fn test_nearest_background_empty() {
let binary: Array2<bool> = Array2::from_elem((0, 0), false);
let (nr, nc) = nearest_background(&binary).expect("should succeed");
assert_eq!(nr.len(), 0);
assert_eq!(nc.len(), 0);
}
#[test]
fn test_edt_vs_cityblock_ordering() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let edt = euclidean_dt(&binary).expect("edt");
let city = cityblock_dt(&binary).expect("cityblock");
for r in 0..5 {
for c in 0..5 {
assert!(
edt[[r, c]] <= city[[r, c]] + 1e-10,
"EDT should be <= cityblock at ({}, {}): edt={}, city={}",
r,
c,
edt[[r, c]],
city[[r, c]]
);
}
}
}
#[test]
fn test_edt_symmetric() {
let binary = array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false],
];
let dist = euclidean_dt(&binary).expect("should succeed");
let rows = dist.nrows();
let cols = dist.ncols();
for r in 0..rows {
for c in 0..cols {
assert_abs_diff_eq!(dist[[r, c]], dist[[rows - 1 - r, c]], epsilon = 1e-10);
assert_abs_diff_eq!(dist[[r, c]], dist[[r, cols - 1 - c]], epsilon = 1e-10);
}
}
}
}