1use super::config::*;
42use super::phase_utils::rss_combine;
43use crate::utils::morphology::{erode_mask, dilate_mask, morphological_close, fill_holes, signal_gated_erosion};
44
45fn check_volumes(name: &str, volumes: &[&[f64]], n_voxels: usize) -> Result<(), PipelineError> {
47 for (e, v) in volumes.iter().enumerate() {
48 if v.len() != n_voxels {
49 return Err(PipelineError::InvalidInput(format!(
50 "{name} echo {} has {} voxels, the grid has {n_voxels}", e + 1, v.len()
51 )));
52 }
53 }
54 Ok(())
55}
56
57pub fn section_magnitude(
68 input: MaskingInput,
69 magnitudes: &[&[f64]],
70 metadata: &ScanMetadata,
71) -> Result<Option<Vec<f64>>, PipelineError> {
72 check_volumes("magnitude", magnitudes, metadata.grid().n_total())?;
73 let (Some(first), Some(last)) = (magnitudes.first(), magnitudes.last()) else {
74 return Ok(None);
75 };
76 Ok(Some(match input {
77 MaskingInput::MagnitudeFirst => first.to_vec(),
78 MaskingInput::MagnitudeLast => last.to_vec(),
79 MaskingInput::Magnitude | MaskingInput::PhaseQuality => rss_combine(magnitudes)?,
80 }))
81}
82
83fn section_input(
86 input: MaskingInput,
87 phases: &[&[f64]],
88 magnitude: Option<&[f64]>,
89 metadata: &ScanMetadata,
90) -> Result<Vec<f64>, PipelineError> {
91 match input {
92 MaskingInput::MagnitudeFirst | MaskingInput::Magnitude | MaskingInput::MagnitudeLast => {
93 magnitude.map(<[f64]>::to_vec).ok_or_else(|| {
94 PipelineError::InvalidInput(format!("a {input:?} mask section needs magnitude data"))
95 })
96 }
97 MaskingInput::PhaseQuality => {
98 let n_voxels = metadata.grid().n_total();
99 check_volumes("phase", phases, n_voxels)?;
100 let Some(&phase1) = phases.first() else {
101 return Err(PipelineError::InvalidInput(
102 "a PhaseQuality mask section needs phase data".into(),
103 ));
104 };
105 let ones;
106 let weight = match magnitude {
107 Some(m) => m,
108 None => {
109 ones = vec![1.0; n_voxels];
110 &ones
111 }
112 };
113 let all_ones = vec![1u8; n_voxels];
114 let grid = metadata.grid();
115 Ok(if phases.len() >= 2 && metadata.echo_times.len() >= 2 {
116 crate::unwrap::voxel_quality_romeo(
117 phase1, weight,
118 Some(phases[1]),
119 metadata.echo_times[0], metadata.echo_times[1],
120 &all_ones, &grid,
121 )
122 } else {
123 crate::unwrap::voxel_quality_romeo(
124 phase1, weight,
125 None,
126 metadata.echo_times.first().copied().unwrap_or(0.02),
127 0.0, &all_ones, &grid,
128 )
129 })
130 }
131 }
132}
133
134pub fn resolve_masking_input(
146 input: MaskingInput,
147 phases: &[&[f64]],
148 magnitudes: &[&[f64]],
149 metadata: &ScanMetadata,
150) -> Result<Vec<f64>, PipelineError> {
151 let magnitude = section_magnitude(input, magnitudes, metadata)?;
152 section_input(input, phases, magnitude.as_deref(), metadata)
153}
154
155pub fn build_mask_section(
164 section: &MaskSection,
165 phases: &[&[f64]],
166 magnitudes: &[&[f64]],
167 metadata: &ScanMetadata,
168) -> Result<Vec<u8>, PipelineError> {
169 let n_voxels = metadata.dims.0 * metadata.dims.1 * metadata.dims.2;
170 let magnitude = section_magnitude(section.input, magnitudes, metadata)?;
171 let input_data = section_input(section.input, phases, magnitude.as_deref(), metadata)?;
172 apply_mask_ops(vec![1u8; n_voxels], §ion.all_ops(), &input_data, magnitude.as_deref(), metadata)
173}
174
175pub fn apply_mask_ops(
198 mask: Vec<u8>,
199 ops: &[MaskOp],
200 input_data: &[f64],
201 magnitude: Option<&[f64]>,
202 metadata: &ScanMetadata,
203) -> Result<Vec<u8>, PipelineError> {
204 let (nx, ny, nz) = metadata.dims;
205 let grid = metadata.grid();
206 let n_voxels = nx * ny * nz;
207 if mask.len() != n_voxels {
208 return Err(PipelineError::DimensionMismatch { expected: n_voxels, got: mask.len() });
209 }
210 let mut mask = mask;
211
212 for op in ops {
213 match op {
214 MaskOp::Threshold { method, value } => {
215 let threshold = match method {
216 MaskThresholdMethod::Otsu => {
217 crate::utils::otsu_threshold(input_data, 256)
218 }
219 MaskThresholdMethod::Fixed => value.unwrap_or(0.5),
220 MaskThresholdMethod::Percentile => {
221 let pct = value.unwrap_or(75.0) / 100.0;
222 let mut sorted: Vec<f64> = input_data.iter()
223 .filter(|v| v.is_finite() && **v > 0.0)
224 .copied().collect();
225 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
226 if sorted.is_empty() { 0.0 }
227 else {
228 let idx = ((sorted.len() as f64 * pct) as usize).min(sorted.len() - 1);
229 sorted[idx]
230 }
231 }
232 };
233 mask = input_data.iter()
234 .map(|&v| if v > threshold { 1u8 } else { 0u8 })
235 .collect();
236 }
237 MaskOp::Bet(params) => {
238 let mag_data = magnitude.ok_or_else(|| {
239 PipelineError::InvalidInput("BET requires magnitude data".into())
240 })?;
241 let order = crate::bet::FslLeftRightOrder::from_affine(metadata.affine.as_ref());
244 mask = crate::bet::run_bet(mag_data, &grid, params, order, |_, _| {});
245 }
246 MaskOp::Erode { iterations } => {
247 mask = erode_mask(&mask, &grid, *iterations);
248 }
249 MaskOp::Dilate { iterations } => {
250 mask = dilate_mask(&mask, &grid, *iterations);
251 }
252 MaskOp::Close { radius } => {
253 mask = morphological_close(&mask, &grid, *radius as i32);
254 }
255 MaskOp::FillHoles { max_size } => {
256 let effective_size = if *max_size == 0 { n_voxels / 20 } else { *max_size };
257 mask = fill_holes(&mask, &grid, effective_size);
258 }
259 MaskOp::GaussianSmooth { sigma_mm } => {
260 let sigma = *sigma_mm;
261 let mask_f64: Vec<f64> = mask.iter().map(|&m| m as f64).collect();
262 let smoothed = crate::utils::gaussian_smooth_3d(
263 &mask_f64,
264 [sigma, sigma, sigma],
265 None, None, 3,
266 &grid,
267 );
268 mask = smoothed.iter().map(|&v| if v > 0.5 { 1u8 } else { 0u8 }).collect();
269 }
270 MaskOp::SignalErode(params) => {
271 let mag_data = magnitude.ok_or_else(|| {
272 PipelineError::InvalidInput("signal-gated erosion requires magnitude data".into())
273 })?;
274 mask = signal_gated_erosion(&mask, mag_data, &grid, params);
275 }
276 MaskOp::HdBet(params) => {
277 let mag_data = magnitude.ok_or_else(|| {
278 PipelineError::InvalidInput("HD-BET requires magnitude data".into())
279 })?;
280 mask = run_hd_bet(mag_data, &grid, params)?;
281 }
282 MaskOp::Rs2Net(params) => {
283 let mag_data = magnitude.ok_or_else(|| {
284 PipelineError::InvalidInput("RS2-Net requires magnitude data".into())
285 })?;
286 mask = run_rs2_net(mag_data, &grid, params)?;
287 }
288 MaskOp::MindGrab(params) => {
289 let mag_data = magnitude.ok_or_else(|| {
290 PipelineError::InvalidInput("MindGrab requires magnitude data".into())
291 })?;
292 let affine = metadata.affine.as_ref().ok_or_else(|| {
293 PipelineError::InvalidInput("MindGrab requires the image affine".into())
294 })?;
295 mask = run_mindgrab(mag_data, &grid, affine, params)?;
296 }
297 }
298 }
299
300 Ok(mask)
301}
302
303#[cfg(feature = "onnx")]
306fn run_hd_bet(
307 magnitude: &[f64],
308 grid: &crate::Grid,
309 params: &crate::bet::HdBetParams,
310) -> Result<Vec<u8>, PipelineError> {
311 let bytes = crate::models::primary_weight("hd-bet").map_err(PipelineError::InvalidConfig)?;
312 crate::bet::hd_bet(magnitude, grid, &bytes, params, |_, _| {})
313 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
314}
315
316#[cfg(not(feature = "onnx"))]
317fn run_hd_bet(
318 _magnitude: &[f64],
319 _grid: &crate::Grid,
320 _params: &crate::bet::HdBetParams,
321) -> Result<Vec<u8>, PipelineError> {
322 Err(PipelineError::InvalidConfig(
323 "HD-BET requires building qsm-core with the 'onnx' feature".into(),
324 ))
325}
326
327#[cfg(feature = "onnx")]
330fn run_rs2_net(
331 magnitude: &[f64],
332 grid: &crate::Grid,
333 params: &crate::bet::Rs2NetParams,
334) -> Result<Vec<u8>, PipelineError> {
335 let bytes = crate::models::primary_weight("rs2-net").map_err(PipelineError::InvalidConfig)?;
336 crate::bet::rs2_net(magnitude, grid, &bytes, params, |_, _| {})
337 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
338}
339
340#[cfg(not(feature = "onnx"))]
341fn run_rs2_net(
342 _magnitude: &[f64],
343 _grid: &crate::Grid,
344 _params: &crate::bet::Rs2NetParams,
345) -> Result<Vec<u8>, PipelineError> {
346 Err(PipelineError::InvalidConfig(
347 "RS2-Net requires building qsm-core with the 'onnx' feature".into(),
348 ))
349}
350
351#[cfg(feature = "onnx")]
354fn run_mindgrab(
355 magnitude: &[f64],
356 grid: &crate::Grid,
357 affine: &[f64; 16],
358 params: &crate::bet::MindGrabParams,
359) -> Result<Vec<u8>, PipelineError> {
360 let bytes = crate::models::primary_weight("mindgrab").map_err(PipelineError::InvalidConfig)?;
361 crate::bet::mindgrab(magnitude, grid, affine, &bytes, params, |_, _| {})
362 .map_err(|e| PipelineError::AlgorithmError(e.to_string()))
363}
364
365#[cfg(not(feature = "onnx"))]
366fn run_mindgrab(
367 _magnitude: &[f64],
368 _grid: &crate::Grid,
369 _affine: &[f64; 16],
370 _params: &crate::bet::MindGrabParams,
371) -> Result<Vec<u8>, PipelineError> {
372 Err(PipelineError::InvalidConfig(
373 "MindGrab requires building qsm-core with the 'onnx' feature".into(),
374 ))
375}
376
377pub fn run_masking(
389 sections: &[MaskSection],
390 phases: &[&[f64]],
391 magnitudes: &[&[f64]],
392 metadata: &ScanMetadata,
393) -> Result<Vec<u8>, PipelineError> {
394 let (nx, ny, nz) = metadata.dims;
395 let n_voxels = nx * ny * nz;
396
397 if sections.is_empty() {
398 return Err(PipelineError::InvalidConfig("no mask sections configured".into()));
399 }
400
401 let mut final_mask = vec![0u8; n_voxels];
402 for section in sections {
403 let section_mask = build_mask_section(section, phases, magnitudes, metadata)?;
404 for (f, s) in final_mask.iter_mut().zip(§ion_mask) {
405 *f |= s;
406 }
407 }
408
409 Ok(final_mask)
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn test_metadata() -> ScanMetadata {
417 ScanMetadata {
418 dims: (8, 8, 8),
419 voxel_size: (1.0, 1.0, 1.0),
420 echo_times: vec![0.005, 0.010],
421 field_strength: 3.0,
422 b0_direction: (0.0, 0.0, 1.0),
423 slice_geometry: None,
424 affine: None,
425 }
426 }
427
428 #[test]
429 fn test_run_masking_otsu_threshold() {
430 let meta = test_metadata();
431 let n = 8 * 8 * 8;
432 let mut mag = vec![0.1; n];
434 for i in n / 2..n {
435 mag[i] = 10.0;
436 }
437
438 let sections = vec![MaskSection {
439 input: MaskingInput::Magnitude,
440 generator: MaskOp::Threshold {
441 method: MaskThresholdMethod::Otsu,
442 value: None,
443 },
444 refinements: vec![],
445 }];
446
447 let result = run_masking(§ions, &[], &[&mag], &meta).unwrap();
448 assert_eq!(result.len(), n);
449
450 let bright_count: usize = result[n / 2..].iter().map(|&m| m as usize).sum();
452 let dark_count: usize = result[..n / 2].iter().map(|&m| m as usize).sum();
453 assert!(bright_count > dark_count, "Otsu should separate bright from dark");
454 }
455
456 #[test]
457 fn test_run_masking_with_refinements() {
458 let meta = test_metadata();
459 let n = 8 * 8 * 8;
460 let mag = vec![10.0; n]; let sections = vec![MaskSection {
463 input: MaskingInput::Magnitude,
464 generator: MaskOp::Threshold {
465 method: MaskThresholdMethod::Fixed,
466 value: Some(0.5),
467 },
468 refinements: vec![
469 MaskOp::Erode { iterations: 1 },
470 MaskOp::Dilate { iterations: 1 },
471 ],
472 }];
473
474 let result = run_masking(§ions, &[], &[&mag], &meta).unwrap();
475 assert_eq!(result.len(), n);
476 let (nx, ny, nz) = meta.dims;
478 let center = nx / 2 + (ny / 2) * nx + (nz / 2) * nx * ny;
479 assert_eq!(result[center], 1, "center should survive erode+dilate");
480 }
481
482 #[test]
483 fn test_run_masking_or_sections() {
484 let meta = test_metadata();
485 let n = 8 * 8 * 8;
486 let mag = vec![10.0; n];
487
488 let sections = vec![
492 MaskSection {
493 input: MaskingInput::Magnitude,
494 generator: MaskOp::Threshold {
495 method: MaskThresholdMethod::Fixed,
496 value: Some(0.5),
497 },
498 refinements: vec![],
499 },
500 MaskSection {
501 input: MaskingInput::Magnitude,
502 generator: MaskOp::Threshold {
503 method: MaskThresholdMethod::Fixed,
504 value: Some(0.5),
505 },
506 refinements: vec![],
507 },
508 ];
509
510 let result = run_masking(§ions, &[], &[&mag], &meta).unwrap();
511 let count: usize = result.iter().map(|&m| m as usize).sum();
512 assert_eq!(count, n, "OR of identical sections should give full mask");
513 }
514
515 #[test]
516 fn test_masking_fixed_threshold() {
517 let meta = test_metadata();
518 let n = 8 * 8 * 8;
519 let mag = vec![10.0; n];
520 let sections = vec![MaskSection {
521 input: MaskingInput::Magnitude,
522 generator: MaskOp::Threshold { method: MaskThresholdMethod::Fixed, value: Some(5.0) },
523 refinements: vec![],
524 }];
525 let result = run_masking(§ions, &[], &[&mag], &meta).unwrap();
526 let count: usize = result.iter().map(|&m| m as usize).sum();
527 assert_eq!(count, n, "all voxels above threshold=5");
528 }
529
530 #[test]
531 fn test_masking_percentile_threshold() {
532 let meta = test_metadata();
533 let n = 8 * 8 * 8;
534 let mag: Vec<f64> = (0..n).map(|i| i as f64).collect();
535 let sections = vec![MaskSection {
536 input: MaskingInput::Magnitude,
537 generator: MaskOp::Threshold { method: MaskThresholdMethod::Percentile, value: Some(50.0) },
538 refinements: vec![],
539 }];
540 let result = run_masking(§ions, &[], &[&mag], &meta).unwrap();
541 let count: usize = result.iter().map(|&m| m as usize).sum();
542 assert!(count > 0 && count < n, "percentile should mask ~half");
543 }
544
545 #[test]
546 fn test_masking_close_and_fill_holes() {
547 let meta = test_metadata();
548 let n = 8 * 8 * 8;
549 let mag = vec![10.0; n];
550 let sections = vec![MaskSection {
551 input: MaskingInput::Magnitude,
552 generator: MaskOp::Threshold { method: MaskThresholdMethod::Fixed, value: Some(0.5) },
553 refinements: vec![
554 MaskOp::Close { radius: 1 },
555 MaskOp::FillHoles { max_size: 0 },
556 MaskOp::GaussianSmooth { sigma_mm: 1.0 },
557 ],
558 }];
559 let result = run_masking(§ions, &[], &[&mag], &meta).unwrap();
560 assert_eq!(result.len(), n);
561 }
562
563 #[test]
564 fn test_masking_phase_quality_input() {
565 let meta = test_metadata();
566 let n = 8 * 8 * 8;
567 let phase1 = vec![0.5; n];
568 let phase2 = vec![1.0; n];
569 let mag = vec![1.0; n];
570 let sections = vec![MaskSection {
571 input: MaskingInput::PhaseQuality,
572 generator: MaskOp::Threshold { method: MaskThresholdMethod::Otsu, value: None },
573 refinements: vec![],
574 }];
575 let result = run_masking(§ions, &[&phase1, &phase2], &[&mag], &meta).unwrap();
576 assert_eq!(result.len(), n);
577 }
578
579 #[test]
580 fn test_masking_signal_erode() {
581 let meta = test_metadata();
582 let n = 8 * 8 * 8;
583 let mag = vec![10.0; n];
584 let section = |refinement| vec![MaskSection {
585 input: MaskingInput::Magnitude,
586 generator: MaskOp::Threshold { method: MaskThresholdMethod::Fixed, value: Some(5.0) },
587 refinements: vec![refinement],
588 }];
589 let params = crate::utils::morphology::SignalErosionParams { min_component: 1, ..Default::default() };
590 let result = run_masking(§ion(MaskOp::SignalErode(params.clone())), &[], &[&mag], &meta).unwrap();
591 let plain = run_masking(§ion(MaskOp::Erode { iterations: 1 }), &[], &[&mag], &meta).unwrap();
593 assert_eq!(result, plain);
594 assert!(run_masking(§ion(MaskOp::SignalErode(params)), &[], &[], &meta).is_err());
596 }
597
598 #[test]
599 fn test_masking_hd_bet_requires_magnitude() {
600 let meta = test_metadata();
601 let sections = vec![MaskSection {
602 input: MaskingInput::Magnitude,
603 generator: MaskOp::HdBet(Default::default()),
604 refinements: vec![],
605 }];
606 assert!(run_masking(§ions, &[], &[], &meta).is_err());
607 #[cfg(not(feature = "onnx"))]
608 assert!(run_masking(§ions, &[], &[&vec![1.0; 512]], &meta).is_err());
609 }
610
611 #[test]
612 fn test_masking_rs2_net_requires_magnitude() {
613 let meta = test_metadata();
614 let sections = vec![MaskSection {
615 input: MaskingInput::Magnitude,
616 generator: MaskOp::Rs2Net(Default::default()),
617 refinements: vec![],
618 }];
619 assert!(run_masking(§ions, &[], &[], &meta).is_err());
620 #[cfg(not(feature = "onnx"))]
621 assert!(run_masking(§ions, &[], &[&vec![1.0; 512]], &meta).is_err());
622 }
623
624 #[test]
625 fn test_masking_mindgrab_requires_magnitude_and_affine() {
626 let mut meta = test_metadata();
627 let sections = vec![MaskSection {
628 input: MaskingInput::Magnitude,
629 generator: MaskOp::MindGrab(Default::default()),
630 refinements: vec![],
631 }];
632 let mag = vec![1.0; 512];
633 assert!(run_masking(§ions, &[], &[], &meta).is_err());
634 let err = run_masking(§ions, &[], &[&mag], &meta).unwrap_err();
636 assert!(err.to_string().contains("affine"), "{err}");
637 meta.affine = Some([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]);
638 #[cfg(not(feature = "onnx"))]
639 assert!(run_masking(§ions, &[], &[&mag], &meta).is_err());
640 }
641
642 #[test]
646 fn test_apply_mask_ops_matches_build_mask_section() {
647 let meta = test_metadata();
648 let n = 8 * 8 * 8;
649 let e1: Vec<f64> = (0..n).map(|i| 50.0 + ((i * 7) % 13) as f64).collect();
650 let e2: Vec<f64> = (0..n).map(|i| ((i * 37) % 19) as f64).collect();
651 let input = section_magnitude(MaskingInput::MagnitudeLast, &[&e1, &e2], &meta).unwrap().unwrap();
653 let mag = input.clone();
654 assert_eq!(input, e2);
655 let section = MaskSection {
656 input: MaskingInput::MagnitudeLast,
657 generator: MaskOp::Threshold { method: MaskThresholdMethod::Fixed, value: Some(5.0) },
658 refinements: vec![
659 MaskOp::Dilate { iterations: 1 },
660 MaskOp::FillHoles { max_size: 0 },
661 MaskOp::Erode { iterations: 1 },
662 MaskOp::SignalErode(crate::utils::morphology::SignalErosionParams { min_component: 1, ..Default::default() }),
663 ],
664 };
665 let whole = build_mask_section(§ion, &[], &[&e1, &e2], &meta).unwrap();
666
667 let mut step = vec![1u8; n];
668 for op in section.all_ops() {
669 step = apply_mask_ops(step, std::slice::from_ref(&op), &input, Some(&mag), &meta).unwrap();
670 }
671 assert_eq!(step, whole);
672 }
673
674 #[test]
675 fn test_apply_mask_ops_needs_magnitude_and_matching_length() {
676 let meta = test_metadata();
677 let n = 8 * 8 * 8;
678 let input = vec![10.0; n];
679 let ops = [MaskOp::SignalErode(Default::default())];
682 assert!(apply_mask_ops(vec![1u8; n], &ops, &input, None, &meta).is_err());
683 assert!(apply_mask_ops(vec![1u8; n - 1], &[MaskOp::Erode { iterations: 1 }], &input, Some(&input), &meta).is_err());
685 }
686
687 fn two_region_echoes() -> Vec<Vec<f64>> {
690 let n = 8 * 8 * 8;
691 let left = |i: usize| i % 8 < 4;
692 vec![
693 (0..n).map(|i| if left(i) { 10.0 } else { 1.0 }).collect(),
694 vec![5.0; n],
695 (0..n).map(|i| if left(i) { 1.0 } else { 10.0 }).collect(),
696 ]
697 }
698
699 fn fixed(input: MaskingInput) -> MaskSection {
700 MaskSection {
701 input,
702 generator: MaskOp::Threshold { method: MaskThresholdMethod::Fixed, value: Some(6.0) },
703 refinements: vec![],
704 }
705 }
706
707 fn count(mask: &[u8]) -> usize {
708 mask.iter().map(|&m| m as usize).sum()
709 }
710
711 #[test]
713 fn magnitude_inputs_are_distinguishable() {
714 let meta = test_metadata();
715 let echoes = two_region_echoes();
716 let mags: Vec<&[f64]> = echoes.iter().map(Vec::as_slice).collect();
717 let get = |input| resolve_masking_input(input, &[], &mags, &meta).unwrap();
718 assert_eq!(get(MaskingInput::MagnitudeFirst), echoes[0]);
719 assert_eq!(get(MaskingInput::MagnitudeLast), echoes[2]);
720 let rss = get(MaskingInput::Magnitude);
721 assert!((rss[0] - (100.0f64 + 25.0 + 1.0).sqrt()).abs() < 1e-12);
722 assert_ne!(rss, echoes[0]);
723 assert_ne!(rss, echoes[2]);
724 }
725
726 #[test]
729 fn each_section_uses_its_own_input() {
730 let meta = test_metadata();
731 let echoes = two_region_echoes();
732 let mags: Vec<&[f64]> = echoes.iter().map(Vec::as_slice).collect();
733 let n = 8 * 8 * 8;
734 let build = |input| build_mask_section(&fixed(input), &[], &mags, &meta).unwrap();
735
736 let first = build(MaskingInput::MagnitudeFirst);
737 let last = build(MaskingInput::MagnitudeLast);
738 assert_eq!(count(&first), n / 2);
740 assert_eq!(count(&last), n / 2);
741 assert!(first.iter().zip(&last).all(|(a, b)| a + b == 1));
742
743 let both = run_masking(&[fixed(MaskingInput::MagnitudeFirst), fixed(MaskingInput::MagnitudeLast)], &[], &mags, &meta).unwrap();
745 assert_eq!(count(&both), n);
746
747 let swapped = run_masking(&[fixed(MaskingInput::MagnitudeFirst), fixed(MaskingInput::MagnitudeFirst)], &[], &mags, &meta).unwrap();
749 assert_eq!(swapped, first);
750 assert_eq!(build(MaskingInput::MagnitudeFirst), first);
752 }
753
754 #[test]
757 fn signal_gated_ops_use_the_sections_magnitude() {
758 let meta = test_metadata();
759 let n = 8 * 8 * 8;
760 let e1 = vec![10.0; n];
762 let e_last: Vec<f64> = (0..n).map(|i| if i % 8 == 0 { 0.5 } else { 10.0 }).collect();
763 let mags: [&[f64]; 2] = [&e1, &e_last];
764 let params = crate::utils::morphology::SignalErosionParams { min_component: 1, ..Default::default() };
765 let section = |input| MaskSection {
766 input,
767 generator: MaskOp::Threshold { method: MaskThresholdMethod::Fixed, value: Some(0.1) },
768 refinements: vec![MaskOp::SignalErode(params.clone())],
769 };
770 let all = vec![1u8; n];
771 let erode_with = |m: &[f64]| apply_mask_ops(all.clone(), &[MaskOp::SignalErode(params.clone())], m, Some(m), &meta).unwrap();
772 let on_first = erode_with(&e1);
773 let on_last = erode_with(&e_last);
774 assert_ne!(on_first, on_last, "test magnitudes do not separate the two cases");
775
776 assert_eq!(build_mask_section(§ion(MaskingInput::MagnitudeFirst), &[], &mags, &meta).unwrap(), on_first);
777 assert_eq!(build_mask_section(§ion(MaskingInput::MagnitudeLast), &[], &mags, &meta).unwrap(), on_last);
778 }
779
780 #[test]
783 fn phase_quality_is_weighted_by_the_rss() {
784 let meta = test_metadata();
785 let n = 8 * 8 * 8;
786 let phase1: Vec<f64> = (0..n).map(|i| ((i * 13) % 7) as f64 * 0.4 - 1.2).collect();
787 let phase2: Vec<f64> = phase1.iter().map(|p| 2.0 * p).collect();
788 let echoes = two_region_echoes();
789 let mags: Vec<&[f64]> = echoes.iter().map(Vec::as_slice).collect();
790 let rss = rss_combine(&mags).unwrap();
791 let phases: [&[f64]; 2] = [&phase1, &phase2];
792 let q = resolve_masking_input(MaskingInput::PhaseQuality, &phases, &mags, &meta).unwrap();
793 let q_rss = resolve_masking_input(MaskingInput::PhaseQuality, &phases, &[&rss], &meta).unwrap();
794 let q_e1 = resolve_masking_input(MaskingInput::PhaseQuality, &phases, &mags[..1], &meta).unwrap();
795 assert_eq!(q, q_rss);
796 assert_ne!(q, q_e1);
797 assert_eq!(section_magnitude(MaskingInput::PhaseQuality, &mags, &meta).unwrap(), Some(rss));
798 }
799
800 #[test]
801 fn missing_or_mismatched_data_is_an_error() {
802 let meta = test_metadata();
803 let n = 8 * 8 * 8;
804 let mag = vec![1.0; n];
805 let short = vec![1.0; n - 1];
806 for input in [MaskingInput::MagnitudeFirst, MaskingInput::Magnitude, MaskingInput::MagnitudeLast] {
807 assert!(resolve_masking_input(input, &[], &[], &meta).is_err(), "{input:?} without magnitude");
808 assert!(resolve_masking_input(input, &[], &[&mag, &short], &meta).is_err(), "{input:?} short echo");
809 }
810 assert!(resolve_masking_input(MaskingInput::PhaseQuality, &[], &[&mag], &meta).is_err());
811 assert!(resolve_masking_input(MaskingInput::PhaseQuality, &[&short], &[&mag], &meta).is_err());
812 assert!(resolve_masking_input(MaskingInput::PhaseQuality, &[&mag], &[], &meta).is_ok());
814 }
815
816 #[test]
817 fn test_run_masking_empty_sections() {
818 let meta = test_metadata();
819 let result = run_masking(&[], &[], &[], &meta);
820 assert!(result.is_err());
821 }
822
823 #[test]
826 fn test_bet_op_passes_every_bet_param_through() {
827 use crate::bet::{run_bet, BetParams};
828 let n = 20;
829 let meta = ScanMetadata { dims: (n, n, n), ..test_metadata() };
830 let grid = meta.grid();
831 let c = (n as f64 - 1.0) / 2.0;
833 let mag: Vec<f64> = (0..n * n * n).map(|idx| {
834 let (i, j, k) = (idx % n, (idx / n) % n, idx / (n * n));
835 let r = (((i as f64 - c) / 7.0).powi(2) + ((j as f64 - c) / 6.0).powi(2)
836 + ((k as f64 - c) / 5.0).powi(2)).sqrt();
837 10.0 + 190.0 / (1.0 + ((r - 1.0) * 2.5).exp())
838 }).collect();
839 let base = BetParams { iterations: 50, subdivisions: 1, ..BetParams::default() };
840 let base_mask = run_bet(&mag, &grid, &base, crate::bet::FslLeftRightOrder::from_affine(meta.affine.as_ref()), |_, _| {});
841
842 let variants = [
843 BetParams { fractional_intensity: 0.2, ..base.clone() },
844 BetParams { voxel_scale: 2.0, ..base.clone() },
845 BetParams { smoothness: 3.0, ..base.clone() },
846 BetParams { gradient_threshold: 0.5, ..base.clone() },
847 BetParams { iterations: 5, ..base.clone() },
848 BetParams { subdivisions: 2, ..base.clone() },
849 ];
850 for params in variants {
851 let direct = run_bet(&mag, &grid, ¶ms, crate::bet::FslLeftRightOrder::from_affine(meta.affine.as_ref()), |_, _| {});
852 assert!(direct != base_mask, "{params:?} should change the mask, or this test proves nothing");
853 let op = MaskOp::Bet(params.clone());
854 let via_op = apply_mask_ops(vec![1; n * n * n], &[op], &mag, Some(&mag), &meta).unwrap();
855 assert_eq!(via_op, direct, "MaskOp::Bet dropped a field of {params:?}");
856 }
857 }
858}