use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::{s, Array3};
use scirs2_core::ndarray::ArrayView3;
#[derive(Debug, Clone)]
pub enum StructuringElement3D {
Cube(usize),
Sphere(f64),
Cross,
Cross26,
Custom(Array3<bool>),
}
impl StructuringElement3D {
pub fn to_array(&self) -> Array3<bool> {
match self {
StructuringElement3D::Cube(radius) => {
let side = 2 * radius + 1;
Array3::from_elem((side, side, side), true)
}
StructuringElement3D::Sphere(radius) => {
let r = radius.ceil() as usize;
let side = 2 * r + 1;
let cr = r as f64;
Array3::from_shape_fn((side, side, side), |(z, y, x)| {
let dz = z as f64 - cr;
let dy = y as f64 - cr;
let dx = x as f64 - cr;
dz * dz + dy * dy + dx * dx <= radius * radius + 1e-9
})
}
StructuringElement3D::Cross => {
let mut arr = Array3::from_elem((3, 3, 3), false);
arr[[1, 1, 1]] = true;
arr[[0, 1, 1]] = true;
arr[[2, 1, 1]] = true;
arr[[1, 0, 1]] = true;
arr[[1, 2, 1]] = true;
arr[[1, 1, 0]] = true;
arr[[1, 1, 2]] = true;
arr
}
StructuringElement3D::Cross26 => Array3::from_elem((3, 3, 3), true),
StructuringElement3D::Custom(arr) => arr.clone(),
}
}
pub fn center(&self) -> (usize, usize, usize) {
let arr = self.to_array();
let shape = arr.shape();
(shape[0] / 2, shape[1] / 2, shape[2] / 2)
}
}
fn erode_pass(
src: &Array3<bool>,
se: &Array3<bool>,
) -> Array3<bool> {
let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
let mut out = Array3::from_elem((sz, sy, sx), false);
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
let mut fits = true;
'se_loop: for dz in 0..ez {
for dy in 0..ey {
for dx in 0..ex {
if !se[[dz, dy, dx]] {
continue;
}
let nz = iz as isize + dz as isize - cz as isize;
let ny = iy as isize + dy as isize - cy as isize;
let nx = ix as isize + dx as isize - cx as isize;
if nz < 0
|| ny < 0
|| nx < 0
|| nz >= sz as isize
|| ny >= sy as isize
|| nx >= sx as isize
{
fits = false;
break 'se_loop;
}
if !src[[nz as usize, ny as usize, nx as usize]] {
fits = false;
break 'se_loop;
}
}
}
}
out[[iz, iy, ix]] = fits;
}
}
}
out
}
fn dilate_pass(
src: &Array3<bool>,
se: &Array3<bool>,
) -> Array3<bool> {
let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
let mut out = Array3::from_elem((sz, sy, sx), false);
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
if !src[[iz, iy, ix]] {
continue;
}
for dz in 0..ez {
for dy in 0..ey {
for dx in 0..ex {
if !se[[dz, dy, dx]] {
continue;
}
let nz = iz as isize + dz as isize - cz as isize;
let ny = iy as isize + dy as isize - cy as isize;
let nx = ix as isize + dx as isize - cx as isize;
if nz >= 0
&& ny >= 0
&& nx >= 0
&& nz < sz as isize
&& ny < sy as isize
&& nx < sx as isize
{
out[[nz as usize, ny as usize, nx as usize]] = true;
}
}
}
}
}
}
}
out
}
pub fn binary_erosion3d(
image: ArrayView3<bool>,
structuring_element: &StructuringElement3D,
iterations: usize,
) -> NdimageResult<Array3<bool>> {
if iterations == 0 {
return Err(NdimageError::InvalidInput(
"iterations must be at least 1".to_string(),
));
}
let se = structuring_element.to_array();
let mut current = image.to_owned();
for _ in 0..iterations {
current = erode_pass(¤t, &se);
}
Ok(current)
}
pub fn binary_dilation3d(
image: ArrayView3<bool>,
structuring_element: &StructuringElement3D,
iterations: usize,
) -> NdimageResult<Array3<bool>> {
if iterations == 0 {
return Err(NdimageError::InvalidInput(
"iterations must be at least 1".to_string(),
));
}
let se = structuring_element.to_array();
let mut current = image.to_owned();
for _ in 0..iterations {
current = dilate_pass(¤t, &se);
}
Ok(current)
}
pub fn binary_opening3d(
image: ArrayView3<bool>,
structuring_element: &StructuringElement3D,
) -> NdimageResult<Array3<bool>> {
let se = structuring_element.to_array();
let eroded = erode_pass(&image.to_owned(), &se);
Ok(dilate_pass(&eroded, &se))
}
pub fn binary_closing3d(
image: ArrayView3<bool>,
structuring_element: &StructuringElement3D,
) -> NdimageResult<Array3<bool>> {
let se = structuring_element.to_array();
let dilated = dilate_pass(&image.to_owned(), &se);
Ok(erode_pass(&dilated, &se))
}
fn grey_erode_pass(src: &Array3<f64>, se: &Array3<bool>) -> Array3<f64> {
let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
let mut out = Array3::<f64>::from_elem((sz, sy, sx), f64::INFINITY);
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
let mut min_val = f64::INFINITY;
for dz in 0..ez {
for dy in 0..ey {
for dx in 0..ex {
if !se[[dz, dy, dx]] {
continue;
}
let nz = iz as isize + dz as isize - cz as isize;
let ny = iy as isize + dy as isize - cy as isize;
let nx = ix as isize + dx as isize - cx as isize;
if nz >= 0
&& ny >= 0
&& nx >= 0
&& nz < sz as isize
&& ny < sy as isize
&& nx < sx as isize
{
let v = src[[nz as usize, ny as usize, nx as usize]];
if v < min_val {
min_val = v;
}
}
}
}
}
out[[iz, iy, ix]] = if min_val.is_infinite() {
src[[iz, iy, ix]]
} else {
min_val
};
}
}
}
out
}
fn grey_dilate_pass(src: &Array3<f64>, se: &Array3<bool>) -> Array3<f64> {
let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
let mut out = Array3::<f64>::from_elem((sz, sy, sx), f64::NEG_INFINITY);
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
let mut max_val = f64::NEG_INFINITY;
for dz in 0..ez {
for dy in 0..ey {
for dx in 0..ex {
if !se[[dz, dy, dx]] {
continue;
}
let nz = iz as isize + dz as isize - cz as isize;
let ny = iy as isize + dy as isize - cy as isize;
let nx = ix as isize + dx as isize - cx as isize;
if nz >= 0
&& ny >= 0
&& nx >= 0
&& nz < sz as isize
&& ny < sy as isize
&& nx < sx as isize
{
let v = src[[nz as usize, ny as usize, nx as usize]];
if v > max_val {
max_val = v;
}
}
}
}
}
out[[iz, iy, ix]] = if max_val.is_infinite() {
src[[iz, iy, ix]]
} else {
max_val
};
}
}
}
out
}
pub fn grey_erosion3d(
image: ArrayView3<f64>,
structuring_element: &StructuringElement3D,
) -> NdimageResult<Array3<f64>> {
let se = structuring_element.to_array();
Ok(grey_erode_pass(&image.to_owned(), &se))
}
pub fn grey_dilation3d(
image: ArrayView3<f64>,
structuring_element: &StructuringElement3D,
) -> NdimageResult<Array3<f64>> {
let se = structuring_element.to_array();
Ok(grey_dilate_pass(&image.to_owned(), &se))
}
struct UnionFind {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
UnionFind {
parent: (0..n).collect(),
rank: vec![0; n],
}
}
fn find(&mut self, mut x: usize) -> usize {
while self.parent[x] != x {
self.parent[x] = self.parent[self.parent[x]];
x = self.parent[x];
}
x
}
fn union(&mut self, a: usize, b: usize) {
let ra = self.find(a);
let rb = self.find(b);
if ra == rb {
return;
}
if self.rank[ra] < self.rank[rb] {
self.parent[ra] = rb;
} else if self.rank[ra] > self.rank[rb] {
self.parent[rb] = ra;
} else {
self.parent[rb] = ra;
self.rank[ra] += 1;
}
}
}
fn neighbor_offsets(connectivity: usize) -> Vec<(isize, isize, isize)> {
match connectivity {
6 => vec![
(-1, 0, 0),
(0, -1, 0),
(0, 0, -1),
],
18 => {
let mut offsets = Vec::new();
for dz in -1isize..=1 {
for dy in -1isize..=1 {
for dx in -1isize..=1 {
let nonzero = (dz != 0) as usize
+ (dy != 0) as usize
+ (dx != 0) as usize;
if nonzero > 0 && nonzero <= 2 {
if dz < 0 || (dz == 0 && dy < 0) || (dz == 0 && dy == 0 && dx < 0) {
offsets.push((dz, dy, dx));
}
}
}
}
}
offsets
}
_ => {
let mut offsets = Vec::new();
for dz in -1isize..=1 {
for dy in -1isize..=1 {
for dx in -1isize..=1 {
if dz == 0 && dy == 0 && dx == 0 {
continue;
}
if dz < 0 || (dz == 0 && dy < 0) || (dz == 0 && dy == 0 && dx < 0) {
offsets.push((dz, dy, dx));
}
}
}
}
offsets
}
}
}
pub fn label3d(
binary_image: ArrayView3<bool>,
connectivity: usize,
) -> NdimageResult<(Array3<i32>, usize)> {
let shape = binary_image.shape();
let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
let n_voxels = sz * sy * sx;
let mut labels = vec![0usize; n_voxels];
let mut uf = UnionFind::new(n_voxels + 1); let mut next_label = 1usize;
let offsets = neighbor_offsets(connectivity);
let flat = |z: usize, y: usize, x: usize| z * sy * sx + y * sx + x;
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
if !binary_image[[iz, iy, ix]] {
continue;
}
let mut neighbor_labels: Vec<usize> = Vec::new();
for &(dz, dy, dx) in &offsets {
let nz = iz as isize + dz;
let ny = iy as isize + dy;
let nx = ix as isize + dx;
if nz < 0
|| ny < 0
|| nx < 0
|| nz >= sz as isize
|| ny >= sy as isize
|| nx >= sx as isize
{
continue;
}
let nb = flat(nz as usize, ny as usize, nx as usize);
if labels[nb] > 0 {
neighbor_labels.push(labels[nb]);
}
}
let idx = flat(iz, iy, ix);
if neighbor_labels.is_empty() {
labels[idx] = next_label;
next_label += 1;
} else {
let min_lbl = neighbor_labels
.iter()
.copied()
.min()
.unwrap_or(next_label);
labels[idx] = min_lbl;
for &nl in &neighbor_labels {
uf.union(min_lbl, nl);
}
}
}
}
}
let mut root_to_final = vec![0usize; next_label];
let mut n_components = 0usize;
let mut out = Array3::<i32>::zeros((sz, sy, sx));
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
let idx = flat(iz, iy, ix);
if labels[idx] == 0 {
continue;
}
let root = uf.find(labels[idx]);
if root_to_final[root] == 0 {
n_components += 1;
root_to_final[root] = n_components;
}
out[[iz, iy, ix]] = root_to_final[root] as i32;
}
}
}
Ok((out, n_components))
}
#[derive(Debug, Clone)]
pub struct Object3DProps {
pub label: i32,
pub volume: usize,
pub centroid: (f64, f64, f64),
pub bounding_box: ((usize, usize, usize), (usize, usize, usize)),
pub equivalent_diameter: f64,
pub surface_area_approx: f64,
}
pub fn object_properties3d(label_image: &Array3<i32>, n_objects: usize) -> Vec<Object3DProps> {
let shape = label_image.shape();
let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
let mut volumes = vec![0usize; n_objects + 1];
let mut sum_z = vec![0.0f64; n_objects + 1];
let mut sum_y = vec![0.0f64; n_objects + 1];
let mut sum_x = vec![0.0f64; n_objects + 1];
let mut min_z = vec![usize::MAX; n_objects + 1];
let mut min_y = vec![usize::MAX; n_objects + 1];
let mut min_x = vec![usize::MAX; n_objects + 1];
let mut max_z = vec![0usize; n_objects + 1];
let mut max_y = vec![0usize; n_objects + 1];
let mut max_x = vec![0usize; n_objects + 1];
let mut surface = vec![0usize; n_objects + 1];
let face_neighbors: [(isize, isize, isize); 6] = [
(-1, 0, 0),
(1, 0, 0),
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
];
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
let lbl = label_image[[iz, iy, ix]] as usize;
if lbl == 0 || lbl > n_objects {
continue;
}
volumes[lbl] += 1;
sum_z[lbl] += iz as f64;
sum_y[lbl] += iy as f64;
sum_x[lbl] += ix as f64;
if iz < min_z[lbl] {
min_z[lbl] = iz;
}
if iy < min_y[lbl] {
min_y[lbl] = iy;
}
if ix < min_x[lbl] {
min_x[lbl] = ix;
}
if iz > max_z[lbl] {
max_z[lbl] = iz;
}
if iy > max_y[lbl] {
max_y[lbl] = iy;
}
if ix > max_x[lbl] {
max_x[lbl] = ix;
}
let mut on_surface = false;
for &(dz, dy, dx) in &face_neighbors {
let nz = iz as isize + dz;
let ny = iy as isize + dy;
let nx = ix as isize + dx;
if nz < 0
|| ny < 0
|| nx < 0
|| nz >= sz as isize
|| ny >= sy as isize
|| nx >= sx as isize
{
on_surface = true;
break;
}
if label_image[[nz as usize, ny as usize, nx as usize]] != lbl as i32 {
on_surface = true;
break;
}
}
if on_surface {
surface[lbl] += 1;
}
}
}
}
let pi = std::f64::consts::PI;
(1..=n_objects)
.map(|lbl| {
let vol = volumes[lbl];
let centroid = if vol > 0 {
(
sum_z[lbl] / vol as f64,
sum_y[lbl] / vol as f64,
sum_x[lbl] / vol as f64,
)
} else {
(0.0, 0.0, 0.0)
};
let equiv_diam = if vol > 0 {
2.0 * (3.0 * vol as f64 / (4.0 * pi)).powf(1.0 / 3.0)
} else {
0.0
};
let bb_min = (
if min_z[lbl] == usize::MAX { 0 } else { min_z[lbl] },
if min_y[lbl] == usize::MAX { 0 } else { min_y[lbl] },
if min_x[lbl] == usize::MAX { 0 } else { min_x[lbl] },
);
Object3DProps {
label: lbl as i32,
volume: vol,
centroid,
bounding_box: (bb_min, (max_z[lbl], max_y[lbl], max_x[lbl])),
equivalent_diameter: equiv_diam,
surface_area_approx: surface[lbl] as f64,
}
})
.collect()
}
pub fn distance_transform_edt3d(
binary_image: ArrayView3<bool>,
) -> NdimageResult<Array3<f64>> {
let shape = binary_image.shape();
let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
if sz == 0 || sy == 0 || sx == 0 {
return Ok(Array3::zeros((sz, sy, sx)));
}
let inf_sq = (sz * sz + sy * sy + sx * sx + 1) as f64;
let mut g = Array3::<f64>::from_elem((sz, sy, sx), inf_sq);
for iz in 0..sz {
for iy in 0..sy {
let mut prev = if !binary_image[[iz, iy, 0]] {
0.0
} else {
inf_sq
};
g[[iz, iy, 0]] = prev;
for ix in 1..sx {
if !binary_image[[iz, iy, ix]] {
prev = 0.0;
} else if prev < inf_sq {
prev += 1.0;
}
g[[iz, iy, ix]] = prev * prev;
}
let mut prev_back = if !binary_image[[iz, iy, sx - 1]] {
0.0
} else {
inf_sq
};
for ix in (0..sx).rev() {
if !binary_image[[iz, iy, ix]] {
prev_back = 0.0;
} else if prev_back < inf_sq {
prev_back += 1.0;
}
let bval = prev_back * prev_back;
if bval < g[[iz, iy, ix]] {
g[[iz, iy, ix]] = bval;
}
}
}
}
let mut h = Array3::<f64>::from_elem((sz, sy, sx), inf_sq);
for iz in 0..sz {
for ix in 0..sx {
let mut s = vec![0.0f64; sy]; let mut t = vec![0usize; sy]; let f = |q: usize| g[[iz, q, ix]] + (q as f64).powi(2);
let mut k = 0usize;
t[0] = 0;
s[0] = f64::NEG_INFINITY;
for q in 1..sy {
loop {
let sq = ((f(q) - f(t[k])) / (2.0 * q as f64 - 2.0 * t[k] as f64) + 0.5)
.floor();
if k == 0 || sq > s[k] {
k += 1;
s[k] = sq;
t[k] = q;
break;
}
if k > 0 {
k -= 1;
} else {
break;
}
}
}
let mut j = k;
for q in (0..sy).rev() {
while j > 0 && s[j] > q as f64 {
j -= 1;
}
h[[iz, q, ix]] = f(t[j]) + (q as f64 - t[j] as f64).powi(2)
- (t[j] as f64).powi(2);
}
}
}
let mut out = Array3::<f64>::zeros((sz, sy, sx));
for iy in 0..sy {
for ix in 0..sx {
let f = |q: usize| h[[q, iy, ix]] + (q as f64).powi(2);
let mut s = vec![0.0f64; sz];
let mut t = vec![0usize; sz];
let mut k = 0usize;
t[0] = 0;
s[0] = f64::NEG_INFINITY;
for q in 1..sz {
loop {
let sq = ((f(q) - f(t[k])) / (2.0 * q as f64 - 2.0 * t[k] as f64) + 0.5)
.floor();
if k == 0 || sq > s[k] {
k += 1;
s[k] = sq;
t[k] = q;
break;
}
if k > 0 {
k -= 1;
} else {
break;
}
}
}
let mut j = k;
for q in (0..sz).rev() {
while j > 0 && s[j] > q as f64 {
j -= 1;
}
let dist_sq = f(t[j]) + (q as f64 - t[j] as f64).powi(2)
- (t[j] as f64).powi(2);
out[[q, iy, ix]] = dist_sq.max(0.0).sqrt();
}
}
}
Ok(out)
}
pub fn skeletonize3d(binary_image: ArrayView3<bool>) -> NdimageResult<Array3<bool>> {
let shape = binary_image.shape();
let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
if sz == 0 || sy == 0 || sx == 0 {
return Err(NdimageError::InvalidInput(
"image must be non-empty".to_string(),
));
}
let mut current = binary_image.to_owned();
let directions: [(isize, isize, isize); 6] = [
(-1, 0, 0),
(1, 0, 0),
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
];
let mut changed = true;
while changed {
changed = false;
for &(dz, dy, dx) in &directions {
let mut candidates = Vec::new();
for iz in 0..sz {
for iy in 0..sy {
for ix in 0..sx {
if !current[[iz, iy, ix]] {
continue;
}
let nz = iz as isize + dz;
let ny = iy as isize + dy;
let nx = ix as isize + dx;
let is_border = if nz < 0
|| ny < 0
|| nx < 0
|| nz >= sz as isize
|| ny >= sy as isize
|| nx >= sx as isize
{
true
} else {
!current[[nz as usize, ny as usize, nx as usize]]
};
if is_border && is_simple_point(¤t, iz, iy, ix) {
candidates.push((iz, iy, ix));
}
}
}
}
for (iz, iy, ix) in candidates {
current[[iz, iy, ix]] = false;
changed = true;
}
}
}
Ok(current)
}
fn is_simple_point(vol: &Array3<bool>, iz: usize, iy: usize, ix: usize) -> bool {
let shape = vol.shape();
let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
let mut nbhood = [[[false; 3]; 3]; 3];
for dz in 0..3usize {
for dy in 0..3usize {
for dx in 0..3usize {
if dz == 1 && dy == 1 && dx == 1 {
continue;
}
let nz = iz as isize + dz as isize - 1;
let ny = iy as isize + dy as isize - 1;
let nx = ix as isize + dx as isize - 1;
if nz >= 0
&& ny >= 0
&& nx >= 0
&& nz < sz as isize
&& ny < sy as isize
&& nx < sx as isize
{
nbhood[dz][dy][dx] = vol[[nz as usize, ny as usize, nx as usize]];
}
}
}
}
let fg_components = count_components_26(&nbhood, true);
if fg_components != 1 {
return false;
}
let mut bg_nbhood = [[[true; 3]; 3]; 3];
for dz in 0..3usize {
for dy in 0..3usize {
for dx in 0..3usize {
if dz == 1 && dy == 1 && dx == 1 {
bg_nbhood[dz][dy][dx] = true;
} else {
bg_nbhood[dz][dy][dx] = !nbhood[dz][dy][dx];
}
}
}
}
let bg_components = count_components_6(&bg_nbhood, true);
bg_components == 1
}
fn count_components_26(nbhood: &[[[bool; 3]; 3]; 3], target: bool) -> usize {
let mut visited = [[[false; 3]; 3]; 3];
let mut count = 0;
for sz in 0..3usize {
for sy in 0..3usize {
for sx in 0..3usize {
if nbhood[sz][sy][sx] == target && !visited[sz][sy][sx] {
let mut queue = std::collections::VecDeque::new();
queue.push_back((sz, sy, sx));
visited[sz][sy][sx] = true;
while let Some((cz, cy, cx)) = queue.pop_front() {
for dz in 0..3usize {
for dy in 0..3usize {
for dx in 0..3usize {
if dz == 1 && dy == 1 && dx == 1 {
continue;
}
let nz = cz as isize + dz as isize - 1;
let ny = cy as isize + dy as isize - 1;
let nx = cx as isize + dx as isize - 1;
if nz >= 0
&& ny >= 0
&& nx >= 0
&& nz < 3
&& ny < 3
&& nx < 3
{
let (nzu, nyu, nxu) =
(nz as usize, ny as usize, nx as usize);
if nbhood[nzu][nyu][nxu] == target
&& !visited[nzu][nyu][nxu]
{
visited[nzu][nyu][nxu] = true;
queue.push_back((nzu, nyu, nxu));
}
}
}
}
}
}
count += 1;
}
}
}
}
count
}
fn count_components_6(nbhood: &[[[bool; 3]; 3]; 3], target: bool) -> usize {
let offsets: [(isize, isize, isize); 6] = [
(-1, 0, 0),
(1, 0, 0),
(0, -1, 0),
(0, 1, 0),
(0, 0, -1),
(0, 0, 1),
];
let mut visited = [[[false; 3]; 3]; 3];
let mut count = 0;
for sz in 0..3usize {
for sy in 0..3usize {
for sx in 0..3usize {
if nbhood[sz][sy][sx] == target && !visited[sz][sy][sx] {
let mut queue = std::collections::VecDeque::new();
queue.push_back((sz, sy, sx));
visited[sz][sy][sx] = true;
while let Some((cz, cy, cx)) = queue.pop_front() {
for &(dz, dy, dx) in &offsets {
let nz = cz as isize + dz;
let ny = cy as isize + dy;
let nx = cx as isize + dx;
if nz >= 0 && ny >= 0 && nx >= 0 && nz < 3 && ny < 3 && nx < 3 {
let (nzu, nyu, nxu) = (nz as usize, ny as usize, nx as usize);
if nbhood[nzu][nyu][nxu] == target && !visited[nzu][nyu][nxu] {
visited[nzu][nyu][nxu] = true;
queue.push_back((nzu, nyu, nxu));
}
}
}
}
count += 1;
}
}
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array3;
fn sphere_5() -> Array3<bool> {
Array3::from_shape_fn((5, 5, 5), |(z, y, x)| {
let dz = z as f64 - 2.0;
let dy = y as f64 - 2.0;
let dx = x as f64 - 2.0;
dz * dz + dy * dy + dx * dx <= 4.0 + 1e-9
})
}
fn solid_cube() -> Array3<bool> {
Array3::from_elem((4, 4, 4), true)
}
fn two_blobs() -> Array3<bool> {
let mut v = Array3::from_elem((5, 5, 5), false);
for z in 0..2usize {
for y in 0..2usize {
for x in 0..2usize {
v[[z, y, x]] = true;
v[[z + 3, y + 3, x + 3]] = true;
}
}
}
v
}
#[test]
fn test_se_cube_shape() {
let se = StructuringElement3D::Cube(1);
let arr = se.to_array();
assert_eq!(arr.shape(), &[3, 3, 3]);
assert!(arr.iter().all(|&v| v));
}
#[test]
fn test_se_sphere_center() {
let se = StructuringElement3D::Sphere(2.0);
let arr = se.to_array();
let (cz, cy, cx) = se.center();
assert!(arr[[cz, cy, cx]]);
}
#[test]
fn test_se_cross_6_connectivity() {
let se = StructuringElement3D::Cross;
let arr = se.to_array();
assert!(arr[[1, 1, 1]]); assert!(arr[[0, 1, 1]]); assert!(arr[[2, 1, 1]]); assert!(!arr[[0, 0, 0]]); let count = arr.iter().filter(|&&v| v).count();
assert_eq!(count, 7);
}
#[test]
fn test_se_cross26_full() {
let se = StructuringElement3D::Cross26;
let arr = se.to_array();
assert_eq!(arr.iter().filter(|&&v| v).count(), 27);
}
#[test]
fn test_binary_erosion_shrinks() {
let img = sphere_5();
let se = StructuringElement3D::Cross;
let eroded = binary_erosion3d(img.view(), &se, 1).expect("erosion failed");
let before: usize = img.iter().filter(|&&v| v).count();
let after: usize = eroded.iter().filter(|&&v| v).count();
assert!(after <= before, "erosion should not grow the object");
}
#[test]
fn test_binary_dilation_grows() {
let img = sphere_5();
let se = StructuringElement3D::Cross;
let dilated = binary_dilation3d(img.view(), &se, 1).expect("dilation failed");
let before: usize = img.iter().filter(|&&v| v).count();
let after: usize = dilated.iter().filter(|&&v| v).count();
assert!(after >= before, "dilation should not shrink the object");
}
#[test]
fn test_binary_erosion_zero_iterations_error() {
let img = sphere_5();
let se = StructuringElement3D::Cross;
let result = binary_erosion3d(img.view(), &se, 0);
assert!(result.is_err());
}
#[test]
fn test_binary_opening_smaller_than_original() {
let img = sphere_5();
let se = StructuringElement3D::Cube(1);
let opened = binary_opening3d(img.view(), &se).expect("opening failed");
let before: usize = img.iter().filter(|&&v| v).count();
let after: usize = opened.iter().filter(|&&v| v).count();
assert!(after <= before);
}
#[test]
fn test_binary_closing_larger_than_original() {
let mut img = Array3::from_elem((7, 7, 7), false);
img[[3, 3, 3]] = true;
img[[3, 3, 5]] = true;
let se = StructuringElement3D::Cube(1);
let closed = binary_closing3d(img.view(), &se).expect("closing failed");
let before: usize = img.iter().filter(|&&v| v).count();
let after: usize = closed.iter().filter(|&&v| v).count();
assert!(after >= before);
}
#[test]
fn test_binary_erosion_all_background_stays_background() {
let img = Array3::from_elem((5, 5, 5), false);
let se = StructuringElement3D::Cross26;
let eroded = binary_erosion3d(img.view(), &se, 1).expect("erosion failed");
assert!(eroded.iter().all(|&v| !v));
}
#[test]
fn test_grey_erosion_reduces_values() {
let img = Array3::from_shape_fn((5, 5, 5), |(z, y, x)| (z + y + x) as f64);
let se = StructuringElement3D::Cross;
let eroded = grey_erosion3d(img.view(), &se).expect("grey erosion failed");
for ((z, y, x), &v) in eroded.indexed_iter() {
assert!(v <= img[[z, y, x]] + 1e-10);
}
}
#[test]
fn test_grey_dilation_increases_values() {
let img = Array3::from_shape_fn((5, 5, 5), |(z, y, x)| (z + y + x) as f64);
let se = StructuringElement3D::Cross;
let dilated = grey_dilation3d(img.view(), &se).expect("grey dilation failed");
for ((z, y, x), &v) in dilated.indexed_iter() {
assert!(v >= img[[z, y, x]] - 1e-10);
}
}
#[test]
fn test_label3d_single_blob() {
let img = sphere_5();
let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
assert_eq!(n, 1);
for ((z, y, x), &lbl) in labels.indexed_iter() {
if img[[z, y, x]] {
assert_eq!(lbl, 1);
} else {
assert_eq!(lbl, 0);
}
}
}
#[test]
fn test_label3d_two_blobs() {
let img = two_blobs();
let (_, n) = label3d(img.view(), 26).expect("labeling failed");
assert_eq!(n, 2, "expected exactly 2 components");
}
#[test]
fn test_label3d_background_only() {
let img = Array3::from_elem((4, 4, 4), false);
let (_, n) = label3d(img.view(), 6).expect("labeling failed");
assert_eq!(n, 0);
}
#[test]
fn test_label3d_full_volume() {
let img = Array3::from_elem((3, 3, 3), true);
let (_, n) = label3d(img.view(), 6).expect("labeling failed");
assert_eq!(n, 1);
}
#[test]
fn test_object_properties_volume() {
let img = solid_cube();
let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
let props = object_properties3d(&labels, n);
assert_eq!(props.len(), 1);
assert_eq!(props[0].volume, 64); }
#[test]
fn test_object_properties_centroid() {
let img = solid_cube();
let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
let props = object_properties3d(&labels, n);
let c = props[0].centroid;
assert!((c.0 - 1.5).abs() < 1e-6);
assert!((c.1 - 1.5).abs() < 1e-6);
assert!((c.2 - 1.5).abs() < 1e-6);
}
#[test]
fn test_object_properties_equiv_diameter() {
let img = solid_cube();
let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
let props = object_properties3d(&labels, n);
assert!(props[0].equivalent_diameter > 0.0);
}
#[test]
fn test_edt3d_background_zero() {
let img = Array3::from_elem((5, 5, 5), false);
let dist = distance_transform_edt3d(img.view()).expect("EDT failed");
assert!(dist.iter().all(|&v| v == 0.0));
}
#[test]
fn test_edt3d_center_voxel() {
let mut img = Array3::from_elem((5, 5, 5), true);
for z in 0..5usize {
for y in 0..5usize {
for x in 0..5usize {
if z == 0 || z == 4 || y == 0 || y == 4 || x == 0 || x == 4 {
img[[z, y, x]] = false;
}
}
}
}
let dist = distance_transform_edt3d(img.view()).expect("EDT failed");
assert!(
(dist[[2, 2, 2]] - 2.0).abs() < 0.5,
"Expected ~2.0, got {}",
dist[[2, 2, 2]]
);
}
#[test]
fn test_edt3d_single_fg_voxel() {
let mut img = Array3::from_elem((3, 3, 3), false);
img[[1, 1, 1]] = true;
let dist = distance_transform_edt3d(img.view()).expect("EDT failed");
assert_eq!(dist[[0, 0, 0]], 0.0);
assert!((dist[[1, 1, 1]] - 1.0).abs() < 0.5);
}
#[test]
fn test_skeletonize3d_preserves_nonempty() {
let img = sphere_5();
let skel = skeletonize3d(img.view()).expect("skeletonize failed");
for ((z, y, x), &v) in skel.indexed_iter() {
if v {
assert!(img[[z, y, x]], "skeleton contains voxel not in original");
}
}
}
#[test]
fn test_skeletonize3d_empty_error() {
let img: Array3<bool> = Array3::from_elem((0, 0, 0), false);
let result = skeletonize3d(img.view());
assert!(result.is_err());
}
#[test]
fn test_skeletonize3d_single_voxel() {
let mut img = Array3::from_elem((3, 3, 3), false);
img[[1, 1, 1]] = true;
let skel = skeletonize3d(img.view()).expect("skeletonize failed");
assert!(skel[[1, 1, 1]]);
}
}