From db0fda2c90d4f3bdd02ba16e65704a0fa1517df3 Mon Sep 17 00:00:00 2001 From: Salim Mansour Date: Thu, 29 Oct 2020 16:51:51 -0400 Subject: [PATCH 1/9] Ported bbregister workflow from fmriprep --- dmriprep/cli/parser.py | 17 ++ dmriprep/config/__init__.py | 5 + dmriprep/workflows/base.py | 26 ++- dmriprep/workflows/dwi/registration.py | 268 +++++++++++++++++++++++++ 4 files changed, 315 insertions(+), 1 deletion(-) create mode 100755 dmriprep/workflows/dwi/registration.py diff --git a/dmriprep/cli/parser.py b/dmriprep/cli/parser.py index 0d6458b4..52d1684f 100644 --- a/dmriprep/cli/parser.py +++ b/dmriprep/cli/parser.py @@ -219,6 +219,23 @@ def _bids_filter(value): https://www.nipreps.org/dmriprep/en/%s/spaces.html""" % (currentv.base_version if is_release else "latest"), ) + g_conf.add_argument( + "--bold2t1w-init", + action="store", + default="register", + 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.", + ) + g_conf.add_argument( + "--bold2t1w-dof", + action="store", + default=6, + choices=[6, 9, 12], + type=int, + help="Degrees of freedom when registering BOLD to T1w images. " + "6 degrees (rotation and translation) are used by default.", + ) # ANTs options g_ants = parser.add_argument_group("Specific options for ANTs registrations") diff --git a/dmriprep/config/__init__.py b/dmriprep/config/__init__.py index 0f8ca8bb..9df45c35 100644 --- a/dmriprep/config/__init__.py +++ b/dmriprep/config/__init__.py @@ -436,6 +436,11 @@ class workflow(_Config): ignore = None """Ignore particular steps for *dMRIPrep*.""" longitudinal = False + """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" """Run FreeSurfer ``recon-all`` with the ``-logitudinal`` flag.""" run_reconall = True """Run FreeSurfer's surface reconstruction.""" diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index ce6e9cb8..e7f4af7a 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -18,7 +18,7 @@ from ..utils.bids import collect_data from .dwi.base import init_early_b0ref_wf from .fmap.base import init_fmap_estimation_wf - +from .dwi.registration import init_bbreg_wf def init_dmriprep_wf(): """ @@ -354,6 +354,30 @@ def init_single_subject_wf(subject_id): ]) # fmt:on + if config.workflow.run_reconall: + from niworkflows.interfaces.nibabel import ApplyMask + + # Mask the T1 + t1w_brain = pe.Node(ApplyMask(), name='t1w_brain') + + bbr_wf = init_bbreg_wf(use_bbr=True, bold2t1w_dof=config.workflow.bold2t1w_dof, + bold2t1w_init=config.workflow.bold2t1w_init, omp_nthreads=config.nipype.omp_nthreads) + + workflow.connect([ + # T1 Mask + (anat_preproc_wf, t1w_brain, [('outputnode.t1w_preproc', 'in_file'), + ('outputnode.t1w_mask', 'in_mask')]), + # BBregister + (split_info, bbr_wf, [('dwi_file', 'inputnode.in_file')]), + (t1w_brain, bbr_wf, [('out_file', 'inputnode.t1w_brain')]), + (anat_preproc_wf, bbr_wf, [('outputnode.t1w_dseg', 'inputnode.t1w_dseg')]), + (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), + (bids_info, bbr_wf, [('subject', 'inputnode.subject_id')]), + (anat_preproc_wf, bbr_wf, [ + ('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm') + ]) + ]) + fmap_estimation_wf = init_fmap_estimation_wf( subject_data["dwi"], debug=config.execution.debug ) diff --git a/dmriprep/workflows/dwi/registration.py b/dmriprep/workflows/dwi/registration.py new file mode 100755 index 00000000..26308f0f --- /dev/null +++ b/dmriprep/workflows/dwi/registration.py @@ -0,0 +1,268 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +""" +Registration workflows +++++++++++++++++++++++ + +.. autofunction:: init_bbreg_wf + +""" +from ... import config + +import os +import os.path as op + +import pkg_resources as pkgr + +from nipype.pipeline import engine as pe +from nipype.interfaces import utility as niu, fsl, c3 + +from ...interfaces import DerivativesDataSink + +DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB +LOGGER = config.loggers.workflow + +def init_bbreg_wf(use_bbr, bold2t1w_dof, bold2t1w_init, omp_nthreads, name='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. + + 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. + The ``use_bbr`` option permits a high degree of control over registration. + If ``False``, standard, affine coregistration will be performed using + FreeSurfer's ``mri_coreg`` tool. + If ``True``, ``bbregister`` will be seeded with the initial transform found + by ``mri_coreg`` (equivalent to running ``bbregister --init-coreg``). + If ``None``, after ``bbregister`` is run, the resulting affine transform + will be compared to the initial transform found by ``mri_coreg``. + Excessive deviation will result in rejecting the BBR refinement and + accepting the original, affine registration. + + Workflow Graph + .. workflow :: + :graph2use: orig + :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) + + + Parameters + ---------- + 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' + If ``'header'``, use header information for initialization of BOLD and T1 images. + If ``'register'``, align volumes by their centers. + name : :obj:`str`, optional + Workflow name (default: bbreg_wf) + + Inputs + ------ + in_file + Reference BOLD image to be registered + fsnative2t1w_xfm + FSL-style affine matrix translating from FreeSurfer T1.mgz to T1w + subjects_dir + FreeSurfer SUBJECTS_DIR + subject_id + FreeSurfer subject ID (must have folder in SUBJECTS_DIR) + t1w_brain + 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`) + + Outputs + ------- + itk_bold_to_t1 + Affine transform from ``ref_bold_brain`` to T1 space (ITK format) + itk_t1_to_bold + Affine transform from T1 space to BOLD space (ITK format) + out_report + Reportlet for assessing registration quality + fallback + Boolean indicating whether BBR was rejected (mri_coreg registration returned) + + """ + from niworkflows.engine.workflows import LiterateWorkflow as Workflow + # See https://github.com/nipreps/fmriprep/issues/768 + from niworkflows.interfaces.freesurfer import ( + PatchedBBRegisterRPT as BBRegisterRPT, + PatchedMRICoregRPT as MRICoregRPT, + PatchedLTAConvert as LTAConvert + ) + from niworkflows.interfaces.nitransforms import ConcatenateXFMs + + workflow = Workflow(name=name) + workflow.__desc__ = """\ +The BOLD reference was then co-registered to the T1w reference using +`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], + reason='' if bold2t1w_dof == 6 else + 'to account for distortions remaining in the BOLD reference') + + inputnode = pe.Node( + niu.IdentityInterface([ + 'in_file', + 'fsnative2t1w_xfm', 'subjects_dir', 'subject_id', # BBRegister + 't1w_dseg', 't1w_brain']), # FLIRT BBR + name='inputnode') + outputnode = pe.Node( + niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'out_report', 'fallback']), + name='outputnode') + + if bold2t1w_init not in ("register", "header"): + raise ValueError(f"Unknown BOLD-T1w initialization option: {bold2t1w_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 use_bbr is False: + raise ValueError("Cannot disable BBR and use header registration") + if use_bbr is None: + LOGGER.warning("Initializing BBR with header; affine fallback disabled") + use_bbr = True + + 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([ + # Output ITK transforms + (inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]), + (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')]), + ]) + + # Define both nodes, but only connect conditionally + mri_coreg = pe.Node( + MRICoregRPT(dof=bold2t1w_dof, sep=[4], ftol=0.0001, linmintol=0.01, + generate_report=not use_bbr), + name='mri_coreg', n_procs=omp_nthreads, mem_gb=5) + + bbregister = pe.Node( + BBRegisterRPT(dof=bold2t1w_dof, contrast_type='t1', registered_file=True, + out_lta_file=True, generate_report=True), + name='bbregister', mem_gb=12) + + # Use mri_coreg + if bold2t1w_init == "register": + workflow.connect([ + (inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'), + ('subject_id', 'subject_id'), + ('in_file', 'source_file')]), + ]) + + # Short-circuit workflow building, use initial registration + if use_bbr is False: + workflow.connect([ + (mri_coreg, outputnode, [('out_report', 'out_report')]), + (mri_coreg, merge_ltas, [('out_lta_file', 'in1')])]) + outputnode.inputs.fallback = True + + return workflow + + # Use bbregister + workflow.connect([ + (inputnode, bbregister, [('subjects_dir', 'subjects_dir'), + ('subject_id', 'subject_id'), + ('in_file', 'source_file')]), + ]) + + if bold2t1w_init == "header": + bbregister.inputs.init = "header" + else: + workflow.connect([(mri_coreg, bbregister, [('out_lta_file', 'init_reg_file')])]) + + # Short-circuit workflow building, use boundary-based registration + if use_bbr is True: + workflow.connect([ + (bbregister, outputnode, [('out_report', 'out_report')]), + (bbregister, merge_ltas, [('out_lta_file', 'in1')])]) + outputnode.inputs.fallback = False + + return workflow + + # Only reach this point if bold2t1w_init is "register" and use_bbr is None + + transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') + reports = pe.Node(niu.Merge(2), run_without_submitting=True, name='reports') + + lta_ras2ras = pe.MapNode(LTAConvert(out_lta=True), iterfield=['in_lta'], + name='lta_ras2ras', mem_gb=2) + compare_transforms = pe.Node(niu.Function(function=compare_xforms), name='compare_transforms') + + select_transform = pe.Node(niu.Select(), run_without_submitting=True, name='select_transform') + select_report = pe.Node(niu.Select(), run_without_submitting=True, name='select_report') + + workflow.connect([ + (bbregister, transforms, [('out_lta_file', 'in1')]), + (mri_coreg, transforms, [('out_lta_file', 'in2')]), + # Normalize LTA transforms to RAS2RAS (inputs are VOX2VOX) and compare + (transforms, lta_ras2ras, [('out', 'in_lta')]), + (lta_ras2ras, compare_transforms, [('out_lta', 'lta_list')]), + (compare_transforms, outputnode, [('out', 'fallback')]), + # Select output transform + (transforms, select_transform, [('out', 'inlist')]), + (compare_transforms, select_transform, [('out', 'index')]), + (select_transform, merge_ltas, [('out', 'in1')]), + # Select output report + (bbregister, reports, [('out_report', 'in1')]), + (mri_coreg, reports, [('out_report', 'in2')]), + (reports, select_report, [('out', 'inlist')]), + (compare_transforms, select_report, [('out', 'index')]), + (select_report, outputnode, [('out', 'out_report')]), + ]) + + return workflow + + +def compare_xforms(lta_list, norm_threshold=15): + """ + Computes a normalized displacement between two affine transforms as the + maximum overall displacement of the midpoints of the faces of a cube, when + each transform is applied to the cube. + This combines displacement resulting from scaling, translation and rotation. + + Although the norm is in mm, in a scaling context, it is not necessarily + equivalent to that distance in translation. + + We choose a default threshold of 15mm as a rough heuristic. + Normalized displacement above 20mm showed clear signs of distortion, while + "good" BBR refinements were frequently below 10mm displaced from the rigid + transform. + The 10-20mm range was more ambiguous, and 15mm chosen as a compromise. + This is open to revisiting in either direction. + + See discussion in + `GitHub issue #681`_ `_ + and the `underlying implementation + `_. + + Parameters + ---------- + + lta_list : :obj:`list` or :obj:`tuple` of :obj:`str` + the two given affines in LTA format + norm_threshold : :obj:`float` + the upper bound limit to the normalized displacement caused by the + second transform relative to the first (default: `15`) + + """ + from niworkflows.interfaces.surf import load_transform + from nipype.algorithms.rapidart import _calc_norm_affine + + bbr_affine = load_transform(lta_list[0]) + fallback_affine = load_transform(lta_list[1]) + + norm, _ = _calc_norm_affine([fallback_affine, bbr_affine], use_differences=True) + + return norm[1] > norm_threshold \ No newline at end of file From 41d2b697a3d723a6ce368ab04952ecc597e3db10 Mon Sep 17 00:00:00 2001 From: slimnsour <54225067+slimnsour@users.noreply.github.com> Date: Thu, 3 Dec 2020 11:00:43 -0500 Subject: [PATCH 2/9] Apply suggestions from @oesteban Co-authored-by: Oscar Esteban --- dmriprep/cli/parser.py | 9 --------- dmriprep/config/__init__.py | 6 ++---- dmriprep/workflows/base.py | 9 ++++++--- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/dmriprep/cli/parser.py b/dmriprep/cli/parser.py index 52d1684f..9b36d7e8 100644 --- a/dmriprep/cli/parser.py +++ b/dmriprep/cli/parser.py @@ -227,15 +227,6 @@ def _bids_filter(value): help='Either "register" (the default) to initialize volumes at center or "header"' " to use the header information when coregistering BOLD to T1w images.", ) - g_conf.add_argument( - "--bold2t1w-dof", - action="store", - default=6, - choices=[6, 9, 12], - type=int, - help="Degrees of freedom when registering BOLD to T1w images. " - "6 degrees (rotation and translation) are used by default.", - ) # ANTs options g_ants = parser.add_argument_group("Specific options for ANTs registrations") diff --git a/dmriprep/config/__init__.py b/dmriprep/config/__init__.py index 9df45c35..658b1f66 100644 --- a/dmriprep/config/__init__.py +++ b/dmriprep/config/__init__.py @@ -436,11 +436,9 @@ class workflow(_Config): ignore = None """Ignore particular steps for *dMRIPrep*.""" longitudinal = False - """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').""" """Run FreeSurfer ``recon-all`` with the ``-logitudinal`` flag.""" run_reconall = True """Run FreeSurfer's surface reconstruction.""" diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index e7f4af7a..88ca17d8 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -360,13 +360,16 @@ def init_single_subject_wf(subject_id): # Mask the T1 t1w_brain = pe.Node(ApplyMask(), name='t1w_brain') - bbr_wf = init_bbreg_wf(use_bbr=True, bold2t1w_dof=config.workflow.bold2t1w_dof, - bold2t1w_init=config.workflow.bold2t1w_init, omp_nthreads=config.nipype.omp_nthreads) + bbr_wf = init_bbreg_wf( + bold2t1w_init=config.workflow.bold2t1w_init, + omp_nthreads=config.nipype.omp_nthreads, + use_bbr=True, + ) workflow.connect([ # T1 Mask (anat_preproc_wf, t1w_brain, [('outputnode.t1w_preproc', 'in_file'), - ('outputnode.t1w_mask', 'in_mask')]), + ('outputnode.t1w_mask', 'in_mask')]), # BBregister (split_info, bbr_wf, [('dwi_file', 'inputnode.in_file')]), (t1w_brain, bbr_wf, [('out_file', 'inputnode.t1w_brain')]), From aeaf24bdf6be5a19b615ba8b3a4e4e237455025b Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 10:35:54 +0100 Subject: [PATCH 3/9] Apply suggestions from code review --- dmriprep/workflows/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index 88ca17d8..3b2729f4 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -357,7 +357,7 @@ def init_single_subject_wf(subject_id): if config.workflow.run_reconall: from niworkflows.interfaces.nibabel import ApplyMask - # Mask the T1 + # Mask the T1w t1w_brain = pe.Node(ApplyMask(), name='t1w_brain') bbr_wf = init_bbreg_wf( @@ -367,10 +367,10 @@ def init_single_subject_wf(subject_id): ) workflow.connect([ - # T1 Mask + # T1w Mask (anat_preproc_wf, t1w_brain, [('outputnode.t1w_preproc', 'in_file'), ('outputnode.t1w_mask', 'in_mask')]), - # BBregister + # BBRegister (split_info, bbr_wf, [('dwi_file', 'inputnode.in_file')]), (t1w_brain, bbr_wf, [('out_file', 'inputnode.t1w_brain')]), (anat_preproc_wf, bbr_wf, [('outputnode.t1w_dseg', 'inputnode.t1w_dseg')]), From b890160fcec2c6dd11bdc226f9278d99b49dcaf2 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 16:20:33 +0100 Subject: [PATCH 4/9] enh: remove 'bold' appearances, use wf from fmriprep --- dmriprep/cli/parser.py | 6 +- dmriprep/config/__init__.py | 6 +- dmriprep/workflows/base.py | 51 +++-- dmriprep/workflows/dwi/registration.py | 268 ------------------------- setup.cfg | 1 + 5 files changed, 40 insertions(+), 292 deletions(-) delete mode 100755 dmriprep/workflows/dwi/registration.py diff --git a/dmriprep/cli/parser.py b/dmriprep/cli/parser.py index 9b36d7e8..96acce0e 100644 --- a/dmriprep/cli/parser.py +++ b/dmriprep/cli/parser.py @@ -191,7 +191,7 @@ def _bids_filter(value): action="store", nargs="+", default=[], - choices=["fieldmaps", "slicetiming", "sbref"], + choices=["fieldmaps", "sbref"], help="ignore selected aspects of the input dataset to disable corresponding " "parts of the workflow (a space delimited list)", ) @@ -220,12 +220,12 @@ def _bids_filter(value): % (currentv.base_version if is_release else "latest"), ) g_conf.add_argument( - "--bold2t1w-init", + "--dwi2t1w-init", action="store", default="register", 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.", + " to use the header information when coregistering DWI to T1w images.", ) # ANTs options diff --git a/dmriprep/config/__init__.py b/dmriprep/config/__init__.py index 658b1f66..45ccdaa4 100644 --- a/dmriprep/config/__init__.py +++ b/dmriprep/config/__init__.py @@ -425,6 +425,9 @@ class workflow(_Config): anat_only = False """Execute the anatomical preprocessing only.""" + dwi2t1w_init = "register" + """Whether to use standard coregistration ('register') or to initialize coregistration from the + DWI header ('header').""" fmap_bspline = None """Regularize fieldmaps with a field of B-Spline basis.""" fmap_demean = None @@ -436,9 +439,6 @@ class workflow(_Config): ignore = None """Ignore particular steps for *dMRIPrep*.""" longitudinal = False - bold2t1w_init = "register" - """Whether to use standard coregistration ('register') or to initialize coregistration from the - BOLD image-header ('header').""" """Run FreeSurfer ``recon-all`` with the ``-logitudinal`` flag.""" run_reconall = True """Run FreeSurfer's surface reconstruction.""" diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index 3b2729f4..10e00a87 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -12,13 +12,14 @@ from niworkflows.utils.misc import fix_multi_T1w_source_name from niworkflows.utils.spaces import Reference from smriprep.workflows.anatomical import init_anat_preproc_wf +from fmriprep.workflows.bold.registration import init_bbreg_wf from ..interfaces import DerivativesDataSink, BIDSDataGrabber from ..interfaces.reports import SubjectSummary, AboutSummary from ..utils.bids import collect_data from .dwi.base import init_early_b0ref_wf from .fmap.base import init_fmap_estimation_wf -from .dwi.registration import init_bbreg_wf + def init_dmriprep_wf(): """ @@ -287,7 +288,7 @@ def init_single_subject_wf(subject_id): return workflow # Append the dMRI section to the existing anatomical excerpt - # That way we do not need to stream down the number of bold datasets + # That way we do not need to stream down the number of DWI datasets anat_preproc_wf.__postdesc__ = ( (anat_preproc_wf.__postdesc__ or "") + f""" @@ -358,28 +359,42 @@ def init_single_subject_wf(subject_id): from niworkflows.interfaces.nibabel import ApplyMask # Mask the T1w - t1w_brain = pe.Node(ApplyMask(), name='t1w_brain') + t1w_brain = pe.Node(ApplyMask(), name="t1w_brain") bbr_wf = init_bbreg_wf( - bold2t1w_init=config.workflow.bold2t1w_init, + bold2t1w_init=config.workflow.dwi2t1w_init, omp_nthreads=config.nipype.omp_nthreads, use_bbr=True, ) - workflow.connect([ - # T1w Mask - (anat_preproc_wf, t1w_brain, [('outputnode.t1w_preproc', 'in_file'), - ('outputnode.t1w_mask', 'in_mask')]), - # BBRegister - (split_info, bbr_wf, [('dwi_file', 'inputnode.in_file')]), - (t1w_brain, bbr_wf, [('out_file', 'inputnode.t1w_brain')]), - (anat_preproc_wf, bbr_wf, [('outputnode.t1w_dseg', 'inputnode.t1w_dseg')]), - (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), - (bids_info, bbr_wf, [('subject', 'inputnode.subject_id')]), - (anat_preproc_wf, bbr_wf, [ - ('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm') - ]) - ]) + workflow.connect( + [ + # T1w Mask + ( + anat_preproc_wf, + t1w_brain, + [ + ("outputnode.t1w_preproc", "in_file"), + ("outputnode.t1w_mask", "in_mask"), + ], + ), + # BBRegister + (split_info, bbr_wf, [("dwi_file", "inputnode.in_file")]), + (t1w_brain, bbr_wf, [("out_file", "inputnode.t1w_brain")]), + ( + anat_preproc_wf, + bbr_wf, + [("outputnode.t1w_dseg", "inputnode.t1w_dseg")], + ), + (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), + (bids_info, bbr_wf, [("subject", "inputnode.subject_id")]), + ( + anat_preproc_wf, + bbr_wf, + [("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm")], + ), + ] + ) fmap_estimation_wf = init_fmap_estimation_wf( subject_data["dwi"], debug=config.execution.debug diff --git a/dmriprep/workflows/dwi/registration.py b/dmriprep/workflows/dwi/registration.py deleted file mode 100755 index 26308f0f..00000000 --- a/dmriprep/workflows/dwi/registration.py +++ /dev/null @@ -1,268 +0,0 @@ -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -""" -Registration workflows -++++++++++++++++++++++ - -.. autofunction:: init_bbreg_wf - -""" -from ... import config - -import os -import os.path as op - -import pkg_resources as pkgr - -from nipype.pipeline import engine as pe -from nipype.interfaces import utility as niu, fsl, c3 - -from ...interfaces import DerivativesDataSink - -DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB -LOGGER = config.loggers.workflow - -def init_bbreg_wf(use_bbr, bold2t1w_dof, bold2t1w_init, omp_nthreads, name='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. - - 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. - The ``use_bbr`` option permits a high degree of control over registration. - If ``False``, standard, affine coregistration will be performed using - FreeSurfer's ``mri_coreg`` tool. - If ``True``, ``bbregister`` will be seeded with the initial transform found - by ``mri_coreg`` (equivalent to running ``bbregister --init-coreg``). - If ``None``, after ``bbregister`` is run, the resulting affine transform - will be compared to the initial transform found by ``mri_coreg``. - Excessive deviation will result in rejecting the BBR refinement and - accepting the original, affine registration. - - Workflow Graph - .. workflow :: - :graph2use: orig - :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) - - - Parameters - ---------- - 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' - If ``'header'``, use header information for initialization of BOLD and T1 images. - If ``'register'``, align volumes by their centers. - name : :obj:`str`, optional - Workflow name (default: bbreg_wf) - - Inputs - ------ - in_file - Reference BOLD image to be registered - fsnative2t1w_xfm - FSL-style affine matrix translating from FreeSurfer T1.mgz to T1w - subjects_dir - FreeSurfer SUBJECTS_DIR - subject_id - FreeSurfer subject ID (must have folder in SUBJECTS_DIR) - t1w_brain - 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`) - - Outputs - ------- - itk_bold_to_t1 - Affine transform from ``ref_bold_brain`` to T1 space (ITK format) - itk_t1_to_bold - Affine transform from T1 space to BOLD space (ITK format) - out_report - Reportlet for assessing registration quality - fallback - Boolean indicating whether BBR was rejected (mri_coreg registration returned) - - """ - from niworkflows.engine.workflows import LiterateWorkflow as Workflow - # See https://github.com/nipreps/fmriprep/issues/768 - from niworkflows.interfaces.freesurfer import ( - PatchedBBRegisterRPT as BBRegisterRPT, - PatchedMRICoregRPT as MRICoregRPT, - PatchedLTAConvert as LTAConvert - ) - from niworkflows.interfaces.nitransforms import ConcatenateXFMs - - workflow = Workflow(name=name) - workflow.__desc__ = """\ -The BOLD reference was then co-registered to the T1w reference using -`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], - reason='' if bold2t1w_dof == 6 else - 'to account for distortions remaining in the BOLD reference') - - inputnode = pe.Node( - niu.IdentityInterface([ - 'in_file', - 'fsnative2t1w_xfm', 'subjects_dir', 'subject_id', # BBRegister - 't1w_dseg', 't1w_brain']), # FLIRT BBR - name='inputnode') - outputnode = pe.Node( - niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'out_report', 'fallback']), - name='outputnode') - - if bold2t1w_init not in ("register", "header"): - raise ValueError(f"Unknown BOLD-T1w initialization option: {bold2t1w_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 use_bbr is False: - raise ValueError("Cannot disable BBR and use header registration") - if use_bbr is None: - LOGGER.warning("Initializing BBR with header; affine fallback disabled") - use_bbr = True - - 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([ - # Output ITK transforms - (inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]), - (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')]), - ]) - - # Define both nodes, but only connect conditionally - mri_coreg = pe.Node( - MRICoregRPT(dof=bold2t1w_dof, sep=[4], ftol=0.0001, linmintol=0.01, - generate_report=not use_bbr), - name='mri_coreg', n_procs=omp_nthreads, mem_gb=5) - - bbregister = pe.Node( - BBRegisterRPT(dof=bold2t1w_dof, contrast_type='t1', registered_file=True, - out_lta_file=True, generate_report=True), - name='bbregister', mem_gb=12) - - # Use mri_coreg - if bold2t1w_init == "register": - workflow.connect([ - (inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'), - ('subject_id', 'subject_id'), - ('in_file', 'source_file')]), - ]) - - # Short-circuit workflow building, use initial registration - if use_bbr is False: - workflow.connect([ - (mri_coreg, outputnode, [('out_report', 'out_report')]), - (mri_coreg, merge_ltas, [('out_lta_file', 'in1')])]) - outputnode.inputs.fallback = True - - return workflow - - # Use bbregister - workflow.connect([ - (inputnode, bbregister, [('subjects_dir', 'subjects_dir'), - ('subject_id', 'subject_id'), - ('in_file', 'source_file')]), - ]) - - if bold2t1w_init == "header": - bbregister.inputs.init = "header" - else: - workflow.connect([(mri_coreg, bbregister, [('out_lta_file', 'init_reg_file')])]) - - # Short-circuit workflow building, use boundary-based registration - if use_bbr is True: - workflow.connect([ - (bbregister, outputnode, [('out_report', 'out_report')]), - (bbregister, merge_ltas, [('out_lta_file', 'in1')])]) - outputnode.inputs.fallback = False - - return workflow - - # Only reach this point if bold2t1w_init is "register" and use_bbr is None - - transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms') - reports = pe.Node(niu.Merge(2), run_without_submitting=True, name='reports') - - lta_ras2ras = pe.MapNode(LTAConvert(out_lta=True), iterfield=['in_lta'], - name='lta_ras2ras', mem_gb=2) - compare_transforms = pe.Node(niu.Function(function=compare_xforms), name='compare_transforms') - - select_transform = pe.Node(niu.Select(), run_without_submitting=True, name='select_transform') - select_report = pe.Node(niu.Select(), run_without_submitting=True, name='select_report') - - workflow.connect([ - (bbregister, transforms, [('out_lta_file', 'in1')]), - (mri_coreg, transforms, [('out_lta_file', 'in2')]), - # Normalize LTA transforms to RAS2RAS (inputs are VOX2VOX) and compare - (transforms, lta_ras2ras, [('out', 'in_lta')]), - (lta_ras2ras, compare_transforms, [('out_lta', 'lta_list')]), - (compare_transforms, outputnode, [('out', 'fallback')]), - # Select output transform - (transforms, select_transform, [('out', 'inlist')]), - (compare_transforms, select_transform, [('out', 'index')]), - (select_transform, merge_ltas, [('out', 'in1')]), - # Select output report - (bbregister, reports, [('out_report', 'in1')]), - (mri_coreg, reports, [('out_report', 'in2')]), - (reports, select_report, [('out', 'inlist')]), - (compare_transforms, select_report, [('out', 'index')]), - (select_report, outputnode, [('out', 'out_report')]), - ]) - - return workflow - - -def compare_xforms(lta_list, norm_threshold=15): - """ - Computes a normalized displacement between two affine transforms as the - maximum overall displacement of the midpoints of the faces of a cube, when - each transform is applied to the cube. - This combines displacement resulting from scaling, translation and rotation. - - Although the norm is in mm, in a scaling context, it is not necessarily - equivalent to that distance in translation. - - We choose a default threshold of 15mm as a rough heuristic. - Normalized displacement above 20mm showed clear signs of distortion, while - "good" BBR refinements were frequently below 10mm displaced from the rigid - transform. - The 10-20mm range was more ambiguous, and 15mm chosen as a compromise. - This is open to revisiting in either direction. - - See discussion in - `GitHub issue #681`_ `_ - and the `underlying implementation - `_. - - Parameters - ---------- - - lta_list : :obj:`list` or :obj:`tuple` of :obj:`str` - the two given affines in LTA format - norm_threshold : :obj:`float` - the upper bound limit to the normalized displacement caused by the - second transform relative to the first (default: `15`) - - """ - from niworkflows.interfaces.surf import load_transform - from nipype.algorithms.rapidart import _calc_norm_affine - - bbr_affine = load_transform(lta_list[0]) - fallback_affine = load_transform(lta_list[1]) - - norm, _ = _calc_norm_affine([fallback_affine, bbr_affine], use_differences=True) - - return norm[1] > norm_threshold \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index aee34daf..2d9d56e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ install_requires = smriprep ~= 0.7.0 templateflow ~= 0.6 toml + fmriprep ~= 20.2 setup_requires = setuptools >= 40.8.0 test_requires = From 6b4e72a6a95363585d6966329375b85c048f5940 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 16:33:16 +0100 Subject: [PATCH 5/9] docs: fmriprep is necessary to build docs --- dmriprep/workflows/base.py | 46 +++++++++++++++----------------------- docs/requirements.txt | 1 + 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index 10e00a87..cef6e3a6 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -367,34 +367,24 @@ def init_single_subject_wf(subject_id): use_bbr=True, ) - workflow.connect( - [ - # T1w Mask - ( - anat_preproc_wf, - t1w_brain, - [ - ("outputnode.t1w_preproc", "in_file"), - ("outputnode.t1w_mask", "in_mask"), - ], - ), - # BBRegister - (split_info, bbr_wf, [("dwi_file", "inputnode.in_file")]), - (t1w_brain, bbr_wf, [("out_file", "inputnode.t1w_brain")]), - ( - anat_preproc_wf, - bbr_wf, - [("outputnode.t1w_dseg", "inputnode.t1w_dseg")], - ), - (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), - (bids_info, bbr_wf, [("subject", "inputnode.subject_id")]), - ( - anat_preproc_wf, - bbr_wf, - [("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm")], - ), - ] - ) + # fmt:off + workflow.connect([ + # T1w Mask + (anat_preproc_wf, t1w_brain, [ + ("outputnode.t1w_preproc", "in_file"), + ("outputnode.t1w_mask", "in_mask"), + ]), + # BBRegister + (split_info, bbr_wf, [("dwi_file", "inputnode.in_file")]), + (t1w_brain, bbr_wf, [("out_file", "inputnode.t1w_brain")]), + (anat_preproc_wf, bbr_wf, [("outputnode.t1w_dseg", "inputnode.t1w_dseg")]), + (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), + (bids_info, bbr_wf, [("subject", "inputnode.subject_id")]), + (anat_preproc_wf, bbr_wf, [ + ("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm") + ]), + ]) + # fmt:on fmap_estimation_wf = init_fmap_estimation_wf( subject_data["dwi"], debug=config.execution.debug diff --git a/docs/requirements.txt b/docs/requirements.txt index beb81f5e..3604c366 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,6 @@ git+https://github.com/AleksandarPetrov/napoleon.git@0dc3f28a309ad602be5f44a9049785a1026451b3#egg=sphinxcontrib-napoleon git+https://github.com/rwblair/sphinxcontrib-versioning.git@39b40b0b84bf872fc398feff05344051bbce0f63#egg=sphinxcontrib-versioning +fmriprep nbsphinx nipype ~= 1.4 git+https://github.com/nipreps/niworkflows.git@master#egg=niworkflows From 55adb116764ea84ec74b6e128578d49ac3d2332b Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 16:43:25 +0100 Subject: [PATCH 6/9] enh: finalize with plotting a report --- dmriprep/config/reports-spec.yml | 17 +++++++++++++++++ dmriprep/workflows/base.py | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/dmriprep/config/reports-spec.yml b/dmriprep/config/reports-spec.yml index 8beea1db..07072d49 100644 --- a/dmriprep/config/reports-spec.yml +++ b/dmriprep/config/reports-spec.yml @@ -49,6 +49,23 @@ sections: all b=0 found in the dataset, after accounting for signal drift. The red contour shows the brain mask calculated using this reference b=0. subtitle: Reference b=0 and brain mask + - bids: {datatype: figures, desc: coreg, suffix: dwi} + caption: Diffusion-weighted data and anatomical data (EPI-space and T1w-space) + were aligned with mri_coreg (FreeSurfer). + WARNING - bbregister refinement rejected. + descriptions: Note that nearest-neighbor interpolation is used in this reportlet + in order to highlight potential slice Inhomogeneities and other artifacts, whereas + the final images are resampled using cubic B-Spline interpolation. + static: false + subtitle: Alignment of functional and anatomical MRI data (volume based) + - bids: {datatype: figures, desc: bbregister, suffix: dwi} + caption: Diffusion-weighted data and anatomical data (EPI-space and T1w-space) + were aligned with bbregister (FreeSurfer). + descriptions: Note that nearest-neighbor interpolation is used in this reportlet + in order to highlight potential slice Inhomogeneities and other artifacts, whereas + the final images are resampled using cubic B-Spline interpolation. + static: false + subtitle: Alignment of functional and anatomical MRI data (surface driven) - name: About reportlets: - bids: {datatype: figures, desc: about, suffix: T1w} diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index cef6e3a6..f8c5dddd 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -362,11 +362,21 @@ def init_single_subject_wf(subject_id): t1w_brain = pe.Node(ApplyMask(), name="t1w_brain") bbr_wf = init_bbreg_wf( + bold2t1w_dof=6, bold2t1w_init=config.workflow.dwi2t1w_init, omp_nthreads=config.nipype.omp_nthreads, use_bbr=True, ) + ds_report_reg = pe.Node( + DerivativesDataSink(base_directory=str(output_dir), datatype="figures",), + name="ds_report_reg", + run_without_submitting=True, + ) + + def _bold_reg_suffix(fallback): + return "coreg" if fallback else "bbregister" + # fmt:off workflow.connect([ # T1w Mask @@ -383,6 +393,9 @@ def init_single_subject_wf(subject_id): (anat_preproc_wf, bbr_wf, [ ("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm") ]), + (bbr_wf, ds_report_reg, [ + ('outputnode.out_report', 'in_file'), + (('outputnode.fallback', _bold_reg_suffix), 'desc')]), ]) # fmt:on From d24fa855c160eebcc8bd91926725c962bb40f557 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 17:05:22 +0100 Subject: [PATCH 7/9] maint: add @slimnsour to developers registry --- .maint/developers.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.maint/developers.json b/.maint/developers.json index 5529971b..c19f031d 100644 --- a/.maint/developers.json +++ b/.maint/developers.json @@ -22,6 +22,11 @@ "name": "Lerma-Usabiaga, Garikoitz", "orcid": "0000-0001-9800-4816" }, + { + "affiliation": "The Centre for Addiction and Mental Health", + "name": "Mansour, Salim", + "orcid": "0000-0002-1092-1650" + }, { "affiliation": "Department of Psychology, University of Texas at Austin, TX, USA", "name": "Pisner, Derek", From bf3299520c4027eb6b61a9097f73e5918aae989e Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 17:43:37 +0100 Subject: [PATCH 8/9] fix: input to bbr should be a *b=0* reference --- dmriprep/workflows/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index f8c5dddd..fbe15106 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -385,7 +385,9 @@ def _bold_reg_suffix(fallback): ("outputnode.t1w_mask", "in_mask"), ]), # BBRegister - (split_info, bbr_wf, [("dwi_file", "inputnode.in_file")]), + (early_b0ref_wf, bbr_wf, [ + ("outputnode.dwi_reference", "inputnode.in_file") + ]), (t1w_brain, bbr_wf, [("out_file", "inputnode.t1w_brain")]), (anat_preproc_wf, bbr_wf, [("outputnode.t1w_dseg", "inputnode.t1w_dseg")]), (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), From bc5b1af22b714c63c0b376065f41650d66e9876b Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Fri, 4 Dec 2020 09:24:55 -0800 Subject: [PATCH 9/9] fix: subject id lacking prefix --- dmriprep/config/reports-spec.yml | 4 ++-- dmriprep/workflows/base.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dmriprep/config/reports-spec.yml b/dmriprep/config/reports-spec.yml index 07072d49..fd188eeb 100644 --- a/dmriprep/config/reports-spec.yml +++ b/dmriprep/config/reports-spec.yml @@ -53,7 +53,7 @@ sections: caption: Diffusion-weighted data and anatomical data (EPI-space and T1w-space) were aligned with mri_coreg (FreeSurfer). WARNING - bbregister refinement rejected. - descriptions: Note that nearest-neighbor interpolation is used in this reportlet + description: Note that nearest-neighbor interpolation is used in this reportlet in order to highlight potential slice Inhomogeneities and other artifacts, whereas the final images are resampled using cubic B-Spline interpolation. static: false @@ -61,7 +61,7 @@ sections: - bids: {datatype: figures, desc: bbregister, suffix: dwi} caption: Diffusion-weighted data and anatomical data (EPI-space and T1w-space) were aligned with bbregister (FreeSurfer). - descriptions: Note that nearest-neighbor interpolation is used in this reportlet + description: Note that nearest-neighbor interpolation is used in this reportlet in order to highlight potential slice Inhomogeneities and other artifacts, whereas the final images are resampled using cubic B-Spline interpolation. static: false diff --git a/dmriprep/workflows/base.py b/dmriprep/workflows/base.py index fbe15106..4df5de88 100755 --- a/dmriprep/workflows/base.py +++ b/dmriprep/workflows/base.py @@ -391,10 +391,11 @@ def _bold_reg_suffix(fallback): (t1w_brain, bbr_wf, [("out_file", "inputnode.t1w_brain")]), (anat_preproc_wf, bbr_wf, [("outputnode.t1w_dseg", "inputnode.t1w_dseg")]), (fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]), - (bids_info, bbr_wf, [("subject", "inputnode.subject_id")]), + (bids_info, bbr_wf, [(("subject", _prefix), "inputnode.subject_id")]), (anat_preproc_wf, bbr_wf, [ ("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm") ]), + (split_info, ds_report_reg, [("dwi_file", "source_file")]), (bbr_wf, ds_report_reg, [ ('outputnode.out_report', 'in_file'), (('outputnode.fallback', _bold_reg_suffix), 'desc')]),