Repository: nilearn/nilearn

Issue:
NiftiLabelsMasker misbehaves for integer niftis
For niftis like the example attached to this issue, NiftiLabelsMasker() from the masker family does not seem to carry out expected behavhior. Using this nifti as argument for labels_img leads .transform() to produce only zero outputs. The reason appears to be that the internal mean() operation on the voxels of each atlas region may not work properly if the data nifti is integer typed. In the case of 0/1 integer values, this leads to <1 values in many atlas regions, and thus to only zeros in the transformed variable outputs.

[some_one_zero_nifti.gz](https://github.com/nilearn/nilearn/files/3628442/some_one_zero_nifti.gz)



Discussion:
thanks @banilo !

this is the problem:

https://github.com/nilearn/nilearn/blob/a2c33401793a09cc1b9b0ae97e02a08b297c503d/nilearn/regions/signal_extraction.py#L105
to clarify, the problem is not related to the image you provided, but to masking
images of integer types. the mean computed in each region is cast to the same
integer type. but then the image returned by the masker is of floating-point
type, leaving the user wondering why the means are truncated to their integral
part
nilearn should either work only with floating-point, or return an image of the
same type as the input.

I will take a crack at fixing this. Would ceiling a decimal value to 1 have the desirable effect?
> I will take a crack at fixing this. Would ceiling a decimal value to 1 have the desirable effect?

no, what you need to do is store the computed signals in np.float64
instead of the input dtype (at the line that I linked above)

in any event, it is a bug. Bugs should be fixed one way or the other.

Resolving patch:
diff --git a/nilearn/regions/signal_extraction.py b/nilearn/regions/signal_extraction.py
--- a/nilearn/regions/signal_extraction.py
+++ b/nilearn/regions/signal_extraction.py
@@ -116,9 +116,10 @@ def img_to_signals_labels(imgs, labels_img, mask_img=None,
         labels_data[np.logical_not(mask_data)] = background_label
 
     data = _safe_get_data(imgs)
+    target_datatype = np.float32 if data.dtype == np.float32 else np.float64
+    # Nilearn issue: 2135, PR: 2195 for why this is necessary.
     signals = np.ndarray((data.shape[-1], len(labels)), order=order,
-                         dtype=data.dtype)
-
+                         dtype=target_datatype)
     reduction_function = getattr(ndimage.measurements, strategy)
     for n, img in enumerate(np.rollaxis(data, -1)):
         signals[n] = np.asarray(reduction_function(img,


Test patch:
diff --git a/nilearn/regions/tests/test_signal_extraction.py b/nilearn/regions/tests/test_signal_extraction.py
--- a/nilearn/regions/tests/test_signal_extraction.py
+++ b/nilearn/regions/tests/test_signal_extraction.py
@@ -5,17 +5,19 @@
 # License: simplified BSD
 
 import numpy as np
+import pytest
 from nose.tools import assert_raises, assert_true
 
 import nibabel
 
+from nilearn.input_data import NiftiLabelsMasker
 from nilearn.regions import signal_extraction
 from nilearn._utils.testing import write_tmp_imgs, assert_raises_regex
 from nilearn._utils.data_gen import generate_timeseries, generate_regions_ts
 from nilearn._utils.data_gen import generate_labeled_regions, generate_maps
 from nilearn._utils.data_gen import generate_fake_fmri
 from nilearn._utils.exceptions import DimensionError
-from nilearn.image import get_data
+from nilearn.image import get_data, new_img_like
 
 _TEST_DIM_ERROR_MSG = ("Input data has incompatible dimensionality: "
                        "Expected dimension is 3D and you provided "
@@ -410,3 +412,27 @@ def test__trim_maps():
     np.testing.assert_equal(mask_data, maps_i_mask)
     mask_data[1, 1, 1] = 1  # reset, just in case.
     np.testing.assert_equal(np.asarray(list(range(4))), maps_i_indices)
+
+
+@pytest.mark.parametrize('target_dtype',
+                         (np.float, np.float32, np.float64, np.int, np.uint),
+                         )
+def test_img_to_signals_labels_non_float_type(target_dtype):
+    fake_fmri_data = np.random.RandomState(0).rand(10, 10, 10, 10) > 0.5
+    fake_affine = np.eye(4, 4).astype(np.float64)
+    fake_fmri_img_orig = nibabel.Nifti1Image(
+                                        fake_fmri_data.astype(np.float64),
+                                        fake_affine,
+                                        )
+    fake_fmri_img_target_dtype = new_img_like(fake_fmri_img_orig,
+                                              fake_fmri_data.astype(target_dtype))
+    fake_mask_data = np.ones((10, 10, 10), dtype=np.uint8)
+    fake_mask = nibabel.Nifti1Image(fake_mask_data, fake_affine)
+
+    masker = NiftiLabelsMasker(fake_mask)
+    masker.fit()
+    timeseries_int = masker.transform(fake_fmri_img_target_dtype)
+    timeseries_float = masker.transform(fake_fmri_img_orig)
+    assert np.sum(timeseries_int) != 0
+    assert np.allclose(timeseries_int, timeseries_float)
+