diff --git a/nibabies/data/FreeSurferLabelRemappings.json b/nibabies/data/FreeSurferLabelRemappings.json new file mode 100644 index 00000000..23f29827 --- /dev/null +++ b/nibabies/data/FreeSurferLabelRemappings.json @@ -0,0 +1,25 @@ +{ + "173": 16, + "174": 16, + "175": 16, + + "500": 53, + "501": 53, + "502": 53, + "503": 53, + "504": 53, + "505": 53, + "506": 53, + "507": 53, + "508": 53, + + "550": 17, + "551": 17, + "552": 17, + "553": 17, + "554": 17, + "555": 17, + "556": 17, + "557": 17, + "558": 17 +} diff --git a/nibabies/interfaces/nibabel.py b/nibabies/interfaces/nibabel.py deleted file mode 100644 index d79acf29..00000000 --- a/nibabies/interfaces/nibabel.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Nibabel-based interfaces to eventually upstream to NiWorkflows.""" -from nipype.interfaces.base import ( - traits, - TraitedSpec, - BaseInterfaceInputSpec, - File, - SimpleInterface, - InputMultiObject, -) - - -class _BinaryDilationInputSpec(BaseInterfaceInputSpec): - in_file = File(exists=True, mandatory=True, desc="binary file to dilate") - radius = traits.Float(3, usedefault=True, desc="structure element (ball) radius") - iterations = traits.Range(low=0, value=1, usedefault=True, desc="repeat dilation") - - -class _BinaryDilationOutputSpec(TraitedSpec): - out_file = File(exists=True, desc="the input file, after binary dilation") - - -class BinaryDilation(SimpleInterface): - """Morphological binary dilation using Scipy.""" - - input_spec = _BinaryDilationInputSpec - output_spec = _BinaryDilationOutputSpec - - def _run_interface(self, runtime): - self._results["out_file"] = _dilate( - self.inputs.in_file, - radius=self.inputs.radius, - iterations=self.inputs.iterations, - newpath=runtime.cwd, - ) - 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 - import numpy as np - import nibabel as nb - from scipy import ndimage - from skimage.morphology import ball - from nipype.utils.filemanip import fname_presuffix - - mask = nb.load(in_file) - newdata = ndimage.binary_dilation( - np.asanyarray(mask.dataobj) > 0, - structure=ball(radius), - iterations=iterations, - ) - - hdr = mask.header.copy() - hdr.set_data_dtype("uint8") - 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 diff --git a/nibabies/interfaces/tests/test_nibabel.py b/nibabies/interfaces/tests/test_nibabel.py deleted file mode 100644 index 4b40d384..00000000 --- a/nibabies/interfaces/tests/test_nibabel.py +++ /dev/null @@ -1,73 +0,0 @@ -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) diff --git a/nibabies/workflows/anatomical/brain_extraction.py b/nibabies/workflows/anatomical/brain_extraction.py index 645a9216..e0dcd108 100644 --- a/nibabies/workflows/anatomical/brain_extraction.py +++ b/nibabies/workflows/anatomical/brain_extraction.py @@ -80,14 +80,13 @@ def init_infant_brain_extraction_wf( from nipype.interfaces.ants import N4BiasFieldCorrection, ImageMath # niworkflows - from niworkflows.interfaces.nibabel import ApplyMask, Binarize, IntensityClip + from niworkflows.interfaces.nibabel import ApplyMask, Binarize, IntensityClip, BinaryDilation from niworkflows.interfaces.fixes import ( FixHeaderRegistration as Registration, FixHeaderApplyTransforms as ApplyTransforms, ) from templateflow.api import get as get_template - from ...interfaces.nibabel import BinaryDilation from ...utils.misc import cohort_by_months # handle template specifics diff --git a/nibabies/workflows/anatomical/registration.py b/nibabies/workflows/anatomical/registration.py index c3fda7cc..0ac73a82 100644 --- a/nibabies/workflows/anatomical/registration.py +++ b/nibabies/workflows/anatomical/registration.py @@ -81,8 +81,7 @@ def init_coregistration_wf( FixHeaderRegistration as Registration, FixHeaderApplyTransforms as ApplyTransforms, ) - from niworkflows.interfaces.nibabel import ApplyMask, Binarize - from ...interfaces.nibabel import BinaryDilation + from niworkflows.interfaces.nibabel import ApplyMask, Binarize, BinaryDilation workflow = pe.Workflow(name) diff --git a/nibabies/workflows/bold/alignment.py b/nibabies/workflows/bold/alignment.py index e88afd12..306a465a 100644 --- a/nibabies/workflows/bold/alignment.py +++ b/nibabies/workflows/bold/alignment.py @@ -34,6 +34,7 @@ def init_subcortical_rois_wf(*, name="subcortical_rois_wf"): Subcortical ROIs in `MNI152NLin6Asym` space """ from templateflow.api import get as get_template + from niworkflows.interfaces.nibabel import MapLabels # TODO: Implement BOLD refinement once InfantFS outputs subj/mri/wmparc.mgz # The code is found at @@ -68,6 +69,13 @@ def init_subcortical_rois_wf(*, name="subcortical_rois_wf"): # name="applywarp_std" # ) + map_labels = pe.Node( + MapLabels( + mappings_file=resource_filename("nibabies", "data/FreeSurferLabelRemappings.json") + ), + name='map_labels', + ) + subcortical_labels = resource_filename( "nibabies", "data/FreeSurferSubcorticalLabelTableLut.txt" ) @@ -78,7 +86,8 @@ def init_subcortical_rois_wf(*, name="subcortical_rois_wf"): # fmt: off workflow.connect([ - (inputnode, refine_bold_rois, [("MNIInfant_aseg", "in_file")]), + (inputnode, map_labels, [("MNIInfant_aseg", "in_file")]), + (map_labels, refine_bold_rois, [("out_file", "in_file")]), # (applywarp_tpl, refine_std_rois, [("out_file", "in_file")]), (refine_bold_rois, outputnode, [("out_file", "MNIInfant_rois")]), ]) @@ -119,7 +128,7 @@ def init_subcortical_mni_alignment_wf(*, vol_sigma=0.8, name="subcortical_mni_al Volume file containing all labels """ from niworkflows.engine.workflows import LiterateWorkflow as Workflow - from ...interfaces.nibabel import MergeROIs + from niworkflows.interfaces.nibabel import MergeROIs from ...interfaces.workbench import ( CiftiCreateDenseTimeseries, CiftiCreateLabel, @@ -350,47 +359,3 @@ def format_agg_rois(rois): """ return rois[0], rois[1:], ("-add %s " * (len(rois) - 1)).strip() - - -def drop_labels(in_file): - """Drop non-subcortical labels""" - from pathlib import Path - import nibabel as nb - import numpy as np - from niworkflows.interfaces.cifti import _reorient_image - - # FreeSurfer LUT values - expected_labels = { - 8, - 10, - 11, - 12, - 13, - 16, - 17, - 18, - 26, - 28, - 47, - 49, - 50, - 51, - 52, - 53, - 54, - 58, - 60, - } - img = _reorient_image(nb.load(in_file), orientation="LAS") - hdr = img.header - data = np.asanyarray(img.dataobj).astype("int16") - hdr.set_data_dtype("int16") - labels = np.unique(data) - - for label in labels: - if label not in expected_labels: - data[data == label] = 0 - - out_file = str(Path("ROIs.nii.gz").absolute()) - img.__class__(data, img.affine, header=hdr).to_filename(out_file) - return out_file diff --git a/setup.cfg b/setup.cfg index 5b28366f..9c39ffea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,10 +24,10 @@ install_requires = nibabel >= 3.0.1 nipype >= 1.5.1 nitime - niworkflows ~= 1.4.0 + niworkflows ~= 1.5.0 numpy >= 1.16.5 pybids >= 0.12.1 - sdcflows ~= 2.0.7 + sdcflows ~= 2.0.12 smriprep ~= 0.8.1 templateflow >= 0.6.0 test_requires =