Skip to content

Commit 5767c77

Browse files
authored
Merge pull request #164 from oesteban/tests/exercise-sdc-syn
ENH: Add one test for the SDC-SyN workflow
2 parents f076542 + 32d693d commit 5767c77

File tree

4 files changed

+204
-16
lines changed

4 files changed

+204
-16
lines changed

.circleci/config.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,15 @@ jobs:
101101
name: Install ds001771
102102
command: |
103103
datalad install -r https://github.com/nipreps-data/ds001771.git
104-
datalad update --merge -d ds001771/
104+
datalad update -r --merge -d ds001771/
105105
datalad get -r -d ds001771/ ds001771/sub-36/fmap/*
106106
107107
- run:
108108
name: Install ds000054
109109
command: |
110110
datalad install -r https://github.com/nipreps-data/ds000054.git
111-
datalad update --merge -d ds000054/
112-
datalad get -r -d ds000054/ ds000054/sub-100185/fmap/*
111+
datalad update -r --merge -d ds000054/
112+
datalad get -r -J 2 -d ds000054/ ds000054/* ds000054/derivatives/*
113113
114114
- save_cache:
115115
key: data-v4-{{ .Branch }}-{{ .BuildNum }}

sdcflows/interfaces/bspline.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,10 @@ def _run_interface(self, runtime):
240240

241241
# Calculate the physical coordinates of target grid
242242
targetnii = nb.load(self.inputs.in_target)
243-
targetaff = targetnii.affine
244243
allmask = np.ones_like(targetnii.dataobj, dtype="uint8")
245-
voxels = np.argwhere(allmask == 1).astype("float32")
246-
points = apply_affine(targetaff.astype("float32"), voxels)
244+
# targetaff = targetnii.affine
245+
# voxels = np.argwhere(allmask == 1).astype("float32")
246+
# points = apply_affine(targetaff.astype("float32"), voxels)
247247

248248
weights = []
249249
coeffs = []

sdcflows/workflows/fit/syn.py

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
"""
44
Estimating the susceptibility distortions without fieldmaps.
55
6+
.. testsetup::
7+
8+
>>> tmpdir = getfixture('tmpdir')
9+
>>> tmp = tmpdir.chdir() # changing to a temporary directory
10+
>>> data = np.zeros((10, 10, 10, 1, 3))
11+
>>> data[..., 1] = 1
12+
>>> nb.Nifti1Image(data, None, None).to_filename(
13+
... tmpdir.join('field.nii.gz').strpath)
14+
615
.. _sdc_fieldmapless :
716
817
Fieldmap-less approaches
@@ -55,7 +64,11 @@
5564

5665

5766
def init_syn_sdc_wf(
58-
*, atlas_threshold=3, debug=False, name="syn_sdc_wf", omp_nthreads=1,
67+
*,
68+
atlas_threshold=3,
69+
debug=False,
70+
name="syn_sdc_wf",
71+
omp_nthreads=1,
5972
):
6073
"""
6174
Build the *fieldmap-less* susceptibility-distortion estimation workflow.
@@ -99,7 +112,7 @@ def init_syn_sdc_wf(
99112
A preprocessed, skull-stripped anatomical (T1w or T2w) image.
100113
std2anat_xfm : :obj:`str`
101114
inverse registration transform of T1w image to MNI template
102-
anat2bold_xfm : :obj:`str`
115+
anat2epi_xfm : :obj:`str`
103116
transform mapping coordinates from the EPI space to the anatomical
104117
space (i.e., the transform to resample anatomical info into EPI space.)
105118
@@ -120,7 +133,14 @@ def init_syn_sdc_wf(
120133
FixHeaderRegistration as Registration,
121134
)
122135
from niworkflows.interfaces.nibabel import Binarize
136+
from ...interfaces.epi import EPIMask
123137
from ...utils.misc import front as _pop
138+
from ...interfaces.bspline import (
139+
BSplineApprox,
140+
DEFAULT_LF_ZOOMS_MM,
141+
DEFAULT_HF_ZOOMS_MM,
142+
DEFAULT_ZOOMS_MM,
143+
)
124144

125145
workflow = Workflow(name=name)
126146
workflow.__desc__ = f"""\
@@ -137,12 +157,13 @@ def init_syn_sdc_wf(
137157
"""
138158
inputnode = pe.Node(
139159
niu.IdentityInterface(
140-
["epi_ref", "epi_mask", "anat_brain", "std2anat_xfm", "anat2bold_xfm"]
160+
["epi_ref", "epi_mask", "anat_brain", "std2anat_xfm", "anat2epi_xfm"]
141161
),
142162
name="inputnode",
143163
)
144164
outputnode = pe.Node(
145-
niu.IdentityInterface(["fmap", "fmap_ref", "fmap_mask"]), name="outputnode",
165+
niu.IdentityInterface(["fmap", "fmap_ref", "fmap_coeff", "fmap_mask"]),
166+
name="outputnode",
146167
)
147168

148169
invert_t1w = pe.Node(Rescale(invert=True), name="invert_t1w", mem_gb=0.3)
@@ -174,29 +195,54 @@ def init_syn_sdc_wf(
174195
n_procs=omp_nthreads,
175196
)
176197

177-
unwarp_ref = pe.Node(ApplyTransforms(interpolation="BSpline"), name="unwarp_ref",)
198+
unwarp_ref = pe.Node(
199+
ApplyTransforms(interpolation="BSpline"),
200+
name="unwarp_ref",
201+
)
202+
203+
epi_mask = pe.Node(EPIMask(), name="epi_mask")
204+
205+
# Extract nonzero component
206+
extract_field = pe.Node(niu.Function(function=_extract_field), name="extract_field")
207+
208+
# Regularize with B-Splines
209+
bs_filter = pe.Node(BSplineApprox(), n_procs=omp_nthreads, name="bs_filter")
210+
bs_filter.interface._always_run = debug
211+
bs_filter.inputs.bs_spacing = (
212+
[DEFAULT_LF_ZOOMS_MM, DEFAULT_HF_ZOOMS_MM] if not debug else [DEFAULT_ZOOMS_MM]
213+
)
214+
bs_filter.inputs.extrapolate = not debug
178215

179216
# fmt: off
180217
workflow.connect([
181-
(inputnode, transform_list, [("anat2bold_xfm", "in1"),
218+
(inputnode, transform_list, [("anat2epi_xfm", "in1"),
182219
("std2anat_xfm", "in2")]),
183220
(inputnode, invert_t1w, [("anat_brain", "in_file"),
184221
(("epi_ref", _pop), "ref_file")]),
185-
(inputnode, anat2epi, [(("epi_ref", _pop), "reference_image")]),
222+
(inputnode, anat2epi, [(("epi_ref", _pop), "reference_image"),
223+
("anat2epi_xfm", "transforms")]),
186224
(inputnode, syn, [(("epi_ref", _pop), "moving_image"),
187225
("epi_mask", "moving_image_masks"),
188226
(("epi_ref", _warp_dir), "restrict_deformation")]),
227+
(inputnode, unwarp_ref, [(("epi_ref", _pop), "reference_image"),
228+
(("epi_ref", _pop), "input_image")]),
189229
(inputnode, prior2epi, [(("epi_ref", _pop), "reference_image")]),
230+
(inputnode, extract_field, [("epi_ref", "epi_meta")]),
190231
(invert_t1w, anat2epi, [("out_file", "input_image")]),
191232
(transform_list, prior2epi, [("out", "transforms")]),
192233
(prior2epi, atlas_msk, [("output_image", "in_file")]),
193234
(anat2epi, syn, [("output_image", "fixed_image")]),
194235
(atlas_msk, syn, [(("out_mask", _fixed_masks_arg), "fixed_image_masks")]),
195-
(syn, outputnode, [("forward_transforms", "fmap")]),
236+
(syn, extract_field, [("forward_transforms", "in_file")]),
196237
(syn, unwarp_ref, [("forward_transforms", "transforms")]),
197-
(inputnode, unwarp_ref, [(("epi_ref", _pop), "reference_image"),
198-
(("epi_ref", _pop), "input_image")]),
238+
(unwarp_ref, epi_mask, [("output_image", "in_file")]),
239+
(extract_field, bs_filter, [("out", "in_data")]),
240+
(epi_mask, bs_filter, [("out_file", "in_mask")]),
199241
(unwarp_ref, outputnode, [("output_image", "fmap_ref")]),
242+
(epi_mask, outputnode, [("out_file", "fmap_mask")]),
243+
(bs_filter, outputnode, [
244+
("out_extrapolated" if not debug else "out_field", "fmap"),
245+
("out_coeff", "fmap_coeff")]),
200246
])
201247
# fmt: on
202248

@@ -231,3 +277,41 @@ def _fixed_masks_arg(mask):
231277
232278
"""
233279
return ["NULL", mask]
280+
281+
282+
def _extract_field(in_file, epi_meta):
283+
"""
284+
Extract the nonzero component of the deformation field estimated by ANTs.
285+
286+
Examples
287+
--------
288+
>>> nii = nb.load(
289+
... _extract_field(
290+
... ["field.nii.gz"],
291+
... ("epi.nii.gz", {"PhaseEncodingDirection": "j-", "TotalReadoutTime": 0.005}))
292+
... )
293+
>>> nii.shape
294+
(10, 10, 10)
295+
296+
>>> np.allclose(nii.get_fdata(), -200)
297+
True
298+
299+
"""
300+
from nipype.utils.filemanip import fname_presuffix
301+
import numpy as np
302+
import nibabel as nb
303+
from sdcflows.utils.epimanip import get_trt
304+
305+
fieldnii = nb.load(in_file[0])
306+
trt = get_trt(epi_meta[1], in_file=epi_meta[0])
307+
data = (
308+
np.squeeze(fieldnii.get_fdata(dtype="float32"))[
309+
..., "ijk".index(epi_meta[1]["PhaseEncodingDirection"][0])
310+
]
311+
/ trt * (-1.0 if epi_meta[1]["PhaseEncodingDirection"].endswith("-") else 1.0)
312+
)
313+
out_file = fname_presuffix(in_file[0], suffix="_fieldmap")
314+
nii = nb.Nifti1Image(data, fieldnii.affine, None)
315+
nii.header.set_xyzt_units(fieldnii.header.get_xyzt_units()[0])
316+
nii.to_filename(out_file)
317+
return out_file
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Test fieldmap-less SDC-SyN."""
2+
import os
3+
import pytest
4+
from nipype.pipeline import engine as pe
5+
from niworkflows.interfaces.nibabel import ApplyMask
6+
7+
from ..syn import init_syn_sdc_wf
8+
9+
10+
@pytest.mark.skipif(os.getenv("TRAVIS") == "true", reason="this is TravisCI")
11+
@pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") == "true", reason="this is GH Actions")
12+
def test_syn_wf(tmpdir, datadir, workdir, outdir):
13+
"""Build and run an SDC-SyN workflow."""
14+
derivs_path = datadir / "ds000054" / "derivatives"
15+
smriprep = derivs_path / "smriprep-0.6" / "sub-100185" / "anat"
16+
17+
wf = pe.Workflow(name="syn_test")
18+
19+
syn_wf = init_syn_sdc_wf(debug=True, omp_nthreads=4)
20+
syn_wf.inputs.inputnode.epi_ref = (
21+
str(
22+
derivs_path
23+
/ "sdcflows-tests"
24+
/ "sub-100185_task-machinegame_run-1_boldref.nii.gz"
25+
),
26+
{"PhaseEncodingDirection": "j-", "TotalReadoutTime": 0.005},
27+
)
28+
syn_wf.inputs.inputnode.epi_mask = str(
29+
derivs_path
30+
/ "sdcflows-tests"
31+
/ "sub-100185_task-machinegame_run-1_desc-brain_mask.nii.gz"
32+
)
33+
syn_wf.inputs.inputnode.anat2epi_xfm = str(
34+
derivs_path / "sdcflows-tests" / "t1w2bold.txt"
35+
)
36+
syn_wf.inputs.inputnode.std2anat_xfm = str(
37+
smriprep / "sub-100185_from-MNI152NLin2009cAsym_to-T1w_mode-image_xfm.h5"
38+
)
39+
40+
t1w_mask = pe.Node(
41+
ApplyMask(
42+
in_file=str(smriprep / "sub-100185_desc-preproc_T1w.nii.gz"),
43+
in_mask=str(smriprep / "sub-100185_desc-brain_mask.nii.gz"),
44+
),
45+
name="t1w_mask",
46+
)
47+
48+
wf.connect(
49+
[
50+
(t1w_mask, syn_wf, [("out_file", "inputnode.anat_brain")]),
51+
]
52+
)
53+
54+
if outdir:
55+
from ...outputs import init_fmap_derivatives_wf, init_fmap_reports_wf
56+
57+
outdir = outdir / "unittests" / "syn_test"
58+
fmap_derivatives_wf = init_fmap_derivatives_wf(
59+
output_dir=str(outdir),
60+
write_coeff=True,
61+
custom_entities={"est": "sdcsyn"},
62+
bids_fmap_id="sdcsyn",
63+
)
64+
fmap_derivatives_wf.inputs.inputnode.source_files = [
65+
str(
66+
derivs_path
67+
/ "sdcflows-tests"
68+
/ "sub-100185_task-machinegame_run-1_boldref.nii.gz"
69+
)
70+
]
71+
fmap_derivatives_wf.inputs.inputnode.fmap_meta = {
72+
"PhaseEncodingDirection": "j-"
73+
}
74+
75+
fmap_reports_wf = init_fmap_reports_wf(
76+
output_dir=str(outdir),
77+
fmap_type="sdcsyn",
78+
)
79+
fmap_reports_wf.inputs.inputnode.source_files = [
80+
str(
81+
derivs_path
82+
/ "sdcflows-tests"
83+
/ "sub-100185_task-machinegame_run-1_boldref.nii.gz"
84+
)
85+
]
86+
87+
# fmt: off
88+
wf.connect([
89+
(syn_wf, fmap_reports_wf, [
90+
("outputnode.fmap", "inputnode.fieldmap"),
91+
("outputnode.fmap_ref", "inputnode.fmap_ref"),
92+
("outputnode.fmap_mask", "inputnode.fmap_mask")]),
93+
(syn_wf, fmap_derivatives_wf, [
94+
("outputnode.fmap", "inputnode.fieldmap"),
95+
("outputnode.fmap_ref", "inputnode.fmap_ref"),
96+
("outputnode.fmap_coeff", "inputnode.fmap_coeff"),
97+
]),
98+
])
99+
# fmt: on
100+
101+
if workdir:
102+
wf.base_dir = str(workdir)
103+
104+
wf.run(plugin="Linear")

0 commit comments

Comments
 (0)