Repository: nilearn/nilearn

Issue:
Masking in NiftiLabelsMasker doesn't work.
I want to use `NiftiLabelsMasker` to extract voxels of interest using the Destrieux-Atlas. I am working with MRI-Data (not fMRI) so the 4th dimension represents different subjects, not different time points for a single subject. I created a dummy script and tried to use `NiftiLabelsMasker` on the OASIS dataset (which is available through nilearn.datasets.fetch_oasis_vbm). I also played with the mask image (I tried to create a mask that reduces the atlas image to regions 1 and 2). But it seems that the masking doesn't work: Although I provide `NiftiLabelsMasker` with destrieux_atlas_roi_mask I get 148 data columns (which seems to be all 148 regions reported in this paper). I expect NiftiLabelsMasker to output only 2 columns. Here's my code example:

```
from nilearn.image import load_img
from nilearn.datasets import fetch_atlas_destrieux_2009
from nilearn.datasets import fetch_oasis_vbm
from nilearn.image import math_img

from nilearn.input_data import NiftiLabelsMasker

# niftilabels masker cache
niftilabelsmasker_cache = './niftilabelsmasker_cache/'

# fetch OASIS dataset
oasis_img_paths = fetch_oasis_vbm(n_subjects=10)['gray_matter_maps']

# load destrieux atlas
destrieux_atlas_dict = fetch_atlas_destrieux_2009()
destrieux_atlas = load_img(fetch_atlas_destrieux_2009()['maps'])

# get only region 1 and 2
destrieux_atlas_roi_mask = math_img('np.where(((img == 1) | (img == 2)),1,0)',img=destrieux_atlas)

# extract data
niftilabelsmasker = NiftiLabelsMasker(labels_img=destrieux_atlas,
                                      mask_img=destrieux_atlas_roi_mask,
                                      memory=niftilabelsmasker_cache)

img_data = niftilabelsmasker.fit_transform(oasis_img_paths)
```

This issue might be related to #1056 and #1257.

Pinging @bthirion here to confirm that I posted this as issue on GitHub. I originally asked [this question on neurostars](https://neurostars.org/t/problems-with-using-niftilabelsmasker/5483/2).


Discussion:
Indeed the `mask_img` does not seem to be used. I don't understand why this parameter exists; the mask should be everywhere the `labels_img` is different from the background label (0 by default). to use only two regions, set the other regions to 0 in the labels image 
There is a use case for this matter: take all the voxels of region that fill in a grey matter mask. 
> There is a use case for this matter: take all the voxels of region that fill in a grey matter mask. 

But in this case doesn't it seem more natural to define the region we
actually want, by taking the intersection with the mask, before giving
that to the LabelsMasker (which can be done very easily)?

anyway since the parameter exists I agree that the bug of ignoring the
mask should be fixed, but I find this interface accepting both ROIs and
a mask a bit awkward

OK, I 'm not sure what is 'natural' here, but we agree on the diagnosis and on the necessity to fix this.
I think this is a typical trade-off question: Do I want this functionality to be built-in or is it easy enough to calculate the necessary steps outside `NiftiLabelsMasker` to just leave it away (avoid over-engineering). In this case I would slightly tend to the first option, but I guess, I am also biased: e.g. for my project, I use information from meta-analyses to mask my images to specific voxels, but I also want to use an atlas to parcellate the images into regions and aggregate the voxel values. So I definitely need both functionalities and (personally) would be happy to have it included out-of-the-box in `NiftiLabelsMasker`. 

> There is a use case for this matter: take all the voxels of region that fill in a grey matter mask.

I am not sure about this argument: Aren't most atlases focusing either on grey or white matter in the first place (aka. they contain either zeros in the grey or white matter areas)? And if they are whole-brain-atlases, why would I need a second mask image then (apart from my use case of course, but then we end up, where I began).

Long story short, I agree that this bug should be fixed either way (take `mask_img` out of `NiftiLabelsMasker` or keep it in but make it work).



Taking a look at other masker objects (e.g. NiftiMapsMasker), they all have the mask_img argment.
I think we should thus keep it for consistency (and thus fix the bug !)

Resolving patch:
diff --git a/nilearn/input_data/nifti_labels_masker.py b/nilearn/input_data/nifti_labels_masker.py
--- a/nilearn/input_data/nifti_labels_masker.py
+++ b/nilearn/input_data/nifti_labels_masker.py
@@ -19,17 +19,20 @@ class _ExtractionFunctor(object):
 
     func_name = 'nifti_labels_masker_extractor'
 
-    def __init__(self, _resampled_labels_img_, background_label, strategy):
+    def __init__(self, _resampled_labels_img_, background_label, strategy,
+                 mask_img):
         self._resampled_labels_img_ = _resampled_labels_img_
         self.background_label = background_label
         self.strategy = strategy
+        self.mask_img = mask_img
 
     def __call__(self, imgs):
         from ..regions import signal_extraction
 
         return signal_extraction.img_to_signals_labels(
             imgs, self._resampled_labels_img_,
-            background_label=self.background_label, strategy=self.strategy)
+            background_label=self.background_label, strategy=self.strategy,
+            mask_img=self.mask_img)
 
 
 class NiftiLabelsMasker(BaseMasker, CacheMixin):
