Skip to content

Commit 336c611

Browse files
authored
Merge pull request #197 from mgxd/enh/pepolar-alignment
ENH: Uniformize the grid&affine across EPI "blips" before TOPUP
2 parents d184cec + 38f30ca commit 336c611

File tree

3 files changed

+97
-41
lines changed

3 files changed

+97
-41
lines changed

sdcflows/interfaces/epi.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class _GetReadoutTimeInputSpec(BaseInterfaceInputSpec):
1818
class _GetReadoutTimeOutputSpec(TraitedSpec):
1919
readout_time = traits.Float
2020
pe_direction = traits.Enum("i", "i-", "j", "j-", "k", "k-")
21+
pe_dir_fsl = traits.Enum("x", "x-", "y", "y-", "z", "z-")
2122

2223

2324
class GetReadoutTime(SimpleInterface):
@@ -34,4 +35,10 @@ def _run_interface(self, runtime):
3435
self.inputs.in_file if isdefined(self.inputs.in_file) else None,
3536
)
3637
self._results["pe_direction"] = self.inputs.metadata["PhaseEncodingDirection"]
38+
self._results["pe_dir_fsl"] = (
39+
self.inputs.metadata["PhaseEncodingDirection"]
40+
.replace("i", "x")
41+
.replace("j", "y")
42+
.replace("k", "z")
43+
)
3744
return runtime

sdcflows/interfaces/utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,67 @@ def _run_interface(self, runtime):
6666
return runtime
6767

6868

69+
class _UniformGridInputSpec(BaseInterfaceInputSpec):
70+
in_data = InputMultiObject(
71+
File(exists=True),
72+
mandatory=True,
73+
desc="list of input data",
74+
)
75+
reference = traits.Int(0, usedefault=True, desc="reference index")
76+
77+
78+
class _UniformGridOutputSpec(TraitedSpec):
79+
out_data = OutputMultiObject(File(exists=True))
80+
reference = File(exists=True)
81+
82+
83+
class UniformGrid(SimpleInterface):
84+
"""Ensure all images in input have the same spatial parameters."""
85+
86+
input_spec = _UniformGridInputSpec
87+
output_spec = _UniformGridOutputSpec
88+
89+
def _run_interface(self, runtime):
90+
import nibabel as nb
91+
import numpy as np
92+
from nitransforms.linear import Affine
93+
from nipype.utils.filemanip import fname_presuffix
94+
95+
retval = [None] * len(self.inputs.in_data)
96+
self._results["reference"] = self.inputs.in_data[self.inputs.reference]
97+
retval[self.inputs.reference] = self._results["reference"]
98+
99+
refnii = nb.load(self._results["reference"])
100+
refshape = refnii.shape[:3]
101+
refaff = refnii.affine
102+
103+
resampler = Affine(reference=refnii)
104+
for i, fname in enumerate(self.inputs.in_data):
105+
if retval[i] is not None:
106+
continue
107+
108+
nii = nb.load(fname)
109+
retval[i] = fname_presuffix(
110+
fname, suffix=f"_regrid{i:03d}", newpath=runtime.cwd
111+
)
112+
113+
if np.allclose(nii.shape[:3], refshape) and np.allclose(nii.affine, refaff):
114+
if np.all(nii.affine == refaff):
115+
retval[i] = fname
116+
else:
117+
# Set reference's affine if difference is small
118+
nii.__class__(nii.dataobj, refaff, nii.header).to_filename(
119+
retval[i]
120+
)
121+
continue
122+
123+
resampler.apply(nii).to_filename(retval[i])
124+
125+
self._results["out_data"] = retval
126+
127+
return runtime
128+
129+
69130
class _ConvertWarpInputSpec(BaseInterfaceInputSpec):
70131
in_file = File(exists=True, mandatory=True, desc="output of 3dQwarp")
71132

sdcflows/workflows/fit/pepolar.py

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424

2525

