-
Notifications
You must be signed in to change notification settings - Fork 285
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
[ENH] Efforts towards keeping memory low #807
Changes from 16 commits
d2d1dab
61a6feb
0cfffcf
4073b0c
84caba8
ca3ffdc
3d56ac5
87366f1
37bc9d9
d140328
08d807d
ff78055
e03cfb0
fc25c04
4cc627e
b8b5696
74de82a
64ce662
e6bc9af
e7b8357
6d5167f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,17 +14,19 @@ | |
import scipy.ndimage as nd | ||
from nilearn.image import resample_to_img | ||
|
||
from niworkflows.nipype import logging | ||
from niworkflows.nipype.utils.filemanip import fname_presuffix | ||
from niworkflows.nipype.interfaces.base import ( | ||
traits, isdefined, TraitedSpec, BaseInterfaceInputSpec, File, InputMultiPath, | ||
SimpleInterface | ||
) | ||
|
||
IFLOGGER = logging.getLogger('interfaces') | ||
|
||
|
||
class TPM2ROIInputSpec(BaseInterfaceInputSpec): | ||
t1_tpm = File(exists=True, mandatory=True, desc='Tissue probability map file in T1 space') | ||
t1_mask = File(exists=True, mandatory=True, desc='Binary mask of skull-stripped T1w image') | ||
bold_mask = File(exists=True, mandatory=True, desc='Binary mask of skull-stripped BOLD image') | ||
in_tpm = File(exists=True, mandatory=True, desc='Tissue probability map file in T1 space') | ||
in_mask = File(exists=True, mandatory=True, desc='Binary mask of skull-stripped T1w image') | ||
mask_erode_mm = traits.Float(xor=['mask_erode_prop'], | ||
desc='erode input mask (kernel width in mm)') | ||
erode_mm = traits.Float(xor=['erode_prop'], | ||
|
@@ -47,13 +49,10 @@ class TPM2ROI(SimpleInterface): | |
|
||
This interface follows the following logic: | ||
|
||
#. Erode ``t1_mask`` by ``mask_erode_mm`` and apply to ``t1_tpm`` | ||
#. Erode ``in_mask`` by ``mask_erode_mm`` and apply to ``in_tpm`` | ||
#. Threshold masked TPM at ``prob_thresh`` | ||
#. Erode resulting mask by ``erode_mm`` | ||
|
||
Both the eroded brain mask and eroded ROI mask are then resampled to BOLD | ||
resolution, and masked by ``bold_mask``. | ||
|
||
""" | ||
|
||
input_spec = TPM2ROIInputSpec | ||
|
@@ -73,9 +72,8 @@ def _run_interface(self, runtime): | |
if not isdefined(erode_prop): | ||
erode_prop = None | ||
roi_file, eroded_mask = _tpm2roi( | ||
self.inputs.t1_tpm, | ||
self.inputs.t1_mask, | ||
self.inputs.bold_mask, | ||
self.inputs.in_tpm, | ||
self.inputs.in_mask, | ||
mask_erode_mm, | ||
erode_mm, | ||
mask_erode_prop, | ||
|
@@ -87,49 +85,49 @@ def _run_interface(self, runtime): | |
return runtime | ||
|
||
|
||
class CombineROIsInputSpec(BaseInterfaceInputSpec): | ||
class AddTPMsInputSpec(BaseInterfaceInputSpec): | ||
in_files = InputMultiPath(File(exists=True), mandatory=True, desc='input list of ROIs') | ||
ref_header = File(exists=True, mandatory=True, | ||
desc='reference NIfTI file with desired output header/affine') | ||
indexes = traits.List(traits.Int, desc='select specific maps') | ||
|
||
|
||
class CombineROIsOutputSpec(TraitedSpec): | ||
class AddTPMsOutputSpec(TraitedSpec): | ||
out_file = File(exists=True, desc='union of binarized input files') | ||
|
||
|
||
class CombineROIs(SimpleInterface): | ||
class AddTPMs(SimpleInterface): | ||
"""Generate the union (logical or) of a series of ROI masks""" | ||
input_spec = CombineROIsInputSpec | ||
output_spec = CombineROIsOutputSpec | ||
input_spec = AddTPMsInputSpec | ||
output_spec = AddTPMsOutputSpec | ||
|
||
def _run_interface(self, runtime): | ||
self._results['out_file'] = _combine_rois(self.inputs.in_files, self.inputs.ref_header) | ||
return runtime | ||
in_files = self.inputs.in_files | ||
|
||
indexes = list(range(len(in_files))) | ||
if isdefined(self.inputs.indexes): | ||
indexes = self.inputs.indexes | ||
|
||
class ConcatROIsInputSpec(BaseInterfaceInputSpec): | ||
in_file = File(exists=True, mandatory=True, desc='input file') | ||
in_mask = File(exists=True, mandatory=True, desc='input mask') | ||
ref_header = File(exists=True, mandatory=True, | ||
desc='reference NIfTI file with desired output header/affine') | ||
if len(self.inputs.in_files) < 2: | ||
self._results['out_file'] = in_files[0] | ||
return runtime | ||
|
||
first_fname = in_files[indexes[0]] | ||
if len(indexes) == 1: | ||
self._results['out_file'] = first_fname | ||
return runtime | ||
|
||
class ConcatROIsOutputSpec(TraitedSpec): | ||
out_file = File(exists=True, desc='output average file') | ||
im = nb.load(first_fname) | ||
data = im.get_data() | ||
|
||
for idx in indexes[1:]: | ||
data += nb.load(in_files[idx]).get_data() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The goal here is to avoid keeping N files in memory? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really, I was just avoiding the use of fsl.maths. This interface is just to add tissue probability maps which are small files by definition. Maybe I don't get where you want to get at... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just kind of wondering about the whole goal of this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. If you're not trying to save memory and you really want the sum and not the logical OR (the docstring needs fixing, btw), you can replace a lot of this logic (up to this point): def _run_interface(self, runtime):
indices = self.inputs.indices or slice(None)
in_files = np.array(self.inputs.in_files)[indices]
if len(in_files) == 1:
self._results['out_file'] = in_files[0]
return runtime
im = nb.concat_images(in_files)
data = im.get_data().sum(axis=3) |
||
|
||
class ConcatROIs(SimpleInterface): | ||
"""Concatenate two ROI files along time axis | ||
data[data > 1] = 1.0 | ||
data[data < 0] = 0.0 | ||
|
||
``in_file`` is resampled to match ``in_mask``, and the two concatenated | ||
datasets are saved with the affine and header of ``ref_header``. | ||
""" | ||
input_spec = ConcatROIsInputSpec | ||
output_spec = ConcatROIsOutputSpec | ||
out_file = fname_presuffix(first_fname, suffix='_tpmsum') | ||
im.__class__(data, im.affine, im.header).to_filename(out_file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is boolean? Might be worth making sure that it's saved as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'm double checking. That should be a float. This interface works directly on the TPMs, and it should not change the original dtype. I think it is safe to leave it this way. |
||
self._results['out_file'] = out_file | ||
|
||
def _run_interface(self, runtime): | ||
self._results['out_file'] = _concat_rois( | ||
self.inputs.in_file, self.inputs.in_mask, self.inputs.ref_header) | ||
return runtime | ||
|
||
|
||
|
@@ -196,40 +194,36 @@ def _run_interface(self, runtime): | |
return runtime | ||
|
||
|
||
def _tpm2roi(t1_tpm, t1_mask, bold_mask, mask_erosion_mm=None, erosion_mm=None, | ||
def _tpm2roi(in_tpm, in_mask, mask_erosion_mm=None, erosion_mm=None, | ||
mask_erosion_prop=None, erosion_prop=None, pthres=0.95): | ||
""" | ||
Generate a mask from a tissue probability map | ||
""" | ||
tpm_img = nb.load(t1_tpm) | ||
bold_mask_img = nb.load(bold_mask) | ||
|
||
probability_map_data = (tpm_img.get_data() >= pthres).astype(np.uint8) | ||
tpm_img = nb.load(in_tpm) | ||
roi_mask = (tpm_img.get_data() >= pthres).astype(np.uint8) | ||
|
||
eroded_mask_file = bold_mask | ||
eroded_mask_file = None | ||
erode_in = (mask_erosion_mm is not None and mask_erosion_mm > 0 or | ||
mask_erosion_prop is not None and mask_erosion_prop < 1) | ||
if erode_in: | ||
eroded_mask_file = os.path.abspath("eroded_mask.nii.gz") | ||
|
||
mask_img = nb.load(t1_mask) | ||
eroded_mask_file = fname_presuffix(in_mask, suffix='_eroded') | ||
mask_img = nb.load(in_mask) | ||
mask_data = mask_img.get_data().astype(np.uint8) | ||
if mask_erosion_mm: | ||
iter_n = max(int(mask_erosion_mm / max(mask_img.header.get_zooms())), 1) | ||
mask_data = nd.binary_erosion(mask_img.get_data().astype(np.uint8), iterations=iter_n) | ||
mask_data = nd.binary_erosion(mask_data, iterations=iter_n) | ||
else: | ||
mask_data = mask_img.get_data().astype(np.uint8) | ||
orig_vol = np.sum(mask_data > 0) | ||
while np.sum(mask_data > 0) / orig_vol > mask_erosion_prop: | ||
mask_data = nd.binary_erosion(mask_data, iterations=1) | ||
|
||
# Resample to BOLD and save | ||
eroded_t1 = nb.Nifti1Image(mask_data, mask_img.affine, mask_img.header) | ||
eroded_t1.set_data_dtype(np.uint8) | ||
eroded_bold = _resample_and_mask(eroded_t1, bold_mask_img, interpolation='nearest') | ||
eroded_bold.to_filename(eroded_mask_file) | ||
# Store mask | ||
eroded = nb.Nifti1Image(mask_data, mask_img.affine, mask_img.header) | ||
eroded.set_data_dtype(np.uint8) | ||
eroded.to_filename(eroded_mask_file) | ||
|
||
# Mask TPM data (no effect if not eroded) | ||
probability_map_data[~mask_data] = 0 | ||
roi_mask[~mask_data] = 0 | ||
|
||
# shrinking | ||
erode_out = (erosion_mm is not None and erosion_mm > 0 or | ||
|
@@ -238,19 +232,18 @@ def _tpm2roi(t1_tpm, t1_mask, bold_mask, mask_erosion_mm=None, erosion_mm=None, | |
if erosion_mm: | ||
iter_n = max(int(erosion_mm / max(tpm_img.header.get_zooms())), 1) | ||
iter_n = int(erosion_mm / max(tpm_img.header.get_zooms())) | ||
probability_map_data = nd.binary_erosion(probability_map_data, iterations=iter_n) | ||
roi_mask = nd.binary_erosion(roi_mask, iterations=iter_n) | ||
else: | ||
orig_vol = np.sum(probability_map_data > 0) | ||
while np.sum(probability_map_data > 0) / orig_vol > erosion_prop: | ||
probability_map_data = nd.binary_erosion(probability_map_data, iterations=1) | ||
orig_vol = np.sum(roi_mask > 0) | ||
while np.sum(roi_mask > 0) / orig_vol > erosion_prop: | ||
roi_mask = nd.binary_erosion(roi_mask, iterations=1) | ||
|
||
# Create image to resample | ||
img_t1 = nb.Nifti1Image(probability_map_data, tpm_img.affine, tpm_img.header) | ||
img_t1.set_data_dtype(np.uint8) | ||
roi_img = _resample_and_mask(img_t1, bold_mask_img, interpolation='nearest') | ||
|
||
roi_img.to_filename("roi.nii.gz") | ||
return os.path.abspath("roi.nii.gz"), eroded_mask_file | ||
roi_fname = fname_presuffix(in_tpm, suffix='_roi') | ||
roi_img = nb.Nifti1Image(roi_mask, tpm_img.affine, tpm_img.header) | ||
roi_img.set_data_dtype(np.uint8) | ||
roi_img.to_filename(roi_fname) | ||
return roi_fname, eroded_mask_file or in_mask | ||
|
||
|
||
def _resample_and_mask(source_img, mask_img, interpolation='continuous'): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar nitpick: indices