Skip to content

ENH: Subcortical alignment workflow #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c9db42a
WIP: Subcortical MNI alignment
mgxd May 27, 2021
8f05c5e
RF: CiftiCreateDenseTimeseries optional argument
mgxd Jun 3, 2021
9e55548
WIP: Subcortical alignment
mgxd Jun 3, 2021
8bf6d50
RF: CreateDenseTimeseries default output
mgxd Jun 3, 2021
83184a0
RF: set a default for CiftiCreateLabel out_file
mgxd Jun 3, 2021
639658c
WIP: Subcortical alignment
mgxd Jun 3, 2021
c53b264
ENH: Finalize subcortical alignment workflow
mgxd Jun 4, 2021
be0b05d
STY: Pep8
mgxd Jun 4, 2021
1ffdfa7
FIX: Use `value` rather than `default_value`
mgxd Jun 4, 2021
7c29e62
RF: Move CiftiDilateInputSpec
mgxd Jun 4, 2021
c00848f
ENH: CiftiSeparate
mgxd Jun 4, 2021
959f248
FIX: CreateDenseFromTemplate formatting
mgxd Jun 4, 2021
6bfa64c
FIX: Set volume all file
mgxd Jun 4, 2021
90884a9
TST: Add simple test for label file parsing
mgxd Jun 7, 2021
99611d8
TST: Output formatting
mgxd Jun 7, 2021
fb75f29
ENH: Set MNI Infant as the default output space
mgxd Jul 19, 2021
9cc6a80
FIX: Syntax errors, connections, everything really
mgxd Jul 20, 2021
ad3cc2e
ENH: Add helper script to test workflow
mgxd Jul 20, 2021
4acaa31
FIX: more faulty connections
mgxd Jul 20, 2021
d0851fc
ENH: Run in MultiProc, assign default nipype configuration
mgxd Jul 21, 2021
662058d
FIX: Make outputs more wb_command friendly
mgxd Jul 21, 2021
618ae58
FIX: Various bugs
mgxd Jul 22, 2021
7861ad2
FIX: Missing required values, use patched `CiftiSmooth`
mgxd Jul 22, 2021
a3ae7cb
DBG: Set to linear to avoid memory issues
mgxd Jul 22, 2021
147f42b
FIX: Doctests
mgxd Jul 22, 2021
559f7ea
FIX: Improve out_file name
mgxd Jul 22, 2021
630d0f5
STY: PEP8
mgxd Jul 23, 2021
3d909ab
ENH: Reduce memory requirement
mgxd Jul 23, 2021
422fe62
FIX: Cap ApplyXFM memory usage
mgxd Jul 23, 2021
acf5450
ENH: Allow setting nipype plugin via CLI
mgxd Jul 23, 2021
d7eda9f
Update nibabies/workflows/bold/alignment.py
mgxd Jul 24, 2021
72a7a69
Update nibabies/workflows/bold/alignment.py
mgxd Jul 24, 2021
0c6000c
Update nibabies/workflows/bold/alignment.py
mgxd Jul 24, 2021
e42c7d9
RF: Create dedicated interface for ROI combination
mgxd Jul 24, 2021
08529f4
ENH: Add unit test for MergeROIs interface
mgxd Jul 24, 2021
1aed572
FIX: Cleanup unused parameter, docstring
mgxd Jul 26, 2021
10f1f45
STY: Black
mgxd Jul 26, 2021
bbeb265
ENH: Use available transform, set volumetric sigma as parameter
mgxd Jul 26, 2021
9fe1243
FIX: Keep testing script updated
mgxd Jul 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions nibabies/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,12 @@ def init_spaces(checkpoint=True):
if checkpoint and not spaces.is_cached():
spaces.checkpoint()

if workflow.age_months is not None:
from .utils.misc import cohort_by_months

if "MNIInfant" not in spaces.get_spaces(nonstandard=False, dim=(3,)):
cohort = cohort_by_months("MNIInfant", workflow.age_months)
spaces.add(Reference("MNIInfant", {"cohort": cohort}))
# # Add the default standard space if not already present (required by several sub-workflows)
# if "MNI152NLin2009cAsym" not in spaces.get_spaces(nonstandard=False, dim=(3,)):
# spaces.add(Reference("MNI152NLin2009cAsym", {}))
Expand All @@ -695,10 +701,9 @@ def init_spaces(checkpoint=True):
# # Make sure there's a normalization to FSL for AROMA to use.
# spaces.add(Reference("MNI152NLin6Asym", {"res": "2"}))

cifti_output = workflow.cifti_output
if workflow.cifti_output:
# CIFTI grayordinates to corresponding FSL-MNI resolutions.
vol_res = "2" if cifti_output == "91k" else "1"
vol_res = "2" if workflow.cifti_output == "91k" else "1"
spaces.add(Reference("fsaverage", {"den": "164k"}))
spaces.add(Reference("MNI152NLin6Asym", {"res": vol_res}))

