Skip to content

Commit 618be5b

Browse files
committed
fix: final revision of the base workflow, preparing for new API
1 parent b3db08a commit 618be5b

File tree

6 files changed

+391
-329
lines changed

6 files changed

+391
-329
lines changed

sdcflows/interfaces/utils.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
Utilities.
5+
6+
.. testsetup::
7+
8+
>>> tmpdir = getfixture('tmpdir')
9+
>>> tmp = tmpdir.chdir() # changing to a temporary directory
10+
>>> nb.Nifti1Image(np.zeros((90, 90, 60)), None, None).to_filename(
11+
... tmpdir.join('epi.nii.gz').strpath)
12+
13+
"""
14+
15+
from nipype import logging
16+
from nipype.interfaces.base import (
17+
BaseInterfaceInputSpec,
18+
TraitedSpec,
19+
File,
20+
traits,
21+
SimpleInterface,
22+
InputMultiObject,
23+
OutputMultiObject,
24+
)
25+
26+
LOGGER = logging.getLogger("nipype.interface")
27+
28+
29+
class _FlattenInputSpec(BaseInterfaceInputSpec):
30+
in_data = InputMultiObject(
31+
File(exists=True), mandatory=True, desc="list of input data",
32+
)
33+
in_meta = InputMultiObject(
34+
traits.DictStrAny, mandatory=True, desc="list of metadata",
35+
)
36+
max_trs = traits.Int(50, usedefault=True, desc="only pick first TRs")
37+
38+
39+
class _FlattenOutputSpec(TraitedSpec):
40+
out_list = OutputMultiObject(
41+
traits.Tuple(File(exists=True), traits.DictStrAny,), desc="list of output files"
42+
)
43+
out_data = OutputMultiObject(File(exists=True))
44+
out_meta = OutputMultiObject(traits.DictStrAny)
45+
46+
47+
class Flatten(SimpleInterface):
48+
"""Flatten a list of 3D and 4D files (and metadata)."""
49+
50+
input_spec = _FlattenInputSpec
51+
output_spec = _FlattenOutputSpec
52+
53+
def _run_interface(self, runtime):
54+
self._results["out_list"] = _flatten(
55+
zip(self.inputs.inlist, self.inputs.in_meta),
56+
max_trs=self.inputs.max_trs,
57+
out_dir=runtime.cwd,
58+
)
59+
60+
# Unzip out_data, out_meta outputs.
61+
self._results["out_data"], self._results["out_meta"] = zip(
62+
*self._results["out_list"]
63+
)
64+
return runtime
65+
66+
67+
def _flatten(inlist, max_trs=50, out_dir=None):
68+
"""
69+
Split the input EPIs and generate a flattened list with corresponding metadata.
70+
71+
Inputs
72+
------
73+
inlist : :obj:`list` of :obj:`tuple`
74+
List of pairs (filepath, metadata)
75+
max_trs : int
76+
Index of frame after which all volumes will be discarded
77+
from the input EPI images.
78+
79+
"""
80+
from pathlib import Path
81+
import nibabel as nb
82+
83+
out_dir = Path(out_dir) if out_dir is not None else Path()
84+
85+
output = []
86+
for i, (path, meta) in enumerate(inlist):
87+
img = nb.load(path)
88+
if len(img.shape) == 3:
89+
output.append((path, meta))
90+
else:
91+
splitnii = nb.four_to_three(img.slicer[:, :, :, :max_trs])
92+
stem = Path(path).name.rpartition(".nii")[0]
93+
94+
for j, nii in enumerate(splitnii):
95+
out_name = (out_dir / f"{stem}_idx-{j:03}.nii.gz").absolute()
96+
nii.to_filename(out_name)
97+
output.append((str(out_name), meta))
98+
99+
return output

sdcflows/models/fieldmap.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
3838

3939

40-
def init_fmap_wf(omp_nthreads, mode="phase-diff", name="fmap_wf"):
40+
def init_fmap_wf(omp_nthreads, mode="phasediff", name="fmap_wf"):
4141
"""
4242
Estimate the fieldmap based on a field-mapping MRI acquisition.
4343
@@ -111,7 +111,7 @@ def init_fmap_wf(omp_nthreads, mode="phase-diff", name="fmap_wf"):
111111
])
112112
# fmt: on
113113

