Repository: nilearn/nilearn

Issue:
extracting voxels timeseries within a sphere and using inverse_transform with NiftiSpheresMasker
Is there an appropriate way to extract voxels time series within spheres ? I am using  `iter_signals_from_spheres`

``` Python
import nibabel
from nilearn import datasets, input_data
from nilearn.input_data.nifti_spheres_masker import _iter_signals_from_spheres
adhd = datasets.fetch_adhd(n_subjects=1)
func_filename = adhd.func[0]
seeds_masker = input_data.NiftiSpheresMasker([(-6, 50, -1)], radius=6.)
seeds_masker.fit()
sphere_timeseries = _iter_signals_from_spheres(
    seeds_masker.seeds_, nibabel.load(func_filename),
    seeds_masker.radius,
    seeds_masker.allow_overlap).next()
```

I also want to go from array to image using `inverse_transform` but I got an error

``` Python
image = seeds_masker.inverse_transform(sphere_timeseries.T)
```

> 'NiftiSpheresMasker' object has no attribute 'mask_img_'



Discussion:
Yes, your code to get the signals should work.

The error you get is a design bug, actually. BaseMasker has a "valid" `inverse_transform` method though it should raise a NotImplemented error. NiftiSpheresMasker has no `inverse_transform` method.

Can't we have an `inverse_transform` method for `NiftiSpheresMasker` ?

> Can't we have an inverse_transform method for NiftiSpheresMasker ?

PR welcome!

For non-overlapping spheres, it should indeed be a quickfix. For non-overlapping... It's more complicated (as you can guess).

I don't have time to do that now, I spent my nilearn time on other things. If you need, I should be able to do a quick draft though.

I wanted it for a connectivity example I plan to PR to demo the impact of changing the radius in spheric ROIs.
OK I will take a look to see if it's not too hard for me :)

> I wanted it for a connectivity example I plan to PR to demo the impact of changing the radius in spheric ROIs.

If you have enough seeds, It would be interesting to use them to cluster the whole brain volume (so end up with non-spherical regions) and compare that to spheres.

OK I will try that too

> For non-overlapping spheres, it should indeed be a quickfix. For non-overlapping... It's more complicated (as you can guess)

I looked into it this afternoon and it looks complex for me. May be I can try to work on #1229 to become familiar with the inverse_transform concept

Hi,
it's kinda weird for me to bump this quite old topic. However, I've been looking for something like salma's solution at the beginning of the issue to extract voxel-wise TS from a sphere for a while and have to admit I didn't look at the solution close enough. 
I even hacked the NiftiSpheresMasker function to create spherical masks/niftis based on the parameters, which can then be used with the NiftiMasker or LabelsMasker, however, it's quite inefficient.

My two cents on this:
* I think in general (also for debugging) an inverse transform for the SpheresMasker might be quite interesting/important. I can try to deal with implementing the inverse transform, which can take long, as I am short on time currently (well the issues has been open for some years as well...). Also I think having it only for non-overlapping ROIs wouldn't be an issue at first? I.e. throwing an error, if ROIs overlap?
 
*  Would a small tutorial be helpful to show how to extract voxel-wise timeseries from spheres? I think this is a functionality former SPM users might also be looking for.

Best,
Simon
+1 for an inverse transform. 
Imho it's fine to use a stupid heuristic for overlapping spheres, at least at the beginning. To me, the least stupid solution is to assign voxels to the nearest sphere center.
Throwing an error for overlapping ROIs is bad: it will break people's pipeline for bad reasons.
+1 for a tutorial on that
Thx @SRSteinkamp 

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
@@ -8,10 +8,11 @@
 import warnings
 from sklearn import neighbors
 from joblib import Memory
+from scipy import sparse
 
 from ..image.resampling import coord_transform
 from .._utils.niimg_conversions import _safe_get_data
-from .._utils import CacheMixin
+from .._utils import CacheMixin, logger
 from .._utils.niimg import img_data_dtype
 from .._utils.niimg_conversions import check_niimg_4d, check_niimg_3d
 from .._utils.class_inspect import get_params
@@ -22,12 +23,48 @@
 
 def _apply_mask_and_get_affinity(seeds, niimg, radius, allow_overlap,
                                  mask_img=None):
+    '''Utility function to get only the rows which are occupied by sphere at
+    given seed locations and the provided radius. Rows are in target_affine and
+    target_shape space.
+
+    Parameters
+    ----------
+    seeds: List of triplets of coordinates in native space
+        Seed definitions. List of coordinates of the seeds in the same space
+        as target_affine.
+    niimg: 3D/4D Niimg-like object
+        See http://nilearn.github.io/manipulating_images/input_output.html
+        Images to process. It must boil down to a 4D image with scans
+        number as last dimension.
+    radius: float
+        Indicates, in millimeters, the radius for the sphere around the seed.
+    allow_overlap: boolean
+        If False, a ValueError is raised if VOIs overlap
+    mask_img: Niimg-like object,
+        Mask to apply to regions before extracting signals. If niimg is None,
+        mask_img is used as a reference space in which the spheres 'indices are
+        placed.
+
+    Returns
+    -------
+    X: 2D numpy.ndarray
+        Signal for each brain voxel in the (masked) niimgs.
+        shape: (number of scans, number of voxels)
+    A: scipy.sparse.lil_matrix
+        Contains the boolean indices for each sphere.
+        shape: (number of seeds, number of voxels)
+    '''
     seeds = list(seeds)
