Repository: scikit-image/scikit-image

Issue:
threshold_multiotsu fails for array with 0
## Description
For the input array below with 3 classes, one of which being 0, threshold_multiotsu with nbins=128 returns the error `The input image has only 2 different values. It cannot be thresholded in 3 classes.` However, increasing nbins to 180 or changing `arr[0] = 0.1` avoids this error.

## Way to reproduce
```
arr = np.asarray([0.0, 0.12570100514093369, 0.0007003172065461084])
skimage.filters.threshold_multiotsu(arr, nbins=128)
>>> ValueError: The input image has only 2 different values. It cannot be thresholded in 3 classes.
```

```
arr = np.asarray([0.0, 0.12570100514093369, 0.0007003172065461084])
skimage.filters.threshold_multiotsu(arr, nbins=180)
>>> array([0.00034917, 0.00104751])
```

## Version information
```python
Python 3.8.13
scikit-image==0.19.1
numpy==1.22.0
```



Discussion:
Interesting, thanks for the report @callum-jpg! What's happening here is that the number of different values is being counted *after* discretisation into bins, which makes sense because the binned data is what will be used when computing the multi-threshold.

To be honest, I wonder whether we should just call this expected behaviour, as I don't see a nice way to fix this in the general case (e.g. a big image with only 3 different values as above, two of which are very close). Perhaps we can improve the error message to something like, "After discretization into bins, the image only has 2 values, which cannot be separated into 3 classes. If there are more unique values before discretization, try increasing the number of bins."

Another idea would be to return the same value for the two thresholds, but that might be surprising also...
Thank you for your report, @callum-jpg!
> "After discretization into bins, the image only has 2 values, which cannot be separated into 3 classes. If there are more unique values before discretization, try increasing the number of bins."

