Repository: scikit-image/scikit-image

Issue:
colorconv functions cast float32 array to double
## Description

During my explorations for fixing #4274, I found that `rgb2lab` converts `float32` arrays to `double`. It turns out that a majority of `colorconv` functions reproduce the same issue.

## Way to reproduce
```python
>>> from skimage import color, data, util
>>> img = data.coffee()
>>> img32 = util.img_as_float32(img)
>>> color.rgb2lab(img32).dtype
dtype('float64')
```

## Version information

```python
3.7.3 | packaged by conda-forge | (default, Jul  1 2019, 21:52:21) 
[GCC 7.3.0]
Linux-4.15.0-66-generic-x86_64-with-debian-buster-sid
scikit-image version: 0.16.2
numpy version: 1.17.3
```




Discussion:

Resolving patch:
diff --git a/skimage/color/colorconv.py b/skimage/color/colorconv.py
--- a/skimage/color/colorconv.py
+++ b/skimage/color/colorconv.py
@@ -447,7 +447,7 @@ def hsv2rgb(hsv):
            '10': (1.0, 1.0, 1.0)}}
 
 
-def get_xyz_coords(illuminant, observer):
+def get_xyz_coords(illuminant, observer, dtype=float):
     """Get the XYZ coordinates of the given illuminant and observer [1]_.
 
     Parameters
@@ -456,11 +456,13 @@ def get_xyz_coords(illuminant, observer):
         The name of the illuminant (the function is NOT case sensitive).
     observer : {"2", "10"}, optional
         The aperture angle of the observer.
+    dtype: dtype, optional
+        Output data type.
 
     Returns
     -------
-    (x, y, z) : tuple
-        A tuple with 3 elements containing the XYZ coordinates of the given
+    out : array
+        Array with 3 elements containing the XYZ coordinates of the given
         illuminant.
 
     Raises
@@ -476,7 +478,7 @@ def get_xyz_coords(illuminant, observer):
     """
     illuminant = illuminant.upper()
     try:
-        return illuminants[illuminant][observer]
+        return np.asarray(illuminants[illuminant][observer], dtype=dtype)
     except KeyError:
         raise ValueError("Unknown illuminant/observer combination\
         (\'{0}\', \'{1}\')".format(illuminant, observer))
@@ -581,12 +583,12 @@ def _convert(matrix, arr):
 
     Returns
     -------
-    out : ndarray, dtype=float
+    out : ndarray
         The converted array.
     """
     arr = _prepare_colorarray(arr)
 
-    return arr @ matrix.T.copy()
+    return arr @ matrix.T.astype(arr.dtype)
 
 
 def xyz2rgb(xyz):
@@ -908,7 +910,7 @@ def xyz2lab(xyz, illuminant="D65", observer="2"):
     """
     arr = _prepare_colorarray(xyz)
 
-    xyz_ref_white = get_xyz_coords(illuminant, observer)
+    xyz_ref_white = get_xyz_coords(illuminant, observer, arr.dtype)
 
     # scale by CIE XYZ tristimulus values of the reference white point
     arr = arr / xyz_ref_white


Test patch:
diff --git a/skimage/color/tests/test_colorconv.py b/skimage/color/tests/test_colorconv.py
--- a/skimage/color/tests/test_colorconv.py
+++ b/skimage/color/tests/test_colorconv.py
@@ -15,7 +15,7 @@
 from skimage._shared.testing import assert_array_almost_equal
 from skimage._shared.testing import TestCase
 
