Repository: nilearn/nilearn

Issue:
Add `allow_overlap` in NiftiMapsMasker, NiftiSpheresMasker
In NiftiMapsMasker, some people want to prevent any overlap. Such option can be useful. On the other hand, some people may want to allow overlapping spheres in NiftiSpheresMasker.



Discussion:

Resolving patch:
diff --git a/nilearn/input_data/nifti_maps_masker.py b/nilearn/input_data/nifti_maps_masker.py
--- a/nilearn/input_data/nifti_maps_masker.py
+++ b/nilearn/input_data/nifti_maps_masker.py
@@ -2,10 +2,12 @@
 Transformer for computing ROI signals.
 """
 
+import numpy as np
 from sklearn.externals.joblib import Memory
 
 from .. import _utils
 from .._utils import logger, CacheMixin
+from .._utils.niimg import _get_data_dtype
 from .._utils.class_inspect import get_params
 from .._utils.niimg_conversions import _check_same_fov
 from .. import region
@@ -45,6 +47,10 @@ class NiftiMapsMasker(BaseMasker, CacheMixin):
         See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg.
         Mask to apply to regions before extracting signals.
 
+    allow_overlap: boolean, optional
+        If False, an error is raised if the maps overlaps (ie at least two
+        maps have a non-zero value for the same voxel). Default is True.
+
     smoothing_fwhm: float, optional
         If smoothing_fwhm is not None, it gives the full-width half maximum in
         millimeters of the spatial smoothing to apply to the signal.
@@ -102,6 +108,7 @@ class NiftiMapsMasker(BaseMasker, CacheMixin):
     # memory and memory_level are used by CacheMixin.
 
     def __init__(self, maps_img, mask_img=None,
+                 allow_overlap=True,
                  smoothing_fwhm=None, standardize=False, detrend=False,
                  low_pass=None, high_pass=None, t_r=None,
                  resampling_target="data",
@@ -110,6 +117,9 @@ def __init__(self, maps_img, mask_img=None,
         self.maps_img = maps_img
         self.mask_img = mask_img
 
+        # Maps Masker parameter
+        self.allow_overlap = allow_overlap
+
         # Parameters for image.smooth
         self.smoothing_fwhm = smoothing_fwhm
 
@@ -258,6 +268,23 @@ def transform_single_imgs(self, imgs, confounds=None):
                         target_shape=ref_img.shape[:3],
                         target_affine=ref_img.get_affine())
 
+        if not self.allow_overlap:
+            # Check if there is an overlap.
+
+            # If float, we set low values to 0
+            dtype = _get_data_dtype(self._resampled_maps_img_)
+            data = self._resampled_maps_img_.get_data()
+            if dtype.kind == 'f':
+                data[data < np.finfo(dtype).eps] = 0.
+
+            # Check the overlaps
+            if np.any(np.sum(data > 0., axis=3) > 1):
+                raise ValueError(
+                    'Overlap detected in the maps. The overlap may be '
+                    'due to the atlas itself or possibly introduced by '
+                    'resampling'
+                )
+
         target_shape = None
         target_affine = None
         if self.resampling_target != 'data':
diff --git a/nilearn/input_data/nifti_spheres_masker.py b/nilearn/input_data/nifti_spheres_masker.py
--- a/nilearn/input_data/nifti_spheres_masker.py
+++ b/nilearn/input_data/nifti_spheres_masker.py
@@ -17,7 +17,33 @@
 from .base_masker import filter_and_extract, BaseMasker
 
 
-def _iter_signals_from_spheres(seeds, niimg, radius, mask_img=None):
+def _iter_signals_from_spheres(seeds, niimg, radius, allow_overlap,
+                               mask_img=None):
+    """Utility function to iterate over spheres.
+
+    Parameters
+    ==========
+    seeds: List of triplet of coordinates in native space
+        Seed definitions. List of coordinates of the seeds in the same space
+        as the images (typically MNI or TAL).
+
+    imgs: 3D/4D Niimg-like object
+        See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg.
+        Images to process. It must boil down to a 4D image with scans
+        number as last dimension.
+
+    radius: float, optional
+        Indicates, in millimeters, the radius for the sphere around the seed.
+        Default is None (signal is extracted on a single voxel).
+
+    allow_overlap: boolean
+        If False, an error is raised if the maps overlaps (ie at least two
+        maps have a non-zero value for the same voxel). Default is False.
+
+    mask_img: Niimg-like object, optional
+        See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg.
+        Mask to apply to regions before extracting signals.
+    """
     seeds = list(seeds)
     affine = niimg.get_affine()
 
@@ -58,6 +84,10 @@ def _iter_signals_from_spheres(seeds, niimg, radius, mask_img=None):
             pass
     del mask_coords
 
+    if not allow_overlap:
+        if np.any(A.sum(axis=0) >= 2):
+            raise ValueError('Overlap detected between spheres')
+
     for i, row in enumerate(A.rows):
         if len(row) == 0:
             raise ValueError('Sphere around seed #%i is empty' % i)
@@ -68,10 +98,11 @@ class _ExtractionFunctor(object):
 
     func_name = 'nifti_spheres_masker_extractor'
 
-    def __init__(self, seeds_, radius, mask_img):
+    def __init__(self, seeds_, radius, mask_img, allow_overlap):
         self.seeds_ = seeds_
         self.radius = radius
         self.mask_img = mask_img
+        self.allow_overlap = allow_overlap
 
     def __call__(self, imgs):
         n_seeds = len(self.seeds_)
@@ -79,7 +110,8 @@ def __call__(self, imgs):
 
         signals = np.empty((imgs.shape[3], n_seeds))
         for i, sphere in enumerate(_iter_signals_from_spheres(
-                self.seeds_, imgs, self.radius, mask_img=self.mask_img)):
+                self.seeds_, imgs, self.radius, self.allow_overlap,
+                mask_img=self.mask_img)):
             signals[:, i] = np.mean(sphere, axis=1)
 
         return signals, None
@@ -106,6 +138,10 @@ class NiftiSpheresMasker(BaseMasker, CacheMixin):
         See http://nilearn.github.io/building_blocks/manipulating_mr_images.html#niimg.
         Mask to apply to regions before extracting signals.
 
+    allow_overlap: boolean, optional
+        If False, an error is raised if the maps overlaps (ie at least two
+        maps have a non-zero value for the same voxel). Default is False.
+
     smoothing_fwhm: float, optional
         If smoothing_fwhm is not None, it gives the full-width half maximum in
         millimeters of the spatial smoothing to apply to the signal.
@@ -148,7 +184,7 @@ class NiftiSpheresMasker(BaseMasker, CacheMixin):
     """
     # memory and memory_level are used by CacheMixin.
 
