Repository: scikit-image/scikit-image

Issue:
ValueError in exposure.histogram
There is a bug in the way exposure.histogram handles integer arrays. The problem is that the values of the array are shifted because np.bincount can only handle non-negative values. There is however no protection from integer overflow, as in the following example:

``` python
>>> from skimage import exposure
>>> exposure.histogram(array([-1,127],dtype=int8))

Traceback (most recent call last):

  File "<ipython-input-93-11812a617109>", line 1, in <module>
    exposure.histogram(array([-1,127],dtype=int8))

  File "C:\Anaconda\lib\site-packages\skimage\exposure\exposure.py", line 60, in histogram
    hist = np.bincount(image.ravel() - offset)

ValueError: The first argument of bincount must be non-negative
```

My solution would be to view the array as the equivalent unsigned type in the call to bincount.



Discussion:
Good find @Dimchord.  The question is, what is the right way to convert the array to unsigned?  Apparently we do not provide for it out of the box:

``` python
In [3]: img_as_uint([-1, 127])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-3-43263f7bc87c> in <module>()
----> 1 img_as_uint([-1, 127])

/home/silvester/workspace/scikit-image/skimage/util/dtype.py in img_as_uint(image, force_copy)
    308 
    309     """
--> 310     return convert(image, np.uint16, force_copy)
    311 
    312 

/home/silvester/workspace/scikit-image/skimage/util/dtype.py in convert(image, dtype, force_copy, uniform)
     97 
     98     if not (dtype_in in _supported_types and dtype in _supported_types):
---> 99         raise ValueError("can not convert %s to %s." % (dtypeobj_in, dtypeobj))
    100 
    101     def sign_loss():

ValueError: can not convert int64 to uint16.
```

I see three options: 
- Raise an error at the start of the function
- Truncate the negative numbers
- Rescale to be positive

``` python
In [30]: skimage.img_as_uint(np.array([-1, 127], dtype='int8'))
/home/tcaswell/.virtualenvs/conda2k/lib/python2.7/site-packages/skimage/util/dtype.py:103: UserWarning: Possible sign loss when converting negative image of type int8 to positive image of type uint16.
  "%s to positive image of type %s." % (dtypeobj_in, dtypeobj))
/home/tcaswell/.virtualenvs/conda2k/lib/python2.7/site-packages/skimage/util/dtype.py:107: UserWarning: Possible precision loss when converting from int8 to uint16
  "%s to %s" % (dtypeobj_in, dtypeobj))
Out[30]: array([    0, 65535], dtype=uint16)

```

Attempting to make the bit-counts a bit more compatible does not help either...

Yikes!

My preference would be to shift the input image by offsetting by the minimum value, compute the histogram, and then move the histogram bins accordingly. No need to fiddle around with dtypes.

I agree @jni.


Resolving patch:
diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py
--- a/skimage/exposure/exposure.py
+++ b/skimage/exposure/exposure.py
@@ -62,9 +62,18 @@ def histogram(image, nbins=256):
     # For integer types, histogramming with bincount is more efficient.
     if np.issubdtype(image.dtype, np.integer):
         offset = 0
-        if np.min(image) < 0:
-            offset = np.min(image)
-        hist = np.bincount(image.ravel() - offset)
+        image_min = np.min(image)
+        if image_min < 0:
+            offset = image_min
+            image_range = np.max(image).astype(np.int64) - image_min
+            # get smallest dtype that can hold both minimum and offset maximum
+            offset_dtype = np.promote_types(np.min_scalar_type(image_range),
+                                            np.min_scalar_type(image_min))
+            if image.dtype != offset_dtype:
+                # prevent overflow errors when offsetting
+                image = image.astype(offset_dtype)
+            image = image - offset
+        hist = np.bincount(image.ravel())
         bin_centers = np.arange(len(hist)) + offset
 
         # clip histogram to start with a non-zero bin


Test patch:
diff --git a/skimage/exposure/tests/test_exposure.py b/skimage/exposure/tests/test_exposure.py
--- a/skimage/exposure/tests/test_exposure.py
+++ b/skimage/exposure/tests/test_exposure.py
@@ -12,6 +12,27 @@
 from skimage.util.dtype import dtype_range
 
 
+# Test integer histograms
+# =======================
+
+def test_negative_overflow():
+    im = np.array([-1, 127], dtype=np.int8)
+    frequencies, bin_centers = exposure.histogram(im)
+    assert_array_equal(bin_centers, np.arange(-1, 128))
+    assert frequencies[0] == 1
+    assert frequencies[-1] == 1
+    assert_array_equal(frequencies[1:-1], 0)
+
+
+def test_all_negative_image():
+    im = np.array([-128, -1], dtype=np.int8)
+    frequencies, bin_centers = exposure.histogram(im)
+    assert_array_equal(bin_centers, np.arange(-128, 0))
+    assert frequencies[0] == 1
+    assert frequencies[-1] == 1
+    assert_array_equal(frequencies[1:-1], 0)
+
+
 # Test histogram equalization
 # ===========================