-from skimage.util import img_as_float, img_as_ubyte
+from skimage.util import img_as_float, img_as_ubyte, img_as_float32
 from skimage.io import imread
 from skimage.color import (rgb2hsv, hsv2rgb,
                            rgb2xyz, xyz2rgb,
@@ -93,6 +93,13 @@ def test_rgba2rgb_error_grayscale(self):
     def test_rgba2rgb_error_rgb(self):
         self.assertRaises(ValueError, rgba2rgb, self.img_rgb)
 
+    def test_rgba2rgb_dtype(self):
+        rgba = self.img_rgba.astype('float64')
+        rgba32 = img_as_float32(rgba)
+
+        assert rgba2rgb(rgba).dtype == rgba.dtype
+        assert rgba2rgb(rgba32).dtype == rgba32.dtype
+
     # RGB to HSV
     def test_rgb2hsv_conversion(self):
         rgb = img_as_float(self.img_rgb)[::16, ::16]
@@ -109,6 +116,13 @@ def test_rgb2hsv_error_grayscale(self):
     def test_rgb2hsv_error_one_element(self):
         self.assertRaises(ValueError, rgb2hsv, self.img_rgb[0, 0])
 
+    def test_rgb2hsv_dtype(self):
+        rgb = img_as_float(self.img_rgb)
+        rgb32 = img_as_float32(self.img_rgb)
+
+        assert rgb2hsv(rgb).dtype == rgb.dtype
+        assert rgb2hsv(rgb32).dtype == rgb32.dtype
+
     # HSV to RGB
     def test_hsv2rgb_conversion(self):
         rgb = self.img_rgb.astype("float32")[::16, ::16]
@@ -125,6 +139,17 @@ def test_hsv2rgb_error_grayscale(self):
     def test_hsv2rgb_error_one_element(self):
         self.assertRaises(ValueError, hsv2rgb, self.img_rgb[0, 0])
 
+    def test_hsv2rgb_dtype(self):
+        rgb = self.img_rgb.astype("float32")[::16, ::16]
+        # create HSV image with colorsys
+        hsv = np.array([colorsys.rgb_to_hsv(pt[0], pt[1], pt[2])
+                        for pt in rgb.reshape(-1, 3)],
+                       dtype='float64').reshape(rgb.shape)
+        hsv32 = hsv.astype('float32')
+
+        assert hsv2rgb(hsv).dtype == hsv.dtype
+        assert hsv2rgb(hsv32).dtype == hsv32.dtype
+
     # RGB to XYZ
     def test_rgb2xyz_conversion(self):
         gt = np.array([[[0.950456, 1.      , 1.088754],
@@ -145,11 +170,25 @@ def test_rgb2xyz_error_grayscale(self):
     def test_rgb2xyz_error_one_element(self):
         self.assertRaises(ValueError, rgb2xyz, self.img_rgb[0, 0])
 
+    def test_rgb2xyz_dtype(self):
+        img = self.colbars_array
+        img32 = img.astype('float32')
+
+        assert rgb2xyz(img).dtype == img.dtype
+        assert rgb2xyz(img32).dtype == img32.dtype
+
     # XYZ to RGB
     def test_xyz2rgb_conversion(self):
         assert_almost_equal(xyz2rgb(rgb2xyz(self.colbars_array)),
                             self.colbars_array)
 
+    def test_xyz2rgb_dtype(self):
+        img = rgb2xyz(self.colbars_array)
+        img32 = img.astype('float32')
+
+        assert xyz2rgb(img).dtype == img.dtype
+        assert xyz2rgb(img32).dtype == img32.dtype
+
     # RGB<->XYZ roundtrip on another image
     def test_xyz_rgb_roundtrip(self):
         img_rgb = img_as_float(self.img_rgb)
@@ -194,12 +233,26 @@ def test_rgb2rgbcie_conversion(self):
                         [ 0.        ,  0.        ,  0.        ]]])
         assert_almost_equal(rgb2rgbcie(self.colbars_array), gt)
 
+    def test_rgb2rgbcie_dtype(self):
+        img = self.colbars_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert rgb2rgbcie(img).dtype == img.dtype
+        assert rgb2rgbcie(img32).dtype == img32.dtype
+
     # RGB CIE to RGB
     def test_rgbcie2rgb_conversion(self):
         # only roundtrip test, we checked rgb2rgbcie above already
         assert_almost_equal(rgbcie2rgb(rgb2rgbcie(self.colbars_array)),
                             self.colbars_array)
 
+    def test_rgbcie2rgb_dtype(self):
+        img = rgb2rgbcie(self.colbars_array).astype('float64')
+        img32 = img.astype('float32')
+
+        assert rgbcie2rgb(img).dtype == img.dtype
+        assert rgbcie2rgb(img32).dtype == img32.dtype
+
     def test_convert_colorspace(self):
         colspaces = ['HSV', 'RGB CIE', 'XYZ', 'YCbCr', 'YPbPr', 'YDbDr']
         colfuncs_from = [
@@ -247,6 +300,13 @@ def test_rgb2grey_alpha(self):
     def test_rgb2grey_on_grey(self):
         rgb2grey(np.random.rand(5, 5))
 
+    def test_rgb2grey_dtype(self):
+        img = np.random.rand(10, 10, 3).astype('float64')
+        img32 = img.astype('float32')
+
+        assert rgb2grey(img).dtype == img.dtype
+        assert rgb2grey(img32).dtype == img32.dtype
+
     # test matrices for xyz2lab and lab2xyz generated using
     # http://www.easyrgb.com/index.php?X=CALC
     # Note: easyrgb website displays xyz*100
@@ -271,6 +331,13 @@ def test_xyz2lab(self):
                                       xyz2lab(self.xyz_array, I, "2"),
                                       decimal=2)
 
+    def test_xyz2lab_dtype(self):
+        img = self.xyz_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert xyz2lab(img).dtype == img.dtype
+        assert xyz2lab(img32).dtype == img32.dtype
+
     def test_lab2xyz(self):
         assert_array_almost_equal(lab2xyz(self.lab_array),
                                   self.xyz_array, decimal=3)
@@ -301,6 +368,13 @@ def test_lab2xyz(self):
         except ValueError:
             pass
 
+    def test_lab2xyz_dtype(self):
+        img = self.lab_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert lab2xyz(img).dtype == img.dtype
+        assert lab2xyz(img32).dtype == img32.dtype
+
     def test_rgb2lab_brucelindbloom(self):
         """
         Test the RGB->Lab conversion by comparing to the calculator on the
@@ -324,6 +398,20 @@ def test_lab_rgb_roundtrip(self):
         img_rgb = img_as_float(self.img_rgb)
         assert_array_almost_equal(lab2rgb(rgb2lab(img_rgb)), img_rgb)
 
+    def test_rgb2lab_dtype(self):
+        img = self.colbars_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert rgb2lab(img).dtype == img.dtype
+        assert rgb2lab(img32).dtype == img32.dtype
+
+    def test_lab2rgb_dtype(self):
+        img = self.lab_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert lab2rgb(img).dtype == img.dtype
+        assert lab2rgb(img32).dtype == img32.dtype
+
     # test matrices for xyz2luv and luv2xyz generated using
     # http://www.easyrgb.com/index.php?X=CALC
     # Note: easyrgb website displays xyz*100
@@ -348,6 +436,13 @@ def test_xyz2luv(self):
                                       xyz2luv(self.xyz_array, I, "2"),
                                       decimal=2)
 
+    def test_xyz2luv_dtype(self):
+        img = self.xyz_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert xyz2luv(img).dtype == img.dtype
+        assert xyz2luv(img32).dtype == img32.dtype
+
     def test_luv2xyz(self):
         assert_array_almost_equal(luv2xyz(self.luv_array),
                                   self.xyz_array, decimal=3)
@@ -367,6 +462,13 @@ def test_luv2xyz(self):
             assert_array_almost_equal(luv2xyz(luv_array_I_obs, I, "2"),
                                       self.xyz_array, decimal=3)
 
+    def test_luv2xyz_dtype(self):
+        img = self.luv_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert luv2xyz(img).dtype == img.dtype
+        assert luv2xyz(img32).dtype == img32.dtype
+
     def test_rgb2luv_brucelindbloom(self):
         """
         Test the RGB->Lab conversion by comparing to the calculator on the
@@ -387,6 +489,20 @@ def test_rgb2luv_brucelindbloom(self):
         assert_array_almost_equal(rgb2luv(self.colbars_array),
                                   gt_array, decimal=2)
 
+    def test_rgb2luv_dtype(self):
+        img = self.colbars_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert rgb2luv(img).dtype == img.dtype
+        assert rgb2luv(img32).dtype == img32.dtype
+
+    def test_luv2rgb_dtype(self):
+        img = self.luv_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert luv2rgb(img).dtype == img.dtype
+        assert luv2rgb(img32).dtype == img32.dtype
+
     def test_luv_rgb_roundtrip(self):
         img_rgb = img_as_float(self.img_rgb)
         assert_array_almost_equal(luv2rgb(rgb2luv(img_rgb)), img_rgb)
@@ -469,6 +585,20 @@ def test_yuv_roundtrip(self):
         assert_array_almost_equal(ycbcr2rgb(rgb2ycbcr(img_rgb)), img_rgb)
         assert_array_almost_equal(ydbdr2rgb(rgb2ydbdr(img_rgb)), img_rgb)
 
+    def test_rgb2yuv_dtype(self):
+        img = self.colbars_array.astype('float64')
+        img32 = img.astype('float32')
+
+        assert rgb2yuv(img).dtype == img.dtype
+        assert rgb2yuv(img32).dtype == img32.dtype
+
+    def test_yuv2rgb_dtype(self):
+        img = rgb2yuv(self.colbars_array).astype('float64')
+        img32 = img.astype('float32')
+
+        assert yuv2rgb(img).dtype == img.dtype
+        assert yuv2rgb(img32).dtype == img32.dtype
+
     def test_rgb2yiq_conversion(self):
         rgb = img_as_float(self.img_rgb)[::16, ::16]
         yiq = rgb2yiq(rgb).reshape(-1, 3)