-    def __init__(self, seeds, radius=None, mask_img=None,
+    def __init__(self, seeds, radius=None, mask_img=None, allow_overlap=False,
                  smoothing_fwhm=None, standardize=False, detrend=False,
                  low_pass=None, high_pass=None, t_r=None,
                  memory=Memory(cachedir=None, verbose=0), memory_level=1,
@@ -156,6 +192,7 @@ def __init__(self, seeds, radius=None, mask_img=None,
         self.seeds = seeds
         self.mask_img = mask_img
         self.radius = radius
+        self.allow_overlap = allow_overlap
 
         # Parameters for _smooth_array
         self.smoothing_fwhm = smoothing_fwhm
@@ -250,7 +287,8 @@ def transform_single_imgs(self, imgs, confounds=None):
                 filter_and_extract,
                 ignore=['verbose', 'memory', 'memory_level'])(
             # Images
-            imgs, _ExtractionFunctor(self.seeds_, self.radius, self.mask_img),
+            imgs, _ExtractionFunctor(self.seeds_, self.radius, self.mask_img,
+                                     self.allow_overlap),
             # Pre-processing
             params,
             confounds=confounds,


Test patch:
diff --git a/nilearn/input_data/tests/test_nifti_maps_masker.py b/nilearn/input_data/tests/test_nifti_maps_masker.py
--- a/nilearn/input_data/tests/test_nifti_maps_masker.py
+++ b/nilearn/input_data/tests/test_nifti_maps_masker.py
@@ -1,7 +1,7 @@
 """Test the nifti_region module
 
 Functions in this file only test features added by the NiftiLabelsMasker class,
-not the underlying functions (clean(), img_to_signals_labels(), etc.). See
+non_overlappingt the underlying functions (clean(), img_to_signals_labels(), etc.). See
 test_masking.py and test_signal.py for details.
 """
 
@@ -13,7 +13,7 @@
 from nilearn.input_data.nifti_maps_masker import NiftiMapsMasker
 from nilearn._utils import testing, as_ndarray
 from nilearn._utils.exceptions import DimensionError
-from nilearn._utils.testing import assert_less
+from nilearn._utils.testing import assert_less, assert_raises_regex
 
 
 def generate_random_img(shape, length=1, affine=np.eye(4),
@@ -233,3 +233,38 @@ def test_nifti_maps_masker_2():
                                    masker.maps_img_.get_affine())
     assert_equal(fmri11_img_r.shape,
                  (masker.maps_img_.shape[:3] + (length,)))
+
+
+def test_nifti_maps_masker_overlap():
+    # Test resampling in NiftiMapsMasker
+    affine = np.eye(4)
+    shape = (5, 5, 5)
+    length = 10
+
+    fmri_img, _ = generate_random_img(shape, affine=affine,
+                                      length=length)
+    non_overlapping_maps = np.zeros(shape + (2,))
+    non_overlapping_maps[:2, :, :, 0] = 1.
+    non_overlapping_maps[2:, :, :, 1] = 1.
+    non_overlapping_maps_img = nibabel.Nifti1Image(non_overlapping_maps,
+                                                   affine)
+
+    overlapping_maps = np.zeros(shape + (2,))
+    overlapping_maps[:3, :, :, 0] = 1.
+    overlapping_maps[2:, :, :, 1] = 1.
+    overlapping_maps_img = nibabel.Nifti1Image(overlapping_maps, affine)
+
+    overlapping_masker = NiftiMapsMasker(non_overlapping_maps_img,
+                                         allow_overlap=True)
+    overlapping_masker.fit_transform(fmri_img)
+    overlapping_masker = NiftiMapsMasker(overlapping_maps_img,
+                                         allow_overlap=True)
+    overlapping_masker.fit_transform(fmri_img)
+
+    non_overlapping_masker = NiftiMapsMasker(non_overlapping_maps_img,
+                                             allow_overlap=False)
+    non_overlapping_masker.fit_transform(fmri_img)
+    non_overlapping_masker = NiftiMapsMasker(overlapping_maps_img,
+                                             allow_overlap=False)
+    assert_raises_regex(ValueError, 'Overlap detected',
+                        non_overlapping_masker.fit_transform, fmri_img)
diff --git a/nilearn/input_data/tests/test_nifti_spheres_masker.py b/nilearn/input_data/tests/test_nifti_spheres_masker.py
--- a/nilearn/input_data/tests/test_nifti_spheres_masker.py
+++ b/nilearn/input_data/tests/test_nifti_spheres_masker.py
@@ -70,3 +70,25 @@ def test_anisotropic_sphere_extraction():
 def test_errors():
     masker = NiftiSpheresMasker(([1, 2]), radius=.2)
     assert_raises_regex(ValueError, 'Seeds must be a list .+', masker.fit)
+
+
+def test_nifti_spheres_masker_overlap():
+    # Test resampling in NiftiMapsMasker
+    affine = np.eye(4)
+    shape = (5, 5, 5)
+
+    data = np.random.random(shape + (5,))
+    fmri_img = nibabel.Nifti1Image(data, affine)
+
+    seeds = [(0, 0, 0), (2, 2, 2)]
+
+    o_masker = NiftiSpheresMasker(seeds, radius=1, allow_overlap=True)
+    o_masker.fit_transform(fmri_img)
+    o_masker = NiftiSpheresMasker(seeds, radius=2, allow_overlap=True)
+    o_masker.fit_transform(fmri_img)
+
+    no_masker = NiftiSpheresMasker(seeds, radius=1, allow_overlap=False)
+    no_masker.fit_transform(fmri_img)
+    no_masker = NiftiSpheresMasker(seeds, radius=2, allow_overlap=False)
+    assert_raises_regex(ValueError, 'Overlap detected',
+                        no_masker.fit_transform, fmri_img)