Repository: scikit-image/scikit-image

Issue:
Denoise throws ValueError
## Description

`_denoise_cy.pyx` throws a `ValueError` saying a `double` was expected but a `float` was received.

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/aether/programs/anaconda3/envs/cellprofiler/lib/python2.7/site-packages/skimage/restoration/_denoise.py", line 117, in denoise_bilateral
    bins, mode, cval)
  File "skimage/restoration/_denoise_cy.pyx", line 114, in skimage.restoration._denoise_cy._denoise_bilateral
ValueError: Buffer dtype mismatch, expected 'double' but got 'float'
```

## Way to reproduce

- [X] Code example
```
import numpy as np
import skimage.restoration

sigma = 16.0
sigma_range = .2
np.random.seed(0)
image = np.random.uniform(size=(100, 100)).astype(np.float32)
mask = np.ones(image.shape, bool)
mask[40:60, 45:65] = False
expected = skimage.restoration.denoise_bilateral(
    image=image,
    multichannel=False,
    sigma_color=sigma_range,
    sigma_spatial=sigma,
)
```
- [ ] Relevant images (if any)
- [X] Operating system and version: Ubuntu 16.04
- [X] Python version
```
>>> print(sys.version)
2.7.14 |Anaconda, Inc.| (default, Dec  7 2017, 17:05:42) 
[GCC 7.2.0]
```
- [X] scikit-image version (run `skimage.__version__`)
```
>>> skimage.__version__
'0.14.1'
```



Discussion:
Holy moly! I believe #3110 introduced this: before, we would have upconverted your array to float64.

@hmaarrfk added some nice fused typedefs in #3253. We should fast-track that so that we can use Cython fused types for float everywhere doubles are used currently — including in `denoise_bilateral`.
@jni, I think you should just pull out the relevant file. I'll deal with the conflicts later. (whoever works on this should pull out the relevant file)

Resolving patch:
diff --git a/benchmarks/benchmark_transform_warp.py b/benchmarks/benchmark_transform_warp.py
new file mode 100644
--- /dev/null
+++ b/benchmarks/benchmark_transform_warp.py
@@ -0,0 +1,50 @@
+import numpy as np
+from skimage.transform import SimilarityTransform, warp
+from skimage.util.dtype import convert
+import warnings
+import functools
+import inspect
+
+
+class WarpSuite:
+    params = ([np.uint8, np.uint16, np.float32, np.float64],
+              [128, 1024, 4096],
+              [0, 1, 3],
+              # [np.float32, np.float64]
+              )
+    # param_names = ['dtype_in', 'N', 'order', 'dtype_tform']
+    param_names = ['dtype_in', 'N', 'order']
+
+    # def setup(self, dtype_in, N, order, dtype_tform):
+    def setup(self, dtype_in, N, order):
+        with warnings.catch_warnings():
+            warnings.filterwarnings("ignore", "Possible precision loss")
+            self.image = convert(np.random.random((N, N)), dtype=dtype_in)
+        self.tform = SimilarityTransform(scale=1, rotation=np.pi / 10,
+                                         translation=(0, 4))
+        self.tform.params = self.tform.params.astype('float32')
+        self.order = order
+
+        if 'dtype' in inspect.signature(warp).parameters:
+            self.warp = functools.partial(warp, dtype=self.image.dtype)
+        else:
+            # Keep a call to functools to have the same number of python
+            # function calls
+            self.warp = functools.partial(warp)
+
+    # def time_same_type(self, dtype_in, N, order, dtype_tform):
+    def time_same_type(self, dtype_in, N, order):
+        """Test the case where the users wants to preserve their same low
+        precision data type."""
+        result = self.warp(self.image, self.tform, order=self.order,
+                           preserve_range=True)
+
+        # convert back to input type, no-op if same type
+        result = result.astype(dtype_in, copy=False)
+
+    # def time_to_float64(self, dtype_in, N, order, dtype_form):
+    def time_to_float64(self, dtype_in, N, order):
+        """Test the case where want to upvert to float64 for continued
+        transformations."""
+        result = warp(self.image, self.tform, order=self.order,
+                      preserve_range=True)
diff --git a/skimage/restoration/_denoise.py b/skimage/restoration/_denoise.py
--- a/skimage/restoration/_denoise.py
+++ b/skimage/restoration/_denoise.py
@@ -1,14 +1,33 @@
+import itertools
 import scipy.stats
 import numpy as np
 from math import ceil
 from .. import img_as_float
-from ..restoration._denoise_cy import _denoise_bilateral, _denoise_tv_bregman
+from ._denoise_cy import _denoise_bilateral, _denoise_tv_bregman
 from .._shared.utils import warn
 import pywt
 import skimage.color as color
 import numbers
 
 
+def _gaussian_weight(sigma_sqr, value):
+    return np.exp(-0.5 * value * value / sigma_sqr)
+
+
+def _compute_color_lut(bins, sigma, max_value, dtype):
+    color_lut = _gaussian_weight(sigma**2, np.linspace(0, max_value/bins, bins,
+                                                       endpoint=False))
+    return color_lut
+
+
+def _compute_range_lut(win_size, sigma):
+    grid_points = np.arange(-win_size, win_size + 1)
+    rr, cc = np.meshgrid(grid_points, grid_points, indexing='ij')
+    d = np.hypot(rr, cc)
+    range_lut = _gaussian_weight(sigma**2, d).ravel()
+    return range_lut
+
+
 def denoise_bilateral(image, win_size=None, sigma_color=None, sigma_spatial=1,
                       bins=10000, mode='constant', cval=0, multichannel=False):
     """Denoise image using bilateral filter.
@@ -109,8 +128,43 @@ def denoise_bilateral(image, win_size=None, sigma_color=None, sigma_spatial=1,
     if win_size is None:
         win_size = max(5, 2 * int(ceil(3 * sigma_spatial)) + 1)
 
-    return _denoise_bilateral(image, win_size, sigma_color, sigma_spatial,
-                              bins, mode, cval)
+    min_value = image.min()
+    max_value = image.max()
+
+    if min_value == max_value:
+        return image
+
+    # if image.max() is 0, then dist_scale can have an unverified value
+    # and color_lut[<int>(dist * dist_scale)] may cause a segmentation fault
+    # so we verify we have a positive image and that the max is not 0.0.
+    if min_value < 0.0:
+        raise ValueError("Image must contain only positive values")
+
+    if max_value == 0.0:
+        raise ValueError("The maximum value found in the image was 0.")
+
+    image = np.atleast_3d(img_as_float(image))
+    image = np.ascontiguousarray(image)
+
+    sigma_color = sigma_color or image.std()
+
+    color_lut = _compute_color_lut(bins, sigma_color, max_value, image.dtype)
+
+    range_lut = _compute_range_lut(win_size, sigma_spatial)
+
+    out = np.empty(image.shape, dtype=image.dtype)
+
+    dims = image.shape[2]
+
+    # There are a number of arrays needed in the Cython function.
+    # It's easier to allocate them outside of Cython so that all
+    # arrays are in the same type, then just copy the empty array
+    # where needed within Cython.
+    empty_dims = np.empty(dims, dtype=image.dtype)
+
+    return _denoise_bilateral(image, image.max(), win_size, sigma_color,
+                              sigma_spatial, bins, mode, cval, color_lut,
+                              range_lut, empty_dims, out)
 
 
 def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True):
@@ -157,7 +211,18 @@ def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True):
     .. [4] https://web.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf
 
     """
-    return _denoise_tv_bregman(image, weight, max_iter, eps, isotropic)
+    image = np.atleast_3d(img_as_float(image))
+    image = np.ascontiguousarray(image)
+
+    rows = image.shape[0]
+    cols = image.shape[1]
+    dims = image.shape[2]
+
+    shape_ext = (rows + 2, cols + 2, dims)
+
+    out = np.zeros(shape_ext, image.dtype)
+    _denoise_tv_bregman(image, weight, max_iter, eps, isotropic, out)
+    return np.squeeze(out[1:-1, 1:-1])
 
 
 def _denoise_tv_chambolle_nd(image, weight=0.1, eps=2.e-4, n_iter_max=200):


Test patch:
diff --git a/skimage/restoration/tests/test_denoise.py b/skimage/restoration/tests/test_denoise.py
--- a/skimage/restoration/tests/test_denoise.py
+++ b/skimage/restoration/tests/test_denoise.py
@@ -1,4 +1,5 @@
 import numpy as np
+import pytest
 
 from skimage import restoration, data, color, img_as_float, measure
 from skimage.measure import compare_psnr
@@ -189,6 +190,18 @@ def test_denoise_bilateral_2d():
     assert_(out1[30:45, 5:15].std() > out2[30:45, 5:15].std())
 
 
+@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.double])
+def test_denoise_bilateral_types(dtype):
+    img = checkerboard_gray.copy()[:50, :50]
+    # add some random noise
+    img += 0.5 * img.std() * np.random.rand(*img.shape)
+    img = np.clip(img, 0, 1)
+
+    # check that we can process multiple float types
+    out = restoration.denoise_bilateral(img, sigma_color=0.1,
+                                        sigma_spatial=10, multichannel=False)
+
+
 def test_denoise_bilateral_zeros():
     img = np.zeros((10, 10))
     assert_equal(img, restoration.denoise_bilateral(img, multichannel=False))