1use std::collections::HashMap;
20use std::f64::consts::PI;
21use delaunator::{triangulate, Point};
22use crate::Grid;
23
24pub struct CurvatureResult {
26 pub gaussian_curvature: Vec<f64>,
28 pub mean_curvature: Vec<f64>,
30 pub surface_indices: Vec<usize>,
32}
33
34#[derive(Clone, Copy, Debug)]
36struct Point3D {
37 x: f64,
38 y: f64,
39 z: f64,
40}
41
42impl Point3D {
43 fn new(x: f64, y: f64, z: f64) -> Self {
44 Self { x, y, z }
45 }
46
47 fn sub(&self, other: &Point3D) -> Point3D {
48 Point3D::new(self.x - other.x, self.y - other.y, self.z - other.z)
49 }
50
51 fn dot(&self, other: &Point3D) -> f64 {
52 self.x * other.x + self.y * other.y + self.z * other.z
53 }
54
55 fn cross(&self, other: &Point3D) -> Point3D {
56 Point3D::new(
57 self.y * other.z - self.z * other.y,
58 self.z * other.x - self.x * other.z,
59 self.x * other.y - self.y * other.x,
60 )
61 }
62
63 fn norm(&self) -> f64 {
64 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
65 }
66
67 fn normalize(&self) -> Point3D {
68 let n = self.norm();
69 if n > 1e-10 {
70 Point3D::new(self.x / n, self.y / n, self.z / n)
71 } else {
72 Point3D::new(0.0, 0.0, 0.0)
73 }
74 }
75
76 fn scale(&self, s: f64) -> Point3D {
77 Point3D::new(self.x * s, self.y * s, self.z * s)
78 }
79
80 fn add(&self, other: &Point3D) -> Point3D {
81 Point3D::new(self.x + other.x, self.y + other.y, self.z + other.z)
82 }
83}
84
85#[derive(Clone, Copy, Debug)]
87struct Triangle {
88 v0: usize,
89 v1: usize,
90 v2: usize,
91}
92
93fn extract_surface_voxels(
98 mask: &[u8],
99 nx: usize, ny: usize, nz: usize,
100) -> Vec<usize> {
101 let eroded = erode_mask(mask, nx, ny, nz, 1);
102
103 let mut surface = Vec::new();
104 for i in 0..mask.len() {
105 if mask[i] != 0 && eroded[i] == 0 {
106 surface.push(i);
107 }
108 }
109
110 surface
111}
112
113fn triangulate_surface(
123 points: &[Point3D],
124) -> (Vec<Triangle>, Vec<bool>) {
125 if points.len() < 3 {
126 return (Vec::new(), vec![false; points.len()]);
127 }
128
129 let coords: Vec<Point> = points.iter()
131 .map(|p| Point { x: p.x, y: p.y })
132 .collect();
133
134 let result = triangulate(&coords);
136
137 let mut boundary = vec![false; points.len()];
139 for &idx in &result.hull {
140 boundary[idx] = true;
141 }
142
143 let mut triangles = Vec::with_capacity(result.triangles.len() / 3);
145 for i in (0..result.triangles.len()).step_by(3) {
146 triangles.push(Triangle {
147 v0: result.triangles[i],
148 v1: result.triangles[i + 1],
149 v2: result.triangles[i + 2],
150 });
151 }
152
153 (triangles, boundary)
154}
155
156fn compute_curvatures_from_mesh(
163 points: &[Point3D],
164 triangles: &[Triangle],
165 boundary: &[bool],
166) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
167 let n_points = points.len();
168 let mut gaussian_curvature = vec![0.0f64; n_points];
169 let mut mean_curvature = vec![0.0f64; n_points];
170 let mut angle_sum = vec![0.0f64; n_points];
171 let mut area_mixed = vec![0.0f64; n_points];
172 let mut mean_curv_vec = vec![Point3D::new(0.0, 0.0, 0.0); n_points];
173 let mut normal_vec = vec![Point3D::new(0.0, 0.0, 0.0); n_points];
174
175 for tri in triangles {
177 let p0 = &points[tri.v0];
178 let p1 = &points[tri.v1];
179 let p2 = &points[tri.v2];
180
181 let e01 = p1.sub(p0); let e12 = p2.sub(p1); let e20 = p0.sub(p2); let l01 = e01.norm();
187 let l12 = e12.norm();
188 let l20 = e20.norm();
189
190 if l01 < 1e-10 || l12 < 1e-10 || l20 < 1e-10 {
191 continue;
192 }
193
194 let cross = e01.cross(&e12.scale(-1.0));
196 let area = 0.5 * cross.norm();
197 if area < 1e-10 {
198 continue;
199 }
200
201 let face_normal = cross.normalize();
203
204 let cos_a0 = e01.normalize().dot(&e20.scale(-1.0).normalize());
206 let cos_a1 = e01.scale(-1.0).normalize().dot(&e12.normalize());
207 let cos_a2 = e12.scale(-1.0).normalize().dot(&e20.normalize());
208
209 let a0 = cos_a0.clamp(-1.0, 1.0).acos();
210 let a1 = cos_a1.clamp(-1.0, 1.0).acos();
211 let a2 = cos_a2.clamp(-1.0, 1.0).acos();
212
213 angle_sum[tri.v0] += a0;
215 angle_sum[tri.v1] += a1;
216 angle_sum[tri.v2] += a2;
217
218 let cot_a0 = cos_a0 / (1.0 - cos_a0 * cos_a0).sqrt().max(1e-10);
220 let cot_a1 = cos_a1 / (1.0 - cos_a1 * cos_a1).sqrt().max(1e-10);
221 let cot_a2 = cos_a2 / (1.0 - cos_a2 * cos_a2).sqrt().max(1e-10);
222
223 let obtuse_0 = a0 > PI / 2.0;
226 let obtuse_1 = a1 > PI / 2.0;
227 let obtuse_2 = a2 > PI / 2.0;
228
229 if obtuse_0 {
231 area_mixed[tri.v0] += area / 2.0;
232 } else if obtuse_1 || obtuse_2 {
233 area_mixed[tri.v0] += area / 4.0;
234 } else {
235 area_mixed[tri.v0] += (l20 * l20 * cot_a1 + l01 * l01 * cot_a2) / 8.0;
236 }
237
238 if obtuse_1 {
239 area_mixed[tri.v1] += area / 2.0;
240 } else if obtuse_0 || obtuse_2 {
241 area_mixed[tri.v1] += area / 4.0;
242 } else {
243 area_mixed[tri.v1] += (l01 * l01 * cot_a2 + l12 * l12 * cot_a0) / 8.0;
244 }
245
246 if obtuse_2 {
247 area_mixed[tri.v2] += area / 2.0;
248 } else if obtuse_0 || obtuse_1 {
249 area_mixed[tri.v2] += area / 4.0;
250 } else {
251 area_mixed[tri.v2] += (l12 * l12 * cot_a0 + l20 * l20 * cot_a1) / 8.0;
252 }
253
254 mean_curv_vec[tri.v0] = mean_curv_vec[tri.v0].add(&e01.scale(cot_a2).add(&e20.scale(-cot_a1)));
256 mean_curv_vec[tri.v1] = mean_curv_vec[tri.v1].add(&e12.scale(cot_a0).add(&e01.scale(-cot_a2)));
257 mean_curv_vec[tri.v2] = mean_curv_vec[tri.v2].add(&e20.scale(cot_a1).add(&e12.scale(-cot_a0)));
258
259 let perim = l12 + l20 + l01;
263 if perim > 1e-10 {
264 let incenter = p0.scale(l12).add(&p1.scale(l20)).add(&p2.scale(l01)).scale(1.0 / perim);
265
266 let w0 = 1.0 / p0.sub(&incenter).norm().max(1e-10);
267 let w1 = 1.0 / p1.sub(&incenter).norm().max(1e-10);
268 let w2 = 1.0 / p2.sub(&incenter).norm().max(1e-10);
269
270 normal_vec[tri.v0] = normal_vec[tri.v0].add(&face_normal.scale(w0));
271 normal_vec[tri.v1] = normal_vec[tri.v1].add(&face_normal.scale(w1));
272 normal_vec[tri.v2] = normal_vec[tri.v2].add(&face_normal.scale(w2));
273 }
274 }
275
276 for i in 0..n_points {
279 if boundary[i] {
280 continue;
282 }
283
284 if area_mixed[i] > 1e-10 {
285 gaussian_curvature[i] = (2.0 * PI - angle_sum[i]) / area_mixed[i];
287
288 let mc_vec = mean_curv_vec[i].scale(0.25 / area_mixed[i]);
290 let mc_mag = mc_vec.norm();
291
292 let n_vec = normal_vec[i].normalize();
294 let sign = if mc_vec.dot(&n_vec) < 0.0 { -1.0 } else { 1.0 };
295
296 mean_curvature[i] = sign * mc_mag;
297 }
298 }
299
300 (gaussian_curvature, mean_curvature, area_mixed)
301}
302
303pub fn calculate_curvature_proximity(
318 mask: &[u8],
319 prox1: &[f64],
320 lower_lim: f64,
321 curv_constant: f64,
322 sigma: f64,
323 grid: &Grid,
324) -> (Vec<f64>, Vec<f64>) {
325 let (nx, ny, nz) = grid.dims;
326 let n_total = nx * ny * nz;
327
328 let surface_indices = extract_surface_voxels(mask, nx, ny, nz);
330
331 if surface_indices.is_empty() {
332 return (prox1.to_vec(), vec![1.0; n_total]);
333 }
334
335 let all_points: Vec<Point3D> = surface_indices
337 .iter()
338 .map(|&idx| {
339 let i = idx % nx;
340 let j = (idx / nx) % ny;
341 let k = idx / (nx * ny);
342 Point3D::new(i as f64, j as f64, k as f64)
343 })
344 .collect();
345
346 let mut xy_to_rep: HashMap<(usize, usize), usize> = HashMap::new();
352 let mut is_representative = vec![false; all_points.len()];
353 for (idx, p) in all_points.iter().enumerate() {
354 let key = (p.x as usize, p.y as usize);
355 xy_to_rep.entry(key).or_insert_with(|| {
356 is_representative[idx] = true;
357 idx
358 });
359 }
360
361 let rep_indices: Vec<usize> = (0..all_points.len())
363 .filter(|&i| is_representative[i])
364 .collect();
365 let mut orig_to_rep = vec![0usize; all_points.len()];
366 for (new_idx, &old_idx) in rep_indices.iter().enumerate() {
367 orig_to_rep[old_idx] = new_idx;
368 }
369 let rep_points: Vec<Point3D> = rep_indices.iter().map(|&i| all_points[i].clone()).collect();
370
371 let (triangles, boundary) = triangulate_surface(&rep_points);
373
374 let (gc, _mc, _amixed) = compute_curvatures_from_mesh(&rep_points, &triangles, &boundary);
376
377 let mut curv_i = vec![1.0f64; n_total];
379
380 let max_neg_gc = gc.iter()
382 .filter(|&&v| v < 0.0)
383 .map(|&v| v.abs())
384 .fold(1.0f64, |a, b| a.max(b));
385
386 for (orig_idx, &vol_idx) in surface_indices.iter().enumerate() {
390 if !is_representative[orig_idx] {
391 continue; }
393 let rep_idx = orig_to_rep[orig_idx];
394 let g = gc[rep_idx];
395 let scaled = if g < 0.0 {
396 g / max_neg_gc * curv_constant
397 } else if g > 0.0 {
398 1.0
399 } else {
400 0.0
402 };
403 curv_i[vol_idx] = scaled;
404 }
405
406 let sigmas = [sigma, 2.0 * sigma, 2.0 * sigma];
408 let prox3 = gaussian_smooth_3d_masked(&curv_i, mask, nx, ny, nz, &sigmas);
409
410 let prox3_clamped: Vec<f64> = prox3.iter().enumerate()
412 .map(|(i, &v)| {
413 if mask[i] == 0 {
414 0.0
415 } else if v < 0.5 && v != 0.0 {
416 0.5
417 } else {
418 v
419 }
420 })
421 .collect();
422
423 let mut prox: Vec<f64> = prox1.iter()
425 .zip(prox3_clamped.iter())
426 .map(|(&p1, &p3)| p1 * p3)
427 .collect();
428
429 let surface_mask = create_surface_mask(mask, nx, ny, nz);
435 let dilated_mask = dilate_mask(mask, nx, ny, nz, 5);
436
437 let mut prox4 = vec![0.0f64; n_total];
439 for i in 0..n_total {
440 if surface_mask[i] != 0 {
441 prox4[i] = prox[i];
442 }
443 }
444 for i in 0..n_total {
446 if prox4[i] == 0.0 {
447 prox4[i] = 1.0;
448 }
449 }
450 for i in 0..n_total {
452 if dilated_mask[i] != 0 && mask[i] == 0 {
453 prox4[i] = 0.0;
454 }
455 }
456
457 let prox4_smooth = gaussian_smooth_3d_masked(&prox4, &vec![1u8; n_total], nx, ny, nz, &[5.0, 10.0, 10.0]);
459
460 for i in 0..n_total {
462 if mask[i] == 0 {
463 prox[i] = 0.0;
464 } else if prox[i] < lower_lim && prox[i] != 0.0 {
465 prox[i] = lower_lim;
466 }
467 }
468
469 for i in 0..n_total {
471 prox[i] *= prox4_smooth[i];
472 }
473
474 (prox, curv_i)
475}
476
477fn create_surface_mask(mask: &[u8], nx: usize, ny: usize, nz: usize) -> Vec<u8> {
479 let eroded = erode_mask(mask, nx, ny, nz, 1);
480 let mut surface = vec![0u8; mask.len()];
481
482 for i in 0..mask.len() {
483 if mask[i] != 0 && eroded[i] == 0 {
484 surface[i] = 1;
485 }
486 }
487
488 surface
489}
490
491fn erode_mask(mask: &[u8], nx: usize, ny: usize, nz: usize, radius: i32) -> Vec<u8> {
493 let n_total = nx * ny * nz;
494 let mut eroded = vec![0u8; n_total];
495
496 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
497
498 for k in 0..nz {
499 for j in 0..ny {
500 for i in 0..nx {
501 if mask[idx(i, j, k)] == 0 {
502 continue;
503 }
504
505 let mut all_inside = true;
506
507 'outer: for dz in -radius..=radius {
508 for dy in -radius..=radius {
509 for dx in -radius..=radius {
510 let dist2 = dx * dx + dy * dy + dz * dz;
511 if dist2 > radius * radius {
512 continue;
513 }
514
515 let ni = i as i32 + dx;
516 let nj = j as i32 + dy;
517 let nk = k as i32 + dz;
518
519 if ni < 0 || ni >= nx as i32 ||
520 nj < 0 || nj >= ny as i32 ||
521 nk < 0 || nk >= nz as i32 {
522 all_inside = false;
523 break 'outer;
524 }
525
526 if mask[idx(ni as usize, nj as usize, nk as usize)] == 0 {
527 all_inside = false;
528 break 'outer;
529 }
530 }
531 }
532 }
533
534 if all_inside {
535 eroded[idx(i, j, k)] = 1;
536 }
537 }
538 }
539 }
540
541 eroded
542}
543
544fn dilate_mask(mask: &[u8], nx: usize, ny: usize, nz: usize, radius: i32) -> Vec<u8> {
546 let n_total = nx * ny * nz;
547 let mut dilated = vec![0u8; n_total];
548
549 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
550
551 for k in 0..nz {
552 for j in 0..ny {
553 for i in 0..nx {
554 if mask[idx(i, j, k)] != 0 {
555 for dz in -radius..=radius {
557 for dy in -radius..=radius {
558 for dx in -radius..=radius {
559 let dist2 = dx * dx + dy * dy + dz * dz;
560 if dist2 > radius * radius {
561 continue;
562 }
563
564 let ni = i as i32 + dx;
565 let nj = j as i32 + dy;
566 let nk = k as i32 + dz;
567
568 if ni >= 0 && ni < nx as i32 &&
569 nj >= 0 && nj < ny as i32 &&
570 nk >= 0 && nk < nz as i32 {
571 dilated[idx(ni as usize, nj as usize, nk as usize)] = 1;
572 }
573 }
574 }
575 }
576 }
577 }
578 }
579 }
580
581 dilated
582}
583
584pub fn morphological_close(mask: &[u8], grid: &Grid, radius: i32) -> Vec<u8> {
586 let (nx, ny, nz) = grid.dims;
587 let dilated = dilate_mask(mask, nx, ny, nz, radius);
588 erode_mask(&dilated, nx, ny, nz, radius)
589}
590
591fn gaussian_smooth_3d_masked(
593 data: &[f64],
594 mask: &[u8],
595 nx: usize, ny: usize, nz: usize,
596 sigmas: &[f64; 3],
597) -> Vec<f64> {
598 let smoothed_x = convolve_1d_direction_masked(data, mask, nx, ny, nz, sigmas[0], 'x');
600 let smoothed_xy = convolve_1d_direction_masked(&smoothed_x, mask, nx, ny, nz, sigmas[1], 'y');
601 let smoothed_xyz = convolve_1d_direction_masked(&smoothed_xy, mask, nx, ny, nz, sigmas[2], 'z');
602
603 smoothed_xyz.iter()
605 .enumerate()
606 .map(|(i, &v)| if mask[i] != 0 { v } else { 0.0 })
607 .collect()
608}
609
610fn convolve_1d_direction_masked(
613 data: &[f64],
614 _mask: &[u8],
615 nx: usize, ny: usize, nz: usize,
616 sigma: f64,
617 direction: char,
618) -> Vec<f64> {
619 if sigma <= 0.0 {
620 return data.to_vec();
621 }
622
623 let n_total = nx * ny * nz;
624 let mut result = vec![0.0f64; n_total];
625
626 let kernel_radius = (2.0 * sigma).ceil() as i32;
629 let kernel_size = 2 * kernel_radius + 1;
630 let mut kernel = vec![0.0f64; kernel_size as usize];
631
632 let mut sum = 0.0;
633 for i in 0..kernel_size {
634 let x = (i - kernel_radius) as f64;
635 kernel[i as usize] = (-x * x / (2.0 * sigma * sigma)).exp();
636 sum += kernel[i as usize];
637 }
638
639 for k in kernel.iter_mut() {
641 *k /= sum;
642 }
643
644 let idx = |i: usize, j: usize, k: usize| i + j * nx + k * nx * ny;
645
646 let clamp_x = |x: i32| -> usize { x.max(0).min(nx as i32 - 1) as usize };
648 let clamp_y = |y: i32| -> usize { y.max(0).min(ny as i32 - 1) as usize };
649 let clamp_z = |z: i32| -> usize { z.max(0).min(nz as i32 - 1) as usize };
650
651 match direction {
652 'x' => {
653 for k in 0..nz {
654 for j in 0..ny {
655 for i in 0..nx {
656 let mut conv_sum = 0.0;
657
658 for ki in 0..kernel_size {
659 let offset = ki - kernel_radius;
660 let ni = clamp_x(i as i32 + offset);
661 conv_sum += data[idx(ni, j, k)] * kernel[ki as usize];
662 }
663
664 result[idx(i, j, k)] = conv_sum;
665 }
666 }
667 }
668 }
669 'y' => {
670 for k in 0..nz {
671 for j in 0..ny {
672 for i in 0..nx {
673 let mut conv_sum = 0.0;
674
675 for ki in 0..kernel_size {
676 let offset = ki - kernel_radius;
677 let nj = clamp_y(j as i32 + offset);
678 conv_sum += data[idx(i, nj, k)] * kernel[ki as usize];
679 }
680
681 result[idx(i, j, k)] = conv_sum;
682 }
683 }
684 }
685 }
686 'z' => {
687 for k in 0..nz {
688 for j in 0..ny {
689 for i in 0..nx {
690 let mut conv_sum = 0.0;
691
692 for ki in 0..kernel_size {
693 let offset = ki - kernel_radius;
694 let nk = clamp_z(k as i32 + offset);
695 conv_sum += data[idx(i, j, nk)] * kernel[ki as usize];
696 }
697
698 result[idx(i, j, k)] = conv_sum;
699 }
700 }
701 }
702 }
703 _ => panic!("Invalid convolution direction"),
704 }
705
706 result
707}
708
709pub fn calculate_gaussian_curvature(
712 mask: &[u8],
713 grid: &Grid,
714) -> CurvatureResult {
715 let (nx, ny, nz) = grid.dims;
716 let n_total = nx * ny * nz;
717
718 let surface_indices = extract_surface_voxels(mask, nx, ny, nz);
720
721 if surface_indices.is_empty() {
722 return CurvatureResult {
723 gaussian_curvature: vec![0.0; n_total],
724 mean_curvature: vec![0.0; n_total],
725 surface_indices: Vec::new(),
726 };
727 }
728
729 let all_points: Vec<Point3D> = surface_indices
731 .iter()
732 .map(|&idx| {
733 let i = idx % nx;
734 let j = (idx / nx) % ny;
735 let k = idx / (nx * ny);
736 Point3D::new(i as f64, j as f64, k as f64)
737 })
738 .collect();
739
740 let mut xy_to_rep: HashMap<(usize, usize), usize> = HashMap::new();
742 let mut is_representative = vec![false; all_points.len()];
743 for (idx, p) in all_points.iter().enumerate() {
744 let key = (p.x as usize, p.y as usize);
745 xy_to_rep.entry(key).or_insert_with(|| {
746 is_representative[idx] = true;
747 idx
748 });
749 }
750 let rep_indices: Vec<usize> = (0..all_points.len())
751 .filter(|&i| is_representative[i])
752 .collect();
753 let mut orig_to_rep = vec![0usize; all_points.len()];
754 for (new_idx, &old_idx) in rep_indices.iter().enumerate() {
755 orig_to_rep[old_idx] = new_idx;
756 }
757 let rep_points: Vec<Point3D> = rep_indices.iter().map(|&i| all_points[i].clone()).collect();
758
759 let (triangles, boundary) = triangulate_surface(&rep_points);
761
762 let (gc_points, mc_points, _amixed) = compute_curvatures_from_mesh(&rep_points, &triangles, &boundary);
764
765 let mut gaussian_curvature = vec![0.0f64; n_total];
767 let mut mean_curvature = vec![0.0f64; n_total];
768
769 for (orig_idx, &vol_idx) in surface_indices.iter().enumerate() {
770 if is_representative[orig_idx] {
771 let rep_idx = orig_to_rep[orig_idx];
772 gaussian_curvature[vol_idx] = gc_points[rep_idx];
773 mean_curvature[vol_idx] = mc_points[rep_idx];
774 }
775 }
776
777 CurvatureResult {
778 gaussian_curvature,
779 mean_curvature,
780 surface_indices,
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
789 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
790 }
791
792 #[test]
793 fn test_extract_surface_basic() {
794 let mut mask = vec![0u8; 27];
796 mask[13] = 1; let surface = extract_surface_voxels(&mask, 3, 3, 3);
799 assert_eq!(surface.len(), 1);
800 assert_eq!(surface[0], 13);
801 }
802
803 #[test]
804 fn test_erode_mask() {
805 let mask = vec![1u8; 125];
807 let eroded = erode_mask(&mask, 5, 5, 5, 1);
808
809 let count: usize = eroded.iter().map(|&v| v as usize).sum();
811 assert!(count > 0);
812 assert!(count < 125);
813 }
814
815 #[test]
816 fn test_dilate_mask() {
817 let mut mask = vec![0u8; 125];
819 mask[62] = 1; let dilated = dilate_mask(&mask, 5, 5, 5, 1);
822
823 let count: usize = dilated.iter().map(|&v| v as usize).sum();
825 assert!(count >= 7); }
827
828 fn make_sphere_mask(n: usize, radius: f64) -> Vec<u8> {
834 let center = n as f64 / 2.0;
835 let n_total = n * n * n;
836 let mut mask = vec![0u8; n_total];
837
838 for k in 0..n {
839 for j in 0..n {
840 for i in 0..n {
841 let dx = i as f64 - center;
842 let dy = j as f64 - center;
843 let dz = k as f64 - center;
844 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
845 if dist < radius {
846 mask[i + j * n + k * n * n] = 1;
847 }
848 }
849 }
850 }
851
852 mask
853 }
854
855 #[test]
860 fn test_point3d_sub() {
861 let a = Point3D::new(3.0, 4.0, 5.0);
862 let b = Point3D::new(1.0, 1.0, 1.0);
863 let c = a.sub(&b);
864 assert!((c.x - 2.0).abs() < 1e-10);
865 assert!((c.y - 3.0).abs() < 1e-10);
866 assert!((c.z - 4.0).abs() < 1e-10);
867 }
868
869 #[test]
870 fn test_point3d_dot() {
871 let a = Point3D::new(1.0, 2.0, 3.0);
872 let b = Point3D::new(4.0, 5.0, 6.0);
873 let d = a.dot(&b);
874 assert!((d - 32.0).abs() < 1e-10); }
876
877 #[test]
878 fn test_point3d_cross() {
879 let a = Point3D::new(1.0, 0.0, 0.0);
880 let b = Point3D::new(0.0, 1.0, 0.0);
881 let c = a.cross(&b);
882 assert!((c.x - 0.0).abs() < 1e-10);
883 assert!((c.y - 0.0).abs() < 1e-10);
884 assert!((c.z - 1.0).abs() < 1e-10);
885 }
886
887 #[test]
888 fn test_point3d_norm() {
889 let p = Point3D::new(3.0, 4.0, 0.0);
890 assert!((p.norm() - 5.0).abs() < 1e-10);
891 }
892
893 #[test]
894 fn test_point3d_normalize() {
895 let p = Point3D::new(0.0, 0.0, 5.0);
896 let n = p.normalize();
897 assert!((n.x - 0.0).abs() < 1e-10);
898 assert!((n.y - 0.0).abs() < 1e-10);
899 assert!((n.z - 1.0).abs() < 1e-10);
900 }
901
902 #[test]
903 fn test_point3d_normalize_zero() {
904 let p = Point3D::new(0.0, 0.0, 0.0);
905 let n = p.normalize();
906 assert!((n.x).abs() < 1e-10);
907 assert!((n.y).abs() < 1e-10);
908 assert!((n.z).abs() < 1e-10);
909 }
910
911 #[test]
912 fn test_point3d_scale_and_add() {
913 let a = Point3D::new(1.0, 2.0, 3.0);
914 let b = a.scale(2.0);
915 assert!((b.x - 2.0).abs() < 1e-10);
916 assert!((b.y - 4.0).abs() < 1e-10);
917 assert!((b.z - 6.0).abs() < 1e-10);
918
919 let c = Point3D::new(0.5, 0.5, 0.5);
920 let d = b.add(&c);
921 assert!((d.x - 2.5).abs() < 1e-10);
922 assert!((d.y - 4.5).abs() < 1e-10);
923 assert!((d.z - 6.5).abs() < 1e-10);
924 }
925
926 #[test]
931 fn test_extract_surface_sphere() {
932 let n = 10;
933 let mask = make_sphere_mask(n, 3.5);
934 let surface = extract_surface_voxels(&mask, n, n, n);
935
936 assert!(!surface.is_empty(), "Sphere should have surface voxels");
938
939 for &idx in &surface {
941 assert_eq!(mask[idx], 1, "Surface voxel should be in mask");
942 }
943
944 let mask_count: usize = mask.iter().map(|&v| v as usize).sum();
946 assert!(
947 surface.len() < mask_count,
948 "Surface ({}) should be smaller than total mask ({})",
949 surface.len(),
950 mask_count
951 );
952 }
953
954 #[test]
955 fn test_extract_surface_empty_mask() {
956 let mask = vec![0u8; 27];
957 let surface = extract_surface_voxels(&mask, 3, 3, 3);
958 assert!(surface.is_empty(), "Empty mask should have no surface voxels");
959 }
960
961 #[test]
966 fn test_erode_mask_sphere() {
967 let n = 10;
968 let mask = make_sphere_mask(n, 4.0);
969 let eroded = erode_mask(&mask, n, n, n, 1);
970
971 let orig_count: usize = mask.iter().map(|&v| v as usize).sum();
972 let eroded_count: usize = eroded.iter().map(|&v| v as usize).sum();
973 assert!(
974 eroded_count < orig_count,
975 "Eroded sphere should be smaller: {} < {}",
976 eroded_count,
977 orig_count
978 );
979 assert!(eroded_count > 0, "Eroded sphere should not be empty");
980
981 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
983 assert_eq!(eroded[center], 1, "Center should survive erosion");
984 }
985
986 #[test]
987 fn test_erode_mask_single_voxel() {
988 let mut mask = vec![0u8; 125];
990 mask[62] = 1; let eroded = erode_mask(&mask, 5, 5, 5, 1);
992 let count: usize = eroded.iter().map(|&v| v as usize).sum();
993 assert_eq!(count, 0, "Single voxel should be fully eroded");
994 }
995
996 #[test]
1001 fn test_dilate_mask_sphere() {
1002 let n = 10;
1003 let mask = make_sphere_mask(n, 3.0);
1004 let dilated = dilate_mask(&mask, n, n, n, 1);
1005
1006 let orig_count: usize = mask.iter().map(|&v| v as usize).sum();
1007 let dilated_count: usize = dilated.iter().map(|&v| v as usize).sum();
1008 assert!(
1009 dilated_count > orig_count,
1010 "Dilated sphere should be larger: {} > {}",
1011 dilated_count,
1012 orig_count
1013 );
1014 }
1015
1016 #[test]
1017 fn test_dilate_mask_radius_2() {
1018 let mut mask = vec![0u8; 125];
1019 mask[62] = 1; let dilated = dilate_mask(&mask, 5, 5, 5, 2);
1021 let count: usize = dilated.iter().map(|&v| v as usize).sum();
1022 assert!(count > 7, "Radius-2 dilation should produce more than 7 voxels, got {}", count);
1024 }
1025
1026 #[test]
1031 fn test_morphological_close_fills_small_gaps() {
1032 let n = 10;
1033 let mut mask = make_sphere_mask(n, 4.0);
1034 let surface = extract_surface_voxels(&mask, n, n, n);
1036 if !surface.is_empty() {
1037 mask[surface[0]] = 0;
1038 }
1039
1040 let closed = morphological_close(&mask, &grid(n, n, n), 1);
1041 let orig_count: usize = mask.iter().map(|&v| v as usize).sum();
1042 let closed_count: usize = closed.iter().map(|&v| v as usize).sum();
1043 assert!(
1045 closed_count >= orig_count,
1046 "Closing should not reduce mask size: {} vs {}",
1047 closed_count,
1048 orig_count
1049 );
1050 }
1051
1052 #[test]
1053 fn test_morphological_close_empty() {
1054 let mask = vec![0u8; 27];
1055 let closed = morphological_close(&mask, &grid(3, 3, 3), 1);
1056 let count: usize = closed.iter().map(|&v| v as usize).sum();
1057 assert_eq!(count, 0, "Closing empty mask should stay empty");
1058 }
1059
1060 #[test]
1065 fn test_create_surface_mask_sphere() {
1066 let n = 10;
1067 let mask = make_sphere_mask(n, 4.0);
1068 let surface = create_surface_mask(&mask, n, n, n);
1069 let surface_count: usize = surface.iter().map(|&v| v as usize).sum();
1070 let mask_count: usize = mask.iter().map(|&v| v as usize).sum();
1071
1072 assert!(surface_count > 0, "Surface mask should be non-empty");
1073 assert!(
1074 surface_count < mask_count,
1075 "Surface ({}) should be smaller than mask ({})",
1076 surface_count,
1077 mask_count
1078 );
1079
1080 for i in 0..surface.len() {
1082 if surface[i] > 0 {
1083 assert_eq!(mask[i], 1, "Surface voxel should be in original mask");
1084 }
1085 }
1086 }
1087
1088 #[test]
1093 fn test_triangulate_surface_few_points() {
1094 let points = vec![Point3D::new(0.0, 0.0, 0.0), Point3D::new(1.0, 1.0, 1.0)];
1096 let (triangles, boundary) = triangulate_surface(&points);
1097 assert!(triangles.is_empty(), "Less than 3 points should give no triangles");
1098 assert_eq!(boundary.len(), 2);
1099 }
1100
1101 #[test]
1102 fn test_triangulate_surface_square_points() {
1103 let points = vec![
1105 Point3D::new(0.0, 0.0, 0.0),
1106 Point3D::new(1.0, 0.0, 0.0),
1107 Point3D::new(0.0, 1.0, 0.0),
1108 Point3D::new(1.0, 1.0, 0.0),
1109 ];
1110 let (triangles, boundary) = triangulate_surface(&points);
1111 assert_eq!(triangles.len(), 2, "4 points should produce 2 triangles");
1113 for &b in &boundary {
1115 assert!(b, "All 4 points should be on boundary");
1116 }
1117 }
1118
1119 #[test]
1124 fn test_compute_curvatures_from_mesh_flat_surface() {
1125 let points = vec![
1127 Point3D::new(0.0, 0.0, 0.0),
1128 Point3D::new(1.0, 0.0, 0.0),
1129 Point3D::new(2.0, 0.0, 0.0),
1130 Point3D::new(0.0, 1.0, 0.0),
1131 Point3D::new(1.0, 1.0, 0.0),
1132 Point3D::new(2.0, 1.0, 0.0),
1133 Point3D::new(0.0, 2.0, 0.0),
1134 Point3D::new(1.0, 2.0, 0.0),
1135 Point3D::new(2.0, 2.0, 0.0),
1136 ];
1137
1138 let triangles = vec![
1140 Triangle { v0: 0, v1: 1, v2: 4 },
1141 Triangle { v0: 0, v1: 4, v2: 3 },
1142 Triangle { v0: 1, v1: 2, v2: 5 },
1143 Triangle { v0: 1, v1: 5, v2: 4 },
1144 Triangle { v0: 3, v1: 4, v2: 7 },
1145 Triangle { v0: 3, v1: 7, v2: 6 },
1146 Triangle { v0: 4, v1: 5, v2: 8 },
1147 Triangle { v0: 4, v1: 8, v2: 7 },
1148 ];
1149
1150 let boundary = vec![true, true, true, true, false, true, true, true, true];
1152
1153 let (gc, mc, _amixed) = compute_curvatures_from_mesh(&points, &triangles, &boundary);
1154
1155 assert!(
1157 gc[4].abs() < 1e-6,
1158 "Flat surface should have ~0 Gaussian curvature, got {}",
1159 gc[4]
1160 );
1161 assert!(
1162 mc[4].abs() < 1e-6,
1163 "Flat surface should have ~0 mean curvature, got {}",
1164 mc[4]
1165 );
1166 }
1167
1168 #[test]
1169 fn test_compute_curvatures_from_mesh_degenerate_triangle() {
1170 let points = vec![
1172 Point3D::new(0.0, 0.0, 0.0),
1173 Point3D::new(1.0, 0.0, 0.0),
1174 Point3D::new(2.0, 0.0, 0.0), ];
1176 let triangles = vec![Triangle { v0: 0, v1: 1, v2: 2 }];
1177 let boundary = vec![false, false, false];
1178 let (gc, mc, _amixed) = compute_curvatures_from_mesh(&points, &triangles, &boundary);
1179 assert_eq!(gc.len(), 3);
1181 assert_eq!(mc.len(), 3);
1182 }
1183
1184 #[test]
1185 fn test_compute_curvatures_from_mesh_boundary_zero() {
1186 let points = vec![
1188 Point3D::new(0.0, 0.0, 0.0),
1189 Point3D::new(1.0, 0.0, 0.0),
1190 Point3D::new(0.5, 1.0, 1.0),
1191 ];
1192 let triangles = vec![Triangle { v0: 0, v1: 1, v2: 2 }];
1193 let boundary = vec![true, true, true]; let (gc, mc, _amixed) = compute_curvatures_from_mesh(&points, &triangles, &boundary);
1195 for i in 0..3 {
1196 assert!((gc[i]).abs() < 1e-10, "Boundary vertex GC should be 0");
1197 assert!((mc[i]).abs() < 1e-10, "Boundary vertex MC should be 0");
1198 }
1199 }
1200
1201 #[test]
1206 fn test_convolve_1d_direction_uniform() {
1207 let n = 8;
1208 let data = vec![5.0; n * n * n];
1209 let mask = vec![1u8; n * n * n];
1210
1211 let result_x = convolve_1d_direction_masked(&data, &mask, n, n, n, 1.0, 'x');
1212 let result_y = convolve_1d_direction_masked(&data, &mask, n, n, n, 1.0, 'y');
1213 let result_z = convolve_1d_direction_masked(&data, &mask, n, n, n, 1.0, 'z');
1214
1215 for &v in &result_x {
1217 assert!((v - 5.0).abs() < 0.1, "X convolution should preserve uniform data, got {}", v);
1218 }
1219 for &v in &result_y {
1220 assert!((v - 5.0).abs() < 0.1, "Y convolution should preserve uniform data, got {}", v);
1221 }
1222 for &v in &result_z {
1223 assert!((v - 5.0).abs() < 0.1, "Z convolution should preserve uniform data, got {}", v);
1224 }
1225 }
1226
1227 #[test]
1228 fn test_convolve_1d_direction_zero_sigma() {
1229 let n = 5;
1230 let data = vec![3.0; n * n * n];
1231 let mask = vec![1u8; n * n * n];
1232
1233 let result = convolve_1d_direction_masked(&data, &mask, n, n, n, 0.0, 'x');
1234 assert_eq!(result, data, "Zero sigma should return copy of input");
1235 }
1236
1237 #[test]
1242 fn test_gaussian_smooth_3d_masked_uniform() {
1243 let n = 8;
1244 let data = vec![10.0; n * n * n];
1245 let mask = vec![1u8; n * n * n];
1246 let sigmas = [1.0, 1.0, 1.0];
1247 let result = gaussian_smooth_3d_masked(&data, &mask, n, n, n, &sigmas);
1248 assert_eq!(result.len(), n * n * n);
1249 for &v in &result {
1250 assert!(v.is_finite(), "Result should be finite");
1251 assert!((v - 10.0).abs() < 1.0, "Uniform data should stay near 10.0, got {}", v);
1252 }
1253 }
1254
1255 #[test]
1256 fn test_gaussian_smooth_3d_masked_applies_mask() {
1257 let n = 8;
1258 let data = vec![10.0; n * n * n];
1259 let mut mask = vec![1u8; n * n * n];
1260 for i in 0..(n * n * n / 2) {
1262 mask[i] = 0;
1263 }
1264 let sigmas = [1.0, 1.0, 1.0];
1265 let result = gaussian_smooth_3d_masked(&data, &mask, n, n, n, &sigmas);
1266 for i in 0..result.len() {
1268 if mask[i] == 0 {
1269 assert!((result[i]).abs() < 1e-10, "Masked-out voxel should be 0, got {}", result[i]);
1270 }
1271 }
1272 }
1273
1274 #[test]
1279 fn test_calculate_gaussian_curvature_sphere() {
1280 let n = 12;
1281 let mask = make_sphere_mask(n, 4.5);
1282 let result = calculate_gaussian_curvature(&mask, &grid(n, n, n));
1283
1284 assert_eq!(result.gaussian_curvature.len(), n * n * n);
1285 assert_eq!(result.mean_curvature.len(), n * n * n);
1286 assert!(!result.surface_indices.is_empty(), "Should have surface indices");
1287
1288 for &idx in &result.surface_indices {
1290 assert!(
1291 result.gaussian_curvature[idx].is_finite(),
1292 "GC at surface index {} should be finite",
1293 idx
1294 );
1295 assert!(
1296 result.mean_curvature[idx].is_finite(),
1297 "MC at surface index {} should be finite",
1298 idx
1299 );
1300 }
1301
1302 let surface_set: std::collections::HashSet<usize> =
1304 result.surface_indices.iter().cloned().collect();
1305 for i in 0..(n * n * n) {
1306 if !surface_set.contains(&i) {
1307 assert!(
1308 (result.gaussian_curvature[i]).abs() < 1e-10,
1309 "Non-surface GC should be 0"
1310 );
1311 assert!(
1312 (result.mean_curvature[i]).abs() < 1e-10,
1313 "Non-surface MC should be 0"
1314 );
1315 }
1316 }
1317 }
1318
1319 #[test]
1320 fn test_calculate_gaussian_curvature_empty_mask() {
1321 let n = 5;
1322 let mask = vec![0u8; n * n * n];
1323 let result = calculate_gaussian_curvature(&mask, &grid(n, n, n));
1324 assert!(result.surface_indices.is_empty());
1325 assert!(result.gaussian_curvature.iter().all(|&v| v == 0.0));
1326 assert!(result.mean_curvature.iter().all(|&v| v == 0.0));
1327 }
1328
1329 #[test]
1330 fn test_calculate_gaussian_curvature_single_voxel() {
1331 let mut mask = vec![0u8; 125];
1332 mask[62] = 1; let result = calculate_gaussian_curvature(&mask, &grid(5, 5, 5));
1334 assert_eq!(result.gaussian_curvature.len(), 125);
1337 assert_eq!(result.mean_curvature.len(), 125);
1338 }
1339
1340 #[test]
1345 fn test_calculate_curvature_proximity_sphere() {
1346 let n = 12;
1347 let mask = make_sphere_mask(n, 4.5);
1348 let n_total = n * n * n;
1349
1350 let prox1: Vec<f64> = mask.iter().map(|&v| v as f64).collect();
1352
1353 let (prox, curv_i) = calculate_curvature_proximity(
1354 &mask, &prox1, 0.6, 500.0, 1.0, &grid(n, n, n),
1355 );
1356
1357 assert_eq!(prox.len(), n_total);
1358 assert_eq!(curv_i.len(), n_total);
1359
1360 for (i, &v) in prox.iter().enumerate() {
1362 assert!(v.is_finite(), "Prox at {} should be finite, got {}", i, v);
1363 }
1364
1365 for (i, &v) in curv_i.iter().enumerate() {
1367 assert!(v.is_finite(), "Curv_i at {} should be finite, got {}", i, v);
1368 }
1369 }
1370
1371 #[test]
1372 fn test_calculate_curvature_proximity_empty_surface() {
1373 let n = 5;
1374 let mask = vec![0u8; n * n * n];
1375 let n_total = n * n * n;
1376 let prox1 = vec![1.0; n_total];
1377
1378 let (prox, curv_i) = calculate_curvature_proximity(
1379 &mask, &prox1, 0.6, 500.0, 1.0, &grid(n, n, n),
1380 );
1381
1382 assert_eq!(prox.len(), n_total);
1384 assert_eq!(curv_i.len(), n_total);
1385 for &v in &curv_i {
1386 assert!((v - 1.0).abs() < 1e-10, "Empty surface should give curv_i=1.0");
1387 }
1388 }
1389
1390 #[test]
1391 fn test_calculate_curvature_proximity_respects_mask() {
1392 let n = 12;
1393 let mask = make_sphere_mask(n, 4.5);
1394 let n_total = n * n * n;
1395 let prox1: Vec<f64> = mask.iter().map(|&v| v as f64).collect();
1396
1397 let (prox, _curv_i) = calculate_curvature_proximity(
1398 &mask, &prox1, 0.6, 500.0, 1.0, &grid(n, n, n),
1399 );
1400
1401 for i in 0..n_total {
1404 assert!(prox[i].is_finite(), "Prox should be finite everywhere");
1405 }
1406 }
1407
1408 #[test]
1409 fn test_calculate_curvature_proximity_varying_params() {
1410 let n = 12;
1411 let mask = make_sphere_mask(n, 4.5);
1412 let prox1: Vec<f64> = mask.iter().map(|&v| v as f64).collect();
1413
1414 let (prox_a, _) = calculate_curvature_proximity(
1416 &mask, &prox1, 0.3, 100.0, 0.5, &grid(n, n, n),
1417 );
1418 let (prox_b, _) = calculate_curvature_proximity(
1419 &mask, &prox1, 0.9, 1000.0, 2.0, &grid(n, n, n),
1420 );
1421
1422 for &v in &prox_a {
1424 assert!(v.is_finite());
1425 }
1426 for &v in &prox_b {
1427 assert!(v.is_finite());
1428 }
1429 }
1430}