Repository: scikit-image/scikit-image

Issue:
threshold_otsu check for single-colour is slow on large arrays
Currently in the threshold_otsu function, the code checks if the image has a single colour


    if image.min() == image.max():
        raise ValueError("threshold_otsu is expected to work with images "
                         "having more than one color. The input image seems "
                         "to have just one color {0}.".format(image.min()))`


However, for extremely large arrays min and max become increasingly slow, performing a basically identical sorting algorithm twice over.

I propose replacing this with a much simpler logical check, which will inevitably be more efficient

    np.all(image == image[0])

Checking with timeit:

    import timeit
    setup = '''
    import numpy as np
    
    arr = np.zeros((10000,10000))
    '''

    timeit.timeit("arr.min() == arr.max()", setup=setup, number=50)
    5.534767805999763

    timeit.timeit("np.all(arr == arr[0])", setup=setup, number=50)
    2.377352842999244

As expected, this time disparity gets greater and greater with increasing size.

Further, I disagree with the error statement. It seems very... unconfident in the code! "We think it's maybe possibly not the right thing, so let's throw an error, whatevs". If we genuinely do not have faith in what that code is doing, it should not be public code. It should be more assertive. I propose "threshold_otsu cannot threshold single-valued images", or better yet "cannot threshold single-valued images", as the former implies that other methods can threshold single-valued images - which is of course nonsense!
    

threshold_otsu check for single-colour is slow on large arrays
Currently in the threshold_otsu function, the code checks if the image has a single colour


    if image.min() == image.max():
        raise ValueError("threshold_otsu is expected to work with images "
                         "having more than one color. The input image seems "
                         "to have just one color {0}.".format(image.min()))`


However, for extremely large arrays min and max become increasingly slow, performing a basically identical sorting algorithm twice over.

I propose replacing this with a much simpler logical check, which will inevitably be more efficient

    np.all(image == image[0])

Checking with timeit:

    import timeit
    setup = '''
    import numpy as np
    
    arr = np.zeros((10000,10000))
    '''

    timeit.timeit("arr.min() == arr.max()", setup=setup, number=50)
    5.534767805999763

    timeit.timeit("np.all(arr == arr[0])", setup=setup, number=50)
    2.377352842999244

As expected, this time disparity gets greater and greater with increasing size.

Further, I disagree with the error statement. It seems very... unconfident in the code! "We think it's maybe possibly not the right thing, so let's throw an error, whatevs". If we genuinely do not have faith in what that code is doing, it should not be public code. It should be more assertive. I propose "threshold_otsu cannot threshold single-valued images", or better yet "cannot threshold single-valued images", as the former implies that other methods can threshold single-valued images - which is of course nonsense!
    



Discussion:

Resolving patch:
diff --git a/skimage/filters/thresholding.py b/skimage/filters/thresholding.py
--- a/skimage/filters/thresholding.py
+++ b/skimage/filters/thresholding.py
@@ -251,11 +251,6 @@ def threshold_otsu(image, nbins=256):
         Upper threshold value. All pixels with an intensity higher than
         this value are assumed to be foreground.
 
-    Raises
-    ------
-    ValueError
-         If ``image`` only contains a single grayscale value.
-
     References
     ----------
     .. [1] Wikipedia, https://en.wikipedia.org/wiki/Otsu's_Method
@@ -271,16 +266,15 @@ def threshold_otsu(image, nbins=256):
     -----
     The input image must be grayscale.
     """
-    if len(image.shape) > 2 and image.shape[-1] in (3, 4):
+    if image.ndim > 2 and image.shape[-1] in (3, 4):
         msg = "threshold_otsu is expected to work correctly only for " \
               "grayscale images; image shape {0} looks like an RGB image"
         warn(msg.format(image.shape))
 
     # Check if the image is multi-colored or not
-    if image.min() == image.max():
-        raise ValueError("threshold_otsu is expected to work with images "
-                         "having more than one color. The input image seems "
-                         "to have just one color {0}.".format(image.min()))
+    first_pixel = image.ravel()[0]
+    if np.all(image == first_pixel):
+        return first_pixel
 
     hist, bin_centers = histogram(image.ravel(), nbins, source_range='image')
     hist = hist.astype(float)


Test patch:
diff --git a/skimage/filters/tests/test_thresholding.py b/skimage/filters/tests/test_thresholding.py
--- a/skimage/filters/tests/test_thresholding.py
+++ b/skimage/filters/tests/test_thresholding.py
@@ -242,8 +242,12 @@ def test_otsu_astro_image():
 
 def test_otsu_one_color_image():
     img = np.ones((10, 10), dtype=np.uint8)
-    with testing.raises(ValueError):
-        threshold_otsu(img)
+    assert threshold_otsu(img) == 1
+
+
+def test_otsu_one_color_image_3d():
+    img = np.ones((10, 10, 10), dtype=np.uint8)
+    assert threshold_otsu(img) == 1
 
 
 def test_li_camera_image():