Repository: scikit-image/scikit-image

Issue:
radon function accepts any data type and implicitly converts arrays to float
## Description

It is stipulated in the `radon` function doc that only `float` input arrays are supported but  `radon` actually accepts any data type.

There is no explicit conversion of the array type in `radon` function code. This conversion is made implicitely in the cython `_warp_cy` function.

This is may be related to #3799 and impacts #4128 

## Way to reproduce

```python
import numpy as np
from skimage.transform import radon

pixels = 128
xy = np.arange(-pixels / 2, pixels / 2) + 0.5
x, y = np.meshgrid(xy, xy)
image = x**2 + y**2 <= (pixels/4)**2  # array.dtype is bool
theta = np.linspace(0., 180., max(image.shape), endpoint=False)
radon(image, theta=theta)
radon(image.astype(np.uint8), theta=theta)
radon(image.astype(np.float32), theta=theta)
radon(image.astype(np.float64), theta=theta)
```

## Version information
```python
>>> from __future__ import print_function
>>> import sys; print(sys.version)
3.7.3 | packaged by conda-forge | (default, Jul  1 2019, 21:52:21) 
[GCC 7.3.0]
>>> import platform; print(platform.platform())
Linux-4.15.0-58-generic-x86_64-with-debian-buster-sid
>>> import skimage; print("scikit-image version: {}".format(skimage.__version__))
scikit-image version: 0.15.0
>>> import numpy; print("numpy version: {}".format(numpy.__version__))
numpy version: 1.16.4
```




Discussion:

Resolving patch:
diff --git a/skimage/transform/radon_transform.py b/skimage/transform/radon_transform.py
--- a/skimage/transform/radon_transform.py
+++ b/skimage/transform/radon_transform.py
@@ -5,6 +5,7 @@
 from ._warps import warp
 from ._radon_transform import sart_projection_update
 from .._shared.fft import fftmodule
+from .._shared.utils import convert_to_float
 from warnings import warn
 from functools import partial
 
@@ -20,22 +21,27 @@
 __all__ = ['radon', 'order_angles_golden_ratio', 'iradon', 'iradon_sart']
 
 
-def radon(image, theta=None, circle=True):
-    """Calculates the radon transform of an image given specified
+def radon(image, theta=None, circle=True, *, preserve_range=None):
+    """
+    Calculates the radon transform of an image given specified
     projection angles.
 
     Parameters
     ----------
-    image : array_like, dtype=float
+    image : array_like
         Input image. The rotation axis will be located in the pixel with
         indices ``(image.shape[0] // 2, image.shape[1] // 2)``.
-    theta : array_like, dtype=float, optional
+    theta : array_like, optional
         Projection angles (in degrees). If `None`, the value is set to
         np.arange(180).
     circle : boolean, optional
         Assume image is zero outside the inscribed circle, making the
         width of each projection (the first dimension of the sinogram)
         equal to ``min(image.shape)``.
+    preserve_range : bool, optional
+        Whether to keep the original range of values. Otherwise, the input
+        image is converted according to the conventions of `img_as_float`.
+        Also see https://scikit-image.org/docs/dev/user_guide/data_types.html
 
     Returns
     -------
@@ -63,6 +69,17 @@ def radon(image, theta=None, circle=True):
     if theta is None:
         theta = np.arange(180)
 
+    if preserve_range is None and np.issubdtype(image.dtype, np.integer):
+        warn('Image dtype is not float. By default radon will assume '
+             'you want to preserve the range of your image '
+             '(preserve_range=True). In scikit-image 0.18 this behavior will '
+             'change to preserve_range=False. To avoid this warning, '
+             'explictiely specify the preserve_range parameter.',
+             stacklevel=2)
+        preserve_range = True
+
+    image = convert_to_float(image, preserve_range)
+
     if circle:
         shape_min = min(image.shape)
         radius = shape_min // 2


Test patch:
diff --git a/skimage/transform/tests/test_radon_transform.py b/skimage/transform/tests/test_radon_transform.py
--- a/skimage/transform/tests/test_radon_transform.py
+++ b/skimage/transform/tests/test_radon_transform.py
@@ -59,42 +59,40 @@ def test_iradon_bias_circular_phantom():
 
     reconstruction_fbp = iradon(sinogram, theta=theta)
     error = reconstruction_fbp - image
-    
+
     tol = 5e-5
     roi_err = np.abs(np.mean(error))
     assert( roi_err < tol )
-    
 
-def check_radon_center(shape, circle):
+
+def check_radon_center(shape, circle, dtype, preserve_range):
     # Create a test image with only a single non-zero pixel at the origin
-    image = np.zeros(shape, dtype=np.float)
+    image = np.zeros(shape, dtype=dtype)
     image[(shape[0] // 2, shape[1] // 2)] = 1.
     # Calculate the sinogram
     theta = np.linspace(0., 180., max(shape), endpoint=False)
-    sinogram = radon(image, theta=theta, circle=circle)
+    sinogram = radon(image, theta=theta, circle=circle,
+                     preserve_range=preserve_range)
     # The sinogram should be a straight, horizontal line
     sinogram_max = np.argmax(sinogram, axis=0)
     print(sinogram_max)
     assert np.std(sinogram_max) < 1e-6
 
 
-shapes_for_test_radon_center = [(16, 16), (17, 17)]
-circles_for_test_radon_center = [False, True]
-
-
-@testing.parametrize("shape, circle",
-                     itertools.product(shapes_for_test_radon_center,
-                                       circles_for_test_radon_center))
-def test_radon_center(shape, circle):
-    check_radon_center(shape, circle)
-
-
-rectangular_shapes = [(32, 16), (33, 17)]
+@testing.parametrize("shape", [(16, 16), (17, 17)])
+@testing.parametrize("circle", [False, True])
+@testing.parametrize("dtype", [np.float64, np.float32, np.uint8, bool])
+@testing.parametrize("preserve_range", [False, True])
+def test_radon_center(shape, circle, dtype, preserve_range):
+    check_radon_center(shape, circle, dtype, preserve_range)
 
 
-@testing.parametrize("shape", rectangular_shapes)
-def test_radon_center_rectangular(shape):
-    check_radon_center(shape, False)
+@testing.parametrize("shape", [(32, 16), (33, 17)])
+@testing.parametrize("circle", [False])
+@testing.parametrize("dtype", [np.float64, np.float32, np.uint8, bool])
+@testing.parametrize("preserve_range", [False, True])
+def test_radon_center_rectangular(shape, circle, dtype, preserve_range):
+    check_radon_center(shape, circle, dtype, preserve_range)
 
 
 def check_iradon_center(size, theta, circle):
@@ -143,7 +141,7 @@ def check_iradon_center(size, theta, circle):
 @testing.parametrize("size, theta, circle",
                      itertools.product(sizes_for_test_iradon_center,
                                        thetas_for_test_iradon_center,
-                                       circles_for_test_radon_center))
+                                       circles_for_test_iradon_center))
 def test_iradon_center(size, theta, circle):
     check_iradon_center(size, theta, circle)