I like the idea, @jni, and (as you see with the labels) I'm kinda confused :sweat_smile: I'd need to check alternatives, but I don't think there's much we could do from the algorithm definition. I see that @rsenft1 and others are tinkering with it at [CellProfiler#4436](https://github.com/CellProfiler/CellProfiler/issues/4436); @rsenft1, maybe you have ideas for us :thinking: Could you share a little bit about the bugs you're having, and how are you dealing with them? Thank you! 
> To be honest, I wonder whether we should just call this expected behaviour, as I don't see a nice way to fix this in the general case (e.g. a big image with only 3 different values as above, two of which are very close). Perhaps we can improve the error message to something like, "After discretization into bins, the image only has 2 values, which cannot be separated into 3 classes. If there are more unique values before discretization, try increasing the number of bins."

I agree with @jni, improving the error message is a good solution :wink: 

Resolving patch:
diff --git a/skimage/filters/thresholding.py b/skimage/filters/thresholding.py
--- a/skimage/filters/thresholding.py
+++ b/skimage/filters/thresholding.py
@@ -58,7 +58,7 @@ def _try_all(image, methods=None, figsize=None, num_cols=2, verbose=True):
 
     # Compute the image histogram for better performances
     nbins = 256  # Default in threshold functions
-    hist = histogram(image.ravel(), nbins, source_range='image')
+    hist = histogram(image.reshape(-1), nbins, source_range='image')
 
     # Handle default value
     methods = methods or {}
@@ -66,7 +66,7 @@ def _try_all(image, methods=None, figsize=None, num_cols=2, verbose=True):
     num_rows = math.ceil((len(methods) + 1.) / num_cols)
     fig, ax = plt.subplots(num_rows, num_cols, figsize=figsize,
                            sharex=True, sharey=True)
-    ax = ax.ravel()
+    ax = ax.reshape(-1)
 
     ax[0].imshow(image, cmap=plt.cm.gray)
     ax[0].set_title('Original')
@@ -304,9 +304,9 @@ def _validate_image_histogram(image, hist, nbins=None, normalize=False):
             counts, bin_centers = counts[start:end], bin_centers[start:end]
     else:
         counts, bin_centers = histogram(
-                image.ravel(), nbins, source_range='image', normalize=normalize
+            image.reshape(-1), nbins, source_range='image', normalize=normalize
             )
-    return counts.astype(float), bin_centers
+    return counts.astype('float32', copy=False), bin_centers
 
 
 def threshold_otsu(image=None, nbins=256, *, hist=None):
@@ -357,7 +357,7 @@ def threshold_otsu(image=None, nbins=256, *, hist=None):
     # Check if the image has more than one intensity value; if not, return that
     # value
     if image is not None:
-        first_pixel = image.ravel()[0]
+        first_pixel = image.reshape(-1)[0]
         if np.all(image == first_pixel):
             return first_pixel
 
@@ -430,7 +430,7 @@ def threshold_yen(image=None, nbins=256, *, hist=None):
         return bin_centers[0]
 
     # Calculate probability mass function
-    pmf = counts.astype(np.float32) / counts.sum()
+    pmf = counts.astype('float32', copy=False) / counts.sum()
     P1 = np.cumsum(pmf)  # Cumulative normalized histogram
     P1_sq = np.cumsum(pmf ** 2)
     # Get cumsum calculated from end of squared array:
@@ -511,7 +511,7 @@ def threshold_isodata(image=None, nbins=256, return_all=False, *, hist=None):
         else:
             return bin_centers[0]
 
-    counts = counts.astype(np.float32)
+    counts = counts.astype('float32', copy=False)
 
     # csuml and csumh contain the count of pixels in that bin or lower, and
     # in all bins strictly higher than that bin, respectively
@@ -729,7 +729,7 @@ def threshold_li(image, *, tolerance=None, initial_guess=None,
     if image.dtype.kind in 'iu':
         hist, bin_centers = histogram(image.reshape(-1),
                                       source_range='image')
-        hist = hist.astype(float)
+        hist = hist.astype('float32', copy=False)
         while abs(t_next - t_curr) > tolerance:
             t_curr = t_next
             foreground = bin_centers > t_curr
@@ -837,7 +837,7 @@ def find_local_maxima_idx(hist):
 
     counts, bin_centers = _validate_image_histogram(image, hist, nbins)
 
-    smooth_hist = counts.astype(np.float64, copy=False)
+    smooth_hist = counts.astype('float32', copy=False)
 
     for counter in range(max_num_iter):
         smooth_hist = ndi.uniform_filter1d(smooth_hist, 3)
@@ -923,13 +923,14 @@ def threshold_triangle(image, nbins=256):
     """
     # nbins is ignored for integer arrays
     # so, we recalculate the effective nbins.
-    hist, bin_centers = histogram(image.ravel(), nbins, source_range='image')
+    hist, bin_centers = histogram(image.reshape(-1), nbins,
+                                  source_range='image')
     nbins = len(hist)
 
     # Find peak, lowest and highest gray levels.
     arg_peak_height = np.argmax(hist)
     peak_height = hist[arg_peak_height]
-    arg_low_level, arg_high_level = np.where(hist > 0)[0][[0, -1]]
+    arg_low_level, arg_high_level = np.flatnonzero(hist)[[0, -1]]
 
     # Flip is True if left tail is shorter.
     flip = arg_peak_height - arg_low_level < arg_high_level - arg_peak_height
@@ -1011,7 +1012,6 @@ def _mean_std(image, w):
     padded *= padded
     integral_sq = integral_image(padded, dtype=np.float64)
 
-
     # Create lists of non-zero kernel indices and values
     kernel_indices = list(itertools.product(*tuple([(0, _w) for _w in w])))
     kernel_values = [(-1) ** (image.ndim % 2 != np.sum(indices) % 2)
@@ -1282,15 +1282,18 @@ def threshold_multiotsu(image=None, classes=3, nbins=256, *, hist=None):
     # calculating the histogram and the probability of each gray level.
     prob, bin_centers = _validate_image_histogram(image, hist, nbins,
                                                   normalize=True)
-    prob = prob.astype('float32')
+    prob = prob.astype('float32', copy=False)
 
     nvalues = np.count_nonzero(prob)
     if nvalues < classes:
-        msg = (f'The input image has only {nvalues} different values. '
-               f'It cannot be thresholded in {classes} classes.')
+        msg = (f'After discretization into bins, the input image has '
+               f'only {nvalues} different values. It cannot be thresholded '
+               f'in {classes} classes. If there are more unique values '
+               f'before discretization, try increasing the number of bins '
+               f'(`nbins`).')
         raise ValueError(msg)
     elif nvalues == classes:
-        thresh_idx = np.where(prob > 0)[0][:-1]
+        thresh_idx = np.flatnonzero(prob)[:-1]
     else:
         # Get threshold indices
         try:


Test patch: