Repository: nilearn/nilearn

Issue:
NiftiMasker should "time-mask" 4D volumes
In examples (e.g. plot_haxby_searchlight.py), we often do something like:

``` python
fmri_img = nibabel.load(dataset_files.func)

### Restrict to faces and houses ################################
condition_mask = np.logical_or(conditions == 'face')
fmri_img = nibabel.Nifti1Image(fmri_img.get_data()[..., condition_mask],
                               fmri_img.get_affine().copy())

### Mask data ###############################################
mask_img = nibabel.load(dataset_files.mask)
nifti_masker = NiftiMasker(mask=mask_img)
fmri_masked = nifti_masker.fit_transform(fmri_img)
```

It would be simpler to provide `condition_mask` directly to the NiftiMasker to avoid creating a temporary image.

On top of making things easier to use, one important consideration is that some preprocessings performed by the NiftiMasker (such as detrending) might need the complete time serie to be accurate.
The NiftiMasker should then of course be able to choose whether it has to process the whole time serie or only the desired volumes according to the preprocessings that have to be performed. In the first case, is it important not to have masked the data before, while computation time would be saved in the second case (potentially a lot).



Discussion:
Agreed with the general idea. I would just not call it condition_mask because that refers to the notion of condition which is not common to all datasets. 

<div>-------- Original message --------</div><div>From: Virgile Fritsch notifications@github.com </div><div>Date:18/02/2014  17:45  (GMT+01:00) </div><div>To: nilearn/nilearn nilearn@noreply.github.com </div><div>Subject: [nilearn] NiftiMasker should "time-mask" 4D volumes (#165) </div><div>
</div>In examples (e.g. plot_haxby_searchlight.py), we often do something like:

fmri_img = nibabel.load(dataset_files.func)

### Restrict to faces and houses

condition_mask = np.logical_or(conditions == 'face')
fmri_img = nibabel.Nifti1Image(fmri_img.get_data()[..., condition_mask],
                               fmri_img.get_affine().copy())

### Mask data

mask_img = nibabel.load(dataset_files.mask)
nifti_masker = NiftiMasker(mask=mask_img)
fmri_masked = nifti_masker.fit_transform(fmri_img)
It would be simpler to provide condition_mask directly to the NiftiMasker to avoid creating a temporary image.

On top of making things easier to use, one important consideration is that some preprocessings performed by the NiftiMasker (such as detrending) might need the complete time serie to be accurate.
The NiftiMasker should then of course be able to choose whether it has to process the whole time serie or only the desired volumes according to the preprocessings that have to be performed. In the first case, is it important not to have masked the data before, while computation time would be saved in the second case (potentially a lot).

—
Reply to this email directly or view it on GitHub.


Resolving patch:
diff --git a/nilearn/input_data/base_masker.py b/nilearn/input_data/base_masker.py
--- a/nilearn/input_data/base_masker.py
+++ b/nilearn/input_data/base_masker.py
@@ -8,6 +8,7 @@
 
 import numpy as np
 import itertools
+from nibabel import Nifti1Image
 
 from sklearn.base import BaseEstimator, TransformerMixin
 from sklearn.externals.joblib import Memory, Parallel, delayed
@@ -26,7 +27,8 @@ def filter_and_mask(imgs, mask_img_,
                     memory=Memory(cachedir=None),
                     verbose=0,
                     confounds=None,
-                    copy=True):
+                    copy=True,
+                    sample_mask=None):
     # If we have a string (filename), we won't need to copy, as
     # there will be no side effect
 
