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)
I just compiled scikit-image 0.14.3 from source, which was supposed to fix this issue. Unfortunately, the issue remains in CellProfiler testing (see https://github.com/CellProfiler/CellProfiler/pull/3661#issuecomment-430393640).

I get this error log:
```
    def test_05_01_bilateral(self):
        '''test the smooth module with bilateral filtering'''
        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,
        )

tests/modules/test_smooth.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../miniconda3/envs/cp-wxpython4-py2714/lib/python2.7/site-packages/skimage/restoration/_denoise.py:171: in denoise_bilateral
    range_lut, empty_dims, out)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   ???
E   ValueError: Buffer dtype mismatch, expected 'float32_t' but got 'double'

skimage/restoration/_denoise_cy.pyx:64: ValueError
```
Well well, we still haven't really released 0.14.3, maybe we can skip it and go straight to 0.14.4 ;) I'll look into it this weekend.
:sob: 
Ummmmmm...

This might be why we missed this:

https://github.com/scikit-image/scikit-image/blob/a90fd229212eddec1685318b31fc5ba915b4f4bb/skimage/restoration/tests/test_denoise.py#L187-L196

tldr we parametrize the test on image dtype but then we don't actually use the dtype parameter :man_facepalming: 
Ah, that's almost certainly my fault D: 
@AetherUnbound many eyeballs looked over that code and thought, "looks about right!" :joy:

I think the problem is that we overly constrain the *other* arguments to be `np_floats` — we should use regular double there and cast inside the function. *I think.* Don't have time to follow up right now. =\ Anyway, I'm going to reopen. :disappointed: 
I don't think there is fault so much as oops
Turns out my first forray into cython is not without Oops 😅

Resolving patch:
diff --git a/skimage/restoration/_denoise.py b/skimage/restoration/_denoise.py
--- a/skimage/restoration/_denoise.py
+++ b/skimage/restoration/_denoise.py
@@ -10,21 +10,21 @@
 import numbers
 
 
-def _gaussian_weight(sigma_sqr, value):
-    return np.exp(-0.5 * value * value / sigma_sqr)
+def _gaussian_weight(sigma_sqr, value, *, dtype=float):
+    return np.exp(-0.5 * value * value / sigma_sqr, dtype=dtype)
 
 
-def _compute_color_lut(bins, sigma, max_value, dtype):
-    color_lut = _gaussian_weight(sigma**2, np.linspace(0, max_value/bins, bins,
-                                                       endpoint=False))
+def _compute_color_lut(bins, sigma, max_value, *, dtype=float):
+    values = np.linspace(0, max_value / bins, bins, endpoint=False)
+    color_lut = _gaussian_weight(sigma**2, values, dtype=dtype)
     return color_lut
 
 
-def _compute_range_lut(win_size, sigma):
+def _compute_range_lut(win_size, sigma, *, dtype=float):
     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()
+    range_lut = _gaussian_weight(sigma**2, d, dtype=dtype).ravel()
     return range_lut
 
 
@@ -148,9 +148,10 @@ def denoise_bilateral(image, win_size=None, sigma_color=None, sigma_spatial=1,
 
     sigma_color = sigma_color or image.std()
 
-    color_lut = _compute_color_lut(bins, sigma_color, max_value, image.dtype)
+    color_lut = _compute_color_lut(bins, sigma_color, max_value,
+                                   dtype=image.dtype)
 
-    range_lut = _compute_range_lut(win_size, sigma_spatial)
+    range_lut = _compute_range_lut(win_size, sigma_spatial, dtype=image.dtype)
 
     out = np.empty(image.shape, dtype=image.dtype)
 
@@ -221,7 +222,8 @@ def denoise_tv_bregman(image, weight, max_iter=100, eps=1e-3, isotropic=True):
     shape_ext = (rows + 2, cols + 2, dims)
 
     out = np.zeros(shape_ext, image.dtype)
-    _denoise_tv_bregman(image, weight, max_iter, eps, isotropic, out)
+    _denoise_tv_bregman(image, image.dtype.type(weight), max_iter, eps,
+                        isotropic, out)
     return np.squeeze(out[1:-1, 1:-1])
 
 


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
@@ -190,12 +190,24 @@ 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])
+@pytest.mark.parametrize('dtype', [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)
+    img = np.clip(img, 0, 1).astype(dtype)
+
+    # check that we can process multiple float types
+    out = restoration.denoise_bilateral(img, sigma_color=0.1,
+                                        sigma_spatial=10, multichannel=False)
+
+
+@pytest.mark.parametrize('dtype', [np.float32, np.double])
+def test_denoise_bregman_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).astype(dtype)
 
     # check that we can process multiple float types
     out = restoration.denoise_bilateral(img, sigma_color=0.1,