2626
def init_topup_wf(
27-
omp_nthreads=1, sloppy=False, debug=False, name="pepolar_estimate_wf"
27+
grid_reference=0,
28+
omp_nthreads=1,
29+
sloppy=False,
30+
debug=False,
31+
name="pepolar_estimate_wf",
2832
):
2933
"""
3034
Create the PEPOLAR field estimation workflow based on FSL's ``topup``.
@@ -39,6 +43,8 @@ def init_topup_wf(
3943
4044
Parameters
4145
----------
46+
grid_reference : :obj:`int`
47+
Index of the volume (after flattening) that will be taken for gridding reference.
4248
sloppy : :obj:`bool`
4349
Whether a fast configuration of topup (less accurate) should be applied.
4450
debug : :obj:`bool`
@@ -76,7 +82,7 @@ def init_topup_wf(
7682

7783
from ...utils.misc import front as _front
7884
from ...interfaces.epi import GetReadoutTime
79-
from ...interfaces.utils import Flatten
85+
from ...interfaces.utils import Flatten, UniformGrid
8086
from ...interfaces.bspline import TOPUPCoeffReorient
8187
from ..ancillary import init_brainextraction_wf
8288

@@ -104,6 +110,7 @@ def init_topup_wf(
104110
outputnode.inputs.method = "PEB/PEPOLAR (phase-encoding based / PE-POLARity)"
105111

106112
flatten = pe.Node(Flatten(), name="flatten")
113+
regrid = pe.Node(UniformGrid(reference=grid_reference), name="regrid")
107114
concat_blips = pe.Node(MergeSeries(), name="concat_blips")
108115
readout_time = pe.MapNode(
109116
GetReadoutTime(),
@@ -128,23 +135,18 @@ def init_topup_wf(
128135

129136
brainextraction_wf = init_brainextraction_wf()
130137

131-
def _getfirstpe(in_meta):
132-
if isinstance(in_meta, list):
133-
in_meta = in_meta[0]
134-
return in_meta["PhaseEncodingDirection"]
135-
136138
# fmt: off
137139
workflow.connect([
138140
(inputnode, flatten, [("in_data", "in_data"),
139141
("metadata", "in_meta")]),
140142
(flatten, readout_time, [("out_data", "in_file"),
141143
("out_meta", "metadata")]),
142-
(flatten, concat_blips, [("out_data", "in_files")]),
143-
(flatten, topup, [(("out_meta", _pe2fsl), "encoding_direction")]),
144-
(readout_time, topup, [("readout_time", "readout_times")]),
145-
(concat_blips, topup, [("out_file", "in_file")]),
146-
(flatten, fix_coeff, [(("out_data", _front), "fmap_ref"),
147-
(("out_meta", _getfirstpe), "pe_dir")]),
144+
(flatten, regrid, [("out_data", "in_data")]),
145+
(regrid, concat_blips, [("out_data", "in_files")]),
146+
(readout_time, topup, [("readout_time", "readout_times"),
147+
("pe_dir_fsl", "encoding_direction")]),
148+
(regrid, fix_coeff, [("reference", "fmap_ref")]),
149+
(readout_time, fix_coeff, [(("pe_direction", _front), "pe_dir")]),
148150
(topup, fix_coeff, [("out_fieldcoef", "in_coeff")]),
149151
(topup, outputnode, [("out_jacs", "jacobians"),
150152
("out_mats", "xfms")]),
@@ -158,30 +160,35 @@ def _getfirstpe(in_meta):
158160
if not debug:
159161
# fmt: off
160162
workflow.connect([
163+
(concat_blips, topup, [("out_file", "in_file")]),
161164
(topup, merge_corrected, [("out_corrected", "in_files")]),
162165
(topup, outputnode, [("out_field", "fmap"),
163166
("out_warps", "out_warps")]),
164167
])
165168
# fmt: on
166169
return workflow
167170

171+
from nipype.interfaces.afni.preprocess import Volreg
172+
from niworkflows.interfaces.nibabel import SplitSeries
168173
from ...interfaces.bspline import ApplyCoeffsField
169174

175+
realign = pe.Node(
176+
Volreg(args=f"-base {grid_reference}", outputtype="NIFTI_GZ"),
177+
name="realign_blips",
178+
)
179+
split_blips = pe.Node(SplitSeries(), name="split_blips")
170180
unwarp = pe.Node(ApplyCoeffsField(), name="unwarp")
171181
unwarp.interface._always_run = True
172182

173-
def _getpe(inlist):
174-
if isinstance(inlist, dict):
175-
inlist = [inlist]
176-
177-
return [m["PhaseEncodingDirection"] for m in inlist]
178-
179183
# fmt:off
180184
workflow.connect([
185+
(concat_blips, realign, [("out_file", "in_file")]),
186+
(realign, topup, [("out_file", "in_file")]),
181187
(fix_coeff, unwarp, [("out_coeff", "in_coeff")]),
182-
(flatten, unwarp, [("out_data", "in_target"),
183-
(("out_meta", _getpe), "pe_dir")]),
184-
(readout_time, unwarp, [("readout_time", "ro_time")]),
188+
(realign, split_blips, [("out_file", "in_file")]),
189+
(split_blips, unwarp, [("out_files", "in_target")]),
190+
(readout_time, unwarp, [("readout_time", "ro_time"),
191+
("pe_direction", "pe_dir")]),
185192
(unwarp, outputnode, [("out_warp", "out_warps"),
186193
("out_field", "fmap")]),
187194
(unwarp, merge_corrected, [("out_corrected", "in_files")]),
@@ -343,25 +350,6 @@ def init_3dQwarp_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
343350
return workflow
344351

345352

346-
def _pe2fsl(metadata):
347-
"""
348-
Convert ijk notation to xyz.
349-
350-
Example
351-
-------
352-
>>> _pe2fsl([{"PhaseEncodingDirection": "j-"}, {"PhaseEncodingDirection": "i"}])
353-
['y-', 'x']
354-
355-
"""
356-
return [
357-
m["PhaseEncodingDirection"]
358-
.replace("i", "x")
359-
.replace("j", "y")
360-
.replace("k", "z")
361-
for m in metadata
362-
]
363-
364-
365353
def _sorted_pe(inlist):
366354
"""
367355
Generate suitable inputs to ``3dQwarp``.

0 commit comments

Comments
 (0)