Skip to content

Commit db0fda2

Browse files
slimnsouroesteban
authored andcommitted
Ported bbregister workflow from fmriprep
1 parent 7002869 commit db0fda2

File tree

4 files changed

+315
-1
lines changed

4 files changed

+315
-1
lines changed

dmriprep/cli/parser.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,23 @@ def _bids_filter(value):
219219
https://www.nipreps.org/dmriprep/en/%s/spaces.html"""
220220
% (currentv.base_version if is_release else "latest"),
221221
)
222+
g_conf.add_argument(
223+
"--bold2t1w-init",
224+
action="store",
225+
default="register",
226+
choices=["register", "header"],
227+
help='Either "register" (the default) to initialize volumes at center or "header"'
228+
" to use the header information when coregistering BOLD to T1w images.",
229+
)
230+
g_conf.add_argument(
231+
"--bold2t1w-dof",
232+
action="store",
233+
default=6,
234+
choices=[6, 9, 12],
235+
type=int,
236+
help="Degrees of freedom when registering BOLD to T1w images. "
237+
"6 degrees (rotation and translation) are used by default.",
238+
)
222239

223240
# ANTs options
224241
g_ants = parser.add_argument_group("Specific options for ANTs registrations")

dmriprep/config/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ class workflow(_Config):
436436
ignore = None
437437
"""Ignore particular steps for *dMRIPrep*."""
438438
longitudinal = False
439+
"""Number of ICA components to be estimated by MELODIC
440+
(positive = exact, negative = maximum)."""
441+
bold2t1w_dof = None
442+
"""Degrees of freedom of the BOLD-to-T1w registration steps."""
443+
bold2t1w_init = "register"
439444
"""Run FreeSurfer ``recon-all`` with the ``-logitudinal`` flag."""
440445
run_reconall = True
441446
"""Run FreeSurfer's surface reconstruction."""

dmriprep/workflows/base.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ..utils.bids import collect_data
1919
from .dwi.base import init_early_b0ref_wf
2020
from .fmap.base import init_fmap_estimation_wf
21-
21+
from .dwi.registration import init_bbreg_wf
2222

