1use crate::error::{NdimageError, NdimageResult};
18use scirs2_core::ndarray::Array3;
19use std::collections::{BinaryHeap, VecDeque};
20
21#[derive(Debug, Clone, PartialEq)]
27pub struct VolumeStats {
28 pub voxel_count: usize,
30 pub surface_area_voxels: usize,
32 pub volume_mm3: f64,
34 pub centroid: (f64, f64, f64),
36}
37
38pub fn analyze_volume(binary_3d: &Array3<bool>, voxel_size: f64) -> NdimageResult<VolumeStats> {
52 let shape = binary_3d.shape();
53 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
54 if sz == 0 || sy == 0 || sx == 0 {
55 return Err(NdimageError::InvalidInput(
56 "Volume must be non-empty".to_string(),
57 ));
58 }
59
60 let mut voxel_count: usize = 0;
61 let mut surface_area_voxels: usize = 0;
62 let mut sum_z = 0.0_f64;
63 let mut sum_y = 0.0_f64;
64 let mut sum_x = 0.0_f64;
65
66 let face_offsets: [(isize, isize, isize); 6] = [
68 (-1, 0, 0),
69 (1, 0, 0),
70 (0, -1, 0),
71 (0, 1, 0),
72 (0, 0, -1),
73 (0, 0, 1),
74 ];
75
76 for iz in 0..sz {
77 for iy in 0..sy {
78 for ix in 0..sx {
79 if !binary_3d[[iz, iy, ix]] {
80 continue;
81 }
82 voxel_count += 1;
83 sum_z += iz as f64;
84 sum_y += iy as f64;
85 sum_x += ix as f64;
86
87 let mut is_surface = false;
89 for (dz, dy, dx) in &face_offsets {
90 let nz = iz as isize + dz;
91 let ny = iy as isize + dy;
92 let nx = ix as isize + dx;
93 let outside = nz < 0
94 || ny < 0
95 || nx < 0
96 || nz >= sz as isize
97 || ny >= sy as isize
98 || nx >= sx as isize;
99 let is_bg = outside || !binary_3d[[nz as usize, ny as usize, nx as usize]];
100 if is_bg {
101 is_surface = true;
102 break;
103 }
104 }
105 if is_surface {
106 surface_area_voxels += 1;
107 }
108 }
109 }
110 }
111
112 let volume_mm3 = voxel_count as f64 * voxel_size.powi(3);
113 let (cz, cy, cx) = if voxel_count > 0 {
114 (
115 sum_z / voxel_count as f64,
116 sum_y / voxel_count as f64,
117 sum_x / voxel_count as f64,
118 )
119 } else {
120 (0.0, 0.0, 0.0)
121 };
122
123 Ok(VolumeStats {
124 voxel_count,
125 surface_area_voxels,
126 volume_mm3,
127 centroid: (cz, cy, cx),
128 })
129}
130
131#[rustfmt::skip]
152const MC_EDGE_TABLE: [u16; 256] = [
153 0x000,0x109,0x203,0x30a,0x406,0x50f,0x605,0x70c,
154 0x80c,0x905,0xa0f,0xb06,0xc0a,0xd03,0xe09,0xf00,
155 0x190,0x099,0x393,0x29a,0x596,0x49f,0x795,0x69c,
156 0x99c,0x895,0xb9f,0xa96,0xd9a,0xc93,0xf99,0xe90,
157 0x230,0x339,0x033,0x13a,0x636,0x73f,0x435,0x53c,
158 0xa3c,0xb35,0x83f,0x936,0xe3a,0xf33,0xc39,0xd30,
159 0x3a0,0x2a9,0x1a3,0x0aa,0x7a6,0x6af,0x5a5,0x4ac,
160 0xbac,0xaa5,0x9af,0x8a6,0xfaa,0xea3,0xda9,0xca0,
161 0x460,0x569,0x663,0x76a,0x066,0x16f,0x265,0x36c,
162 0xc6c,0xd65,0xe6f,0xf66,0x86a,0x963,0xa69,0xb60,
163 0x5f0,0x4f9,0x7f3,0x6fa,0x1f6,0x0ff,0x3f5,0x2fc,
164 0xdfc,0xcf5,0xfff,0xef6,0x9fa,0x8f3,0xbf9,0xaf0,
165 0x650,0x759,0x453,0x55a,0x256,0x35f,0x055,0x15c,
166 0xe5c,0xf55,0xc5f,0xd56,0xa5a,0xb53,0x859,0x950,
167 0x7c0,0x6c9,0x5c3,0x4ca,0x3c6,0x2cf,0x1c5,0x0cc,
168 0xfcc,0xec5,0xdcf,0xcc6,0xbca,0xac3,0x9c9,0x8c0,
169 0x8c0,0x9c9,0xac3,0xbca,0xcc6,0xdcf,0xec5,0xfcc,
170 0x0cc,0x1c5,0x2cf,0x3c6,0x4ca,0x5c3,0x6c9,0x7c0,
171 0x950,0x859,0xb53,0xa5a,0xd56,0xc5f,0xf55,0xe5c,
172 0x15c,0x055,0x35f,0x256,0x55a,0x453,0x759,0x650,
173 0xaf0,0xbf9,0x8f3,0x9fa,0xef6,0xfff,0xcf5,0xdfc,
174 0x2fc,0x3f5,0x0ff,0x1f6,0x6fa,0x7f3,0x4f9,0x5f0,
175 0xb60,0xa69,0x963,0x86a,0xf66,0xe6f,0xd65,0xc6c,
176 0x36c,0x265,0x16f,0x066,0x76a,0x663,0x569,0x460,
177 0xca0,0xda9,0xea3,0xfaa,0x8a6,0x9af,0xaa5,0xbac,
178 0x4ac,0x5a5,0x6af,0x7a6,0x0aa,0x1a3,0x2a9,0x3a0,
179 0xd30,0xc39,0xf33,0xe3a,0x936,0x83f,0xb35,0xa3c,
180 0x53c,0x435,0x73f,0x636,0x13a,0x033,0x339,0x230,
181 0xe90,0xf99,0xc93,0xd9a,0xa96,0xb9f,0x895,0x99c,
182 0x69c,0x795,0x49f,0x596,0x29a,0x393,0x099,0x190,
183 0xf00,0xe09,0xd03,0xc0a,0xb06,0xa0f,0x905,0x80c,
184 0x70c,0x605,0x50f,0x406,0x30a,0x203,0x109,0x000,
185];
186
187#[rustfmt::skip]
188const MC_TRI_TABLE: [[i8; 16]; 256] = [
189 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
190 [0,8,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
191 [0,1,9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
192 [1,8,3,9,8,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
193 [1,2,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
194 [0,8,3,1,2,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
195 [9,2,10,0,2,9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
196 [2,8,3,2,10,8,10,9,8,-1,-1,-1,-1,-1,-1,-1],
197 [3,11,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
198 [0,11,2,8,11,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
199 [1,9,0,2,3,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
200 [1,11,2,1,9,11,9,8,11,-1,-1,-1,-1,-1,-1,-1],
201 [3,10,1,11,10,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
202 [0,10,1,0,8,10,8,11,10,-1,-1,-1,-1,-1,-1,-1],
203 [3,9,0,3,11,9,11,10,9,-1,-1,-1,-1,-1,-1,-1],
204 [9,8,10,10,8,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
205 [4,7,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
206 [4,3,0,7,3,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
207 [0,1,9,8,4,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
208 [4,1,9,4,7,1,7,3,1,-1,-1,-1,-1,-1,-1,-1],
209 [1,2,10,8,4,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
210 [3,4,7,3,0,4,1,2,10,-1,-1,-1,-1,-1,-1,-1],
211 [9,2,10,9,0,2,8,4,7,-1,-1,-1,-1,-1,-1,-1],
212 [2,10,9,2,9,7,2,7,3,7,9,4,-1,-1,-1,-1],
213 [8,4,7,3,11,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
214 [11,4,7,11,2,4,2,0,4,-1,-1,-1,-1,-1,-1,-1],
215 [9,0,1,8,4,7,2,3,11,-1,-1,-1,-1,-1,-1,-1],
216 [4,7,11,9,4,11,9,11,2,9,2,1,-1,-1,-1,-1],
217 [3,10,1,3,11,10,7,8,4,-1,-1,-1,-1,-1,-1,-1],
218 [1,11,10,1,4,11,1,0,4,7,11,4,-1,-1,-1,-1],
219 [4,7,8,9,0,11,9,11,10,11,0,3,-1,-1,-1,-1],
220 [4,7,11,4,11,9,9,11,10,-1,-1,-1,-1,-1,-1,-1],
221 [9,5,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
222 [9,5,4,0,8,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
223 [0,5,4,1,5,0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
224 [8,5,4,8,3,5,3,1,5,-1,-1,-1,-1,-1,-1,-1],
225 [1,2,10,9,5,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
226 [3,0,8,1,2,10,4,9,5,-1,-1,-1,-1,-1,-1,-1],
227 [5,2,10,5,4,2,4,0,2,-1,-1,-1,-1,-1,-1,-1],
228 [2,10,5,3,2,5,3,5,4,3,4,8,-1,-1,-1,-1],
229 [9,5,4,2,3,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
230 [0,11,2,0,8,11,4,9,5,-1,-1,-1,-1,-1,-1,-1],
231 [0,5,4,0,1,5,2,3,11,-1,-1,-1,-1,-1,-1,-1],
232 [2,1,5,2,5,8,2,8,11,4,8,5,-1,-1,-1,-1],
233 [10,3,11,10,1,3,9,5,4,-1,-1,-1,-1,-1,-1,-1],
234 [4,9,5,0,8,1,8,10,1,8,11,10,-1,-1,-1,-1],
235 [5,4,0,5,0,11,5,11,10,11,0,3,-1,-1,-1,-1],
236 [5,4,8,5,8,10,10,8,11,-1,-1,-1,-1,-1,-1,-1],
237 [9,7,8,5,7,9,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
238 [9,3,0,9,5,3,5,7,3,-1,-1,-1,-1,-1,-1,-1],
239 [0,7,8,0,1,7,1,5,7,-1,-1,-1,-1,-1,-1,-1],
240 [1,5,3,3,5,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
241 [9,7,8,9,5,7,10,1,2,-1,-1,-1,-1,-1,-1,-1],
242 [10,1,2,9,5,0,5,3,0,5,7,3,-1,-1,-1,-1],
243 [8,0,2,8,2,5,8,5,7,10,5,2,-1,-1,-1,-1],
244 [2,10,5,2,5,3,3,5,7,-1,-1,-1,-1,-1,-1,-1],
245 [7,9,5,7,8,9,3,11,2,-1,-1,-1,-1,-1,-1,-1],
246 [9,5,7,9,7,2,9,2,0,2,7,11,-1,-1,-1,-1],
247 [2,3,11,0,1,8,1,7,8,1,5,7,-1,-1,-1,-1],
248 [11,2,1,11,1,7,7,1,5,-1,-1,-1,-1,-1,-1,-1],
249 [9,5,8,8,5,7,10,1,3,10,3,11,-1,-1,-1,-1],
250 [5,7,0,5,0,9,7,11,0,1,0,10,11,10,0,-1],
251 [11,10,0,11,0,3,10,5,0,8,0,7,5,7,0,-1],
252 [11,10,5,7,11,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
253 [10,6,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
254 [0,8,3,5,10,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
255 [9,0,1,5,10,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
256 [1,8,3,1,9,8,5,10,6,-1,-1,-1,-1,-1,-1,-1],
257 [1,6,5,2,6,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
258 [1,6,5,1,2,6,3,0,8,-1,-1,-1,-1,-1,-1,-1],
259 [9,6,5,9,0,6,0,2,6,-1,-1,-1,-1,-1,-1,-1],
260 [5,9,8,5,8,2,5,2,6,3,2,8,-1,-1,-1,-1],
261 [2,3,11,10,6,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
262 [11,0,8,11,2,0,10,6,5,-1,-1,-1,-1,-1,-1,-1],
263 [0,1,9,2,3,11,5,10,6,-1,-1,-1,-1,-1,-1,-1],
264 [5,10,6,1,9,2,9,11,2,9,8,11,-1,-1,-1,-1],
265 [6,3,11,6,5,3,5,1,3,-1,-1,-1,-1,-1,-1,-1],
266 [0,8,11,0,11,5,0,5,1,5,11,6,-1,-1,-1,-1],
267 [3,11,6,0,3,6,0,6,5,0,5,9,-1,-1,-1,-1],
268 [6,5,9,6,9,11,11,9,8,-1,-1,-1,-1,-1,-1,-1],
269 [5,10,6,4,7,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
270 [4,3,0,4,7,3,6,5,10,-1,-1,-1,-1,-1,-1,-1],
271 [1,9,0,5,10,6,8,4,7,-1,-1,-1,-1,-1,-1,-1],
272 [10,6,5,1,9,7,1,7,3,7,9,4,-1,-1,-1,-1],
273 [6,1,2,6,5,1,4,7,8,-1,-1,-1,-1,-1,-1,-1],
274 [1,2,5,5,2,6,3,0,4,3,4,7,-1,-1,-1,-1],
275 [8,4,7,9,0,5,0,6,5,0,2,6,-1,-1,-1,-1],
276 [7,3,9,7,9,4,3,2,9,5,9,6,2,6,9,-1],
277 [3,11,2,7,8,4,10,6,5,-1,-1,-1,-1,-1,-1,-1],
278 [5,10,6,4,7,2,4,2,0,2,7,11,-1,-1,-1,-1],
279 [0,1,9,4,7,8,2,3,11,5,10,6,-1,-1,-1,-1],
280 [9,2,1,9,11,2,9,4,11,7,11,4,5,10,6,-1],
281 [8,4,7,3,11,5,3,5,1,5,11,6,-1,-1,-1,-1],
282 [5,1,11,5,11,6,1,0,11,7,11,4,0,4,11,-1],
283 [0,5,9,0,6,5,0,3,6,11,6,3,8,4,7,-1],
284 [6,5,9,6,9,11,4,7,9,7,11,9,-1,-1,-1,-1],
285 [10,4,9,6,4,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
286 [4,10,6,4,9,10,0,8,3,-1,-1,-1,-1,-1,-1,-1],
287 [10,0,1,10,6,0,6,4,0,-1,-1,-1,-1,-1,-1,-1],
288 [8,3,1,8,1,6,8,6,4,6,1,10,-1,-1,-1,-1],
289 [1,4,9,1,2,4,2,6,4,-1,-1,-1,-1,-1,-1,-1],
290 [3,0,8,1,2,9,2,4,9,2,6,4,-1,-1,-1,-1],
291 [0,2,4,4,2,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
292 [8,3,2,8,2,4,4,2,6,-1,-1,-1,-1,-1,-1,-1],
293 [10,4,9,10,6,4,11,2,3,-1,-1,-1,-1,-1,-1,-1],
294 [0,8,2,2,8,11,4,9,10,4,10,6,-1,-1,-1,-1],
295 [3,11,2,0,1,6,0,6,4,6,1,10,-1,-1,-1,-1],
296 [6,4,1,6,1,10,4,8,1,2,1,11,8,11,1,-1],
297 [9,6,4,9,3,6,9,1,3,11,6,3,-1,-1,-1,-1],
298 [8,11,1,8,1,0,11,6,1,9,1,4,6,4,1,-1],
299 [3,11,6,3,6,0,0,6,4,-1,-1,-1,-1,-1,-1,-1],
300 [6,4,8,11,6,8,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
301 [7,10,6,7,8,10,8,9,10,-1,-1,-1,-1,-1,-1,-1],
302 [0,7,3,0,10,7,0,9,10,6,7,10,-1,-1,-1,-1],
303 [10,6,7,1,10,7,1,7,8,1,8,0,-1,-1,-1,-1],
304 [10,6,7,10,7,1,1,7,3,-1,-1,-1,-1,-1,-1,-1],
305 [1,2,6,1,6,8,1,8,9,8,6,7,-1,-1,-1,-1],
306 [2,6,9,2,9,1,6,7,9,0,9,3,7,3,9,-1],
307 [7,8,0,7,0,6,6,0,2,-1,-1,-1,-1,-1,-1,-1],
308 [7,3,2,6,7,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
309 [2,3,11,10,6,8,10,8,9,8,6,7,-1,-1,-1,-1],
310 [2,0,7,2,7,11,0,9,7,6,7,10,9,10,7,-1],
311 [1,8,0,1,7,8,1,10,7,6,7,10,2,3,11,-1],
312 [11,2,1,11,1,7,10,6,1,6,7,1,-1,-1,-1,-1],
313 [8,9,6,8,6,7,9,1,6,11,6,3,1,3,6,-1],
314 [0,9,1,11,6,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
315 [7,8,0,7,0,6,3,11,0,11,6,0,-1,-1,-1,-1],
316 [7,11,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
317 [7,6,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
318 [3,0,8,11,7,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
319 [0,1,9,11,7,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
320 [8,1,9,8,3,1,11,7,6,-1,-1,-1,-1,-1,-1,-1],
321 [10,1,2,6,11,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
322 [1,2,10,3,0,8,6,11,7,-1,-1,-1,-1,-1,-1,-1],
323 [2,9,0,2,10,9,6,11,7,-1,-1,-1,-1,-1,-1,-1],
324 [6,11,7,2,10,3,10,8,3,10,9,8,-1,-1,-1,-1],
325 [7,2,3,6,2,7,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
326 [7,0,8,7,6,0,6,2,0,-1,-1,-1,-1,-1,-1,-1],
327 [2,7,6,2,3,7,0,1,9,-1,-1,-1,-1,-1,-1,-1],
328 [1,6,2,1,8,6,1,9,8,8,7,6,-1,-1,-1,-1],
329 [10,7,6,10,1,7,1,3,7,-1,-1,-1,-1,-1,-1,-1],
330 [10,7,6,1,7,10,1,8,7,1,0,8,-1,-1,-1,-1],
331 [0,3,7,0,7,10,0,10,9,6,10,7,-1,-1,-1,-1],
332 [7,6,10,7,10,8,8,10,9,-1,-1,-1,-1,-1,-1,-1],
333 [6,8,4,11,8,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
334 [3,6,11,3,0,6,0,4,6,-1,-1,-1,-1,-1,-1,-1],
335 [8,6,11,8,4,6,9,0,1,-1,-1,-1,-1,-1,-1,-1],
336 [9,4,6,9,6,3,9,3,1,11,3,6,-1,-1,-1,-1],
337 [6,8,4,6,11,8,2,10,1,-1,-1,-1,-1,-1,-1,-1],
338 [1,2,10,3,0,11,0,6,11,0,4,6,-1,-1,-1,-1],
339 [4,11,8,4,6,11,0,2,9,2,10,9,-1,-1,-1,-1],
340 [10,9,3,10,3,2,9,4,3,11,3,6,4,6,3,-1],
341 [8,2,3,8,4,2,4,6,2,-1,-1,-1,-1,-1,-1,-1],
342 [0,4,2,4,6,2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
343 [1,9,0,2,3,4,2,4,6,4,3,8,-1,-1,-1,-1],
344 [1,9,4,1,4,2,2,4,6,-1,-1,-1,-1,-1,-1,-1],
345 [8,1,3,8,6,1,8,4,6,6,10,1,-1,-1,-1,-1],
346 [10,1,0,10,0,6,6,0,4,-1,-1,-1,-1,-1,-1,-1],
347 [4,6,3,4,3,8,6,10,3,0,3,9,10,9,3,-1],
348 [10,9,4,6,10,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
349 [4,9,5,7,6,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
350 [0,8,3,4,9,5,11,7,6,-1,-1,-1,-1,-1,-1,-1],
351 [5,0,1,5,4,0,7,6,11,-1,-1,-1,-1,-1,-1,-1],
352 [11,7,6,8,3,4,3,5,4,3,1,5,-1,-1,-1,-1],
353 [9,5,4,10,1,2,7,6,11,-1,-1,-1,-1,-1,-1,-1],
354 [6,11,7,1,2,10,0,8,3,4,9,5,-1,-1,-1,-1],
355 [7,6,11,5,4,10,4,2,10,4,0,2,-1,-1,-1,-1],
356 [3,4,8,3,5,4,3,2,5,10,5,2,11,7,6,-1],
357 [7,2,3,7,6,2,5,4,9,-1,-1,-1,-1,-1,-1,-1],
358 [9,5,4,0,8,6,0,6,2,6,8,7,-1,-1,-1,-1],
359 [3,6,2,3,7,6,1,5,0,5,4,0,-1,-1,-1,-1],
360 [6,2,8,6,8,7,2,1,8,4,8,5,1,5,8,-1],
361 [9,5,4,10,1,6,1,7,6,1,3,7,-1,-1,-1,-1],
362 [1,6,10,1,7,6,1,0,7,8,7,0,9,5,4,-1],
363 [4,0,10,4,10,5,0,3,10,6,10,7,3,7,10,-1],
364 [7,6,10,7,10,8,5,4,10,4,8,10,-1,-1,-1,-1],
365 [6,9,5,6,11,9,11,8,9,-1,-1,-1,-1,-1,-1,-1],
366 [3,6,11,0,6,3,0,5,6,0,9,5,-1,-1,-1,-1],
367 [0,11,8,0,5,11,0,1,5,5,6,11,-1,-1,-1,-1],
368 [6,11,3,6,3,5,5,3,1,-1,-1,-1,-1,-1,-1,-1],
369 [1,2,10,9,5,11,9,11,8,11,5,6,-1,-1,-1,-1],
370 [0,11,3,0,6,11,0,9,6,5,6,9,1,2,10,-1],
371 [11,8,5,11,5,6,8,0,5,10,5,2,0,2,5,-1],
372 [6,11,3,6,3,5,2,10,3,10,5,3,-1,-1,-1,-1],
373 [5,8,9,5,2,8,5,6,2,3,8,2,-1,-1,-1,-1],
374 [9,5,6,9,6,0,0,6,2,-1,-1,-1,-1,-1,-1,-1],
375 [1,5,8,1,8,0,5,6,8,3,8,2,6,2,8,-1],
376 [1,5,6,2,1,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
377 [1,3,6,1,6,10,3,8,6,5,6,9,8,9,6,-1],
378 [10,1,0,10,0,6,9,5,0,5,6,0,-1,-1,-1,-1],
379 [0,3,8,5,6,10,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
380 [10,5,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
381 [11,5,10,7,5,11,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
382 [11,5,10,11,7,5,8,3,0,-1,-1,-1,-1,-1,-1,-1],
383 [5,11,7,5,10,11,1,9,0,-1,-1,-1,-1,-1,-1,-1],
384 [10,7,5,10,11,7,9,8,1,8,3,1,-1,-1,-1,-1],
385 [11,1,2,11,7,1,7,5,1,-1,-1,-1,-1,-1,-1,-1],
386 [0,8,3,1,2,7,1,7,5,7,2,11,-1,-1,-1,-1],
387 [9,7,5,9,2,7,9,0,2,2,11,7,-1,-1,-1,-1],
388 [7,5,2,7,2,11,5,9,2,3,2,8,9,8,2,-1],
389 [2,5,10,2,3,5,3,7,5,-1,-1,-1,-1,-1,-1,-1],
390 [8,2,0,8,5,2,8,7,5,10,2,5,-1,-1,-1,-1],
391 [9,0,1,2,3,10,3,5,10,3,7,5,-1,-1,-1,-1],
392 [1,2,10,9,8,5,8,7,5,8,3,7, -1,-1,-1,-1],
393 [5,3,7,5,1,3,1,2,3,-1,-1,-1,-1,-1,-1,-1],
394 [5,1,7,7,1,3,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
395 [5,9,7,9,0,7,0,3,7,-1,-1,-1,-1,-1,-1,-1],
396 [0,7,5,0,5,9,7,8,5,1,5,3,8,3,5,-1], [5,0,2,5,2,7,5,7,9,7,2,11,-1,-1,-1,-1],
398 [9,8,2,9,2,1,8,7,2,10,2,5,7,5,2,-1],
399 [2,3,11,0,1,10,0,10,5,0,5,9, -1,-1,-1,-1], [2,3,11,8,1,10,8,10,9,8,0,1,-1,-1,-1,-1], [11,7,6,10,5,4,10,4,2,4,5,3,-1,-1,-1,-1],
402 [11,7,6,10,5,0,10,0,2,0,5,4,-1,-1,-1,-1],
403 [3,11,7,3,7,0,0,7,5,0,5,9,-1,-1,-1,-1], [9,5,7,9,7,8,5,10,7,11,7,6,10,6,7,-1], [0,8,3,5,10,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [4,10,6,4,9,10,8,3,0,-1,-1,-1,-1,-1,-1,-1], [10,6,5,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [11,7,6,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1], [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
410 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
411 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
412 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
413 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
414 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
415 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
416 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
417 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
418 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
419 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
420 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
421 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
422 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
423 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
424 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
425 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
426 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
427 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
428 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
429 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
430 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
431 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
432 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
433 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
434 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
435 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
436 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
437 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
438 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
439 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
440 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
441 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
442 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
443 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
444 [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
445];
446
447const CUBE_VERTICES: [(usize, usize, usize); 8] = [
449 (0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0), (1, 0, 0), (1, 0, 1), (1, 1, 1), (1, 1, 0), ];
458
459const CUBE_EDGES: [(usize, usize); 12] = [
461 (0, 1),
462 (1, 2),
463 (2, 3),
464 (3, 0),
465 (4, 5),
466 (5, 6),
467 (6, 7),
468 (7, 4),
469 (0, 4),
470 (1, 5),
471 (2, 6),
472 (3, 7),
473];
474
475#[inline]
477fn interp_vertex(
478 v0: (f64, f64, f64),
479 v1: (f64, f64, f64),
480 val0: f64,
481 val1: f64,
482 threshold: f64,
483) -> (f64, f64, f64) {
484 let dv = val1 - val0;
485 let t = if dv.abs() < 1e-12 {
486 0.5
487 } else {
488 (threshold - val0) / dv
489 };
490 (
491 v0.0 + t * (v1.0 - v0.0),
492 v0.1 + t * (v1.1 - v0.1),
493 v0.2 + t * (v1.2 - v0.2),
494 )
495}
496
497pub fn marching_cubes_simplified(
513 volume: &Array3<f64>,
514 threshold: f64,
515) -> NdimageResult<Vec<[f64; 9]>> {
516 let shape = volume.shape();
517 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
518 if sz < 2 || sy < 2 || sx < 2 {
519 return Err(NdimageError::InvalidInput(
520 "Volume must be at least 2×2×2 for marching cubes".to_string(),
521 ));
522 }
523
524 let mut triangles: Vec<[f64; 9]> = Vec::new();
525
526 for iz in 0..sz - 1 {
527 for iy in 0..sy - 1 {
528 for ix in 0..sx - 1 {
529 let mut vals = [0.0_f64; 8];
531 let mut positions = [(0.0_f64, 0.0_f64, 0.0_f64); 8];
532 let mut cube_idx: usize = 0;
533
534 for (vi, (dz, dy, dx)) in CUBE_VERTICES.iter().enumerate() {
535 let z = iz + dz;
536 let y = iy + dy;
537 let x = ix + dx;
538 vals[vi] = volume[[z, y, x]];
539 positions[vi] = (z as f64, y as f64, x as f64);
540 if vals[vi] >= threshold {
541 cube_idx |= 1 << vi;
542 }
543 }
544
545 let edge_mask = MC_EDGE_TABLE[cube_idx];
546 if edge_mask == 0 {
547 continue;
548 }
549
550 let mut edge_pts = [(0.0_f64, 0.0_f64, 0.0_f64); 12];
552 for (ei, &(va, vb)) in CUBE_EDGES.iter().enumerate() {
553 if edge_mask & (1 << ei) != 0 {
554 edge_pts[ei] = interp_vertex(
555 positions[va],
556 positions[vb],
557 vals[va],
558 vals[vb],
559 threshold,
560 );
561 }
562 }
563
564 let tri_row = &MC_TRI_TABLE[cube_idx];
566 let mut ti = 0;
567 while ti < 15 {
568 let e0 = tri_row[ti];
569 let e1 = tri_row[ti + 1];
570 let e2 = tri_row[ti + 2];
571 if e0 < 0 {
572 break;
573 }
574 let p0 = edge_pts[e0 as usize];
575 let p1 = edge_pts[e1 as usize];
576 let p2 = edge_pts[e2 as usize];
577 triangles.push([p0.0, p0.1, p0.2, p1.0, p1.1, p1.2, p2.0, p2.1, p2.2]);
578 ti += 3;
579 }
580 }
581 }
582 }
583
584 Ok(triangles)
585}
586
587pub fn isosurface_area(
604 volume: &Array3<f64>,
605 threshold: f64,
606 voxel_size: f64,
607) -> NdimageResult<f64> {
608 let triangles = marching_cubes_simplified(volume, threshold)?;
609 let mut total_area = 0.0_f64;
610 for tri in &triangles {
611 let (az, ay, ax) = (tri[0], tri[1], tri[2]);
613 let (bz, by, bx) = (tri[3], tri[4], tri[5]);
614 let (cz, cy, cx) = (tri[6], tri[7], tri[8]);
615 let (uz, uy, ux) = (bz - az, by - ay, bx - ax);
617 let (vz, vy, vx) = (cz - az, cy - ay, cx - ax);
618 let cross_z = uy * vx - ux * vy;
620 let cross_y = ux * vz - uz * vx;
621 let cross_x = uz * vy - uy * vz;
622 let area = 0.5 * (cross_z * cross_z + cross_y * cross_y + cross_x * cross_x).sqrt();
623 total_area += area;
624 }
625 Ok(total_area * voxel_size * voxel_size)
626}
627
628pub fn connected_components_3d(binary: &Array3<bool>) -> NdimageResult<Array3<usize>> {
645 let shape = binary.shape();
646 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
647 if sz == 0 || sy == 0 || sx == 0 {
648 return Err(NdimageError::InvalidInput(
649 "Volume must be non-empty".to_string(),
650 ));
651 }
652
653 let mut labels = Array3::<usize>::zeros((sz, sy, sx));
654 let mut next_label: usize = 1;
655
656 let neighbours: [(isize, isize, isize); 6] = [
658 (-1, 0, 0),
659 (1, 0, 0),
660 (0, -1, 0),
661 (0, 1, 0),
662 (0, 0, -1),
663 (0, 0, 1),
664 ];
665
666 let mut queue: VecDeque<(usize, usize, usize)> = VecDeque::new();
667
668 for iz in 0..sz {
669 for iy in 0..sy {
670 for ix in 0..sx {
671 if !binary[[iz, iy, ix]] || labels[[iz, iy, ix]] != 0 {
672 continue;
673 }
674 labels[[iz, iy, ix]] = next_label;
676 queue.clear();
677 queue.push_back((iz, iy, ix));
678 while let Some((cz, cy, cx)) = queue.pop_front() {
679 for (dz, dy, dx) in &neighbours {
680 let nz = cz as isize + dz;
681 let ny = cy as isize + dy;
682 let nx = cx as isize + dx;
683 if nz < 0
684 || ny < 0
685 || nx < 0
686 || nz >= sz as isize
687 || ny >= sy as isize
688 || nx >= sx as isize
689 {
690 continue;
691 }
692 let (nzu, nyu, nxu) = (nz as usize, ny as usize, nx as usize);
693 if binary[[nzu, nyu, nxu]] && labels[[nzu, nyu, nxu]] == 0 {
694 labels[[nzu, nyu, nxu]] = next_label;
695 queue.push_back((nzu, nyu, nxu));
696 }
697 }
698 }
699 next_label += 1;
700 }
701 }
702 }
703
704 Ok(labels)
705}
706
707fn sphere_offsets(radius: f64) -> Vec<(isize, isize, isize)> {
713 let r = radius.ceil() as isize;
714 let r2 = radius * radius;
715 let mut offsets = Vec::new();
716 for dz in -r..=r {
717 for dy in -r..=r {
718 for dx in -r..=r {
719 let d2 = (dz * dz + dy * dy + dx * dx) as f64;
720 if d2 <= r2 + 1e-9 {
721 offsets.push((dz, dy, dx));
722 }
723 }
724 }
725 }
726 offsets
727}
728
729pub fn dilation_3d(binary: &Array3<bool>, radius: f64) -> NdimageResult<Array3<bool>> {
747 let shape = binary.shape();
748 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
749 if sz == 0 || sy == 0 || sx == 0 {
750 return Err(NdimageError::InvalidInput(
751 "Volume must be non-empty".to_string(),
752 ));
753 }
754
755 let offsets = sphere_offsets(radius);
756 let mut out = Array3::from_elem((sz, sy, sx), false);
757
758 for iz in 0..sz {
759 for iy in 0..sy {
760 for ix in 0..sx {
761 if !binary[[iz, iy, ix]] {
762 continue;
763 }
764 for (dz, dy, dx) in &offsets {
766 let nz = iz as isize + dz;
767 let ny = iy as isize + dy;
768 let nx = ix as isize + dx;
769 if nz >= 0
770 && ny >= 0
771 && nx >= 0
772 && nz < sz as isize
773 && ny < sy as isize
774 && nx < sx as isize
775 {
776 out[[nz as usize, ny as usize, nx as usize]] = true;
777 }
778 }
779 }
780 }
781 }
782
783 Ok(out)
784}
785
786pub fn erosion_3d(binary: &Array3<bool>, radius: f64) -> NdimageResult<Array3<bool>> {
804 let shape = binary.shape();
805 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
806 if sz == 0 || sy == 0 || sx == 0 {
807 return Err(NdimageError::InvalidInput(
808 "Volume must be non-empty".to_string(),
809 ));
810 }
811
812 let offsets = sphere_offsets(radius);
813 let mut out = Array3::from_elem((sz, sy, sx), false);
814
815 for iz in 0..sz {
816 for iy in 0..sy {
817 for ix in 0..sx {
818 if !binary[[iz, iy, ix]] {
819 continue;
820 }
821 let survives = offsets.iter().all(|(dz, dy, dx)| {
823 let nz = iz as isize + dz;
824 let ny = iy as isize + dy;
825 let nx = ix as isize + dx;
826 if nz < 0
827 || ny < 0
828 || nx < 0
829 || nz >= sz as isize
830 || ny >= sy as isize
831 || nx >= sx as isize
832 {
833 false
835 } else {
836 binary[[nz as usize, ny as usize, nx as usize]]
837 }
838 });
839 out[[iz, iy, ix]] = survives;
840 }
841 }
842 }
843
844 Ok(out)
845}
846
847#[derive(Debug)]
857struct WatershedEntry {
858 val: f64,
860 seq: u64,
862 z: usize,
863 y: usize,
864 x: usize,
865}
866
867impl PartialEq for WatershedEntry {
868 fn eq(&self, other: &Self) -> bool {
869 self.val.to_bits() == other.val.to_bits() && self.seq == other.seq
871 }
872}
873
874impl Eq for WatershedEntry {}
875
876impl PartialOrd for WatershedEntry {
877 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
878 Some(self.cmp(other))
879 }
880}
881
882impl Ord for WatershedEntry {
883 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
884 let ord_val = |v: f64| -> u64 {
888 let bits = v.to_bits();
889 if bits >> 63 == 0 {
892 bits | (1u64 << 63)
893 } else {
894 !bits
895 }
896 };
897 ord_val(other.val)
899 .cmp(&ord_val(self.val))
900 .then(other.seq.cmp(&self.seq))
901 }
902}
903
904pub fn watershed_3d(
920 gradient: &Array3<f64>,
921 seeds: &[(usize, usize, usize)],
922) -> NdimageResult<Array3<usize>> {
923 let shape = gradient.shape();
924 let (sz, sy, sx) = (shape[0], shape[1], shape[2]);
925 if sz == 0 || sy == 0 || sx == 0 {
926 return Err(NdimageError::InvalidInput(
927 "Gradient volume must be non-empty".to_string(),
928 ));
929 }
930
931 let mut labels = Array3::<usize>::zeros((sz, sy, sx));
932 let mut heap: BinaryHeap<WatershedEntry> = BinaryHeap::new();
933 let mut seq: u64 = 0;
934
935 for (label_idx, &(sz_s, sy_s, sx_s)) in seeds.iter().enumerate() {
937 if sz_s >= sz || sy_s >= sy || sx_s >= sx {
938 return Err(NdimageError::InvalidInput(format!(
939 "Seed ({sz_s},{sy_s},{sx_s}) is out of bounds ({sz},{sy},{sx})"
940 )));
941 }
942 let label = label_idx + 1;
943 if labels[[sz_s, sy_s, sx_s]] == 0 {
944 labels[[sz_s, sy_s, sx_s]] = label;
945 heap.push(WatershedEntry {
946 val: gradient[[sz_s, sy_s, sx_s]],
947 seq,
948 z: sz_s,
949 y: sy_s,
950 x: sx_s,
951 });
952 seq += 1;
953 }
954 }
955
956 let face_offsets: [(isize, isize, isize); 6] = [
957 (-1, 0, 0),
958 (1, 0, 0),
959 (0, -1, 0),
960 (0, 1, 0),
961 (0, 0, -1),
962 (0, 0, 1),
963 ];
964
965 while let Some(entry) = heap.pop() {
966 let lbl = labels[[entry.z, entry.y, entry.x]];
967 if lbl == 0 {
968 continue;
969 }
970 for (dz, dy, dx) in &face_offsets {
971 let nz = entry.z as isize + dz;
972 let ny = entry.y as isize + dy;
973 let nx = entry.x as isize + dx;
974 if nz < 0
975 || ny < 0
976 || nx < 0
977 || nz >= sz as isize
978 || ny >= sy as isize
979 || nx >= sx as isize
980 {
981 continue;
982 }
983 let (nzu, nyu, nxu) = (nz as usize, ny as usize, nx as usize);
984 if labels[[nzu, nyu, nxu]] == 0 {
985 labels[[nzu, nyu, nxu]] = lbl;
986 heap.push(WatershedEntry {
987 val: gradient[[nzu, nyu, nxu]],
988 seq,
989 z: nzu,
990 y: nyu,
991 x: nxu,
992 });
993 seq += 1;
994 }
995 }
996 }
997
998 Ok(labels)
999}
1000
1001#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use scirs2_core::ndarray::Array3;
1009
1010 #[test]
1011 fn test_analyze_volume_solid_cube() {
1012 let vol = Array3::from_elem((4, 4, 4), true);
1013 let stats = analyze_volume(&vol, 1.0).expect("analyze_volume failed");
1014 assert_eq!(stats.voxel_count, 64);
1015 assert!((stats.volume_mm3 - 64.0).abs() < 1e-9);
1016 assert!(stats.surface_area_voxels < 64);
1018 assert!((stats.centroid.0 - 1.5).abs() < 1e-9);
1019 }
1020
1021 #[test]
1022 fn test_analyze_volume_empty_mask() {
1023 let vol = Array3::from_elem((3, 3, 3), false);
1024 let stats = analyze_volume(&vol, 1.0).expect("analyze_volume failed");
1025 assert_eq!(stats.voxel_count, 0);
1026 assert_eq!(stats.volume_mm3, 0.0);
1027 }
1028
1029 #[test]
1030 fn test_connected_components_two_blobs() {
1031 let mut vol = Array3::from_elem((5, 5, 5), false);
1032 vol[[0, 0, 0]] = true;
1034 vol[[0, 0, 1]] = true;
1035 vol[[0, 1, 0]] = true;
1036 vol[[4, 4, 4]] = true;
1038 vol[[4, 4, 3]] = true;
1039
1040 let labels = connected_components_3d(&vol).expect("connected_components failed");
1041 let l_a = labels[[0, 0, 0]];
1043 let l_b = labels[[4, 4, 4]];
1044 assert_ne!(l_a, 0);
1045 assert_ne!(l_b, 0);
1046 assert_ne!(l_a, l_b);
1047 assert_eq!(labels[[0, 0, 1]], l_a);
1049 assert_eq!(labels[[0, 1, 0]], l_a);
1050 assert_eq!(labels[[4, 4, 3]], l_b);
1052 }
1053
1054 #[test]
1055 fn test_dilation_grows_volume() {
1056 let mut vol = Array3::from_elem((7, 7, 7), false);
1057 vol[[3, 3, 3]] = true;
1058 let dilated = dilation_3d(&vol, 1.5).expect("dilation failed");
1059 let count_before = 1_usize;
1060 let count_after = dilated.iter().filter(|&&v| v).count();
1061 assert!(count_after > count_before);
1062 }
1063
1064 #[test]
1065 fn test_erosion_shrinks_volume() {
1066 let vol = Array3::from_elem((5, 5, 5), true);
1067 let eroded = erosion_3d(&vol, 1.0).expect("erosion failed");
1068 let count = eroded.iter().filter(|&&v| v).count();
1069 assert!(count < 125);
1070 }
1071
1072 #[test]
1073 fn test_watershed_3d_two_seeds() {
1074 let grad = Array3::<f64>::zeros((5, 5, 5));
1075 let seeds = [(0, 0, 0), (4, 4, 4)];
1076 let labels = watershed_3d(&grad, &seeds).expect("watershed failed");
1077 assert!(labels.iter().any(|&v| v == 1));
1079 assert!(labels.iter().any(|&v| v == 2));
1080 }
1081
1082 #[test]
1083 fn test_marching_cubes_sphere() {
1084 let size = 8_usize;
1086 let center = size as f64 / 2.0;
1087 let vol = Array3::from_shape_fn((size, size, size), |(z, y, x)| {
1088 let dz = z as f64 - center;
1089 let dy = y as f64 - center;
1090 let dx = x as f64 - center;
1091 (dz * dz + dy * dy + dx * dx).sqrt() - 2.5
1092 });
1093 let tris = marching_cubes_simplified(&vol, 0.0).expect("marching_cubes failed");
1094 assert!(
1095 !tris.is_empty(),
1096 "Expected non-empty triangle list for sphere SDF"
1097 );
1098 }
1099}