Repository: scikit-image/scikit-image

Issue:
`img_as_uint` fails with >=32bit integer dtypes.
```
In [3]: exposure.equalize_adapthist(np.random.randint(256, size=(32, 32)).astype(np.uint32))
/usr/lib/python3.5/site-packages/skimage/util/dtype.py:111: UserWarning: Possible precision loss when converting from uint32 to uint16
  "%s to %s" % (dtypeobj_in, dtypeobj))
Out[3]: 
array([[ nan,  nan,  nan, ...,  nan,  nan,  nan],
       [ nan,  nan,  nan, ...,  nan,  nan,  nan],
       [ nan,  nan,  nan, ...,  nan,  nan,  nan],
       ..., 
       [ nan,  nan,  nan, ...,  nan,  nan,  nan],
       [ nan,  nan,  nan, ...,  nan,  nan,  nan],
       [ nan,  nan,  nan, ...,  nan,  nan,  nan]])

In [4]: exposure.equalize_adapthist(np.random.randint(256, size=(32, 32)))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-253568f6b83d> in <module>()
----> 1 exposure.equalize_adapthist(np.random.randint(256, size=(32, 32)))

/usr/lib/python3.5/site-packages/skimage/color/adapt_rgb.py in image_filter_adapted(image, *args, **kwargs)
     38                 return apply_to_rgb(image_filter, image, *args, **kwargs)
     39             else:
---> 40                 return image_filter(image, *args, **kwargs)
     41         return image_filter_adapted
     42     return decorator

/usr/lib/python3.5/site-packages/skimage/exposure/_adapthist.py in equalize_adapthist(image, ntiles_x, ntiles_y, clip_limit, nbins)
     75     .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE
     76     """
---> 77     image = img_as_uint(image)
     78     image = rescale_intensity(image, out_range=(0, NR_OF_GREY - 1))
     79     out = _clahe(image, ntiles_x, ntiles_y, clip_limit * nbins, nbins)

/usr/lib/python3.5/site-packages/skimage/util/dtype.py in img_as_uint(image, force_copy)
    313 
    314     """
--> 315     return convert(image, np.uint16, force_copy)
    316 
    317 

/usr/lib/python3.5/site-packages/skimage/util/dtype.py in convert(image, dtype, force_copy, uniform)
    101 
    102     if not (dtype_in in _supported_types and dtype in _supported_types):
--> 103         raise ValueError("can not convert %s to %s." % (dtypeobj_in, dtypeobj))
    104 
    105     def sign_loss():

ValueError: can not convert int64 to uint16.
```

Arch Linux, Python 3.5.0, numpy 1.9.3, skimage 0.11.3



Discussion:
Hi @anntzer, I updated your title to highlight the root cause, thanks for reporting.  My thought is that we should allow this conversion and warn, what do you think, @jni?

Actually I don't see why you need to convert to uint8 first; in fact if the conversion is linear (I guess it is?) then you may well be destroying at that stage some of the contrast that you were hoping to enhance by CLAHE.
(Here I used `randint(256)` but that was probably a bad example, histogram equalization should work just as well for 16+bits images.)

The reference implementation for CLAHE uses 12-bit resolution.  I'd argue that any more than that is just playing with noise.

