1use crate::Grid;
9use crate::utils::bias_correction::{check_volume, idx3d};
10
11pub mod signal_erosion;
12pub use signal_erosion::{signal_gated_erosion, SignalErosionParams};
13
14pub fn erode_mask(mask: &[u8], grid: &Grid, iterations: usize) -> Vec<u8> {
19 let (nx, ny, nz) = grid.dims;
20 let mut current = mask.to_vec();
21 for _ in 0..iterations {
22 let mut eroded = current.clone();
23 for z in 0..nz {
24 for y in 0..ny {
25 for x in 0..nx {
26 let idx = x + y * nx + z * nx * ny;
27 if current[idx] == 0 {
28 continue;
29 }
30 if x == 0
31 || x == nx - 1
32 || y == 0
33 || y == ny - 1
34 || z == 0
35 || z == nz - 1
36 || current[idx - 1] == 0
37 || current[idx + 1] == 0
38 || current[idx - nx] == 0
39 || current[idx + nx] == 0
40 || current[idx - nx * ny] == 0
41 || current[idx + nx * ny] == 0
42 {
43 eroded[idx] = 0;
44 }
45 }
46 }
47 }
48 current = eroded;
49 }
50 current
51}
52
53pub fn dilate_mask(mask: &[u8], grid: &Grid, iterations: usize) -> Vec<u8> {
57 let (nx, ny, nz) = grid.dims;
58 let mut current = mask.to_vec();
59 for _ in 0..iterations {
60 let mut dilated = current.clone();
61 for z in 0..nz {
62 for y in 0..ny {
63 for x in 0..nx {
64 let idx = x + y * nx + z * nx * ny;
65 if current[idx] == 1 {
66 continue;
67 }
68 let has_neighbor = (x > 0 && current[idx - 1] == 1)
69 || (x < nx - 1 && current[idx + 1] == 1)
70 || (y > 0 && current[idx - nx] == 1)
71 || (y < ny - 1 && current[idx + nx] == 1)
72 || (z > 0 && current[idx - nx * ny] == 1)
73 || (z < nz - 1 && current[idx + nx * ny] == 1);
74 if has_neighbor {
75 dilated[idx] = 1;
76 }
77 }
78 }
79 }
80 current = dilated;
81 }
82 current
83}
84
85pub(crate) fn erode_ball(mask: &[u8], nx: usize, ny: usize, nz: usize, radius: i32) -> Vec<u8> {
89 erode_ball_padded(mask, nx, ny, nz, radius, false)
90}
91
92pub(crate) fn imerode_sphere(mask: &[u8], nx: usize, ny: usize, nz: usize, radius: i32) -> Vec<u8> {
95 erode_ball_padded(mask, nx, ny, nz, radius, true)
96}
97
98fn erode_ball_padded(mask: &[u8], nx: usize, ny: usize, nz: usize, radius: i32, edge_inside: bool) -> Vec<u8> {
99 let n_total = nx * ny * nz;
100 let mut eroded = vec![0u8; n_total];
101
102 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
103
104 for k in 0..nz {
105 for j in 0..ny {
106 for i in 0..nx {
107 if mask[idx(i, j, k)] == 0 {
108 continue;
109 }
110
111 let mut all_inside = true;
112
113 'outer: for dz in -radius..=radius {
114 for dy in -radius..=radius {
115 for dx in -radius..=radius {
116 let dist2 = dx * dx + dy * dy + dz * dz;
117 if dist2 > radius * radius {
118 continue;
119 }
120
121 let ni = i as i32 + dx;
122 let nj = j as i32 + dy;
123 let nk = k as i32 + dz;
124
125 if ni < 0 || ni >= nx as i32 ||
126 nj < 0 || nj >= ny as i32 ||
127 nk < 0 || nk >= nz as i32 {
128 if edge_inside {
129 continue;
130 }
131 all_inside = false;
132 break 'outer;
133 }
134
135 if mask[idx(ni as usize, nj as usize, nk as usize)] == 0 {
136 all_inside = false;
137 break 'outer;
138 }
139 }
140 }
141 }
142
143 if all_inside {
144 eroded[idx(i, j, k)] = 1;
145 }
146 }
147 }
148 }
149
150 eroded
151}
152
153pub(crate) fn dilate_ball(mask: &[u8], nx: usize, ny: usize, nz: usize, radius: i32) -> Vec<u8> {
156 let n_total = nx * ny * nz;
157 let mut dilated = vec![0u8; n_total];
158
159 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
160
161 for k in 0..nz {
162 for j in 0..ny {
163 for i in 0..nx {
164 if mask[idx(i, j, k)] != 0 {
165 for dz in -radius..=radius {
167 for dy in -radius..=radius {
168 for dx in -radius..=radius {
169 let dist2 = dx * dx + dy * dy + dz * dz;
170 if dist2 > radius * radius {
171 continue;
172 }
173
174 let ni = i as i32 + dx;
175 let nj = j as i32 + dy;
176 let nk = k as i32 + dz;
177
178 if ni >= 0 && ni < nx as i32 &&
179 nj >= 0 && nj < ny as i32 &&
180 nk >= 0 && nk < nz as i32 {
181 dilated[idx(ni as usize, nj as usize, nk as usize)] = 1;
182 }
183 }
184 }
185 }
186 }
187 }
188 }
189 }
190
191 dilated
192}
193
194pub fn morphological_close(mask: &[u8], grid: &Grid, radius: i32) -> Vec<u8> {
197 let (nx, ny, nz) = grid.dims;
198 let dilated = dilate_ball(mask, nx, ny, nz, radius);
199 erode_ball(&dilated, nx, ny, nz, radius)
200}
201
202fn flood_fill_component(
204 mask: &[u8],
205 visited: &mut [bool],
206 start: usize,
207 nx: usize, ny: usize, nz: usize,
208) -> Vec<usize> {
209 let mut component = Vec::new();
210 let mut stack = vec![start];
211
212 while let Some(idx) = stack.pop() {
213 if visited[idx] || mask[idx] != 0 {
214 continue;
215 }
216
217 visited[idx] = true;
218 component.push(idx);
219
220 let k = idx / (nx * ny);
222 let rem = idx % (nx * ny);
223 let j = rem / nx;
224 let i = rem % nx;
225
226 if i > 0 {
228 let n = idx3d(i - 1, j, k, nx, ny);
229 if !visited[n] && mask[n] == 0 { stack.push(n); }
230 }
231 if i + 1 < nx {
232 let n = idx3d(i + 1, j, k, nx, ny);
233 if !visited[n] && mask[n] == 0 { stack.push(n); }
234 }
235 if j > 0 {
236 let n = idx3d(i, j - 1, k, nx, ny);
237 if !visited[n] && mask[n] == 0 { stack.push(n); }
238 }
239 if j + 1 < ny {
240 let n = idx3d(i, j + 1, k, nx, ny);
241 if !visited[n] && mask[n] == 0 { stack.push(n); }
242 }
243 if k > 0 {
244 let n = idx3d(i, j, k - 1, nx, ny);
245 if !visited[n] && mask[n] == 0 { stack.push(n); }
246 }
247 if k + 1 < nz {
248 let n = idx3d(i, j, k + 1, nx, ny);
249 if !visited[n] && mask[n] == 0 { stack.push(n); }
250 }
251 }
252
253 component
254}
255
256pub fn fill_holes(mask: &[u8], grid: &Grid, max_hole_size: usize) -> Vec<u8> {
263 let (nx, ny, nz) = grid.dims;
264 let n_total = check_volume("fill_holes: mask", mask.len(), grid);
265 let mut result = mask.to_vec();
266 let mut visited = vec![false; n_total];
267
268 for idx in 0..n_total {
270 if mask[idx] == 0 && !visited[idx] {
271 let component = flood_fill_component(mask, &mut visited, idx, nx, ny, nz);
272
273 let mut touches_boundary = false;
275 for &cidx in &component {
276 let k = cidx / (nx * ny);
277 let rem = cidx % (nx * ny);
278 let j = rem / nx;
279 let i = rem % nx;
280
281 if i == 0 || i == nx - 1 || j == 0 || j == ny - 1 || k == 0 || k == nz - 1 {
282 touches_boundary = true;
283 break;
284 }
285 }
286
287 if !touches_boundary && component.len() <= max_hole_size {
289 for cidx in component {
290 result[cidx] = 1;
291 }
292 }
293 }
294 }
295
296 result
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
304 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
305 }
306
307 fn make_sphere_mask(n: usize, radius: f64) -> Vec<u8> {
309 let center = n as f64 / 2.0;
310 let mut mask = vec![0u8; n * n * n];
311 for k in 0..n {
312 for j in 0..n {
313 for i in 0..n {
314 let dx = i as f64 - center;
315 let dy = j as f64 - center;
316 let dz = k as f64 - center;
317 if (dx * dx + dy * dy + dz * dz).sqrt() < radius {
318 mask[i + j * n + k * n * n] = 1;
319 }
320 }
321 }
322 }
323 mask
324 }
325
326 #[test]
327 fn test_erode_mask_cube() {
328 let mask = vec![1u8; 27];
330 let result = erode_mask(&mask, &grid(3, 3, 3), 1);
331 assert_eq!(result.iter().filter(|&&v| v == 1).count(), 1); }
333
334 #[test]
335 fn test_dilate_mask_single_voxel() {
336 let mut mask = vec![0u8; 125];
338 mask[2 + 2 * 5 + 2 * 25] = 1; let result = dilate_mask(&mask, &grid(5, 5, 5), 1);
340 assert_eq!(result.iter().filter(|&&v| v == 1).count(), 7); }
342
343 #[test]
344 fn test_erode_ball() {
345 let mask = vec![1u8; 125];
347 let eroded = erode_ball(&mask, 5, 5, 5, 1);
348
349 let count: usize = eroded.iter().map(|&v| v as usize).sum();
351 assert!(count > 0);
352 assert!(count < 125);
353 }
354
355 #[test]
356 fn test_dilate_ball() {
357 let mut mask = vec![0u8; 125];
359 mask[62] = 1; let dilated = dilate_ball(&mask, 5, 5, 5, 1);
362
363 let count: usize = dilated.iter().map(|&v| v as usize).sum();
365 assert!(count >= 7); }
367
368 #[test]
373 fn test_erode_ball_sphere() {
374 let n = 10;
375 let mask = make_sphere_mask(n, 4.0);
376 let eroded = erode_ball(&mask, n, n, n, 1);
377
378 let orig_count: usize = mask.iter().map(|&v| v as usize).sum();
379 let eroded_count: usize = eroded.iter().map(|&v| v as usize).sum();
380 assert!(
381 eroded_count < orig_count,
382 "Eroded sphere should be smaller: {} < {}",
383 eroded_count,
384 orig_count
385 );
386 assert!(eroded_count > 0, "Eroded sphere should not be empty");
387
388 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
390 assert_eq!(eroded[center], 1, "Center should survive erosion");
391 }
392
393 #[test]
394 fn test_erode_ball_single_voxel() {
395 let mut mask = vec![0u8; 125];
397 mask[62] = 1; let eroded = erode_ball(&mask, 5, 5, 5, 1);
399 let count: usize = eroded.iter().map(|&v| v as usize).sum();
400 assert_eq!(count, 0, "Single voxel should be fully eroded");
401 }
402
403 #[test]
408 fn test_dilate_ball_sphere() {
409 let n = 10;
410 let mask = make_sphere_mask(n, 3.0);
411 let dilated = dilate_ball(&mask, n, n, n, 1);
412
413 let orig_count: usize = mask.iter().map(|&v| v as usize).sum();
414 let dilated_count: usize = dilated.iter().map(|&v| v as usize).sum();
415 assert!(
416 dilated_count > orig_count,
417 "Dilated sphere should be larger: {} > {}",
418 dilated_count,
419 orig_count
420 );
421 }
422
423 #[test]
424 fn test_dilate_ball_radius_2() {
425 let mut mask = vec![0u8; 125];
426 mask[62] = 1; let dilated = dilate_ball(&mask, 5, 5, 5, 2);
428 let count: usize = dilated.iter().map(|&v| v as usize).sum();
429 assert!(count > 7, "Radius-2 dilation should produce more than 7 voxels, got {}", count);
431 }
432
433 #[test]
438 fn test_morphological_close_fills_small_gaps() {
439 let n = 10;
440 let mut mask = make_sphere_mask(n, 4.0);
441 let eroded = erode_ball(&mask, n, n, n, 1);
443 let surface: Vec<usize> = (0..mask.len()).filter(|&i| mask[i] != 0 && eroded[i] == 0).collect();
444 if !surface.is_empty() {
445 mask[surface[0]] = 0;
446 }
447
448 let closed = morphological_close(&mask, &grid(n, n, n), 1);
449 let orig_count: usize = mask.iter().map(|&v| v as usize).sum();
450 let closed_count: usize = closed.iter().map(|&v| v as usize).sum();
451 assert!(
453 closed_count >= orig_count,
454 "Closing should not reduce mask size: {} vs {}",
455 closed_count,
456 orig_count
457 );
458 }
459
460 #[test]
461 fn test_morphological_close_empty() {
462 let mask = vec![0u8; 27];
463 let closed = morphological_close(&mask, &grid(3, 3, 3), 1);
464 let count: usize = closed.iter().map(|&v| v as usize).sum();
465 assert_eq!(count, 0, "Closing empty mask should stay empty");
466 }
467
468 #[test]
473 fn test_fill_holes_basic() {
474 let mut mask = vec![1u8; 27];
476 mask[13] = 0; let filled = fill_holes(&mask, &grid(3, 3, 3), 5);
479 assert_eq!(filled[13], 1, "Center hole should be filled");
480 }
481
482 #[test]
487 fn test_fill_holes_boundary_hole_not_filled() {
488 let mut mask = vec![1u8; 125]; mask[0] = 0; let filled = fill_holes(&mask, &grid(5, 5, 5), 100);
492 assert_eq!(filled[0], 0, "Boundary hole should not be filled");
493 }
494
495 #[test]
496 fn test_fill_holes_large_hole_not_filled() {
497 let n = 7;
499 let n_total = n * n * n;
500 let mut mask = vec![1u8; n_total];
501 for k in 2..5 {
503 for j in 2..5 {
504 for i in 2..5 {
505 mask[i + j * n + k * n * n] = 0;
506 }
507 }
508 }
509 let filled = fill_holes(&mask, &grid(n, n, n), 5); let center = 3 + 3 * n + 3 * n * n;
512 assert_eq!(filled[center], 0, "Large hole should not be filled");
513 }
514
515 #[test]
516 #[should_panic(expected = "fill_holes: mask has 216 voxels but the grid is 8x8x8 (512 voxels)")]
517 fn fill_holes_refuses_mask_not_matching_grid() {
518 fill_holes(&vec![1u8; 6 * 6 * 6], &grid(8, 8, 8), 10);
519 }
520
521 #[test]
526 fn test_flood_fill_component_single() {
527 let mask = vec![1u8; 27]; let mut visited = vec![false; 27];
529 let component = flood_fill_component(&mask, &mut visited, 0, 3, 3, 3);
531 assert!(component.is_empty(), "All filled mask should have no zero component");
532 }
533
534 #[test]
535 fn test_flood_fill_component_connected_zeros() {
536 let n = 5;
537 let n_total = n * n * n;
538 let mut mask = vec![1u8; n_total];
539 for i in 1..4 {
541 mask[i + 2 * n + 2 * n * n] = 0;
542 }
543 let mut visited = vec![false; n_total];
544 let start = 1 + 2 * n + 2 * n * n;
545 let component = flood_fill_component(&mask, &mut visited, start, n, n, n);
546 assert_eq!(component.len(), 3, "Should find 3 connected zero voxels");
547 }
548}