Repository: nilearn/nilearn

Issue:
Update image.py
dtype issue for anlyzeImage


Discussion:
The patch LGTM. Can you add a non-regression test ?
This looks good so far. Could you add a tests in nilearn/image/tests/test_image.py and nilearn/tests/test_masking.py? One for function new_img_like and compute_epi_mask

A dummy image can be created like this, for instance.
```python
data = np.ones((5, 6, 7))
img = nibabel.spm2.Spm2AnalyzeImage(data, affine=np.diag((4, 3, 2, 1)))
```
We might will have similar issues with nibabel.spm99.Spm99AnalyzeImage
Is there any option in nibabel where we can collectively check SpmAnalyze images for both ?
Is this alive? If not, please close it by this weekend.
Okay, so I'm looking at this and trying to come up with a way to do this that is general and doesn't require looking at hidden class variables.

1) Just use `np.uint8`. Bools will never be negative, and I *think* all classes permit `uint8`s.
2) A quick test (memoized):

```Python
@functools.lru_cache
def supports_dtype(klass, dtype):
    try:
        klass(np.zeros((1,1,1), dtype=dtype).set_data_dtype(dtype)
        return True
    except:
        return False
```

3) Just go ahead and access the private object:

```Python
>>> np.int8 in nb.Nifti1Image.header_class._data_type_codes
True
>>> np.int8 in nb.AnalyzeImage.header_class._data_type_codes
False
```

4) A new nibabel method/property that lists valid dtypes for the image. It does seem useful to query.

cc @thomasbazeille 
If you were to modify nibabel, I'd suggest to have less private variables. That way we could just like at the relevant variable.

Solution 1 seems appealing. The only drawback is that it changes user-facing results. I don't think that it is the end of the world, and I would be in favor of doing this and clearly advertising it in the changelog.
I've opened a discussion about a possible API over in https://github.com/nipy/nibabel/issues/913.
I try to rebuild a minimal failure example, but I'm not sure I find it. @Leo-YF-Liu, can you give pointers as there is no associated issue here.

Is it something like : 
```
import nibabel
import numpy as np
int8_data = np.ones((5, 6, 7),dtype=np.int8())

img = nibabel.spm2.Spm2AnalyzeImage(np.ones((5, 6, 7)), affine=np.diag((4, 3, 2, 1)))

from nilearn.image import new_img_like
new_img_like(img,int8_dat)
```
I'm not sure it's a very common usecase, but certainly still worth fixing 
It's specifically for coercing boolean data to a saveable dtype:

```Python
import nibabel as nb
import numpy as np
from nilearn import image as nli

in_img = nb.AnalyzeImage(np.random.default_rng().random((5, 5, 5)), affine=np.eye(4))

out_img = nli.new_image_like(in_img, data=in_img.get_fdata() > 0.5)
```

Edit: FWIW, my expectation of `nli.new_image_like(n_img, data=np.int8(<whatever>))` would be not to cast the data.
Hello, is this worth reviving or should we close? From what I can gather here, it seems that there is no clear consensus on which of [these 4 approaches](https://github.com/nilearn/nilearn/pull/1500#issuecomment-631530102) to go with. @effigies do you have any input regarding: 
> A new nibabel method/property that lists valid dtypes for the image. It does seem useful to query.



Personally I'd be okay with 1+4. `uint8` has better compatibility across image formats and software suites than `int8`, and we can add the proposed method to nibabel. I just didn't get much feedback last time and this completely fell of my radar.
@effigies 1+4 also seem reasonable to me. I don't see any drawbacks except for what was outlined by @GaelVaroquaux about 1 changing user-facing results but I think we can live with that. And it would be specified in the changelog. If no one objects, I'll open a new PR to make the change on our side. Though not necessary in this case, I do agree that 4 would be useful to have as well in nibabel. And if someone thinks it's a bad idea to always default to `uint8` we can then use the query.

Resolving patch:
diff --git a/nilearn/image/image.py b/nilearn/image/image.py
--- a/nilearn/image/image.py
+++ b/nilearn/image/image.py
@@ -716,7 +716,11 @@ def new_img_like(ref_niimg, data, affine=None, copy_header=False):
         Reference image. The new image will be of the same type.
 
     data : :class:`numpy.ndarray`
-        Data to be stored in the image.
+        Data to be stored in the image. If data dtype is a boolean, then data
+        is cast to 'uint8' by default.
+
+    .. versionchanged:: 0.9.2dev
+        Changed default dtype casting of booleans from 'int8' to 'uint8'.
 
     affine : 4x4 :class:`numpy.ndarray`, optional
         Transformation matrix.
@@ -755,10 +759,7 @@ def new_img_like(ref_niimg, data, affine=None, copy_header=False):
     if affine is None:
         affine = ref_niimg.affine
     if data.dtype == bool:
-        default_dtype = np.int8
-        if isinstance(ref_niimg, nibabel.freesurfer.mghformat.MGHImage):
-            default_dtype = np.uint8
-        data = as_ndarray(data, dtype=default_dtype)
+        data = as_ndarray(data, dtype=np.uint8)
     data = _downcast_from_int64_if_possible(data)
     header = None
     if copy_header:


Test patch:
diff --git a/nilearn/image/tests/test_image.py b/nilearn/image/tests/test_image.py
--- a/nilearn/image/tests/test_image.py
+++ b/nilearn/image/tests/test_image.py
@@ -7,7 +7,8 @@
 import tempfile
 
 import nibabel
-from nibabel import Nifti1Image
+from nibabel import Nifti1Image, AnalyzeImage
+from nibabel.freesurfer import MGHImage
 import numpy as np
 import pytest
 
@@ -45,7 +46,7 @@ def test_get_data():
     assert data is img._data_cache
     mask_img = new_img_like(img, data > 0)
     data = get_data(mask_img)
-    assert data.dtype == np.dtype('int8')
+    assert data.dtype == np.dtype('uint8')
     img_3d = index_img(img, 0)
     with tempfile.TemporaryDirectory() as tempdir:
         filename = os.path.join(tempdir, 'img_{}.nii.gz')
@@ -824,3 +825,14 @@ def test_new_img_like_mgh_image():
     data = np.zeros((5, 5, 5), dtype=np.uint8)
     niimg = nibabel.freesurfer.MGHImage(dataobj=data, affine=np.eye(4))
     new_img_like(niimg, data.astype(float), niimg.affine, copy_header=True)
+
+
+@pytest.mark.parametrize("image", [MGHImage, AnalyzeImage])
+def test_new_img_like_boolean_data(image):
+    """Check defaulting boolean input data to np.uint8 dtype is valid for
+    encoding with nibabel image classes MGHImage and AnalyzeImage.
+    """
+    data = np.random.randn(5, 5, 5).astype('uint8')
+    in_img = image(dataobj=data, affine=np.eye(4))
+    out_img = new_img_like(in_img, data=in_img.get_fdata() > 0.5)
+    assert get_data(out_img).dtype == 'uint8'