Repository: scikit-image/scikit-image

Issue:
threshold_triangle fails for non-integer images
## Description

Thresholding with the triangle method can fail or give unreliable results for non-integer images. This is because of how the histogram is constructed, and because downstream of that there are a few places where bin number is equated with image data values. Observed when trying to use skimage.filters.try_all_threshold(). 

I'd be happy to make a PR with a fix.
## Way to reproduce

``` python
from skimage.filters import thresholding

image = skimage.data.text()

# expected behavior, from the unit test
thresholding.threshold_triangle(image)
104

# by default uses 256 bins
thresholding.threshold_triangle(image.astype(float))
191.365234375

# should bin the same as when it's an integer image
thresholding.threshold_triangle(image.astype(float), nbins=image.max())
147.474619289

# expected behavior, from the unit test
thresholding.threshold_triangle(np.invert(image))
151

# bin centers are not integers but are used as indices
thresholding.threshold_triangle(np.invert(image).astype(float))
IndexError                                Traceback (most recent call last)
<ipython-input-110-04d94652cd41> in <module>()
----> 1 thresholding.threshold_triangle(np.invert(image).astype(float))

/python2.7/site-packages/skimage/filters/thresholding.pyc in threshold_triangle(image, nbins)
    720     width = arg_peak_height - arg_low_level
    721     x1 = np.arange(width)
--> 722     y1 = hist[x1 + arg_low_level]
    723 
    724     # Normalize.

IndexError: arrays used as indices must be of integer (or boolean) type
```

[If reporting a bug, please include the following important information:]
- [x] Code example
- [ ] Relevant images (if any)
- [ ] Operating system and version
- [x] Python version
  Python 2.7.11
- [x] scikit-image version
  0.13dev

threshold_triangle fails for non-integer images
## Description

Thresholding with the triangle method can fail or give unreliable results for non-integer images. This is because of how the histogram is constructed, and because downstream of that there are a few places where bin number is equated with image data values. Observed when trying to use skimage.filters.try_all_threshold(). 

I'd be happy to make a PR with a fix.
## Way to reproduce

``` python
from skimage.filters import thresholding

image = skimage.data.text()

# expected behavior, from the unit test
thresholding.threshold_triangle(image)
104

# by default uses 256 bins
thresholding.threshold_triangle(image.astype(float))
191.365234375

# should bin the same as when it's an integer image
thresholding.threshold_triangle(image.astype(float), nbins=image.max())
147.474619289

# expected behavior, from the unit test
thresholding.threshold_triangle(np.invert(image))
151

# bin centers are not integers but are used as indices
thresholding.threshold_triangle(np.invert(image).astype(float))
IndexError                                Traceback (most recent call last)
<ipython-input-110-04d94652cd41> in <module>()
----> 1 thresholding.threshold_triangle(np.invert(image).astype(float))

/python2.7/site-packages/skimage/filters/thresholding.pyc in threshold_triangle(image, nbins)
    720     width = arg_peak_height - arg_low_level
    721     x1 = np.arange(width)
--> 722     y1 = hist[x1 + arg_low_level]
    723 
    724     # Normalize.

IndexError: arrays used as indices must be of integer (or boolean) type
```

[If reporting a bug, please include the following important information:]
- [x] Code example
- [ ] Relevant images (if any)
- [ ] Operating system and version
- [x] Python version
  Python 2.7.11
- [x] scikit-image version
  0.13dev



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
@@ -687,6 +687,8 @@ def threshold_triangle(image, nbins=256):
     .. [1] Zack, G. W., Rogers, W. E. and Latt, S. A., 1977,
        Automatic Measurement of Sister Chromatid Exchange Frequency,
        Journal of Histochemistry and Cytochemistry 25 (7), pp. 741-753
+    .. [2] ImageJ AutoThresholder code,
+       http://fiji.sc/wiki/index.php/Auto_Threshold
 
     Examples
     --------
@@ -695,10 +697,10 @@ def threshold_triangle(image, nbins=256):
     >>> thresh = threshold_triangle(image)
     >>> binary = image > thresh
     """
-    # nbins is ignored for interger arrays
+    # nbins is ignored for integer arrays
     # so, we recalculate the effective nbins.
     hist, bin_centers = histogram(image.ravel(), nbins)
-    nbins = bin_centers[-1] - bin_centers[0] + 1
+    nbins = len(hist)
 
     # Find peak, lowest and highest gray levels.
     arg_peak_height = np.argmax(hist)
@@ -727,14 +729,13 @@ def threshold_triangle(image, nbins=256):
     width /= norm
 
     # Maximize the length.
-    d = peak_height * arg_low_level - width * hist[arg_low_level]
-    length = peak_height * x1 - width * y1 - d
-    level = np.argmax(length) + arg_low_level
+    # The ImageJ implementation includes an additional constant when calculating
+    # the length, but here we omit it as it does not affect the location of the
+    # minimum.
+    length = peak_height * x1 - width * y1
+    arg_level = np.argmax(length) + arg_low_level
 
     if flip:
-        level = nbins - level - 1
+        arg_level = nbins - arg_level - 1
 
-    # The histogram doesn't start at zero, shift it.
-    level += bin_centers[0]
-
-    return level
+    return bin_centers[arg_level]


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
@@ -320,13 +320,28 @@ def test_mean():
     assert(threshold_mean(img) == 1.)
 
 
-def test_triangle_images():
+def test_triangle_uint_images():
     assert(threshold_triangle(np.invert(data.text())) == 151)
     assert(threshold_triangle(data.text()) == 104)
     assert(threshold_triangle(data.coins()) == 80)
     assert(threshold_triangle(np.invert(data.coins())) == 175)
 
 
+def test_triangle_float_images():
+    text = data.text()
+    int_bins = text.max() - text.min() + 1
+    # Set nbins to match the uint case and threshold as float.
+    assert(round(threshold_triangle(
+        text.astype(np.float), nbins=int_bins)) == 104)
+    # Check that rescaling image to floats in unit interval is equivalent.
+    assert(round(threshold_triangle(text / 255., nbins=int_bins) * 255) == 104)
+    # Repeat for inverted image.
+    assert(round(threshold_triangle(
+        np.invert(text).astype(np.float), nbins=int_bins)) == 151)
+    assert (round(threshold_triangle(
+        np.invert(text) / 255., nbins=int_bins) * 255) == 151)
+
+
 def test_triangle_flip():
     # Depending on the skewness, the algorithm flips the histogram.
     # We check that the flip doesn't affect too much the result.