Repository: scikit-image/scikit-image

Issue:
`channel_axis` argument in `exposure.histogram` does not always work
## Description

<!--
(Note: for guidance on how to use `scikit-image`, please post instead on https://forum.image.sc/tag/scikit-image)
-->
Current `channel_axis` implementation assumes to have the same binning for all channels to `np.stack` before returning. But it fails in most real cases, apparently. This issue was actually raised in #3772 when it was merged. Also [test](https://github.com/scikit-image/scikit-image/blob/6a4083354f326152935166d0406127118382f30b/skimage/exposure/tests/test_exposure.py#L132) does not raise error because it happens to construct an array with the same length of binning. 

I cannot think of other implementations to make it work while keeping skimage's histogram behavior that I like (I could get much nicer visualization using matplotlib). Returning both returns as list would be the best way, I believe. 


## Way to reproduce
```python
# This code passes only when 3 channels ended up having the same binning, which is not guaranteed. 
import numpy as np
from skimage import exposure

rng = np.random.default_rng(1)
img_rgb = rng.integers(0, 256, (4, 4, 3))
hist, bin_centers = exposure.histogram(img_rgb, channel_axis=-1)

Traceback (most recent call last):
  File "/home/seongbinlim/test_hist.py", line 6, in <module>
    hist, bin_centers = exposure.histogram(img_rgb, channel_axis=-1)
  File "/home/seongbinlim/miniconda3/envs/bioio/lib/python3.9/site-packages/skimage/_shared/utils.py", line 406, in fixed_func
    return func(*args, **kwargs)
  File "/home/seongbinlim/miniconda3/envs/bioio/lib/python3.9/site-packages/skimage/exposure/exposure.py", line 264, in histogram
    hist = np.stack(hist, axis=0)
  File "<__array_function__ internals>", line 5, in stack
  File "/home/seongbinlim/miniconda3/envs/bioio/lib/python3.9/site-packages/numpy/core/shape_base.py", line 426, in stack
    raise ValueError('all input arrays must have the same shape')
ValueError: all input arrays must have the same shape

```


## Version information
```python
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print(f'scikit-image version: {skimage.__version__}')
import numpy; print(f'numpy version: {numpy.__version__}')
```

```python
# your output here
3.9.7 (default, Sep 16 2021, 13:09:58)
[GCC 7.5.0]
Linux-4.18.0-193.6.3.el8_2.x86_64-x86_64-with-glibc2.28
scikit-image version: 0.19.2
numpy version: 1.21.0
```



Discussion:
Thanks for reporting this @sbinnee, I will take a look. I made a small modification to your example to set a specific seed to `default_rng` so that the failure always occurs.

The issue reported here will only happen for integer data types when the minimum across all channels is > 0 and is not the same for each channel. I will make a small PR to fix it.


Resolving patch:
diff --git a/skimage/exposure/exposure.py b/skimage/exposure/exposure.py
--- a/skimage/exposure/exposure.py
+++ b/skimage/exposure/exposure.py
@@ -29,9 +29,7 @@ def _offset_array(arr, low_boundary, high_boundary):
             # prevent overflow errors when offsetting
             arr = arr.astype(offset_dtype)
         arr = arr - offset
-    else:
-        offset = 0
-    return arr, offset
+    return arr
 
 
 def _bincount_histogram_centers(image, source_range):
@@ -75,8 +73,10 @@ def _bincount_histogram(image, source_range, bin_centers=None):
     if bin_centers is None:
         bin_centers = _bincount_histogram_centers(image, source_range)
     image_min, image_max = bin_centers[0], bin_centers[-1]
-    image, offset = _offset_array(image, image_min, image_max)
-    hist = np.bincount(image.ravel(), minlength=image_max - image_min + 1)
+    image = _offset_array(image, image_min, image_max)
+    hist = np.bincount(
+        image.ravel(), minlength=image_max - min(image_min, 0) + 1
+    )
     if source_range == 'image':
         idx = max(image_min, 0)
         hist = hist[idx:]


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
@@ -54,6 +54,15 @@ def test_int_range_image():
     assert_equal(bin_centers[-1], 100)
 
 
+def test_multichannel_int_range_image():
+    im = np.array([[10, 5], [100, 102]], dtype=np.int8)
+    frequencies, bin_centers = exposure.histogram(im, channel_axis=-1)
+    for ch in range(im.shape[-1]):
+        assert_equal(len(frequencies[ch]), len(bin_centers))
+    assert_equal(bin_centers[0], 5)
+    assert_equal(bin_centers[-1], 102)
+
+
 def test_peak_uint_range_dtype():
     im = np.array([10, 100], dtype=np.uint8)
     frequencies, bin_centers = exposure.histogram(im, source_range='dtype')