Repository: scikit-image/scikit-image

Issue:
denoise_bilateral works separately on channels, but no warning raised for 3-D images
The implementation of `restoration.denoise_bilateral` is 2-D only, with the last dimension considered to be for channels. However, this is not documented in the docstring, and no warning is raised if a 3-D image (with last dimension > 3) is passed. 

I used `denoise_bilateral` in the past on 3-D images thinking that the implementation was 3-D. We could consider making the code 3-D or nD in the future, but for now fixing the docstring + warning looks like a reasonable goal for 0.12.



Discussion:

Resolving patch:
diff --git a/skimage/restoration/_denoise.py b/skimage/restoration/_denoise.py
--- a/skimage/restoration/_denoise.py
+++ b/skimage/restoration/_denoise.py
@@ -3,10 +3,11 @@
 from .. import img_as_float
 from ..restoration._denoise_cy import _denoise_bilateral, _denoise_tv_bregman
 from .._shared.utils import _mode_deprecations
+import warnings
 
 
 def denoise_bilateral(image, win_size=5, sigma_range=None, sigma_spatial=1,
-                      bins=10000, mode='constant', cval=0):
+                      bins=10000, mode='constant', cval=0, multichannel=True):
     """Denoise image using bilateral filter.
 
     This is an edge-preserving and noise reducing denoising filter. It averages
@@ -45,6 +46,9 @@ def denoise_bilateral(image, win_size=5, sigma_range=None, sigma_spatial=1,
     cval : string
         Used in conjunction with mode 'constant', the value outside
         the image boundaries.
+    multichannel : bool
+        Whether the last axis of the image is to be interpreted as multiple
+        channels or another spatial dimension.
 
     Returns
     -------
@@ -64,6 +68,38 @@ def denoise_bilateral(image, win_size=5, sigma_range=None, sigma_spatial=1,
     >>> noisy = np.clip(noisy, 0, 1)
     >>> denoised = denoise_bilateral(noisy, sigma_range=0.05, sigma_spatial=15)
     """
+    if multichannel:
+        if image.ndim != 3:
+            if image.ndim == 2:
+                raise ValueError("Use ``multichannel=False`` for 2D grayscale "
+                                 "images. The last axis of the input image "
+                                 "must be multiple color channels not another "
+                                 "spatial dimension.")
+            else:
+                raise ValueError("Bilateral filter is only implemented for "
+                                 "2D grayscale images (image.ndim == 2) and "
+                                 "2D multichannel (image.ndim == 3) images, "
+                                 "but the input image has {0} dimensions. "
+                                 "".format(image.ndim))
+        elif image.shape[2] not in (3, 4):
+            if image.shape[2] > 4:
+                warnings.warn("The last axis of the input image is interpreted "
+                              "as channels. Input image with shape {0} has {1} "
+                              "channels in last axis. ``denoise_bilateral`` is "
+                              "implemented for 2D grayscale and color images "
+                              "only.".format(image.shape, image.shape[2]))
+            else:
+                msg = "Input image must be grayscale, RGB, or RGBA; but has shape {0}."
+                warnings.warn(msg.format(image.shape))
+    else:
+        if image.ndim > 2:
+            raise ValueError("Bilateral filter is not implemented for "
+                             "grayscale images of 3 or more dimensions, "
+                             "but input image has {0} dimension. Use "
+                             "``multichannel=True`` for 2-D RGB "
+                             "images.".format(image.shape))
+
+
     mode = _mode_deprecations(mode)
     return _denoise_bilateral(image, win_size, sigma_range, sigma_spatial,
                               bins, mode, cval)


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
@@ -2,6 +2,7 @@
 from numpy.testing import run_module_suite, assert_raises, assert_equal
 
 from skimage import restoration, data, color, img_as_float, measure
+from skimage._shared._warnings import expected_warnings
 
 
 np.random.seed(1234)
@@ -159,34 +160,56 @@ def test_denoise_bilateral_2d():
     img = np.clip(img, 0, 1)
 
     out1 = restoration.denoise_bilateral(img, sigma_range=0.1,
-                                         sigma_spatial=20)
+                                         sigma_spatial=20, multichannel=False)
     out2 = restoration.denoise_bilateral(img, sigma_range=0.2,
-                                         sigma_spatial=30)
+                                         sigma_spatial=30, multichannel=False)
 
     # make sure noise is reduced in the checkerboard cells
     assert img[30:45, 5:15].std() > out1[30:45, 5:15].std()
     assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std()
 
 
-def test_denoise_bilateral_3d():
+def test_denoise_bilateral_color():
     img = checkerboard.copy()
     # add some random noise
     img += 0.5 * img.std() * np.random.rand(*img.shape)
     img = np.clip(img, 0, 1)
 
-    out1 = restoration.denoise_bilateral(img, sigma_range=0.1,
-                                         sigma_spatial=20)
-    out2 = restoration.denoise_bilateral(img, sigma_range=0.2,
-                                         sigma_spatial=30)
+    out1 = restoration.denoise_bilateral(img, sigma_range=0.1, sigma_spatial=20)
+    out2 = restoration.denoise_bilateral(img, sigma_range=0.2, sigma_spatial=30)
 
     # make sure noise is reduced in the checkerboard cells
     assert img[30:45, 5:15].std() > out1[30:45, 5:15].std()
     assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std()
 
 
+def test_denoise_bilateral_3d_grayscale():
+    img =  np.ones((50, 50, 3))
+    assert_raises(ValueError, restoration.denoise_bilateral, img,
+                  multichannel=False)
+
+
+def test_denoise_bilateral_3d_multichannel():
+    img = np.ones((50, 50, 50))
+    with expected_warnings(["grayscale"]):
+        result = restoration.denoise_bilateral(img)
+
+    expected = np.empty_like(img)
+    expected.fill(np.nan)
+
+    assert_equal(result, expected)
+
+
+def test_denoise_bilateral_multidimensional():
+    img = np.ones((10, 10, 10, 10))
+    assert_raises(ValueError, restoration.denoise_bilateral, img)
+    assert_raises(ValueError, restoration.denoise_bilateral, img,
+                  multichannel=True)
+
+
 def test_denoise_bilateral_nan():
     img = np.NaN + np.empty((50, 50))
-    out = restoration.denoise_bilateral(img)
+    out = restoration.denoise_bilateral(img, multichannel=False)
     assert_equal(img, out)