Then adapthist could support at least 16bit images? (as there's no 12bit in numpy and 8bit is certainly a bit short)

Right, `img_as_uint` converts to a 16-bit image.

Fair enough.
Regardless of the "usefulness" of keeping more than 12 bits while doing contrast enhancement (which I understand well), is there a real computational cost to supporting such images?

I'd say supporting them is fine, as an input, but the calculations should still be done as-is, on a 16bit image, since is already tested and known.  We take the stance that the data type of the image is not guaranteed to be the same after an image transform in general.

@blink1073 I don't understand why we are arguing about 12 bits but `NR_OF_GREY` in `_adapthist.py` is `2**14`?

Either way, yes, we should certainly support any type of input image. This again relates to #1234. 

Ha, you're right @jni, I was mis-remembering.  

/ping @jni @blink1073

@stefanv Just had a look at the code. In this case, we should not rescale, imho, but the whole `convert` machinery revolves around rescaling. I don't know how to fix this without extreme special-casing... Suggestions welcome!

Raise a warning and assume 64-bit int is actually a 16-bit image?

Seems reasonable to me to do what we do in `imshow`, and check the max value when downcasting, and issue a warning if we assume it should not be rescaled.


Resolving patch:
diff --git a/skimage/util/dtype.py b/skimage/util/dtype.py
--- a/skimage/util/dtype.py
+++ b/skimage/util/dtype.py
@@ -21,8 +21,8 @@
 integer_types = (np.uint8, np.uint16, np.int8, np.int16)
 
 _supported_types = (np.bool_, np.bool8,
-                    np.uint8, np.uint16, np.uint32,
-                    np.int8, np.int16, np.int32,
+                    np.uint8, np.uint16, np.uint32, np.uint64,
+                    np.int8, np.int16, np.int32, np.int64,
                     np.float32, np.float64)
 
 if np.__version__ >= "1.6.0":
@@ -125,7 +125,18 @@ def _scale(a, n, m, copy=True):
         # Numbers can be represented exactly only if m is a multiple of n
         # Output array is of same kind as input.
         kind = a.dtype.kind
-        if n == m:
+        if n > m and a.max() < 2 ** m:
+            mnew = int(np.ceil(m / 2) * 2)
+            if mnew > m:
+                dtype = "int%s" % mnew
+            else:
+                dtype = "uint%s" % mnew
+            n = int(np.ceil(n / 2) * 2)
+            msg = ("Downcasting %s to %s without scaling because max "
+                   "value %s fits in %s" % (a.dtype, dtype, a.max(), dtype))
+            warn(msg)
+            return a.astype(_dtype2(kind, m))
+        elif n == m:
             return a.copy() if copy else a
         elif n > m:
             # downscale with precision loss


Test patch:
diff --git a/skimage/io/tests/test_mpl_imshow.py b/skimage/io/tests/test_mpl_imshow.py
--- a/skimage/io/tests/test_mpl_imshow.py
+++ b/skimage/io/tests/test_mpl_imshow.py
@@ -87,8 +87,7 @@ def test_outside_standard_range():
 
 def test_nonstandard_type():
     plt.figure()
-    with expected_warnings(["Non-standard image type",
-                            "Low image dynamic range"]):
+    with expected_warnings(["Low image dynamic range"]):
         ax_im = io.imshow(im64)
     assert ax_im.get_clim() == (im64.min(), im64.max())
     assert n_subplots(ax_im) == 2
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
@@ -29,7 +29,7 @@ def test_range():
                         (img_as_float, np.float64),
                         (img_as_uint, np.uint16),
                         (img_as_ubyte, np.ubyte)]:
-            
+
             with expected_warnings(['precision loss|sign loss|\A\Z']):
                 y = f(x)
 
@@ -62,7 +62,7 @@ def test_range_extra_dtypes():
     for dtype_in, dt in dtype_pairs:
         imin, imax = dtype_range_extra[dtype_in]
         x = np.linspace(imin, imax, 10).astype(dtype_in)
-        
+
         with expected_warnings(['precision loss|sign loss|\A\Z']):
             y = convert(x, dt)
 
@@ -72,9 +72,12 @@ def test_range_extra_dtypes():
                y, omin, omax, np.dtype(dt))
 
 
-def test_unsupported_dtype():
+def test_downcast():
     x = np.arange(10).astype(np.uint64)
-    assert_raises(ValueError, img_as_int, x)
+    with expected_warnings('Downcasting'):
+        y = img_as_int(x)
+    assert np.allclose(y, x.astype(np.int16))
+    assert y.dtype == np.int16, y.dtype
 
 
 def test_float_out_of_range():