Skip to content

Commit 7032782

Browse files
authored
ENH: Leverage T2w if available for BOLD -> anat coregistration (#3208)
This PR adds a new parameter `use_t2w` to `init_bold_reg_wf`, which enables/disables using a T2w reference as an intermediate for bbregister. This PR only changes the behavior of the `bbreg_wf` fork, the FSL workflow remains using the T1w reference. cc @madisoth as this was inspired based on some comparison work from him
2 parents f719949 + 08dfd68 commit 7032782

File tree

10 files changed

+193
-85
lines changed

10 files changed

+193
-85
lines changed

fmriprep/cli/parser.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def _build_parser(**kwargs):
3131
3232
``kwargs`` are passed to ``argparse.ArgumentParser`` (mainly useful for debugging).
3333
"""
34-
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
34+
from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentParser
3535
from functools import partial
3636
from pathlib import Path
3737

@@ -40,6 +40,27 @@ def _build_parser(**kwargs):
4040

4141
from .version import check_latest, is_flagged
4242

43+
deprecations = {
44+
# parser attribute name: (replacement flag, version slated to be removed in)
45+
'use_aroma': (None, '24.0.0'),
46+
'aroma_melodic_dim': (None, '24.0.0'),
47+
'aroma_err_on_warn': (None, '24.0.0'),
48+
'bold2t1w_init': ('--bold2anat-init', '24.2.0'),
49+
'bold2t1w_dof': ('--bold2anat-dof', '24.2.0'),
50+
}
51+
52+
class DeprecatedAction(Action):
53+
def __call__(self, parser, namespace, values, option_string=None):
54+
new_opt, rem_vers = deprecations.get(self.dest, (None, None))
55+
msg = (
56+
f"{self.option_strings} has been deprecated and will be removed in "
57+
f"{rem_vers or 'a later version'}."
58+
)
59+
if new_opt:
60+
msg += f" Please use `{new_opt}` instead."
61+
print(msg, file=sys.stderr)
62+
delattr(namespace, self.dest)
63+
4364
def _path_exists(path, parser):
4465
"""Ensure a given path exists."""
4566
if path is None or not Path(path).exists():
@@ -313,19 +334,32 @@ def _slice_time_ref(value, parser):
313334
)
314335
g_conf.add_argument(
315336
"--bold2t1w-init",
316-
action="store",
317-
default="register",
337+
action=DeprecatedAction,
318338
choices=["register", "header"],
319-
help='Either "register" (the default) to initialize volumes at center or "header"'
320-
" to use the header information when coregistering BOLD to T1w images.",
339+
help="Deprecated - use `--bold2anat-init` instead.",
321340
)
322341
g_conf.add_argument(
323342
"--bold2t1w-dof",
343+
action=DeprecatedAction,
344+
choices=[6, 9, 12],
345+
type=int,
346+
help="Deprecated - use `--bold2anat-dof` instead.",
347+
)
348+
g_conf.add_argument(
349+
"--bold2anat-init",
350+
choices=["auto", "t1w", "t2w", "header"],
351+
default="auto",
352+
help="Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used "
353+
"if available, otherwise the T1w image. `t1w` forces use of the T1w, `t2w` forces use of "
354+
"the T2w, and `header` uses the BOLD header information without an initial registration.",
355+
)
356+
g_conf.add_argument(
357+
"--bold2anat-dof",
324358
action="store",
325359
default=6,
326360
choices=[6, 9, 12],
327361
type=int,
328-
help="Degrees of freedom when registering BOLD to T1w images. "
362+
help="Degrees of freedom when registering BOLD to anatomical images. "
329363
"6 degrees (rotation and translation) are used by default.",
330364
)
331365
g_conf.add_argument(
@@ -445,23 +479,20 @@ def _slice_time_ref(value, parser):
445479
g_aroma = parser.add_argument_group("[DEPRECATED] Options for running ICA_AROMA")
446480
g_aroma.add_argument(
447481
"--use-aroma",
448-
action="store_true",
449-
default=False,
482+
action=DeprecatedAction,
450483
help="Deprecated. Will raise an error in 24.0.",
451484
)
452485
g_aroma.add_argument(
453486
"--aroma-melodic-dimensionality",
454487
dest="aroma_melodic_dim",
455-
action="store",
456-
default=0,
488+
action=DeprecatedAction,
457489
type=int,
458490
help="Deprecated. Will raise an error in 24.0.",
459491
)
460492
g_aroma.add_argument(
461493
"--error-on-aroma-warnings",
462-
action="store_true",
494+
action=DeprecatedAction,
463495
dest="aroma_err_on_warn",
464-
default=False,
465496
help="Deprecated. Will raise an error in 24.0.",
466497
)
467498

fmriprep/config.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -525,11 +525,12 @@ class workflow(_Config):
525525
aroma_melodic_dim = None
526526
"""Number of ICA components to be estimated by MELODIC
527527
(positive = exact, negative = maximum)."""
528-
bold2t1w_dof = None
529-
"""Degrees of freedom of the BOLD-to-T1w registration steps."""
530-
bold2t1w_init = "register"
531-
"""Whether to use standard coregistration ('register') or to initialize coregistration from the
532-
BOLD image-header ('header')."""
528+
bold2anat_dof = None
529+
"""Degrees of freedom of the BOLD-to-anatomical registration steps."""
530+
bold2anat_init = "auto"
531+
"""Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used
532+
if available, otherwise the T1w image. `t1w` forces use of the T1w, `t2w` forces use of
533+
the T2w, and `header` uses the BOLD header information without an initial registration."""
533534
cifti_output = None
534535
"""Generate HCP Grayordinates, accepts either ``'91k'`` (default) or ``'170k'``."""
535536
dummy_scans = None

fmriprep/data/tests/config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ write_graph = false
3232
anat_only = false
3333
aroma_err_on_warn = false
3434
aroma_melodic_dim = -200
35-
bold2t1w_dof = 6
35+
bold2anat_dof = 6
3636
fmap_bspline = false
3737
force_syn = false
3838
hires = true

fmriprep/interfaces/patches.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
from random import randint
3030
from time import sleep
3131

32+
import nipype.interfaces.freesurfer as fs
33+
import nipype.interfaces.io as nio
3234
from nipype.algorithms import confounds as nac
35+
from nipype.interfaces.base import File, traits
3336
from numpy.linalg.linalg import LinAlgError
3437

3538

@@ -77,3 +80,38 @@ def _run_interface(self, runtime):
7780
sleep(randint(start + 4, start + 10))
7881

7982
return runtime
83+
84+
85+
class _MRICoregInputSpec(fs.registration.MRICoregInputSpec):
86+
reference_file = File(
87+
argstr="--ref %s",
88+
desc="reference (target) file",
89+
copyfile=False,
90+
)
91+
subject_id = traits.Str(
92+
argstr="--s %s",
93+
position=1,
94+
requires=["subjects_dir"],
95+
desc="freesurfer subject ID (implies ``reference_mask == "
96+
"aparc+aseg.mgz`` unless otherwise specified)",
97+
)
98+
99+
100+
class MRICoreg(fs.MRICoreg):
101+
"""
102+
Patched that allows setting both a reference file and the subjects dir.
103+
"""
104+
105+
input_spec = _MRICoregInputSpec
106+
107+
108+
class _FSSourceOutputSpec(nio.FSSourceOutputSpec):
109+
T2 = File(desc="Intensity normalized whole-head volume", loc="mri")
110+
111+
112+
class FreeSurferSource(nio.FreeSurferSource):
113+
"""
114+
Patch to allow grabbing the T2 volume, if available
115+
"""
116+
117+
output_spec = _FSSourceOutputSpec

fmriprep/interfaces/reports.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,12 @@ class FunctionalSummaryInputSpec(TraitedSpec):
213213
6, 9, 12, desc='Registration degrees of freedom', mandatory=True
214214
)
215215
registration_init = traits.Enum(
216-
'register',
216+
't1w',
217+
't2w',
217218
'header',
218219
mandatory=True,
219220
desc='Whether to initialize registration with the "header"'
220-
' or by centering the volumes ("register")',
221+
' or by centering the volumes ("t1w" or "t2w")',
221222
)
222223
tr = traits.Float(desc='Repetition time', mandatory=True)
223224
dummy_scans = traits.Either(traits.Int(), None, desc='number of dummy scans specified by user')

fmriprep/workflows/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,15 @@ def init_single_subject_wf(subject_id: str):
637637
num_bold=len(bold_runs)
638638
)
639639

640+
# Before initializing BOLD workflow, select/verify anatomical target for coregistration
641+
if config.workflow.bold2anat_init in ('auto', 't2w'):
642+
has_t2w = subject_data['t2w'] or 't2w_preproc' in anatomical_cache
643+
if config.workflow.bold2anat_init == 't2w' and not has_t2w:
644+
raise OSError(
645+
"A T2w image is expected for BOLD-to-anatomical coregistration and was not found"
646+
)
647+
config.workflow.bold2anat_init = 't2w' if has_t2w else 't1w'
648+
640649
for bold_series in bold_runs:
641650
bold_file = bold_series[0]
642651
fieldmap_id = estimator_map.get(bold_file)

fmriprep/workflows/bold/fit.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def init_bold_fit_wf(
203203
from fmriprep.utils.misc import estimate_bold_mem_usage
204204

205205
layout = config.execution.layout
206+
bids_filters = config.execution.get().get('bids_filters', {})
206207

207208
# Fitting operates on the shortest echo
208209
# This could become more complicated in the future
@@ -211,7 +212,7 @@ def init_bold_fit_wf(
211212
# Collect sbref files, sorted by EchoTime
212213
sbref_files = get_sbrefs(
213214
bold_series,
214-
entity_overrides=config.execution.get().get('bids_filters', {}).get('sbref', {}),
215+
entity_overrides=bids_filters.get('sbref', {}),
215216
layout=layout,
216217
)
217218

@@ -312,8 +313,8 @@ def init_bold_fit_wf(
312313
FunctionalSummary(
313314
distortion_correction="None", # Can override with connection
314315
registration=("FSL", "FreeSurfer")[config.workflow.run_reconall],
315-
registration_dof=config.workflow.bold2t1w_dof,
316-
registration_init=config.workflow.bold2t1w_init,
316+
registration_dof=config.workflow.bold2anat_dof,
317+
registration_init=config.workflow.bold2anat_init,
317318
pe_direction=metadata.get("PhaseEncodingDirection"),
318319
echo_idx=entities.get("echo", []),
319320
tr=metadata["RepetitionTime"],
@@ -586,8 +587,8 @@ def init_bold_fit_wf(
586587
if not boldref2anat_xform:
587588
# calculate BOLD registration to T1w
588589
bold_reg_wf = init_bold_reg_wf(
589-
bold2t1w_dof=config.workflow.bold2t1w_dof,
590-
bold2t1w_init=config.workflow.bold2t1w_init,
590+
bold2anat_dof=config.workflow.bold2anat_dof,
591+
bold2anat_init=config.workflow.bold2anat_init,
591592
use_bbr=config.workflow.use_bbr,
592593
freesurfer=config.workflow.run_reconall,
593594
omp_nthreads=omp_nthreads,

0 commit comments

Comments
 (0)