Skip to content

Commit d649398

Browse files
authored
Merge pull request #165 from oesteban/enh/syn-oblique-patch
FIX: Make images "plumb" before running ANTs-SyN (and roll-back afterwards)
2 parents 5767c77 + f1a4948 commit d649398

File tree

4 files changed

+231
-19
lines changed

4 files changed

+231
-19
lines changed

sdcflows/interfaces/bspline.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,19 +241,13 @@ def _run_interface(self, runtime):
241241
# Calculate the physical coordinates of target grid
242242
targetnii = nb.load(self.inputs.in_target)
243243
allmask = np.ones_like(targetnii.dataobj, dtype="uint8")
244-
# targetaff = targetnii.affine
245-
# voxels = np.argwhere(allmask == 1).astype("float32")
246-
# points = apply_affine(targetaff.astype("float32"), voxels)
247244

248245
weights = []
249246
coeffs = []
250247

251248
for cname in self.inputs.in_coeff:
252249
coeff_nii = nb.load(cname)
253250
wmat = grid_bspline_weights(targetnii, coeff_nii)
254-
# wmat = bspline_weights(
255-
# points, coeff_nii, mem_percent=0.1 if self.inputs.low_mem else None,
256-
# )
257251
weights.append(wmat)
258252
coeffs.append(coeff_nii.get_fdata(dtype="float32").reshape(-1))
259253

@@ -284,6 +278,7 @@ def _run_interface(self, runtime):
284278
field[..., phaseEncDim] = data.reshape(-1)
285279
aff = targetnii.affine.copy()
286280
aff[:3, 3] = 0.0
281+
# Multiplying by the affine implicitly applies the voxel size to the shift map
287282
field = nb.affines.apply_affine(aff, field).reshape(fieldshape)
288283
warpnii = targetnii.__class__(
289284
field[:, :, :, np.newaxis, :].astype("float32"), targetnii.affine, None

sdcflows/interfaces/tests/test_utils.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
import nibabel as nb
55

6-
from ..utils import Flatten, ConvertWarp
6+
from ..utils import Flatten, ConvertWarp, Deoblique, Reoblique
77

88

99
def test_Flatten(tmpdir):
@@ -43,3 +43,47 @@ def test_ConvertWarp(tmpdir, shape):
4343
assert nii.header.get_data_dtype() == np.float32
4444
assert nii.header.get_intent() == ("vector", (), "")
4545
assert nii.shape == (10, 10, 10, 1, 3)
46+
47+
48+
@pytest.mark.parametrize(
49+
"angles,oblique",
50+
[
51+
((0, 0, 0), False),
52+
((0.9, 0.001, 0.001), True),
53+
((0, 0, 2 * np.pi), False),
54+
],
55+
)
56+
def test_Xeoblique(tmpdir, angles, oblique):
57+
"""Exercise De/Reoblique interfaces."""
58+
59+
tmpdir.chdir()
60+
61+
affine = nb.affines.from_matvec(nb.eulerangles.euler2mat(*angles))
62+
nb.Nifti1Image(np.zeros((10, 10, 10), dtype="uint8"), affine, None).to_filename(
63+
"epi.nii.gz"
64+
)
65+
66+
result = (
67+
Deoblique(
68+
in_epi="epi.nii.gz",
69+
mask_epi="epi.nii.gz",
70+
in_anat="epi.nii.gz",
71+
mask_anat="epi.nii.gz",
72+
)
73+
.run()
74+
.outputs
75+
)
76+
77+
assert np.allclose(nb.load(result.out_epi).affine, affine) is not oblique
78+
79+
reoblique = (
80+
Reoblique(
81+
in_plumb=result.out_epi,
82+
in_field=result.out_epi,
83+
in_epi="epi.nii.gz",
84+
)
85+
.run()
86+
.outputs
87+
)
88+
89+
assert np.allclose(nb.load(reoblique.out_epi).affine, affine)

sdcflows/interfaces/utils.py

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,30 @@
1111
OutputMultiObject,
1212
)
1313

14+
OBLIQUE_THRESHOLD_DEG = 0.5
15+
1416

