Repository: scikit-image/scikit-image

Issue:
resize output data type is not consistent with input data type
## Description

The `resize` function actually always returns `float64` arrays whatever is the data type of the input array to be resized.

## Way to reproduce

```python
from skimage.transform import resize
from skimage import data, img_as_float32, img_as_float
img_u8 = data.camera()
img_f32 = img_as_float32(img_u8)
img_f64 = img_as_float(img_u8)
print(resize(img_u8, (256, 256)).dtype,
      resize(img_f32, (256, 256)).dtype,
      resize(img_f64, (256, 256)).dtype)

# outputs: float64 float64 float64
```

## Version information

```
3.7.3 | packaged by conda-forge | (default, Jul  1 2019, 21:52:21) 
[GCC 7.3.0]
Linux-4.15.0-58-generic-x86_64-with-debian-buster-sid
scikit-image version: 0.15.0
numpy version: 1.16.4
```




Discussion:
@rfezzani this is expected behaviour. For more information, please see this page:

https://scikit-image.org/docs/dev/user_guide/data_types.html

> Functions in skimage are designed so that they accept any of these dtypes, but, for efficiency, may return an image of a different dtype (see Output types). If you need a particular dtype, skimage provides utility functions that convert dtypes and properly rescale image intensities (see Input types).

To get back the original data type, use `skimage.util.img_as_{ubyte,uint16,uint32}` etc.

We realise that this can be confusing, but haven't found a satisfactory solution yet. See #3009 for discussion. The main issue here is that we want to use float for chaining operations, or you can unwittingly get accumulation of rounding errors in complicated pipelines.

I'm going to close this as a duplicate of #3009. I suggest we continue discussing there, unless you think there is something very specific about resize that you want to say. =)
Float 32 should not be upcast IMO. This might be a one liner PR.
I understand @jni, but as you said, it is a little bit confusing.
As pointed out by @hmaarrfk, float32 should not be upcast. This behavior was rectified by PR #3110 for the `img_as_float` function.
In the PR #3983, I ended up using `scipy.ndimage.zoom` instead of `resize` to allow computation using any floating point precision.
I traced it to here:

https://github.com/scikit-image/scikit-image/blob/6aaac3e0c8603826092f3d2086e0b37e51f883f0/skimage/_shared/utils.py#L228-L249

I don't think it'll be an easy fix. I presume @rfezzani you are using `preserve_range=True`. This should not be necessary for float32 images, since we stopped (iirc) range-checking float images. So if you use `preserve_range=False`, it should go through `img_as_float` which will not touch the image (I think) if it is float32. Could you give that a go?

I'll reopen this because it's a specific issue with `skimage._shared.utils.convert_to_float`, rather than our usual machinery.
Hi @jni, sorry my late answer. `preserve_range` is set to `False` by default so, looking at the "**way to reproduce**" example in the PR description, this parameter doesn't provide an easy solution to our problem.
I investigated some more. `skimage._shared.utils.convert_to_float` is not the only offending function. This is the easy part of the fix since, if `preserve_range` is `True`, we can simply add a check for the input image dtype and convert it to `double` only if it is not already a float type.
The true guilty function is `skimage.transform._warps_cy._warp_fast` that is specialized to the `double` type, and that's the hard part! :slightly_smiling_face: 

Resolving patch:
diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py
--- a/skimage/_shared/utils.py
+++ b/skimage/_shared/utils.py
@@ -226,7 +226,7 @@ def check_random_state(seed):
 
 
 def convert_to_float(image, preserve_range):
-    """Convert input image to double image with the appropriate range.
+    """Convert input image to float image with the appropriate range.
 
     Parameters
     ----------
@@ -237,13 +237,21 @@ def convert_to_float(image, preserve_range):
         using img_as_float. Also see
         https://scikit-image.org/docs/dev/user_guide/data_types.html
 
+    Notes:
+    ------
+    * Input images with `float32` data type are not upcast.
+
     Returns
     -------
     image : ndarray
         Transformed version of the input.
+
     """
     if preserve_range:
-        image = image.astype(np.double)
+        # Convert image to double only if it is not single or double
+        # precision float
+        if image.dtype.char not in 'df':
+            image = image.astype(float)
     else:
         image = img_as_float(image)
     return image
diff --git a/skimage/transform/_warps.py b/skimage/transform/_warps.py
--- a/skimage/transform/_warps.py
+++ b/skimage/transform/_warps.py
@@ -841,17 +841,19 @@ def warp(image, inverse_map, map_args={}, output_shape=None, order=1,
             matrix = np.linalg.inv(inverse_map.__self__.params)
 
         if matrix is not None:
-            matrix = matrix.astype(np.double)
+            matrix = matrix.astype(image.dtype)
+            ctype = 'float32_t' if image.dtype == np.float32 else 'float64_t'
             if image.ndim == 2:
-                warped = _warp_fast(image, matrix,
-                                    output_shape=output_shape,
-                                    order=order, mode=mode, cval=cval)
+                warped = _warp_fast[ctype](image, matrix,
+                                           output_shape=output_shape,
+                                           order=order, mode=mode, cval=cval)
             elif image.ndim == 3:
                 dims = []
                 for dim in range(image.shape[2]):
-                    dims.append(_warp_fast(image[..., dim], matrix,
-                                           output_shape=output_shape,
-                                           order=order, mode=mode, cval=cval))
+                    dims.append(_warp_fast[ctype](image[..., dim], matrix,
+                                                  output_shape=output_shape,
+                                                  order=order, mode=mode,
+                                                  cval=cval))
                 warped = np.dstack(dims)
 
     if warped is None:


Test patch:
diff --git a/skimage/transform/tests/test_warps.py b/skimage/transform/tests/test_warps.py
--- a/skimage/transform/tests/test_warps.py
+++ b/skimage/transform/tests/test_warps.py
@@ -331,6 +331,22 @@ def test_resize3d_bilinear():
     assert_almost_equal(resized, ref)
 
 
+def test_resize_dtype():
+    x = np.zeros((5, 5))
+    x_f32 = x.astype(np.float32)
+    x_u8 = x.astype(np.uint8)
+    x_b = x.astype(bool)
+
+    assert resize(x, (10, 10), preserve_range=False).dtype == x.dtype
+    assert resize(x, (10, 10), preserve_range=True).dtype == x.dtype
+    assert resize(x_u8, (10, 10), preserve_range=False).dtype == np.double
+    assert resize(x_u8, (10, 10), preserve_range=True).dtype == np.double
+    assert resize(x_b, (10, 10), preserve_range=False).dtype == np.double
+    assert resize(x_b, (10, 10), preserve_range=True).dtype == np.double
+    assert resize(x_f32, (10, 10), preserve_range=False).dtype == x_f32.dtype
+    assert resize(x_f32, (10, 10), preserve_range=True).dtype == x_f32.dtype
+
+
 def test_swirl():
     image = img_as_float(data.checkerboard())