Repository: nilearn/nilearn

Issue:
NiftiSpheresMasker crashes when radius is small
When trying this:

``` python
from nilearn.datasets import fetch_adhd
from nilearn.input_data import NiftiSpheresMasker

dataset = fetch_adhd(n_subjects=1)
dmn_coords = [(0, -52, 18), (-46, -68, 32), (46, -68, 32), (0, 50, -5)]
masker = NiftiSpheresMasker(seeds=dmn_coords, radius=1)
data = masker.fit_transform(dataset.func[0])
```

The spheres are then empty.



Discussion:
@mrahim small tip, you can use python after the triple back-quotes to have color highlighting in your snippet.

Other than that I can reproduce, here is the full traceback:

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-8-78da5de8a645> in <module>()
----> 1 data = masker.fit_transform(dataset.func[0])

/home/lesteve/dev/nilearn/nilearn/input_data/nifti_spheres_masker.py in fit_transform(self, imgs, confounds)
    256     def fit_transform(self, imgs, confounds=None):
    257         """Prepare and perform signal extraction"""
--> 258         return self.fit().transform(imgs, confounds=confounds)
    259 
    260     def _check_fitted(self):

/home/lesteve/dev/nilearn/nilearn/input_data/base_masker.py in transform(self, imgs, confounds)
    174         self._check_fitted()
    175 
--> 176         return self.transform_single_imgs(imgs, confounds)
    177 
    178     def fit_transform(self, X, y=None, confounds=None, **fit_params):

/home/lesteve/dev/nilearn/nilearn/input_data/nifti_spheres_masker.py in transform_single_imgs(self, imgs, confounds)
    302             memory_level=self.memory_level,
    303             # kwargs
--> 304             verbose=self.verbose)
    305 
    306         return signals

/home/lesteve/miniconda3/lib/python3.4/site-packages/sklearn/externals/joblib/memory.py in __call__(self, *args, **kwargs)
    279 
    280     def __call__(self, *args, **kwargs):
--> 281         return self.func(*args, **kwargs)
    282 
    283     def call_and_shelve(self, *args, **kwargs):

/home/lesteve/dev/nilearn/nilearn/input_data/base_masker.py in filter_and_extract(imgs, extraction_function, parameters, memory_level, memory, verbose, confounds, copy)
     96     region_signals, aux = cache(extraction_function, memory,
     97                                 func_memory_level=2,
---> 98                                 memory_level=memory_level)(imgs)
     99 
    100     # Temporal

/home/lesteve/miniconda3/lib/python3.4/site-packages/sklearn/externals/joblib/memory.py in __call__(self, *args, **kwargs)
    279 
    280     def __call__(self, *args, **kwargs):
--> 281         return self.func(*args, **kwargs)
    282 
    283     def call_and_shelve(self, *args, **kwargs):

/home/lesteve/dev/nilearn/nilearn/input_data/nifti_spheres_masker.py in __call__(self, imgs)
    116         for i, sphere in enumerate(_iter_signals_from_spheres(
    117                 self.seeds_, imgs, self.radius, self.allow_overlap,
--> 118                 mask_img=self.mask_img)):
    119             signals[:, i] = np.mean(sphere, axis=1)
    120 

/home/lesteve/dev/nilearn/nilearn/input_data/nifti_spheres_masker.py in _iter_signals_from_spheres(seeds, niimg, radius, allow_overlap, mask_img)
     95     for i, row in enumerate(A.rows):
     96         if len(row) == 0:
---> 97             raise ValueError('Sphere around seed #%i is empty' % i)
     98         yield X[:, row]
     99 

