Repository: scikit-image/scikit-image

Issue:
In `scikit-image==0.23.1` `img_as_ubyte` stop accepting `np.ulonglong` 
### Description:

Code to validate supported types:

https://github.com/scikit-image/scikit-image/blob/ea6c1c637d8e8c377a5e76bd82a7e9a74f9b7b89/skimage/util/dtype.py#L272-L273

Seems like sometimes numpy provides `numpy.ulonglong` dtype that is different from `np.uint64`, but I do not know why. 

![obraz](https://github.com/scikit-image/scikit-image/assets/3826210/dc1adb58-d32e-4226-a0ce-6771ff08acbd)



### Way to reproduce:

```python
import numpy as np
from skimage.util import img_as_ubyte

data = np.arange(50, dtype=np.uint64)
img_as_ubyte(data * 256 ** (data.dtype.itemsize - 1))
```

Traceback:
```python
Traceback (most recent call last):
  File "/home/czaki/.pyenv/versions/napari_3.11/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3553, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-17-4bd2de21f420>", line 1, in <module>
    img_as_ubyte(data * 256 ** (data.dtype.itemsize - 1))
  File "/home/czaki/.pyenv/versions/3.11.5/envs/napari_3.11/lib/python3.11/site-packages/skimage/util/dtype.py", line 564, in img_as_ubyte
    return _convert(image, np.uint8, force_copy)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/czaki/.pyenv/versions/3.11.5/envs/napari_3.11/lib/python3.11/site-packages/skimage/util/dtype.py", line 273, in _convert
    raise ValueError(f'Cannot convert from {dtypeobj_in} to ' f'{dtypeobj_out}.')
ValueError: Cannot convert from uint64 to uint8.

```

This code works with `scikit-image=0.22.0` and is extracted from: 
https://github.com/napari/napari/blob/50b314f7ad2e7cd4ef21b5705b3c6b363c0857ed/napari/layers/_tests/test_utils.py#L11-L19



### Version information:

```Shell
3.11.5 (main, Sep 25 2023, 13:38:35) [GCC 11.4.0]
Linux-6.5.0-27-generic-x86_64-with-glibc2.35
scikit-image version: 0.23.1
numpy version: 1.26.4
```



Discussion:
cc @jni This breaks napari tests. I did not see such a crash in pre-test). 
@Czaki I don't understand though why this is a 0.23.1 problem — looking at all the relevant lines of code, they are all >3y old...
:eyes: Sorry for that. Looks like it might have been introduced while we were updating code for NumPy 2 ([this change](https://github.com/scikit-image/scikit-image/pull/7288/files#diff-7959905b4c33d2cefa7da06da5df8a54094174bdc11b41e4aef632ee88e029f6L38)).

hahaha I was just about to say that it might be part of that transition @lagru! 😂 
and of course it makes sense that blame doesn't pick it up: it's a removed line.
is np.ulonglong deprecated? (ie why was it removed?)
@Czaki 

> `img_as_ubyte(data * 256 ** (data.dtype.itemsize - 1))`

this is a pretty wild line 😂 why do we use it and can we get rid of it in our own tests? Is this to test our ability to display crazy-range data? I hate edge cases... Why can't all the world be regular... 😂 
I can certainly reproduce. I find it curious that 
```python
data.dtype  # dtype('uint64')
data.dtype.type  # numpy.uint64

(data * 256 ** (data.dtype.itemsize - 1)).dtype  # dtype('uint64')
(data * 256 ** (data.dtype.itemsize - 1)).dtype.type  # numpy.ulonglong
```

> is np.ulonglong deprecated? (ie why was it removed?)

I don't think so? I don't remember all the context around this but in https://github.com/scikit-image/scikit-image/commit/d1712b5ca3bf1268bfcd30cf93757bfeb1838bc1 I confidently state

> These should be more explicit and should be more reliable
in ensuring that for example int32 is available on all platforms.

See also https://github.com/scikit-image/scikit-image/pull/7288#issuecomment-1948863993. Seems like made it "more robust" only for float32 and did away with numpy.ulonglong support. :sweat_smile:  
How do we proceed from here. Can / do you want to work around this? We can certainly just add `np.ulonglong` back and make a patch release as well...

However, I'd love to understand why `dtype.type` is changing first.
I wonder why you didn't pick this up in all the nightly and pre-releases?
> I wonder why you didn't pick this up in all the nightly and pre-releases?

Because we use python 3.9 for pre release tests... 
@lagru @Czaki I plan to release 0.23.2 soon https://github.com/scikit-image/scikit-image/milestone/28. I would like to merge #7389 first to test the updated artifact names while I am thinking about it (rather than wait for the release candidate for 0.24).

Here is my plan:
- [ ] merge #7389
- [ ] fix #7385
- [ ] fix #7391
- [ ] release 0.23.2rc0
- [ ] confirm that it passes the napari CI (https://github.com/napari/napari/pull/6826)
- [ ] release 0.23.2
I do not see any. 
I'm confused, before releasing 0.23.2 shouldn't we address this issue first? E.g. by adding `np.ulonglong` back? Otherwise https://github.com/napari/napari/pull/6826 should continue to fail?
We might also address https://github.com/scikit-image/scikit-image/issues/7391. Looks like we just need to update a cast. 
> I'm confused, before releasing 0.23.2 shouldn't we address this issue first? E.g. by adding `np.ulonglong` back? Otherwise [napari/napari#6826](https://github.com/napari/napari/pull/6826) should continue to fail?

Whoops, I was confused. For some reason, I thought you already made a PR to address this issue.
Okay, I'll try to get both PRs out before going to bed today. https://github.com/scikit-image/scikit-image/issues/7391 might be a bit tricky because I can't yet reproduce / don't have access to the failing architectures..
@lagru #7389 is ready to review and merge. I tested it and everything works now.
Just noticed. The dtype behavior in https://github.com/scikit-image/scikit-image/issues/7385#issuecomment-2049529877 is only valid pre NumPy 2.0. In NumPy 2.0 `dtype.type` is `numpy.uint64` in all cases.

Resolving patch:
diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py
--- a/skimage/util/dtype.py
+++ b/skimage/util/dtype.py
@@ -15,17 +15,29 @@
     'dtype_limits',
 ]
 
+# Some of these may or may not be aliases depending on architecture & platform
 _integer_types = (
     np.int8,
+    np.byte,
     np.int16,
+    np.short,
     np.int32,
     np.int64,
+    np.longlong,
+    np.int_,
     np.intp,
+    np.intc,
+    int,
     np.uint8,
+    np.ubyte,
     np.uint16,
+    np.ushort,
     np.uint32,
     np.uint64,
+    np.ulonglong,
+    np.uint,
     np.uintp,
+    np.uintc,
 )
 _integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types}
 dtype_range = {


Test patch:
diff --git a/skimage/util/tests/test_dtype.py b/skimage/util/tests/test_dtype.py
--- a/skimage/util/tests/test_dtype.py
+++ b/skimage/util/tests/test_dtype.py
@@ -227,3 +227,15 @@ def test_int_to_float():
 
     assert_equal(floats.max(), 1)
     assert_equal(floats.min(), -1)
+
+
+def test_img_as_ubyte_supports_npulonglong():
+    # Pre NumPy <2.0.0, `data_scaled.dtype.type` is `np.ulonglong` instead of
+    # np.uint64 as one might expect. This caused issues with `img_as_ubyte` due
+    # to `np.ulonglong` missing from `skimage.util.dtype._integer_types`.
+    # This doesn't seem to be an issue for NumPy >=2.0.0.
+    # https://github.com/scikit-image/scikit-image/issues/7385
+    data = np.arange(50, dtype=np.uint64)
+    data_scaled = data * 256 ** (data.dtype.itemsize - 1)
+    result = img_as_ubyte(data_scaled)
+    assert result.dtype == np.uint8