1#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
15#[derive(Clone, Debug)]
16pub struct HomogeneityParams {
17 pub sigma_mm: f64,
19 pub nbox: usize,
21}
22
23impl Default for HomogeneityParams {
24 fn default() -> Self {
25 Self {
26 sigma_mm: 7.0,
27 nbox: 3,
28 }
29 }
30}
31
32use std::collections::VecDeque;
33use crate::Grid;
34
35#[inline(always)]
37fn idx3d(i: usize, j: usize, k: usize, nx: usize, ny: usize) -> usize {
38 i + j * nx + k * nx * ny
39}
40
41fn get_box_sizes(sigma: f64, n: usize) -> Vec<usize> {
50 if sigma <= 0.0 || n == 0 {
51 return vec![0; n];
52 }
53
54 let wideal = ((12.0 * sigma * sigma / n as f64) + 1.0).sqrt();
56
57 let wl_float = wideal - (wideal + 1.0) % 2.0;
59 let wl = wl_float.round() as usize;
60 let wl = if wl.is_multiple_of(2) { wl + 1 } else { wl }; let wu = wl + 2;
62
63 let wl_f = wl as f64;
65 let n_f = n as f64;
66 let mideal = (12.0 * sigma * sigma - n_f * wl_f * wl_f - 4.0 * n_f * wl_f - 3.0 * n_f)
67 / (-4.0 * wl_f - 4.0);
68 let m = mideal.round() as usize;
69
70 (0..n).map(|i| if i < m { wl } else { wu }).collect()
71}
72
73fn check_box_sizes(boxsizes: &mut [Vec<usize>], dims: &[usize]) {
75 for (dim, bs) in boxsizes.iter_mut().enumerate() {
76 if dim >= dims.len() {
77 continue;
78 }
79 for b in bs.iter_mut() {
80 if *b % 2 == 0 {
82 *b += 1;
83 }
84 let max_size = dims[dim] / 2;
86 if *b > max_size {
87 *b = if max_size.is_multiple_of(2) { max_size + 1 } else { max_size };
88 }
89 }
90 }
91}
92
93fn box_filter_line(line: &mut [f64], boxsize: usize) {
97 if boxsize < 3 || line.len() < boxsize {
98 return;
99 }
100
101 let n = line.len();
102 let r = boxsize / 2;
103
104 let mut queue: VecDeque<f64> = VecDeque::with_capacity(boxsize);
106
107 let mut lsum: f64 = line[..r].iter().sum();
109 for i in 0..r {
110 queue.push_back(line[i]);
111 }
112
113 for i in 0..=r {
115 lsum += line[i + r];
116 queue.push_back(line[i + r]);
117 line[i] = lsum / (r + i + 1) as f64;
118 }
119
120 for i in (r + 1)..(n - r) {
122 let old = queue.pop_front().unwrap();
123 lsum += line[i + r] - old;
124 queue.push_back(line[i + r]);
125 line[i] = lsum / boxsize as f64;
126 }
127
128 for i in (n - r)..n {
130 let old = queue.pop_front().unwrap();
131 lsum -= old;
132 line[i] = lsum / (r + n - i) as f64;
133 }
134}
135
136fn box_filter_line_weighted(line: &mut [f64], weight: &mut [f64], boxsize: usize) {
138 if boxsize < 3 || line.len() < boxsize {
139 return;
140 }
141
142 let n = line.len();
143 let r = boxsize / 2;
144
145 let mut lq: VecDeque<f64> = VecDeque::with_capacity(boxsize);
146 let mut wq: VecDeque<f64> = VecDeque::with_capacity(boxsize);
147
148 let mut sum = f64::EPSILON; let mut wsum = f64::EPSILON;
151 let mut wsmooth = f64::EPSILON;
152
153 for i in 0..boxsize {
154 sum += line[i] * weight[i];
155 wsum += weight[i];
156 wsmooth += weight[i] * weight[i];
157 lq.push_back(line[i]);
158 wq.push_back(weight[i]);
159 }
160
161 for i in (r + 1)..(n - r) {
163 let w = weight[i + r];
164 let l = line[i + r];
165 let wold = wq.pop_front().unwrap();
166 let lold = lq.pop_front().unwrap();
167 wq.push_back(w);
168 lq.push_back(l);
169
170 sum += l * w - lold * wold;
171 wsum += w - wold;
172 line[i] = sum / wsum;
173 wsmooth += w * w - wold * wold;
174 weight[i] = wsmooth / wsum;
175 }
176}
177
178fn nan_box_filter_line(line: &mut [f64], boxsize: usize) {
181 if boxsize < 3 || line.len() < boxsize {
182 return;
183 }
184
185 let n = line.len();
186 let r = boxsize / 2;
187 let maxfills = r;
188
189 let mut orig = vec![f64::NAN; n + boxsize - 1];
191 orig[r..r + n].copy_from_slice(line);
192
193 let mut lsum = 0.0;
195 for i in (r + 1)..=(2 * r) {
196 if !orig[i].is_nan() {
197 lsum += orig[i];
198 }
199 }
200
201 let mut nfills = 0usize;
202 let mut nvalids = 0usize;
203
204 #[derive(PartialEq, Clone, Copy)]
205 enum Mode { Nan, Normal, Fill }
206 let mut mode = Mode::Nan;
207
208 for i in 0..n {
209 match mode {
211 Mode::Normal => {
212 if orig[i + 2 * r].is_nan() {
213 mode = Mode::Fill;
214 }
215 }
216 Mode::Nan => {
217 if orig[i + 2 * r].is_nan() {
218 nvalids = 0;
219 } else {
220 nvalids += 1;
221 }
222 if nvalids == boxsize {
223 mode = Mode::Normal;
224 lsum = 0.0;
225 for j in i..=(i + 2 * r) {
226 lsum += orig[j];
227 }
228 line[i] = lsum / boxsize as f64;
229 continue;
230 }
231 }
232 Mode::Fill => {
233 if orig[i + 2 * r].is_nan() {
234 nfills += 1;
235 if nfills > maxfills {
236 mode = Mode::Nan;
237 nfills = 0;
238 lsum = 0.0;
239 nvalids = 0;
240 }
241 } else {
242 mode = Mode::Normal;
243 nfills = 0;
244 }
245 }
246 }
247
248 match mode {
250 Mode::Normal => {
251 if i > 0 {
252 lsum += orig[i + 2 * r] - orig[i - 1];
253 }
254 line[i] = lsum / boxsize as f64;
255 }
256 Mode::Fill => {
257 if i > 0 {
258 lsum -= orig[i - 1];
259 }
260 line[i] = (lsum - orig[i]) / (boxsize - 2) as f64;
261
262 let extrapolated = if i >= r {
264 2.0 * line[i] - line[i - r]
265 } else {
266 line[i]
267 };
268 orig[i + 2 * r] = extrapolated;
269 if i + r < n {
270 line[i + r] = extrapolated;
271 }
272 lsum += orig[i + 2 * r];
273 }
274 Mode::Nan => {
275 }
277 }
278 }
279}
280
281pub fn gaussian_smooth_3d(
293 data: &[f64],
294 sigma: [f64; 3],
295 mask: Option<&[u8]>,
296 mut weight: Option<&mut [f64]>,
297 nbox: usize,
298 grid: &Grid,
299) -> Vec<f64> {
300 let (nx, ny, nz) = grid.dims;
301 let n_total = nx * ny * nz;
302 let mut result: Vec<f64> = data.iter().map(|&v| v as f64).collect();
303
304 let mut boxsizes: Vec<Vec<usize>> = sigma.iter()
306 .map(|&s| get_box_sizes(s, nbox))
307 .collect();
308
309 check_box_sizes(&mut boxsizes, &[nx, ny, nz]);
310
311 if let Some(m) = mask {
313 for i in 0..n_total {
314 if m[i] == 0 {
315 result[i] = f64::NAN;
316 }
317 }
318 }
319
320 for ibox in 0..nbox {
322 let bsize_x = boxsizes[0][ibox];
324 if nx > 1 && bsize_x >= 3 {
325 let reverse = mask.is_some() && ibox % 2 == 1;
327
328 for k in 0..nz {
329 for j in 0..ny {
330 let mut line: Vec<f64> = (0..nx).map(|i| {
331 let idx = if reverse { nx - 1 - i } else { i };
332 result[idx3d(idx, j, k, nx, ny)]
333 }).collect();
334
335 if mask.is_some() {
336 nan_box_filter_line(&mut line, bsize_x);
337 } else if let Some(ref mut w) = weight.as_deref_mut() {
338 let mut wline: Vec<f64> = (0..nx).map(|i| {
339 let idx = if reverse { nx - 1 - i } else { i };
340 w[idx3d(idx, j, k, nx, ny)]
341 }).collect();
342 box_filter_line_weighted(&mut line, &mut wline, bsize_x);
343 for i in 0..nx {
344 let idx = if reverse { nx - 1 - i } else { i };
345 w[idx3d(idx, j, k, nx, ny)] = wline[i];
346 }
347 } else {
348 box_filter_line(&mut line, bsize_x);
349 }
350
351 for i in 0..nx {
352 let idx = if reverse { nx - 1 - i } else { i };
353 result[idx3d(idx, j, k, nx, ny)] = line[i];
354 }
355 }
356 }
357 }
358
359 let bsize_y = boxsizes[1][ibox];
361 if ny > 1 && bsize_y >= 3 {
362 let reverse = mask.is_some() && ibox % 2 == 1;
363
364 for k in 0..nz {
365 for i in 0..nx {
366 let mut line: Vec<f64> = (0..ny).map(|j| {
367 let idx = if reverse { ny - 1 - j } else { j };
368 result[idx3d(i, idx, k, nx, ny)]
369 }).collect();
370
371 if mask.is_some() {
372 nan_box_filter_line(&mut line, bsize_y);
373 } else if let Some(ref mut w) = weight.as_deref_mut() {
374 let mut wline: Vec<f64> = (0..ny).map(|j| {
375 let idx = if reverse { ny - 1 - j } else { j };
376 w[idx3d(i, idx, k, nx, ny)]
377 }).collect();
378 box_filter_line_weighted(&mut line, &mut wline, bsize_y);
379 for j in 0..ny {
380 let idx = if reverse { ny - 1 - j } else { j };
381 w[idx3d(i, idx, k, nx, ny)] = wline[j];
382 }
383 } else {
384 box_filter_line(&mut line, bsize_y);
385 }
386
387 for j in 0..ny {
388 let idx = if reverse { ny - 1 - j } else { j };
389 result[idx3d(i, idx, k, nx, ny)] = line[j];
390 }
391 }
392 }
393 }
394
395 let bsize_z = boxsizes[2][ibox];
397 if nz > 1 && bsize_z >= 3 {
398 let reverse = mask.is_some() && ibox % 2 == 1;
399
400 for j in 0..ny {
401 for i in 0..nx {
402 let mut line: Vec<f64> = (0..nz).map(|k| {
403 let idx = if reverse { nz - 1 - k } else { k };
404 result[idx3d(i, j, idx, nx, ny)]
405 }).collect();
406
407 if mask.is_some() {
408 nan_box_filter_line(&mut line, bsize_z);
409 } else if let Some(ref mut w) = weight.as_deref_mut() {
410 let mut wline: Vec<f64> = (0..nz).map(|k| {
411 let idx = if reverse { nz - 1 - k } else { k };
412 w[idx3d(i, j, idx, nx, ny)]
413 }).collect();
414 box_filter_line_weighted(&mut line, &mut wline, bsize_z);
415 for k in 0..nz {
416 let idx = if reverse { nz - 1 - k } else { k };
417 w[idx3d(i, j, idx, nx, ny)] = wline[k];
418 }
419 } else {
420 box_filter_line(&mut line, bsize_z);
421 }
422
423 for k in 0..nz {
424 let idx = if reverse { nz - 1 - k } else { k };
425 result[idx3d(i, j, idx, nx, ny)] = line[k];
426 }
427 }
428 }
429 }
430 }
431
432 result
433}
434
435pub fn gaussian_smooth_3d_boxsizes(
437 data: &[f64],
438 boxsizes: &[Vec<usize>],
439 nbox: usize,
440 grid: &Grid,
441) -> Vec<f64> {
442 let (nx, ny, nz) = grid.dims;
443 let mut result = data.to_vec();
444
445 for ibox in 0..nbox {
447 if nx > 1 && ibox < boxsizes[0].len() {
449 let bsize = boxsizes[0][ibox];
450 if bsize >= 3 {
451 for k in 0..nz {
452 for j in 0..ny {
453 let mut line: Vec<f64> = (0..nx).map(|i| result[idx3d(i, j, k, nx, ny)]).collect();
454 box_filter_line(&mut line, bsize);
455 for i in 0..nx {
456 result[idx3d(i, j, k, nx, ny)] = line[i];
457 }
458 }
459 }
460 }
461 }
462
463 if ny > 1 && ibox < boxsizes[1].len() {
465 let bsize = boxsizes[1][ibox];
466 if bsize >= 3 {
467 for k in 0..nz {
468 for i in 0..nx {
469 let mut line: Vec<f64> = (0..ny).map(|j| result[idx3d(i, j, k, nx, ny)]).collect();
470 box_filter_line(&mut line, bsize);
471 for j in 0..ny {
472 result[idx3d(i, j, k, nx, ny)] = line[j];
473 }
474 }
475 }
476 }
477 }
478
479 if nz > 1 && ibox < boxsizes[2].len() {
481 let bsize = boxsizes[2][ibox];
482 if bsize >= 3 {
483 for j in 0..ny {
484 for i in 0..nx {
485 let mut line: Vec<f64> = (0..nz).map(|k| result[idx3d(i, j, k, nx, ny)]).collect();
486 box_filter_line(&mut line, bsize);
487 for k in 0..nz {
488 result[idx3d(i, j, k, nx, ny)] = line[k];
489 }
490 }
491 }
492 }
493 }
494 }
495
496 result
497}
498
499fn flood_fill_component(
505 mask: &[u8],
506 visited: &mut [bool],
507 start: usize,
508 nx: usize, ny: usize, nz: usize,
509) -> Vec<usize> {
510 let mut component = Vec::new();
511 let mut stack = vec![start];
512
513 while let Some(idx) = stack.pop() {
514 if visited[idx] || mask[idx] != 0 {
515 continue;
516 }
517
518 visited[idx] = true;
519 component.push(idx);
520
521 let k = idx / (nx * ny);
523 let rem = idx % (nx * ny);
524 let j = rem / nx;
525 let i = rem % nx;
526
527 if i > 0 {
529 let n = idx3d(i - 1, j, k, nx, ny);
530 if !visited[n] && mask[n] == 0 { stack.push(n); }
531 }
532 if i + 1 < nx {
533 let n = idx3d(i + 1, j, k, nx, ny);
534 if !visited[n] && mask[n] == 0 { stack.push(n); }
535 }
536 if j > 0 {
537 let n = idx3d(i, j - 1, k, nx, ny);
538 if !visited[n] && mask[n] == 0 { stack.push(n); }
539 }
540 if j + 1 < ny {
541 let n = idx3d(i, j + 1, k, nx, ny);
542 if !visited[n] && mask[n] == 0 { stack.push(n); }
543 }
544 if k > 0 {
545 let n = idx3d(i, j, k - 1, nx, ny);
546 if !visited[n] && mask[n] == 0 { stack.push(n); }
547 }
548 if k + 1 < nz {
549 let n = idx3d(i, j, k + 1, nx, ny);
550 if !visited[n] && mask[n] == 0 { stack.push(n); }
551 }
552 }
553
554 component
555}
556
557pub fn fill_holes(mask: &[u8], grid: &Grid, max_hole_size: usize) -> Vec<u8> {
563 let (nx, ny, nz) = grid.dims;
564 let n_total = nx * ny * nz;
565 let mut result = mask.to_vec();
566 let mut visited = vec![false; n_total];
567
568 for idx in 0..n_total {
570 if mask[idx] == 0 && !visited[idx] {
571 let component = flood_fill_component(mask, &mut visited, idx, nx, ny, nz);
572
573 let mut touches_boundary = false;
575 for &cidx in &component {
576 let k = cidx / (nx * ny);
577 let rem = cidx % (nx * ny);
578 let j = rem / nx;
579 let i = rem % nx;
580
581 if i == 0 || i == nx - 1 || j == 0 || j == ny - 1 || k == 0 || k == nz - 1 {
582 touches_boundary = true;
583 break;
584 }
585 }
586
587 if !touches_boundary && component.len() <= max_hole_size {
589 for cidx in component {
590 result[cidx] = 1;
591 }
592 }
593 }
594 }
595
596 result
597}
598
599pub fn robust_mask(mag: &[f64], grid: &Grid) -> Vec<u8> {
608 let (nx, ny, nz) = grid.dims;
609 let n_total = nx * ny * nz;
610
611 let mut samples: Vec<f64> = mag.iter()
613 .filter(|&&v| v.is_finite() && v > 0.0)
614 .copied()
615 .collect();
616
617 if samples.is_empty() {
618 return vec![0u8; n_total];
619 }
620
621 samples.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
622
623 let len = samples.len();
624
625 let q05_idx = ((0.05 * len as f64) as usize).min(len - 1);
627 let q15_idx = ((0.15 * len as f64) as usize).min(len - 1);
628 let q80_idx = ((0.80 * len as f64) as usize).min(len - 1);
629 let q99_idx = ((0.99 * len as f64) as usize).min(len - 1);
630
631 let q05 = samples[q05_idx];
632 let q15 = samples[q15_idx];
633 let q80 = samples[q80_idx];
634 let q99 = samples[q99_idx];
635
636 let high_samples: Vec<f64> = samples.iter()
638 .filter(|&&v| v >= q80 && v <= q99)
639 .copied()
640 .collect();
641
642 let high_intensity = if high_samples.is_empty() {
643 q99
644 } else {
645 high_samples.iter().sum::<f64>() / high_samples.len() as f64
646 };
647
648 let low_samples: Vec<f64> = samples.iter()
650 .filter(|&&v| v <= q15)
651 .copied()
652 .collect();
653
654 let mut noise = if low_samples.is_empty() {
655 0.0
656 } else {
657 low_samples.iter().sum::<f64>() / low_samples.len() as f64
658 };
659
660 if noise > high_intensity / 10.0 {
662 let very_low_samples: Vec<f64> = samples.iter()
663 .filter(|&&v| v <= q05)
664 .copied()
665 .collect();
666
667 noise = if very_low_samples.is_empty() {
668 0.0
669 } else {
670 very_low_samples.iter().sum::<f64>() / very_low_samples.len() as f64
671 };
672
673 if noise > high_intensity / 10.0 {
674 noise = 0.0;
675 }
676 }
677
678 let threshold = (5.0 * noise).max(high_intensity / 5.0);
680
681 let mut mask_f64: Vec<f64> = mag.iter()
683 .map(|&v| if v > threshold { 1.0 } else { 0.0 })
684 .collect();
685
686 let boxsizes1 = vec![vec![5], vec![5], vec![5]];
688 mask_f64 = gaussian_smooth_3d_boxsizes(&mask_f64, &boxsizes1, 1, grid);
689 let mut mask: Vec<u8> = mask_f64.iter()
690 .map(|&v| if v > 0.4 { 1 } else { 0 })
691 .collect();
692
693 let max_hole_size = n_total / 20;
695 mask = fill_holes(&mask, grid, max_hole_size);
696
697 mask_f64 = mask.iter().map(|&v| v as f64).collect();
699 let boxsizes2 = vec![vec![3, 3], vec![3, 3], vec![3, 3]];
700 mask_f64 = gaussian_smooth_3d_boxsizes(&mask_f64, &boxsizes2, 2, grid);
701 mask = mask_f64.iter()
702 .map(|&v| if v > 0.6 { 1 } else { 0 })
703 .collect();
704
705 mask
706}
707
708fn box_segment(
717 image: &[f64],
718 mask: &[u8],
719 nbox: usize,
720 nx: usize, ny: usize, nz: usize,
721) -> Vec<u8> {
722 let n_total = nx * ny * nz;
723 let mut vote_count = vec![0u8; n_total];
724
725 let box_shift_x = nx.div_ceil(nbox);
727 let box_shift_y = ny.div_ceil(nbox);
728 let box_shift_z = nz.div_ceil(nbox);
729
730 let mut cz = 0;
732 while cz < nz {
733 let mut cy = 0;
734 while cy < ny {
735 let mut cx = 0;
736 while cx < nx {
737 let x_start = cx.saturating_sub(box_shift_x);
739 let x_end = (cx + box_shift_x).min(nx);
740 let y_start = cy.saturating_sub(box_shift_y);
741 let y_end = (cy + box_shift_y).min(ny);
742 let z_start = cz.saturating_sub(box_shift_z);
743 let z_end = (cz + box_shift_z).min(nz);
744
745 let mut box_vals: Vec<f64> = Vec::new();
747 for z in z_start..z_end {
748 for y in y_start..y_end {
749 for x in x_start..x_end {
750 let idx = idx3d(x, y, z, nx, ny);
751 if mask[idx] > 0 && image[idx].is_finite() {
752 box_vals.push(image[idx]);
753 }
754 }
755 }
756 }
757
758 if box_vals.is_empty() {
759 cx += box_shift_x;
760 continue;
761 }
762
763 box_vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
765 let q90_idx = ((0.9 * box_vals.len() as f64) as usize).min(box_vals.len() - 1);
766 let q90 = box_vals[q90_idx];
767
768 let width = 0.1;
770 let low = (1.0 - width) * q90;
771 let high = (1.0 + width) * q90;
772
773 for z in z_start..z_end {
775 for y in y_start..y_end {
776 for x in x_start..x_end {
777 let idx = idx3d(x, y, z, nx, ny);
778 if mask[idx] > 0 {
779 let v = image[idx];
780 if v > low && v < high {
781 vote_count[idx] = vote_count[idx].saturating_add(1);
782 }
783 }
784 }
785 }
786 }
787
788 cx += box_shift_x;
789 }
790 cy += box_shift_y;
791 }
792 cz += box_shift_z;
793 }
794
795 let mut segmented = vec![0u8; n_total];
797 for i in 0..n_total {
798 if vote_count[i] >= 2 && mask[i] > 0 {
799 segmented[i] = 1;
800 }
801 }
802
803 segmented
804}
805
806fn fill_and_smooth(
815 lowpass: &mut [f64],
816 stable_mean: f64,
817 sigma2: [f64; 3],
818 grid: &Grid,
819) {
820 let n_total = grid.n_total();
821
822 let mut weight = vec![1.2f64; n_total];
825
826 for i in 0..n_total {
827 if lowpass[i] < stable_mean / 4.0 ||
828 lowpass[i].is_nan() ||
829 lowpass[i] > 10.0 * stable_mean {
830 lowpass[i] = 3.0 * stable_mean;
831 weight[i] = 0.2; }
833 }
834
835 let nbox = 3; let smoothed = gaussian_smooth_3d(lowpass, sigma2, None, Some(&mut weight), nbox, grid);
838 lowpass.copy_from_slice(&smoothed);
839}
840
841pub fn get_sensitivity(
850 mag: &[f64],
851 grid: &Grid,
852 sigma_mm: f64,
853 nbox: usize,
854) -> Vec<f64> {
855 let (nx, ny, nz) = grid.dims;
856 let (vx, vy, vz) = grid.voxel_size;
857 let n_total = nx * ny * nz;
858
859 let sigma = [sigma_mm / vx, sigma_mm / vy, sigma_mm / vz];
861
862 let mask = robust_mask(mag, grid);
864
865 let segmentation = box_segment(mag, &mask, nbox, nx, ny, nz);
867
868 let factor: f64 = 0.7;
870 let sigma1 = [
871 (1.0_f64 - factor * factor).sqrt() * sigma[0],
872 (1.0_f64 - factor * factor).sqrt() * sigma[1],
873 (1.0_f64 - factor * factor).sqrt() * sigma[2],
874 ];
875 let sigma2 = [
876 factor * sigma[0],
877 factor * sigma[1],
878 factor * sigma[2],
879 ];
880
881 let mut lowpass = gaussian_smooth_3d(mag, sigma1, Some(&segmentation), None, 8, grid);
883
884 let mut sum = 0.0;
886 let mut count = 0usize;
887 for i in 0..n_total {
888 if mask[i] > 0 && mag[i].is_finite() {
889 sum += mag[i];
890 count += 1;
891 }
892 }
893 let stable_mean = if count > 0 { sum / count as f64 } else { 1.0 };
894
895 fill_and_smooth(&mut lowpass, stable_mean, sigma2, grid);
897
898 lowpass
899}
900
901pub fn makehomogeneous(
915 mag: &[f64],
916 grid: &Grid,
917 sigma_mm: f64,
918 nbox: usize,
919) -> Vec<f64> {
920 let sensitivity = get_sensitivity(mag, grid, sigma_mm, nbox);
921 let n_total = grid.n_total();
922
923 let mut result = vec![0.0; n_total];
924 for i in 0..n_total {
925 if sensitivity[i] > 1e-10 && !sensitivity[i].is_nan() {
926 result[i] = mag[i] / sensitivity[i];
927 } else {
928 result[i] = mag[i];
929 }
930 }
931
932 result
933}
934
935pub fn rss_combine(
947 mags_flat: &[f64],
948 n_echoes: usize,
949 n_total: usize,
950) -> Vec<f64> {
951 let mut result = vec![0.0; n_total];
952
953 for e in 0..n_echoes {
954 let offset = e * n_total;
955 for i in 0..n_total {
956 let v = mags_flat[offset + i];
957 result[i] += v * v;
958 }
959 }
960
961 for i in 0..n_total {
962 result[i] = result[i].sqrt();
963 }
964
965 result
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971
972 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
973 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
974 }
975
976 #[test]
977 fn test_get_box_sizes() {
978 let sizes = get_box_sizes(5.0, 3);
980 assert_eq!(sizes.len(), 3);
981 for &s in &sizes {
984 assert!(s % 2 == 1, "Box size should be odd, got {}", s);
985 assert!(s >= 3 && s <= 11, "Box size should be in reasonable range, got {}", s);
986 }
987 }
988
989 #[test]
990 fn test_box_filter_line() {
991 let mut line = vec![1.0; 10];
993 box_filter_line(&mut line, 3);
994 for &v in &line {
995 assert!((v - 1.0).abs() < 1e-10, "Uniform line should stay uniform");
996 }
997 }
998
999 #[test]
1000 fn test_fill_holes_basic() {
1001 let mut mask = vec![1u8; 27];
1003 mask[13] = 0; let filled = fill_holes(&mask, &grid(3, 3, 3), 5);
1006 assert_eq!(filled[13], 1, "Center hole should be filled");
1007 }
1008
1009 #[test]
1010 fn test_robust_mask_basic() {
1011 let mag = vec![100.0; 27];
1013 let mask = robust_mask(&mag, &grid(3, 3, 3));
1014 let masked_count: usize = mask.iter().map(|&v| v as usize).sum();
1016 assert!(masked_count > 0, "Should have some masked voxels");
1017 }
1018
1019 #[test]
1020 fn test_rss_combine() {
1021 let mags = vec![
1023 3.0, 0.0, 0.0, 5.0, 4.0, 0.0, 0.0, 12.0, ];
1026 let result = rss_combine(&mags, 2, 4);
1027
1028 assert!((result[0] - 5.0).abs() < 1e-10);
1030 assert!((result[1] - 0.0).abs() < 1e-10);
1032 assert!((result[3] - 13.0).abs() < 1e-10);
1034 }
1035
1036 fn make_sphere_phantom(n: usize, bias: bool) -> (Vec<f64>, Vec<u8>) {
1043 let center = n as f64 / 2.0;
1044 let radius = n as f64 / 2.0 - 2.0;
1045 let n_total = n * n * n;
1046 let mut mag = vec![0.0f64; n_total];
1047 let mut mask = vec![0u8; n_total];
1048
1049 for k in 0..n {
1050 for j in 0..n {
1051 for i in 0..n {
1052 let dx = i as f64 - center;
1053 let dy = j as f64 - center;
1054 let dz = k as f64 - center;
1055 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
1056 let idx = i + j * n + k * n * n;
1057
1058 if dist < radius {
1059 let base = 100.0;
1061 let bias_val = if bias {
1063 1.0 + 0.5 * (i as f64 / n as f64)
1064 } else {
1065 1.0
1066 };
1067 mag[idx] = base * bias_val;
1068 mask[idx] = 1;
1069 } else {
1070 mag[idx] = 1.0 + 0.5 * ((i + j + k) % 3) as f64;
1072 }
1073 }
1074 }
1075 }
1076
1077 (mag, mask)
1078 }
1079
1080 #[test]
1085 fn test_get_box_sizes_zero_sigma() {
1086 let sizes = get_box_sizes(0.0, 3);
1087 assert_eq!(sizes, vec![0, 0, 0]);
1088 }
1089
1090 #[test]
1091 fn test_get_box_sizes_zero_n() {
1092 let sizes = get_box_sizes(5.0, 0);
1093 assert!(sizes.is_empty());
1094 }
1095
1096 #[test]
1097 fn test_get_box_sizes_negative_sigma() {
1098 let sizes = get_box_sizes(-1.0, 3);
1099 assert_eq!(sizes, vec![0, 0, 0]);
1100 }
1101
1102 #[test]
1103 fn test_get_box_sizes_large_sigma() {
1104 let sizes = get_box_sizes(20.0, 4);
1105 assert_eq!(sizes.len(), 4);
1106 for &s in &sizes {
1107 assert!(s % 2 == 1, "Box size should be odd, got {}", s);
1108 assert!(s >= 3, "Box size should be at least 3, got {}", s);
1109 }
1110 }
1111
1112 #[test]
1117 fn test_check_box_sizes_clamps_to_half_image() {
1118 let mut boxsizes = vec![vec![99], vec![99], vec![99]];
1120 let dims = [10, 10, 10];
1121 check_box_sizes(&mut boxsizes, &dims);
1122 for bs in &boxsizes {
1123 for &b in bs {
1124 assert!(b <= dims[0], "Box size should be clamped, got {}", b);
1125 assert!(b % 2 == 1, "Box size should be odd, got {}", b);
1126 }
1127 }
1128 }
1129
1130 #[test]
1131 fn test_check_box_sizes_makes_even_odd() {
1132 let mut boxsizes = vec![vec![4], vec![6], vec![8]];
1133 let dims = [100, 100, 100];
1134 check_box_sizes(&mut boxsizes, &dims);
1135 for bs in &boxsizes {
1136 for &b in bs {
1137 assert!(b % 2 == 1, "Box size should be odd, got {}", b);
1138 }
1139 }
1140 }
1141
1142 #[test]
1147 fn test_box_filter_line_too_small_boxsize() {
1148 let mut line = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1150 let original = line.clone();
1151 box_filter_line(&mut line, 1);
1152 assert_eq!(line, original);
1153 }
1154
1155 #[test]
1156 fn test_box_filter_line_larger_than_data() {
1157 let mut line = vec![1.0, 2.0];
1159 let original = line.clone();
1160 box_filter_line(&mut line, 5);
1161 assert_eq!(line, original);
1162 }
1163
1164 #[test]
1165 fn test_box_filter_line_smoothing_effect() {
1166 let mut line = vec![0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1168 box_filter_line(&mut line, 5);
1169 assert!(line[3] < 10.0, "Spike should be smoothed");
1171 assert!(line[2] > 0.0 || line[4] > 0.0, "Neighbors should gain intensity");
1173 for &v in &line {
1175 assert!(v.is_finite(), "All values should be finite");
1176 }
1177 }
1178
1179 #[test]
1184 fn test_box_filter_line_weighted_too_small() {
1185 let mut line = vec![1.0, 2.0];
1186 let mut weight = vec![1.0, 1.0];
1187 let orig_l = line.clone();
1188 let orig_w = weight.clone();
1189 box_filter_line_weighted(&mut line, &mut weight, 1);
1190 assert_eq!(line, orig_l);
1191 assert_eq!(weight, orig_w);
1192 }
1193
1194 #[test]
1195 fn test_box_filter_line_weighted_uniform() {
1196 let mut line = vec![5.0; 10];
1197 let mut weight = vec![1.0; 10];
1198 box_filter_line_weighted(&mut line, &mut weight, 3);
1199 for i in 2..8 {
1202 assert!(
1203 (line[i] - 5.0).abs() < 0.5,
1204 "Uniform weighted line should stay near 5.0, got {} at index {}",
1205 line[i], i
1206 );
1207 }
1208 }
1209
1210 #[test]
1211 fn test_box_filter_line_weighted_varying_weights() {
1212 let mut line = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1213 let mut weight = vec![1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0];
1214 box_filter_line_weighted(&mut line, &mut weight, 3);
1215 for &v in &line {
1217 assert!(v.is_finite(), "All values should be finite after weighted filter");
1218 }
1219 for &w in &weight {
1220 assert!(w.is_finite(), "All weights should be finite after weighted filter");
1221 }
1222 }
1223
1224 #[test]
1229 fn test_nan_box_filter_line_too_small() {
1230 let mut line = vec![1.0, 2.0];
1231 let original = line.clone();
1232 nan_box_filter_line(&mut line, 5);
1233 assert_eq!(line, original);
1234 }
1235
1236 #[test]
1237 fn test_nan_box_filter_line_no_nans() {
1238 let mut line = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1240 nan_box_filter_line(&mut line, 3);
1241 for &v in &line {
1243 assert!(v.is_finite(), "All values should be finite after nan box filter");
1244 }
1245 }
1249
1250 #[test]
1251 fn test_nan_box_filter_line_with_nans() {
1252 let mut line = vec![5.0, 5.0, 5.0, f64::NAN, f64::NAN, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0];
1254 nan_box_filter_line(&mut line, 3);
1255 let finite_count = line.iter().filter(|v| v.is_finite()).count();
1257 assert!(finite_count >= 6, "Most values should be finite, got {}", finite_count);
1258 }
1259
1260 #[test]
1261 fn test_nan_box_filter_line_all_nan_except_edges() {
1262 let mut line = vec![1.0; 12];
1264 for i in 2..10 {
1265 line[i] = f64::NAN;
1266 }
1267 nan_box_filter_line(&mut line, 3);
1268 assert!(line[0].is_finite() || line[0].is_nan());
1270 }
1271
1272 #[test]
1277 fn test_gaussian_smooth_3d_uniform_no_mask() {
1278 let n = 8;
1279 let data = vec![5.0; n * n * n];
1280 let result = gaussian_smooth_3d(&data, [2.0, 2.0, 2.0], None, None, 3, &grid(n, n, n));
1281 assert_eq!(result.len(), n * n * n);
1282 for &v in &result {
1284 assert!(
1285 (v - 5.0).abs() < 0.5,
1286 "Uniform data should stay near 5.0, got {}",
1287 v
1288 );
1289 }
1290 }
1291
1292 #[test]
1293 fn test_gaussian_smooth_3d_with_mask() {
1294 let n = 10;
1295 let (mag, mask) = make_sphere_phantom(n, false);
1296 let result = gaussian_smooth_3d(&mag, [1.5, 1.5, 1.5], Some(&mask), None, 4, &grid(n, n, n));
1298 assert_eq!(result.len(), n * n * n);
1299 for i in 0..result.len() {
1301 if mask[i] > 0 {
1302 assert!(result[i].is_finite(), "Masked voxel at {} should be finite", i);
1303 }
1304 }
1305 }
1306
1307 #[test]
1308 fn test_gaussian_smooth_3d_with_weights() {
1309 let n = 8;
1310 let data = vec![10.0; n * n * n];
1311 let mut weight = vec![1.0; n * n * n];
1312 let result = gaussian_smooth_3d(
1313 &data, [1.5, 1.5, 1.5], None, Some(&mut weight), 3, &grid(n, n, n),
1314 );
1315 assert_eq!(result.len(), n * n * n);
1316 for &v in &result {
1317 assert!(v.is_finite(), "Result should be finite");
1318 }
1319 }
1320
1321 #[test]
1326 fn test_gaussian_smooth_3d_boxsizes_uniform() {
1327 let n = 8;
1328 let data = vec![3.0; n * n * n];
1329 let boxsizes = vec![vec![3, 3], vec![3, 3], vec![3, 3]];
1330 let result = gaussian_smooth_3d_boxsizes(&data, &boxsizes, 2, &grid(n, n, n));
1331 assert_eq!(result.len(), n * n * n);
1332 for &v in &result {
1333 assert!(
1334 (v - 3.0).abs() < 0.5,
1335 "Uniform data should stay near 3.0, got {}",
1336 v
1337 );
1338 }
1339 }
1340
1341 #[test]
1342 fn test_gaussian_smooth_3d_boxsizes_spike() {
1343 let n = 10;
1344 let n_total = n * n * n;
1345 let mut data = vec![0.0; n_total];
1346 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
1348 data[center] = 100.0;
1349 let boxsizes = vec![vec![5, 5], vec![5, 5], vec![5, 5]];
1350 let result = gaussian_smooth_3d_boxsizes(&data, &boxsizes, 2, &grid(n, n, n));
1351 assert_eq!(result.len(), n_total);
1352 assert!(
1354 result[center] < 100.0,
1355 "Spike should be smoothed, got {}",
1356 result[center]
1357 );
1358 for &v in &result {
1360 assert!(v.is_finite(), "All values should be finite");
1361 }
1362 }
1363
1364 #[test]
1369 fn test_fill_holes_boundary_hole_not_filled() {
1370 let mut mask = vec![1u8; 125]; mask[0] = 0; let filled = fill_holes(&mask, &grid(5, 5, 5), 100);
1374 assert_eq!(filled[0], 0, "Boundary hole should not be filled");
1375 }
1376
1377 #[test]
1378 fn test_fill_holes_large_hole_not_filled() {
1379 let n = 7;
1381 let n_total = n * n * n;
1382 let mut mask = vec![1u8; n_total];
1383 for k in 2..5 {
1385 for j in 2..5 {
1386 for i in 2..5 {
1387 mask[i + j * n + k * n * n] = 0;
1388 }
1389 }
1390 }
1391 let filled = fill_holes(&mask, &grid(n, n, n), 5); let center = 3 + 3 * n + 3 * n * n;
1394 assert_eq!(filled[center], 0, "Large hole should not be filled");
1395 }
1396
1397 #[test]
1402 fn test_robust_mask_sphere() {
1403 let n = 12;
1404 let (mag, _) = make_sphere_phantom(n, false);
1405 let mask = robust_mask(&mag, &grid(n, n, n));
1406 assert_eq!(mask.len(), n * n * n);
1407 let masked_count: usize = mask.iter().map(|&v| v as usize).sum();
1408 assert!(masked_count > 0, "Should have masked voxels for sphere phantom");
1410 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
1412 assert_eq!(mask[center], 1, "Center of sphere should be masked");
1413 }
1414
1415 #[test]
1416 fn test_robust_mask_empty() {
1417 let mag = vec![0.0; 27];
1419 let mask = robust_mask(&mag, &grid(3, 3, 3));
1420 let count: usize = mask.iter().map(|&v| v as usize).sum();
1421 assert_eq!(count, 0, "Zero magnitude should produce empty mask");
1422 }
1423
1424 #[test]
1425 fn test_robust_mask_nan_values() {
1426 let mut mag = vec![100.0; 125];
1428 mag[0] = f64::NAN;
1429 mag[10] = f64::NAN;
1430 mag[50] = f64::INFINITY;
1431 let mask = robust_mask(&mag, &grid(5, 5, 5));
1432 assert_eq!(mask.len(), 125);
1433 for &v in &mask {
1435 assert!(v == 0 || v == 1, "Mask values should be 0 or 1");
1436 }
1437 }
1438
1439 #[test]
1444 fn test_box_segment_uniform() {
1445 let n = 10;
1446 let (mag, mask) = make_sphere_phantom(n, false);
1447 let seg = box_segment(&mag, &mask, 3, n, n, n);
1448 assert_eq!(seg.len(), n * n * n);
1449 let seg_count: usize = seg.iter().map(|&v| v as usize).sum();
1451 assert!(seg_count > 0, "Box segment should find some tissue voxels");
1452 }
1453
1454 #[test]
1455 fn test_box_segment_empty_mask() {
1456 let n = 8;
1457 let mag = vec![100.0; n * n * n];
1458 let mask = vec![0u8; n * n * n]; let seg = box_segment(&mag, &mask, 3, n, n, n);
1460 let count: usize = seg.iter().map(|&v| v as usize).sum();
1461 assert_eq!(count, 0, "Empty mask should produce no segmentation");
1462 }
1463
1464 #[test]
1469 fn test_fill_and_smooth_basic() {
1470 let n = 10;
1471 let n_total = n * n * n;
1472 let stable_mean = 100.0;
1473 let mut lowpass = vec![stable_mean; n_total];
1474 lowpass[0] = 0.0;
1476 lowpass[100] = f64::NAN;
1477 lowpass[200] = 2000.0; let sigma2 = [2.0, 2.0, 2.0];
1480 fill_and_smooth(&mut lowpass, stable_mean, sigma2, &grid(n, n, n));
1481
1482 for (i, &v) in lowpass.iter().enumerate() {
1484 assert!(v.is_finite(), "Value at {} should be finite, got {}", i, v);
1485 }
1486 }
1487
1488 #[test]
1489 fn test_fill_and_smooth_preserves_approximate_mean() {
1490 let n = 8;
1491 let n_total = n * n * n;
1492 let stable_mean = 50.0;
1493 let mut lowpass = vec![stable_mean; n_total];
1494 let sigma2 = [1.5, 1.5, 1.5];
1495 fill_and_smooth(&mut lowpass, stable_mean, sigma2, &grid(n, n, n));
1496
1497 let mean: f64 = lowpass.iter().sum::<f64>() / n_total as f64;
1499 assert!(
1500 (mean - stable_mean).abs() < stable_mean * 0.5,
1501 "Mean should be approximately preserved, got {} vs {}",
1502 mean,
1503 stable_mean
1504 );
1505 }
1506
1507 #[test]
1512 fn test_get_sensitivity_sphere() {
1513 let n = 12;
1514 let (mag, _) = make_sphere_phantom(n, true);
1515 let sensitivity = get_sensitivity(&mag, &grid(n, n, n), 4.0, 5);
1516 assert_eq!(sensitivity.len(), n * n * n);
1517 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
1519 assert!(
1520 sensitivity[center].is_finite(),
1521 "Sensitivity at center should be finite"
1522 );
1523 assert!(
1524 sensitivity[center] > 0.0,
1525 "Sensitivity at center should be positive, got {}",
1526 sensitivity[center]
1527 );
1528 }
1529
1530 #[test]
1531 fn test_get_sensitivity_output_size() {
1532 let n = 10;
1533 let (mag, _) = make_sphere_phantom(n, false);
1534 let sensitivity = get_sensitivity(&mag, &grid(n, n, n), 3.0, 3);
1535 assert_eq!(sensitivity.len(), n * n * n);
1536 }
1537
1538 #[test]
1543 fn test_makehomogeneous_output_size_and_finite() {
1544 let n = 12;
1545 let (mag, _) = make_sphere_phantom(n, true);
1546 let result = makehomogeneous(&mag, &grid(n, n, n), 4.0, 5);
1547 assert_eq!(result.len(), n * n * n);
1548 for (i, &v) in result.iter().enumerate() {
1550 assert!(v.is_finite(), "Output at {} should be finite, got {}", i, v);
1551 }
1552 }
1553
1554 #[test]
1555 fn test_makehomogeneous_reduces_bias() {
1556 let n = 12;
1557 let (mag_biased, mask) = make_sphere_phantom(n, true);
1558 let result = makehomogeneous(&mag_biased, &grid(n, n, n), 4.0, 5);
1559
1560 let mut original_vals = Vec::new();
1562 let mut corrected_vals = Vec::new();
1563 for i in 0..(n * n * n) {
1564 if mask[i] > 0 {
1565 original_vals.push(mag_biased[i]);
1566 corrected_vals.push(result[i]);
1567 }
1568 }
1569
1570 let orig_mean = original_vals.iter().sum::<f64>() / original_vals.len() as f64;
1572 let orig_std = (original_vals
1573 .iter()
1574 .map(|v| (v - orig_mean).powi(2))
1575 .sum::<f64>()
1576 / original_vals.len() as f64)
1577 .sqrt();
1578 let orig_cv = orig_std / orig_mean;
1579
1580 let corr_mean = corrected_vals.iter().sum::<f64>() / corrected_vals.len() as f64;
1581 let corr_std = (corrected_vals
1582 .iter()
1583 .map(|v| (v - corr_mean).powi(2))
1584 .sum::<f64>()
1585 / corrected_vals.len() as f64)
1586 .sqrt();
1587 let corr_cv = corr_std / corr_mean;
1588
1589 assert!(
1590 corr_cv <= orig_cv * 2.0,
1591 "Corrected CV ({}) should not be much worse than original CV ({})",
1592 corr_cv,
1593 orig_cv
1594 );
1595 }
1596
1597 #[test]
1598 fn test_makehomogeneous_no_bias_preserves() {
1599 let n = 12;
1600 let (mag, mask) = make_sphere_phantom(n, false);
1601 let result = makehomogeneous(&mag, &grid(n, n, n), 4.0, 5);
1602
1603 for i in 0..(n * n * n) {
1605 if mask[i] > 0 {
1606 assert!(
1607 result[i] > 0.0,
1608 "Inside sphere voxel {} should be positive, got {}",
1609 i,
1610 result[i]
1611 );
1612 }
1613 }
1614 }
1615
1616 #[test]
1617 fn test_makehomogeneous_anisotropic_voxels() {
1618 let n = 12;
1619 let (mag, _) = make_sphere_phantom(n, true);
1620 let result = makehomogeneous(&mag, &Grid::new(n, n, n, 0.5, 0.5, 2.0), 4.0, 5);
1622 assert_eq!(result.len(), n * n * n);
1623 for &v in &result {
1624 assert!(v.is_finite(), "All output values should be finite");
1625 }
1626 }
1627
1628 #[test]
1633 fn test_flood_fill_component_single() {
1634 let mask = vec![1u8; 27]; let mut visited = vec![false; 27];
1636 let component = flood_fill_component(&mask, &mut visited, 0, 3, 3, 3);
1638 assert!(component.is_empty(), "All filled mask should have no zero component");
1639 }
1640
1641 #[test]
1642 fn test_flood_fill_component_connected_zeros() {
1643 let n = 5;
1644 let n_total = n * n * n;
1645 let mut mask = vec![1u8; n_total];
1646 for i in 1..4 {
1648 mask[i + 2 * n + 2 * n * n] = 0;
1649 }
1650 let mut visited = vec![false; n_total];
1651 let start = 1 + 2 * n + 2 * n * n;
1652 let component = flood_fill_component(&mask, &mut visited, start, n, n, n);
1653 assert_eq!(component.len(), 3, "Should find 3 connected zero voxels");
1654 }
1655
1656 #[test]
1661 fn test_gaussian_smooth_3d_mask_reverse_passes() {
1662 let n = 10;
1664 let (mag, mask) = make_sphere_phantom(n, false);
1665 let result = gaussian_smooth_3d(&mag, [1.5, 1.5, 1.5], Some(&mask), None, 4, &grid(n, n, n));
1667 assert_eq!(result.len(), n * n * n);
1668 for i in 0..result.len() {
1670 if mask[i] > 0 {
1671 }
1673 }
1674 let finite_count = result.iter().filter(|v| v.is_finite()).count();
1676 assert!(finite_count > 0, "Should have some finite values");
1677 }
1678}