From b1b11eec203cff01e288b2cb10573a9199e70e99 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Fri, 12 Jan 2024 17:15:43 -0500 Subject: [PATCH 01/22] ENH: Make preprocessed T2w available to BOLD workflows --- fmriprep/workflows/base.py | 1 + fmriprep/workflows/bold/base.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py index 55ff76266..2cfd40249 100644 --- a/fmriprep/workflows/base.py +++ b/fmriprep/workflows/base.py @@ -669,6 +669,7 @@ def init_single_subject_wf(subject_id: str): f'outputnode.sphere_reg_{"msm" if msm_sulc else "fsLR"}', 'inputnode.sphere_reg_fsLR', ), + ('outputnode.t2w_preproc', 'inputnode.t2w_preproc'), # Optional ]), ]) # fmt:skip if fieldmap_id: diff --git a/fmriprep/workflows/bold/base.py b/fmriprep/workflows/bold/base.py index c7289c6d0..6c7c66cce 100644 --- a/fmriprep/workflows/bold/base.py +++ b/fmriprep/workflows/bold/base.py @@ -209,6 +209,7 @@ def init_bold_wf( "t1w_mask", "t1w_dseg", "t1w_tpms", + "t2w_preproc", # Optional # FreeSurfer outputs "subjects_dir", "subject_id", @@ -260,6 +261,7 @@ def init_bold_wf( ('t1w_preproc', 'inputnode.t1w_preproc'), ('t1w_mask', 'inputnode.t1w_mask'), ('t1w_dseg', 'inputnode.t1w_dseg'), + ('t2w_preproc', 'inputnode.t2w_preproc'), ('subjects_dir', 'inputnode.subjects_dir'), ('subject_id', 'inputnode.subject_id'), ('fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm'), From 73e6c9f427f24d6b72bd78701c6f2c634ee650eb Mon Sep 17 00:00:00 2001 From: mathiasg Date: Fri, 12 Jan 2024 17:17:17 -0500 Subject: [PATCH 02/22] ENH: Add T2w as an intermediate during bbr if available --- fmriprep/interfaces/patches.py | 25 ++++++++++++++++++ fmriprep/workflows/bold/fit.py | 9 +++++++ fmriprep/workflows/bold/registration.py | 34 +++++++++++++++++++++---- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/fmriprep/interfaces/patches.py b/fmriprep/interfaces/patches.py index f9773728b..9d660a019 100644 --- a/fmriprep/interfaces/patches.py +++ b/fmriprep/interfaces/patches.py @@ -29,7 +29,9 @@ from random import randint from time import sleep +import nipype.interfaces.freesurfer as fs from nipype.algorithms import confounds as nac +from nipype.interfaces.base import File, traits from numpy.linalg.linalg import LinAlgError @@ -77,3 +79,26 @@ def _run_interface(self, runtime): sleep(randint(start + 4, start + 10)) return runtime + + +class MRICoregInputSpec(fs.registration.MRICoregInputSpec): + reference_file = File( + argstr="--ref %s", + desc="reference (target) file", + copyfile=False, + ) + subject_id = traits.Str( + argstr="--s %s", + position=1, + requires=["subjects_dir"], + desc="freesurfer subject ID (implies ``reference_mask == " + "aparc+aseg.mgz`` unless otherwise specified)", + ) + + +class MRICoreg(fs.MRICoreg): + """ + Patched that allows setting both a reference file and the subjects dir. + """ + + input_spec = MRICoregInputSpec diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index f81285e62..7ee12fb33 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -215,6 +215,11 @@ def init_bold_fit_wf( layout=layout, ) + # T2w is not required, but if available, use to improve BOLD -> anat coreg + has_t2w = layout.get( + suffix='T2w', extension='.nii.gz', **config.execution.get().get('bids_filters', {}) + ) + basename = os.path.basename(bold_file) sbref_msg = f"No single-band-reference found for {basename}." if sbref_files and "sbref" in config.workflow.ignore: @@ -267,6 +272,8 @@ def init_bold_fit_wf( "subjects_dir", "subject_id", "fsnative2t1w_xfm", + # Optional - improvements if available + "t2w_preproc", ], ), name="inputnode", @@ -592,6 +599,7 @@ def init_bold_fit_wf( freesurfer=config.workflow.run_reconall, omp_nthreads=omp_nthreads, mem_gb=mem_gb["resampled"], + use_t2w=has_t2w, sloppy=config.execution.sloppy, ) @@ -609,6 +617,7 @@ def init_bold_fit_wf( ("t1w_preproc", "inputnode.t1w_preproc"), ("t1w_mask", "inputnode.t1w_mask"), ("t1w_dseg", "inputnode.t1w_dseg"), + ("t2w_preproc", "inputnode.t2w_preproc"), # Undefined if --fs-no-reconall, but this is safe ("subjects_dir", "inputnode.subjects_dir"), ("subject_id", "inputnode.subject_id"), diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index bde2423f2..6969c16da 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -48,12 +48,14 @@ def init_bold_reg_wf( + *, freesurfer: bool, use_bbr: bool, bold2t1w_dof: AffineDOF, bold2t1w_init: RegistrationInit, mem_gb: float, omp_nthreads: int, + use_t2w: bool = False, name: str = 'bold_reg_wf', sloppy: bool = False, ): @@ -97,6 +99,8 @@ def init_bold_reg_wf( Size of BOLD file in GB omp_nthreads : :obj:`int` Maximum number of threads an individual process may use + use_t2w: :obj:`bool` or None + name : :obj:`str` Name of workflow (default: ``bold_reg_wf``) @@ -196,13 +200,14 @@ def init_bbreg_wf( bold2t1w_dof: AffineDOF, bold2t1w_init: RegistrationInit, omp_nthreads: int, + use_t2w: bool = False, name: str = 'bbreg_wf', ): """ Build a workflow to run FreeSurfer's ``bbregister``. This workflow uses FreeSurfer's ``bbregister`` to register a BOLD image to - a T1-weighted structural image. + a T2-weighted structural image (if available), and ultimately a T1-weighted structual image. It is a counterpart to :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`, which performs the same task using FSL's FLIRT with a BBR cost function. @@ -266,11 +271,12 @@ def init_bbreg_wf( Boolean indicating whether BBR was rejected (mri_coreg registration returned) """ - from nipype.interfaces.freesurfer import BBRegister, MRICoreg + from nipype.interfaces.freesurfer import BBRegister from niworkflows.engine.workflows import LiterateWorkflow as Workflow - from niworkflows.interfaces.freesurfer import PatchedLTAConvert as LTAConvert from niworkflows.interfaces.nitransforms import ConcatenateXFMs + from fmriprep.interfaces.patches import MRICoreg + workflow = Workflow(name=name) workflow.__desc__ = """\ The BOLD reference was then co-registered to the T1w reference using @@ -293,6 +299,7 @@ def init_bbreg_wf( 't1w_preproc', # FLIRT BBR 't1w_mask', 't1w_dseg', + 't2w_preproc', # Conditional ] ), name='inputnode', @@ -314,7 +321,6 @@ def init_bbreg_wf( LOGGER.warning("Initializing BBR with header; affine fallback disabled") use_bbr = True - # Define both nodes, but only connect conditionally mri_coreg = pe.Node( MRICoreg(dof=bold2t1w_dof, sep=[4], ftol=0.0001, linmintol=0.01), name='mri_coreg', @@ -322,6 +328,10 @@ def init_bbreg_wf( mem_gb=5, ) + # Create separate mri_coreg to initialize bbregister + mri_coreg_t2w = mri_coreg.clone('mri_coreg_t2w') + mri_coreg_t2w.inputs.reference_mask = False + bbregister = pe.Node( BBRegister( dof=bold2t1w_dof, @@ -362,6 +372,15 @@ def init_bbreg_wf( (mri_coreg, transforms, [('out_lta_file', 'in2')]), ]) # fmt:skip + if use_t2w: + workflow.connect([ + (inputnode, mri_coreg_t2w, [ + ('subjects_dir', 'subjects_dir'), + ('subject_id', 'subject_id'), + ('in_file', 'source_file'), + ('t2w_preproc', 'reference_file')]), + ]) # fmt:skip + # Short-circuit workflow building, use initial registration if use_bbr is False: outputnode.inputs.fallback = True @@ -369,7 +388,9 @@ def init_bbreg_wf( return workflow # Otherwise bbregister will also be used - workflow.connect(mri_coreg, 'out_lta_file', bbregister, 'init_reg_file') + workflow.connect( + mri_coreg_t2w if use_t2w else mri_coreg, 'out_lta_file', bbregister, 'init_reg_file' + ) # Use bbregister workflow.connect([ @@ -379,6 +400,9 @@ def init_bbreg_wf( (bbregister, transforms, [('out_lta_file', 'in1')]), ]) # fmt:skip + if use_t2w: + workflow.connect(inputnode, 't2w_preproc', bbregister, 'intermediate_file') + # Short-circuit workflow building, use boundary-based registration if use_bbr is True: outputnode.inputs.fallback = False From 2fc94037cff85f102e264dda1296f2e7ea9cefa8 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Tue, 16 Jan 2024 15:56:57 -0500 Subject: [PATCH 03/22] FIX: Propagate T2w in bold_reg_wf --- fmriprep/workflows/bold/registration.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 6969c16da..8af3539da 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -149,6 +149,7 @@ def init_bold_reg_wf( 'subjects_dir', 'subject_id', 'fsnative2t1w_xfm', + 't2w_preproc', # Optional ] ), name='inputnode', @@ -162,6 +163,7 @@ def init_bold_reg_wf( if freesurfer: bbr_wf = init_bbreg_wf( use_bbr=use_bbr, + use_t2w=use_t2w, bold2t1w_dof=bold2t1w_dof, bold2t1w_init=bold2t1w_init, omp_nthreads=omp_nthreads, @@ -184,6 +186,7 @@ def init_bold_reg_wf( ('t1w_preproc', 'inputnode.t1w_preproc'), ('t1w_mask', 'inputnode.t1w_mask'), ('t1w_dseg', 'inputnode.t1w_dseg'), + ('t2w_preproc', 'inputnode.t2w_preproc'), ]), (bbr_wf, outputnode, [ ('outputnode.itk_bold_to_t1', 'itk_bold_to_t1'), @@ -427,6 +430,7 @@ def init_fsl_bbr_wf( bold2t1w_init: RegistrationInit, omp_nthreads: int, sloppy: bool = False, + use_t2w: bool = False, name: str = 'fsl_bbr_wf', ): """ @@ -525,6 +529,7 @@ def init_fsl_bbr_wf( 't1w_preproc', # FLIRT BBR 't1w_mask', 't1w_dseg', + 't2w_preproc', ] ), name='inputnode', From 0adca0d25a21cd2eb6e301351e2dccc0b76b8c6e Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 17 Jan 2024 21:10:24 -0500 Subject: [PATCH 04/22] DOC: Update docstring, workflow description --- fmriprep/workflows/bold/registration.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 8af3539da..294af7165 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -210,7 +210,8 @@ def init_bbreg_wf( Build a workflow to run FreeSurfer's ``bbregister``. This workflow uses FreeSurfer's ``bbregister`` to register a BOLD image to - a T2-weighted structural image (if available), and ultimately a T1-weighted structual image. + a T1-weighted structual image, leveraging a T2-weighted image (if available) + in the intermediate for improved tissue contrast. It is a counterpart to :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`, which performs the same task using FSL's FLIRT with a BBR cost function. @@ -244,6 +245,8 @@ def init_bbreg_wf( bold2t1w_init : str, 'header' or 'register' If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'register'``, align volumes by their centers. + use_t2w : :obj:`bool`, optional + If a T2w reference image is available, use it as an intermediate for BBR. name : :obj:`str`, optional Workflow name (default: bbreg_wf) @@ -263,6 +266,8 @@ def init_bbreg_wf( Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`) t1w_dseg Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`) + t2w_preproc + T2w reference in T1w space (Only used if ``use_t2w`` is ``True``) Outputs ------- @@ -292,6 +297,11 @@ def init_bbreg_wf( else 'to account for distortions remaining in the BOLD reference', ) + if use_t2w: + workflow.__desc__ += ( + " A T2w reference was used as an intermediate volume to improve tissue contrast." "" + ) + inputnode = pe.Node( niu.IdentityInterface( [ @@ -469,6 +479,8 @@ def init_fsl_bbr_wf( bold2t1w_init : str, 'header' or 'register' If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'register'``, align volumes by their centers. + use_t2w : :obj:`bool`, optional + Unused name : :obj:`str`, optional Workflow name (default: fsl_bbr_wf) From eacd39b3abc7008d4e603d965d938a93202652de Mon Sep 17 00:00:00 2001 From: mathiasg Date: Thu, 18 Jan 2024 09:51:34 -0500 Subject: [PATCH 05/22] RF: Ensure T2w check is bool --- fmriprep/workflows/bold/fit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 7ee12fb33..415106c26 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -216,8 +216,10 @@ def init_bold_fit_wf( ) # T2w is not required, but if available, use to improve BOLD -> anat coreg - has_t2w = layout.get( - suffix='T2w', extension='.nii.gz', **config.execution.get().get('bids_filters', {}) + has_t2w = bool( + layout.get( + suffix='T2w', extension='.nii.gz', **config.execution.get().get('bids_filters', {}) + ) ) basename = os.path.basename(bold_file) From 51d5ab58210e5e2f57d59e3e80577885f49c2dae Mon Sep 17 00:00:00 2001 From: mathiasg Date: Thu, 18 Jan 2024 11:50:52 -0500 Subject: [PATCH 06/22] RF: Extract bids filters, only use T2w filters --- fmriprep/workflows/bold/fit.py | 7 +++---- fmriprep/workflows/bold/registration.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 415106c26..d52a78408 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -203,6 +203,7 @@ def init_bold_fit_wf( from fmriprep.utils.misc import estimate_bold_mem_usage layout = config.execution.layout + bids_filters = config.execution.get().get('bids_filters', {}) # Fitting operates on the shortest echo # This could become more complicated in the future @@ -211,15 +212,13 @@ def init_bold_fit_wf( # Collect sbref files, sorted by EchoTime sbref_files = get_sbrefs( bold_series, - entity_overrides=config.execution.get().get('bids_filters', {}).get('sbref', {}), + entity_overrides=bids_filters.get('sbref', {}), layout=layout, ) # T2w is not required, but if available, use to improve BOLD -> anat coreg has_t2w = bool( - layout.get( - suffix='T2w', extension='.nii.gz', **config.execution.get().get('bids_filters', {}) - ) + layout.get(suffix='T2w', extension=['.nii', '.nii.gz'], **bids_filters.get('t2w', {})) ) basename = os.path.basename(bold_file) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 294af7165..b8401bf01 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -210,7 +210,7 @@ def init_bbreg_wf( Build a workflow to run FreeSurfer's ``bbregister``. This workflow uses FreeSurfer's ``bbregister`` to register a BOLD image to - a T1-weighted structual image, leveraging a T2-weighted image (if available) + a T1-weighted structural image, leveraging a T2-weighted image (if available) in the intermediate for improved tissue contrast. It is a counterpart to :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`, From db948e8d4e404f095015975662c2f1c4e09c5596 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Thu, 18 Jan 2024 16:13:49 -0500 Subject: [PATCH 07/22] TST: Reflect new report generation exit code The exit code for `--reports-only` should be an indicator of report generation status. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 648a4fed4..5956090df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -667,7 +667,7 @@ jobs: --reports-only --config-file /tmp/${DATASET}/work/${UUID}/config.toml -vv RET=$? set -e - [[ "$RET" -eq "1" ]] + [[ "$RET" -eq "0" ]] # ensure report was generated successfully - run: name: Clean working directory when: on_success From fd73828affe4d12c2b4071a21917ce296322676b Mon Sep 17 00:00:00 2001 From: mathiasg Date: Thu, 18 Jan 2024 16:17:37 -0500 Subject: [PATCH 08/22] RF: Deduplication --- fmriprep/cli/run.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/fmriprep/cli/run.py b/fmriprep/cli/run.py index 551dc014c..69f8bf71b 100644 --- a/fmriprep/cli/run.py +++ b/fmriprep/cli/run.py @@ -224,17 +224,14 @@ def main(): write_bidsignore(config.execution.fmriprep_dir) if failed_reports: - config.loggers.cli.error( - "Report generation was not successful for the following participants : %s.", - ", ".join(failed_reports), + msg = ( + 'Report generation was not successful for the following participants ' + f': {", ".join(failed_reports)}.' ) + config.loggers.cli.error(msg) + if sentry_sdk is not None: + sentry_sdk.capture_message(msg, level="error") - if sentry_sdk is not None and failed_reports: - sentry_sdk.capture_message( - "Report generation was not successful for the following participants : %s.", - ", ".join(failed_reports), - level="error", - ) sys.exit(int((errno + len(failed_reports)) > 0)) From 97edb514bd509d2e3335d91598349dbc80d77e6f Mon Sep 17 00:00:00 2001 From: mathiasg Date: Mon, 22 Jan 2024 12:28:24 -0500 Subject: [PATCH 09/22] MAINT: Modify arguments, add action for deprecations --- fmriprep/cli/parser.py | 55 +++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py index 5da912240..dcf473d55 100644 --- a/fmriprep/cli/parser.py +++ b/fmriprep/cli/parser.py @@ -31,7 +31,7 @@ def _build_parser(**kwargs): ``kwargs`` are passed to ``argparse.ArgumentParser`` (mainly useful for debugging). """ - from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser + from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentParser from functools import partial from pathlib import Path @@ -40,6 +40,27 @@ def _build_parser(**kwargs): from .version import check_latest, is_flagged + deprecations = { + # parser attribute name: (replacement flag, version slated to be removed in) + 'use_aroma': (None, '24.0.0'), + 'aroma_melodic_dim': (None, '24.0.0'), + 'aroma_err_on_warn': (None, '24.0.0'), + 'bold2t1w_init': ('--bold2anat-init', '24.2.0'), + 'bold2t1w_dof': ('--bold2anat-dof', '24.2.0'), + } + + class DeprecatedAction(Action): + def __call__(self, parser, namespace, values, option_string=None): + new_opt, rem_vers = deprecations.get(self.dest, (None, None)) + msg = ( + f"{self.option_strings} has been deprecated and will be removed in " + f"{rem_vers or 'a later version'}." + ) + if new_opt: + msg += f" Please use `{new_opt}` instead." + print(msg, file=sys.stderr) + delattr(namespace, self.dest) + def _path_exists(path, parser): """Ensure a given path exists.""" if path is None or not Path(path).exists(): @@ -313,19 +334,32 @@ def _slice_time_ref(value, parser): ) g_conf.add_argument( "--bold2t1w-init", - action="store", - default="register", + action=DeprecatedAction, choices=["register", "header"], - help='Either "register" (the default) to initialize volumes at center or "header"' - " to use the header information when coregistering BOLD to T1w images.", + help="Deprecated - use `--bold2anat-init` instead.", ) g_conf.add_argument( "--bold2t1w-dof", + action=DeprecatedAction, + choices=[6, 9, 12], + type=int, + help="Deprecated - use `--bold2anat-dof` instead.", + ) + g_conf.add_argument( + "--bold2anat-init", + choices=["auto", "t1w", "header"], + default="auto", + help="Method of BOLD to anatomical coregistration. If `auto`, a T2w image is used if " + "available, otherwise the T1w image. `t1w` forces use of the T1w, and `header` uses " + "the T1w header information.", + ) + g_conf.add_argument( + "--bold2anat-dof", action="store", default=6, choices=[6, 9, 12], type=int, - help="Degrees of freedom when registering BOLD to T1w images. " + help="Degrees of freedom when registering BOLD to anatomical images. " "6 degrees (rotation and translation) are used by default.", ) g_conf.add_argument( @@ -445,23 +479,20 @@ def _slice_time_ref(value, parser): g_aroma = parser.add_argument_group("[DEPRECATED] Options for running ICA_AROMA") g_aroma.add_argument( "--use-aroma", - action="store_true", - default=False, + action=DeprecatedAction, help="Deprecated. Will raise an error in 24.0.", ) g_aroma.add_argument( "--aroma-melodic-dimensionality", dest="aroma_melodic_dim", - action="store", - default=0, + action=DeprecatedAction, type=int, help="Deprecated. Will raise an error in 24.0.", ) g_aroma.add_argument( "--error-on-aroma-warnings", - action="store_true", + action=DeprecatedAction, dest="aroma_err_on_warn", - default=False, help="Deprecated. Will raise an error in 24.0.", ) From 84d4bca187c1f9ddb0167c1001b3f1da7afdfd96 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 14:30:04 -0500 Subject: [PATCH 10/22] RF: bold2t1w -> bold2anat --- fmriprep/config.py | 11 +- fmriprep/data/tests/config.toml | 2 +- fmriprep/workflows/bold/fit.py | 8 +- fmriprep/workflows/bold/registration.py | 128 +++++++++++------------- fmriprep/workflows/tests/test_base.py | 22 ++-- 5 files changed, 82 insertions(+), 89 deletions(-) diff --git a/fmriprep/config.py b/fmriprep/config.py index f7ec4551b..aabea97c7 100644 --- a/fmriprep/config.py +++ b/fmriprep/config.py @@ -525,11 +525,12 @@ class workflow(_Config): aroma_melodic_dim = None """Number of ICA components to be estimated by MELODIC (positive = exact, negative = maximum).""" - bold2t1w_dof = None - """Degrees of freedom of the BOLD-to-T1w registration steps.""" - bold2t1w_init = "register" - """Whether to use standard coregistration ('register') or to initialize coregistration from the - BOLD image-header ('header').""" + bold2anat_dof = None + """Degrees of freedom of the BOLD-to-anatomical registration steps.""" + bold2anat_init = "auto" + """Method of BOLD to anatomical coregistration. The target anatomical (``'t1w'``, ``'t2w'``) + can be specified, otherwise ``'auto'`` will prefer a T2w image but fall back to T1w if none + are available. Alternatively, ``'header'`` will use the T1w header information.""" cifti_output = None """Generate HCP Grayordinates, accepts either ``'91k'`` (default) or ``'170k'``.""" dummy_scans = None diff --git a/fmriprep/data/tests/config.toml b/fmriprep/data/tests/config.toml index 0519e4702..b1b7e31b6 100644 --- a/fmriprep/data/tests/config.toml +++ b/fmriprep/data/tests/config.toml @@ -32,7 +32,7 @@ write_graph = false anat_only = false aroma_err_on_warn = false aroma_melodic_dim = -200 -bold2t1w_dof = 6 +bold2anat_dof = 6 fmap_bspline = false force_syn = false hires = true diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index d52a78408..e916dc9d3 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -320,8 +320,8 @@ def init_bold_fit_wf( FunctionalSummary( distortion_correction="None", # Can override with connection registration=("FSL", "FreeSurfer")[config.workflow.run_reconall], - registration_dof=config.workflow.bold2t1w_dof, - registration_init=config.workflow.bold2t1w_init, + registration_dof=config.workflow.bold2anat_dof, + registration_init=config.workflow.bold2anat_init, pe_direction=metadata.get("PhaseEncodingDirection"), echo_idx=entities.get("echo", []), tr=metadata["RepetitionTime"], @@ -594,8 +594,8 @@ def init_bold_fit_wf( if not boldref2anat_xform: # calculate BOLD registration to T1w bold_reg_wf = init_bold_reg_wf( - bold2t1w_dof=config.workflow.bold2t1w_dof, - bold2t1w_init=config.workflow.bold2t1w_init, + bold2anat_dof=config.workflow.bold2anat_dof, + bold2anat_init=config.workflow.bold2anat_init, use_bbr=config.workflow.use_bbr, freesurfer=config.workflow.run_reconall, omp_nthreads=omp_nthreads, diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index b8401bf01..aec6807aa 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -38,21 +38,20 @@ from nipype.pipeline import engine as pe from ... import config, data -from ...interfaces import DerivativesDataSink DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB LOGGER = config.loggers.workflow AffineDOF = ty.Literal[6, 9, 12] -RegistrationInit = ty.Literal['register', 'header'] +RegistrationInit = ty.Literal['t1w', 't2w', 'header'] def init_bold_reg_wf( *, freesurfer: bool, use_bbr: bool, - bold2t1w_dof: AffineDOF, - bold2t1w_init: RegistrationInit, + bold2anat_dof: AffineDOF, + bold2anat_init: RegistrationInit, mem_gb: float, omp_nthreads: int, use_t2w: bool = False, @@ -80,8 +79,8 @@ def init_bold_reg_wf( mem_gb=3, omp_nthreads=1, use_bbr=True, - bold2t1w_dof=9, - bold2t1w_init='register') + bold2anat_dof=9, + bold2anat_init='auto') Parameters ---------- @@ -90,17 +89,16 @@ def init_bold_reg_wf( use_bbr : :obj:`bool` or None Enable/disable boundary-based registration refinement. If ``None``, test BBR result for distortion before accepting. - bold2t1w_dof : 6, 9 or 12 - Degrees-of-freedom for BOLD-T1w registration - bold2t1w_init : str, 'header' or 'register' + bold2anat_dof : 6, 9 or 12 + Degrees-of-freedom for BOLD-anatomical registration + bold2anat_init : str, 't1w', 't2w' or 'register' If ``'header'``, use header information for initialization of BOLD and T1 images. - If ``'register'``, align volumes by their centers. + If ``'t1w'``, align BOLD to T1w by their centers. + If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. mem_gb : :obj:`float` Size of BOLD file in GB omp_nthreads : :obj:`int` Maximum number of threads an individual process may use - use_t2w: :obj:`bool` or None - name : :obj:`str` Name of workflow (default: ``bold_reg_wf``) @@ -163,16 +161,15 @@ def init_bold_reg_wf( if freesurfer: bbr_wf = init_bbreg_wf( use_bbr=use_bbr, - use_t2w=use_t2w, - bold2t1w_dof=bold2t1w_dof, - bold2t1w_init=bold2t1w_init, + bold2anat_dof=bold2anat_dof, + bold2anat_init=bold2anat_init, omp_nthreads=omp_nthreads, ) else: bbr_wf = init_fsl_bbr_wf( use_bbr=use_bbr, - bold2t1w_dof=bold2t1w_dof, - bold2t1w_init=bold2t1w_init, + bold2anat_dof=bold2anat_dof, + bold2anat_init=bold2anat_init, sloppy=sloppy, omp_nthreads=omp_nthreads, ) @@ -200,18 +197,16 @@ def init_bold_reg_wf( def init_bbreg_wf( use_bbr: bool, - bold2t1w_dof: AffineDOF, - bold2t1w_init: RegistrationInit, + bold2anat_dof: AffineDOF, + bold2anat_init: RegistrationInit, omp_nthreads: int, - use_t2w: bool = False, name: str = 'bbreg_wf', ): """ Build a workflow to run FreeSurfer's ``bbregister``. This workflow uses FreeSurfer's ``bbregister`` to register a BOLD image to - a T1-weighted structural image, leveraging a T2-weighted image (if available) - in the intermediate for improved tissue contrast. + a T1-weighted structural image. It is a counterpart to :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`, which performs the same task using FSL's FLIRT with a BBR cost function. @@ -231,8 +226,8 @@ def init_bbreg_wf( :simple_form: yes from fmriprep.workflows.bold.registration import init_bbreg_wf - wf = init_bbreg_wf(use_bbr=True, bold2t1w_dof=9, - bold2t1w_init='register', omp_nthreads=1) + wf = init_bbreg_wf(use_bbr=True, bold2anat_dof=9, + bold2anat_init='t1w', omp_nthreads=1) Parameters @@ -240,11 +235,12 @@ def init_bbreg_wf( use_bbr : :obj:`bool` or None Enable/disable boundary-based registration refinement. If ``None``, test BBR result for distortion before accepting. - bold2t1w_dof : 6, 9 or 12 - Degrees-of-freedom for BOLD-T1w registration - bold2t1w_init : str, 'header' or 'register' + bold2anat_dof : 6, 9 or 12 + Degrees-of-freedom for BOLD-anatomical registration + bold2anat_init : str, 't1w', 't2w' or 'register' If ``'header'``, use header information for initialization of BOLD and T1 images. - If ``'register'``, align volumes by their centers. + If ``'t1w'``, align BOLD to T1w by their centers. + If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. use_t2w : :obj:`bool`, optional If a T2w reference image is available, use it as an intermediate for BBR. name : :obj:`str`, optional @@ -267,7 +263,7 @@ def init_bbreg_wf( t1w_dseg Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`) t2w_preproc - T2w reference in T1w space (Only used if ``use_t2w`` is ``True``) + T2w reference in T1w space (Only used if ``bold2anat_init`` is ``t2w``) Outputs ------- @@ -291,15 +287,16 @@ def init_bbreg_wf( `bbregister` (FreeSurfer) which implements boundary-based registration [@bbr]. Co-registration was configured with {dof} degrees of freedom{reason}. """.format( - dof={6: 'six', 9: 'nine', 12: 'twelve'}[bold2t1w_dof], + dof={6: 'six', 9: 'nine', 12: 'twelve'}[bold2anat_dof], reason='' - if bold2t1w_dof == 6 + if bold2anat_dof == 6 else 'to account for distortions remaining in the BOLD reference', ) + use_t2w = bold2anat_init == 't2w' if use_t2w: workflow.__desc__ += ( - " A T2w reference was used as an intermediate volume to improve tissue contrast." "" + " A T2w reference was used as an intermediate volume to improve tissue contrast." ) inputnode = pe.Node( @@ -322,12 +319,12 @@ def init_bbreg_wf( name='outputnode', ) - if bold2t1w_init not in ("register", "header"): - raise ValueError(f"Unknown BOLD-T1w initialization option: {bold2t1w_init}") + if bold2anat_init not in ty.get_args(RegistrationInit): + raise ValueError(f"Unknown BOLD-anat initialization option: {bold2anat_init}") # For now make BBR unconditional - in the future, we can fall back to identity, # but adding the flexibility without testing seems a bit dangerous - if bold2t1w_init == "header": + if bold2anat_init == "header": if use_bbr is False: raise ValueError("Cannot disable BBR and use header registration") if use_bbr is None: @@ -335,26 +332,24 @@ def init_bbreg_wf( use_bbr = True mri_coreg = pe.Node( - MRICoreg(dof=bold2t1w_dof, sep=[4], ftol=0.0001, linmintol=0.01), + MRICoreg(dof=bold2anat_dof, sep=[4], ftol=0.0001, linmintol=0.01), name='mri_coreg', n_procs=omp_nthreads, mem_gb=5, ) - - # Create separate mri_coreg to initialize bbregister - mri_coreg_t2w = mri_coreg.clone('mri_coreg_t2w') - mri_coreg_t2w.inputs.reference_mask = False + if use_t2w: + mri_coreg.inputs.reference_mask = False bbregister = pe.Node( BBRegister( - dof=bold2t1w_dof, + dof=bold2anat_dof, contrast_type='t2', out_lta_file=True, ), name='bbregister', mem_gb=12, ) - if bold2t1w_init == "header": + if bold2anat_init == "header": bbregister.inputs.init = "header" transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') @@ -377,7 +372,7 @@ def init_bbreg_wf( ]) # fmt:skip # Do not initialize with header, use mri_coreg - if bold2t1w_init == "register": + if bold2anat_init == "register": workflow.connect([ (inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'), ('subject_id', 'subject_id'), @@ -386,13 +381,7 @@ def init_bbreg_wf( ]) # fmt:skip if use_t2w: - workflow.connect([ - (inputnode, mri_coreg_t2w, [ - ('subjects_dir', 'subjects_dir'), - ('subject_id', 'subject_id'), - ('in_file', 'source_file'), - ('t2w_preproc', 'reference_file')]), - ]) # fmt:skip + workflow.connect(inputnode, 't2w_preproc', mri_coreg, 'reference_file') # Short-circuit workflow building, use initial registration if use_bbr is False: @@ -401,9 +390,7 @@ def init_bbreg_wf( return workflow # Otherwise bbregister will also be used - workflow.connect( - mri_coreg_t2w if use_t2w else mri_coreg, 'out_lta_file', bbregister, 'init_reg_file' - ) + workflow.connect(mri_coreg, 'out_lta_file', bbregister, 'init_reg_file') # Use bbregister workflow.connect([ @@ -422,7 +409,7 @@ def init_bbreg_wf( return workflow - # Only reach this point if bold2t1w_init is "register" and use_bbr is None + # Only reach this point if bold2anat_init is "register" and use_bbr is None compare_transforms = pe.Node(niu.Function(function=compare_xforms), name='compare_transforms') workflow.connect([ @@ -436,8 +423,8 @@ def init_bbreg_wf( def init_fsl_bbr_wf( use_bbr: bool, - bold2t1w_dof: AffineDOF, - bold2t1w_init: RegistrationInit, + bold2anat_dof: AffineDOF, + bold2anat_init: RegistrationInit, omp_nthreads: int, sloppy: bool = False, use_t2w: bool = False, @@ -466,7 +453,7 @@ def init_fsl_bbr_wf( :simple_form: yes from fmriprep.workflows.bold.registration import init_fsl_bbr_wf - wf = init_fsl_bbr_wf(use_bbr=True, bold2t1w_dof=9, bold2t1w_init='register', omp_nthreads=1) + wf = init_fsl_bbr_wf(use_bbr=True, bold2anat_dof=9, bold2anat_init='t1w', omp_nthreads=1) Parameters @@ -474,11 +461,12 @@ def init_fsl_bbr_wf( use_bbr : :obj:`bool` or None Enable/disable boundary-based registration refinement. If ``None``, test BBR result for distortion before accepting. - bold2t1w_dof : 6, 9 or 12 - Degrees-of-freedom for BOLD-T1w registration - bold2t1w_init : str, 'header' or 'register' + bold2anat_dof : 6, 9 or 12 + Degrees-of-freedom for BOLD-anatomical registration + bold2anat_init : str, 't1w', 't2w' or 'register' If ``'header'``, use header information for initialization of BOLD and T1 images. - If ``'register'``, align volumes by their centers. + If ``'t1w'``, align BOLD to T1w by their centers. + If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. use_t2w : :obj:`bool`, optional Unused name : :obj:`str`, optional @@ -525,9 +513,9 @@ def init_fsl_bbr_wf( Co-registration was configured with {dof} degrees of freedom{reason}. """.format( fsl_ver=fsl.FLIRT().version or '', - dof={6: 'six', 9: 'nine', 12: 'twelve'}[bold2t1w_dof], + dof={6: 'six', 9: 'nine', 12: 'twelve'}[bold2anat_dof], reason='' - if bold2t1w_dof == 6 + if bold2anat_dof == 6 else 'to account for distortions remaining in the BOLD reference', ) @@ -554,17 +542,21 @@ def init_fsl_bbr_wf( wm_mask = pe.Node(niu.Function(function=_dseg_label), name='wm_mask') wm_mask.inputs.label = 2 # BIDS default is WM=2 - if bold2t1w_init not in ("register", "header"): - raise ValueError(f"Unknown BOLD-T1w initialization option: {bold2t1w_init}") + if bold2anat_init not in ty.get_args(RegistrationInit): + raise ValueError(f"Unknown BOLD-T1w initialization option: {bold2anat_init}") - if bold2t1w_init == "header": + if bold2anat_init == "header": raise NotImplementedError("Header-based registration initialization not supported for FSL") + if bold2anat_init == "t2w": + LOGGER.warning( + "T2w intermediate for FSL is not implemented, registering with T1w instead." + ) # Mask T1w_preproc with T1w_mask to make T1w_brain mask_t1w_brain = pe.Node(ApplyMask(), name='mask_t1w_brain') mri_coreg = pe.Node( - MRICoreg(dof=bold2t1w_dof, sep=[4], ftol=0.0001, linmintol=0.01), + MRICoreg(dof=bold2anat_dof, sep=[4], ftol=0.0001, linmintol=0.01), name='mri_coreg', n_procs=omp_nthreads, mem_gb=5, @@ -618,7 +610,7 @@ def init_fsl_bbr_wf( return workflow flt_bbr = pe.Node( - fsl.FLIRT(cost_func='bbr', dof=bold2t1w_dof, args="-basescale 1"), + fsl.FLIRT(cost_func='bbr', dof=bold2anat_dof, args="-basescale 1"), name='flt_bbr', ) diff --git a/fmriprep/workflows/tests/test_base.py b/fmriprep/workflows/tests/test_base.py index 027609459..0bea4c510 100644 --- a/fmriprep/workflows/tests/test_base.py +++ b/fmriprep/workflows/tests/test_base.py @@ -105,7 +105,7 @@ def bids_root(tmp_path_factory): def _make_params( - bold2t1w_init: str = "register", + bold2anat_init: str = "auto", use_bbr: bool | None = None, dummy_scans: int | None = None, me_output_echos: bool = False, @@ -125,7 +125,7 @@ def _make_params( if bids_filters is None: bids_filters = {} return ( - bold2t1w_init, + bold2anat_init, use_bbr, dummy_scans, me_output_echos, @@ -146,7 +146,7 @@ def _make_params( @pytest.mark.parametrize("anat_only", [False, True]) @pytest.mark.parametrize( ( - "bold2t1w_init", + "bold2anat_init", "use_bbr", "dummy_scans", "me_output_echos", @@ -163,12 +163,12 @@ def _make_params( ), [ _make_params(), - _make_params(bold2t1w_init="header"), + _make_params(bold2anat_init="header"), _make_params(use_bbr=True), _make_params(use_bbr=False), - _make_params(bold2t1w_init="header", use_bbr=True), + _make_params(bold2anat_init="header", use_bbr=True), # Currently disabled - # _make_params(bold2t1w_init="header", use_bbr=False), + # _make_params(bold2anat_init="header", use_bbr=False), _make_params(dummy_scans=2), _make_params(me_output_echos=True), _make_params(medial_surface_nan=True), @@ -183,9 +183,9 @@ def _make_params( _make_params(freesurfer=False, use_bbr=True), _make_params(freesurfer=False, use_bbr=False), # Currently unsupported: - # _make_params(freesurfer=False, bold2t1w_init="header"), - # _make_params(freesurfer=False, bold2t1w_init="header", use_bbr=True), - # _make_params(freesurfer=False, bold2t1w_init="header", use_bbr=False), + # _make_params(freesurfer=False, bold2anat_init="header"), + # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=True), + # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=False), # Regression test for gh-3154: _make_params(bids_filters={'sbref': {'suffix': 'sbref'}}), ], @@ -195,7 +195,7 @@ def test_init_fmriprep_wf( tmp_path: Path, level: str, anat_only: bool, - bold2t1w_init: str, + bold2anat_init: str, use_bbr: bool | None, dummy_scans: int | None, me_output_echos: bool, @@ -213,7 +213,7 @@ def test_init_fmriprep_wf( with mock_config(bids_dir=bids_root): config.workflow.level = level config.workflow.anat_only = anat_only - config.workflow.bold2t1w_init = bold2t1w_init + config.workflow.bold2anat_init = bold2anat_init config.workflow.use_bbr = use_bbr config.workflow.dummy_scans = dummy_scans config.execution.me_output_echos = me_output_echos From 3b335fcd57ac621df1844b8003494c24f573d6d7 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 15:37:29 -0500 Subject: [PATCH 11/22] ENH: Patch FSSource output T2, simplify in-workflow T2 use logic --- fmriprep/interfaces/patches.py | 17 +++++++++++++++-- fmriprep/workflows/base.py | 4 ++++ fmriprep/workflows/bold/fit.py | 1 - fmriprep/workflows/bold/registration.py | 22 +++++++--------------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/fmriprep/interfaces/patches.py b/fmriprep/interfaces/patches.py index 9d660a019..49351a23d 100644 --- a/fmriprep/interfaces/patches.py +++ b/fmriprep/interfaces/patches.py @@ -30,6 +30,7 @@ from time import sleep import nipype.interfaces.freesurfer as fs +import nipype.interfaces.io as nio from nipype.algorithms import confounds as nac from nipype.interfaces.base import File, traits from numpy.linalg.linalg import LinAlgError @@ -81,7 +82,7 @@ def _run_interface(self, runtime): return runtime -class MRICoregInputSpec(fs.registration.MRICoregInputSpec): +class _MRICoregInputSpec(fs.registration.MRICoregInputSpec): reference_file = File( argstr="--ref %s", desc="reference (target) file", @@ -101,4 +102,16 @@ class MRICoreg(fs.MRICoreg): Patched that allows setting both a reference file and the subjects dir. """ - input_spec = MRICoregInputSpec + input_spec = _MRICoregInputSpec + + +class _FSSourceOutputSpec(nio.FSSourceOutputSpec): + T2 = File(desc="Intensity normalized whole-head volume", loc="mri") + + +class FreeSurferSource(nio.FreeSurferSource): + """ + Patch to allow grabbing the T2 volume, if available + """ + + output_spec = _FSSourceOutputSpec diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py index 2cfd40249..acc2951b9 100644 --- a/fmriprep/workflows/base.py +++ b/fmriprep/workflows/base.py @@ -623,6 +623,10 @@ def init_single_subject_wf(subject_id: str): num_bold=len(bold_runs) ) + # Before initializing BOLD workflow, select/verify anatomical target for coregistration + if config.workflow.bold2anat_init in ('auto', 't2w'): + config.workflow.bold2anat_init = 't2w' if subject_data['t2w'] else 't1w' + for bold_series in bold_runs: bold_file = bold_series[0] fieldmap_id = estimator_map.get(bold_file) diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index e916dc9d3..a550293e6 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -600,7 +600,6 @@ def init_bold_fit_wf( freesurfer=config.workflow.run_reconall, omp_nthreads=omp_nthreads, mem_gb=mem_gb["resampled"], - use_t2w=has_t2w, sloppy=config.execution.sloppy, ) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index aec6807aa..85ffbacf9 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -54,7 +54,6 @@ def init_bold_reg_wf( bold2anat_init: RegistrationInit, mem_gb: float, omp_nthreads: int, - use_t2w: bool = False, name: str = 'bold_reg_wf', sloppy: bool = False, ): @@ -147,7 +146,6 @@ def init_bold_reg_wf( 'subjects_dir', 'subject_id', 'fsnative2t1w_xfm', - 't2w_preproc', # Optional ] ), name='inputnode', @@ -183,7 +181,6 @@ def init_bold_reg_wf( ('t1w_preproc', 'inputnode.t1w_preproc'), ('t1w_mask', 'inputnode.t1w_mask'), ('t1w_dseg', 'inputnode.t1w_dseg'), - ('t2w_preproc', 'inputnode.t2w_preproc'), ]), (bbr_wf, outputnode, [ ('outputnode.itk_bold_to_t1', 'itk_bold_to_t1'), @@ -241,8 +238,6 @@ def init_bbreg_wf( If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'t1w'``, align BOLD to T1w by their centers. If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. - use_t2w : :obj:`bool`, optional - If a T2w reference image is available, use it as an intermediate for BBR. name : :obj:`str`, optional Workflow name (default: bbreg_wf) @@ -262,8 +257,6 @@ def init_bbreg_wf( Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`) t1w_dseg Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`) - t2w_preproc - T2w reference in T1w space (Only used if ``bold2anat_init`` is ``t2w``) Outputs ------- @@ -279,7 +272,7 @@ def init_bbreg_wf( from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.interfaces.nitransforms import ConcatenateXFMs - from fmriprep.interfaces.patches import MRICoreg + from fmriprep.interfaces.patches import FreeSurferSource, MRICoreg workflow = Workflow(name=name) workflow.__desc__ = """\ @@ -309,7 +302,6 @@ def init_bbreg_wf( 't1w_preproc', # FLIRT BBR 't1w_mask', 't1w_dseg', - 't2w_preproc', # Conditional ] ), name='inputnode', @@ -331,6 +323,8 @@ def init_bbreg_wf( LOGGER.warning("Initializing BBR with header; affine fallback disabled") use_bbr = True + fssource = pe.Node(FreeSurferSource(), name='fssource') + mri_coreg = pe.Node( MRICoreg(dof=bold2anat_dof, sep=[4], ftol=0.0001, linmintol=0.01), name='mri_coreg', @@ -362,6 +356,8 @@ def init_bbreg_wf( concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm') workflow.connect([ + (inputnode, fssource, [('subjects_dir', 'subjects_dir'), + ('subject_id', 'subject_id')]), (inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]), # Wire up the co-registration alternatives (transforms, select_transform, [('out', 'inlist')]), @@ -381,7 +377,7 @@ def init_bbreg_wf( ]) # fmt:skip if use_t2w: - workflow.connect(inputnode, 't2w_preproc', mri_coreg, 'reference_file') + workflow.connect(fssource, 'T2', mri_coreg, 'reference_file') # Short-circuit workflow building, use initial registration if use_bbr is False: @@ -401,7 +397,7 @@ def init_bbreg_wf( ]) # fmt:skip if use_t2w: - workflow.connect(inputnode, 't2w_preproc', bbregister, 'intermediate_file') + workflow.connect(fssource, 'T2', bbregister, 'intermediate_file') # Short-circuit workflow building, use boundary-based registration if use_bbr is True: @@ -427,7 +423,6 @@ def init_fsl_bbr_wf( bold2anat_init: RegistrationInit, omp_nthreads: int, sloppy: bool = False, - use_t2w: bool = False, name: str = 'fsl_bbr_wf', ): """ @@ -467,8 +462,6 @@ def init_fsl_bbr_wf( If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'t1w'``, align BOLD to T1w by their centers. If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. - use_t2w : :obj:`bool`, optional - Unused name : :obj:`str`, optional Workflow name (default: fsl_bbr_wf) @@ -529,7 +522,6 @@ def init_fsl_bbr_wf( 't1w_preproc', # FLIRT BBR 't1w_mask', 't1w_dseg', - 't2w_preproc', ] ), name='inputnode', From b92a4c9c07389aefc8d8fa464f88391e3b9d69e2 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 15:40:38 -0500 Subject: [PATCH 12/22] FIX: Update FunctionalSummary expected values --- fmriprep/interfaces/reports.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fmriprep/interfaces/reports.py b/fmriprep/interfaces/reports.py index 5108e6236..0ba92fb14 100644 --- a/fmriprep/interfaces/reports.py +++ b/fmriprep/interfaces/reports.py @@ -213,11 +213,12 @@ class FunctionalSummaryInputSpec(TraitedSpec): 6, 9, 12, desc='Registration degrees of freedom', mandatory=True ) registration_init = traits.Enum( - 'register', + 't1w', + 't2w', 'header', mandatory=True, desc='Whether to initialize registration with the "header"' - ' or by centering the volumes ("register")', + ' or by centering the volumes ("t1w" or "t2w")', ) tr = traits.Float(desc='Repetition time', mandatory=True) dummy_scans = traits.Either(traits.Int(), None, desc='number of dummy scans specified by user') From 8ef01535dddc156c5fcdcfe4436d2b618447cc99 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 15:43:17 -0500 Subject: [PATCH 13/22] FIX: Drop unused connection --- fmriprep/workflows/bold/fit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index a550293e6..740fa84ec 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -617,7 +617,6 @@ def init_bold_fit_wf( ("t1w_preproc", "inputnode.t1w_preproc"), ("t1w_mask", "inputnode.t1w_mask"), ("t1w_dseg", "inputnode.t1w_dseg"), - ("t2w_preproc", "inputnode.t2w_preproc"), # Undefined if --fs-no-reconall, but this is safe ("subjects_dir", "inputnode.subjects_dir"), ("subject_id", "inputnode.subject_id"), From ddcee99f02a606f0ea4a4410440d98b9c4a723cc Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 17:21:31 -0500 Subject: [PATCH 14/22] FIX: Remove brittle query --- fmriprep/workflows/bold/fit.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 740fa84ec..8bca33670 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -216,11 +216,6 @@ def init_bold_fit_wf( layout=layout, ) - # T2w is not required, but if available, use to improve BOLD -> anat coreg - has_t2w = bool( - layout.get(suffix='T2w', extension=['.nii', '.nii.gz'], **bids_filters.get('t2w', {})) - ) - basename = os.path.basename(bold_file) sbref_msg = f"No single-band-reference found for {basename}." if sbref_files and "sbref" in config.workflow.ignore: From 00772918d08bf1031014970ccb26c59bb9833182 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 17:22:02 -0500 Subject: [PATCH 15/22] FIX: Purge traces of "register" value --- fmriprep/workflows/bold/registration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index 85ffbacf9..c73f24dff 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -90,7 +90,7 @@ def init_bold_reg_wf( If ``None``, test BBR result for distortion before accepting. bold2anat_dof : 6, 9 or 12 Degrees-of-freedom for BOLD-anatomical registration - bold2anat_init : str, 't1w', 't2w' or 'register' + bold2anat_init : str, 't1w', 't2w' or 'header' If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'t1w'``, align BOLD to T1w by their centers. If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. @@ -234,7 +234,7 @@ def init_bbreg_wf( If ``None``, test BBR result for distortion before accepting. bold2anat_dof : 6, 9 or 12 Degrees-of-freedom for BOLD-anatomical registration - bold2anat_init : str, 't1w', 't2w' or 'register' + bold2anat_init : str, 't1w', 't2w' or 'header' If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'t1w'``, align BOLD to T1w by their centers. If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. @@ -368,7 +368,7 @@ def init_bbreg_wf( ]) # fmt:skip # Do not initialize with header, use mri_coreg - if bold2anat_init == "register": + if bold2anat_init != "header": workflow.connect([ (inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'), ('subject_id', 'subject_id'), @@ -405,7 +405,7 @@ def init_bbreg_wf( return workflow - # Only reach this point if bold2anat_init is "register" and use_bbr is None + # Only reach this point if bold2anat_init is "t1w" or "t2w" and use_bbr is None compare_transforms = pe.Node(niu.Function(function=compare_xforms), name='compare_transforms') workflow.connect([ @@ -458,7 +458,7 @@ def init_fsl_bbr_wf( If ``None``, test BBR result for distortion before accepting. bold2anat_dof : 6, 9 or 12 Degrees-of-freedom for BOLD-anatomical registration - bold2anat_init : str, 't1w', 't2w' or 'register' + bold2anat_init : str, 't1w', 't2w' or 'header' If ``'header'``, use header information for initialization of BOLD and T1 images. If ``'t1w'``, align BOLD to T1w by their centers. If ``'t2w'``, align BOLD to T1w using the T2w as an intermediate. From 4524d165b18746d5dfb3eb2570d1382d966954d8 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 17:50:43 -0500 Subject: [PATCH 16/22] TST: Set proper default --- fmriprep/workflows/tests/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fmriprep/workflows/tests/test_base.py b/fmriprep/workflows/tests/test_base.py index 0bea4c510..74ae6633f 100644 --- a/fmriprep/workflows/tests/test_base.py +++ b/fmriprep/workflows/tests/test_base.py @@ -105,7 +105,7 @@ def bids_root(tmp_path_factory): def _make_params( - bold2anat_init: str = "auto", + bold2anat_init: str = "t1w", use_bbr: bool | None = None, dummy_scans: int | None = None, me_output_echos: bool = False, From 6c448a257bdf728ee4e7261d0b2f7766c3240dab Mon Sep 17 00:00:00 2001 From: mathiasg Date: Wed, 24 Jan 2024 21:02:05 -0500 Subject: [PATCH 17/22] TST: Parametrize bold2anat in test --- fmriprep/workflows/bold/tests/test_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fmriprep/workflows/bold/tests/test_base.py b/fmriprep/workflows/bold/tests/test_base.py index 93035c640..e4597100c 100644 --- a/fmriprep/workflows/bold/tests/test_base.py +++ b/fmriprep/workflows/bold/tests/test_base.py @@ -35,6 +35,7 @@ def bids_root(tmp_path_factory): @pytest.mark.parametrize("fieldmap_id", ["phasediff", None]) @pytest.mark.parametrize("freesurfer", [False, True]) @pytest.mark.parametrize("level", ["minimal", "resampling", "full"]) +@pytest.mark.parametrize("bold2anat_init", ["t1w", "t2w"]) def test_bold_wf( bids_root: Path, tmp_path: Path, @@ -42,6 +43,7 @@ def test_bold_wf( fieldmap_id: str | None, freesurfer: bool, level: str, + bold2anat_init: str, ): """Test as many combinations of precomputed files and input configurations as possible.""" @@ -65,6 +67,7 @@ def test_bold_wf( img.to_filename(path) with mock_config(bids_dir=bids_root): + config.workflow.bold2anat_init = bold2anat_init config.workflow.level = level config.workflow.run_reconall = freesurfer wf = init_bold_wf( From 91265b0aa83511c875bcf80fe90a4bc71565cc03 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Thu, 25 Jan 2024 08:03:48 -0500 Subject: [PATCH 18/22] Apply suggestions from code review Co-authored-by: Chris Markiewicz --- fmriprep/cli/parser.py | 4 ++-- fmriprep/workflows/base.py | 2 +- fmriprep/workflows/bold/registration.py | 7 ++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py index dcf473d55..1ee8dbd04 100644 --- a/fmriprep/cli/parser.py +++ b/fmriprep/cli/parser.py @@ -349,9 +349,9 @@ def _slice_time_ref(value, parser): "--bold2anat-init", choices=["auto", "t1w", "header"], default="auto", - help="Method of BOLD to anatomical coregistration. If `auto`, a T2w image is used if " + help="Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used if " "available, otherwise the T1w image. `t1w` forces use of the T1w, and `header` uses " - "the T1w header information.", + "the BOLD header information without an initial registration.", ) g_conf.add_argument( "--bold2anat-dof", diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py index acc2951b9..1367edeeb 100644 --- a/fmriprep/workflows/base.py +++ b/fmriprep/workflows/base.py @@ -624,7 +624,7 @@ def init_single_subject_wf(subject_id: str): ) # Before initializing BOLD workflow, select/verify anatomical target for coregistration - if config.workflow.bold2anat_init in ('auto', 't2w'): + if config.workflow.bold2anat_init == 'auto': config.workflow.bold2anat_init = 't2w' if subject_data['t2w'] else 't1w' for bold_series in bold_runs: diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index c73f24dff..a8a64fdeb 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -289,7 +289,7 @@ def init_bbreg_wf( use_t2w = bold2anat_init == 't2w' if use_t2w: workflow.__desc__ += ( - " A T2w reference was used as an intermediate volume to improve tissue contrast." + " The aligned T2w image was used for initial co-registration." ) inputnode = pe.Node( @@ -312,7 +312,7 @@ def init_bbreg_wf( ) if bold2anat_init not in ty.get_args(RegistrationInit): - raise ValueError(f"Unknown BOLD-anat initialization option: {bold2anat_init}") + raise ValueError(f"Unknown BOLD-to-anatomical initialization option: {bold2anat_init}") # For now make BBR unconditional - in the future, we can fall back to identity, # but adding the flexibility without testing seems a bit dangerous @@ -396,9 +396,6 @@ def init_bbreg_wf( (bbregister, transforms, [('out_lta_file', 'in1')]), ]) # fmt:skip - if use_t2w: - workflow.connect(fssource, 'T2', bbregister, 'intermediate_file') - # Short-circuit workflow building, use boundary-based registration if use_bbr is True: outputnode.inputs.fallback = False From d5a282e31730c582de91e103a112f255f3c58719 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Thu, 25 Jan 2024 11:29:04 -0500 Subject: [PATCH 19/22] ENH: Add option to force t2w --- fmriprep/cli/parser.py | 8 ++++---- fmriprep/config.py | 6 +++--- fmriprep/workflows/base.py | 10 +++++++--- fmriprep/workflows/bold/registration.py | 12 ++++++------ 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py index 1ee8dbd04..519fde2f6 100644 --- a/fmriprep/cli/parser.py +++ b/fmriprep/cli/parser.py @@ -347,11 +347,11 @@ def _slice_time_ref(value, parser): ) g_conf.add_argument( "--bold2anat-init", - choices=["auto", "t1w", "header"], + choices=["auto", "t1w", "t2w", "header"], default="auto", - help="Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used if " - "available, otherwise the T1w image. `t1w` forces use of the T1w, and `header` uses " - "the BOLD header information without an initial registration.", + help="Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used " + "if available, otherwise the T1w image. `t1w` forces use of the T1w, `t2w` forces use of " + "the T2w, and `header` uses the BOLD header information without an initial registration.", ) g_conf.add_argument( "--bold2anat-dof", diff --git a/fmriprep/config.py b/fmriprep/config.py index aabea97c7..ad0761f35 100644 --- a/fmriprep/config.py +++ b/fmriprep/config.py @@ -528,9 +528,9 @@ class workflow(_Config): bold2anat_dof = None """Degrees of freedom of the BOLD-to-anatomical registration steps.""" bold2anat_init = "auto" - """Method of BOLD to anatomical coregistration. The target anatomical (``'t1w'``, ``'t2w'``) - can be specified, otherwise ``'auto'`` will prefer a T2w image but fall back to T1w if none - are available. Alternatively, ``'header'`` will use the T1w header information.""" + """Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used + if available, otherwise the T1w image. `t1w` forces use of the T1w, `t2w` forces use of + the T2w, and `header` uses the BOLD header information without an initial registration.""" cifti_output = None """Generate HCP Grayordinates, accepts either ``'91k'`` (default) or ``'170k'``.""" dummy_scans = None diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py index 1367edeeb..a152ce357 100644 --- a/fmriprep/workflows/base.py +++ b/fmriprep/workflows/base.py @@ -624,8 +624,13 @@ def init_single_subject_wf(subject_id: str): ) # Before initializing BOLD workflow, select/verify anatomical target for coregistration - if config.workflow.bold2anat_init == 'auto': - config.workflow.bold2anat_init = 't2w' if subject_data['t2w'] else 't1w' + if config.workflow.bold2anat_init in ('auto', 't2w'): + has_t2w = subject_data['t2w'] or 't2w_preproc' in anatomical_cache + if config.workflow.bold2anat_init == 't2w' and not has_t2w: + raise OSError( + "A T2w image is expected for BOLD-to-anatomical coregistration and was not found" + ) + config.workflow.bold2anat_init = 't2w' if has_t2w else 't1w' for bold_series in bold_runs: bold_file = bold_series[0] @@ -673,7 +678,6 @@ def init_single_subject_wf(subject_id: str): f'outputnode.sphere_reg_{"msm" if msm_sulc else "fsLR"}', 'inputnode.sphere_reg_fsLR', ), - ('outputnode.t2w_preproc', 'inputnode.t2w_preproc'), # Optional ]), ]) # fmt:skip if fieldmap_id: diff --git a/fmriprep/workflows/bold/registration.py b/fmriprep/workflows/bold/registration.py index a8a64fdeb..716ac7a0e 100644 --- a/fmriprep/workflows/bold/registration.py +++ b/fmriprep/workflows/bold/registration.py @@ -288,9 +288,7 @@ def init_bbreg_wf( use_t2w = bold2anat_init == 't2w' if use_t2w: - workflow.__desc__ += ( - " The aligned T2w image was used for initial co-registration." - ) + workflow.__desc__ += " The aligned T2w image was used for initial co-registration." inputnode = pe.Node( niu.IdentityInterface( @@ -356,8 +354,6 @@ def init_bbreg_wf( concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm') workflow.connect([ - (inputnode, fssource, [('subjects_dir', 'subjects_dir'), - ('subject_id', 'subject_id')]), (inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]), # Wire up the co-registration alternatives (transforms, select_transform, [('out', 'inlist')]), @@ -377,7 +373,11 @@ def init_bbreg_wf( ]) # fmt:skip if use_t2w: - workflow.connect(fssource, 'T2', mri_coreg, 'reference_file') + workflow.connect([ + (inputnode, fssource, [('subjects_dir', 'subjects_dir'), + ('subject_id', 'subject_id')]), + (fssource, mri_coreg, [('T2', 'reference_file')]), + ]) # fmt:skip # Short-circuit workflow building, use initial registration if use_bbr is False: From 08dfd68984d30d5d6c77350a313bbe764d6ccd87 Mon Sep 17 00:00:00 2001 From: Mathias Goncalves Date: Thu, 25 Jan 2024 13:13:56 -0500 Subject: [PATCH 20/22] Apply suggestions from code review Co-authored-by: Chris Markiewicz --- fmriprep/workflows/bold/base.py | 2 -- fmriprep/workflows/bold/fit.py | 2 -- fmriprep/workflows/tests/test_base.py | 4 +++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/fmriprep/workflows/bold/base.py b/fmriprep/workflows/bold/base.py index 6c7c66cce..c7289c6d0 100644 --- a/fmriprep/workflows/bold/base.py +++ b/fmriprep/workflows/bold/base.py @@ -209,7 +209,6 @@ def init_bold_wf( "t1w_mask", "t1w_dseg", "t1w_tpms", - "t2w_preproc", # Optional # FreeSurfer outputs "subjects_dir", "subject_id", @@ -261,7 +260,6 @@ def init_bold_wf( ('t1w_preproc', 'inputnode.t1w_preproc'), ('t1w_mask', 'inputnode.t1w_mask'), ('t1w_dseg', 'inputnode.t1w_dseg'), - ('t2w_preproc', 'inputnode.t2w_preproc'), ('subjects_dir', 'inputnode.subjects_dir'), ('subject_id', 'inputnode.subject_id'), ('fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm'), diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py index 8bca33670..a652c2ef1 100644 --- a/fmriprep/workflows/bold/fit.py +++ b/fmriprep/workflows/bold/fit.py @@ -268,8 +268,6 @@ def init_bold_fit_wf( "subjects_dir", "subject_id", "fsnative2t1w_xfm", - # Optional - improvements if available - "t2w_preproc", ], ), name="inputnode", diff --git a/fmriprep/workflows/tests/test_base.py b/fmriprep/workflows/tests/test_base.py index 74ae6633f..05463549c 100644 --- a/fmriprep/workflows/tests/test_base.py +++ b/fmriprep/workflows/tests/test_base.py @@ -105,7 +105,7 @@ def bids_root(tmp_path_factory): def _make_params( - bold2anat_init: str = "t1w", + bold2anat_init: str = "auto", use_bbr: bool | None = None, dummy_scans: int | None = None, me_output_echos: bool = False, @@ -163,6 +163,8 @@ def _make_params( ), [ _make_params(), + _make_params(bold2anat_init="t1w"), + _make_params(bold2anat_init="t2w"), _make_params(bold2anat_init="header"), _make_params(use_bbr=True), _make_params(use_bbr=False), From 6fb9f1767bef16a6a05f2ac6996f49dd7a10a622 Mon Sep 17 00:00:00 2001 From: mathiasg Date: Thu, 25 Jan 2024 16:23:14 -0500 Subject: [PATCH 21/22] TST: Remove test now that functionality has changed --- .circleci/config.yml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5956090df..600db1caf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -647,27 +647,6 @@ jobs: --output-spaces MNI152NLin2009cAsym:res-2 anat func \ --mem-mb 14336 --nthreads 4 -vv --debug compcor - run: *check_outputs - - run: - name: Generate report with one artificial error - command: | - set -x - sudo mv /tmp/${DATASET}/fmriprep/sub-100185.html \ - /tmp/${DATASET}/fmriprep/sub-100185_noerror.html - UUID=$(grep uuid /tmp/${DATASET}/work/*/config.toml | cut -d\" -f 2 | tail -n 1) - mkdir -p /tmp/${DATASET}/fmriprep/sub-100185/log/$UUID/ - cp /tmp/src/fmriprep/fmriprep/data/tests/crash_files/*.txt \ - /tmp/${DATASET}/fmriprep/sub-100185/log/$UUID/ - set +e - fmriprep-docker -i nipreps/fmriprep:latest \ - -e FMRIPREP_DEV 1 --user $(id -u):$(id -g) \ - --config $PWD/nipype.cfg -w /tmp/${DATASET}/work \ - /tmp/data/${DATASET} /tmp/${DATASET}/fmriprep participant \ - --fs-no-reconall --sloppy --write-graph \ - --output-spaces MNI152NLin2009cAsym:res-2 anat func \ - --reports-only --config-file /tmp/${DATASET}/work/${UUID}/config.toml -vv - RET=$? - set -e - [[ "$RET" -eq "0" ]] # ensure report was generated successfully - run: name: Clean working directory when: on_success From e4235189eb0d8f3d62a8f1e7ac29ff8d7ac26247 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 25 Jan 2024 16:31:18 -0500 Subject: [PATCH 22/22] Support lists in bids_filter containing null or * --- fmriprep/cli/parser.py | 21 ++++++++++++++++----- fmriprep/config.py | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py index a62ab7f7f..9e426b771 100644 --- a/fmriprep/cli/parser.py +++ b/fmriprep/cli/parser.py @@ -90,13 +90,24 @@ def _to_gb(value): def _drop_sub(value): return value[4:] if value.startswith("sub-") else value - def _filter_pybids_none_any(dct): + def _process_value(value): import bids - return { - k: bids.layout.Query.NONE if v is None else (bids.layout.Query.ANY if v == "*" else v) - for k, v in dct.items() - } + if value is None: + return bids.layout.Query.NONE + elif value == "*": + return bids.layout.Query.ANY + else: + return value + + def _filter_pybids_none_any(dct): + d = {} + for k, v in dct.items(): + if isinstance(v, list): + d[k] = [_process_value(val) for val in v] + else: + d[k] = _process_value(v) + return d def _bids_filter(value, parser): from json import JSONDecodeError, loads diff --git a/fmriprep/config.py b/fmriprep/config.py index ad0761f35..1dfc7783f 100644 --- a/fmriprep/config.py +++ b/fmriprep/config.py @@ -493,12 +493,21 @@ def init(cls): if cls.bids_filters: from bids.layout import Query + def _process_value(value): + """Convert string with "Query" in it to Query object.""" + if isinstance(value, list): + return [_process_value(val) for val in value] + else: + return ( + getattr(Query, value[7:-4]) + if not isinstance(value, Query) and "Query" in value + else value + ) + # unserialize pybids Query enum values for acq, filters in cls.bids_filters.items(): - cls.bids_filters[acq] = { - k: getattr(Query, v[7:-4]) if not isinstance(v, Query) and "Query" in v else v - for k, v in filters.items() - } + for k, v in filters.items(): + cls.bids_filters[acq][k] = _process_value(v) if "all" in cls.debug: cls.debug = list(DEBUG_MODES)