Skip to content

Commit 0e681c9

Browse files
authored
Merge pull request #72 from mgxd/enh/subcortical-cifti
ENH: Subcortical alignment workflow
2 parents 1cdf9da + 9fe1243 commit 0e681c9

File tree

10 files changed

+839
-138
lines changed

10 files changed

+839
-138
lines changed

nibabies/config.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,12 @@ def init_spaces(checkpoint=True):
684684
if checkpoint and not spaces.is_cached():
685685
spaces.checkpoint()
686686

687+
if workflow.age_months is not None:
688+
from .utils.misc import cohort_by_months
689+
690+
if "MNIInfant" not in spaces.get_spaces(nonstandard=False, dim=(3,)):
691+
cohort = cohort_by_months("MNIInfant", workflow.age_months)
692+
spaces.add(Reference("MNIInfant", {"cohort": cohort}))
687693
# # Add the default standard space if not already present (required by several sub-workflows)
688694
# if "MNI152NLin2009cAsym" not in spaces.get_spaces(nonstandard=False, dim=(3,)):
689695
# spaces.add(Reference("MNI152NLin2009cAsym", {}))
@@ -695,10 +701,9 @@ def init_spaces(checkpoint=True):
695701
# # Make sure there's a normalization to FSL for AROMA to use.
696702
# spaces.add(Reference("MNI152NLin6Asym", {"res": "2"}))
697703

698-
cifti_output = workflow.cifti_output
699704
if workflow.cifti_output:
700705
# CIFTI grayordinates to corresponding FSL-MNI resolutions.
701-
vol_res = "2" if cifti_output == "91k" else "1"
706+
vol_res = "2" if workflow.cifti_output == "91k" else "1"
702707
spaces.add(Reference("fsaverage", {"den": "164k"}))
703708
spaces.add(Reference("MNI152NLin6Asym", {"res": vol_res}))
704709

