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