Expand Down
4 changes: 3 additions & 1 deletion nibabies/interfaces/conftest.py → nibabies/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""py.test configuration"""
from pathlib import Path
import pytest
from tempfile import TemporaryDirectory
from pkg_resources import resource_filename

import pytest

FILES = (
'functional.nii',
Expand Down Expand Up @@ -32,3 +33,4 @@ def data_dir():
@pytest.fixture(autouse=True)
def set_namespace(doctest_namespace, data_dir):
doctest_namespace["data_dir"] = data_dir
doctest_namespace["test_data"] = Path(resource_filename("nibabies", "tests/data"))
57 changes: 57 additions & 0 deletions nibabies/interfaces/nibabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
BaseInterfaceInputSpec,
File,
SimpleInterface,
InputMultiObject,
)


Expand Down Expand Up @@ -34,6 +35,24 @@ def _run_interface(self, runtime):
return runtime


class MergeROIsInputSpec(BaseInterfaceInputSpec):
in_files = InputMultiObject(File(exists=True), desc="ROI files to be merged")


class MergeROIsOutputSpec(TraitedSpec):
out_file = File(exists=True, desc="NIfTI containing all ROIs")


class MergeROIs(SimpleInterface):
"""Combine multiple region of interest files (3D or 4D) into a single file"""
input_spec = MergeROIsInputSpec
output_spec = MergeROIsOutputSpec

def _run_interface(self, runtime):
self._results["out_file"] = _merge_rois(self.inputs.in_files, newpath=runtime.cwd)
return runtime


def _dilate(in_file, radius=3, iterations=1, newpath=None):
"""Dilate (binary) input mask."""
from pathlib import Path
Expand All @@ -55,3 +74,41 @@ def _dilate(in_file, radius=3, iterations=1, newpath=None):
out_file = fname_presuffix(in_file, suffix="_dil", newpath=newpath or Path.cwd())
mask.__class__(newdata.astype("uint8"), mask.affine, hdr).to_filename(out_file)
return out_file


def _merge_rois(in_files, newpath=None):
"""
Aggregate individual 4D ROI files together into a single subcortical NIfTI.
All ROI images are sanity checked with regards to:
1) Shape
2) Affine
3) Overlap

If any of these checks fail, an ``AssertionError`` will be raised.
"""
from pathlib import Path
import nibabel as nb
import numpy as np

img = nb.load(in_files[0])
data = np.array(img.dataobj)
affine = img.affine
header = img.header

nonzero = np.any(data, axis=3)
for roi in in_files[1:]:
img = nb.load(roi)
assert img.shape == data.shape, "Mismatch in image shape"
assert np.allclose(img.affine, affine), "Mismatch in affine"
roi_data = np.asanyarray(img.dataobj)
roi_nonzero = np.any(roi_data, axis=3)
assert not np.any(roi_nonzero & nonzero), "Overlapping ROIs"
nonzero |= roi_nonzero
data += roi_data
del roi_data

if newpath is None:
newpath = Path()
out_file = str((Path(newpath) / "combined.nii.gz").absolute())
img.__class__(data, affine, header).to_filename(out_file)
return out_file
Empty file.
73 changes: 73 additions & 0 deletions nibabies/interfaces/tests/test_nibabel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import uuid

import nibabel as nb
import numpy as np
import pytest

from ..nibabel import MergeROIs


@pytest.fixture
def create_roi(tmp_path):
files = []

def _create_roi(affine, img_data, roi_index):
img_data[tuple(roi_index)] = 1
nii = nb.Nifti1Image(img_data, affine)
filename = tmp_path / f"{str(uuid.uuid4())}.nii.gz"
files.append(filename)
nii.to_filename(filename)
return filename

yield _create_roi

for f in files:
f.unlink()


# create a slightly off affine
bad_affine = np.eye(4)
bad_affine[0, -1] = -1


@pytest.mark.parametrize(
"affine, data, roi_index, error, err_message",
[
(np.eye(4), np.zeros((2, 2, 2, 2), dtype=int), [1, 0], None, None),
(
np.eye(4),
np.zeros((2, 2, 3, 2), dtype=int),
[1, 0],
True,
"Mismatch in image shape",
),
(
bad_affine,
np.zeros((2, 2, 2, 2), dtype=int),
[1, 0],
True,
"Mismatch in affine",
),
(
np.eye(4),
np.zeros((2, 2, 2, 2), dtype=int),
[0, 0, 0],
True,
"Overlapping ROIs",
),
],
)
def test_merge_rois(tmpdir, create_roi, affine, data, roi_index, error, err_message):
tmpdir.chdir()
roi0 = create_roi(np.eye(4), np.zeros((2, 2, 2, 2), dtype=int), [0, 0])
roi1 = create_roi(np.eye(4), np.zeros((2, 2, 2, 2), dtype=int), [0, 1])
test_roi = create_roi(affine, data, roi_index)

merge = MergeROIs(in_files=[roi0, roi1, test_roi])
if error is None:
merge.run()
return
# otherwise check expected exceptions
with pytest.raises(AssertionError) as err:
merge.run()
assert err_message in str(err.value)
Loading