From ff05251068a4145e2d61e8c1aaf8c9c3cfeb8548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Thu, 16 May 2024 21:13:46 +0200 Subject: [PATCH 1/8] enh: add a node flipping the functional data enh: rerun registration using this flipped functional image --- fmriprep/workflows/bold/registration.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 4be31d303..1547efce7 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -271,6 +271,7 @@ def init_bbreg_wf( """ from nipype.interfaces.freesurfer import BBRegister from niworkflows.engine.workflows import LiterateWorkflow as Workflow + from niworkflows.interfaces.morphology import AxisFlip from niworkflows.interfaces.nitransforms import ConcatenateXFMs from fmriprep.interfaces.patches import FreeSurferSource, MRICoreg @@ -347,6 +348,16 @@ def init_bbreg_wf( if bold2anat_init == 'header': bbregister.inputs.init = 'header' + lr_flip = pe.Node(AxisFlip(axis=0), name='flip') + bbregister_flipped = pe.Node( + BBRegister( + dof=bold2anat_dof, + contrast_type='t2', + out_lta_file=True, + ), + name='bbregister', + mem_gb=12, + ) transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') # In cases where Merge(2) only has `in1` or `in2` defined # output list will just contain a single element @@ -396,6 +407,10 @@ def init_bbreg_wf( (inputnode, bbregister, [('subjects_dir', 'subjects_dir'), ('subject_id', 'subject_id'), ('in_file', 'source_file')]), + (inputnode, bbregister_flipped, [('subjects_dir', 'subjects_dir'), + ('subject_id', 'subject_id')]), + (inputnode, lr_flip), [('in_file', 'in_file')], + (lr_flip, bbregister_flipped), [('out_file', 'source_file')], (bbregister, transforms, [('out_lta_file', 'in1')]), ]) # fmt:skip From 975db161ffdf9f15225f42ab2da60da7ec2c5dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Thu, 16 May 2024 21:19:46 +0200 Subject: [PATCH 2/8] enh: implement an interface to compare costs function from registration with original and flipped functional image to detect left-right flip enh: propagate the flip_info through the workflow so it can be integrated into the visual report --- fmriprep/interfaces/reports.py | 43 +++++++++++++++++++++++++ fmriprep/workflows/bold/registration.py | 18 +++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/fmriprep/interfaces/reports.py b/fmriprep/interfaces/reports.py index 8f26c374c..1bb447262 100644 --- a/fmriprep/interfaces/reports.py +++ b/fmriprep/interfaces/reports.py @@ -218,6 +218,12 @@ class FunctionalSummaryInputSpec(TraitedSpec): desc='Whether to initialize registration with the "header"' ' or by centering the volumes ("t1w" or "t2w")', ) + flip_info = traits.Dict( + traits.Enum('lr_flip_warning', 'cost_original', 'cost_flipped'), + traits.Either(traits.Bool(), traits.Float()), + desc='Left-right flip check warning and registration costs', + mandatory=True + ) tr = traits.Float(desc='Repetition time', mandatory=True) dummy_scans = traits.Either(traits.Int(), None, desc='number of dummy scans specified by user') algo_dummy_scans = traits.Int(desc='number of dummy scans determined by algorithm') @@ -369,3 +375,40 @@ def get_world_pedir(ornt, pe_direction): f'Orientation: {ornt}; PE dir: {pe_direction}' ) return 'Could not be determined - assuming Anterior-Posterior' + + +class _CheckFlipInputSpec(BaseInterfaceInputSpec): + cost_original = File( + exists=True, + mandatory=True, + desc='cost associated with registration of BOLD to original T1w images', + ) + cost_flipped = File( + exists=True, + mandatory=True, + desc='cost associated with registration of BOLD to the flipped T1w images', + ) + + +class _CheckFlipOutputSpec(TraitedSpec): + flip_info = traits.Dict( + traits.Enum('warning', 'cost_original', 'cost_flipped'), + traits.Either(traits.Bool(), traits.Float()), + desc='Left-right flip check warning and registration costs', + mandatory=True + ) + + +class CheckFlip(SimpleInterface): + """Check for a LR flip by comparing registration cost functions.""" + + input_spec = _CheckFlipInputSpec + output_spec = _CheckFlipOutputSpec + + def _run_interface(self, runtime): + self._results['flip_info'] = { + 'warning': self.inputs.cost_flipped < self.inputs.cost_original, + 'cost_original': self.inputs.cost_original, + 'cost_flipped': self.inputs.cost_flipped, + } + return runtime diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 1547efce7..8029b0175 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -127,6 +127,8 @@ def init_bold_reg_wf( Affine transform from T1 space to BOLD space (ITK format) fallback Boolean indicating whether BBR was rejected (mri_coreg registration returned) + flip_info + Information regarding whether a left-right flip was detected See Also -------- @@ -153,7 +155,9 @@ def init_bold_reg_wf( ) outputnode = pe.Node( - niu.IdentityInterface(fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback']), + niu.IdentityInterface( + fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info'] + ), name='outputnode', ) @@ -187,6 +191,7 @@ def init_bold_reg_wf( ('outputnode.itk_bold_to_t1', 'itk_bold_to_t1'), ('outputnode.itk_t1_to_bold', 'itk_t1_to_bold'), ('outputnode.fallback', 'fallback'), + ('outputnode.flip_info', 'flip_info'), ]), ]) # fmt:skip @@ -267,6 +272,8 @@ def init_bbreg_wf( Affine transform from T1 space to BOLD space (ITK format) fallback Boolean indicating whether BBR was rejected (mri_coreg registration returned) + flip_info + Information regarding whether a left-right flip was detected """ from nipype.interfaces.freesurfer import BBRegister @@ -275,6 +282,7 @@ def init_bbreg_wf( from niworkflows.interfaces.nitransforms import ConcatenateXFMs from fmriprep.interfaces.patches import FreeSurferSource, MRICoreg + from fmriprep.interfaces.reports import CheckFlip workflow = Workflow(name=name) workflow.__desc__ = """\ @@ -309,7 +317,7 @@ def init_bbreg_wf( name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback']), + niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info']), name='outputnode', ) @@ -358,6 +366,9 @@ def init_bbreg_wf( name='bbregister', mem_gb=12, ) + + check_flip = pe.Node(CheckFlip(), name='check_flip') + transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') # In cases where Merge(2) only has `in1` or `in2` defined # output list will just contain a single element @@ -411,7 +422,10 @@ def init_bbreg_wf( ('subject_id', 'subject_id')]), (inputnode, lr_flip), [('in_file', 'in_file')], (lr_flip, bbregister_flipped), [('out_file', 'source_file')], + (bbregister, check_flip), [('min_cost_file', 'cost_original')], + (bbregister_flipped, check_flip), [('min_cost_file', 'cost_flipped')], (bbregister, transforms, [('out_lta_file', 'in1')]), + (check_flip, outputnode, [('flip_info', 'flip_info')]), ]) # fmt:skip # Short-circuit workflow building, use boundary-based registration From 9cac594ec64aae8484ab71a34c137449cebafdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Thu, 16 May 2024 21:21:23 +0200 Subject: [PATCH 3/8] enh: dedicate a line to left-right flip check in functional summary within visual report enh: report costs in registration with original and flipped functional image --- fmriprep/interfaces/reports.py | 20 ++++++++++++++++++++ fmriprep/workflows/bold/fit.py | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/fmriprep/interfaces/reports.py b/fmriprep/interfaces/reports.py index 1bb447262..64edda47b 100644 --- a/fmriprep/interfaces/reports.py +++ b/fmriprep/interfaces/reports.py @@ -67,6 +67,17 @@ \t\t\t
  • Slice timing correction: {stc}
  • \t\t\t
  • Susceptibility distortion correction: {sdc}
  • \t\t\t
  • Registration: {registration}
  • +\t\t\t
  • Left-right flip check warning: {lr_flip_warning}
  • +\t\t +\t\t\t +\t\t\t\t +\t\t\t\t +\t\t\t +\t\t\t +\t\t\t\t +\t\t\t\t +\t\t\t +\t\t
    Original Registration CostFlipped Registration Cost
    {cost_original}{cost_flipped}
    \t\t\t
  • Non-steady-state volumes: {dummy_scan_desc}
  • \t\t \t\t @@ -285,6 +296,12 @@ def _generate_segment(self): if n_echos > 2: multiecho = f'Multi-echo EPI sequence: {n_echos} echoes.' + lr_flip_warning = ( + 'LR flip detected' + if self.inputs.flip_info.get('lr_flip_warning', False) + else 'none' + ) + return FUNCTIONAL_TEMPLATE.format( pedir=pedir, stc=stc, @@ -294,6 +311,9 @@ def _generate_segment(self): dummy_scan_desc=dummy_scan_msg, multiecho=multiecho, ornt=self.inputs.orientation, + lr_flip_warning=lr_flip_warning, + cost_original=self.input.flip_info.get('cost_original', None), + cost_flipped=self.input.flip_info.get('cost_flipped', None), ) diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 25fca9134..89e156287 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -624,7 +624,8 @@ def init_bold_fit_wf( (regref_buffer, ds_boldreg_wf, [('boldref', 'inputnode.source_files')]), (bold_reg_wf, ds_boldreg_wf, [('outputnode.itk_bold_to_t1', 'inputnode.xform')]), (ds_boldreg_wf, outputnode, [('outputnode.xform', 'boldref2anat_xfm')]), - (bold_reg_wf, summary, [('outputnode.fallback', 'fallback')]), + (bold_reg_wf, summary, [('outputnode.fallback', 'fallback'), + ('outputnode.flip_info', 'flip_info')]), ]) # fmt:on else: From 7a976df14cbd78af3dbca9b982d4f5f81700db1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Thu, 20 Jun 2024 07:43:25 +0200 Subject: [PATCH 4/8] enh: define a flip_info output for fsl_bbr_wf because otherwise test of the workflow initialisation fails complaining this connection does not exist. The output is the dictionary with the same keys as the flip_info for the bbr_wf except the value associated with the keys are empty. --- fmriprep/workflows/bold/registration.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index c6e918fa5..0c71eeb72 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -560,7 +560,7 @@ def init_fsl_bbr_wf( name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback']), + niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info']), name='outputnode', ) @@ -605,6 +605,11 @@ def init_fsl_bbr_wf( name='fsl2itk_inv', mem_gb=DEFAULT_MEMORY_MIN_GB, ) + + def flip_detection_not_implemented_yet(): + return {'warning': 'Flip detection not implemented yet for alignment with FSL flirt', 'cost_original': '', 'cost_flipped': ''} + + check_flip = pe.Node(niu.Function(function=flip_detection_not_implemented_yet, output_names=['flip_info']), name='check_flip') # fmt:off workflow.connect([ (inputnode, mask_t1w_brain, [('t1w_preproc', 'in_file'), @@ -619,6 +624,7 @@ def init_fsl_bbr_wf( (invt_bbr, fsl2itk_inv, [('out_file', 'transform_file')]), (fsl2itk_fwd, outputnode, [('itk_transform', 'itk_bold_to_t1')]), (fsl2itk_inv, outputnode, [('itk_transform', 'itk_t1_to_bold')]), + (check_flip, outputnode, [('flip_info', 'flip_info')]), ]) # fmt:on From 4232b3a3d44f3b6a3676c8f7d7e269f75c2bd6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Thu, 20 Jun 2024 08:25:24 +0200 Subject: [PATCH 5/8] sty: ruff enh: point to new branch of niworkflows that implement AxisFlip --- fmriprep/interfaces/reports.py | 4 ++-- fmriprep/workflows/bold/registration.py | 13 ++++++++++--- pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/fmriprep/interfaces/reports.py b/fmriprep/interfaces/reports.py index 64edda47b..76d91a36a 100644 --- a/fmriprep/interfaces/reports.py +++ b/fmriprep/interfaces/reports.py @@ -233,7 +233,7 @@ class FunctionalSummaryInputSpec(TraitedSpec): traits.Enum('lr_flip_warning', 'cost_original', 'cost_flipped'), traits.Either(traits.Bool(), traits.Float()), desc='Left-right flip check warning and registration costs', - mandatory=True + mandatory=True, ) tr = traits.Float(desc='Repetition time', mandatory=True) dummy_scans = traits.Either(traits.Int(), None, desc='number of dummy scans specified by user') @@ -415,7 +415,7 @@ class _CheckFlipOutputSpec(TraitedSpec): traits.Enum('warning', 'cost_original', 'cost_flipped'), traits.Either(traits.Bool(), traits.Float()), desc='Left-right flip check warning and registration costs', - mandatory=True + mandatory=True, ) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 0c71eeb72..ff9ce30c8 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -607,9 +607,16 @@ def init_fsl_bbr_wf( ) def flip_detection_not_implemented_yet(): - return {'warning': 'Flip detection not implemented yet for alignment with FSL flirt', 'cost_original': '', 'cost_flipped': ''} - - check_flip = pe.Node(niu.Function(function=flip_detection_not_implemented_yet, output_names=['flip_info']), name='check_flip') + return { + 'warning': 'Flip detection not implemented yet for alignment with FSL flirt', + 'cost_original': '', + 'cost_flipped': '', + } + + check_flip = pe.Node( + niu.Function(function=flip_detection_not_implemented_yet, output_names=['flip_info']), + name='check_flip', + ) # fmt:off workflow.connect([ (inputnode, mask_t1w_brain, [('t1w_preproc', 'in_file'), diff --git a/pyproject.toml b/pyproject.toml index f707950ac..2b3a32354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "nireports @ git+https://github.com/nipreps/nireports.git@main", "nitime", "nitransforms >= 21.0.0", - "niworkflows @ git+https://github.com/nipreps/niworkflows.git@master", + "niworkflows @ git+https://github.com/celprov/niworkflows.git@enh/flip_image", "numpy >= 1.22", "packaging", "pandas", From 522753b58da84455fd3e402d7f6710d94e8cf852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Sat, 22 Jun 2024 07:16:50 +0200 Subject: [PATCH 6/8] enh: move the transform handling within bbreg_wf in its own workflow to avoid duplicating this part of the code when additionally outputting the transform betweeen the flipped bold and the t1w. --- fmriprep/workflows/bold/registration.py | 79 ++++++++++++++++++------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index ff9ce30c8..cdfd2f54b 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -279,7 +279,6 @@ def init_bbreg_wf( from nipype.interfaces.freesurfer import BBRegister from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.morphology import AxisFlip - from niworkflows.interfaces.nitransforms import ConcatenateXFMs from fmriprep.interfaces.patches import FreeSurferSource, MRICoreg from fmriprep.interfaces.reports import CheckFlip @@ -369,24 +368,8 @@ def init_bbreg_wf( check_flip = pe.Node(CheckFlip(), name='check_flip') - transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') - # In cases where Merge(2) only has `in1` or `in2` defined - # output list will just contain a single element - select_transform = pe.Node( - niu.Select(index=0), run_without_submitting=True, name='select_transform' - ) - merge_ltas = pe.Node(niu.Merge(2), name='merge_ltas', run_without_submitting=True) - concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm') - - workflow.connect([ - (inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]), - # Wire up the co-registration alternatives - (transforms, select_transform, [('out', 'inlist')]), - (select_transform, merge_ltas, [('out', 'in1')]), - (merge_ltas, concat_xfm, [('out', 'in_xfms')]), - (concat_xfm, outputnode, [('out_xfm', 'itk_bold_to_t1')]), - (concat_xfm, outputnode, [('out_inv', 'itk_t1_to_bold')]), - ]) # fmt:skip + transform_wf = _transform_handling_wf(use_bbr=use_bbr) + transform_flipped_wf = _transform_handling_wf(use_bbr=use_bbr) # Do not initialize with header, use mri_coreg if bold2anat_init != 'header': @@ -394,7 +377,8 @@ def init_bbreg_wf( (inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'), ('subject_id', 'subject_id'), ('in_file', 'source_file')]), - (mri_coreg, transforms, [('out_lta_file', 'in2')]), + (mri_coreg, transform_wf, [('out_lta_file', 'inputnode.in2')]), + (mri_coreg, transform_flipped_wf, [('out_lta_file', 'inputnode.in2')]), ]) # fmt:skip if use_t2w: @@ -422,12 +406,65 @@ def init_bbreg_wf( ('subject_id', 'subject_id')]), (inputnode, lr_flip), [('in_file', 'in_file')], (lr_flip, bbregister_flipped), [('out_file', 'source_file')], + (lr_flip, outputnode), [('out_file', 'flipped_bold')], (bbregister, check_flip), [('min_cost_file', 'cost_original')], + (bbregister, transform_wf, [('out_lta_file', 'inputnode.in1')]), (bbregister_flipped, check_flip), [('min_cost_file', 'cost_flipped')], - (bbregister, transforms, [('out_lta_file', 'in1')]), + (bbregister_flipped, transform_flipped_wf, [('out_lta_file', 'inputnode.in1')]), (check_flip, outputnode, [('flip_info', 'flip_info')]), + (transform_wf, outputnode, [('outputnode.itk_bold_to_t1', 'itk_bold_to_t1'), + ('outputnode.itk_t1_to_bold', 'itk_t1_to_bold'), + ('outputnode.fallback', 'fallback')]), ]) # fmt:skip + return workflow + +def _transform_handling_wf(use_bbr : bool, name: str = 'transform_handling_wf'): + """ + Wire up the co-registration alternatives + + Parameters + ---------- + use_bbr : :obj:`bool` or None + Enable/disable boundary-based registration refinement. + If ``None``, test BBR result for distortion before accepting. + """ + from niworkflows.engine.workflows import LiterateWorkflow as Workflow + from niworkflows.interfaces.nitransforms import ConcatenateXFMs + + workflow = Workflow(name=name) + inputnode = pe.Node( + niu.IdentityInterface( + [ + 'in1', + 'in2', + ] + ), + name='inputnode', + ) + outputnode = pe.Node( + niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback',]), + name='outputnode', + ) + + transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') + # In cases where Merge(2) only has `in1` or `in2` defined + # output list will just contain a single element + select_transform = pe.Node( + niu.Select(index=0), run_without_submitting=True, name='select_transform' + ) + merge_ltas = pe.Node(niu.Merge(2), name='merge_ltas', run_without_submitting=True) + concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm') + + workflow.connect([ + (inputnode, transforms, [('in1', 'in1'), ('in2', 'in2')]), + (transforms, select_transform, [('out', 'inlist')]), + (select_transform, merge_ltas, [('out', 'in1')]), + (merge_ltas, concat_xfm, [('out', 'in_xfms')]), + (concat_xfm, outputnode, [('out_xfm', 'itk_bold_to_t1')]), + (concat_xfm, outputnode, [('out_inv', 'itk_t1_to_bold')]), + ]) + # Short-circuit workflow building, use boundary-based registration if use_bbr is True: outputnode.inputs.fallback = False From 9cb952724aac69f971398bb35abe389ca6b1f45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Sat, 22 Jun 2024 08:41:57 +0200 Subject: [PATCH 7/8] fix: I realized that I mistakenly understood that I was flipping the T1w image when I was actually flipping the BOLD, any previous commit that states the T1w was flipped is a mistake. enh: plot the alignment between the flipped BOLD and T1w as a complement to left-right flip check through cost comparison. The plot is hidden in a drop down section. --- fmriprep/data/reports-spec-func.yml | 15 ++++++ fmriprep/data/reports-spec.yml | 15 ++++++ fmriprep/workflows/bold/fit.py | 8 ++++ fmriprep/workflows/bold/outputs.py | 64 +++++++++++++++++++++++++ fmriprep/workflows/bold/registration.py | 14 ++++-- 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/fmriprep/data/reports-spec-func.yml b/fmriprep/data/reports-spec-func.yml index 04acea2c4..cf932014e 100644 --- a/fmriprep/data/reports-spec-func.yml +++ b/fmriprep/data/reports-spec-func.yml @@ -104,6 +104,21 @@ sections: anatomical white matter mask, which appears as a red contour. static: false subtitle: Alignment of functional and anatomical MRI data (coregistration) + - button: + type: button + class: btn btn-primary + data-toggle: collapse + data-target: '#flippedcoreg' + text: 'Alignment of functional and flipped anatomical MRI data' + - div: + class: collapse + id: 'flippedcoreg' + reportlets: + - bids: {datatype: figures, desc: flippedcoreg, suffix: bold} + caption: This panel shows the alignment of the reference EPI (BOLD) image to the + left-right flipped anatomical (T1-weighted) image. + static: false + subtitle: Left-right flip check, alignment of functional and flipped anatomical MRI data - bids: {datatype: figures, desc: rois, suffix: bold} caption: Brain mask calculated on the BOLD signal (red contour), along with the regions of interest (ROIs) used for the estimation of physiological and movement diff --git a/fmriprep/data/reports-spec.yml b/fmriprep/data/reports-spec.yml index 4ec02bda6..17bc8caf4 100644 --- a/fmriprep/data/reports-spec.yml +++ b/fmriprep/data/reports-spec.yml @@ -130,6 +130,21 @@ sections: anatomical white matter mask, which appears as a red contour. static: false subtitle: Alignment of functional and anatomical MRI data (coregistration) + - button: + type: button + class: btn btn-primary + data-toggle: collapse + data-target: '#flippedcoreg' + text: 'Alignment of functional and flipped anatomical MRI data' + - div: + class: collapse + id: 'flippedcoreg' + reportlets: + - bids: {datatype: figures, desc: flippedcoreg, suffix: bold} + caption: This panel shows the alignment of the reference EPI (BOLD) image to the + left-right flipped anatomical (T1-weighted) image. + static: false + subtitle: Left-right flip check, alignment of functional and flipped anatomical MRI data - bids: {datatype: figures, desc: rois, suffix: bold} caption: Brain mask calculated on the BOLD signal (red contour), along with the regions of interest (ROIs) used for the estimation of physiological and movement diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 674f72622..3161c3322 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -289,6 +289,9 @@ def init_bold_fit_wf( 'boldref2fmap_xfm', 'movpar_file', 'rmsd_file', + # LR flip check + 'fboldref2anat_xfm', + 'flipped_boldref', ], ), name='outputnode', @@ -373,6 +376,8 @@ def init_bold_fit_wf( ('coreg_boldref', 'inputnode.coreg_boldref'), ('bold_mask', 'inputnode.bold_mask'), ('boldref2anat_xfm', 'inputnode.boldref2anat_xfm'), + ('fboldref2anat_xfm', 'inputnode.fboldref2anat_xfm'), + ('flipped_boldref', 'inputnode.flipped_boldref'), ]), (summary, func_fit_reports_wf, [('out_report', 'inputnode.summary_report')]), ]) @@ -632,6 +637,9 @@ def init_bold_fit_wf( (regref_buffer, ds_boldreg_wf, [('boldref', 'inputnode.source_files')]), (bold_reg_wf, ds_boldreg_wf, [('outputnode.itk_bold_to_t1', 'inputnode.xform')]), (ds_boldreg_wf, outputnode, [('outputnode.xform', 'boldref2anat_xfm')]), + (bold_reg_wf, outputnode, [ + ('outputnode.itk_fbold_to_t1', 'fboldref2anat_xfm'), + ('outputnode.flipped_boldref', 'flipped_boldref')]), (bold_reg_wf, summary, [('outputnode.fallback', 'fallback'), ('outputnode.flip_info', 'flip_info')]), ]) diff --git a/fmriprep/workflows/bold/outputs.py b/fmriprep/workflows/bold/outputs.py index 4f3450b11..17f1e0dbf 100644 --- a/fmriprep/workflows/bold/outputs.py +++ b/fmriprep/workflows/bold/outputs.py @@ -203,6 +203,9 @@ def init_func_fit_reports_wf( 't1w_dseg', 'fieldmap', 'fmap_ref', + # LR flip check + 'fboldref2anat_xfm', + 'flipped_boldref', # May be missing 'subject_id', 'subjects_dir', @@ -267,6 +270,29 @@ def init_func_fit_reports_wf( mem_gb=1, ) + # LR flip check + t1w_flipped_boldref = pe.Node( + ApplyTransforms( + dimension=3, + default_value=0, + float=True, + invert_transform_flags=[True], + interpolation='LanczosWindowedSinc', + ), + name='t1w_flipped_boldref', + mem_gb=1, + ) + flipped_boldref_wm = pe.Node( + ApplyTransforms( + dimension=3, + default_value=0, + invert_transform_flags=[True], + interpolation='NearestNeighbor', + ), + name='boldref_wm', + mem_gb=1, + ) + # fmt:off workflow.connect([ (inputnode, ds_summary, [ @@ -288,6 +314,18 @@ def init_func_fit_reports_wf( ('boldref2anat_xfm', 'transforms'), ]), (t1w_wm, boldref_wm, [('out', 'input_image')]), + # LR flip check + (inputnode, t1w_flipped_boldref, [ + ('t1w_preproc', 'input_image'), + ('flipped_boldref', 'reference_image'), + ('fboldref2anat_xfm', 'transforms'), + ]), + (inputnode, flipped_boldref_wm, [ + ('flipped_boldref', 'reference_image'), + ('fboldref2anat_xfm', 'transforms'), + ]), + (t1w_wm, flipped_boldref_wm, [('out', 'input_image')]), + ]) # fmt:on @@ -409,6 +447,27 @@ def init_func_fit_reports_wf( name='ds_epi_t1_report', ) + flipped_epi_t1_report = pe.Node( + SimpleBeforeAfter( + before_label='T1w', + after_label='EPI', + dismiss_affine=True, + ), + name='flipped_epi_t1_report', + mem_gb=0.1, + ) + + ds_flipped_epi_t1_report = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + desc='flippedcoreg', + suffix='bold', + datatype='figures', + dismiss_entities=dismiss_echo(), + ), + name='ds_flipped_epi_t1_report', + ) + # fmt:off workflow.connect([ (inputnode, epi_t1_report, [('coreg_boldref', 'after')]), @@ -416,6 +475,11 @@ def init_func_fit_reports_wf( (boldref_wm, epi_t1_report, [('output_image', 'wm_seg')]), (inputnode, ds_epi_t1_report, [('source_file', 'source_file')]), (epi_t1_report, ds_epi_t1_report, [('out_report', 'in_file')]), + (inputnode, flipped_epi_t1_report, [('flipped_boldref', 'after')]), + (t1w_flipped_boldref, flipped_epi_t1_report, [('output_image', 'before')]), + (flipped_boldref_wm, flipped_epi_t1_report, [('output_image', 'wm_seg')]), + (inputnode, ds_flipped_epi_t1_report, [('source_file', 'source_file')]), + (flipped_epi_t1_report, ds_flipped_epi_t1_report, [('out_report', 'in_file')]), ]) # fmt:on diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index cdfd2f54b..2ee738cfd 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -156,7 +156,7 @@ def init_bold_reg_wf( outputnode = pe.Node( niu.IdentityInterface( - fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info'] + fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info', 'itk_fbold_to_t1', 'flipped_boldref'] ), name='outputnode', ) @@ -192,6 +192,8 @@ def init_bold_reg_wf( ('outputnode.itk_t1_to_bold', 'itk_t1_to_bold'), ('outputnode.fallback', 'fallback'), ('outputnode.flip_info', 'flip_info'), + ('outputnode.itk_fbold_to_t1', 'itk_fbold_to_t1'), + ('outputnode.flipped_boldref', 'flipped_boldref'), ]), ]) # fmt:skip @@ -316,7 +318,7 @@ def init_bbreg_wf( name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info']), + niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info', 'itk_fbold_to_t1', 'flipped_boldref']), name='outputnode', ) @@ -365,6 +367,8 @@ def init_bbreg_wf( name='bbregister', mem_gb=12, ) + if bold2anat_init == 'header': + bbregister_flipped.inputs.init = 'header' check_flip = pe.Node(CheckFlip(), name='check_flip') @@ -396,6 +400,7 @@ def init_bbreg_wf( # Otherwise bbregister will also be used workflow.connect(mri_coreg, 'out_lta_file', bbregister, 'init_reg_file') + workflow.connect(mri_coreg, 'out_lta_file', bbregister_flipped, 'init_reg_file') # Use bbregister workflow.connect([ @@ -406,7 +411,7 @@ def init_bbreg_wf( ('subject_id', 'subject_id')]), (inputnode, lr_flip), [('in_file', 'in_file')], (lr_flip, bbregister_flipped), [('out_file', 'source_file')], - (lr_flip, outputnode), [('out_file', 'flipped_bold')], + (lr_flip, outputnode), [('out_file', 'flipped_boldref')], (bbregister, check_flip), [('min_cost_file', 'cost_original')], (bbregister, transform_wf, [('out_lta_file', 'inputnode.in1')]), (bbregister_flipped, check_flip), [('min_cost_file', 'cost_flipped')], @@ -415,6 +420,7 @@ def init_bbreg_wf( (transform_wf, outputnode, [('outputnode.itk_bold_to_t1', 'itk_bold_to_t1'), ('outputnode.itk_t1_to_bold', 'itk_t1_to_bold'), ('outputnode.fallback', 'fallback')]), + (transform_flipped_wf, outputnode, [('outputnode.itk_bold_to_t1', 'itk_fbold_to_t1')]), ]) # fmt:skip return workflow @@ -597,7 +603,7 @@ def init_fsl_bbr_wf( name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info']), + niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info', 'itk_fbold_to_t1',]), name='outputnode', ) From 827db7bec5f32babe223943878d7bdce205242a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9line=20Provins?= Date: Sat, 22 Jun 2024 09:06:58 +0200 Subject: [PATCH 8/8] sty: ruff --- fmriprep/workflows/bold/registration.py | 59 +++++++++++++++++++------ 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 2ee738cfd..a53070cea 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -156,7 +156,14 @@ def init_bold_reg_wf( outputnode = pe.Node( niu.IdentityInterface( - fields=['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info', 'itk_fbold_to_t1', 'flipped_boldref'] + fields=[ + 'itk_bold_to_t1', + 'itk_t1_to_bold', + 'fallback', + 'flip_info', + 'itk_fbold_to_t1', + 'flipped_boldref', + ] ), name='outputnode', ) @@ -318,7 +325,16 @@ def init_bbreg_wf( name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info', 'itk_fbold_to_t1', 'flipped_boldref']), + niu.IdentityInterface( + [ + 'itk_bold_to_t1', + 'itk_t1_to_bold', + 'fallback', + 'flip_info', + 'itk_fbold_to_t1', + 'flipped_boldref', + ] + ), name='outputnode', ) @@ -425,7 +441,8 @@ def init_bbreg_wf( return workflow -def _transform_handling_wf(use_bbr : bool, name: str = 'transform_handling_wf'): + +def _transform_handling_wf(use_bbr: bool, name: str = 'transform_handling_wf'): """ Wire up the co-registration alternatives @@ -449,7 +466,13 @@ def _transform_handling_wf(use_bbr : bool, name: str = 'transform_handling_wf'): name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback',]), + niu.IdentityInterface( + [ + 'itk_bold_to_t1', + 'itk_t1_to_bold', + 'fallback', + ] + ), name='outputnode', ) @@ -462,14 +485,16 @@ def _transform_handling_wf(use_bbr : bool, name: str = 'transform_handling_wf'): merge_ltas = pe.Node(niu.Merge(2), name='merge_ltas', run_without_submitting=True) concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm') - workflow.connect([ - (inputnode, transforms, [('in1', 'in1'), ('in2', 'in2')]), - (transforms, select_transform, [('out', 'inlist')]), - (select_transform, merge_ltas, [('out', 'in1')]), - (merge_ltas, concat_xfm, [('out', 'in_xfms')]), - (concat_xfm, outputnode, [('out_xfm', 'itk_bold_to_t1')]), - (concat_xfm, outputnode, [('out_inv', 'itk_t1_to_bold')]), - ]) + workflow.connect( + [ + (inputnode, transforms, [('in1', 'in1'), ('in2', 'in2')]), + (transforms, select_transform, [('out', 'inlist')]), + (select_transform, merge_ltas, [('out', 'in1')]), + (merge_ltas, concat_xfm, [('out', 'in_xfms')]), + (concat_xfm, outputnode, [('out_xfm', 'itk_bold_to_t1')]), + (concat_xfm, outputnode, [('out_inv', 'itk_t1_to_bold')]), + ] + ) # Short-circuit workflow building, use boundary-based registration if use_bbr is True: @@ -603,7 +628,15 @@ def init_fsl_bbr_wf( name='inputnode', ) outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'fallback', 'flip_info', 'itk_fbold_to_t1',]), + niu.IdentityInterface( + [ + 'itk_bold_to_t1', + 'itk_t1_to_bold', + 'fallback', + 'flip_info', + 'itk_fbold_to_t1', + ] + ), name='outputnode', )