114-
if mode == "phase-diff":
114+
if mode == "phasediff":
115115
workflow.__desc__ = """\
116116
A *B<sub>0</sub>* nonuniformity map (or *fieldmap*) was estimated from the
117117
phase-drift map(s) measure with two consecutive GRE (gradient-recall echo)

sdcflows/models/pepolar.py

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
5353
5454
Outputs
5555
-------
56-
fieldmap : :obj:`str`
56+
fmap : :obj:`str`
5757
The path of the estimated fieldmap.
58-
corrected : :obj:`str`
58+
fmap_ref : :obj:`str`
5959
The path of an unwarped conversion of files in ``in_data``.
6060
6161
"""
@@ -74,8 +74,8 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
7474
outputnode = pe.Node(
7575
niu.IdentityInterface(
7676
fields=[
77-
"corrected",
78-
"fieldmap",
77+
"fmap_ref",
78+
"fmap",
7979
"coefficients",
8080
"jacobians",
8181
"xfms",
@@ -87,7 +87,9 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
8787

8888
concat_blips = pe.Node(MergeSeries(), name="concat_blips")
8989
readout_time = pe.MapNode(
90-
GetReadoutTime(), name="readout_time", iterfield=["metadata", "in_file"],
90+
GetReadoutTime(),
91+
name="readout_time",
92+
iterfield=["metadata", "in_file"],
9193
run_without_submitting=True,
9294
)
9395

@@ -106,8 +108,8 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
106108
(inputnode, topup, [(("metadata", _pe2fsl), "encoding_direction")]),
107109
(readout_time, topup, [("readout_time", "readout_times")]),
108110
(concat_blips, topup, [("out_file", "in_file")]),
109-
(topup, outputnode, [("out_corrected", "corrected"),
110-
("out_field", "fieldmap"),
111+
(topup, outputnode, [("out_corrected", "fmap_ref"),
112+
("out_field", "fmap"),
111113
("out_fieldcoef", "coefficients"),
112114
("out_jacs", "jacobians"),
113115
("out_mats", "xfms"),
@@ -118,7 +120,7 @@ def init_topup_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
118120
return workflow
119121

120122

121-
def init_3dQwarp_wf(pe_dir, omp_nthreads=1, name="pepolar_estimate_wf"):
123+
def init_3dQwarp_wf(omp_nthreads=1, name="pepolar_estimate_wf"):
122124
"""
123125
Create the PEPOLAR field estimation workflow based on AFNI's ``3dQwarp``.
124126
@@ -132,12 +134,10 @@ def init_3dQwarp_wf(pe_dir, omp_nthreads=1, name="pepolar_estimate_wf"):
132134
:simple_form: yes
133135
134136
from sdcflows.models.pepolar import init_3dQwarp_wf
135-
wf = init_3dQwarp_wf(pe_dir="j")
137+
wf = init_3dQwarp_wf()
136138
137139
Parameters
138140
----------
139-
pe_dir : :obj:`str`
140-
PE direction (BIDS compatible)
141141
name : :obj:`str`
142142
Name for this workflow
143143
omp_nthreads : :obj:`int`
@@ -150,34 +150,66 @@ def init_3dQwarp_wf(pe_dir, omp_nthreads=1, name="pepolar_estimate_wf"):
150150
151151
Outputs
152152
-------
153-
fieldmap : :obj:`str`
153+
fmap : :obj:`str`
154154
The path of the estimated fieldmap.
155-
corrected : :obj:`str`
155+
fmap_ref : :obj:`str`
156156
The path of an unwarped conversion of the first element of ``in_data``.
157157
158158
"""
159159
from nipype.interfaces import afni
160160
from niworkflows.interfaces import CopyHeader
161-
from niworkflows.interfaces.registration import ANTSApplyTransformsRPT
161+
from niworkflows.interfaces.fixes import (
162+
FixHeaderRegistration as Registration,
163+
FixHeaderApplyTransforms as ApplyTransforms,
164+
)
165+
from niworkflows.interfaces.freesurfer import StructuralReference
166+
from niworkflows.func.util import init_enhance_and_skullstrip_bold_wf
167+
from ..interfaces.utils import Flatten
162168

163169
workflow = Workflow(name=name)
164170
workflow.__desc__ = f"""{_PEPOLAR_DESC} \
165171
with `3dQwarp` @afni (AFNI {''.join(['%02d' % v for v in afni.Info().version() or []])}).
166172
"""
167173

168-
inputnode = pe.Node(niu.IdentityInterface(fields=["in_data"]), name="inputnode")
174+
inputnode = pe.Node(niu.IdentityInterface(fields=["in_data", "metadata"]),
175+
name="inputnode")
169176

170177
outputnode = pe.Node(
171-
niu.IdentityInterface(fields=["fieldmap", "corrected"]), name="outputnode"
178+
niu.IdentityInterface(fields=["fmap", "fmap_ref"]), name="outputnode"
179+
)
180+
181+
flatten = pe.Node(Flatten(), name="flatten")
182+
sort_pe = pe.Node(niu.Function(
183+
function=_sorted_pe, output_names=["sorted", "qwarp_args"]),
184+
name="sort_pe", run_without_submitting=True)
185+
186+
merge_pes = pe.MapNode(StructuralReference(
187+
auto_detect_sensitivity=True,
188+
initial_timepoint=1,
189+
fixed_timepoint=True, # Align to first image
190+
intensity_scaling=True,
191+
# 7-DOF (rigid + intensity)
192+
no_iteration=True,
193+
subsample_threshold=200,
194+
out_file='template.nii.gz'),
195+
name='merge_pes',
196+
iterfield=["in_files"],
197+
)
198+
199+
pe0_wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=omp_nthreads, name="pe0_wf")
200+
pe1_wf = init_enhance_and_skullstrip_bold_wf(omp_nthreads=omp_nthreads, name="pe1_wf")
201+
202+
align_pes = pe.Node(
203+
Registration(
204+
from_file=_pkg_fname("sdcflows", "data/translation_rigid.json"),
205+
output_warped_image=True,
206+
),
207+
name="align_pes",
208+
n_procs=omp_nthreads,
172209
)
173210

174211
qwarp = pe.Node(
175212
afni.QwarpPlusMinus(
176-
args={
177-
"i": "-noYdis -noZdis",
178-
"j": "-noXdis -noZdis",
179-
"k": "-noXdis -noYdis",
180-
}[pe_dir[0]],
181213
blur=[-1, -1],
182214
environ={"OMP_NUM_THREADS": f"{omp_nthreads}"},
183215
minpatch=9,
@@ -194,9 +226,8 @@ def init_3dQwarp_wf(pe_dir, omp_nthreads=1, name="pepolar_estimate_wf"):
194226
cphdr_warp = pe.Node(CopyHeader(), name="cphdr_warp", mem_gb=0.01)
195227

196228
unwarp_reference = pe.Node(
197-
ANTSApplyTransformsRPT(
229+
ApplyTransforms(
198230
dimension=3,
199-
generate_report=False,
200231
float=True,
201232
interpolation="LanczosWindowedSinc",
202233
),
@@ -205,16 +236,25 @@ def init_3dQwarp_wf(pe_dir, omp_nthreads=1, name="pepolar_estimate_wf"):
205236

206237
# fmt: off
207238
workflow.connect([
208-
(inputnode, qwarp, [(("in_data", _front), "in_file"),
209-
(("in_data", _last), "base_file")]),
239+
(inputnode, flatten, [("in_data", "in_data"),
240+
("metadata", "in_meta")]),
241+
(flatten, sort_pe, [("out_list", "inlist")]),
242+
(sort_pe, qwarp, [("qwarp_args", "args")]),
243+
(sort_pe, merge_pes, [("sorted", "in_files")]),
244+
(merge_pes, pe0_wf, [(("out_file", _front), "inputnode.in_file")]),
245+
(merge_pes, pe1_wf, [(("out_file", _last), "inputnode.in_file")]),
246+
(pe0_wf, align_pes, [("outputnode.skull_stripped_file", "fixed_image")]),
247+
(pe1_wf, align_pes, [("outputnode.skull_stripped_file", "moving_image")]),
248+
(pe0_wf, qwarp, [("outputnode.skull_stripped_file", "in_file")]),
249+
(align_pes, qwarp, [("warped_image", "base_file")]),
210250
(inputnode, cphdr_warp, [(("in_data", _front), "hdr_file")]),
211251
(qwarp, cphdr_warp, [("source_warp", "in_file")]),
212252
(cphdr_warp, to_ants, [("out_file", "in_file")]),
213253
(to_ants, unwarp_reference, [("out", "transforms")]),
214254
(inputnode, unwarp_reference, [("in_reference", "reference_image"),
215255
("in_reference", "input_image")]),
216-
(unwarp_reference, outputnode, [("output_image", "corrected")]),
217-
(to_ants, outputnode, [("out", "fieldmap")]),
256+
(unwarp_reference, outputnode, [("output_image", "fmap_ref")]),
257+
(to_ants, outputnode, [("out", "fmap")]),
218258
])
219259
# fmt: on
220260
return workflow
@@ -238,11 +278,35 @@ def _fix_hdr(in_file, newpath=None):
238278
def _pe2fsl(metadata):
239279
"""Convert ijk notation to xyz."""
240280
return [
241-
m["PhaseEncodingDirection"].replace("i", "x").replace("j", "y").replace("k", "z")
281+
m["PhaseEncodingDirection"]
282+
.replace("i", "x")
283+
.replace("j", "y")
284+
.replace("k", "z")
242285
for m in metadata
243286
]
244287

245288

289+
def _sorted_pe(inlist):
290+
out_ref = [inlist[0][0]]
291+
out_opp = []
292+
293+
ref_pe = inlist[0][1]["PhaseEncodingDirection"]
294+
for d, m in inlist[1:]:
295+
pe = m["PhaseEncodingDirection"]
296+
if pe == ref_pe:
297+
out_ref.append(d)
298+
elif pe[0] == ref_pe[0]:
299+
out_opp.append(d)
300+
else:
301+
raise ValueError("Cannot handle orthogonal PE encodings.")
302+
303+
return [out_ref, out_opp], {
304+
"i": "-noYdis -noZdis",
305+
"j": "-noXdis -noZdis",
306+
"k": "-noXdis -noYdis",
307+
}[ref_pe[0]]
308+
309+
246310
def _front(inlist):
247311
if isinstance(inlist, (list, tuple)):
248312
return inlist[0]

sdcflows/models/syn.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,11 @@ def init_syn_sdc_wf(omp_nthreads, epi_pe=None, atlas_threshold=3, name="syn_sdc_
8181
8282
Outputs
8383
-------
84-
out_reference
85-
the ``in_reference`` image after unwarping
86-
out_reference_brain
87-
the ``in_reference_brain`` image after unwarping
88-
out_warp
84+
fmap
8985
the corresponding :abbr:`DFM (displacements field map)` compatible with
9086
ANTs
91-
out_mask
92-
mask of the unwarped input file
87+
fmap_ref
88+
the ``in_reference`` image after unwarping
9389
9490
References
9591
----------
@@ -135,10 +131,7 @@ def init_syn_sdc_wf(omp_nthreads, epi_pe=None, atlas_threshold=3, name="syn_sdc_
135131
name="inputnode",
136132
)
137133
outputnode = pe.Node(
138-
niu.IdentityInterface(
139-
["out_reference", "out_reference_brain", "out_mask", "out_warp"]
140-
),
141-
name="outputnode",
134+
niu.IdentityInterface(["fmap", "fmap_ref", "fmap_mask"]), name="outputnode",
142135
)
143136

144137
# Collect predefined data
@@ -224,16 +217,13 @@ def init_syn_sdc_wf(omp_nthreads, epi_pe=None, atlas_threshold=3, name="syn_sdc_
224217
(inputnode, syn, [("in_reference_brain", "moving_image")]),
225218
(t1_2_ref, syn, [("output_image", "fixed_image")]),
226219
(fixed_image_masks, syn, [("out", "fixed_image_masks")]),
227-
(syn, outputnode, [("forward_transforms", "out_warp")]),
220+
(syn, outputnode, [("forward_transforms", "fmap")]),
228221
(syn, unwarp_ref, [("forward_transforms", "transforms")]),
229222
(inputnode, unwarp_ref, [("in_reference", "reference_image"),
230223
("in_reference", "input_image")]),
231224
(unwarp_ref, skullstrip_bold_wf, [
232225
("output_image", "inputnode.in_file")]),
233-
(unwarp_ref, outputnode, [("output_image", "out_reference")]),
234-
(skullstrip_bold_wf, outputnode, [
235-
("outputnode.skull_stripped_file", "out_reference_brain"),
236-
("outputnode.mask_file", "out_mask")]),
226+
(unwarp_ref, outputnode, [("output_image", "fmap_ref")]),
237227
])
238228
# fmt: on
239229

0 commit comments

Comments
 (0)