Skip to content

Commit 9f37e54

Browse files
committed
use BIDSLayout to query tags indicative of B1-unwarping correction, raise if inconsistent within contrast
1 parent 6501da1 commit 9f37e54

File tree

4 files changed

+93
-5
lines changed

4 files changed

+93
-5
lines changed

Diff for: smriprep/cli/run.py

+6
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def get_parser():
173173
action='store_true',
174174
help='treat dataset as longitudinal - may increase runtime',
175175
)
176+
g_conf.add_argument(
177+
'--gradunwarp-file',
178+
metavar='PATH',
179+
type=Path,
180+
help='Path to vendor file for gradunwarp gradient distortion ' 'correction.',
181+
)
176182

177183
# ANTs options
178184
g_ants = parser.add_argument_group('Specific options for ANTs registrations')

Diff for: smriprep/workflows/anatomical.py

+64-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import typing as ty
2626

27+
import bids
2728
from nipype import logging
2829
from nipype.interfaces import (
2930
freesurfer as fs,
@@ -47,12 +48,14 @@
4748
from niworkflows.interfaces.freesurfer import (
4849
StructuralReference,
4950
)
51+
from niworkflows.interfaces.gradunwarp import GradUnwarp
5052
from niworkflows.interfaces.header import ValidateImage
5153
from niworkflows.interfaces.images import Conform, TemplateDimensions
5254
from niworkflows.interfaces.nibabel import ApplyMask, Binarize
5355
from niworkflows.interfaces.nitransforms import ConcatenateXFMs
5456
from niworkflows.utils.misc import add_suffix
5557
from niworkflows.utils.spaces import Reference, SpatialReferences
58+
from niworkflows.workflows.gradunwarp import init_gradunwarp_wf
5659

5760
from ..data import load_resource
5861
from ..interfaces import DerivativesDataSink
@@ -94,6 +97,7 @@
9497
def init_anat_preproc_wf(
9598
*,
9699
bids_root: str,
100+
layout: bids.BIDSLayout,
97101
output_dir: str,
98102
freesurfer: bool,
99103
hires: bool,
@@ -113,6 +117,7 @@ def init_anat_preproc_wf(
113117
name: str = 'anat_preproc_wf',
114118
skull_strip_fixed_seed: bool = False,
115119
fs_no_resume: bool = False,
120+
gradunwarp_file: str | None = None,
116121
):
117122
"""
118123
Stage the anatomical preprocessing steps of *sMRIPrep*.
@@ -150,6 +155,8 @@ def init_anat_preproc_wf(
150155
----------
151156
bids_root : :obj:`str`
152157
Path of the input BIDS dataset root
158+
layout : BIDSLayout object
159+
BIDS dataset layout
153160
output_dir : :obj:`str`
154161
Directory in which to save derivatives
155162
freesurfer : :obj:`bool`
@@ -189,6 +196,8 @@ def init_anat_preproc_wf(
189196
EXPERT: Import pre-computed FreeSurfer reconstruction without resuming.
190197
The user is responsible for ensuring that all necessary files are present.
191198
(default: ``False``).
199+
gradunwarp_file : :obj:`str`, optional
200+
Gradient unwarping filename (default: None)
192201
193202
Inputs
194203
------
@@ -265,6 +274,7 @@ def init_anat_preproc_wf(
265274

266275
anat_fit_wf = init_anat_fit_wf(
267276
bids_root=bids_root,
277+
layout=layout,
268278
output_dir=output_dir,
269279
freesurfer=freesurfer,
270280
hires=hires,
@@ -282,6 +292,7 @@ def init_anat_preproc_wf(
282292
omp_nthreads=omp_nthreads,
283293
skull_strip_fixed_seed=skull_strip_fixed_seed,
284294
fs_no_resume=fs_no_resume,
295+
gradunwarp_file=gradunwarp_file,
285296
)
286297
template_iterator_wf = init_template_iterator_wf(spaces=spaces, sloppy=sloppy)
287298
ds_std_volumes_wf = init_ds_anat_volumes_wf(
@@ -448,6 +459,7 @@ def init_anat_preproc_wf(
448459
def init_anat_fit_wf(
449460
*,
450461
bids_root: str,
462+
layout: bids.BIDSLayout,
451463
output_dir: str,
452464
freesurfer: bool,
453465
hires: bool,
@@ -466,6 +478,7 @@ def init_anat_fit_wf(
466478
name='anat_fit_wf',
467479
skull_strip_fixed_seed: bool = False,
468480
fs_no_resume: bool = False,
481+
gradunwarp_file: str | None = None,
469482
):
470483
"""
471484
Stage the anatomical preprocessing steps of *sMRIPrep*.
@@ -511,6 +524,8 @@ def init_anat_fit_wf(
511524
----------
512525
bids_root : :obj:`str`
513526
Path of the input BIDS dataset root
527+
layout : BIDSLayout object
528+
BIDS dataset layout
514529
output_dir : :obj:`str`
515530
Directory in which to save derivatives
516531
freesurfer : :obj:`bool`
@@ -546,6 +561,12 @@ def init_anat_fit_wf(
546561
Do not use a random seed for skull-stripping - will ensure
547562
run-to-run replicability when used with --omp-nthreads 1
548563
(default: ``False``).
564+
fs_no_resume : bool
565+
EXPERT: Import pre-computed FreeSurfer reconstruction without resuming.
566+
The user is responsible for ensuring that all necessary files are present.
567+
(default: ``False``).
568+
gradunwarp_file : :obj:`str`, optional
569+
Gradient unwarping filename (default: None)
549570
550571
Inputs
551572
------
@@ -760,12 +781,14 @@ def init_anat_fit_wf(
760781
non-uniformity (INU) with `N4BiasFieldCorrection` [@n4], distributed with ANTs {ants_ver}
761782
[@ants, RRID:SCR_004757]"""
762783
desc += '.\n' if num_t1w > 1 else ', and used as T1w-reference throughout the workflow.\n'
763-
784+
t1w_metas = [layout.get_file(t).get_metadata() for t in t1w]
764785
anat_template_wf = init_anat_template_wf(
765786
longitudinal=longitudinal,
766787
omp_nthreads=omp_nthreads,
767788
num_files=num_t1w,
768789
contrast='T1w',
790+
gradunwarp_file=gradunwarp_file,
791+
metadata=t1w_metas,
769792
name='anat_template_wf',
770793
)
771794
ds_template_wf = init_ds_template_wf(output_dir=output_dir, num_t1w=num_t1w)
@@ -1131,11 +1154,14 @@ def init_anat_fit_wf(
11311154

11321155
if t2w and not have_t2w:
11331156
LOGGER.info('ANAT Stage 7: Creating T2w template')
1157+
t2w_metas = [layout.get_file(t).get_metadata() for t in t1w]
11341158
t2w_template_wf = init_anat_template_wf(
11351159
longitudinal=longitudinal,
11361160
omp_nthreads=omp_nthreads,
11371161
num_files=len(t2w),
11381162
contrast='T2w',
1163+
metadata=t2w_metas,
1164+
gradunwarp_file=gradunwarp_file,
11391165
name='t2w_template_wf',
11401166
)
11411167
bbreg = pe.Node(
@@ -1376,6 +1402,8 @@ def init_anat_template_wf(
13761402
omp_nthreads: int,
13771403
num_files: int,
13781404
contrast: str,
1405+
metadata: dict,
1406+
gradunwarp_file: str | None = None,
13791407
name: str = 'anat_template_wf',
13801408
):
13811409
"""
@@ -1388,7 +1416,8 @@ def init_anat_template_wf(
13881416
13891417
from smriprep.workflows.anatomical import init_anat_template_wf
13901418
wf = init_anat_template_wf(
1391-
longitudinal=False, omp_nthreads=1, num_files=1, contrast="T1w"
1419+
longitudinal=False, omp_nthreads=1, num_files=1, contrast="T1w",
1420+
gradunwarp_file=None,
13921421
)
13931422
13941423
Parameters
@@ -1402,6 +1431,8 @@ def init_anat_template_wf(
14021431
Number of images
14031432
contrast : :obj:`str`
14041433
Name of contrast, for reporting purposes, e.g., T1w, T2w, PDw
1434+
gradunwarp_file : :obj:`str`, optional
1435+
Gradient unwarping filename (default: None)
14051436
name : :obj:`str`, optional
14061437
Workflow name (default: anat_template_wf)
14071438
@@ -1449,9 +1480,39 @@ def init_anat_template_wf(
14491480
)
14501481
anat_conform = pe.MapNode(Conform(), iterfield='in_file', name='anat_conform')
14511482

1483+
1484+
# -1 Gradient unwarping (optional)
1485+
if gradunwarp_file:
1486+
nds = [
1487+
(meta.get('NonlinearGradientCorrection', None) or
1488+
'ND' in meta.get('ImageType', []) or False)
1489+
for meta in metadata]
1490+
if any(nds) and not all(nds):
1491+
raise RuntimeError(f"Inconsistent distortion correction metadata across {contrast} images.")
1492+
if not any(nds):
1493+
gradunwarp_file = None
1494+
if gradunwarp_file:
1495+
gradunwarp_ver = GradUnwarp.version()
1496+
workflow.__desc__ += f"""\
1497+
{"Each" if num_files > 1 else "The"} {contrast} image was corrected for gradient
1498+
non-linearity with `gradunwarp` [@gradunwarp] {gradunwarp_ver} [@gradunwarp]\n"""
1499+
gradunwarp_wf = init_gradunwarp_wf('gradunward_T1w')
1500+
gradunwarp_wf.inputs.inputnode.grad_file = gradunwarp_file
1501+
# fmt:off
1502+
workflow.connect([
1503+
(inputnode, gradunwarp_wf, [('anat_files', 'inputnode.input_file')]),
1504+
(gradunwarp_wf, anat_ref_dimensions, [('outputnode.corrected_file', 't1w_list')]),
1505+
])
1506+
else:
1507+
workflow.connect(
1508+
[
1509+
(inputnode, anat_ref_dimensions, [('anat_files', 't1w_list')]),
1510+
]
1511+
)
1512+
# fmt:on
1513+
14521514
# fmt:off
14531515
workflow.connect([
1454-
(inputnode, anat_ref_dimensions, [('anat_files', 't1w_list')]),
14551516
(anat_ref_dimensions, denoise, [('t1w_valid_list', 'input_image')]),
14561517
(anat_ref_dimensions, anat_conform, [
14571518
('target_zooms', 'target_zooms'),

Diff for: smriprep/workflows/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ def init_single_subject_wf(
422422
# Preprocessing of T1w (includes registration to MNI)
423423
anat_preproc_wf = init_anat_preproc_wf(
424424
bids_root=layout.root,
425+
layout=layout,
425426
sloppy=sloppy,
426427
debug=debug,
427428
precomputed=deriv_cache,

Diff for: smriprep/workflows/tests/test_anatomical.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22

33
import nibabel as nb
44
import numpy as np
5+
import bids
56
import pytest
67
from nipype.pipeline.engine.utils import generate_expanded_graph
78
from niworkflows.utils.spaces import Reference, SpatialReferences
89
from niworkflows.utils.testing import generate_bids_skeleton
10+
from niworkflows.interfaces import gradunwarp
911

1012
from ..anatomical import init_anat_fit_wf, init_anat_preproc_wf
1113

14+
gradunwarp_file_params = [None]
15+
if gradunwarp.GradUnwarp.version():
16+
from gradunwarp.core.tests.test_regression import siemens_gradfile
17+
gradunwarp_file_params.append(siemens_gradfile)
18+
1219
BASE_LAYOUT = {
1320
'01': {
1421
'anat': [
@@ -73,14 +80,17 @@ def test_init_anat_preproc_wf(
7380
output_dir = tmp_path / 'output'
7481
output_dir.mkdir()
7582

83+
bids_layout = bids.BIDSLayout(bids_root)
84+
7685
init_anat_preproc_wf(
7786
bids_root=str(bids_root),
87+
layout=bids_layout,
7888
output_dir=str(output_dir),
7989
freesurfer=freesurfer,
8090
hires=False,
8191
longitudinal=False,
8292
msm_sulc=False,
83-
t1w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T1w.nii.gz')],
93+
t1w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_run-1_T1w.nii.gz')],
8494
t2w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T2w.nii.gz')],
8595
skull_strip_mode='force',
8696
skull_strip_template=Reference('OASIS30ANTs'),
@@ -96,23 +106,28 @@ def test_init_anat_preproc_wf(
96106

97107
@pytest.mark.parametrize('msm_sulc', [True, False])
98108
@pytest.mark.parametrize('skull_strip_mode', ['skip', 'force'])
109+
@pytest.mark.parametrize('gradunwarp_file', gradunwarp_file_params)
99110
def test_anat_fit_wf(
100111
bids_root: Path,
101112
tmp_path: Path,
102113
msm_sulc: bool,
103114
skull_strip_mode: str,
115+
gradunwarp_file: str,
104116
):
105117
output_dir = tmp_path / 'output'
106118
output_dir.mkdir()
107119

120+
bids_layout = bids.BIDSLayout(bids_root)
121+
108122
init_anat_fit_wf(
109123
bids_root=str(bids_root),
124+
layout=bids_layout,
110125
output_dir=str(output_dir),
111126
freesurfer=True,
112127
hires=False,
113128
longitudinal=False,
114129
msm_sulc=msm_sulc,
115-
t1w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T1w.nii.gz')],
130+
t1w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_run-1_T1w.nii.gz')],
116131
t2w=[str(bids_root / 'sub-01' / 'anat' / 'sub-01_T2w.nii.gz')],
117132
skull_strip_mode=skull_strip_mode,
118133
skull_strip_template=Reference('OASIS30ANTs'),
@@ -122,6 +137,7 @@ def test_anat_fit_wf(
122137
),
123138
precomputed={},
124139
omp_nthreads=1,
140+
gradunwarp_file=gradunwarp_file,
125141
)
126142

127143

@@ -200,9 +216,13 @@ def test_anat_fit_precomputes(
200216
for path in xfm.values():
201217
Path(path).touch()
202218

219+
220+
bids_layout = bids.BIDSLayout(bids_root)
221+
203222
# Create workflow
204223
wf = init_anat_fit_wf(
205224
bids_root=str(bids_root),
225+
layout=bids_layout,
206226
output_dir=str(output_dir),
207227
freesurfer=True,
208228
hires=False,

0 commit comments

Comments
 (0)