diff --git a/src/smriprep/utils/misc.py b/src/smriprep/utils/misc.py
index 3c473b49a2..c40abf2a4a 100644
--- a/src/smriprep/utils/misc.py
+++ b/src/smriprep/utils/misc.py
@@ -22,6 +22,8 @@
#
"""Self-contained utilities to be used within Function nodes."""
+import typing as ty
+
def apply_lut(in_dseg, lut, newpath=None):
"""Map the input discrete segmentation to a new label set (lookup table, LUT)."""
@@ -95,3 +97,48 @@ def fs_isRunning(subjects_dir, subject_id, mtime_tol=86400, logger=None):
if logger:
logger.warn(f'Removed "IsRunning*" files found under {subj_dir}')
return subjects_dir
+
+
+def collect_anat(
+ subject_data: dict, precomputed: dict, reference_anat: ty.Literal['T1w', 'T2w'] = 'T1w'
+):
+ """
+ Collects the anatomical inputs for a given subject and organises the
+ files and associated information into ``reference`` and ``aux`` keys
+ to pass to :py:func:`init_anat_fit_wf`
+
+ Parameters
+ ----------
+ subject_data: :obj:`dict`
+ lists of input data
+ precomputed: :obj:`dict`
+ cache of derivative files
+ reference_anat: :obj:`str`
+ MR image type (T1w, T2w, etc.) of primary anatomical scan
+
+ Returns
+ -------
+ anat_inputs: :obj:`dict`
+ """
+ ref_anat = reference_anat.lower()
+ if ref_anat not in subject_data.keys():
+ raise FileNotFoundError
+
+ anat_inputs = {
+ modality: {
+ 'data': subject_data[modality],
+ 'n': len(subject_data[modality]),
+ 'precomputed': f'{modality}_preproc' in precomputed,
+ 'role': 'reference' if modality.capitalize() == reference_anat else 'aux',
+ }
+ for modality in ['t1w', 't2w', 'flair']
+ if modality in subject_data.keys()
+ }
+ anat_inputs[reference_anat.lower()].update(
+ {
+ f'have_{preproc}': f'{reference_anat.lower()}_{preproc}' in precomputed
+ for preproc in ['mask', 'tpms', 'dseg']
+ }
+ )
+
+ return anat_inputs
diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py
index 0a9c3c7311..22d4a74d82 100644
--- a/src/smriprep/workflows/anatomical.py
+++ b/src/smriprep/workflows/anatomical.py
@@ -101,14 +101,12 @@ def init_anat_preproc_wf(
hires: bool,
longitudinal: bool,
msm_sulc: bool,
- t1w: list,
- t2w: list,
+ anat: dict,
skull_strip_mode: str,
skull_strip_template: Reference,
spaces: SpatialReferences,
precomputed: dict,
omp_nthreads: int,
- flair: list = (), # Remove default after callers start passing it
debug: bool = False,
sloppy: bool = False,
cifti_output: ty.Literal['91k', '170k', False] = False,
@@ -162,8 +160,6 @@ def init_anat_preproc_wf(
longitudinal : :obj:`bool`
Create unbiased structural template, regardless of number of inputs
(may increase runtime)
- t1w : :obj:`list`
- List of T1-weighted structural images.
skull_strip_mode : :obj:`str`
Determiner for T1-weighted skull stripping (`force` ensures skull stripping,
`skip` ignores skull stripping, and `auto` automatically ignores skull stripping
@@ -209,16 +205,16 @@ def init_anat_preproc_wf(
Outputs
-------
- t1w_preproc
- The T1w reference map, which is calculated as the average of bias-corrected
- and preprocessed T1w images, defining the anatomical space.
- t1w_mask
+ anat_preproc
+ The anatomical reference map, which is calculated as the average of bias-corrected
+ and preprocessed anatomical images, defining the anatomical space.
+ anat_mask
Brain (binary) mask estimated by brain extraction.
- t1w_dseg
+ anat_dseg
Brain tissue segmentation of the preprocessed structural image, including
gray-matter (GM), white-matter (WM) and cerebrospinal fluid (CSF).
- t1w_tpms
- List of tissue probability maps corresponding to ``t1w_dseg``.
+ anat_tpms
+ List of tissue probability maps corresponding to ``anat_dseg``.
template
List of template names to which the structural image has been registered
anat2std_xfm
@@ -233,8 +229,8 @@ def init_anat_preproc_wf(
subject_id
FreeSurfer subject ID; use as input to a node to ensure that it is run after
FreeSurfer reconstruction is completed.
- fsnative2t1w_xfm
- ITK-style affine matrix translating from FreeSurfer-conformed subject space to T1w
+ fsnative2anat_xfm
+ ITK-style affine matrix translating from FreeSurfer-conformed subject space to anatomical
"""
workflow = Workflow(name=name)
@@ -249,15 +245,15 @@ def init_anat_preproc_wf(
'template',
'subjects_dir',
'subject_id',
- 't1w_preproc',
- 't1w_mask',
- 't1w_dseg',
- 't1w_tpms',
+ 'anat_preproc',
+ 'anat_mask',
+ 'anat_dseg',
+ 'anat_tpms',
'anat2std_xfm',
'std2anat_xfm',
- 'fsnative2t1w_xfm',
- 't1w_aparc',
- 't1w_aseg',
+ 'fsnative2anat_xfm',
+ 'anat_aparc',
+ 'anat_aseg',
'sphere_reg',
'sphere_reg_fsLR',
]
@@ -275,9 +271,7 @@ def init_anat_preproc_wf(
skull_strip_mode=skull_strip_mode,
skull_strip_template=skull_strip_template,
spaces=spaces,
- t1w=t1w,
- t2w=t2w,
- flair=flair,
+ anat=anat,
precomputed=precomputed,
debug=debug,
sloppy=sloppy,
@@ -304,13 +298,13 @@ def init_anat_preproc_wf(
('outputnode.template', 'template'),
('outputnode.subjects_dir', 'subjects_dir'),
('outputnode.subject_id', 'subject_id'),
- ('outputnode.t1w_preproc', 't1w_preproc'),
- ('outputnode.t1w_mask', 't1w_mask'),
- ('outputnode.t1w_dseg', 't1w_dseg'),
- ('outputnode.t1w_tpms', 't1w_tpms'),
+ ('outputnode.anat_preproc', 'anat_preproc'),
+ ('outputnode.anat_mask', 'anat_mask'),
+ ('outputnode.anat_dseg', 'anat_dseg'),
+ ('outputnode.anat_tpms', 'anat_tpms'),
('outputnode.anat2std_xfm', 'anat2std_xfm'),
('outputnode.std2anat_xfm', 'std2anat_xfm'),
- ('outputnode.fsnative2t1w_xfm', 'fsnative2t1w_xfm'),
+ ('outputnode.fsnative2anat_xfm', 'fsnative2anat_xfm'),
('outputnode.sphere_reg', 'sphere_reg'),
(f"outputnode.sphere_reg_{'msm' if msm_sulc else 'fsLR'}", 'sphere_reg_fsLR'),
('outputnode.anat_ribbon', 'anat_ribbon'),
@@ -320,11 +314,11 @@ def init_anat_preproc_wf(
('outputnode.anat2std_xfm', 'inputnode.anat2std_xfm'),
]),
(anat_fit_wf, ds_std_volumes_wf, [
- ('outputnode.t1w_valid_list', 'inputnode.source_files'),
- ('outputnode.t1w_preproc', 'inputnode.anat_preproc'),
- ('outputnode.t1w_mask', 'inputnode.anat_mask'),
- ('outputnode.t1w_dseg', 'inputnode.anat_dseg'),
- ('outputnode.t1w_tpms', 'inputnode.anat_tpms'),
+ ('outputnode.anat_valid_list', 'inputnode.source_files'),
+ ('outputnode.anat_preproc', 'inputnode.anat_preproc'),
+ ('outputnode.anat_mask', 'inputnode.anat_mask'),
+ ('outputnode.anat_dseg', 'inputnode.anat_dseg'),
+ ('outputnode.anat_tpms', 'inputnode.anat_tpms'),
]),
(template_iterator_wf, ds_std_volumes_wf, [
('outputnode.std_t1w', 'inputnode.ref_file'),
@@ -348,33 +342,33 @@ def init_anat_preproc_wf(
workflow.connect([
(anat_fit_wf, surface_derivatives_wf, [
- ('outputnode.t1w_preproc', 'inputnode.reference'),
+ ('outputnode.anat_preproc', 'inputnode.reference'),
('outputnode.subjects_dir', 'inputnode.subjects_dir'),
('outputnode.subject_id', 'inputnode.subject_id'),
- ('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2anat_xfm'),
+ ('outputnode.fsnative2anat_xfm', 'inputnode.fsnative2anat_xfm'),
]),
(anat_fit_wf, ds_surfaces_wf, [
- ('outputnode.t1w_valid_list', 'inputnode.source_files'),
+ ('outputnode.anat_valid_list', 'inputnode.source_files'),
]),
(surface_derivatives_wf, ds_surfaces_wf, [
('outputnode.inflated', 'inputnode.inflated'),
]),
(anat_fit_wf, ds_curv_wf, [
- ('outputnode.t1w_valid_list', 'inputnode.source_files'),
+ ('outputnode.anat_valid_list', 'inputnode.source_files'),
]),
(surface_derivatives_wf, ds_curv_wf, [
('outputnode.curv', 'inputnode.curv'),
]),
(anat_fit_wf, ds_fs_segs_wf, [
- ('outputnode.t1w_valid_list', 'inputnode.source_files'),
+ ('outputnode.anat_valid_list', 'inputnode.source_files'),
]),
(surface_derivatives_wf, ds_fs_segs_wf, [
('outputnode.out_aseg', 'inputnode.anat_fs_aseg'),
('outputnode.out_aparc', 'inputnode.anat_fs_aparc'),
]),
(surface_derivatives_wf, outputnode, [
- ('outputnode.out_aseg', 't1w_aseg'),
- ('outputnode.out_aparc', 't1w_aparc'),
+ ('outputnode.out_aseg', 'anat_aseg'),
+ ('outputnode.out_aparc', 'anat_aparc'),
]),
]) # fmt:skip
@@ -440,10 +434,10 @@ def init_anat_preproc_wf(
('outputnode.midthickness_fsLR', 'inputnode.midthickness_fsLR'),
]),
(anat_fit_wf, ds_fsLR_surfaces_wf, [
- ('outputnode.t1w_valid_list', 'inputnode.source_files'),
+ ('outputnode.anat_valid_list', 'inputnode.source_files'),
]),
(anat_fit_wf, ds_grayord_metrics_wf, [
- ('outputnode.t1w_valid_list', 'inputnode.source_files'),
+ ('outputnode.anat_valid_list', 'inputnode.source_files'),
]),
(resample_surfaces_wf, ds_fsLR_surfaces_wf, [
('outputnode.white_fsLR', 'inputnode.white'),
@@ -471,14 +465,12 @@ def init_anat_fit_wf(
hires: bool,
longitudinal: bool,
msm_sulc: bool,
- t1w: list,
- t2w: list,
+ anat: dict,
skull_strip_mode: str,
skull_strip_template: Reference,
spaces: SpatialReferences,
precomputed: dict,
omp_nthreads: int,
- flair: list = (), # Remove default after callers start passing it
debug: bool = False,
sloppy: bool = False,
name='anat_fit_wf',
@@ -490,7 +482,7 @@ def init_anat_fit_wf(
This includes:
- - T1w reference: realigning and then averaging T1w images.
+ - anatomical reference: realigning and then averaging reference anatomical images.
- Brain extraction and INU (bias field) correction.
- Brain tissue segmentation.
- Spatial normalization to standard spaces.
@@ -582,18 +574,18 @@ def init_anat_fit_wf(
Outputs
-------
- t1w_preproc
- The T1w reference map, which is calculated as the average of bias-corrected
- and preprocessed T1w images, defining the anatomical space.
- t1w_mask
+ anat_preproc
+ The anatomical reference map, which is calculated as the average of bias-corrected
+ and preprocessed anatomical images, defining the anatomical space.
+ anat_mask
Brain (binary) mask estimated by brain extraction.
- t1w_dseg
+ anat_dseg
Brain tissue segmentation of the preprocessed structural image, including
gray-matter (GM), white-matter (WM) and cerebrospinal fluid (CSF).
- t1w_tpms
- List of tissue probability maps corresponding to ``t1w_dseg``.
- t1w_valid_list
- List of input T1w images accepted for preprocessing. If t1w_preproc is
+ anat_tpms
+ List of tissue probability maps corresponding to ``anat_dseg``.
+ anat_valid_list
+ List of input anatomical images accepted for preprocessing. If anat_preproc is
precomputed, this is always a list containing that image.
template
List of template names to which the structural image has been registered
@@ -609,8 +601,8 @@ def init_anat_fit_wf(
subject_id
FreeSurfer subject ID; use as input to a node to ensure that it is run after
FreeSurfer reconstruction is completed.
- fsnative2t1w_xfm
- ITK-style affine matrix translating from FreeSurfer-conformed subject space to T1w
+ fsnative2anat_xfm
+ ITK-style affine matrix translating from FreeSurfer-conformed subject space to anatomical
See Also
--------
@@ -619,18 +611,22 @@ def init_anat_fit_wf(
"""
workflow = Workflow(name=name)
- num_t1w = len(t1w)
+ ref_anat = [modality for modality in anat.keys() if anat[modality]['role'] == 'reference'][0]
+ aux_anat = [modality for modality in anat.keys() if anat[modality]['role'] == 'aux']
+ aux_weighted = [x for x in ['t1w', 't2w'] if x in aux_anat]
+ num_ref = anat[ref_anat]['n']
+ ref_string = ref_anat.capitalize()
desc = f"""
Anatomical data preprocessing
-: A total of {num_t1w} T1-weighted (T1w) images were found within the input
-BIDS dataset."""
+: A total of {num_ref} {ref_string.strip('w')}-weighted \
+({ref_string}) images were found within the input BIDS dataset."""
- have_t1w = 't1w_preproc' in precomputed
- have_t2w = 't2w_preproc' in precomputed
- have_mask = 't1w_mask' in precomputed
- have_dseg = 't1w_dseg' in precomputed
- have_tpms = 't1w_tpms' in precomputed
+ have_ref = anat[ref_anat]['precomputed']
+ have_aux = any(anat[modality]['precomputed'] for modality in aux_anat)
+ have_mask = anat[ref_anat]['have_mask']
+ have_dseg = anat[ref_anat]['have_dseg']
+ have_tpms = anat[ref_anat]['have_tpms']
# Organization
# ------------
@@ -650,13 +646,13 @@ def init_anat_fit_wf(
niu.IdentityInterface(
fields=[
# Primary derivatives
- 't1w_preproc',
- 't2w_preproc',
- 't1w_mask',
- 't1w_dseg',
- 't1w_tpms',
+ 'anat_preproc',
+ 'aux_preproc',
+ 'anat_mask',
+ 'anat_dseg',
+ 'anat_tpms',
'anat2std_xfm',
- 'fsnative2t1w_xfm',
+ 'fsnative2anat_xfm',
# Surface and metric derivatives for fsLR resampling
'white',
'pial',
@@ -674,7 +670,7 @@ def init_anat_fit_wf(
'template',
'subjects_dir',
'subject_id',
- 't1w_valid_list',
+ 'anat_valid_list',
]
),
name='outputnode',
@@ -689,13 +685,13 @@ def init_anat_fit_wf(
)
# Stage 2 results
- t1w_buffer = pe.Node(
- niu.IdentityInterface(fields=['t1w_preproc', 't1w_mask', 't1w_brain', 'ants_seg']),
- name='t1w_buffer',
+ anat_buffer = pe.Node(
+ niu.IdentityInterface(fields=['anat_preproc', 'anat_mask', 'anat_brain', 'ants_seg']),
+ name='anat_buffer',
)
# Stage 3 results
seg_buffer = pe.Node(
- niu.IdentityInterface(fields=['t1w_dseg', 't1w_tpms']),
+ niu.IdentityInterface(fields=['anat_dseg', 'anat_tpms']),
name='seg_buffer',
)
# Stage 4 results: collated template names, forward and reverse transforms
@@ -705,7 +701,7 @@ def init_anat_fit_wf(
# Stage 6 results: Refined stage 2 results; may be direct copy if no refinement
refined_buffer = pe.Node(
- niu.IdentityInterface(fields=['t1w_mask', 't1w_brain']),
+ niu.IdentityInterface(fields=['anat_mask', 'anat_brain']),
name='refined_buffer',
)
@@ -724,13 +720,13 @@ def init_anat_fit_wf(
# fmt:off
workflow.connect([
(seg_buffer, outputnode, [
- ('t1w_dseg', 't1w_dseg'),
- ('t1w_tpms', 't1w_tpms'),
+ ('anat_dseg', 'anat_dseg'),
+ ('anat_tpms', 'anat_tpms'),
]),
(anat2std_buffer, outputnode, [('out', 'anat2std_xfm')]),
(std2anat_buffer, outputnode, [('out', 'std2anat_xfm')]),
(template_buffer, outputnode, [('out', 'template')]),
- (sourcefile_buffer, outputnode, [('source_files', 't1w_valid_list')]),
+ (sourcefile_buffer, outputnode, [('source_files', 'anat_valid_list')]),
(surfaces_buffer, outputnode, [
('white', 'white'),
('pial', 'pial'),
@@ -755,10 +751,10 @@ def init_anat_fit_wf(
# fmt:off
workflow.connect([
(outputnode, anat_reports_wf, [
- ('t1w_valid_list', 'inputnode.source_file'),
- ('t1w_preproc', 'inputnode.t1w_preproc'),
- ('t1w_mask', 'inputnode.t1w_mask'),
- ('t1w_dseg', 'inputnode.t1w_dseg'),
+ ('anat_valid_list', 'inputnode.source_file'),
+ ('anat_preproc', 'inputnode.t1w_preproc'),
+ ('anat_mask', 'inputnode.t1w_mask'),
+ ('anat_dseg', 'inputnode.t1w_dseg'),
('template', 'inputnode.template'),
('anat2std_xfm', 'inputnode.anat2std_xfm'),
('subjects_dir', 'inputnode.subjects_dir'),
@@ -770,29 +766,33 @@ def init_anat_fit_wf(
# Stage 1: Conform images and validate
# If desc-preproc_T1w.nii.gz is provided, just validate it
anat_validate = pe.Node(ValidateImage(), name='anat_validate', run_without_submitting=True)
- if not have_t1w:
+ if not have_ref:
LOGGER.info('ANAT Stage 1: Adding template workflow')
ants_ver = ANTsInfo.version() or '(version unknown)'
desc += f"""\
- {'Each' if num_t1w > 1 else 'The'} T1w image was corrected for intensity
+ {'Each' if num_ref > 1 else 'The'} {ref_string} image was corrected for intensity
non-uniformity (INU) with `N4BiasFieldCorrection` [@n4], distributed with ANTs {ants_ver}
[@ants, RRID:SCR_004757]"""
- desc += '.\n' if num_t1w > 1 else ', and used as T1w-reference throughout the workflow.\n'
+ desc += (
+ '.\n'
+ if num_ref > 1
+ else (f', and used as {ref_string}-reference throughout the workflow.\n')
+ )
anat_template_wf = init_anat_template_wf(
longitudinal=longitudinal,
omp_nthreads=omp_nthreads,
- num_files=num_t1w,
- image_type='T1w',
+ num_files=num_ref,
+ image_type=ref_string,
name='anat_template_wf',
)
ds_template_wf = init_ds_template_wf(
- output_dir=output_dir, num_anat=num_t1w, image_type='T1w'
+ output_dir=output_dir, num_anat=num_ref, image_type=ref_string
)
# fmt:off
workflow.connect([
- (inputnode, anat_template_wf, [('t1w', 'inputnode.anat_files')]),
+ (inputnode, anat_template_wf, [(ref_anat, 'inputnode.anat_files')]),
(anat_template_wf, anat_validate, [('outputnode.anat_ref', 'in_file')]),
(anat_template_wf, sourcefile_buffer, [
('outputnode.anat_valid_list', 'source_files'),
@@ -804,40 +804,40 @@ def init_anat_fit_wf(
('outputnode.anat_realign_xfm', 'inputnode.anat_ref_xfms'),
]),
(sourcefile_buffer, ds_template_wf, [('source_files', 'inputnode.source_files')]),
- (t1w_buffer, ds_template_wf, [('t1w_preproc', 'inputnode.anat_preproc')]),
- (ds_template_wf, outputnode, [('outputnode.anat_preproc', 't1w_preproc')]),
+ (anat_buffer, ds_template_wf, [('anat_preproc', 'inputnode.anat_preproc')]),
+ (ds_template_wf, outputnode, [('outputnode.anat_preproc', 'anat_preproc')]),
])
# fmt:on
else:
- LOGGER.info('ANAT Found preprocessed T1w - skipping Stage 1')
- desc += """ A preprocessed T1w image was provided as a precomputed input
-and used as T1w-reference throughout the workflow.
+ LOGGER.info(f'ANAT Found preprocessed {ref_string} - skipping Stage 1')
+ desc += f""" A preprocessed {ref_string} image was provided as a precomputed input
+and used as {ref_string}-reference throughout the workflow.
"""
- anat_validate.inputs.in_file = precomputed['t1w_preproc']
- sourcefile_buffer.inputs.source_files = [precomputed['t1w_preproc']]
+ anat_validate.inputs.in_file = precomputed[f'{ref_anat}_preproc']
+ sourcefile_buffer.inputs.source_files = [precomputed[f'{ref_anat}_preproc']]
# fmt:off
workflow.connect([
- (anat_validate, t1w_buffer, [('out_file', 't1w_preproc')]),
- (t1w_buffer, outputnode, [('t1w_preproc', 't1w_preproc')]),
+ (anat_validate, anat_buffer, [('out_file', 'anat_preproc')]),
+ (anat_buffer, outputnode, [('anat_preproc', 'anat_preproc')]),
])
# fmt:on
# Stage 2: INU correction and masking
- # We always need to generate t1w_brain; how to do that depends on whether we have
- # a pre-corrected T1w or precomputed mask, or are given an already masked image
+ # We always need to generate anat_brain; how to do that depends on whether we have
+ # a pre-corrected anatomical or precomputed mask, or are given an already masked image
if not have_mask:
LOGGER.info('ANAT Stage 2: Preparing brain extraction workflow')
if skull_strip_mode == 'auto':
- run_skull_strip = not all(_is_skull_stripped(img) for img in t1w)
+ run_skull_strip = not all(_is_skull_stripped(img) for img in anat[ref_anat]['data'])
else:
run_skull_strip = {'force': True, 'skip': False}[skull_strip_mode]
# Brain extraction
if run_skull_strip:
desc += f"""\
-The T1w-reference was then skull-stripped with a *Nipype* implementation of
+The {ref_string}-reference was then skull-stripped with a *Nipype* implementation of
the `antsBrainExtraction.sh` workflow (from ANTs), using {skull_strip_template.fullname}
as target template.
"""
@@ -851,24 +851,24 @@ def init_anat_fit_wf(
# fmt:off
workflow.connect([
(anat_validate, brain_extraction_wf, [('out_file', 'inputnode.in_files')]),
- (brain_extraction_wf, t1w_buffer, [
- ('outputnode.out_mask', 't1w_mask'),
- (('outputnode.out_file', _pop), 't1w_brain'),
+ (brain_extraction_wf, anat_buffer, [
+ ('outputnode.out_mask', 'anat_mask'),
+ (('outputnode.out_file', _pop), 'anat_brain'),
('outputnode.out_segm', 'ants_seg'),
]),
])
- if not have_t1w:
+ if not have_ref:
workflow.connect([
- (brain_extraction_wf, t1w_buffer, [
- (('outputnode.bias_corrected', _pop), 't1w_preproc'),
+ (brain_extraction_wf, anat_buffer, [
+ (('outputnode.bias_corrected', _pop), 'anat_preproc'),
]),
])
# fmt:on
- # Determine mask from T1w and uniformize
- elif not have_t1w:
+ # Determine mask from anatomical and uniformize
+ elif not have_ref:
LOGGER.info('ANAT Stage 2: Skipping skull-strip, INU-correction only')
- desc += """\
-The provided T1w image was previously skull-stripped; a brain mask was
+ desc += f"""\
+The provided {ref_string} image was previously skull-stripped; a brain mask was
derived from the input image.
"""
n4_only_wf = init_n4_only_wf(
@@ -878,10 +878,10 @@ def init_anat_fit_wf(
# fmt:off
workflow.connect([
(anat_validate, n4_only_wf, [('out_file', 'inputnode.in_files')]),
- (n4_only_wf, t1w_buffer, [
- (('outputnode.bias_corrected', _pop), 't1w_preproc'),
- ('outputnode.out_mask', 't1w_mask'),
- (('outputnode.out_file', _pop), 't1w_brain'),
+ (n4_only_wf, anat_buffer, [
+ (('outputnode.bias_corrected', _pop), 'anat_preproc'),
+ ('outputnode.out_mask', 'anat_mask'),
+ (('outputnode.out_file', _pop), 'anat_brain'),
('outputnode.out_segm', 'ants_seg'),
]),
])
@@ -889,30 +889,30 @@ def init_anat_fit_wf(
# Binarize the already uniformized image
else:
LOGGER.info('ANAT Stage 2: Skipping skull-strip, generating mask from input')
- desc += """\
-The provided T1w image was previously skull-stripped; a brain mask was
+ desc += f"""\
+The provided {ref_string} image was previously skull-stripped; a brain mask was
derived from the input image.
"""
binarize = pe.Node(Binarize(thresh_low=2), name='binarize')
# fmt:off
workflow.connect([
(anat_validate, binarize, [('out_file', 'in_file')]),
- (anat_validate, t1w_buffer, [('out_file', 't1w_brain')]),
- (binarize, t1w_buffer, [('out_file', 't1w_mask')]),
+ (anat_validate, anat_buffer, [('out_file', 'anat_brain')]),
+ (binarize, anat_buffer, [('out_file', 'anat_mask')]),
])
# fmt:on
- ds_t1w_mask_wf = init_ds_mask_wf(
+ ds_anat_mask_wf = init_ds_mask_wf(
bids_root=bids_root,
output_dir=output_dir,
mask_type='brain',
- name='ds_t1w_mask_wf',
+ name='ds_anat_mask_wf',
)
# fmt:off
workflow.connect([
- (sourcefile_buffer, ds_t1w_mask_wf, [('source_files', 'inputnode.source_files')]),
- (refined_buffer, ds_t1w_mask_wf, [('t1w_mask', 'inputnode.mask_file')]),
- (ds_t1w_mask_wf, outputnode, [('outputnode.mask_file', 't1w_mask')]),
+ (sourcefile_buffer, ds_anat_mask_wf, [('source_files', 'inputnode.source_files')]),
+ (refined_buffer, ds_anat_mask_wf, [('anat_mask', 'inputnode.mask_file')]),
+ (ds_anat_mask_wf, outputnode, [('outputnode.mask_file', 'anat_mask')]),
])
# fmt:on
else:
@@ -920,12 +920,12 @@ def init_anat_fit_wf(
desc += """\
A pre-computed brain mask was provided as input and used throughout the workflow.
"""
- t1w_buffer.inputs.t1w_mask = precomputed['t1w_mask']
+ anat_buffer.inputs.anat_mask = precomputed[f'{ref_anat}_mask']
# If we have a mask, always apply it
- apply_mask = pe.Node(ApplyMask(in_mask=precomputed['t1w_mask']), name='apply_mask')
+ apply_mask = pe.Node(ApplyMask(in_mask=precomputed[f'{ref_anat}_mask']), name='apply_mask')
workflow.connect([(anat_validate, apply_mask, [('out_file', 'in_file')])])
# Run N4 if it hasn't been pre-run
- if not have_t1w:
+ if not have_ref:
LOGGER.info('ANAT Skipping skull-strip, INU-correction only')
n4_only_wf = init_n4_only_wf(
omp_nthreads=omp_nthreads,
@@ -934,16 +934,16 @@ def init_anat_fit_wf(
# fmt:off
workflow.connect([
(apply_mask, n4_only_wf, [('out_file', 'inputnode.in_files')]),
- (n4_only_wf, t1w_buffer, [
- (('outputnode.bias_corrected', _pop), 't1w_preproc'),
- (('outputnode.out_file', _pop), 't1w_brain'),
+ (n4_only_wf, anat_buffer, [
+ (('outputnode.bias_corrected', _pop), 'anat_preproc'),
+ (('outputnode.out_file', _pop), 'anat_brain'),
]),
])
# fmt:on
else:
LOGGER.info('ANAT Skipping Stage 2')
- workflow.connect([(apply_mask, t1w_buffer, [('out_file', 't1w_brain')])])
- workflow.connect([(refined_buffer, outputnode, [('t1w_mask', 't1w_mask')])])
+ workflow.connect([(apply_mask, anat_buffer, [('out_file', 'anat_brain')])])
+ workflow.connect([(refined_buffer, outputnode, [('anat_mask', 'anat_mask')])])
# Stage 3: Segmentation
if not (have_dseg and have_tpms):
@@ -952,30 +952,30 @@ def init_anat_fit_wf(
desc += f"""\
Brain tissue segmentation of cerebrospinal fluid (CSF),
white-matter (WM) and gray-matter (GM) was performed on
-the brain-extracted T1w using `fast` [FSL {fsl_ver}, RRID:SCR_002823, @fsl_fast].
+the brain-extracted {ref_string} using `fast` [FSL {fsl_ver}, RRID:SCR_002823, @fsl_fast].
"""
fast = pe.Node(
FAST(segments=True, no_bias=True, probability_maps=True, bias_iters=0),
name='fast',
mem_gb=3,
)
- lut_t1w_dseg = pe.Node(niu.Function(function=_apply_bids_lut), name='lut_t1w_dseg')
- lut_t1w_dseg.inputs.lut = (0, 3, 1, 2) # Maps: 0 -> 0, 3 -> 1, 1 -> 2, 2 -> 3.
+ lut_anat_dseg = pe.Node(niu.Function(function=_apply_bids_lut), name='lut_anat_dseg')
+ lut_anat_dseg.inputs.lut = (0, 3, 1, 2) # Maps: 0 -> 0, 3 -> 1, 1 -> 2, 2 -> 3.
fast2bids = pe.Node(
niu.Function(function=_probseg_fast2bids),
name='fast2bids',
run_without_submitting=True,
)
- workflow.connect([(refined_buffer, fast, [('t1w_brain', 'in_files')])])
+ workflow.connect([(refined_buffer, fast, [('anat_brain', 'in_files')])])
# fmt:off
if not have_dseg:
ds_dseg_wf = init_ds_dseg_wf(output_dir=output_dir)
workflow.connect([
- (fast, lut_t1w_dseg, [('partial_volume_map', 'in_dseg')]),
+ (fast, lut_anat_dseg, [('partial_volume_map', 'in_dseg')]),
(sourcefile_buffer, ds_dseg_wf, [('source_files', 'inputnode.source_files')]),
- (lut_t1w_dseg, ds_dseg_wf, [('out', 'inputnode.anat_dseg')]),
- (ds_dseg_wf, seg_buffer, [('outputnode.anat_dseg', 't1w_dseg')]),
+ (lut_anat_dseg, ds_dseg_wf, [('out', 'inputnode.anat_dseg')]),
+ (ds_dseg_wf, seg_buffer, [('outputnode.anat_dseg', 'anat_dseg')]),
])
if not have_tpms:
ds_tpms_wf = init_ds_tpms_wf(output_dir=output_dir)
@@ -983,7 +983,7 @@ def init_anat_fit_wf(
(fast, fast2bids, [('partial_volume_files', 'inlist')]),
(sourcefile_buffer, ds_tpms_wf, [('source_files', 'inputnode.source_files')]),
(fast2bids, ds_tpms_wf, [('out', 'inputnode.anat_tpms')]),
- (ds_tpms_wf, seg_buffer, [('outputnode.anat_tpms', 't1w_tpms')]),
+ (ds_tpms_wf, seg_buffer, [('outputnode.anat_tpms', 'anat_tpms')]),
])
# fmt:on
else:
@@ -991,11 +991,11 @@ def init_anat_fit_wf(
if have_dseg:
LOGGER.info('ANAT Found discrete segmentation')
desc += 'Precomputed discrete tissue segmentations were provided as inputs.\n'
- seg_buffer.inputs.t1w_dseg = precomputed['t1w_dseg']
+ seg_buffer.inputs.anat_dseg = precomputed[f'{ref_anat}_dseg']
if have_tpms:
LOGGER.info('ANAT Found tissue probability maps')
desc += 'Precomputed tissue probabiilty maps were provided as inputs.\n'
- seg_buffer.inputs.t1w_tpms = precomputed['t1w_tpms']
+ seg_buffer.inputs.anat_tpms = precomputed[f'{ref_anat}_tpms']
# Stage 4: Normalization
templates = []
@@ -1019,14 +1019,14 @@ def init_anat_fit_wf(
templates=templates,
)
ds_template_registration_wf = init_ds_template_registration_wf(
- output_dir=output_dir, image_type='T1w'
+ output_dir=output_dir, image_type=ref_string
)
# fmt:off
workflow.connect([
(inputnode, register_template_wf, [('roi', 'inputnode.lesion_mask')]),
- (t1w_buffer, register_template_wf, [('t1w_preproc', 'inputnode.moving_image')]),
- (refined_buffer, register_template_wf, [('t1w_mask', 'inputnode.moving_mask')]),
+ (anat_buffer, register_template_wf, [('anat_preproc', 'inputnode.moving_image')]),
+ (refined_buffer, register_template_wf, [('anat_mask', 'inputnode.moving_mask')]),
(sourcefile_buffer, ds_template_registration_wf, [
('source_files', 'inputnode.source_files')
]),
@@ -1047,9 +1047,9 @@ def init_anat_fit_wf(
if have_mask or not freesurfer:
# fmt:off
workflow.connect([
- (t1w_buffer, refined_buffer, [
- ('t1w_mask', 't1w_mask'),
- ('t1w_brain', 't1w_brain'),
+ (anat_buffer, refined_buffer, [
+ ('anat_mask', 'anat_mask'),
+ ('anat_brain', 'anat_brain'),
]),
])
# fmt:on
@@ -1074,10 +1074,12 @@ def init_anat_fit_wf(
fs_no_resume=fs_no_resume,
precomputed=precomputed,
)
- if t2w or flair:
- t2w_or_flair = 'T2-weighted' if t2w else 'FLAIR'
+ if have_aux:
+ weighted_or_flair = (
+ aux_weighted[0].capitalize().replace('w', '-weighted') if aux_weighted else 'FLAIR'
+ )
surface_recon_wf.__desc__ += f"""\
-A {t2w_or_flair} image was used to improve pial surface refinement.
+A {weighted_or_flair} image was used to improve pial surface refinement.
"""
# fmt:off
@@ -1092,8 +1094,8 @@ def init_anat_fit_wf(
('subject_id', 'inputnode.subject_id'),
]),
(fs_isrunning, surface_recon_wf, [('out', 'inputnode.subjects_dir')]),
- (anat_validate, surface_recon_wf, [('out_file', 'inputnode.t1w')]),
- (t1w_buffer, surface_recon_wf, [('t1w_brain', 'inputnode.skullstripped_t1')]),
+ (anat_validate, surface_recon_wf, [('out_file', f'inputnode.{ref_anat}')]),
+ (anat_buffer, surface_recon_wf, [('anat_brain', 'inputnode.skullstripped_t1')]),
(surface_recon_wf, outputnode, [
('outputnode.subjects_dir', 'subjects_dir'),
('outputnode.subject_id', 'subject_id'),
@@ -1103,7 +1105,9 @@ def init_anat_fit_wf(
fsnative_xfms = precomputed.get('transforms', {}).get('fsnative')
if not fsnative_xfms:
- ds_fs_registration_wf = init_ds_fs_registration_wf(output_dir=output_dir, image_type='T1w')
+ ds_fs_registration_wf = init_ds_fs_registration_wf(
+ output_dir=output_dir, image_type=ref_string
+ )
# fmt:off
workflow.connect([
(sourcefile_buffer, ds_fs_registration_wf, [
@@ -1113,16 +1117,16 @@ def init_anat_fit_wf(
('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2anat_xfm'),
]),
(ds_fs_registration_wf, outputnode, [
- ('outputnode.fsnative2anat_xfm', 'fsnative2t1w_xfm'),
+ ('outputnode.fsnative2anat_xfm', 'fsnative2anat_xfm'),
]),
])
# fmt:on
elif 'reverse' in fsnative_xfms:
- LOGGER.info('ANAT Found fsnative-T1w transform - skipping registration')
- outputnode.inputs.fsnative2t1w_xfm = fsnative_xfms['reverse']
+ LOGGER.info(f'ANAT Found fsnative-{ref_string} transform - skipping registration')
+ outputnode.inputs.fsnative2anat_xfm = fsnative_xfms['reverse']
else:
raise RuntimeError(
- 'Found a T1w-to-fsnative transform without the reverse. Time to handle this.'
+ f'Found a {ref_string}-to-fsnative transform without the reverse. Time to handle this.'
)
if not have_mask:
@@ -1138,31 +1142,32 @@ def init_anat_fit_wf(
('outputnode.subject_id', 'inputnode.subject_id'),
('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2anat_xfm'),
]),
- (t1w_buffer, refinement_wf, [
- ('t1w_preproc', 'inputnode.reference_image'),
+ (anat_buffer, refinement_wf, [
+ ('anat_preproc', 'inputnode.reference_image'),
('ants_seg', 'inputnode.ants_segs'),
]),
- (t1w_buffer, applyrefined, [('t1w_preproc', 'in_file')]),
+ (anat_buffer, applyrefined, [('anat_preproc', 'in_file')]),
(refinement_wf, applyrefined, [('outputnode.out_brainmask', 'mask_file')]),
- (refinement_wf, refined_buffer, [('outputnode.out_brainmask', 't1w_mask')]),
- (applyrefined, refined_buffer, [('out_file', 't1w_brain')]),
+ (refinement_wf, refined_buffer, [('outputnode.out_brainmask', 'anat_mask')]),
+ (applyrefined, refined_buffer, [('out_file', 'anat_brain')]),
])
# fmt:on
else:
LOGGER.info('ANAT Found brain mask - skipping Stage 6')
- if t2w and not have_t2w:
- LOGGER.info('ANAT Stage 7: Creating T2w template')
- t2w_template_wf = init_anat_template_wf(
+ if aux_weighted and not have_aux:
+ aux_str = aux_weighted[0]
+ LOGGER.info('ANAT Stage 7: Creating aux template')
+ aux_template_wf = init_anat_template_wf(
longitudinal=longitudinal,
omp_nthreads=omp_nthreads,
- num_files=len(t2w),
- image_type='T2w',
- name='t2w_template_wf',
+ num_files=anat[aux_str]['n'],
+ image_type=aux_str.capitalize(),
+ name='aux_template_wf',
)
bbreg = pe.Node(
fs.BBRegister(
- contrast_type='t2',
+ contrast_type=aux_str.strip('w'),
init='coreg',
dof=6,
out_lta_file=True,
@@ -1171,45 +1176,47 @@ def init_anat_fit_wf(
name='bbreg',
)
coreg_xfms = pe.Node(niu.Merge(2), name='merge_xfms', run_without_submitting=True)
- t2wtot1w_xfm = pe.Node(ConcatenateXFMs(), name='t2wtot1w_xfm', run_without_submitting=True)
- t2w_resample = pe.Node(
+ auxtoanat_xfm = pe.Node(
+ ConcatenateXFMs(), name='auxtoanat_xfm', run_without_submitting=True
+ )
+ aux_resample = pe.Node(
ApplyTransforms(
dimension=3,
default_value=0,
float=True,
interpolation='LanczosWindowedSinc',
),
- name='t2w_resample',
+ name='aux_resample',
)
- ds_t2w_preproc = pe.Node(
+ ds_aux_preproc = pe.Node(
DerivativesDataSink(base_directory=output_dir, desc='preproc', compress=True),
- name='ds_t2w_preproc',
+ name='ds_aux_preproc',
run_without_submitting=True,
)
- ds_t2w_preproc.inputs.SkullStripped = False
+ ds_aux_preproc.inputs.SkullStripped = False
workflow.connect([
- (inputnode, t2w_template_wf, [('t2w', 'inputnode.anat_files')]),
- (t2w_template_wf, bbreg, [('outputnode.anat_ref', 'source_file')]),
+ (inputnode, aux_template_wf, [(aux_str, 'inputnode.anat_files')]),
+ (aux_template_wf, bbreg, [('outputnode.anat_ref', 'source_file')]),
(surface_recon_wf, bbreg, [
('outputnode.subject_id', 'subject_id'),
('outputnode.subjects_dir', 'subjects_dir'),
]),
(bbreg, coreg_xfms, [('out_lta_file', 'in1')]),
(surface_recon_wf, coreg_xfms, [('outputnode.fsnative2t1w_xfm', 'in2')]),
- (coreg_xfms, t2wtot1w_xfm, [('out', 'in_xfms')]),
- (t2w_template_wf, t2w_resample, [('outputnode.anat_ref', 'input_image')]),
- (t1w_buffer, t2w_resample, [('t1w_preproc', 'reference_image')]),
- (t2wtot1w_xfm, t2w_resample, [('out_xfm', 'transforms')]),
- (inputnode, ds_t2w_preproc, [('t2w', 'source_file')]),
- (t2w_resample, ds_t2w_preproc, [('output_image', 'in_file')]),
- (ds_t2w_preproc, outputnode, [('out_file', 't2w_preproc')]),
+ (coreg_xfms, auxtoanat_xfm, [('out', 'in_xfms')]),
+ (aux_template_wf, aux_resample, [('outputnode.anat_ref', 'input_image')]),
+ (anat_buffer, aux_resample, [('anat_preproc', 'reference_image')]),
+ (auxtoanat_xfm, aux_resample, [('out_xfm', 'transforms')]),
+ (inputnode, ds_aux_preproc, [(aux_str, 'source_file')]),
+ (aux_resample, ds_aux_preproc, [('output_image', 'in_file')]),
+ (ds_aux_preproc, outputnode, [('out_file', 'aux_preproc')]),
]) # fmt:skip
- elif not t2w:
- LOGGER.info('ANAT No T2w images provided - skipping Stage 7')
+ elif not aux_weighted:
+ LOGGER.info('ANAT No auxiliary images provided - skipping Stage 7')
else:
- LOGGER.info('ANAT Found preprocessed T2w - skipping Stage 7')
+ LOGGER.info('ANAT Found preprocessed auxiliary - skipping Stage 7')
# Stages 8-10: Surface conversion and registration
# sphere_reg is needed to generate sphere_reg_fsLR
@@ -1313,8 +1320,8 @@ def init_anat_fit_wf(
)
# fmt:off
workflow.connect([
- (t1w_buffer, anat_ribbon_wf, [
- ('t1w_preproc', 'inputnode.ref_file'),
+ (anat_buffer, anat_ribbon_wf, [
+ ('anat_preproc', 'inputnode.ref_file'),
]),
(surfaces_buffer, anat_ribbon_wf, [
('white', 'inputnode.white'),
@@ -1458,7 +1465,7 @@ def init_anat_template_wf(
name='outputnode',
)
- # 0. Denoise and reorient T1w image(s) to RAS and resample to common voxel space
+ # 0. Denoise and reorient anat image(s) to RAS and resample to common voxel space
anat_ref_dimensions = pe.Node(TemplateDimensions(), name='anat_ref_dimensions')
denoise = pe.MapNode(
DenoiseImage(noise_model='Rician', num_threads=omp_nthreads),
@@ -1501,11 +1508,11 @@ def init_anat_template_wf(
name='anat_conform_xfm',
)
- # 1. Template (only if several T1w images)
+ # 1. Template (only if several anat images)
# 1a. Correct for bias field: the bias field is an additive factor
# in log-transformed intensity units. Therefore, it is not a linear
# combination of fields and N4 fails with merged images.
- # 1b. Align and merge if several T1w images are provided
+ # 1b. Align and merge if several anat images are provided
n4_correct = pe.MapNode(
N4BiasFieldCorrection(dimension=3, copy_header=True),
iterfield='input_image',
@@ -1645,7 +1652,7 @@ def _probseg_fast2bids(inlist):
def _is_skull_stripped(img):
- """Check if T1w images are skull-stripped."""
+ """Check if anat images are skull-stripped."""
import nibabel as nb
import numpy as np
diff --git a/src/smriprep/workflows/base.py b/src/smriprep/workflows/base.py
index 730fe1f656..cc22ae21e8 100644
--- a/src/smriprep/workflows/base.py
+++ b/src/smriprep/workflows/base.py
@@ -36,6 +36,7 @@
from ..__about__ import __version__
from ..interfaces import DerivativesDataSink
+from ..utils.misc import collect_anat
from .anatomical import init_anat_preproc_wf
@@ -430,10 +431,8 @@ def init_single_subject_wf(
fs_no_resume=fs_no_resume,
longitudinal=longitudinal,
msm_sulc=msm_sulc,
+ anat=collect_anat(subject_data, deriv_cache, 'T1w'),
name='anat_preproc_wf',
- t1w=subject_data['t1w'],
- t2w=subject_data['t2w'],
- flair=subject_data['flair'],
omp_nthreads=omp_nthreads,
output_dir=output_dir,
skull_strip_fixed_seed=skull_strip_fixed_seed,
diff --git a/src/smriprep/workflows/tests/test_anatomical.py b/src/smriprep/workflows/tests/test_anatomical.py
index d34cd5de7e..24ab82b6a1 100644
--- a/src/smriprep/workflows/tests/test_anatomical.py
+++ b/src/smriprep/workflows/tests/test_anatomical.py
@@ -4,9 +4,11 @@
import numpy as np
import pytest
from nipype.pipeline.engine.utils import generate_expanded_graph
+from niworkflows.utils.bids import collect_data
from niworkflows.utils.spaces import Reference, SpatialReferences
from niworkflows.utils.testing import generate_bids_skeleton
+from ...utils.misc import collect_anat
from ..anatomical import init_anat_fit_wf, init_anat_preproc_wf
BASE_LAYOUT = {
@@ -80,8 +82,7 @@ def test_init_anat_preproc_wf(
hires=False,
longitudinal=False,
msm_sulc=False,
- t1w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T1w.nii.gz')],
- t2w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T2w.nii.gz')],
+ anat=collect_anat(collect_data(bids_root, '01')[0], {}, 'T1w'),
skull_strip_mode='force',
skull_strip_template=Reference('OASIS30ANTs'),
spaces=SpatialReferences(
@@ -112,8 +113,7 @@ def test_anat_fit_wf(
hires=False,
longitudinal=False,
msm_sulc=msm_sulc,
- t1w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T1w.nii.gz')],
- t2w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T2w.nii.gz')],
+ anat=collect_anat(collect_data(bids_root, '01')[0], {}, 'T1w'),
skull_strip_mode=skull_strip_mode,
skull_strip_template=Reference('OASIS30ANTs'),
spaces=SpatialReferences(
@@ -154,13 +154,6 @@ def test_anat_fit_precomputes(
output_dir = tmp_path / 'output'
output_dir.mkdir()
- # Construct inputs
- t1w_list = [
- str(bids_root / 'sub-01' / 'anat' / 'sub-01_run-1_T1w.nii.gz'),
- str(bids_root / 'sub-01' / 'anat' / 'sub-01_run-2_T1w.nii.gz'),
- ][:t1w]
- t2w_list = [str(bids_root / 'sub-01' / 'anat' / 'sub-01_T2w.nii.gz')][:t2w]
-
# Construct precomputed files
empty_img = nb.Nifti1Image(np.zeros((1, 1, 1)), np.eye(4))
precomputed = {}
@@ -200,6 +193,7 @@ def test_anat_fit_precomputes(
for path in xfm.values():
Path(path).touch()
+ anat = collect_anat(collect_data(bids_root, '01')[0], precomputed, 'T1w')
# Create workflow
wf = init_anat_fit_wf(
bids_root=str(bids_root),
@@ -208,8 +202,7 @@ def test_anat_fit_precomputes(
hires=False,
longitudinal=False,
msm_sulc=True,
- t1w=t1w_list,
- t2w=t2w_list,
+ anat=anat,
skull_strip_mode=skull_strip_mode,
skull_strip_template=Reference('OASIS30ANTs'),
spaces=SpatialReferences(