1use crate::error::{NdimageError, NdimageResult};
15use scirs2_core::ndarray::ArrayView3;
16use scirs2_core::ndarray::{s, Array3};
17
18#[derive(Debug, Clone)]
24pub enum StructuringElement3D {
25 Cube(usize),
27 Sphere(f64),
29 Cross,
31 Cross26,
33 Custom(Array3<bool>),
35}
36
37impl StructuringElement3D {
38 pub fn to_array(&self) -> Array3<bool> {
40 match self {
41 StructuringElement3D::Cube(radius) => {
42 let side = 2 * radius + 1;
43 Array3::from_elem((side, side, side), true)
44 }
45 StructuringElement3D::Sphere(radius) => {
46 let r = radius.ceil() as usize;
47 let side = 2 * r + 1;
48 let cr = r as f64;
49 Array3::from_shape_fn((side, side, side), |(z, y, x)| {
50 let dz = z as f64 - cr;
51 let dy = y as f64 - cr;
52 let dx = x as f64 - cr;
53 dz * dz + dy * dy + dx * dx <= radius * radius + 1e-9
54 })
55 }
56 StructuringElement3D::Cross => {
57 let mut arr = Array3::from_elem((3, 3, 3), false);
58 arr[[1, 1, 1]] = true;
60 arr[[0, 1, 1]] = true;
62 arr[[2, 1, 1]] = true;
63 arr[[1, 0, 1]] = true;
65 arr[[1, 2, 1]] = true;
66 arr[[1, 1, 0]] = true;
68 arr[[1, 1, 2]] = true;
69 arr
70 }
71 StructuringElement3D::Cross26 => Array3::from_elem((3, 3, 3), true),
72 StructuringElement3D::Custom(arr) => arr.clone(),
73 }
74 }
75
76 pub fn center(&self) -> (usize, usize, usize) {
78 let arr = self.to_array();
79 let shape = arr.shape();
80 (shape[0] / 2, shape[1] / 2, shape[2] / 2)
81 }
82}
83
84fn erode_pass(src: &Array3<bool>, se: &Array3<bool>) -> Array3<bool> {
90 let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
91 let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
92 let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
93
94 let mut out = Array3::from_elem((sz, sy, sx), false);
95 for iz in 0..sz {
96 for iy in 0..sy {
97 for ix in 0..sx {
98 let mut fits = true;
99 'se_loop: for dz in 0..ez {
100 for dy in 0..ey {
101 for dx in 0..ex {
102 if !se[[dz, dy, dx]] {
103 continue;
104 }
105 let nz = iz as isize + dz as isize - cz as isize;
106 let ny = iy as isize + dy as isize - cy as isize;
107 let nx = ix as isize + dx as isize - cx as isize;
108 if nz < 0
109 || ny < 0
110 || nx < 0
111 || nz >= sz as isize
112 || ny >= sy as isize
113 || nx >= sx as isize
114 {
115 fits = false;
116 break 'se_loop;
117 }
118 if !src[[nz as usize, ny as usize, nx as usize]] {
119 fits = false;
120 break 'se_loop;
121 }
122 }
123 }
124 }
125 out[[iz, iy, ix]] = fits;
126 }
127 }
128 }
129 out
130}
131
132fn dilate_pass(src: &Array3<bool>, se: &Array3<bool>) -> Array3<bool> {
134 let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
135 let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
136 let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
137
138 let mut out = Array3::from_elem((sz, sy, sx), false);
139 for iz in 0..sz {
140 for iy in 0..sy {
141 for ix in 0..sx {
142 if !src[[iz, iy, ix]] {
143 continue;
144 }
145 for dz in 0..ez {
146 for dy in 0..ey {
147 for dx in 0..ex {
148 if !se[[dz, dy, dx]] {
149 continue;
150 }
151 let nz = iz as isize + dz as isize - cz as isize;
152 let ny = iy as isize + dy as isize - cy as isize;
153 let nx = ix as isize + dx as isize - cx as isize;
154 if nz >= 0
155 && ny >= 0
156 && nx >= 0
157 && nz < sz as isize
158 && ny < sy as isize
159 && nx < sx as isize
160 {
161 out[[nz as usize, ny as usize, nx as usize]] = true;
162 }
163 }
164 }
165 }
166 }
167 }
168 }
169 out
170}
171
172pub fn binary_erosion3d(
191 image: ArrayView3<bool>,
192 structuring_element: &StructuringElement3D,
193 iterations: usize,
194) -> NdimageResult<Array3<bool>> {
195 if iterations == 0 {
196 return Err(NdimageError::InvalidInput(
197 "iterations must be at least 1".to_string(),
198 ));
199 }
200 let se = structuring_element.to_array();
201 let mut current = image.to_owned();
202 for _ in 0..iterations {
203 current = erode_pass(¤t, &se);
204 }
205 Ok(current)
206}
207
208pub fn binary_dilation3d(
223 image: ArrayView3<bool>,
224 structuring_element: &StructuringElement3D,
225 iterations: usize,
226) -> NdimageResult<Array3<bool>> {
227 if iterations == 0 {
228 return Err(NdimageError::InvalidInput(
229 "iterations must be at least 1".to_string(),
230 ));
231 }
232 let se = structuring_element.to_array();
233 let mut current = image.to_owned();
234 for _ in 0..iterations {
235 current = dilate_pass(¤t, &se);
236 }
237 Ok(current)
238}
239
240pub fn binary_opening3d(
245 image: ArrayView3<bool>,
246 structuring_element: &StructuringElement3D,
247) -> NdimageResult<Array3<bool>> {
248 let se = structuring_element.to_array();
249 let eroded = erode_pass(&image.to_owned(), &se);
250 Ok(dilate_pass(&eroded, &se))
251}
252
253pub fn binary_closing3d(
257 image: ArrayView3<bool>,
258 structuring_element: &StructuringElement3D,
259) -> NdimageResult<Array3<bool>> {
260 let se = structuring_element.to_array();
261 let dilated = dilate_pass(&image.to_owned(), &se);
262 Ok(erode_pass(&dilated, &se))
263}
264
265fn grey_erode_pass(src: &Array3<f64>, se: &Array3<bool>) -> Array3<f64> {
271 let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
272 let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
273 let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
274
275 let mut out = Array3::<f64>::from_elem((sz, sy, sx), f64::INFINITY);
276 for iz in 0..sz {
277 for iy in 0..sy {
278 for ix in 0..sx {
279 let mut min_val = f64::INFINITY;
280 for dz in 0..ez {
281 for dy in 0..ey {
282 for dx in 0..ex {
283 if !se[[dz, dy, dx]] {
284 continue;
285 }
286 let nz = iz as isize + dz as isize - cz as isize;
287 let ny = iy as isize + dy as isize - cy as isize;
288 let nx = ix as isize + dx as isize - cx as isize;
289 if nz >= 0
290 && ny >= 0
291 && nx >= 0
292 && nz < sz as isize
293 && ny < sy as isize
294 && nx < sx as isize
295 {
296 let v = src[[nz as usize, ny as usize, nx as usize]];
297 if v < min_val {
298 min_val = v;
299 }
300 }
301 }
302 }
303 }
304 out[[iz, iy, ix]] = if min_val.is_infinite() {
305 src[[iz, iy, ix]]
306 } else {
307 min_val
308 };
309 }
310 }
311 }
312 out
313}
314
315fn grey_dilate_pass(src: &Array3<f64>, se: &Array3<bool>) -> Array3<f64> {
317 let (sz, sy, sx) = (src.shape()[0], src.shape()[1], src.shape()[2]);
318 let (ez, ey, ex) = (se.shape()[0], se.shape()[1], se.shape()[2]);
319 let (cz, cy, cx) = (ez / 2, ey / 2, ex / 2);
320
321 let mut out = Array3::<f64>::from_elem((sz, sy, sx), f64::NEG_INFINITY);
322 for iz in 0..sz {
323 for iy in 0..sy {
324 for ix in 0..sx {
325 let mut max_val = f64::NEG_INFINITY;
326 for dz in 0..ez {
327 for dy in 0..ey {
328 for dx in 0..ex {
329 if !se[[dz, dy, dx]] {
330 continue;
331 }
332 let nz = iz as isize + dz as isize - cz as isize;
333 let ny = iy as isize + dy as isize - cy as isize;
334 let nx = ix as isize + dx as isize - cx as isize;
335 if nz >= 0
336 && ny >= 0
337 && nx >= 0
338 && nz < sz as isize
339 && ny < sy as isize
340 && nx < sx as isize
341 {
342 let v = src[[nz as usize, ny as usize, nx as usize]];
343 if v > max_val {
344 max_val = v;
345 }
346 }
347 }
348 }
349 }
350 out[[iz, iy, ix]] = if max_val.is_infinite() {
351 src[[iz, iy, ix]]
352 } else {
353 max_val
354 };
355 }
356 }
357 }
358 out
359}
360
361pub fn grey_erosion3d(
366 image: ArrayView3<f64>,
367 structuring_element: &StructuringElement3D,
368) -> NdimageResult<Array3<f64>> {
369 let se = structuring_element.to_array();
370 Ok(grey_erode_pass(&image.to_owned(), &se))
371}
372
373pub fn grey_dilation3d(
378 image: ArrayView3<f64>,
379 structuring_element: &StructuringElement3D,
380) -> NdimageResult<Array3<f64>> {
381 let se = structuring_element.to_array();
382 Ok(grey_dilate_pass(&image.to_owned(), &se))
383}
384
385struct UnionFind {
391 parent: Vec<usize>,
392 rank: Vec<usize>,
393}
394
395impl UnionFind {
396 fn new(n: usize) -> Self {
397 UnionFind {
398 parent: (0..n).collect(),
399 rank: vec![0; n],
400 }
401 }
402
403 fn find(&mut self, mut x: usize) -> usize {
404 while self.parent[x] != x {
405 self.parent[x] = self.parent[self.parent[x]];
407 x = self.parent[x];
408 }
409 x
410 }
411
412 fn union(&mut self, a: usize, b: usize) {
413 let ra = self.find(a);
414 let rb = self.find(b);
415 if ra == rb {
416 return;
417 }
418 if self.rank[ra] < self.rank[rb] {
419 self.parent[ra] = rb;
420 } else if self.rank[ra] > self.rank[rb] {
421 self.parent[rb] = ra;
422 } else {
423 self.parent[rb] = ra;
424 self.rank[ra] += 1;
425 }
426 }
427}
428
429fn neighbor_offsets(connectivity: usize) -> Vec<(isize, isize, isize)> {
431 match connectivity {
432 6 => vec![(-1, 0, 0), (0, -1, 0), (0, 0, -1)],
433 18 => {
434 let mut offsets = Vec::new();
435 for dz in -1isize..=1 {
436 for dy in -1isize..=1 {
437 for dx in -1isize..=1 {
438 let nonzero = (dz != 0) as usize + (dy != 0) as usize + (dx != 0) as usize;
439 if nonzero > 0 && nonzero <= 2 {
440 if dz < 0 || (dz == 0 && dy < 0) || (dz == 0 && dy == 0 && dx < 0) {
442 offsets.push((dz, dy, dx));
443 }
444 }
445 }
446 }
447 }
448 offsets
449 }
450 _ => {
451 let mut offsets = Vec::new();
453 for dz in -1isize..=1 {
454 for dy in -1isize..=1 {
455 for dx in -1isize..=1 {
456 if dz == 0 && dy == 0 && dx == 0 {
457 continue;
458 }
459 if dz < 0 || (dz == 0 && dy < 0) || (dz == 0 && dy == 0 && dx < 0) {
460 offsets.push((dz, dy, dx));
461 }
462 }
463 }
464 }
465 offsets
466 }
467 }
468}
469
470pub fn label3d(
485 binary_image: ArrayView3<bool>,
486 connectivity: usize,
487) -> NdimageResult<(Array3<i32>, usize)> {
488 let shape = binary_image.shape();
489 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
490 let n_voxels = sz * sy * sx;
491
492 let mut labels = vec![0usize; n_voxels];
494 let mut uf = UnionFind::new(n_voxels + 1); let mut next_label = 1usize;
496
497 let offsets = neighbor_offsets(connectivity);
498
499 let flat = |z: usize, y: usize, x: usize| z * sy * sx + y * sx + x;
500
501 for iz in 0..sz {
503 for iy in 0..sy {
504 for ix in 0..sx {
505 if !binary_image[[iz, iy, ix]] {
506 continue;
507 }
508 let mut neighbor_labels: Vec<usize> = Vec::new();
509 for &(dz, dy, dx) in &offsets {
510 let nz = iz as isize + dz;
511 let ny = iy as isize + dy;
512 let nx = ix as isize + dx;
513 if nz < 0
514 || ny < 0
515 || nx < 0
516 || nz >= sz as isize
517 || ny >= sy as isize
518 || nx >= sx as isize
519 {
520 continue;
521 }
522 let nb = flat(nz as usize, ny as usize, nx as usize);
523 if labels[nb] > 0 {
524 neighbor_labels.push(labels[nb]);
525 }
526 }
527
528 let idx = flat(iz, iy, ix);
529 if neighbor_labels.is_empty() {
530 labels[idx] = next_label;
531 next_label += 1;
532 } else {
533 let min_lbl = neighbor_labels.iter().copied().min().unwrap_or(next_label);
534 labels[idx] = min_lbl;
535 for &nl in &neighbor_labels {
536 uf.union(min_lbl, nl);
537 }
538 }
539 }
540 }
541 }
542
543 let mut root_to_final = vec![0usize; next_label];
545 let mut n_components = 0usize;
546 let mut out = Array3::<i32>::zeros((sz, sy, sx));
547
548 for iz in 0..sz {
549 for iy in 0..sy {
550 for ix in 0..sx {
551 let idx = flat(iz, iy, ix);
552 if labels[idx] == 0 {
553 continue;
554 }
555 let root = uf.find(labels[idx]);
556 if root_to_final[root] == 0 {
557 n_components += 1;
558 root_to_final[root] = n_components;
559 }
560 out[[iz, iy, ix]] = root_to_final[root] as i32;
561 }
562 }
563 }
564
565 Ok((out, n_components))
566}
567
568#[derive(Debug, Clone)]
574pub struct Object3DProps {
575 pub label: i32,
577 pub volume: usize,
579 pub centroid: (f64, f64, f64),
581 pub bounding_box: ((usize, usize, usize), (usize, usize, usize)),
583 pub equivalent_diameter: f64,
585 pub surface_area_approx: f64,
587}
588
589pub fn object_properties3d(label_image: &Array3<i32>, n_objects: usize) -> Vec<Object3DProps> {
599 let shape = label_image.shape();
600 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
601
602 let mut volumes = vec![0usize; n_objects + 1];
604 let mut sum_z = vec![0.0f64; n_objects + 1];
605 let mut sum_y = vec![0.0f64; n_objects + 1];
606 let mut sum_x = vec![0.0f64; n_objects + 1];
607 let mut min_z = vec![usize::MAX; n_objects + 1];
608 let mut min_y = vec![usize::MAX; n_objects + 1];
609 let mut min_x = vec![usize::MAX; n_objects + 1];
610 let mut max_z = vec![0usize; n_objects + 1];
611 let mut max_y = vec![0usize; n_objects + 1];
612 let mut max_x = vec![0usize; n_objects + 1];
613 let mut surface = vec![0usize; n_objects + 1];
614
615 let face_neighbors: [(isize, isize, isize); 6] = [
617 (-1, 0, 0),
618 (1, 0, 0),
619 (0, -1, 0),
620 (0, 1, 0),
621 (0, 0, -1),
622 (0, 0, 1),
623 ];
624
625 for iz in 0..sz {
626 for iy in 0..sy {
627 for ix in 0..sx {
628 let lbl = label_image[[iz, iy, ix]] as usize;
629 if lbl == 0 || lbl > n_objects {
630 continue;
631 }
632 volumes[lbl] += 1;
633 sum_z[lbl] += iz as f64;
634 sum_y[lbl] += iy as f64;
635 sum_x[lbl] += ix as f64;
636 if iz < min_z[lbl] {
637 min_z[lbl] = iz;
638 }
639 if iy < min_y[lbl] {
640 min_y[lbl] = iy;
641 }
642 if ix < min_x[lbl] {
643 min_x[lbl] = ix;
644 }
645 if iz > max_z[lbl] {
646 max_z[lbl] = iz;
647 }
648 if iy > max_y[lbl] {
649 max_y[lbl] = iy;
650 }
651 if ix > max_x[lbl] {
652 max_x[lbl] = ix;
653 }
654
655 let mut on_surface = false;
657 for &(dz, dy, dx) in &face_neighbors {
658 let nz = iz as isize + dz;
659 let ny = iy as isize + dy;
660 let nx = ix as isize + dx;
661 if nz < 0
662 || ny < 0
663 || nx < 0
664 || nz >= sz as isize
665 || ny >= sy as isize
666 || nx >= sx as isize
667 {
668 on_surface = true;
669 break;
670 }
671 if label_image[[nz as usize, ny as usize, nx as usize]] != lbl as i32 {
672 on_surface = true;
673 break;
674 }
675 }
676 if on_surface {
677 surface[lbl] += 1;
678 }
679 }
680 }
681 }
682
683 let pi = std::f64::consts::PI;
684
685 (1..=n_objects)
686 .map(|lbl| {
687 let vol = volumes[lbl];
688 let centroid = if vol > 0 {
689 (
690 sum_z[lbl] / vol as f64,
691 sum_y[lbl] / vol as f64,
692 sum_x[lbl] / vol as f64,
693 )
694 } else {
695 (0.0, 0.0, 0.0)
696 };
697 let equiv_diam = if vol > 0 {
699 2.0 * (3.0 * vol as f64 / (4.0 * pi)).powf(1.0 / 3.0)
700 } else {
701 0.0
702 };
703 let bb_min = (
704 if min_z[lbl] == usize::MAX {
705 0
706 } else {
707 min_z[lbl]
708 },
709 if min_y[lbl] == usize::MAX {
710 0
711 } else {
712 min_y[lbl]
713 },
714 if min_x[lbl] == usize::MAX {
715 0
716 } else {
717 min_x[lbl]
718 },
719 );
720 Object3DProps {
721 label: lbl as i32,
722 volume: vol,
723 centroid,
724 bounding_box: (bb_min, (max_z[lbl], max_y[lbl], max_x[lbl])),
725 equivalent_diameter: equiv_diam,
726 surface_area_approx: surface[lbl] as f64,
727 }
728 })
729 .collect()
730}
731
732pub fn distance_transform_edt3d(binary_image: ArrayView3<bool>) -> NdimageResult<Array3<f64>> {
748 let shape = binary_image.shape();
749 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
750 if sz == 0 || sy == 0 || sx == 0 {
751 return Ok(Array3::zeros((sz, sy, sx)));
752 }
753
754 let inf_sq = (sz * sz + sy * sy + sx * sx + 1) as f64;
755
756 let mut g = Array3::<f64>::from_elem((sz, sy, sx), inf_sq);
759
760 for iz in 0..sz {
761 for iy in 0..sy {
762 let mut prev = if !binary_image[[iz, iy, 0]] {
764 0.0
765 } else {
766 inf_sq
767 };
768 g[[iz, iy, 0]] = prev;
769 for ix in 1..sx {
770 if !binary_image[[iz, iy, ix]] {
771 prev = 0.0;
772 } else if prev < inf_sq {
773 prev += 1.0;
774 }
775 g[[iz, iy, ix]] = prev * prev;
776 }
777 let mut prev_back = if !binary_image[[iz, iy, sx - 1]] {
779 0.0
780 } else {
781 inf_sq
782 };
783 for ix in (0..sx).rev() {
784 if !binary_image[[iz, iy, ix]] {
785 prev_back = 0.0;
786 } else if prev_back < inf_sq {
787 prev_back += 1.0;
788 }
789 let bval = prev_back * prev_back;
790 if bval < g[[iz, iy, ix]] {
791 g[[iz, iy, ix]] = bval;
792 }
793 }
794 }
795 }
796
797 let mut h = Array3::<f64>::from_elem((sz, sy, sx), inf_sq);
800
801 for iz in 0..sz {
802 for ix in 0..sx {
803 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);
807
808 let mut k = 0usize;
809 t[0] = 0;
810 s[0] = f64::NEG_INFINITY;
811
812 for q in 1..sy {
813 loop {
815 let sq =
816 ((f(q) - f(t[k])) / (2.0 * q as f64 - 2.0 * t[k] as f64) + 0.5).floor();
817 if k == 0 || sq > s[k] {
818 k += 1;
819 s[k] = sq;
820 t[k] = q;
821 break;
822 }
823 if k > 0 {
824 k -= 1;
825 } else {
826 break;
827 }
828 }
829 }
830
831 let mut j = k;
832 for q in (0..sy).rev() {
833 while j > 0 && s[j] > q as f64 {
834 j -= 1;
835 }
836 h[[iz, q, ix]] = f(t[j]) + (q as f64 - t[j] as f64).powi(2) - (t[j] as f64).powi(2);
837 }
838 }
839 }
840
841 let mut out = Array3::<f64>::zeros((sz, sy, sx));
843
844 for iy in 0..sy {
845 for ix in 0..sx {
846 let f = |q: usize| h[[q, iy, ix]] + (q as f64).powi(2);
847 let mut s = vec![0.0f64; sz];
848 let mut t = vec![0usize; sz];
849 let mut k = 0usize;
850 t[0] = 0;
851 s[0] = f64::NEG_INFINITY;
852
853 for q in 1..sz {
854 loop {
855 let sq =
856 ((f(q) - f(t[k])) / (2.0 * q as f64 - 2.0 * t[k] as f64) + 0.5).floor();
857 if k == 0 || sq > s[k] {
858 k += 1;
859 s[k] = sq;
860 t[k] = q;
861 break;
862 }
863 if k > 0 {
864 k -= 1;
865 } else {
866 break;
867 }
868 }
869 }
870
871 let mut j = k;
872 for q in (0..sz).rev() {
873 while j > 0 && s[j] > q as f64 {
874 j -= 1;
875 }
876 let dist_sq = f(t[j]) + (q as f64 - t[j] as f64).powi(2) - (t[j] as f64).powi(2);
877 out[[q, iy, ix]] = dist_sq.max(0.0).sqrt();
878 }
879 }
880 }
881
882 Ok(out)
883}
884
885pub fn skeletonize3d(binary_image: ArrayView3<bool>) -> NdimageResult<Array3<bool>> {
906 let shape = binary_image.shape();
907 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
908 if sz == 0 || sy == 0 || sx == 0 {
909 return Err(NdimageError::InvalidInput(
910 "image must be non-empty".to_string(),
911 ));
912 }
913
914 let mut current = binary_image.to_owned();
915
916 let directions: [(isize, isize, isize); 6] = [
918 (-1, 0, 0),
919 (1, 0, 0),
920 (0, -1, 0),
921 (0, 1, 0),
922 (0, 0, -1),
923 (0, 0, 1),
924 ];
925
926 let mut changed = true;
927 while changed {
928 changed = false;
929 for &(dz, dy, dx) in &directions {
930 let mut candidates = Vec::new();
932 for iz in 0..sz {
933 for iy in 0..sy {
934 for ix in 0..sx {
935 if !current[[iz, iy, ix]] {
936 continue;
937 }
938 let nz = iz as isize + dz;
939 let ny = iy as isize + dy;
940 let nx = ix as isize + dx;
941 let is_border = if nz < 0
943 || ny < 0
944 || nx < 0
945 || nz >= sz as isize
946 || ny >= sy as isize
947 || nx >= sx as isize
948 {
949 true
950 } else {
951 !current[[nz as usize, ny as usize, nx as usize]]
952 };
953 if is_border && is_simple_point(¤t, iz, iy, ix) {
954 candidates.push((iz, iy, ix));
955 }
956 }
957 }
958 }
959 for (iz, iy, ix) in candidates {
960 current[[iz, iy, ix]] = false;
961 changed = true;
962 }
963 }
964 }
965
966 Ok(current)
967}
968
969fn is_simple_point(vol: &Array3<bool>, iz: usize, iy: usize, ix: usize) -> bool {
975 let shape = vol.shape();
976 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
977
978 let mut nbhood = [[[false; 3]; 3]; 3];
980 for dz in 0..3usize {
981 for dy in 0..3usize {
982 for dx in 0..3usize {
983 if dz == 1 && dy == 1 && dx == 1 {
984 continue;
985 }
986 let nz = iz as isize + dz as isize - 1;
987 let ny = iy as isize + dy as isize - 1;
988 let nx = ix as isize + dx as isize - 1;
989 if nz >= 0
990 && ny >= 0
991 && nx >= 0
992 && nz < sz as isize
993 && ny < sy as isize
994 && nx < sx as isize
995 {
996 nbhood[dz][dy][dx] = vol[[nz as usize, ny as usize, nx as usize]];
997 }
998 }
999 }
1000 }
1001
1002 let fg_components = count_components_26(&nbhood, true);
1004 if fg_components != 1 {
1005 return false;
1006 }
1007
1008 let mut bg_nbhood = [[[true; 3]; 3]; 3];
1010 for dz in 0..3usize {
1011 for dy in 0..3usize {
1012 for dx in 0..3usize {
1013 if dz == 1 && dy == 1 && dx == 1 {
1014 bg_nbhood[dz][dy][dx] = true;
1016 } else {
1017 bg_nbhood[dz][dy][dx] = !nbhood[dz][dy][dx];
1019 }
1020 }
1021 }
1022 }
1023 let bg_components = count_components_6(&bg_nbhood, true);
1024 bg_components == 1
1025}
1026
1027fn count_components_26(nbhood: &[[[bool; 3]; 3]; 3], target: bool) -> usize {
1029 let mut visited = [[[false; 3]; 3]; 3];
1030 let mut count = 0;
1031 for sz in 0..3usize {
1032 for sy in 0..3usize {
1033 for sx in 0..3usize {
1034 if nbhood[sz][sy][sx] == target && !visited[sz][sy][sx] {
1035 let mut queue = std::collections::VecDeque::new();
1037 queue.push_back((sz, sy, sx));
1038 visited[sz][sy][sx] = true;
1039 while let Some((cz, cy, cx)) = queue.pop_front() {
1040 for dz in 0..3usize {
1041 for dy in 0..3usize {
1042 for dx in 0..3usize {
1043 if dz == 1 && dy == 1 && dx == 1 {
1044 continue;
1045 }
1046 let nz = cz as isize + dz as isize - 1;
1047 let ny = cy as isize + dy as isize - 1;
1048 let nx = cx as isize + dx as isize - 1;
1049 if nz >= 0 && ny >= 0 && nx >= 0 && nz < 3 && ny < 3 && nx < 3 {
1050 let (nzu, nyu, nxu) =
1051 (nz as usize, ny as usize, nx as usize);
1052 if nbhood[nzu][nyu][nxu] == target
1053 && !visited[nzu][nyu][nxu]
1054 {
1055 visited[nzu][nyu][nxu] = true;
1056 queue.push_back((nzu, nyu, nxu));
1057 }
1058 }
1059 }
1060 }
1061 }
1062 }
1063 count += 1;
1064 }
1065 }
1066 }
1067 }
1068 count
1069}
1070
1071fn count_components_6(nbhood: &[[[bool; 3]; 3]; 3], target: bool) -> usize {
1073 let offsets: [(isize, isize, isize); 6] = [
1074 (-1, 0, 0),
1075 (1, 0, 0),
1076 (0, -1, 0),
1077 (0, 1, 0),
1078 (0, 0, -1),
1079 (0, 0, 1),
1080 ];
1081 let mut visited = [[[false; 3]; 3]; 3];
1082 let mut count = 0;
1083 for sz in 0..3usize {
1084 for sy in 0..3usize {
1085 for sx in 0..3usize {
1086 if nbhood[sz][sy][sx] == target && !visited[sz][sy][sx] {
1087 let mut queue = std::collections::VecDeque::new();
1088 queue.push_back((sz, sy, sx));
1089 visited[sz][sy][sx] = true;
1090 while let Some((cz, cy, cx)) = queue.pop_front() {
1091 for &(dz, dy, dx) in &offsets {
1092 let nz = cz as isize + dz;
1093 let ny = cy as isize + dy;
1094 let nx = cx as isize + dx;
1095 if nz >= 0 && ny >= 0 && nx >= 0 && nz < 3 && ny < 3 && nx < 3 {
1096 let (nzu, nyu, nxu) = (nz as usize, ny as usize, nx as usize);
1097 if nbhood[nzu][nyu][nxu] == target && !visited[nzu][nyu][nxu] {
1098 visited[nzu][nyu][nxu] = true;
1099 queue.push_back((nzu, nyu, nxu));
1100 }
1101 }
1102 }
1103 }
1104 count += 1;
1105 }
1106 }
1107 }
1108 }
1109 count
1110}
1111
1112#[cfg(test)]
1117mod tests {
1118 use super::*;
1119 use scirs2_core::ndarray::Array3;
1120
1121 fn sphere_5() -> Array3<bool> {
1123 Array3::from_shape_fn((5, 5, 5), |(z, y, x)| {
1124 let dz = z as f64 - 2.0;
1125 let dy = y as f64 - 2.0;
1126 let dx = x as f64 - 2.0;
1127 dz * dz + dy * dy + dx * dx <= 4.0 + 1e-9
1128 })
1129 }
1130
1131 fn solid_cube() -> Array3<bool> {
1133 Array3::from_elem((4, 4, 4), true)
1134 }
1135
1136 fn two_blobs() -> Array3<bool> {
1138 let mut v = Array3::from_elem((5, 5, 5), false);
1139 for z in 0..2usize {
1140 for y in 0..2usize {
1141 for x in 0..2usize {
1142 v[[z, y, x]] = true;
1143 v[[z + 3, y + 3, x + 3]] = true;
1144 }
1145 }
1146 }
1147 v
1148 }
1149
1150 #[test]
1153 fn test_se_cube_shape() {
1154 let se = StructuringElement3D::Cube(1);
1155 let arr = se.to_array();
1156 assert_eq!(arr.shape(), &[3, 3, 3]);
1157 assert!(arr.iter().all(|&v| v));
1158 }
1159
1160 #[test]
1161 fn test_se_sphere_center() {
1162 let se = StructuringElement3D::Sphere(2.0);
1163 let arr = se.to_array();
1164 let (cz, cy, cx) = se.center();
1165 assert!(arr[[cz, cy, cx]]);
1166 }
1167
1168 #[test]
1169 fn test_se_cross_6_connectivity() {
1170 let se = StructuringElement3D::Cross;
1171 let arr = se.to_array();
1172 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();
1178 assert_eq!(count, 7);
1179 }
1180
1181 #[test]
1182 fn test_se_cross26_full() {
1183 let se = StructuringElement3D::Cross26;
1184 let arr = se.to_array();
1185 assert_eq!(arr.iter().filter(|&&v| v).count(), 27);
1186 }
1187
1188 #[test]
1191 fn test_binary_erosion_shrinks() {
1192 let img = sphere_5();
1193 let se = StructuringElement3D::Cross;
1194 let eroded = binary_erosion3d(img.view(), &se, 1).expect("erosion failed");
1195 let before: usize = img.iter().filter(|&&v| v).count();
1196 let after: usize = eroded.iter().filter(|&&v| v).count();
1197 assert!(after <= before, "erosion should not grow the object");
1198 }
1199
1200 #[test]
1201 fn test_binary_dilation_grows() {
1202 let img = sphere_5();
1203 let se = StructuringElement3D::Cross;
1204 let dilated = binary_dilation3d(img.view(), &se, 1).expect("dilation failed");
1205 let before: usize = img.iter().filter(|&&v| v).count();
1206 let after: usize = dilated.iter().filter(|&&v| v).count();
1207 assert!(after >= before, "dilation should not shrink the object");
1208 }
1209
1210 #[test]
1211 fn test_binary_erosion_zero_iterations_error() {
1212 let img = sphere_5();
1213 let se = StructuringElement3D::Cross;
1214 let result = binary_erosion3d(img.view(), &se, 0);
1215 assert!(result.is_err());
1216 }
1217
1218 #[test]
1219 fn test_binary_opening_smaller_than_original() {
1220 let img = sphere_5();
1221 let se = StructuringElement3D::Cube(1);
1222 let opened = binary_opening3d(img.view(), &se).expect("opening failed");
1223 let before: usize = img.iter().filter(|&&v| v).count();
1224 let after: usize = opened.iter().filter(|&&v| v).count();
1225 assert!(after <= before);
1226 }
1227
1228 #[test]
1229 fn test_binary_closing_larger_than_original() {
1230 let mut img = Array3::from_elem((7, 7, 7), false);
1232 img[[3, 3, 3]] = true;
1233 img[[3, 3, 5]] = true;
1234 let se = StructuringElement3D::Cube(1);
1235 let closed = binary_closing3d(img.view(), &se).expect("closing failed");
1236 let before: usize = img.iter().filter(|&&v| v).count();
1237 let after: usize = closed.iter().filter(|&&v| v).count();
1238 assert!(after >= before);
1239 }
1240
1241 #[test]
1242 fn test_binary_erosion_all_background_stays_background() {
1243 let img = Array3::from_elem((5, 5, 5), false);
1244 let se = StructuringElement3D::Cross26;
1245 let eroded = binary_erosion3d(img.view(), &se, 1).expect("erosion failed");
1246 assert!(eroded.iter().all(|&v| !v));
1247 }
1248
1249 #[test]
1252 fn test_grey_erosion_reduces_values() {
1253 let img = Array3::from_shape_fn((5, 5, 5), |(z, y, x)| (z + y + x) as f64);
1254 let se = StructuringElement3D::Cross;
1255 let eroded = grey_erosion3d(img.view(), &se).expect("grey erosion failed");
1256 for ((z, y, x), &v) in eroded.indexed_iter() {
1258 assert!(v <= img[[z, y, x]] + 1e-10);
1259 }
1260 }
1261
1262 #[test]
1263 fn test_grey_dilation_increases_values() {
1264 let img = Array3::from_shape_fn((5, 5, 5), |(z, y, x)| (z + y + x) as f64);
1265 let se = StructuringElement3D::Cross;
1266 let dilated = grey_dilation3d(img.view(), &se).expect("grey dilation failed");
1267 for ((z, y, x), &v) in dilated.indexed_iter() {
1268 assert!(v >= img[[z, y, x]] - 1e-10);
1269 }
1270 }
1271
1272 #[test]
1275 fn test_label3d_single_blob() {
1276 let img = sphere_5();
1277 let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
1278 assert_eq!(n, 1);
1279 for ((z, y, x), &lbl) in labels.indexed_iter() {
1281 if img[[z, y, x]] {
1282 assert_eq!(lbl, 1);
1283 } else {
1284 assert_eq!(lbl, 0);
1285 }
1286 }
1287 }
1288
1289 #[test]
1290 fn test_label3d_two_blobs() {
1291 let img = two_blobs();
1292 let (_, n) = label3d(img.view(), 26).expect("labeling failed");
1293 assert_eq!(n, 2, "expected exactly 2 components");
1294 }
1295
1296 #[test]
1297 fn test_label3d_background_only() {
1298 let img = Array3::from_elem((4, 4, 4), false);
1299 let (_, n) = label3d(img.view(), 6).expect("labeling failed");
1300 assert_eq!(n, 0);
1301 }
1302
1303 #[test]
1304 fn test_label3d_full_volume() {
1305 let img = Array3::from_elem((3, 3, 3), true);
1306 let (_, n) = label3d(img.view(), 6).expect("labeling failed");
1307 assert_eq!(n, 1);
1308 }
1309
1310 #[test]
1313 fn test_object_properties_volume() {
1314 let img = solid_cube();
1315 let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
1316 let props = object_properties3d(&labels, n);
1317 assert_eq!(props.len(), 1);
1318 assert_eq!(props[0].volume, 64); }
1320
1321 #[test]
1322 fn test_object_properties_centroid() {
1323 let img = solid_cube();
1324 let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
1325 let props = object_properties3d(&labels, n);
1326 let c = props[0].centroid;
1327 assert!((c.0 - 1.5).abs() < 1e-6);
1328 assert!((c.1 - 1.5).abs() < 1e-6);
1329 assert!((c.2 - 1.5).abs() < 1e-6);
1330 }
1331
1332 #[test]
1333 fn test_object_properties_equiv_diameter() {
1334 let img = solid_cube();
1335 let (labels, n) = label3d(img.view(), 26).expect("labeling failed");
1336 let props = object_properties3d(&labels, n);
1337 assert!(props[0].equivalent_diameter > 0.0);
1339 }
1340
1341 #[test]
1344 fn test_edt3d_background_zero() {
1345 let img = Array3::from_elem((5, 5, 5), false);
1346 let dist = distance_transform_edt3d(img.view()).expect("EDT failed");
1347 assert!(dist.iter().all(|&v| v == 0.0));
1348 }
1349
1350 #[test]
1351 fn test_edt3d_center_voxel() {
1352 let mut img = Array3::from_elem((5, 5, 5), true);
1357 for z in 0..5usize {
1359 for y in 0..5usize {
1360 for x in 0..5usize {
1361 if z == 0 || z == 4 || y == 0 || y == 4 || x == 0 || x == 4 {
1362 img[[z, y, x]] = false;
1363 }
1364 }
1365 }
1366 }
1367 let dist = distance_transform_edt3d(img.view()).expect("EDT failed");
1368 assert!(
1370 (dist[[2, 2, 2]] - 2.0).abs() < 0.5,
1371 "Expected ~2.0, got {}",
1372 dist[[2, 2, 2]]
1373 );
1374 }
1375
1376 #[test]
1377 fn test_edt3d_single_fg_voxel() {
1378 let mut img = Array3::from_elem((3, 3, 3), false);
1379 img[[1, 1, 1]] = true;
1380 let dist = distance_transform_edt3d(img.view()).expect("EDT failed");
1381 assert_eq!(dist[[0, 0, 0]], 0.0);
1383 assert!((dist[[1, 1, 1]] - 1.0).abs() < 0.5);
1385 }
1386
1387 #[test]
1390 fn test_skeletonize3d_preserves_nonempty() {
1391 let img = sphere_5();
1392 let skel = skeletonize3d(img.view()).expect("skeletonize failed");
1393 for ((z, y, x), &v) in skel.indexed_iter() {
1395 if v {
1396 assert!(img[[z, y, x]], "skeleton contains voxel not in original");
1397 }
1398 }
1399 }
1400
1401 #[test]
1402 fn test_skeletonize3d_empty_error() {
1403 let img: Array3<bool> = Array3::from_elem((0, 0, 0), false);
1404 let result = skeletonize3d(img.view());
1405 assert!(result.is_err());
1406 }
1407
1408 #[test]
1409 fn test_skeletonize3d_single_voxel() {
1410 let mut img = Array3::from_elem((3, 3, 3), false);
1411 img[[1, 1, 1]] = true;
1412 let skel = skeletonize3d(img.view()).expect("skeletonize failed");
1413 assert!(skel[[1, 1, 1]]);
1416 }
1417}