1use crate::bgremove::{
9 IsmvParams, LbvParams, PdfParams, ResharpParams, SharpParams, SdfParams, VsharpParams,
10 HarperellaParams, MsmvParams,
11};
12use crate::inversion::{
13 IlsqrParams, MediParams, NltvParams, RtsParams, TgvParams, TikhonovParams, TkdParams, TvParams,
14 NdiParams, FansiParams, L1QsmParams, WhQsmParams, HdQsmParams, TfiParams, AmpPeParams,
15};
16use crate::separation::{
17 ChiSepIlsqrParams, ChiSepParams, DecomposeParams, HcChisepParams, R2starQsmParams,
18 WaveSepParams,
19};
20use crate::unwrap::romeo::RomeoParams;
21use crate::utils::multi_echo::{B0WeightType, LinearFitParams};
22use crate::utils::QsmartParams;
23
24
25#[derive(Clone, Copy, Debug, PartialEq)]
31pub enum UnwrappingAlgorithm {
32 Romeo,
33 Laplacian,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum OrientationSupport {
44 Arbitrary,
48 NotApplicable,
51 AxialOnly,
60}
61
62impl OrientationSupport {
63 pub fn requires_axial(&self) -> bool {
65 matches!(self, OrientationSupport::AxialOnly)
66 }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq)]
71pub enum BgRemovalAlgorithm {
72 Vsharp,
73 Pdf,
74 Lbv,
75 Ismv,
76 Sharp,
77 Resharp,
78 Harperella,
79 Iharperella,
80 Bfrnet,
83}
84
85#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
87#[derive(Clone, Copy, Debug, PartialEq)]
88pub enum InversionAlgorithm {
89 Tkd,
90 Tsvd,
91 Tikhonov,
92 Tv,
93 Rts,
94 Nltv,
95 Medi,
96 Tfi,
98 Ilsqr,
99 Tgv,
100 Qsmart,
101 Ndi,
103 Fansi,
105 FansiTgv,
107 L1qsm,
109 Whqsm,
111 Hdqsm,
113 AmpPe,
115 Xqsm,
118 Qsmnet,
121 QsmnetPlus,
124 Autoqsm,
128 Qsmgan,
130 Ir2qsm,
132 Lpcnn,
134 ModlQsm,
137 Nextqsm,
140 Iqsm,
144 IqsmPlus,
147}
148
149#[derive(Clone, Copy, Debug, PartialEq)]
151pub enum B0EstimationMethod {
152 WeightedAvg,
153 LinearFit,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq)]
158pub enum QsmReference {
159 Mean,
160 None,
161}
162
163#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum MaskingInput {
170 MagnitudeFirst,
171 Magnitude,
172 MagnitudeLast,
173 PhaseQuality,
174}
175
176#[derive(Clone, Copy, Debug, PartialEq)]
178pub enum MaskThresholdMethod {
179 Otsu,
180 Fixed,
181 Percentile,
182}
183
184#[derive(Clone, Debug, PartialEq)]
186pub enum MaskOp {
187 Threshold {
188 method: MaskThresholdMethod,
189 value: Option<f64>,
190 },
191 Bet {
192 fractional_intensity: f64,
193 },
194 Erode {
195 iterations: usize,
196 },
197 Dilate {
198 iterations: usize,
199 },
200 Close {
201 radius: usize,
202 },
203 FillHoles {
204 max_size: usize,
205 },
206 GaussianSmooth {
207 sigma_mm: f64,
208 },
209 SignalErode(crate::utils::SignalErosionParams),
217 HdBet(crate::bet::HdBetParams),
221}
222
223impl MaskOp {
224 pub fn dl_model_id(&self) -> Option<&'static str> {
226 match self {
227 Self::HdBet(_) => Some("hd-bet"),
228 Self::Threshold { .. } | Self::Bet { .. } | Self::Erode { .. } | Self::Dilate { .. }
229 | Self::Close { .. } | Self::FillHoles { .. } | Self::GaussianSmooth { .. }
230 | Self::SignalErode(_) => None,
231 }
232 }
233}
234
235#[derive(Clone, Debug, PartialEq)]
237pub struct MaskSection {
238 pub input: MaskingInput,
239 pub generator: MaskOp,
240 pub refinements: Vec<MaskOp>,
241}
242
243impl MaskSection {
244 pub fn all_ops(&self) -> Vec<MaskOp> {
246 let mut ops = vec![self.generator.clone()];
247 ops.extend(self.refinements.iter().cloned());
248 ops
249 }
250}
251
252#[derive(Clone, Debug)]
258pub struct MaskingConfig {
259 pub inhomogeneity_correction: bool,
260 pub homogeneity_sigma_mm: f64,
261 pub homogeneity_nbox: usize,
262 pub sections: Vec<MaskSection>,
263}
264
265impl Default for MaskingConfig {
266 fn default() -> Self {
267 Self {
268 inhomogeneity_correction: true,
269 homogeneity_sigma_mm: 7.0,
270 homogeneity_nbox: 15,
271 sections: vec![MaskSection {
272 input: MaskingInput::PhaseQuality,
273 generator: MaskOp::Threshold {
274 method: MaskThresholdMethod::Otsu,
275 value: None,
276 },
277 refinements: vec![
278 MaskOp::Dilate { iterations: 1 },
279 MaskOp::FillHoles { max_size: 0 },
280 MaskOp::Erode { iterations: 1 },
281 ],
282 }],
283 }
284 }
285}
286
287#[derive(Clone, Debug)]
289pub struct FieldMappingConfig {
290 pub unwrapping_algorithm: UnwrappingAlgorithm,
291 pub phase_offset_removal: bool,
292 pub phase_offset_sigma: [f64; 3],
293 pub bipolar_correction: bool,
294 pub b0_estimation: B0EstimationMethod,
295 pub b0_weight_type: B0WeightType,
296 pub romeo_params: RomeoParams,
297 pub linear_fit_params: LinearFitParams,
298}
299
300impl Default for FieldMappingConfig {
301 fn default() -> Self {
302 Self {
303 unwrapping_algorithm: UnwrappingAlgorithm::Romeo,
304 phase_offset_removal: true,
305 phase_offset_sigma: [10.0, 10.0, 5.0],
306 bipolar_correction: false,
307 b0_estimation: B0EstimationMethod::WeightedAvg,
308 b0_weight_type: B0WeightType::PhaseSNR,
309 romeo_params: RomeoParams::default(),
310 linear_fit_params: LinearFitParams::default(),
311 }
312 }
313}
314
315#[derive(Clone, Debug)]
317pub struct BgRemovalConfig {
318 pub algorithm: BgRemovalAlgorithm,
319 pub vsharp: VsharpParams,
320 pub pdf: PdfParams,
321 pub lbv: LbvParams,
322 pub ismv: IsmvParams,
323 pub sharp: SharpParams,
324 pub resharp: ResharpParams,
325 pub harperella: HarperellaParams,
326 pub sdf: SdfParams,
327 pub msmv: MsmvParams,
330 pub msmv_refine: bool,
334}
335
336impl Default for BgRemovalConfig {
337 fn default() -> Self {
338 Self {
339 algorithm: BgRemovalAlgorithm::Vsharp,
340 vsharp: VsharpParams::default(),
341 pdf: PdfParams::default(),
342 lbv: LbvParams::default(),
343 ismv: IsmvParams::default(),
344 sharp: SharpParams::default(),
345 resharp: ResharpParams::default(),
346 harperella: HarperellaParams::default(),
347 sdf: SdfParams::default(),
348 msmv: MsmvParams::default(),
349 msmv_refine: false,
350 }
351 }
352}
353
354#[derive(Clone, Debug)]
356pub struct InversionConfig {
357 pub algorithm: InversionAlgorithm,
358 pub tkd: TkdParams,
359 pub tsvd: TkdParams,
360 pub tikhonov: TikhonovParams,
361 pub tv: TvParams,
362 pub rts: RtsParams,
363 pub nltv: NltvParams,
364 pub medi: MediParams,
365 pub tfi: TfiParams,
366 pub ilsqr: IlsqrParams,
367 pub tgv: TgvParams,
368 pub qsmart: QsmartParams,
369 pub ndi: NdiParams,
370 pub fansi: FansiParams,
373 pub l1qsm: L1QsmParams,
374 pub whqsm: WhQsmParams,
375 pub hdqsm: HdQsmParams,
376 pub amp_pe: AmpPeParams,
378 pub tile: Option<(usize, usize)>,
385}
386
387impl Default for InversionConfig {
388 fn default() -> Self {
389 Self {
390 algorithm: InversionAlgorithm::Rts,
391 tkd: TkdParams::default(),
392 tsvd: TkdParams::default(),
393 tikhonov: TikhonovParams::default(),
394 tv: TvParams::default(),
395 rts: RtsParams::default(),
396 nltv: NltvParams::default(),
397 medi: MediParams::default(),
398 tfi: TfiParams::default(),
399 ilsqr: IlsqrParams::default(),
400 tgv: TgvParams::default(),
401 qsmart: QsmartParams::default(),
402 ndi: NdiParams::default(),
403 fansi: FansiParams::default(),
404 l1qsm: L1QsmParams::default(),
405 whqsm: WhQsmParams::default(),
406 hdqsm: HdQsmParams::default(),
407 amp_pe: AmpPeParams::default(),
408 tile: None,
409 }
410 }
411}
412
413#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
415#[derive(Clone, Copy, Debug, PartialEq)]
416pub enum SeparationAlgorithm {
417 ChiSepIlsqr,
419 ChiSepMedi,
421 R2starQsm,
423 WaveSep,
425 Decompose,
427 HcChisep,
429 SusepNet,
432 ChiSepNet,
435}
436
437impl BgRemovalAlgorithm {
438 pub fn orientation_support(&self) -> OrientationSupport {
445 match self {
446 BgRemovalAlgorithm::Pdf => OrientationSupport::Arbitrary,
447 BgRemovalAlgorithm::Vsharp
448 | BgRemovalAlgorithm::Lbv
449 | BgRemovalAlgorithm::Ismv
450 | BgRemovalAlgorithm::Sharp
451 | BgRemovalAlgorithm::Resharp
452 | BgRemovalAlgorithm::Harperella
453 | BgRemovalAlgorithm::Iharperella
454 | BgRemovalAlgorithm::Bfrnet => OrientationSupport::NotApplicable,
455 }
456 }
457}
458
459impl InversionAlgorithm {
460 pub fn orientation_support(&self) -> OrientationSupport {
466 match self {
467 InversionAlgorithm::Tkd
468 | InversionAlgorithm::Tsvd
469 | InversionAlgorithm::Tikhonov
470 | InversionAlgorithm::Tv
471 | InversionAlgorithm::Rts
472 | InversionAlgorithm::Nltv
473 | InversionAlgorithm::Medi
474 | InversionAlgorithm::Tfi
475 | InversionAlgorithm::Ilsqr
476 | InversionAlgorithm::Tgv
477 | InversionAlgorithm::Qsmart
478 | InversionAlgorithm::Ndi
479 | InversionAlgorithm::Fansi
480 | InversionAlgorithm::FansiTgv
481 | InversionAlgorithm::L1qsm
482 | InversionAlgorithm::Whqsm
483 | InversionAlgorithm::Hdqsm
484 | InversionAlgorithm::AmpPe => OrientationSupport::Arbitrary,
485 InversionAlgorithm::Xqsm
486 | InversionAlgorithm::Qsmnet
487 | InversionAlgorithm::QsmnetPlus
488 | InversionAlgorithm::Autoqsm
489 | InversionAlgorithm::Qsmgan
490 | InversionAlgorithm::Ir2qsm
491 | InversionAlgorithm::Lpcnn
492 | InversionAlgorithm::ModlQsm
493 | InversionAlgorithm::Nextqsm
494 | InversionAlgorithm::Iqsm
495 | InversionAlgorithm::IqsmPlus => OrientationSupport::AxialOnly,
496 }
497 }
498}
499
500impl SeparationAlgorithm {
501 pub fn orientation_support(&self) -> OrientationSupport {
507 match self {
508 SeparationAlgorithm::ChiSepIlsqr | SeparationAlgorithm::ChiSepMedi => {
509 OrientationSupport::Arbitrary
510 }
511 SeparationAlgorithm::R2starQsm
512 | SeparationAlgorithm::WaveSep
513 | SeparationAlgorithm::Decompose
514 | SeparationAlgorithm::HcChisep => OrientationSupport::NotApplicable,
515 SeparationAlgorithm::SusepNet | SeparationAlgorithm::ChiSepNet => {
516 OrientationSupport::AxialOnly
517 }
518 }
519 }
520}
521
522impl BgRemovalAlgorithm {
531 pub fn dl_model_id(self) -> Option<&'static str> {
533 match self {
534 Self::Bfrnet => Some("bfrnet"),
535 Self::Vsharp | Self::Pdf | Self::Lbv | Self::Ismv | Self::Sharp
536 | Self::Resharp | Self::Harperella | Self::Iharperella => None,
537 }
538 }
539 pub const VARIANTS: &'static [Self] = &[
541 Self::Vsharp, Self::Pdf, Self::Lbv, Self::Ismv, Self::Sharp,
542 Self::Resharp, Self::Harperella, Self::Iharperella, Self::Bfrnet,
543 ];
544}
545
546impl InversionAlgorithm {
547 pub fn dl_model_id(self) -> Option<&'static str> {
549 match self {
550 Self::Xqsm => Some("xqsm"),
551 Self::Qsmnet => Some("qsmnet"),
552 Self::QsmnetPlus => Some("qsmnet-plus"),
553 Self::Autoqsm => Some("autoqsm"),
554 Self::Qsmgan => Some("qsmgan"),
555 Self::Ir2qsm => Some("ir2qsm"),
556 Self::Lpcnn => Some("lpcnn"),
557 Self::ModlQsm => Some("modl-qsm"),
558 Self::Nextqsm => Some("nextqsm"),
559 Self::Iqsm => Some("iqsm"),
560 Self::IqsmPlus => Some("iqsm-plus"),
561 Self::Tkd | Self::Tsvd | Self::Tikhonov | Self::Tv | Self::Rts | Self::Nltv
562 | Self::Medi | Self::Tfi | Self::Ilsqr | Self::Tgv | Self::Qsmart | Self::Ndi
563 | Self::Fansi | Self::FansiTgv | Self::L1qsm | Self::Whqsm | Self::Hdqsm
564 | Self::AmpPe => None,
565 }
566 }
567 pub const VARIANTS: &'static [Self] = &[
568 Self::Tkd, Self::Tsvd, Self::Tikhonov, Self::Tv, Self::Rts, Self::Nltv, Self::Medi,
569 Self::Tfi, Self::Ilsqr, Self::Tgv, Self::Qsmart, Self::Ndi, Self::Fansi, Self::FansiTgv,
570 Self::L1qsm, Self::Whqsm, Self::Hdqsm, Self::AmpPe, Self::Xqsm, Self::Qsmnet,
571 Self::QsmnetPlus, Self::Autoqsm, Self::Qsmgan, Self::Ir2qsm, Self::Lpcnn, Self::ModlQsm,
572 Self::Nextqsm, Self::Iqsm, Self::IqsmPlus,
573 ];
574}
575
576impl SeparationAlgorithm {
577 pub fn dl_model_id(self) -> Option<&'static str> {
579 match self {
580 Self::SusepNet => Some("susep-net"),
581 Self::ChiSepNet => Some("chi-sepnet"),
582 Self::ChiSepIlsqr | Self::ChiSepMedi | Self::R2starQsm | Self::WaveSep
583 | Self::Decompose | Self::HcChisep => None,
584 }
585 }
586 pub const VARIANTS: &'static [Self] = &[
587 Self::ChiSepIlsqr, Self::ChiSepMedi, Self::R2starQsm, Self::WaveSep,
588 Self::Decompose, Self::HcChisep, Self::SusepNet, Self::ChiSepNet,
589 ];
590}
591
592#[derive(Clone, Debug)]
594pub struct SeparationConfig {
595 pub algorithm: SeparationAlgorithm,
596 pub chi_sep_ilsqr: ChiSepIlsqrParams,
598 pub chi_sep_medi: ChiSepParams,
599 pub r2star_qsm: R2starQsmParams,
601 pub wavesep: WaveSepParams,
602 pub decompose: DecomposeParams,
604 pub hc_chisep: HcChisepParams,
606}
607
608impl Default for SeparationConfig {
609 fn default() -> Self {
610 Self {
611 algorithm: SeparationAlgorithm::ChiSepIlsqr,
612 chi_sep_ilsqr: ChiSepIlsqrParams::default(),
613 chi_sep_medi: ChiSepParams::default(),
614 r2star_qsm: R2starQsmParams::default(),
615 wavesep: WaveSepParams::default(),
616 decompose: DecomposeParams::default(),
617 hc_chisep: HcChisepParams::default(),
618 }
619 }
620}
621
622#[derive(Clone, Debug)]
632pub struct QsmPipelineConfig {
633 pub masking: MaskingConfig,
634 pub field_mapping: FieldMappingConfig,
635 pub bg_removal: BgRemovalConfig,
636 pub inversion: InversionConfig,
637 pub reference: QsmReference,
638}
639
640impl Default for QsmPipelineConfig {
641 fn default() -> Self {
642 Self {
643 masking: MaskingConfig::default(),
644 field_mapping: FieldMappingConfig::default(),
645 bg_removal: BgRemovalConfig::default(),
646 inversion: InversionConfig::default(),
647 reference: QsmReference::Mean,
648 }
649 }
650}
651
652#[derive(Clone, Debug)]
658pub struct ScanMetadata {
659 pub dims: (usize, usize, usize),
661 pub voxel_size: (f64, f64, f64),
663 pub echo_times: Vec<f64>,
665 pub field_strength: f64,
667 pub b0_direction: (f64, f64, f64),
669}
670
671impl ScanMetadata {
672 #[inline]
674 pub fn grid(&self) -> crate::Grid {
675 crate::Grid {
676 dims: self.dims,
677 voxel_size: self.voxel_size,
678 }
679 }
680}
681
682pub struct FieldMappingResult {
684 pub b0_field_ppm: Vec<f64>,
686 pub phase_offset: Option<Vec<f64>>,
688}
689
690pub struct BgRemovalResult {
692 pub local_field_ppm: Vec<f64>,
694 pub eroded_mask: Vec<u8>,
696}
697
698#[derive(Debug)]
700pub enum PipelineError {
701 InvalidConfig(String),
703 InvalidInput(String),
705 AlgorithmError(String),
707 DimensionMismatch { expected: usize, got: usize },
709}
710
711impl std::fmt::Display for PipelineError {
712 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
713 match self {
714 Self::InvalidConfig(msg) => write!(f, "invalid config: {}", msg),
715 Self::InvalidInput(msg) => write!(f, "invalid input: {}", msg),
716 Self::AlgorithmError(msg) => write!(f, "algorithm error: {}", msg),
717 Self::DimensionMismatch { expected, got } => {
718 write!(f, "dimension mismatch: expected {}, got {}", expected, got)
719 }
720 }
721 }
722}
723
724impl std::error::Error for PipelineError {}
725
726#[cfg(test)]
727mod orientation_tests {
728 use super::*;
729
730 #[test]
733 fn classical_inversions_take_a_direction() {
734 for a in [
735 InversionAlgorithm::Tkd, InversionAlgorithm::Tsvd, InversionAlgorithm::Tikhonov,
736 InversionAlgorithm::Tv, InversionAlgorithm::Rts, InversionAlgorithm::Nltv,
737 InversionAlgorithm::Medi, InversionAlgorithm::Tfi, InversionAlgorithm::Ilsqr,
738 InversionAlgorithm::Tgv, InversionAlgorithm::Qsmart, InversionAlgorithm::Ndi,
739 InversionAlgorithm::Fansi, InversionAlgorithm::FansiTgv, InversionAlgorithm::L1qsm,
740 InversionAlgorithm::Whqsm, InversionAlgorithm::Hdqsm, InversionAlgorithm::AmpPe,
741 ] {
742 assert_eq!(a.orientation_support(), OrientationSupport::Arbitrary, "{a:?}");
743 assert!(!a.orientation_support().requires_axial(), "{a:?}");
744 }
745 }
746
747 #[test]
749 fn learned_inversions_need_axial_data() {
750 for a in [
751 InversionAlgorithm::Xqsm, InversionAlgorithm::Qsmnet, InversionAlgorithm::QsmnetPlus,
752 InversionAlgorithm::Autoqsm, InversionAlgorithm::Qsmgan, InversionAlgorithm::Ir2qsm,
753 InversionAlgorithm::Lpcnn, InversionAlgorithm::ModlQsm, InversionAlgorithm::Nextqsm,
754 InversionAlgorithm::Iqsm, InversionAlgorithm::IqsmPlus,
755 ] {
756 assert!(a.orientation_support().requires_axial(), "{a:?}");
757 }
758 }
759
760 #[test]
762 fn background_removal_is_direction_independent_except_pdf() {
763 assert_eq!(BgRemovalAlgorithm::Pdf.orientation_support(), OrientationSupport::Arbitrary);
764 for a in [
765 BgRemovalAlgorithm::Vsharp, BgRemovalAlgorithm::Sharp, BgRemovalAlgorithm::Resharp,
766 BgRemovalAlgorithm::Ismv, BgRemovalAlgorithm::Lbv, BgRemovalAlgorithm::Harperella,
767 BgRemovalAlgorithm::Iharperella, BgRemovalAlgorithm::Bfrnet,
768 ] {
769 assert_eq!(a.orientation_support(), OrientationSupport::NotApplicable, "{a:?}");
770 assert!(!a.orientation_support().requires_axial(), "{a:?}");
771 }
772 }
773
774 #[test]
776 fn only_learned_methods_force_a_resample() {
777 for a in [SeparationAlgorithm::ChiSepIlsqr, SeparationAlgorithm::ChiSepMedi] {
778 assert_eq!(a.orientation_support(), OrientationSupport::Arbitrary, "{a:?}");
779 }
780 for a in [SeparationAlgorithm::SusepNet, SeparationAlgorithm::ChiSepNet] {
781 assert!(a.orientation_support().requires_axial(), "{a:?}");
782 }
783 for a in [
784 SeparationAlgorithm::R2starQsm, SeparationAlgorithm::WaveSep,
785 SeparationAlgorithm::Decompose, SeparationAlgorithm::HcChisep,
786 ] {
787 assert!(!a.orientation_support().requires_axial(), "{a:?}");
788 }
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::models::{all_models, ModelStage};
796
797 #[test]
803 fn registry_models_are_pipeline_wired() {
804 let inv: Vec<&str> = InversionAlgorithm::VARIANTS.iter().filter_map(|v| v.dl_model_id()).collect();
805 let bfr: Vec<&str> = BgRemovalAlgorithm::VARIANTS.iter().filter_map(|v| v.dl_model_id()).collect();
806 let sep: Vec<&str> = SeparationAlgorithm::VARIANTS.iter().filter_map(|v| v.dl_model_id()).collect();
807 for m in all_models() {
808 let (wired, enum_name) = match m.stage {
809 ModelStage::BackgroundRemoval => (bfr.contains(&m.id), "BgRemovalAlgorithm"),
810 ModelStage::DipoleInversion | ModelStage::SingleStep => (inv.contains(&m.id), "InversionAlgorithm"),
811 ModelStage::ChiSeparation => (sep.contains(&m.id), "SeparationAlgorithm"),
812 ModelStage::BrainExtraction => {
813 (MaskOp::HdBet(Default::default()).dl_model_id() == Some(m.id), "MaskOp")
814 }
815 ModelStage::PhaseToField => continue,
817 };
818 assert!(wired, "registry model '{}' (stage {:?}) has no {} variant — wire it or add a dl_model_id mapping", m.id, m.stage, enum_name);
819 }
820 }
821
822 #[test]
823 fn test_default_config() {
824 let config = QsmPipelineConfig::default();
825 assert_eq!(config.field_mapping.unwrapping_algorithm, UnwrappingAlgorithm::Romeo);
826 assert_eq!(config.bg_removal.algorithm, BgRemovalAlgorithm::Vsharp);
827 assert_eq!(config.inversion.algorithm, InversionAlgorithm::Rts);
828 assert_eq!(config.reference, QsmReference::Mean);
829 assert!(config.field_mapping.phase_offset_removal);
830 assert!(!config.field_mapping.bipolar_correction);
831 }
832
833 #[test]
834 fn test_default_masking_config() {
835 let config = MaskingConfig::default();
836 assert_eq!(config.sections.len(), 1);
837 assert_eq!(config.sections[0].input, MaskingInput::PhaseQuality);
838 assert_eq!(config.sections[0].refinements.len(), 3);
839 }
840
841 #[test]
842 fn test_mask_section_all_ops() {
843 let section = MaskSection {
844 input: MaskingInput::Magnitude,
845 generator: MaskOp::Threshold { method: MaskThresholdMethod::Otsu, value: None },
846 refinements: vec![MaskOp::Erode { iterations: 1 }, MaskOp::Dilate { iterations: 2 }],
847 };
848 let ops = section.all_ops();
849 assert_eq!(ops.len(), 3);
850 assert!(matches!(ops[0], MaskOp::Threshold { .. }));
851 assert!(matches!(ops[1], MaskOp::Erode { iterations: 1 }));
852 assert!(matches!(ops[2], MaskOp::Dilate { iterations: 2 }));
853 }
854}