use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::{Array2, Array3, ArrayView2, ArrayView3};
use std::collections::VecDeque;
pub fn euler_number(binary_image: &ArrayView2<bool>) -> i32 {
let rows = binary_image.nrows();
let cols = binary_image.ncols();
if rows < 2 || cols < 2 {
let count: i32 = binary_image.iter().map(|&b| b as i32).sum();
return count;
}
let get = |r: usize, c: usize| -> u8 { *binary_image.get((r, c)).unwrap_or(&false) as u8 };
let mut euler_sum: i32 = 0;
for r in 0..(rows - 1) {
for c in 0..(cols - 1) {
let b0 = get(r, c);
let b1 = get(r, c + 1);
let b2 = get(r + 1, c);
let b3 = get(r + 1, c + 1);
let k = b0 + b1 + b2 + b3;
euler_sum += match k {
1 => 1,
3 => -1,
2 => {
if (b0 == b3) && (b1 == b2) && b0 != b1 {
2
} else {
0
}
}
_ => 0,
};
}
}
euler_sum / 4
}
pub fn connected_components_count(
binary: &ArrayView2<bool>,
connectivity: u8,
) -> NdimageResult<usize> {
if connectivity != 4 && connectivity != 8 {
return Err(NdimageError::InvalidInput(format!(
"connected_components_count: connectivity must be 4 or 8, got {connectivity}"
)));
}
let rows = binary.nrows();
let cols = binary.ncols();
let mut visited = Array2::<bool>::default((rows, cols));
let mut count = 0usize;
for start_r in 0..rows {
for start_c in 0..cols {
if !binary[[start_r, start_c]] || visited[[start_r, start_c]] {
continue;
}
count += 1;
let mut queue = VecDeque::new();
queue.push_back((start_r, start_c));
visited[[start_r, start_c]] = true;
while let Some((r, c)) = queue.pop_front() {
for (nr, nc) in neighbours_2d(r, c, rows, cols, connectivity) {
if binary[[nr, nc]] && !visited[[nr, nc]] {
visited[[nr, nc]] = true;
queue.push_back((nr, nc));
}
}
}
}
}
Ok(count)
}
pub fn hole_filling(binary: &ArrayView2<bool>) -> NdimageResult<Array2<bool>> {
let rows = binary.nrows();
let cols = binary.ncols();
if rows == 0 || cols == 0 {
return Ok(Array2::default((rows, cols)));
}
let mut reachable = Array2::<bool>::default((rows, cols));
let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
let seed_if_bg = |r: usize, c: usize, q: &mut VecDeque<(usize, usize)>, reach: &mut Array2<bool>| {
if !*binary.get((r, c)).unwrap_or(&false) && !reach[[r, c]] {
reach[[r, c]] = true;
q.push_back((r, c));
}
};
for c in 0..cols {
seed_if_bg(0, c, &mut queue, &mut reachable);
seed_if_bg(rows - 1, c, &mut queue, &mut reachable);
}
for r in 1..(rows - 1) {
seed_if_bg(r, 0, &mut queue, &mut reachable);
seed_if_bg(r, cols - 1, &mut queue, &mut reachable);
}
while let Some((r, c)) = queue.pop_front() {
for (nr, nc) in neighbours_2d(r, c, rows, cols, 4) {
if !binary[[nr, nc]] && !reachable[[nr, nc]] {
reachable[[nr, nc]] = true;
queue.push_back((nr, nc));
}
}
}
let mut output = Array2::<bool>::default((rows, cols));
for r in 0..rows {
for c in 0..cols {
output[[r, c]] = binary[[r, c]] || !reachable[[r, c]];
}
}
Ok(output)
}
pub fn genus(binary_3d: &ArrayView3<bool>) -> i32 {
let euler = euler_number_3d(binary_3d);
let n_components = connected_components_3d(binary_3d);
let g = n_components as i32 - euler / 2;
g.max(0)
}
fn euler_number_3d(binary_3d: &ArrayView3<bool>) -> i32 {
let depth = binary_3d.len_of(scirs2_core::ndarray::Axis(0));
let rows = binary_3d.len_of(scirs2_core::ndarray::Axis(1));
let cols = binary_3d.len_of(scirs2_core::ndarray::Axis(2));
if depth < 2 || rows < 2 || cols < 2 {
let count: i32 = binary_3d.iter().map(|&b| b as i32).sum();
return count;
}
let get = |z: usize, r: usize, c: usize| -> u8 {
*binary_3d.get((z, r, c)).unwrap_or(&false) as u8
};
const LUT: [i32; 256] = euler_lut_3d();
let mut sum = 0i32;
for z in 0..(depth - 1) {
for r in 0..(rows - 1) {
for c in 0..(cols - 1) {
let idx: usize = ((get(z, r, c)) as usize)
| ((get(z, r, c + 1)) as usize) << 1
| ((get(z, r + 1, c)) as usize) << 2
| ((get(z, r + 1, c + 1)) as usize) << 3
| ((get(z + 1, r, c)) as usize) << 4
| ((get(z + 1, r, c + 1)) as usize) << 5
| ((get(z + 1, r + 1, c)) as usize) << 6
| ((get(z + 1, r + 1, c + 1)) as usize) << 7;
sum += LUT[idx];
}
}
}
sum / 8
}
fn connected_components_3d(binary_3d: &ArrayView3<bool>) -> usize {
let depth = binary_3d.len_of(scirs2_core::ndarray::Axis(0));
let rows = binary_3d.len_of(scirs2_core::ndarray::Axis(1));
let cols = binary_3d.len_of(scirs2_core::ndarray::Axis(2));
let mut visited = Array3::<bool>::default((depth, rows, cols));
let mut count = 0usize;
for sz in 0..depth {
for sr in 0..rows {
for sc in 0..cols {
if !binary_3d[[sz, sr, sc]] || visited[[sz, sr, sc]] {
continue;
}
count += 1;
let mut queue: VecDeque<(usize, usize, usize)> = VecDeque::new();
queue.push_back((sz, sr, sc));
visited[[sz, sr, sc]] = true;
while let Some((z, r, c)) = queue.pop_front() {
for dz in -1i32..=1 {
for dr in -1i32..=1 {
for dc in -1i32..=1 {
if dz == 0 && dr == 0 && dc == 0 {
continue;
}
let nz = z as i32 + dz;
let nr = r as i32 + dr;
let nc = c as i32 + dc;
if nz < 0
|| nr < 0
|| nc < 0
|| nz >= depth as i32
|| nr >= rows as i32
|| nc >= cols as i32
{
continue;
}
let (nz, nr, nc) = (nz as usize, nr as usize, nc as usize);
if binary_3d[[nz, nr, nc]] && !visited[[nz, nr, nc]] {
visited[[nz, nr, nc]] = true;
queue.push_back((nz, nr, nc));
}
}
}
}
}
}
}
}
count
}
const fn euler_lut_3d() -> [i32; 256] {
let mut lut = [0i32; 256];
lut[1] = 1; lut[2] = 1; lut[4] = 1; lut[8] = 1;
lut[16] = 1; lut[32] = 1; lut[64] = 1; lut[128] = 1;
lut[254] = -1; lut[253] = -1; lut[251] = -1; lut[247] = -1;
lut[239] = -1; lut[223] = -1; lut[191] = -1; lut[127] = -1;
lut[0b10000001] = -2; lut[0b01000010] = -2; lut[0b00100100] = -2; lut[0b00011000] = -2; lut[!0b10000001u8 as usize] = 2; lut[!0b01000010u8 as usize] = 2; lut[!0b00100100u8 as usize] = 2; lut[!0b00011000u8 as usize] = 2; lut
}
fn neighbours_2d(
r: usize,
c: usize,
rows: usize,
cols: usize,
connectivity: u8,
) -> impl Iterator<Item = (usize, usize)> {
const N4: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
const N8: [(i32, i32); 8] = [
(-1, -1), (-1, 0), (-1, 1),
(0, -1), (0, 1),
(1, -1), (1, 0), (1, 1),
];
let offsets: &'static [(i32, i32)] = if connectivity == 4 { &N4 } else { &N8 };
let (r, c, rows, cols) = (r as i32, c as i32, rows as i32, cols as i32);
offsets.iter().filter_map(move |&(dr, dc)| {
let nr = r + dr;
let nc = c + dc;
if nr >= 0 && nr < rows && nc >= 0 && nc < cols {
Some((nr as usize, nc as usize))
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array2, Array3};
#[test]
fn test_euler_solid_square() {
let mut img = Array2::<bool>::default((7, 7));
for r in 1..6 {
for c in 1..6 {
img[[r, c]] = true;
}
}
let e = euler_number(&img.view());
assert_eq!(e, 1, "solid square Euler number should be 1, got {e}");
}
#[test]
fn test_euler_ring() {
let mut ring = Array2::<bool>::default((9, 9));
for r in 0..9u32 {
for c in 0..9u32 {
let dr = r as i32 - 4;
let dc = c as i32 - 4;
let d = dr * dr + dc * dc;
ring[[r as usize, c as usize]] = d >= 4 && d <= 16;
}
}
let e = euler_number(&ring.view());
assert_eq!(e, 0, "ring Euler number should be 0, got {e}");
}
#[test]
fn test_euler_empty() {
let img = Array2::<bool>::default((5, 5));
assert_eq!(euler_number(&img.view()), 0);
}
#[test]
fn test_euler_single_pixel() {
let mut img = Array2::<bool>::default((3, 3));
img[[1, 1]] = true;
assert_eq!(euler_number(&img.view()), 1);
}
#[test]
fn test_cc_two_blobs_4() {
let mut img = Array2::<bool>::default((5, 9));
img[[0, 0]] = true;
img[[0, 1]] = true;
img[[1, 0]] = true;
img[[1, 1]] = true;
img[[0, 7]] = true;
img[[0, 8]] = true;
img[[1, 7]] = true;
img[[1, 8]] = true;
let n = connected_components_count(&img.view(), 4).expect("connected_components_count should succeed with connectivity=4");
assert_eq!(n, 2);
}
#[test]
fn test_cc_diagonal_4_vs_8() {
let mut img = Array2::<bool>::default((3, 3));
img[[0, 0]] = true;
img[[1, 1]] = true;
let n4 = connected_components_count(&img.view(), 4).expect("connected_components_count should succeed with connectivity=4");
let n8 = connected_components_count(&img.view(), 8).expect("connected_components_count should succeed with connectivity=8");
assert_eq!(n4, 2, "4-connectivity: diagonal pixels are separate");
assert_eq!(n8, 1, "8-connectivity: diagonal pixels are connected");
}
#[test]
fn test_cc_invalid_connectivity() {
let img = Array2::<bool>::default((3, 3));
assert!(connected_components_count(&img.view(), 6).is_err());
}
#[test]
fn test_cc_empty() {
let img = Array2::<bool>::default((4, 4));
let n = connected_components_count(&img.view(), 4).expect("connected_components_count should succeed on empty image");
assert_eq!(n, 0);
}
#[test]
fn test_hole_filling_frame() {
let mut img = Array2::<bool>::default((5, 5));
for r in 0..5 {
for c in 0..5 {
img[[r, c]] = r == 0 || r == 4 || c == 0 || c == 4;
}
}
let filled = hole_filling(&img.view()).expect("hole_filling should succeed on valid framed image");
assert!(filled[[2, 2]], "centre pixel should be filled");
assert!(filled[[0, 0]]);
}
#[test]
fn test_hole_filling_no_holes() {
let mut img = Array2::<bool>::default((5, 5));
for r in 0..5 {
for c in 0..5 {
img[[r, c]] = true;
}
}
let filled = hole_filling(&img.view()).expect("hole_filling should succeed on fully filled image");
for r in 0..5 {
for c in 0..5 {
assert!(filled[[r, c]]);
}
}
}
#[test]
fn test_hole_filling_all_background() {
let img = Array2::<bool>::default((4, 4));
let filled = hole_filling(&img.view()).expect("hole_filling should succeed on all-background image");
for r in 0..4 {
for c in 0..4 {
assert!(!filled[[r, c]]);
}
}
}
#[test]
fn test_genus_solid_ball() {
let mut ball = Array3::<bool>::default((7, 7, 7));
for z in 0..7usize {
for y in 0..7usize {
for x in 0..7usize {
let dz = z as i32 - 3;
let dy = y as i32 - 3;
let dx = x as i32 - 3;
ball[[z, y, x]] = dz * dz + dy * dy + dx * dx <= 9;
}
}
}
let g = genus(&ball.view());
assert_eq!(g, 0, "solid ball genus = 0, got {g}");
}
#[test]
fn test_genus_empty_volume() {
let vol = Array3::<bool>::default((4, 4, 4));
let g = genus(&vol.view());
assert_eq!(g, 0);
}
}