1517
class _FlattenInputSpec(BaseInterfaceInputSpec):
1618
in_data = InputMultiObject(
17-
File(exists=True), mandatory=True, desc="list of input data",
19+
File(exists=True),
20+
mandatory=True,
21+
desc="list of input data",
1822
)
1923
in_meta = InputMultiObject(
20-
traits.DictStrAny, mandatory=True, desc="list of metadata",
24+
traits.DictStrAny,
25+
mandatory=True,
26+
desc="list of metadata",
2127
)
2228
max_trs = traits.Int(50, usedefault=True, desc="only pick first TRs")
2329

2430

2531
class _FlattenOutputSpec(TraitedSpec):
2632
out_list = OutputMultiObject(
27-
traits.Tuple(File(exists=True), traits.DictStrAny,), desc="list of output files"
33+
traits.Tuple(
34+
File(exists=True),
35+
traits.DictStrAny,
36+
),
37+
desc="list of output files",
2838
)
2939
out_data = OutputMultiObject(File(exists=True))
3040
out_meta = OutputMultiObject(traits.DictStrAny)
@@ -71,6 +81,97 @@ def _run_interface(self, runtime):
7181
return runtime
7282

7383

84+
class _DeobliqueInputSpec(BaseInterfaceInputSpec):
85+
in_epi = File(
86+
exists=True, mandatory=True, desc="the EPI dataset potentially oblique"
87+
)
88+
mask_epi = File(
89+
exists=True,
90+
mandatory=True,
91+
desc="a binary mask corresponding to the EPI dataset",
92+
)
93+
in_anat = File(
94+
exists=True,
95+
mandatory=True,
96+
desc="the corresponging anatomical dataset potentially oblique",
97+
)
98+
mask_anat = File(
99+
exists=True,
100+
mandatory=True,
101+
desc="a binary mask corresponding to the anatomical dataset",
102+
)
103+
104+
105+
class _DeobliqueOutputSpec(TraitedSpec):
106+
out_epi = File(exists=True, desc="the plumb, EPI dataset")
107+
out_anat = File(exists=True, desc="the plumb, anatomical dataset")
108+
mask_epi = File(
109+
exists=True,
110+
mandatory=True,
111+
desc="a binary mask corresponding to the EPI dataset",
112+
)
113+
mask_anat = File(
114+
exists=True,
115+
mandatory=True,
116+
desc="a binary mask corresponding to the anatomical dataset",
117+
)
118+
119+
120+
class Deoblique(SimpleInterface):
121+
"""Make a dataset plumb."""
122+
123+
input_spec = _DeobliqueInputSpec
124+
output_spec = _DeobliqueOutputSpec
125+
126+
def _run_interface(self, runtime):
127+
(
128+
self._results["out_epi"],
129+
self._results["out_anat"],
130+
self._results["mask_epi"],
131+
self._results["mask_anat"],
132+
) = _deoblique(
133+
self.inputs.in_epi,
134+
self.inputs.in_anat,
135+
self.inputs.mask_epi,
136+
self.inputs.mask_anat,
137+
newpath=runtime.cwd,
138+
)
139+
return runtime
140+
141+
142+
class _ReobliqueInputSpec(BaseInterfaceInputSpec):
143+
in_plumb = File(exists=True, mandatory=True, desc="the plumb EPI image")
144+
in_field = File(
145+
exists=True,
146+
mandatory=True,
147+
desc="the plumb field map, extracted from the displacements field estimated by SyN",
148+
)
149+
in_epi = File(
150+
exists=True, mandatory=True, desc="the original, potentially oblique EPI image"
151+
)
152+
153+
154+
class _ReobliqueOutputSpec(TraitedSpec):
155+
out_epi = File(exists=True, desc="the reoblique'd EPI image")
156+
out_field = File(exists=True, desc="the reoblique'd EPI image")
157+
158+
159+
class Reoblique(SimpleInterface):
160+
"""Make a dataset plumb."""
161+
162+
input_spec = _ReobliqueInputSpec
163+
output_spec = _ReobliqueOutputSpec
164+
165+
def _run_interface(self, runtime):
166+
(self._results["out_epi"], self._results["out_field"],) = _reoblique(
167+
self.inputs.in_epi,
168+
self.inputs.in_plumb,
169+
self.inputs.in_field,
170+
newpath=runtime.cwd,
171+
)
172+
return runtime
173+
174+
74175
def _flatten(inlist, max_trs=50, out_dir=None):
75176
"""
76177
Split the input EPIs and generate a flattened list with corresponding metadata.
@@ -120,3 +221,63 @@ def _qwarp2ants(in_file, newpath=None):
120221
data = np.squeeze(nii.get_fdata(dtype="float32"))[..., np.newaxis, :]
121222
nb.Nifti1Image(data, nii.affine, hdr).to_filename(out_file)
122223
return out_file
224+
225+
226+
def _deoblique(in_epi, in_anat, mask_epi, mask_anat, newpath=None):
227+
import numpy as np
228+
import nibabel as nb
229+
from nipype.utils.filemanip import fname_presuffix
230+
231+
epinii = nb.load(in_epi)
232+
if np.all(nb.affines.obliquity(epinii.affine)) < OBLIQUE_THRESHOLD_DEG:
233+
return in_epi, in_anat, mask_epi, mask_anat
234+
235+
newaff = np.eye(4)
236+
newaff[:3, :3] = np.diag(np.array(epinii.header.get_zooms()[:3]))
237+
newaff[:3, 3] -= (np.array(epinii.shape[:3]) - 1) * 0.5
238+
239+
hdr = epinii.header.copy()
240+
hdr.set_qform(newaff, code=1)
241+
hdr.set_sform(newaff, code=1)
242+
newepi = epinii.__class__(epinii.dataobj, newaff, hdr)
243+
out_epi = fname_presuffix(in_epi, suffix="_plumb", newpath=newpath)
244+
newepi.to_filename(out_epi)
245+
246+
out_files = [out_epi]
247+
for fname in (in_anat, mask_epi, mask_anat):
248+
nii = nb.load(fname)
249+
hdr = nii.header.copy()
250+
hdr.set_qform(newaff, code=1)
251+
hdr.set_sform(newaff, code=1)
252+
newnii = nii.__class__(np.asanyarray(nii.dataobj), newaff, hdr)
253+
out = fname_presuffix(fname, suffix="_plumb", newpath=newpath)
254+
newnii.to_filename(out)
255+
out_files.append(out)
256+
257+
return tuple(out_files)
258+
259+
260+
def _reoblique(in_epi, in_plumb, in_field, newpath=None):
261+
import numpy as np
262+
import nibabel as nb
263+
from nipype.utils.filemanip import fname_presuffix
264+
265+
epinii = nb.load(in_epi)
266+
if np.all(nb.affines.obliquity(epinii.affine)) < OBLIQUE_THRESHOLD_DEG:
267+
return in_plumb, in_field
268+
269+
out_files = [
270+
fname_presuffix(f, suffix="_oriented", newpath=newpath)
271+
for f in (in_plumb, in_field)
272+
]
273+
plumbnii = nb.load(in_plumb)
274+
plumbnii.__class__(plumbnii.dataobj, epinii.affine, epinii.header).to_filename(
275+
out_files[0]
276+
)
277+
278+
fmapnii = nb.load(in_field)
279+
hdr = fmapnii.header.copy()
280+
hdr.set_qform(*epinii.header.get_qform(coded=True))
281+
hdr.set_sform(*epinii.header.get_sform(coded=True))
282+
fmapnii.__class__(fmapnii.dataobj, epinii.affine, hdr).to_filename(out_files[1])
283+
return out_files

sdcflows/workflows/fit/syn.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def init_syn_sdc_wf(
135135
from niworkflows.interfaces.nibabel import Binarize
136136
from ...interfaces.epi import EPIMask
137137
from ...utils.misc import front as _pop
138+
from ...interfaces.utils import Deoblique, Reoblique
138139
from ...interfaces.bspline import (
139140
BSplineApprox,
140141
DEFAULT_LF_ZOOMS_MM,
@@ -188,6 +189,9 @@ def init_syn_sdc_wf(
188189
)
189190
atlas_msk = pe.Node(Binarize(thresh_low=atlas_threshold), name="atlas_msk")
190191

192+
deoblique = pe.Node(Deoblique(), name="deoblique")
193+
reoblique = pe.Node(Reoblique(), name="reoblique")
194+
191195
# SyN Registration Core
192196
syn = pe.Node(
193197
Registration(from_file=pkgrf("sdcflows", "data/susceptibility_syn.json")),
@@ -221,24 +225,31 @@ def init_syn_sdc_wf(
221225
(("epi_ref", _pop), "ref_file")]),
222226
(inputnode, anat2epi, [(("epi_ref", _pop), "reference_image"),
223227
("anat2epi_xfm", "transforms")]),
224-
(inputnode, syn, [(("epi_ref", _pop), "moving_image"),
225-
("epi_mask", "moving_image_masks"),
226-
(("epi_ref", _warp_dir), "restrict_deformation")]),
228+
(inputnode, deoblique, [(("epi_ref", _pop), "in_epi"),
229+
("epi_mask", "mask_epi")]),
230+
(inputnode, reoblique, [(("epi_ref", _pop), "in_epi")]),
231+
(inputnode, syn, [(("epi_ref", _warp_dir), "restrict_deformation")]),
227232
(inputnode, unwarp_ref, [(("epi_ref", _pop), "reference_image"),
228233
(("epi_ref", _pop), "input_image")]),
229234
(inputnode, prior2epi, [(("epi_ref", _pop), "reference_image")]),
230235
(inputnode, extract_field, [("epi_ref", "epi_meta")]),
231236
(invert_t1w, anat2epi, [("out_file", "input_image")]),
232237
(transform_list, prior2epi, [("out", "transforms")]),
233238
(prior2epi, atlas_msk, [("output_image", "in_file")]),
234-
(anat2epi, syn, [("output_image", "fixed_image")]),
235-
(atlas_msk, syn, [(("out_mask", _fixed_masks_arg), "fixed_image_masks")]),
239+
(anat2epi, deoblique, [("output_image", "in_anat")]),
240+
(atlas_msk, deoblique, [("out_mask", "mask_anat")]),
241+
(deoblique, syn, [("out_epi", "moving_image"),
242+
("out_anat", "fixed_image"),
243+
("mask_epi", "moving_image_masks"),
244+
(("mask_anat", _fixed_masks_arg), "fixed_image_masks")]),
236245
(syn, extract_field, [("forward_transforms", "in_file")]),
237246
(syn, unwarp_ref, [("forward_transforms", "transforms")]),
238-
(unwarp_ref, epi_mask, [("output_image", "in_file")]),
239-
(extract_field, bs_filter, [("out", "in_data")]),
247+
(unwarp_ref, reoblique, [("output_image", "in_plumb")]),
248+
(reoblique, epi_mask, [("out_epi", "in_file")]),
249+
(extract_field, reoblique, [("out", "in_field")]),
250+
(reoblique, bs_filter, [("out_field", "in_data")]),
240251
(epi_mask, bs_filter, [("out_file", "in_mask")]),
241-
(unwarp_ref, outputnode, [("output_image", "fmap_ref")]),
252+
(reoblique, outputnode, [("out_epi", "fmap_ref")]),
242253
(epi_mask, outputnode, [("out_file", "fmap_mask")]),
243254
(bs_filter, outputnode, [
244255
("out_extrapolated" if not debug else "out_field", "fmap"),
@@ -308,7 +319,8 @@ def _extract_field(in_file, epi_meta):
308319
np.squeeze(fieldnii.get_fdata(dtype="float32"))[
309320
..., "ijk".index(epi_meta[1]["PhaseEncodingDirection"][0])
310321
]
311-
/ trt * (-1.0 if epi_meta[1]["PhaseEncodingDirection"].endswith("-") else 1.0)
322+
/ trt
323+
* (-1.0 if epi_meta[1]["PhaseEncodingDirection"].endswith("-") else 1.0)
312324
)
313325
out_file = fname_presuffix(in_file[0], suffix="_fieldmap")
314326
nii = nb.Nifti1Image(data, fieldnii.affine, None)

0 commit comments

Comments
 (0)