@@ -37,7 +39,10 @@ def filter_and_mask(imgs, mask_img_,
         class_name = enclosing_scope_name(stack_level=2)
 
     mask_img_ = _utils.check_niimg(mask_img_, ensure_3d=True)
+
     imgs = _utils.check_niimgs(imgs, accept_3d=True)
+    if sample_mask is not None:
+        imgs = image.index_img(imgs, sample_mask)
 
     # Resampling: allows the user to change the affine, the shape or both
     if verbose > 1:
@@ -155,7 +160,8 @@ class BaseMasker(BaseEstimator, TransformerMixin, CacheMixin):
     """Base class for NiftiMaskers
     """
 
-    def transform_single_imgs(self, imgs, confounds=None, copy=True):
+    def transform_single_imgs(self, imgs, confounds=None, copy=True,
+                              sample_mask=None):
         if not hasattr(self, 'mask_img_'):
             raise ValueError('It seems that %s has not been fitted. '
                              'You must call fit() before calling transform().'
@@ -166,15 +172,16 @@ def transform_single_imgs(self, imgs, confounds=None, copy=True):
         for name in ('mask_img', 'mask_args'):
             params.pop(name, None)
         data, _ = self._cache(filter_and_mask, func_memory_level=1,
-                           ignore=['verbose', 'memory', 'copy'])(
-                                imgs, self.mask_img_,
-                                params,
-                                memory_level=self.memory_level,
-                                memory=self.memory,
-                                verbose=self.verbose,
-                                confounds=confounds,
-                                copy=copy
-                            )
+                              ignore=['verbose', 'memory', 'copy'])(
+                                  imgs, self.mask_img_,
+                                  params,
+                                  memory_level=self.memory_level,
+                                  memory=self.memory,
+                                  verbose=self.verbose,
+                                  confounds=confounds,
+                                  copy=copy,
+                                  sample_mask=sample_mask
+                                  )
         return data
 
     def transform_imgs(self, imgs_list, confounds=None, copy=True, n_jobs=1):
diff --git a/nilearn/input_data/nifti_masker.py b/nilearn/input_data/nifti_masker.py
--- a/nilearn/input_data/nifti_masker.py
+++ b/nilearn/input_data/nifti_masker.py
@@ -73,6 +73,13 @@ class NiftiMasker(BaseMasker, CacheMixin):
         to fine-tune mask computation. Please see the related documentation
         for details.
 
+    sample_mask : Any type compatible with numpy-array indexing
+        Masks the niimgs along time/fourth dimension. This complements
+        3D masking by the mask_img argument. This masking step is applied
+        before data preprocessing at the beginning of NiftiMasker.transform.
+        This is useful to perform data subselection as part of a scikit-learn
+        pipeline.
+
     memory : instance of joblib.Memory or string
         Used to cache the masking process.
         By default, no caching is done. If a string is given, it is the
@@ -107,7 +114,7 @@ def __init__(self, mask_img=None, sessions=None, smoothing_fwhm=None,
                  low_pass=None, high_pass=None, t_r=None,
                  target_affine=None, target_shape=None,
                  mask_strategy='background',
-                 mask_args=None,
+                 mask_args=None, sample_mask=None,
                  memory_level=1, memory=Memory(cachedir=None),
                  verbose=0
                  ):
@@ -125,6 +132,7 @@ def __init__(self, mask_img=None, sessions=None, smoothing_fwhm=None,
         self.target_shape = target_shape
         self.mask_strategy = mask_strategy
         self.mask_args = mask_args
+        self.sample_mask = sample_mask
 
         self.memory = memory
         self.memory_level = memory_level
@@ -205,4 +213,4 @@ def transform(self, imgs, confounds=None):
             related documentation for details
         """
         return self.transform_single_imgs(
-            imgs, confounds)
+            imgs, confounds, sample_mask=self.sample_mask)


Test patch:
diff --git a/nilearn/input_data/tests/test_nifti_masker.py b/nilearn/input_data/tests/test_nifti_masker.py
--- a/nilearn/input_data/tests/test_nifti_masker.py
+++ b/nilearn/input_data/tests/test_nifti_masker.py
@@ -15,12 +15,14 @@
 from nose.tools import assert_true, assert_false, assert_raises
 from nose import SkipTest
 import numpy as np
+from numpy.testing import assert_array_equal
 
 from nibabel import Nifti1Image
 import nibabel
 
 from ..nifti_masker import NiftiMasker
 from ..._utils import testing
+from ...image import index_img
 
 
 def test_auto_mask():
@@ -110,6 +112,39 @@ def test_mask_3d():
         assert_raises(TypeError, masker.fit)
 
 
+def test_mask_4d():
+    # Dummy mask
+    mask = np.zeros((10, 10, 10))
+    mask[3:7, 3:7, 3:7] = 1
+    mask_bool = mask.astype(bool)
+    mask_img = Nifti1Image(mask, np.eye(4))
+    n_mask_vox = mask_bool.sum()
+
+    # Dummy data
+    data = np.zeros((10, 10, 10, 3))
+    data[..., 0] = 1
+    data[..., 1] = 2
+    data[..., 2] = 3
+    data_img_4d = Nifti1Image(data, np.eye(4))
+    data_imgs = [index_img(data_img_4d, 0), index_img(data_img_4d, 1),
+                 index_img(data_img_4d, 2)]
+
+    # check whether transform is indeed selecting niimgs subset
+    sample_mask = np.array([0, 2])
+    masker = NiftiMasker(mask_img=mask_img, sample_mask=sample_mask)
+    masker.fit()
+    data_trans = masker.transform(data_imgs)
+    data_trans_img = index_img(data_img_4d, sample_mask)
+    data_trans_direct = data_trans_img.get_data()[mask_bool, :]
+    data_trans_direct = np.swapaxes(data_trans_direct, 0, 1)
+    assert_array_equal(data_trans, data_trans_direct)
+
+    masker = NiftiMasker(mask_img=mask_img, sample_mask=sample_mask)
+    masker.fit()
+    data_trans2 = masker.transform(data_img_4d)
+    assert_array_equal(data_trans2, data_trans_direct)
+
+
 def test_sessions():
     # Test the sessions vector
     data = np.ones((40, 40, 40, 4))