@@ -257,6 +260,8 @@ def transform_single_imgs(self, imgs, confounds=None):
 
         if not hasattr(self, '_resampled_labels_img_'):
             self._resampled_labels_img_ = self.labels_img_
+        if not hasattr(self, '_resampled_mask_img'):
+            self._resampled_mask_img = self.mask_img_
         if self.resampling_target == "data":
             imgs_ = _utils.check_niimg_4d(imgs)
             if not _check_same_fov(imgs_, self._resampled_labels_img_):
@@ -267,6 +272,15 @@ def transform_single_imgs(self, imgs, confounds=None):
                         self.labels_img_, interpolation="nearest",
                         target_shape=imgs_.shape[:3],
                         target_affine=imgs_.affine)
+            if self.mask_img is not None and not _check_same_fov(
+                    imgs_, self._resampled_mask_img):
+                if self.verbose > 0:
+                    print("Resampling mask")
+                self._resampled_mask_img = self._cache(
+                    image.resample_img, func_memory_level=2)(
+                        self.mask_img_, interpolation="nearest",
+                        target_shape=imgs_.shape[:3],
+                        target_affine=imgs_.affine)
             # Remove imgs_ from memory before loading the same image
             # in filter_and_extract.
             del imgs_
@@ -287,7 +301,8 @@ def transform_single_imgs(self, imgs, confounds=None):
                 ignore=['verbose', 'memory', 'memory_level'])(
             # Images
             imgs, _ExtractionFunctor(self._resampled_labels_img_,
-                                     self.background_label, self.strategy),
+                                     self.background_label, self.strategy,
+                                     self._resampled_mask_img),
             # Pre-processing
             params,
             confounds=confounds,


Test patch:
diff --git a/nilearn/input_data/tests/test_nifti_labels_masker.py b/nilearn/input_data/tests/test_nifti_labels_masker.py
--- a/nilearn/input_data/tests/test_nifti_labels_masker.py
+++ b/nilearn/input_data/tests/test_nifti_labels_masker.py
@@ -11,9 +11,10 @@
 import pytest
 
 from nilearn.input_data.nifti_labels_masker import NiftiLabelsMasker
+from nilearn.input_data import NiftiMasker
 from nilearn._utils import testing, as_ndarray, data_gen
 from nilearn._utils.exceptions import DimensionError
-from nilearn.image import get_data
+from nilearn.image import get_data, new_img_like
 
 
 def generate_random_img(shape, length=1, affine=np.eye(4),
@@ -348,3 +349,20 @@ def test_standardization():
                                    (unstandarized_label_signals /
                                     unstandarized_label_signals.mean(0) *
                                     100 - 100))
+
+
+def test_nifti_labels_masker_with_mask():
+    shape = (13, 11, 12)
+    affine = np.eye(4)
+    fmri_img, mask_img = generate_random_img(shape, affine=affine, length=3)
+    labels_img = data_gen.generate_labeled_regions(shape, affine=affine,
+                                                   n_regions=7)
+    masker = NiftiLabelsMasker(
+        labels_img, resampling_target=None, mask_img=mask_img)
+    signals = masker.fit().transform(fmri_img)
+    bg_masker = NiftiMasker(mask_img).fit()
+    masked_labels = bg_masker.inverse_transform(bg_masker.transform(labels_img))
+    masked_masker = NiftiLabelsMasker(
+        masked_labels, resampling_target=None, mask_img=mask_img)
+    masked_signals = masked_masker.fit().transform(fmri_img)
+    assert np.allclose(signals, masked_signals)