nibabies/interfaces/conftest.py renamed to nibabies/conftest.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""py.test configuration"""
22
from pathlib import Path
3-
import pytest
43
from tempfile import TemporaryDirectory
4+
from pkg_resources import resource_filename
55

6+
import pytest
67

78
FILES = (
89
'functional.nii',
@@ -32,3 +33,4 @@ def data_dir():
3233
@pytest.fixture(autouse=True)
3334
def set_namespace(doctest_namespace, data_dir):
3435
doctest_namespace["data_dir"] = data_dir
36+
doctest_namespace["test_data"] = Path(resource_filename("nibabies", "tests/data"))

nibabies/interfaces/nibabel.py

+57
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
BaseInterfaceInputSpec,
66
File,
77
SimpleInterface,
8+
InputMultiObject,
89
)
910

1011

@@ -34,6 +35,24 @@ def _run_interface(self, runtime):
3435
return runtime
3536

3637

38+
class MergeROIsInputSpec(BaseInterfaceInputSpec):
39+
in_files = InputMultiObject(File(exists=True), desc="ROI files to be merged")
40+
41+
42+
class MergeROIsOutputSpec(TraitedSpec):
43+
out_file = File(exists=True, desc="NIfTI containing all ROIs")
44+
45+
46+
class MergeROIs(SimpleInterface):
47+
"""Combine multiple region of interest files (3D or 4D) into a single file"""
48+
input_spec = MergeROIsInputSpec
49+
output_spec = MergeROIsOutputSpec
50+
51+
def _run_interface(self, runtime):
52+
self._results["out_file"] = _merge_rois(self.inputs.in_files, newpath=runtime.cwd)
53+
return runtime
54+
55+
3756
def _dilate(in_file, radius=3, iterations=1, newpath=None):
3857
"""Dilate (binary) input mask."""
3958
from pathlib import Path
@@ -55,3 +74,41 @@ def _dilate(in_file, radius=3, iterations=1, newpath=None):
5574
out_file = fname_presuffix(in_file, suffix="_dil", newpath=newpath or Path.cwd())
5675
mask.__class__(newdata.astype("uint8"), mask.affine, hdr).to_filename(out_file)
5776
return out_file
77+
78+
79+
def _merge_rois(in_files, newpath=None):
80+
"""
81+
Aggregate individual 4D ROI files together into a single subcortical NIfTI.
82+
All ROI images are sanity checked with regards to:
83+
1) Shape
84+
2) Affine
85+
3) Overlap
86+
87+
If any of these checks fail, an ``AssertionError`` will be raised.
88+
"""
89+
from pathlib import Path
90+
import nibabel as nb
91+
import numpy as np
92+
93+
img = nb.load(in_files[0])
94+
data = np.array(img.dataobj)
95+
affine = img.affine
96+
header = img.header
97+
98+
nonzero = np.any(data, axis=3)
99+
for roi in in_files[1:]:
100+
img = nb.load(roi)
101+
assert img.shape == data.shape, "Mismatch in image shape"
102+
assert np.allclose(img.affine, affine), "Mismatch in affine"
103+
roi_data = np.asanyarray(img.dataobj)
104+
roi_nonzero = np.any(roi_data, axis=3)
105+
assert not np.any(roi_nonzero & nonzero), "Overlapping ROIs"
106+
nonzero |= roi_nonzero
107+
data += roi_data
108+
del roi_data
109+
110+
if newpath is None:
111+
newpath = Path()
112+
out_file = str((Path(newpath) / "combined.nii.gz").absolute())
113+
img.__class__(data, affine, header).to_filename(out_file)
114+
return out_file

nibabies/interfaces/tests/__init__.py

Whitespace-only changes.
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import uuid
2+
3+
import nibabel as nb
4+
import numpy as np
5+
import pytest
6+
7+
from ..nibabel import MergeROIs
8+
9+
10+
@pytest.fixture
11+
def create_roi(tmp_path):
12+
files = []
13+
14+
def _create_roi(affine, img_data, roi_index):
15+
img_data[tuple(roi_index)] = 1
16+
nii = nb.Nifti1Image(img_data, affine)
17+
filename = tmp_path / f"{str(uuid.uuid4())}.nii.gz"
18+
files.append(filename)
19+
nii.to_filename(filename)
20+
return filename
21+
22+
yield _create_roi
23+
24+
for f in files:
25+
f.unlink()
26+
27+
28+
# create a slightly off affine
29+
bad_affine = np.eye(4)
30+
bad_affine[0, -1] = -1
31+
32+
33+
@pytest.mark.parametrize(
34+
"affine, data, roi_index, error, err_message",
35+
[
36+
(np.eye(4), np.zeros((2, 2, 2, 2), dtype=int), [1, 0], None, None),
37+
(
38+
np.eye(4),
39+
np.zeros((2, 2, 3, 2), dtype=int),
40+
[1, 0],
41+
True,
42+
"Mismatch in image shape",
43+
),
44+
(
45+
bad_affine,
46+
np.zeros((2, 2, 2, 2), dtype=int),
47+
[1, 0],
48+
True,
49+
"Mismatch in affine",
50+
),
51+
(
52+
np.eye(4),
53+
np.zeros((2, 2, 2, 2), dtype=int),
54+
[0, 0, 0],
55+
True,
56+
"Overlapping ROIs",
57+
),
58+
],
59+
)
60+
def test_merge_rois(tmpdir, create_roi, affine, data, roi_index, error, err_message):
61+
tmpdir.chdir()
62+
roi0 = create_roi(np.eye(4), np.zeros((2, 2, 2, 2), dtype=int), [0, 0])
63+
roi1 = create_roi(np.eye(4), np.zeros((2, 2, 2, 2), dtype=int), [0, 1])
64+
test_roi = create_roi(affine, data, roi_index)
65+
66+
merge = MergeROIs(in_files=[roi0, roi1, test_roi])
67+
if error is None:
68+
merge.run()
69+
return
70+
# otherwise check expected exceptions
71+
with pytest.raises(AssertionError) as err:
72+
merge.run()
73+
assert err_message in str(err.value)

0 commit comments

Comments
 (0)