Skip to main content

qsm_core/utils/
mask.rs

1//! Mask utilities
2//!
3//! Provides functions for creating, eroding, dilating, and applying masks on 3D volumes.
4
5use crate::Grid;
6
7/// Create a binary sphere mask on a 3D volume
8///
9/// Generates a mask where voxels within the specified radius of the center
10/// are set to 1, and all others are 0. Uses Fortran (column-major) ordering
11/// to match NIfTI convention: index = x + y*nx + z*nx*ny.
12///
13/// # Arguments
14/// * `grid` - Volume grid (dimensions and voxel sizes)
15/// * `center_x`, `center_y`, `center_z` - Sphere center in voxel coordinates
16/// * `radius` - Sphere radius in voxels
17///
18/// # Returns
19/// Flattened binary mask of length nx*ny*nz
20pub fn create_sphere_mask(
21    grid: &Grid,
22    center_x: f64, center_y: f64, center_z: f64,
23    radius: f64,
24) -> Vec<u8> {
25    let (nx, ny, nz) = grid.dims;
26    let mut mask = vec![0u8; nx * ny * nz];
27    let r2 = radius * radius;
28
29    for k in 0..nz {
30        for j in 0..ny {
31            for i in 0..nx {
32                let dx = i as f64 - center_x;
33                let dy = j as f64 - center_y;
34                let dz = k as f64 - center_z;
35                if dx * dx + dy * dy + dz * dz <= r2 {
36                    mask[i + j * nx + k * nx * ny] = 1;
37                }
38            }
39        }
40    }
41
42    mask
43}
44
45/// Zero out elements where mask is 0.
46#[inline]
47pub fn apply_mask_zero(data: &mut [f64], mask: &[u8]) {
48    for i in 0..data.len() {
49        if mask[i] == 0 {
50            data[i] = 0.0;
51        }
52    }
53}
54
55/// Erode a binary mask by removing boundary voxels (6-connectivity).
56///
57/// Each iteration removes voxels that have any 6-connected neighbor equal to 0
58/// or that sit on the volume boundary.
59pub fn erode_mask(mask: &[u8], grid: &Grid, iterations: usize) -> Vec<u8> {
60    let (nx, ny, nz) = grid.dims;
61    let mut current = mask.to_vec();
62    for _ in 0..iterations {
63        let mut eroded = current.clone();
64        for z in 0..nz {
65            for y in 0..ny {
66                for x in 0..nx {
67                    let idx = x + y * nx + z * nx * ny;
68                    if current[idx] == 0 {
69                        continue;
70                    }
71                    if x == 0
72                        || x == nx - 1
73                        || y == 0
74                        || y == ny - 1
75                        || z == 0
76                        || z == nz - 1
77                        || current[idx - 1] == 0
78                        || current[idx + 1] == 0
79                        || current[idx - nx] == 0
80                        || current[idx + nx] == 0
81                        || current[idx - nx * ny] == 0
82                        || current[idx + nx * ny] == 0
83                    {
84                        eroded[idx] = 0;
85                    }
86                }
87            }
88        }
89        current = eroded;
90    }
91    current
92}
93
94/// Dilate a binary mask by expanding into neighboring voxels (6-connectivity).
95///
96/// Each iteration adds voxels that have any 6-connected neighbor equal to 1.
97pub fn dilate_mask(mask: &[u8], grid: &Grid, iterations: usize) -> Vec<u8> {
98    let (nx, ny, nz) = grid.dims;
99    let mut current = mask.to_vec();
100    for _ in 0..iterations {
101        let mut dilated = current.clone();
102        for z in 0..nz {
103            for y in 0..ny {
104                for x in 0..nx {
105                    let idx = x + y * nx + z * nx * ny;
106                    if current[idx] == 1 {
107                        continue;
108                    }
109                    let has_neighbor = (x > 0 && current[idx - 1] == 1)
110                        || (x < nx - 1 && current[idx + 1] == 1)
111                        || (y > 0 && current[idx - nx] == 1)
112                        || (y < ny - 1 && current[idx + nx] == 1)
113                        || (z > 0 && current[idx - nx * ny] == 1)
114                        || (z < nz - 1 && current[idx + nx * ny] == 1);
115                    if has_neighbor {
116                        dilated[idx] = 1;
117                    }
118                }
119            }
120        }
121        current = dilated;
122    }
123    current
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
131        Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
132    }
133
134    #[test]
135    fn test_sphere_mask_basic() {
136        let mask = create_sphere_mask(&grid(10, 10, 10), 5.0, 5.0, 5.0, 3.0);
137        assert_eq!(mask.len(), 1000);
138
139        // Center voxel should be inside
140        assert_eq!(mask[5 + 5 * 10 + 5 * 100], 1);
141
142        // Corner should be outside
143        assert_eq!(mask[0], 0);
144
145        // Count should be reasonable for a sphere of radius 3
146        let count: usize = mask.iter().map(|&m| m as usize).sum();
147        assert!(count > 50 && count < 200, "Sphere voxel count {} seems wrong", count);
148    }
149
150    #[test]
151    fn test_sphere_mask_non_cubic() {
152        let mask = create_sphere_mask(&grid(20, 10, 5), 10.0, 5.0, 2.5, 2.0);
153        assert_eq!(mask.len(), 1000);
154
155        // Center should be inside
156        assert_eq!(mask[10 + 5 * 20 + 2 * 20 * 10], 1);
157    }
158
159    #[test]
160    fn test_sphere_mask_zero_radius() {
161        let mask = create_sphere_mask(&grid(5, 5, 5), 2.0, 2.0, 2.0, 0.0);
162        // Only the exact center voxel (distance 0 <= 0)
163        let count: usize = mask.iter().map(|&m| m as usize).sum();
164        assert_eq!(count, 1);
165    }
166
167    #[test]
168    fn test_apply_mask_zero() {
169        let mask = vec![1, 0, 1, 0, 1];
170        let mut data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
171        apply_mask_zero(&mut data, &mask);
172        assert_eq!(data, vec![1.0, 0.0, 3.0, 0.0, 5.0]);
173    }
174
175    #[test]
176    fn test_erode_mask_cube() {
177        // 3x3x3 all-ones mask: erosion should remove everything (all on boundary)
178        let mask = vec![1u8; 27];
179        let result = erode_mask(&mask, &grid(3, 3, 3), 1);
180        assert_eq!(result.iter().filter(|&&v| v == 1).count(), 1); // only center
181    }
182
183    #[test]
184    fn test_dilate_mask_single_voxel() {
185        // Single voxel in center of 5x5x5: dilation should add 6 neighbors
186        let mut mask = vec![0u8; 125];
187        mask[2 + 2 * 5 + 2 * 25] = 1; // center
188        let result = dilate_mask(&mask, &grid(5, 5, 5), 1);
189        assert_eq!(result.iter().filter(|&&v| v == 1).count(), 7); // center + 6 neighbors
190    }
191}