ValueError: Sphere around seed #1 is empty
```


Resolving patch:
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
@@ -31,13 +31,25 @@ def _apply_mask_and_get_affinity(seeds, niimg, radius, allow_overlap,
                                       target_shape=niimg.shape[:3],
                                       interpolation='nearest')
         mask, _ = masking._load_mask_img(mask_img)
-        mask_coords = list(np.where(mask != 0))
+        mask_coords = list(zip(*np.where(mask != 0)))
 
         X = masking._apply_mask_fmri(niimg, mask_img)
     else:
-        mask_coords = list(zip(*np.ndindex(niimg.shape[:3])))
+        mask_coords = list(np.ndindex(niimg.shape[:3]))
         X = niimg.get_data().reshape([-1, niimg.shape[3]]).T
-    mask_coords = np.asarray(mask_coords)
+
+    # For each seed, get coordinates of nearest voxel
+    nearests = []
+    for sx, sy, sz in seeds:
+        nearest = np.round(coord_transform(sx, sy, sz, np.linalg.inv(affine)))
+        nearest = nearest.astype(int)
+        nearest = (nearest[0], nearest[1], nearest[2])
+        try:
+            nearests.append(mask_coords.index(nearest))
+        except ValueError:
+            nearests.append(None)
+
+    mask_coords = np.asarray(list(zip(*mask_coords)))
     mask_coords = coord_transform(mask_coords[0], mask_coords[1],
                                   mask_coords[2], affine)
     mask_coords = np.asarray(mask_coords).T
@@ -51,7 +63,12 @@ def _apply_mask_and_get_affinity(seeds, niimg, radius, allow_overlap,
     clf = neighbors.NearestNeighbors(radius=radius)
     A = clf.fit(mask_coords).radius_neighbors_graph(seeds)
     A = A.tolil()
-    # Include selfs
+    for i, nearest in enumerate(nearests):
+        if nearest is None:
+            continue
+        A[i, nearest] = True
+
+    # Include the voxel containing the seed itself if not masked
     mask_coords = mask_coords.astype(int).tolist()
     for i, seed in enumerate(seeds):
         try:


Test patch:
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
@@ -82,13 +82,47 @@ def test_nifti_spheres_masker_overlap():
 
     seeds = [(0, 0, 0), (2, 2, 2)]
 
-    overlapping_masker = NiftiSpheresMasker(seeds, radius=1, allow_overlap=True)
+    overlapping_masker = NiftiSpheresMasker(seeds, radius=1,
+                                            allow_overlap=True)
     overlapping_masker.fit_transform(fmri_img)
-    overlapping_masker = NiftiSpheresMasker(seeds, radius=2, allow_overlap=True)
+    overlapping_masker = NiftiSpheresMasker(seeds, radius=2,
+                                            allow_overlap=True)
     overlapping_masker.fit_transform(fmri_img)
 
-    noverlapping_masker = NiftiSpheresMasker(seeds, radius=1, allow_overlap=False)
+    noverlapping_masker = NiftiSpheresMasker(seeds, radius=1,
+                                             allow_overlap=False)
     noverlapping_masker.fit_transform(fmri_img)
-    noverlapping_masker = NiftiSpheresMasker(seeds, radius=2, allow_overlap=False)
+    noverlapping_masker = NiftiSpheresMasker(seeds, radius=2,
+                                             allow_overlap=False)
     assert_raises_regex(ValueError, 'Overlap detected',
                         noverlapping_masker.fit_transform, fmri_img)
+
+
+def test_small_radius():
+    affine = np.eye(4)
+    shape = (3, 3, 3)
+
+    data = np.random.random(shape)
+    mask = np.zeros(shape)
+    mask[1, 1, 1] = 1
+    mask[2, 2, 2] = 1
+    affine = np.eye(4) * 1.2
+    seed = (1.4, 1.4, 1.4)
+
+    masker = NiftiSpheresMasker([seed], radius=0.1,
+                                mask_img=nibabel.Nifti1Image(mask, affine))
+    masker.fit_transform(nibabel.Nifti1Image(data, affine))
+
+    # Test if masking is taken into account
+    mask[1, 1, 1] = 0
+    mask[1, 1, 0] = 1
+
+    masker = NiftiSpheresMasker([seed], radius=0.1,
+                                mask_img=nibabel.Nifti1Image(mask, affine))
+    assert_raises_regex(ValueError, 'Sphere around seed #0 is empty',
+                        masker.fit_transform,
+                        nibabel.Nifti1Image(data, affine))
+
+    masker = NiftiSpheresMasker([seed], radius=1.6,
+                                mask_img=nibabel.Nifti1Image(mask, affine))
+    masker.fit_transform(nibabel.Nifti1Image(data, affine))