1use num_complex::Complex64;
192use crate::error::InputError;
193use crate::Grid;
194use crate::fft::{fft3d, ifft3d};
195use crate::kernels::smv::{smv_kernel, sti_smv_kernel, erode_mask_smv};
196use super::sharp::{SmvBox, QSMJL_SMV_DELTA};
197
198pub const CROP_MARGIN: usize = 12;
201
202pub const STI_MIN_RADIUS_MM: f64 = 2.0;
205
206pub const STI_RADIUS_STEP_MM: f64 = 1.0;
208
209pub const STI_GRID_MM: f64 = 1.0;
211
212#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
215pub enum VsharpBall {
216 #[default]
220 StiSuite,
221 QsmJl,
224}
225
226#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
228#[derive(Clone, Copy, Debug, Default, PartialEq)]
229pub enum VsharpGrid {
230 #[default]
233 Native,
234 IsotropicFinest,
237 Isotropic {
240 spacing_mm: f64,
242 },
243}
244
245impl VsharpGrid {
246 pub const ANISOTROPY_TOLERANCE: f64 = 0.01;
250
251 pub fn resolve(self, voxel_size: (f64, f64, f64)) -> Self {
255 match self {
256 VsharpGrid::IsotropicFinest => {
257 let (a, b, c) = voxel_size;
258 let (lo, hi) = (a.min(b).min(c), a.max(b).max(c));
259 if hi > lo * (1.0 + Self::ANISOTROPY_TOLERANCE) {
260 VsharpGrid::Isotropic { spacing_mm: lo }
261 } else {
262 VsharpGrid::Native
263 }
264 }
265 g => g,
266 }
267 }
268}
269
270#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
272#[derive(Clone, Debug)]
273pub struct VsharpParams {
274 pub threshold: f64,
280 pub max_radius: f64,
283 pub min_radius: f64,
287 pub radius_step: f64,
305 pub deconvolve: bool,
315 pub ball: VsharpBall,
318 pub grid: VsharpGrid,
324}
325
326impl VsharpParams {
327 pub fn validate(&self) -> Result<(), InputError> {
331 if !(self.min_radius.is_finite() && self.min_radius > 0.0) {
332 return Err(InputError::param("V-SHARP min_radius", self.min_radius, "finite and > 0 mm"));
333 }
334 if !(self.radius_step.is_finite() && self.radius_step > 0.0) {
335 return Err(InputError::param("V-SHARP radius_step", self.radius_step, "finite and > 0 mm"));
336 }
337 if let VsharpGrid::Isotropic { spacing_mm } = self.grid {
338 if !(spacing_mm.is_finite() && spacing_mm > 0.0) {
339 return Err(InputError::param(
340 "V-SHARP isotropic working grid spacing_mm", spacing_mm, "finite and > 0 mm",
341 ));
342 }
343 }
344 Ok(())
345 }
346
347 pub fn working_grid(&self, grid: &Grid) -> VsharpGrid {
350 self.grid.resolve(grid.voxel_size)
351 }
352}
353
354impl Default for VsharpParams {
355 fn default() -> Self {
356 Self {
357 threshold: 0.05,
359 max_radius: 12.0,
360 min_radius: STI_MIN_RADIUS_MM,
361 radius_step: STI_RADIUS_STEP_MM,
362 deconvolve: false,
363 ball: VsharpBall::StiSuite,
364 grid: VsharpGrid::Isotropic { spacing_mm: STI_GRID_MM },
365 }
366 }
367}
368
369pub fn vsharp(
389 field: &[f64],
390 mask: &[u8],
391 grid: &Grid,
392 params: &VsharpParams,
393 progress: impl FnMut(usize, usize),
394) -> Result<(Vec<f64>, Vec<u8>), InputError> {
395 params.validate()?;
396 let radii = vsharp_radii(params);
397 let deconvolve = params.deconvolve.then_some(params.threshold);
398 Ok(match params.working_grid(grid) {
399 VsharpGrid::Native => vsharp_with_radii(field, mask, grid, &radii, deconvolve, params.ball, progress),
400 VsharpGrid::Isotropic { spacing_mm } => {
401 vsharp_isotropic(field, mask, grid, spacing_mm, &radii, deconvolve, params.ball, progress)
402 }
403 VsharpGrid::IsotropicFinest => unreachable!("working_grid resolves IsotropicFinest"),
404 })
405}
406
407fn vsharp_isotropic(
410 field: &[f64],
411 mask: &[u8],
412 grid: &Grid,
413 h: f64,
414 radii: &[f64],
415 deconvolve: Option<f64>,
416 ball: VsharpBall,
417 progress: impl FnMut(usize, usize),
418) -> (Vec<f64>, Vec<u8>) {
419 let (nx, ny, nz) = grid.dims;
420 let vs = [grid.voxel_size.0, grid.voxel_size.1, grid.voxel_size.2];
421 let mut local_out = vec![0.0; nx * ny * nz];
422 let mut mask_out = vec![0u8; nx * ny * nz];
423 let Some((lo, hi)) = bounding_box(mask, grid.dims) else { return (local_out, mask_out) };
424 let (lo, hi) = sti_even_box(lo, hi, [nx, ny, nz]);
425
426 let pad = CROP_MARGIN;
428 let np = [0, 1, 2].map(|d| hi[d] - lo[d] + 1 + 2 * pad);
429 let mut p = vec![0.0; np[0] * np[1] * np[2]];
430 let mut m = vec![0.0; p.len()];
431 for k in lo[2]..=hi[2] {
432 for j in lo[1]..=hi[1] {
433 for i in lo[0]..=hi[0] {
434 let src = i + j * nx + k * nx * ny;
435 let dst = (i - lo[0] + pad) + (j - lo[1] + pad) * np[0] + (k - lo[2] + pad) * np[0] * np[1];
436 p[dst] = field[src];
437 m[dst] = if mask[src] != 0 { 1.0 } else { 0.0 };
438 }
439 }
440 }
441
442 let ni = [0, 1, 2].map(|d| if vs[d] == h { np[d] } else { (np[d] as f64 * vs[d] / h).ceil() as usize });
446 let fwd = [0, 1, 2].map(|d| {
447 (1..=ni[d]).map(|q| if vs[d] == h { q as f64 } else { q as f64 * h / vs[d] }).collect::<Vec<f64>>()
448 });
449 let pi = trilinear(&p, np, &fwd);
450 let mi = trilinear_mask_f32(&m, np, &fwd);
453 let ne = ni.map(|n| n + n % 2);
454 let mut pe = vec![0.0; ne[0] * ne[1] * ne[2]];
455 let mut me = vec![0u8; pe.len()];
456 for k in 0..ni[2] {
457 for j in 0..ni[1] {
458 for i in 0..ni[0] {
459 let s = i + j * ni[0] + k * ni[0] * ni[1];
460 let d = i + j * ne[0] + k * ne[0] * ne[1];
461 pe[d] = pi[s];
462 me[d] = u8::from(mi[s] > 0.5f32);
463 }
464 }
465 }
466 drop((pi, mi));
467
468 let w_grid = Grid::new(ne[0], ne[1], ne[2], h, h, h);
470 let (local_w, support_w) = vsharp_with_radii(&pe, &me, &w_grid, radii, deconvolve, ball, progress);
471
472 let mut bg_c = vec![0.0; ni[0] * ni[1] * ni[2]];
474 let mut nm_c = vec![0.0; bg_c.len()];
475 for k in 0..ni[2] {
476 for j in 0..ni[1] {
477 for i in 0..ni[0] {
478 let d = i + j * ni[0] + k * ni[0] * ni[1];
479 let s = i + j * ne[0] + k * ne[0] * ne[1];
480 bg_c[d] = pe[s] - local_w[s];
481 nm_c[d] = f64::from(support_w[s]);
482 }
483 }
484 }
485 let back = [0, 1, 2].map(|d| {
486 (1..=np[d]).map(|a| if vs[d] == h { a as f64 } else { a as f64 * vs[d] / h }).collect::<Vec<f64>>()
487 });
488 let bg_n = trilinear(&bg_c, ni, &back);
489 let nm_n = trilinear_mask_f32(&nm_c, ni, &back);
490 for k in lo[2]..=hi[2] {
491 for j in lo[1]..=hi[1] {
492 for i in lo[0]..=hi[0] {
493 let s = (i - lo[0] + pad) + (j - lo[1] + pad) * np[0] + (k - lo[2] + pad) * np[0] * np[1];
494 if nm_n[s] > 0.5f32 {
495 let d = i + j * nx + k * nx * ny;
496 local_out[d] = p[s] - bg_n[s];
497 mask_out[d] = 1;
498 }
499 }
500 }
501 }
502 (local_out, mask_out)
503}
504
505fn sti_even_box(mut lo: [usize; 3], mut hi: [usize; 3], n: [usize; 3]) -> ([usize; 3], [usize; 3]) {
515 for d in 0..3 {
516 if (hi[d] - lo[d] + 1) % 2 == 1 {
517 if hi[d] + 1 < n[d] {
518 hi[d] += 1;
519 } else if lo[d] > 0 {
520 lo[d] -= 1;
521 } else if hi[d] > lo[d] {
522 hi[d] -= 1;
523 }
524 }
525 }
526 (lo, hi)
527}
528
529fn bounding_box(mask: &[u8], dims: (usize, usize, usize)) -> Option<([usize; 3], [usize; 3])> {
531 let (nx, ny, nz) = dims;
532 let mut lo = [usize::MAX; 3];
533 let mut hi = [0usize; 3];
534 let mut any = false;
535 for k in 0..nz {
536 for j in 0..ny {
537 for i in 0..nx {
538 if mask[i + j * nx + k * nx * ny] != 0 {
539 any = true;
540 for (d, v) in [i, j, k].into_iter().enumerate() {
541 lo[d] = lo[d].min(v);
542 hi[d] = hi[d].max(v);
543 }
544 }
545 }
546 }
547 }
548 any.then_some((lo, hi))
549}
550
551fn trilinear(src: &[f64], n: [usize; 3], coords: &[Vec<f64>; 3]) -> Vec<f64> {
555 let w = |d: usize| -> Vec<Option<(usize, f64)>> {
556 coords[d]
557 .iter()
558 .map(|&u| {
559 if !(1.0..=n[d] as f64).contains(&u) {
560 return None;
561 }
562 let t = u - 1.0;
563 let i0 = (t.floor() as usize).min(n[d].saturating_sub(2));
564 Some((i0, t - i0 as f64))
565 })
566 .collect()
567 };
568 let (wx, wy, wz) = (w(0), w(1), w(2));
569 let (ox, oy) = (wx.len(), wy.len());
570 let at = |i: usize, j: usize, k: usize| -> f64 {
571 if i < n[0] && j < n[1] && k < n[2] { src[i + j * n[0] + k * n[0] * n[1]] } else { 0.0 }
572 };
573 let mut out = vec![0.0; ox * oy * wz.len()];
574 for (c, wzk) in wz.iter().enumerate() {
575 let Some((k0, fz)) = *wzk else { continue };
576 for (b, wyj) in wy.iter().enumerate() {
577 let Some((j0, fy)) = *wyj else { continue };
578 for (a, wxi) in wx.iter().enumerate() {
579 let Some((i0, fx)) = *wxi else { continue };
580 let mut v = 0.0;
581 for (dk, gz) in [(0, 1.0 - fz), (1, fz)] {
582 if gz == 0.0 { continue; }
583 for (dj, gy) in [(0, 1.0 - fy), (1, fy)] {
584 if gy == 0.0 { continue; }
585 for (di, gx) in [(0, 1.0 - fx), (1, fx)] {
586 if gx == 0.0 { continue; }
587 v += gx * gy * gz * at(i0 + di, j0 + dj, k0 + dk);
588 }
589 }
590 }
591 out[a + b * ox + c * ox * oy] = v;
592 }
593 }
594 }
595 out
596}
597
598fn trilinear_mask_f32(src: &[f64], n: [usize; 3], coords: &[Vec<f64>; 3]) -> Vec<f32> {
609 let w = |d: usize| -> Vec<Option<(usize, f32)>> {
610 coords[d]
611 .iter()
612 .map(|&u| {
613 if !(1.0..=n[d] as f64).contains(&u) {
614 return None;
615 }
616 let t = u - 1.0;
617 let i0 = (t.floor() as usize).min(n[d].saturating_sub(2));
618 Some((i0, (t - i0 as f64) as f32))
619 })
620 .collect()
621 };
622 let (wx, wy, wz) = (w(0), w(1), w(2));
623 let (ox, oy) = (wx.len(), wy.len());
624 let at = |i: usize, j: usize, k: usize| -> f32 {
625 if i < n[0] && j < n[1] && k < n[2] { src[i + j * n[0] + k * n[0] * n[1]] as f32 } else { 0.0 }
626 };
627 let lerp = |v0: f32, v1: f32, f: f32| (1.0 - f) * v0 + f * v1;
628 let mut out = vec![0.0f32; ox * oy * wz.len()];
629 for (c, wzk) in wz.iter().enumerate() {
630 let Some((k0, fz)) = *wzk else { continue };
631 for (b, wyj) in wy.iter().enumerate() {
632 let Some((j0, fy)) = *wyj else { continue };
633 for (a, wxi) in wx.iter().enumerate() {
634 let Some((i0, fx)) = *wxi else { continue };
635 let plane = |k: usize| {
637 let r0 = lerp(at(i0, j0, k), at(i0, j0 + 1, k), fy);
638 let r1 = lerp(at(i0 + 1, j0, k), at(i0 + 1, j0 + 1, k), fy);
639 lerp(r0, r1, fx)
640 };
641 out[a + b * ox + c * ox * oy] = lerp(plane(k0), plane(k0 + 1), fz);
642 }
643 }
644 }
645 out
646}
647
648fn vsharp_radii(params: &VsharpParams) -> Vec<f64> {
653 let mut radii = Vec::new();
654 let step = params.radius_step;
655 let mut k = 0usize;
656 loop {
657 let r = params.max_radius - k as f64 * step;
658 if r < params.min_radius - 1e-9 {
659 break;
660 }
661 radii.push(r);
662 k += 1;
663 }
664 if radii.is_empty() {
665 radii.push(params.max_radius);
666 }
667 radii
668}
669
670#[allow(clippy::too_many_arguments)]
681pub(crate) fn vsharp_with_radii(
682 field: &[f64],
683 mask: &[u8],
684 grid: &Grid,
685 radii: &[f64],
686 deconvolve: Option<f64>,
687 ball: VsharpBall,
688 mut progress: impl FnMut(usize, usize),
689) -> (Vec<f64>, Vec<u8>) {
690 let (nx, ny, nz) = grid.dims;
691
692 if radii.is_empty() {
693 return (vec![0.0; nx * ny * nz], mask.to_vec());
694 }
695
696 if let (1, Some(threshold), VsharpBall::QsmJl) = (radii.len(), deconvolve, ball) {
698 progress(1, 1);
699 return crate::bgremove::sharp::sharp_core(
700 field, mask, grid, threshold, radii[0]
701 );
702 }
703
704 let r_max = radii.iter().cloned().fold(f64::MIN, f64::max);
705 let Some(bx) = SmvBox::new(mask, grid, r_max) else {
706 return (vec![0.0; nx * ny * nz], vec![0; nx * ny * nz]);
707 };
708 let (local, support) = vsharp_padded(&bx.crop(field), &bx.crop(mask), &bx.grid(grid), radii, deconvolve, ball, progress);
709 (bx.uncrop(&local), bx.uncrop(&support))
710}
711
712fn vsharp_padded(
714 field: &[f64],
715 mask: &[u8],
716 grid: &Grid,
717 radii: &[f64],
718 deconvolve: Option<f64>,
719 ball: VsharpBall,
720 mut progress: impl FnMut(usize, usize),
721) -> (Vec<f64>, Vec<u8>) {
722 let (nx, ny, nz) = grid.dims;
723 let n_total = nx * ny * nz;
724 let n_radii = radii.len();
725
726 let mut sorted_radii = radii.to_vec();
728 sorted_radii.sort_by(|a, b| b.partial_cmp(a).unwrap());
729
730 let mut field_complex: Vec<Complex64> = field.iter()
732 .map(|&x| Complex64::new(x, 0.0))
733 .collect();
734 fft3d(&mut field_complex, nx, ny, nz);
735 let field_fft = field_complex.clone();
736
737 let mut processed = vec![false; n_total];
739 let mut local_field = vec![0.0; n_total];
740 let mut final_mask = vec![0u8; n_total];
741
742 let delta = match ball {
745 VsharpBall::StiSuite => 1.0 - 1e-9,
746 VsharpBall::QsmJl => QSMJL_SMV_DELTA,
747 };
748 let mut inverse_kernel: Option<Vec<f64>> = None;
749
750 for (idx, &radius) in sorted_radii.iter().enumerate() {
751 progress(idx + 1, n_radii);
753
754 let s_kernel = match ball {
756 VsharpBall::StiSuite => sti_smv_kernel(grid, radius),
757 VsharpBall::QsmJl => smv_kernel(grid, radius),
758 };
759
760 let mut s_complex: Vec<Complex64> = s_kernel.iter()
762 .map(|&x| Complex64::new(x, 0.0))
763 .collect();
764 fft3d(&mut s_complex, nx, ny, nz);
765 let s_fft: Vec<f64> = s_complex.iter().map(|c| c.re).collect();
766
767 if let (0, Some(threshold)) = (idx, deconvolve) {
769 inverse_kernel = Some(s_fft.iter().map(|&s| {
770 let one_minus_s = 1.0 - s;
771 if one_minus_s.abs() < threshold {
772 0.0
773 } else {
774 1.0 / one_minus_s
775 }
776 }).collect());
777 }
778
779 let eroded = erode_mask_smv(mask, &s_fft, grid, delta);
781 let current_mask: Vec<bool> = eroded.iter().map(|&m| m == 1).collect();
782
783 let mut filtered = field_fft.clone();
785 for i in 0..n_total {
786 filtered[i] *= 1.0 - s_fft[i];
787 }
788
789 ifft3d(&mut filtered, nx, ny, nz);
790
791 for i in 0..n_total {
792 if current_mask[i] && !processed[i] {
793 local_field[i] = filtered[i].re;
794 processed[i] = true;
795 final_mask[i] = 1;
796 }
797 }
798 }
799
800 if let Some(inv_kernel) = inverse_kernel {
802 let mut local_complex: Vec<Complex64> = local_field.iter()
803 .map(|&x| Complex64::new(x, 0.0))
804 .collect();
805
806 fft3d(&mut local_complex, nx, ny, nz);
807
808 for i in 0..n_total {
809 local_complex[i] *= inv_kernel[i];
810 }
811
812 ifft3d(&mut local_complex, nx, ny, nz);
813
814 for i in 0..n_total {
815 local_field[i] = if final_mask[i] == 1 { local_complex[i].re } else { 0.0 };
816 }
817 }
818
819 (local_field, final_mask)
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 #[test]
829 fn sti_even_box_matches_mask_bounding_box() {
830 let probed = [
831 (9, 1.5, 1, 9, 1, 8), (9, 1.0, 1, 8, 1, 8), (9, 1.0, 2, 9, 2, 9), (9, 1.0, 9, 9, 8, 9),
832 (9, 0.8, 3, 7, 3, 8), (20, 2.5, 5, 10, 5, 10), (20, 1.2, 5, 11, 5, 12), (21, 0.7, 1, 21, 1, 20),
833 (40, 3.0, 40, 40, 39, 40), (8, 1.0, 1, 1, 1, 2),
834 ];
835 for (n, _vs, lo, hi, slo, shi) in probed {
836 let (l, h) = sti_even_box([lo - 1, 0, 0], [hi - 1, 1, 1], [n, 4, 4]);
837 assert_eq!((l[0] + 1, h[0] + 1), (slo, shi), "n {n}, {lo}..={hi}");
838 assert_eq!((l[1], h[1]), (0, 1), "an even extent is kept");
839 }
840 }
841
842 #[test]
843 fn test_vsharp_zero_field() {
844 let n = 8;
845 let field = vec![0.0; n * n * n];
846 let mask = vec![1u8; n * n * n];
847 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
848
849 let radii = vec![4.0, 3.0, 2.0];
850 let (local, _) = vsharp_with_radii(&field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl, |_, _| {});
851
852 for &val in local.iter() {
853 assert!(val.abs() < 1e-10);
854 }
855 }
856
857 #[test]
858 fn test_vsharp_preserves_more_than_sharp() {
859 let n = 16;
860 let field = vec![0.0; n * n * n];
861 let mask = vec![1u8; n * n * n];
862 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
863
864 let radii = vec![5.0, 4.0, 3.0, 2.0];
866 let (_, vsharp_mask) = vsharp_with_radii(&field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl, |_, _| {});
867
868 let (_, sharp_mask) = crate::bgremove::sharp::sharp_core(
870 &field, &mask, &grid, 0.05, 5.0
871 );
872
873 let vsharp_count: usize = vsharp_mask.iter().map(|&m| m as usize).sum();
874 let sharp_count: usize = sharp_mask.iter().map(|&m| m as usize).sum();
875
876 assert!(vsharp_count >= sharp_count,
878 "V-SHARP {} should preserve at least as many as SHARP {}",
879 vsharp_count, sharp_count);
880 }
881
882 #[test]
883 fn test_vsharp_nonuniform_voxels() {
884 let n = 16;
887 let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
888 let mask = vec![1u8; n * n * n];
889 let grid = Grid::new(n, n, n, 0.5, 1.0, 2.0);
890
891 let radii = vec![4.0, 3.0, 2.0];
893 let (local, final_mask) = vsharp_with_radii(
894 &field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl, |_, _| {}
895 );
896
897 for (i, &val) in local.iter().enumerate() {
899 assert!(val.is_finite(), "V-SHARP nonuniform voxels: finite at index {}", i);
900 }
901
902 let mask_count: usize = final_mask.iter().map(|&m| m as usize).sum();
904 assert!(mask_count > 0, "V-SHARP nonuniform: final mask should have some voxels");
905 }
906
907 #[test]
908 fn test_vsharp_single_radius() {
909 let n = 8;
910 let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
911 let mask = vec![1u8; n * n * n];
912 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
913
914 let radii = vec![3.0];
916 let (local, final_mask) = vsharp_with_radii(
917 &field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl, |_, _| {}
918 );
919
920 for (i, &val) in local.iter().enumerate() {
922 assert!(val.is_finite(), "V-SHARP single radius: finite at index {}", i);
923 }
924
925 let (sharp_local, sharp_mask) = crate::bgremove::sharp::sharp_core(
927 &field, &mask, &grid, 0.05, 3.0
928 );
929
930 for i in 0..n*n*n {
931 assert!(
932 (local[i] - sharp_local[i]).abs() < 1e-10,
933 "Single-radius V-SHARP should match SHARP at index {}", i
934 );
935 }
936
937 assert_eq!(final_mask, sharp_mask, "Single-radius V-SHARP mask should match SHARP mask");
938 }
939
940 #[test]
941 fn test_vsharp_empty_radii() {
942 let n = 8;
944 let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
945 let mask = vec![1u8; n * n * n];
946 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
947
948 let (local, returned_mask) = vsharp_with_radii(
949 &field, &mask, &grid, &[], Some(0.05), VsharpBall::QsmJl, |_, _| {}
950 );
951
952 for &val in &local {
953 assert_eq!(val, 0.0, "Empty radii should return zero local field");
954 }
955 assert_eq!(returned_mask, mask, "Empty radii should return original mask");
956 }
957
958 #[test]
959 fn test_vsharp_larger_volume() {
960 let n = 16;
962 let mut field = vec![0.0; n * n * n];
963 for z in 0..n {
965 for y in 0..n {
966 for x in 0..n {
967 field[x + y * n + z * n * n] = (z as f64) * 0.1;
968 }
969 }
970 }
971
972 let mut mask = vec![0u8; n * n * n];
974 let center = n / 2;
975 let radius = n / 3;
976 for z in 0..n {
977 for y in 0..n {
978 for x in 0..n {
979 let dx = (x as i32) - (center as i32);
980 let dy = (y as i32) - (center as i32);
981 let dz = (z as i32) - (center as i32);
982 if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
983 mask[x + y * n + z * n * n] = 1;
984 }
985 }
986 }
987 }
988
989 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
990 let radii = vec![6.0, 4.0, 3.0, 2.0];
991 let (local, final_mask) = vsharp_with_radii(
992 &field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl, |_, _| {}
993 );
994
995 assert_eq!(local.len(), n * n * n);
996 for &val in &local {
997 assert!(val.is_finite(), "V-SHARP larger volume values should be finite");
998 }
999
1000 let mask_count: usize = final_mask.iter().map(|&m| m as usize).sum();
1001 assert!(mask_count > 0, "V-SHARP larger volume should have voxels in final mask");
1002
1003 for i in 0..n * n * n {
1005 if final_mask[i] == 0 {
1006 assert_eq!(local[i], 0.0, "Outside final mask should be zero");
1007 }
1008 }
1009 }
1010
1011 #[test]
1012 fn test_vsharp_with_progress() {
1013 let n = 8;
1014 let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
1015 let mask = vec![1u8; n * n * n];
1016 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1017
1018 let radii = vec![4.0, 3.0, 2.0];
1019 let mut progress_calls = Vec::new();
1020 let (local, _) = vsharp_with_radii(
1021 &field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl,
1022 |idx, total| { progress_calls.push((idx, total)); }
1023 );
1024
1025 assert_eq!(local.len(), n * n * n);
1026 assert!(!progress_calls.is_empty(), "Progress should be called");
1027 for &val in &local {
1028 assert!(val.is_finite());
1029 }
1030 }
1031
1032 #[test]
1033 fn test_vsharp_with_progress_single_radius() {
1034 let n = 8;
1035 let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
1036 let mask = vec![1u8; n * n * n];
1037 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1038
1039 let radii = vec![3.0];
1040 let mut progress_calls = Vec::new();
1041 let (local, _) = vsharp_with_radii(
1042 &field, &mask, &grid, &radii, Some(0.05), VsharpBall::QsmJl,
1043 |idx, total| { progress_calls.push((idx, total)); }
1044 );
1045
1046 assert_eq!(local.len(), n * n * n);
1047 assert!(!progress_calls.is_empty(), "Progress should be called for single radius");
1048 for &val in &local {
1049 assert!(val.is_finite());
1050 }
1051 }
1052
1053 #[test]
1054 fn test_vsharp_with_progress_empty_radii() {
1055 let n = 8;
1056 let field = vec![0.0; n * n * n];
1057 let mask = vec![1u8; n * n * n];
1058 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1059
1060 let mut progress_calls = Vec::new();
1061 let (local, returned_mask) = vsharp_with_radii(
1062 &field, &mask, &grid, &[], Some(0.05), VsharpBall::QsmJl,
1063 |idx, total| { progress_calls.push((idx, total)); }
1064 );
1065
1066 for &val in &local {
1067 assert_eq!(val, 0.0);
1068 }
1069 assert_eq!(returned_mask, mask);
1070 }
1071
1072 #[test]
1073 fn test_vsharp_unsorted_radii() {
1074 let n = 8;
1076 let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
1077 let mask = vec![1u8; n * n * n];
1078 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1079
1080 let radii_sorted = vec![4.0, 3.0, 2.0];
1081 let radii_unsorted = vec![2.0, 4.0, 3.0];
1082
1083 let (local_sorted, mask_sorted) = vsharp_with_radii(
1084 &field, &mask, &grid, &radii_sorted, Some(0.05), VsharpBall::QsmJl, |_, _| {}
1085 );
1086 let (local_unsorted, mask_unsorted) = vsharp_with_radii(
1087 &field, &mask, &grid, &radii_unsorted, Some(0.05), VsharpBall::QsmJl, |_, _| {}
1088 );
1089
1090 assert_eq!(mask_sorted, mask_unsorted, "Sorted and unsorted radii should give same mask");
1092 for i in 0..n * n * n {
1093 assert!(
1094 (local_sorted[i] - local_unsorted[i]).abs() < 1e-10,
1095 "Results should match at index {}",
1096 i
1097 );
1098 }
1099 }
1100
1101 fn sphere_case(n: usize, rad: f64) -> (Vec<f64>, Vec<u8>) {
1104 let c = n as f64 / 2.0;
1105 let mut field = vec![0.0; n * n * n];
1106 let mut mask = vec![0u8; n * n * n];
1107 for z in 0..n {
1108 for y in 0..n {
1109 for x in 0..n {
1110 let (dx, dy, dz) = (x as f64 - c, y as f64 - c, z as f64 - c);
1111 let i = x + y * n + z * n * n;
1112 field[i] = 0.01 * dz + 0.002 * (dx * dx - dy * dy) + (0.3 * dx).sin() * 0.05;
1113 mask[i] = (dx * dx + dy * dy + dz * dz <= rad * rad) as u8;
1114 }
1115 }
1116 }
1117 (field, mask)
1118 }
1119
1120 #[test]
1121 fn test_vsharp_defaults_are_sti_suites() {
1122 let p = VsharpParams::default();
1124 assert!(!p.deconvolve);
1125 assert_eq!(p.grid, VsharpGrid::Isotropic { spacing_mm: STI_GRID_MM });
1126 assert_eq!(p.max_radius, 12.0);
1127 assert_eq!((p.min_radius, p.radius_step), (2.0, 1.0));
1128 }
1129
1130 #[test]
1131 fn test_vsharp_no_deconvolve_is_smv_highpass() {
1132 let n = 20;
1135 let (field, mask) = sphere_case(n, 8.0);
1136 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1137 let radii = [4.0, 3.0, 2.0];
1138 let (local, out_mask) = vsharp_with_radii(&field, &mask, &grid, &radii, None, VsharpBall::QsmJl, |_, _| {});
1139
1140 let delta = 1.0 - 1e-7_f64.sqrt();
1141 let mut expected = vec![0.0; n * n * n];
1142 let mut expected_mask = vec![0u8; n * n * n];
1143 for &r in &radii {
1144 let mut s: Vec<Complex64> = smv_kernel(&grid, r).iter().map(|&x| Complex64::new(x, 0.0)).collect();
1145 fft3d(&mut s, n, n, n);
1146 let s_fft: Vec<f64> = s.iter().map(|c| c.re).collect();
1147 let fits = erode_mask_smv(&mask, &s_fft, &grid, delta);
1148 let mut sf: Vec<Complex64> = field.iter().map(|&x| Complex64::new(x, 0.0)).collect();
1149 fft3d(&mut sf, n, n, n);
1150 for i in 0..sf.len() { sf[i] *= s_fft[i]; }
1151 ifft3d(&mut sf, n, n, n);
1152 for i in 0..n * n * n {
1153 if fits[i] == 1 && expected_mask[i] == 0 {
1154 expected[i] = field[i] - sf[i].re;
1155 expected_mask[i] = 1;
1156 }
1157 }
1158 }
1159 assert_eq!(out_mask, expected_mask);
1160 assert!(out_mask.contains(&1));
1161 for i in 0..n * n * n {
1162 assert!((local[i] - expected[i]).abs() < 1e-12, "voxel {i}: {} vs {}", local[i], expected[i]);
1163 }
1164 }
1165
1166 #[test]
1167 fn test_vsharp_deconvolve_flag() {
1168 let n = 20;
1169 let (field, mask) = sphere_case(n, 8.0);
1170 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1171 let base = VsharpParams {
1172 max_radius: 4.0, min_radius: 1.0, deconvolve: true, grid: VsharpGrid::Native, ..Default::default()
1173 };
1174 let (dec, dec_mask) = vsharp(&field, &mask, &grid, &base, |_, _| {}).unwrap();
1175 let off = VsharpParams { deconvolve: false, ..base.clone() };
1176 let (hp, hp_mask) = vsharp(&field, &mask, &grid, &off, |_, _| {}).unwrap();
1177 assert_eq!(dec_mask, hp_mask);
1179 assert!(dec.iter().zip(&hp).any(|(a, b)| (a - b).abs() > 1e-6));
1180 let (hp2, _) = vsharp(&field, &mask, &grid, &VsharpParams { threshold: 0.9, ..off }, |_, _| {}).unwrap();
1182 assert_eq!(hp, hp2);
1183 for i in 0..n * n * n {
1184 if hp_mask[i] == 0 { assert_eq!(hp[i], 0.0); }
1185 }
1186 }
1187
1188 #[test]
1189 fn test_vsharp_no_deconvolve_single_radius() {
1190 let n = 20;
1192 let (field, mask) = sphere_case(n, 8.0);
1193 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1194 let (hp, hp_mask) = vsharp_with_radii(&field, &mask, &grid, &[3.0], None, VsharpBall::QsmJl, |_, _| {});
1195 let (sh, sh_mask) = crate::bgremove::sharp::sharp_core(&field, &mask, &grid, 0.05, 3.0);
1196 assert_eq!(hp_mask, sh_mask);
1197 assert!(hp.iter().zip(&sh).any(|(a, b)| (a - b).abs() > 1e-6));
1198 }
1199
1200 #[test]
1201 fn test_grid_resolve() {
1202 let finest = VsharpGrid::IsotropicFinest;
1203 assert_eq!(finest.resolve((1.0, 1.0, 1.0)), VsharpGrid::Native);
1204 assert_eq!(finest.resolve((0.5, 0.5, 0.5)), VsharpGrid::Native);
1205 assert_eq!(finest.resolve((1.0, 1.0, 1.005)), VsharpGrid::Native);
1207 assert_eq!(finest.resolve((0.8, 0.8, 3.0)), VsharpGrid::Isotropic { spacing_mm: 0.8 });
1208 assert_eq!(finest.resolve((3.0, 0.8, 0.8)), VsharpGrid::Isotropic { spacing_mm: 0.8 });
1209 assert_eq!(finest.resolve((0.3, 0.3, 0.5)), VsharpGrid::Isotropic { spacing_mm: 0.3 });
1210 let iso1 = VsharpGrid::Isotropic { spacing_mm: 1.0 };
1211 assert_eq!(iso1.resolve((0.3, 0.3, 0.5)), iso1);
1212 assert_eq!(VsharpGrid::Native.resolve((0.8, 0.8, 3.0)), VsharpGrid::Native);
1213 assert_eq!(VsharpParams::default().working_grid(&Grid::new(4, 4, 4, 0.8, 0.8, 3.0)), iso1);
1215 assert_eq!(VsharpParams::default().working_grid(&Grid::new(4, 4, 4, 1.0, 1.0, 1.0)), iso1);
1216 }
1217
1218 fn ellipsoid_case(dims: (usize, usize, usize), vs: (f64, f64, f64), semi: [f64; 3]) -> (Vec<f64>, Vec<u8>) {
1221 let (nx, ny, nz) = dims;
1222 let c = [nx as f64 / 2.0, ny as f64 / 2.0, nz as f64 / 2.0];
1223 let mut field = vec![0.0; nx * ny * nz];
1224 let mut mask = vec![0u8; nx * ny * nz];
1225 for z in 0..nz {
1226 for y in 0..ny {
1227 for x in 0..nx {
1228 let (dx, dy, dz) = ((x as f64 - c[0]) * vs.0, (y as f64 - c[1]) * vs.1, (z as f64 - c[2]) * vs.2);
1229 let i = x + y * nx + z * nx * ny;
1230 field[i] = 0.01 * dz + 0.002 * (dx * dx - dy * dy) + 0.05 * (0.3 * dx).sin() * (0.2 * dz).cos();
1231 let e = (dx / semi[0]).powi(2) + (dy / semi[1]).powi(2) + (dz / semi[2]).powi(2);
1232 mask[i] = (e <= 1.0) as u8;
1233 }
1234 }
1235 }
1236 (field, mask)
1237 }
1238
1239 #[test]
1240 fn test_native_grid_is_unchanged_v_sharp() {
1241 let n = 20;
1244 let (field, mask) = sphere_case(n, 8.0);
1245 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1246 for deconvolve in [true, false] {
1247 let base = VsharpParams { max_radius: 4.0, deconvolve, grid: VsharpGrid::Native, ..Default::default() };
1248 let radii = vsharp_radii(&base);
1249 let expected = vsharp_with_radii(&field, &mask, &grid, &radii, deconvolve.then_some(base.threshold), base.ball, |_, _| {});
1250 assert_eq!(vsharp(&field, &mask, &grid, &base, |_, _| {}).unwrap(), expected);
1251 let finest = VsharpParams { grid: VsharpGrid::IsotropicFinest, ..base.clone() };
1252 assert_eq!(vsharp(&field, &mask, &grid, &finest, |_, _| {}).unwrap(), expected);
1253 }
1254 let ag = Grid::new(24, 24, 10, 0.8, 0.8, 3.0);
1256 let (af, am) = ellipsoid_case(ag.dims, ag.voxel_size, [8.0, 8.0, 12.0]);
1257 let p = VsharpParams { max_radius: 4.0, deconvolve: true, grid: VsharpGrid::Native, ..Default::default() };
1258 let expected = vsharp_with_radii(&af, &am, &ag, &vsharp_radii(&p), Some(p.threshold), p.ball, |_, _| {});
1259 assert_eq!(vsharp(&af, &am, &ag, &p, |_, _| {}).unwrap(), expected);
1260 }
1261
1262 #[test]
1263 fn test_isotropic_working_grids() {
1264 let grid = Grid::new(30, 30, 12, 0.8, 0.8, 3.0);
1265 let (field, mask) = ellipsoid_case(grid.dims, grid.voxel_size, [10.0, 10.0, 14.0]);
1266 let native = VsharpParams { max_radius: 4.0, deconvolve: false, grid: VsharpGrid::Native, ..Default::default() };
1267 let (ln, mn) = vsharp(&field, &mask, &grid, &native, |_, _| {}).unwrap();
1268 let finest = VsharpParams { grid: VsharpGrid::IsotropicFinest, ..native.clone() };
1269 assert_eq!(finest.working_grid(&grid), VsharpGrid::Isotropic { spacing_mm: 0.8 });
1270 let at_finest = VsharpParams { grid: VsharpGrid::Isotropic { spacing_mm: 0.8 }, ..native.clone() };
1271 let iso1 = VsharpParams { grid: VsharpGrid::Isotropic { spacing_mm: 1.0 }, ..native.clone() };
1272 let (lf, mf) = vsharp(&field, &mask, &grid, &finest, |_, _| {}).unwrap();
1273 assert_eq!((lf.clone(), mf.clone()), vsharp(&field, &mask, &grid, &at_finest, |_, _| {}).unwrap());
1274 let (l1, m1) = vsharp(&field, &mask, &grid, &iso1, |_, _| {}).unwrap();
1275 for (l, m) in [(&lf, &mf), (&l1, &m1)] {
1276 assert!(m.contains(&1));
1277 assert!(*m != mn || l.iter().zip(&ln).any(|(a, b)| (a - b).abs() > 1e-6));
1278 for i in 0..l.len() {
1279 if m[i] == 0 { assert_eq!(l[i], 0.0); } else { assert!(mask[i] == 1 && l[i].is_finite()); }
1280 }
1281 }
1282 }
1283
1284 #[test]
1285 fn test_isotropic_grid_empty_mask_and_bad_spacing() {
1286 let grid = Grid::new(8, 8, 4, 0.8, 0.8, 3.0);
1287 let iso1 = VsharpParams { grid: VsharpGrid::Isotropic { spacing_mm: 1.0 }, ..Default::default() };
1288 let (l, m) = vsharp(&vec![1.0; 256], &[0u8; 256], &grid, &iso1, |_, _| {}).unwrap();
1289 assert!(l.iter().all(|&v| v == 0.0) && m.iter().all(|&v| v == 0));
1290 let bad = VsharpParams { grid: VsharpGrid::Isotropic { spacing_mm: 0.0 }, ..Default::default() };
1291 let err = vsharp(&vec![1.0; 256], &[1u8; 256], &grid, &bad, |_, _| {}).unwrap_err();
1292 assert!(err.0.contains("spacing_mm = 0") && err.0.contains("> 0 mm"), "{err}");
1293 }
1294
1295 #[test]
1298 fn min_radius_that_is_not_positive_is_an_error() {
1299 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
1300 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
1301 let p = VsharpParams { min_radius: bad, ..Default::default() };
1302 let err = vsharp(&[0.0; 64], &[1u8; 64], &grid, &p, |_, _| {}).unwrap_err();
1303 assert!(err.0.contains("V-SHARP min_radius") && err.0.contains("> 0 mm"), "{err}");
1304 }
1305 }
1306
1307 #[test]
1308 fn radius_step_that_is_not_positive_is_an_error() {
1309 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
1310 for bad in [0.0, -1.0, f64::NAN] {
1311 let p = VsharpParams { radius_step: bad, ..Default::default() };
1312 let err = vsharp(&[0.0; 64], &[1u8; 64], &grid, &p, |_, _| {}).unwrap_err();
1313 assert!(err.0.contains("V-SHARP radius_step"), "{err}");
1314 }
1315 }
1316
1317 #[test]
1318 fn radius_schedules() {
1319 let sti: Vec<f64> = (2..=12).rev().map(|r| r as f64).collect();
1321 assert_eq!(vsharp_radii(&VsharpParams::default()), sti);
1322 let old = VsharpParams { min_radius: 1.0, radius_step: 1.0, ..Default::default() };
1324 assert_eq!(vsharp_radii(&old), (1..=12).rev().map(|r| r as f64).collect::<Vec<_>>());
1325 let p = VsharpParams { max_radius: 10.0, min_radius: 3.0, radius_step: 2.5, ..Default::default() };
1327 assert_eq!(vsharp_radii(&p), vec![10.0, 7.5, 5.0]);
1328 let p = VsharpParams { max_radius: 1.0, min_radius: 2.0, ..Default::default() };
1329 assert_eq!(vsharp_radii(&p), vec![1.0]);
1330 }
1331
1332 fn prototype_iso(field: &[f64], mask: &[u8], grid: &Grid, p: &VsharpParams) -> (Vec<f64>, Vec<u8>) {
1337 fn kernel_fft(n: [usize; 3], vs: [f64; 3], r: f64) -> (Vec<Complex64>, f64) {
1338 let mut k = vec![Complex64::new(0.0, 0.0); n[0] * n[1] * n[2]];
1339 let lim = r * r;
1340 let h = [0, 1, 2].map(|d| ((r / vs[d]).floor() as i64).min(n[d] as i64 / 2));
1341 let wrap = |d: i64, nd: usize| d.rem_euclid(nd as i64) as usize;
1342 let mut count = 0.0;
1343 for dz in -h[2]..=h[2] { for dy in -h[1]..=h[1] { for dx in -h[0]..=h[0] {
1344 let (x, y, z) = (dx as f64 * vs[0], dy as f64 * vs[1], dz as f64 * vs[2]);
1345 if x * x + y * y + z * z > lim { continue; }
1346 let idx = wrap(dx, n[0]) + wrap(dy, n[1]) * n[0] + wrap(dz, n[2]) * n[0] * n[1];
1347 if k[idx].re == 0.0 { k[idx] = Complex64::new(1.0, 0.0); count += 1.0; }
1348 }}}
1349 fft3d(&mut k, n[0], n[1], n[2]);
1350 (k, count)
1351 }
1352 let to_complex = |v: &[f64]| -> Vec<Complex64> { v.iter().map(|&x| Complex64::new(x, 0.0)).collect() };
1353 let (nx, ny, nz) = grid.dims;
1354 let vs = [grid.voxel_size.0, grid.voxel_size.1, grid.voxel_size.2];
1355 let mut local_out = vec![0.0; nx * ny * nz];
1356 let mut mask_out = vec![0u8; nx * ny * nz];
1357 let Some((lo, hi)) = bounding_box(mask, grid.dims) else { return (local_out, mask_out) };
1358 let (lo, hi) = sti_even_box(lo, hi, [nx, ny, nz]);
1359 let pad = 12;
1360 let np = [0, 1, 2].map(|d| hi[d] - lo[d] + 1 + 2 * pad);
1361 let mut pp = vec![0.0; np[0] * np[1] * np[2]];
1362 let mut m = vec![0.0; pp.len()];
1363 for k in lo[2]..=hi[2] { for j in lo[1]..=hi[1] { for i in lo[0]..=hi[0] {
1364 let src = i + j * nx + k * nx * ny;
1365 let dst = (i - lo[0] + pad) + (j - lo[1] + pad) * np[0] + (k - lo[2] + pad) * np[0] * np[1];
1366 pp[dst] = field[src];
1367 m[dst] = if mask[src] != 0 { 1.0 } else { 0.0 };
1368 }}}
1369 let h = 1.0;
1370 let ni = [0, 1, 2].map(|d| if vs[d] == h { np[d] } else { (np[d] as f64 * vs[d] / h).ceil() as usize });
1371 let fwd = [0, 1, 2].map(|d| (1..=ni[d]).map(|q| if vs[d] == h { q as f64 } else { q as f64 * h / vs[d] }).collect::<Vec<f64>>());
1372 let pi = trilinear(&pp, np, &fwd);
1373 let mi = trilinear_mask_f32(&m, np, &fwd);
1374 let ne = ni.map(|n| n + n % 2);
1375 let mut pe = vec![0.0; ne[0] * ne[1] * ne[2]];
1376 let mut me = vec![0.0; pe.len()];
1377 for k in 0..ni[2] { for j in 0..ni[1] { for i in 0..ni[0] {
1378 let s = i + j * ni[0] + k * ni[0] * ni[1];
1379 let d = i + j * ne[0] + k * ne[0] * ne[1];
1380 pe[d] = pi[s];
1381 me[d] = if mi[s] > 0.5f32 { 1.0 } else { 0.0 };
1382 }}}
1383 let nw = ne[0] * ne[1] * ne[2];
1384 let mut radii = vsharp_radii(p);
1385 radii.sort_by(|a, b| b.partial_cmp(a).unwrap());
1386 let mut p_fft = to_complex(&pe);
1387 fft3d(&mut p_fft, ne[0], ne[1], ne[2]);
1388 let mut m_fft = to_complex(&me);
1389 fft3d(&mut m_fft, ne[0], ne[1], ne[2]);
1390 let mut bg = pe.clone();
1391 let mut nm = vec![0.0; nw];
1392 let mut buf = vec![Complex64::new(0.0, 0.0); nw];
1393 let mut inv: Option<Vec<f64>> = None;
1394 let delta = QSMJL_SMV_DELTA;
1395 for &r in &radii {
1396 let (mut k_fft, count) = kernel_fft(ne, [h; 3], r);
1397 if count == 0.0 { continue; }
1398 for i in 0..nw { buf[i] = m_fft[i] * k_fft[i]; }
1399 ifft3d(&mut buf, ne[0], ne[1], ne[2]);
1400 let fits: Vec<bool> = (0..nw).map(|i| me[i] > 0.0 && buf[i].re / count > delta).collect();
1401 for v in k_fft.iter_mut() { *v /= count; }
1402 if inv.is_none() {
1403 inv = Some(k_fft.iter().map(|s| { let oms = 1.0 - s.re; if oms.abs() < p.threshold { 0.0 } else { 1.0 / oms } }).collect());
1404 }
1405 for i in 0..nw { buf[i] = p_fft[i] * k_fft[i]; }
1406 ifft3d(&mut buf, ne[0], ne[1], ne[2]);
1407 for i in 0..nw { if fits[i] && nm[i] == 0.0 { bg[i] = buf[i].re; nm[i] = 1.0; } }
1408 }
1409 let mut l: Vec<Complex64> = (0..nw).map(|i| Complex64::new((pe[i] - bg[i]) * nm[i], 0.0)).collect();
1410 if p.deconvolve && inv.is_some() {
1411 let w_grid = Grid::new(ne[0], ne[1], ne[2], h, h, h);
1414 let me_u8: Vec<u8> = me.iter().map(|&v| v as u8).collect();
1415 let bx = SmvBox::new(&me_u8, &w_grid, radii[0]).unwrap();
1416 let (bx_n, by_n, bz_n) = bx.np;
1417 let (k_fft, count) = kernel_fft([bx_n, by_n, bz_n], [h; 3], radii[0]);
1418 let inv_b: Vec<f64> = k_fft.iter().map(|s| { let oms = 1.0 - s.re / count; if oms.abs() < p.threshold { 0.0 } else { 1.0 / oms } }).collect();
1419 let lr: Vec<f64> = l.iter().map(|c| c.re).collect();
1420 let mut lb = to_complex(&bx.crop(&lr));
1421 fft3d(&mut lb, bx_n, by_n, bz_n);
1422 for (v, iv) in lb.iter_mut().zip(&inv_b) { *v *= iv; }
1423 ifft3d(&mut lb, bx_n, by_n, bz_n);
1424 let lb: Vec<f64> = lb.iter().map(|c| c.re).collect();
1425 let back = bx.uncrop(&lb);
1426 for i in 0..nw { l[i] = Complex64::new(back[i] * nm[i], 0.0); }
1427 }
1428 let mut bg_c = vec![0.0; ni[0] * ni[1] * ni[2]];
1429 let mut nm_c = vec![0.0; bg_c.len()];
1430 for k in 0..ni[2] { for j in 0..ni[1] { for i in 0..ni[0] {
1431 let d = i + j * ni[0] + k * ni[0] * ni[1];
1432 let s = i + j * ne[0] + k * ne[0] * ne[1];
1433 bg_c[d] = if p.deconvolve { pe[s] - l[s].re } else { bg[s] };
1434 nm_c[d] = nm[s];
1435 }}}
1436 let back = [0, 1, 2].map(|d| (1..=np[d]).map(|a| if vs[d] == h { a as f64 } else { a as f64 * vs[d] / h }).collect::<Vec<f64>>());
1437 let bg_n = trilinear(&bg_c, ni, &back);
1438 let nm_n = trilinear_mask_f32(&nm_c, ni, &back);
1439 for k in lo[2]..=hi[2] { for j in lo[1]..=hi[1] { for i in lo[0]..=hi[0] {
1440 let s = (i - lo[0] + pad) + (j - lo[1] + pad) * np[0] + (k - lo[2] + pad) * np[0] * np[1];
1441 if nm_n[s] > 0.5f32 {
1442 let d = i + j * nx + k * nx * ny;
1443 local_out[d] = pp[s] - bg_n[s];
1444 mask_out[d] = 1;
1445 }
1446 }}}
1447 (local_out, mask_out)
1448 }
1449
1450 #[test]
1451 fn test_isotropic_grid_reproduces_the_prototype() {
1452 let grid = Grid::new(40, 36, 14, 0.8, 0.8, 3.0);
1455 let (field, mask) = ellipsoid_case(grid.dims, grid.voxel_size, [13.0, 11.0, 17.0]);
1456 for deconvolve in [false, true] {
1457 let p = VsharpParams {
1458 max_radius: 6.0,
1459 deconvolve,
1460 ball: VsharpBall::QsmJl,
1462 grid: VsharpGrid::Isotropic { spacing_mm: 1.0 },
1463 ..Default::default()
1464 };
1465 let (want, want_mask) = prototype_iso(&field, &mask, &grid, &p);
1466 let (got, got_mask) = vsharp(&field, &mask, &grid, &p, |_, _| {}).unwrap();
1467 assert_eq!(got_mask, want_mask, "support (deconvolve {deconvolve})");
1468 assert!(got_mask.contains(&1));
1469 let scale = want.iter().map(|v| v.abs()).fold(0.0, f64::max);
1470 let worst = got.iter().zip(&want).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max);
1471 assert!(worst <= 1e-12 * scale.max(1.0), "deconvolve {deconvolve}: max |Δ| {worst} (scale {scale})");
1472 }
1473 }
1474}