-    affine = niimg.affine
 
     # Compute world coordinates of all in-mask voxels.
-
-    if mask_img is not None:
+    if niimg is None:
+        mask, affine = masking._load_mask_img(mask_img)
+        # Get coordinate for alle voxels inside of mask
+        mask_coords = np.asarray(np.nonzero(mask)).T.tolist()
+        X = None
+
+    elif mask_img is not None:
+        affine = niimg.affine
         mask_img = check_niimg_3d(mask_img)
         mask_img = image.resample_img(mask_img, target_affine=affine,
                                       target_shape=niimg.shape[:3],
@@ -36,7 +73,8 @@ def _apply_mask_and_get_affinity(seeds, niimg, radius, allow_overlap,
         mask_coords = list(zip(*np.where(mask != 0)))
 
         X = masking._apply_mask_fmri(niimg, mask_img)
-    else:
+    elif niimg is not None:
+        affine = niimg.affine
         if np.isnan(np.sum(_safe_get_data(niimg))):
             warnings.warn('The imgs you have fed into fit_transform() contains'
                           ' NaN values which will be converted to zeroes ')
@@ -44,6 +82,8 @@ def _apply_mask_and_get_affinity(seeds, niimg, radius, allow_overlap,
         else:
             X = _safe_get_data(niimg).reshape([-1, niimg.shape[3]]).T
         mask_coords = list(np.ndindex(niimg.shape[:3]))
+    else:
+        raise ValueError("Either a niimg or a mask_img must be provided.")
 
     # For each seed, get coordinates of nearest voxel
     nearests = []
@@ -73,11 +113,16 @@ def _apply_mask_and_get_affinity(seeds, niimg, radius, allow_overlap,
     mask_coords = mask_coords.astype(int).tolist()
     for i, seed in enumerate(seeds):
         try:
-            A[i, mask_coords.index(seed)] = True
+            A[i, mask_coords.index(list(map(int, seed)))] = True
         except ValueError:
             # seed is not in the mask
             pass
 
+    sphere_sizes = np.asarray(A.tocsr().sum(axis=1)).ravel()
+    empty_spheres = np.nonzero(sphere_sizes == 0)[0]
+    if len(empty_spheres) != 0:
+        raise ValueError("These spheres are empty: {}".format(empty_spheres))
+
     if not allow_overlap:
         if np.any(A.sum(axis=0) >= 2):
             raise ValueError('Overlap detected between spheres')
@@ -110,8 +155,6 @@ def _iter_signals_from_spheres(seeds, niimg, radius, allow_overlap,
                                         allow_overlap,
                                         mask_img=mask_img)
     for i, row in enumerate(A.rows):
-        if len(row) == 0:
-            raise ValueError('Sphere around seed #%i is empty' % i)
         yield X[:, row]
 
 
@@ -334,3 +377,42 @@ def transform_single_imgs(self, imgs, confounds=None):
             verbose=self.verbose)
 
         return signals
+
+    def inverse_transform(self, region_signals):
+        """Compute voxel signals from spheres signals
+
+        Any mask given at initialization is taken into account. Throws an error
+        if mask_img==None
+
+        Parameters
+        ----------
+        region_signals: 2D numpy.ndarray
+            Signal for each region.
+            shape: (number of scans, number of spheres)
+
+        Returns
+        -------
+        voxel_signals: nibabel.Nifti1Image
+            Signal for each sphere.
+            shape: (mask_img, number of scans).
+        """
+        self._check_fitted()
+
+        logger.log("computing image from signals", verbose=self.verbose)
+
+        if self.mask_img is not None:
+            mask = check_niimg_3d(self.mask_img)
+        else:
+            raise ValueError('Please provide mask_img at initialization to'
+                             ' provide a reference for the inverse_transform.')
+        _, adjacency = _apply_mask_and_get_affinity(
+            self.seeds_, None, self.radius, self.allow_overlap, mask_img=mask)
+        adjacency = adjacency.tocsr()
+        # Compute overlap scaling for mean signal:
+        if self.allow_overlap:
+            n_adjacent_spheres = np.asarray(adjacency.sum(axis=0)).ravel()
+            scale = 1 / np.maximum(1, n_adjacent_spheres)
+            adjacency = adjacency.dot(sparse.diags(scale))
+
+        img = adjacency.T.dot(region_signals.T).T
+        return masking.unmask(img, self.mask_img)


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
@@ -2,10 +2,10 @@
 import numpy as np
 import pytest
 
-from numpy.testing import assert_array_equal
+from numpy.testing import assert_array_equal, assert_array_almost_equal
 
 from nilearn.input_data import NiftiSpheresMasker
-from nilearn.image import get_data
+from nilearn.image import get_data, new_img_like
 
 
 def test_seed_extraction():
@@ -124,7 +124,7 @@ def test_small_radius():
 
     masker = NiftiSpheresMasker([seed], radius=0.1,
                                 mask_img=nibabel.Nifti1Image(mask, affine))
-    with pytest.raises(ValueError, match='Sphere around seed #0 is empty'):
+    with pytest.raises(ValueError, match='These spheres are empty'):
         masker.fit_transform(nibabel.Nifti1Image(data, affine))
 
     masker = NiftiSpheresMasker([seed], radius=1.6,
@@ -178,3 +178,115 @@ def test_standardization():
     np.testing.assert_almost_equal(s.ravel(), data[1, 1, 1] /
                                    data[1, 1, 1].mean() * 100 - 100,
                                    )
+
+
+def test_nifti_spheres_masker_inverse_transform():
+    # Applying the sphere_extraction example from above backwards
+    data = np.random.random((3, 3, 3, 5))
+    img = nibabel.Nifti1Image(data, np.eye(4))
+    masker = NiftiSpheresMasker([(1, 1, 1)], radius=1)
+    # Test the fit
+    masker.fit()
+    # Transform data
+    with pytest.raises(ValueError, match='Please provide mask_img'):
+        masker.inverse_transform(data[0, 0, 0, :])
+
+    # Mask describes the extend of the masker's sphere
+    mask = np.zeros((3, 3, 3), dtype=np.bool)
+    mask[:, 1, 1] = True
+    mask[1, :, 1] = True
+    mask[1, 1, :] = True
+
+    # Now with a mask
+    mask_img = np.zeros((3, 3, 3))
+    mask_img[1, :, :] = 1
+    mask_img = nibabel.Nifti1Image(mask_img, np.eye(4))
+    masker = NiftiSpheresMasker([(1, 1, 1)], radius=1, mask_img=mask_img)
+    masker.fit()
+    s = masker.transform(img)
+    # Create an array mask
+    array_mask = np.logical_and(mask, get_data(mask_img))
+
+    inverse_map = masker.inverse_transform(s)
+
+    # Testing whether mask is applied to inverse transform
+    assert_array_equal(np.mean(get_data(inverse_map), axis=-1) != 0,
+                       array_mask)
+    # Test whether values are preserved
+    assert_array_equal(get_data(inverse_map)[array_mask].mean(0), s[:, 0])
+
+    # Test whether the mask's shape is applied
+    assert_array_equal(inverse_map.shape[:3], mask_img.shape)
+
+
+def test_nifti_spheres_masker_inverse_overlap():
+    # Test overlapping data in inverse_transform
+    affine = np.eye(4)
+    shape = (5, 5, 5)
+
+    data = np.random.random(shape + (5,))
+    fmri_img = nibabel.Nifti1Image(data, affine)
+
+    # Apply mask image - to allow inversion
+    mask_img = new_img_like(fmri_img, np.ones(shape))
+    seeds = [(0, 0, 0), (2, 2, 2)]
+    # Inverse data
+    inv_data = np.random.random(len(seeds))
+
+    overlapping_masker = NiftiSpheresMasker(seeds, radius=1,
+                                            allow_overlap=True,
+                                            mask_img=mask_img).fit()
+    overlapping_masker.inverse_transform(inv_data)
+
+    overlapping_masker = NiftiSpheresMasker(seeds, radius=2,
+                                            allow_overlap=True,
+                                            mask_img=mask_img).fit()
+
+    overlap = overlapping_masker.inverse_transform(inv_data)
+
+    # Test whether overlapping data is averaged
+    assert_array_almost_equal(get_data(overlap)[1, 1, 1], np.mean(inv_data))
+
+    noverlapping_masker = NiftiSpheresMasker(seeds, radius=1,
+                                             allow_overlap=False,
+                                             mask_img=mask_img).fit()
+
+    noverlapping_masker.inverse_transform(inv_data)
+    noverlapping_masker = NiftiSpheresMasker(seeds, radius=2,
+                                             allow_overlap=False,
+                                             mask_img=mask_img).fit()
+
+    with pytest.raises(ValueError, match='Overlap detected'):
+        noverlapping_masker.inverse_transform(inv_data)
+
+
+def test_small_radius_inverse():
+    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))
+    spheres_data = masker.fit_transform(nibabel.Nifti1Image(data, affine))
+    masker.inverse_transform(spheres_data)
+    # 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))
+    masker.fit(nibabel.Nifti1Image(data, affine))
+
+    with pytest.raises(ValueError, match='These spheres are empty'):
+        masker.inverse_transform(spheres_data)
+
+    masker = NiftiSpheresMasker([seed], radius=1.6,
+                                mask_img=nibabel.Nifti1Image(mask, affine))
+    masker.fit(nibabel.Nifti1Image(data, affine))
+    masker.inverse_transform(spheres_data)