2323
def init_dmriprep_wf():
2424
"""
@@ -354,6 +354,30 @@ def init_single_subject_wf(subject_id):
354354
])
355355
# fmt:on
356356

357+
if config.workflow.run_reconall:
358+
from niworkflows.interfaces.nibabel import ApplyMask
359+
360+
# Mask the T1
361+
t1w_brain = pe.Node(ApplyMask(), name='t1w_brain')
362+
363+
bbr_wf = init_bbreg_wf(use_bbr=True, bold2t1w_dof=config.workflow.bold2t1w_dof,
364+
bold2t1w_init=config.workflow.bold2t1w_init, omp_nthreads=config.nipype.omp_nthreads)
365+
366+
workflow.connect([
367+
# T1 Mask
368+
(anat_preproc_wf, t1w_brain, [('outputnode.t1w_preproc', 'in_file'),
369+
('outputnode.t1w_mask', 'in_mask')]),
370+
# BBregister
371+
(split_info, bbr_wf, [('dwi_file', 'inputnode.in_file')]),
372+
(t1w_brain, bbr_wf, [('out_file', 'inputnode.t1w_brain')]),
373+
(anat_preproc_wf, bbr_wf, [('outputnode.t1w_dseg', 'inputnode.t1w_dseg')]),
374+
(fsinputnode, bbr_wf, [("subjects_dir", "inputnode.subjects_dir")]),
375+
(bids_info, bbr_wf, [('subject', 'inputnode.subject_id')]),
376+
(anat_preproc_wf, bbr_wf, [
377+
('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2t1w_xfm')
378+
])
379+
])
380+
357381
fmap_estimation_wf = init_fmap_estimation_wf(
358382
subject_data["dwi"], debug=config.execution.debug
359383
)
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""
4+
Registration workflows
5+
++++++++++++++++++++++
6+
7+
.. autofunction:: init_bbreg_wf
8+
9+
"""
10+
from ... import config
11+
12+
import os
13+
import os.path as op
14+
15+
import pkg_resources as pkgr
16+
17+
from nipype.pipeline import engine as pe
18+
from nipype.interfaces import utility as niu, fsl, c3
19+
20+
from ...interfaces import DerivativesDataSink
21+
22+
DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB
23+
LOGGER = config.loggers.workflow
24+
25+
def init_bbreg_wf(use_bbr, bold2t1w_dof, bold2t1w_init, omp_nthreads, name='bbreg_wf'):
26+
"""
27+
Build a workflow to run FreeSurfer's ``bbregister``.
28+
29+
This workflow uses FreeSurfer's ``bbregister`` to register a BOLD image to
30+
a T1-weighted structural image.
31+
32+
It is a counterpart to :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`,
33+
which performs the same task using FSL's FLIRT with a BBR cost function.
34+
The ``use_bbr`` option permits a high degree of control over registration.
35+
If ``False``, standard, affine coregistration will be performed using
36+
FreeSurfer's ``mri_coreg`` tool.
37+
If ``True``, ``bbregister`` will be seeded with the initial transform found
38+
by ``mri_coreg`` (equivalent to running ``bbregister --init-coreg``).
39+
If ``None``, after ``bbregister`` is run, the resulting affine transform
40+
will be compared to the initial transform found by ``mri_coreg``.
41+
Excessive deviation will result in rejecting the BBR refinement and
42+
accepting the original, affine registration.
43+
44+
Workflow Graph
45+
.. workflow ::
46+
:graph2use: orig
47+
:simple_form: yes
48+
49+
from fmriprep.workflows.bold.registration import init_bbreg_wf
50+
wf = init_bbreg_wf(use_bbr=True, bold2t1w_dof=9,
51+
bold2t1w_init='register', omp_nthreads=1)
52+
53+
54+
Parameters
55+
----------
56+
use_bbr : :obj:`bool` or None
57+
Enable/disable boundary-based registration refinement.
58+
If ``None``, test BBR result for distortion before accepting.
59+
bold2t1w_dof : 6, 9 or 12
60+
Degrees-of-freedom for BOLD-T1w registration
61+
bold2t1w_init : str, 'header' or 'register'
62+
If ``'header'``, use header information for initialization of BOLD and T1 images.
63+
If ``'register'``, align volumes by their centers.
64+
name : :obj:`str`, optional
65+
Workflow name (default: bbreg_wf)
66+
67+
Inputs
68+
------
69+
in_file
70+
Reference BOLD image to be registered
71+
fsnative2t1w_xfm
72+
FSL-style affine matrix translating from FreeSurfer T1.mgz to T1w
73+
subjects_dir
74+
FreeSurfer SUBJECTS_DIR
75+
subject_id
76+
FreeSurfer subject ID (must have folder in SUBJECTS_DIR)
77+
t1w_brain
78+
Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`)
79+
t1w_dseg
80+
Unused (see :py:func:`~fmriprep.workflows.bold.registration.init_fsl_bbr_wf`)
81+
82+
Outputs
83+
-------
84+
itk_bold_to_t1
85+
Affine transform from ``ref_bold_brain`` to T1 space (ITK format)
86+
itk_t1_to_bold
87+
Affine transform from T1 space to BOLD space (ITK format)
88+
out_report
89+
Reportlet for assessing registration quality
90+
fallback
91+
Boolean indicating whether BBR was rejected (mri_coreg registration returned)
92+
93+
"""
94+
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
95+
# See https://github.com/nipreps/fmriprep/issues/768
96+
from niworkflows.interfaces.freesurfer import (
97+
PatchedBBRegisterRPT as BBRegisterRPT,
98+
PatchedMRICoregRPT as MRICoregRPT,
99+
PatchedLTAConvert as LTAConvert
100+
)
101+
from niworkflows.interfaces.nitransforms import ConcatenateXFMs
102+
103+
workflow = Workflow(name=name)
104+
workflow.__desc__ = """\
105+
The BOLD reference was then co-registered to the T1w reference using
106+
`bbregister` (FreeSurfer) which implements boundary-based registration [@bbr].
107+
Co-registration was configured with {dof} degrees of freedom{reason}.
108+
""".format(dof={6: 'six', 9: 'nine', 12: 'twelve'}[bold2t1w_dof],
109+
reason='' if bold2t1w_dof == 6 else
110+
'to account for distortions remaining in the BOLD reference')
111+
112+
inputnode = pe.Node(
113+
niu.IdentityInterface([
114+
'in_file',
115+
'fsnative2t1w_xfm', 'subjects_dir', 'subject_id', # BBRegister
116+
't1w_dseg', 't1w_brain']), # FLIRT BBR
117+
name='inputnode')
118+
outputnode = pe.Node(
119+
niu.IdentityInterface(['itk_bold_to_t1', 'itk_t1_to_bold', 'out_report', 'fallback']),
120+
name='outputnode')
121+
122+
if bold2t1w_init not in ("register", "header"):
123+
raise ValueError(f"Unknown BOLD-T1w initialization option: {bold2t1w_init}")
124+
125+
# For now make BBR unconditional - in the future, we can fall back to identity,
126+
# but adding the flexibility without testing seems a bit dangerous
127+
if bold2t1w_init == "header":
128+
if use_bbr is False:
129+
raise ValueError("Cannot disable BBR and use header registration")
130+
if use_bbr is None:
131+
LOGGER.warning("Initializing BBR with header; affine fallback disabled")
132+
use_bbr = True
133+
134+
merge_ltas = pe.Node(niu.Merge(2), name='merge_ltas', run_without_submitting=True)
135+
concat_xfm = pe.Node(ConcatenateXFMs(inverse=True), name='concat_xfm')
136+
137+
workflow.connect([
138+
# Output ITK transforms
139+
(inputnode, merge_ltas, [('fsnative2t1w_xfm', 'in2')]),
140+
(merge_ltas, concat_xfm, [('out', 'in_xfms')]),
141+
(concat_xfm, outputnode, [('out_xfm', 'itk_bold_to_t1')]),
142+
(concat_xfm, outputnode, [('out_inv', 'itk_t1_to_bold')]),
143+
])
144+
145+
# Define both nodes, but only connect conditionally
146+
mri_coreg = pe.Node(
147+
MRICoregRPT(dof=bold2t1w_dof, sep=[4], ftol=0.0001, linmintol=0.01,
148+
generate_report=not use_bbr),
149+
name='mri_coreg', n_procs=omp_nthreads, mem_gb=5)
150+
151+
bbregister = pe.Node(
152+
BBRegisterRPT(dof=bold2t1w_dof, contrast_type='t1', registered_file=True,
153+
out_lta_file=True, generate_report=True),
154+
name='bbregister', mem_gb=12)
155+
156+
# Use mri_coreg
157+
if bold2t1w_init == "register":
158+
workflow.connect([
159+
(inputnode, mri_coreg, [('subjects_dir', 'subjects_dir'),
160+
('subject_id', 'subject_id'),
161+
('in_file', 'source_file')]),
162+
])
163+
164+
# Short-circuit workflow building, use initial registration
165+
if use_bbr is False:
166+
workflow.connect([
167+
(mri_coreg, outputnode, [('out_report', 'out_report')]),
168+
(mri_coreg, merge_ltas, [('out_lta_file', 'in1')])])
169+
outputnode.inputs.fallback = True
170+
171+
return workflow
172+
173+
# Use bbregister
174+
workflow.connect([
175+
(inputnode, bbregister, [('subjects_dir', 'subjects_dir'),
176+
('subject_id', 'subject_id'),
177+
('in_file', 'source_file')]),
178+
])
179+
180+
if bold2t1w_init == "header":
181+
bbregister.inputs.init = "header"
182+
else:
183+
workflow.connect([(mri_coreg, bbregister, [('out_lta_file', 'init_reg_file')])])
184+
185+
# Short-circuit workflow building, use boundary-based registration
186+
if use_bbr is True:
187+
workflow.connect([
188+
(bbregister, outputnode, [('out_report', 'out_report')]),
189+
(bbregister, merge_ltas, [('out_lta_file', 'in1')])])
190+
outputnode.inputs.fallback = False
191+
192+
return workflow
193+
194+
# Only reach this point if bold2t1w_init is "register" and use_bbr is None
195+
196+
transforms = pe.Node(niu.Merge(2), run_without_submitting=True, name='transforms')
197+
reports = pe.Node(niu.Merge(2), run_without_submitting=True, name='reports')
198+
199+
lta_ras2ras = pe.MapNode(LTAConvert(out_lta=True), iterfield=['in_lta'],
200+
name='lta_ras2ras', mem_gb=2)
201+
compare_transforms = pe.Node(niu.Function(function=compare_xforms), name='compare_transforms')
202+
203+
select_transform = pe.Node(niu.Select(), run_without_submitting=True, name='select_transform')
204+
select_report = pe.Node(niu.Select(), run_without_submitting=True, name='select_report')
205+
206+
workflow.connect([
207+
(bbregister, transforms, [('out_lta_file', 'in1')]),
208+
(mri_coreg, transforms, [('out_lta_file', 'in2')]),
209+
# Normalize LTA transforms to RAS2RAS (inputs are VOX2VOX) and compare
210+
(transforms, lta_ras2ras, [('out', 'in_lta')]),
211+
(lta_ras2ras, compare_transforms, [('out_lta', 'lta_list')]),
212+
(compare_transforms, outputnode, [('out', 'fallback')]),
213+
# Select output transform
214+
(transforms, select_transform, [('out', 'inlist')]),
215+
(compare_transforms, select_transform, [('out', 'index')]),
216+
(select_transform, merge_ltas, [('out', 'in1')]),
217+
# Select output report
218+
(bbregister, reports, [('out_report', 'in1')]),
219+
(mri_coreg, reports, [('out_report', 'in2')]),
220+
(reports, select_report, [('out', 'inlist')]),
221+
(compare_transforms, select_report, [('out', 'index')]),
222+
(select_report, outputnode, [('out', 'out_report')]),
223+
])
224+
225+
return workflow
226+
227+
228+
def compare_xforms(lta_list, norm_threshold=15):
229+
"""
230+
Computes a normalized displacement between two affine transforms as the
231+
maximum overall displacement of the midpoints of the faces of a cube, when
232+
each transform is applied to the cube.
233+
This combines displacement resulting from scaling, translation and rotation.
234+
235+
Although the norm is in mm, in a scaling context, it is not necessarily
236+
equivalent to that distance in translation.
237+
238+
We choose a default threshold of 15mm as a rough heuristic.
239+
Normalized displacement above 20mm showed clear signs of distortion, while
240+
"good" BBR refinements were frequently below 10mm displaced from the rigid
241+
transform.
242+
The 10-20mm range was more ambiguous, and 15mm chosen as a compromise.
243+
This is open to revisiting in either direction.
244+
245+
See discussion in
246+
`GitHub issue #681`_ <https://github.com/nipreps/fmriprep/issues/681>`_
247+
and the `underlying implementation
248+
<https://github.com/nipy/nipype/blob/56b7c81eedeeae884ba47c80096a5f66bd9f8116/nipype/algorithms/rapidart.py#L108-L159>`_.
249+
250+
Parameters
251+
----------
252+
253+
lta_list : :obj:`list` or :obj:`tuple` of :obj:`str`
254+
the two given affines in LTA format
255+
norm_threshold : :obj:`float`
256+
the upper bound limit to the normalized displacement caused by the
257+
second transform relative to the first (default: `15`)
258+
259+
"""
260+
from niworkflows.interfaces.surf import load_transform
261+
from nipype.algorithms.rapidart import _calc_norm_affine
262+
263+
bbr_affine = load_transform(lta_list[0])
264+
fallback_affine = load_transform(lta_list[1])
265+
266+
norm, _ = _calc_norm_affine([fallback_affine, bbr_affine], use_differences=True)
267+
268+
return norm[1] > norm_threshold

0 commit comments

Comments
 (0)