Repository: scikit-image/scikit-image

Issue:
`min_size` is not strictly conformed in the implementation of felzenszwalb
## Description

With `min_size` specified, there're still some segments with sizes that less than it. I don't know if it is an inherent flaw of the algorithm.
## Way to reproduce

```
>>> I = skimage.io.imread('dragonbaby.jpg')
>>> fz = felzenszwalb(I, scale=300, sigma=0.8, min_size=80)
>>> (fz==9).sum()
1
```

![dragonbaby](https://cloud.githubusercontent.com/assets/7010007/14789864/39cd4a56-0ad4-11e6-88a4-235ebdd1e2fd.jpg)



Discussion:
I have compared the output with that of Felzenszwalb's code. Definitely there's a bug in the skimage's implementation.

Hi @mljli !
Thank you for reporting! We'll be happy if you decide to go for a PR with the fix :wink: .

@soupault I've found the problem. Current implementation processes each channel separately and then merges their results. It's the merging criterion that breaks the `min_size` constraint. I wonder if there's a clean solution without rewriting all the code.

@mljli Sounds reasonable. Are you aware of any recommendations from literature?

Thought implementing the "official" multi-channel version might be a good way to go: implemented this here : #2088

By "official" above I meant instead of operating on the channels independently (as implemented before, and in the 2004 version of Felzenszwalb's algorithm), using the 2006 update e.g.: https://github.com/cvjena/Felzenszwalb-Segmentation/blob/master/segment-image.h which is also present on Felzenszwalbs website: http://cs.brown.edu/~pff/segment/index.html


Resolving patch:
diff --git a/skimage/segmentation/_felzenszwalb.py b/skimage/segmentation/_felzenszwalb.py
--- a/skimage/segmentation/_felzenszwalb.py
+++ b/skimage/segmentation/_felzenszwalb.py
@@ -1,7 +1,7 @@
 import numpy as np
 
 from .._shared.utils import warn
-from ._felzenszwalb_cy import _felzenszwalb_grey
+from ._felzenszwalb_cy import _felzenszwalb_cython
 
 
 def felzenszwalb(image, scale=1, sigma=0.8, min_size=20):
@@ -17,9 +17,8 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20):
     controlled indirectly through ``scale``. Segment size within an image can
     vary greatly depending on local contrast.
 
-    For RGB images, the algorithm computes a separate segmentation for each
-    channel and then combines these. The combined segmentation is the
-    intersection of the separate segmentations on the color channels.
+    For RGB images, the algorithm uses the euclidean distance between pixels in
+    color space.
 
     Parameters
     ----------
@@ -50,35 +49,6 @@ def felzenszwalb(image, scale=1, sigma=0.8, min_size=20):
     >>> segments = felzenszwalb(img, scale=3.0, sigma=0.95, min_size=5)
     """
 
-    if image.ndim == 2:
-        # assume single channel image
-        return _felzenszwalb_grey(image, scale=scale, sigma=sigma,
-                                  min_size=min_size)
-
-    elif image.ndim != 3:
-        raise ValueError("Felzenswalb segmentation can only operate on RGB and"
-                         " grey images, but input array of ndim %d given."
-                         % image.ndim)
-
-    # assume we got 2d image with multiple channels
-    n_channels = image.shape[2]
-    if n_channels != 3:
-        warn("Got image with %d channels. Is that really what you"
-             " wanted?" % image.shape[2])
-    segmentations = []
-    # compute quickshift for each channel
-    for c in range(n_channels):
-        channel = np.ascontiguousarray(image[:, :, c])
-        s = _felzenszwalb_grey(channel, scale=scale, sigma=sigma,
-                               min_size=min_size)
-        segmentations.append(s)
-
-    # put pixels in same segment only if in the same segment in all images
-    # we do this by combining the channels to one number
-    n0 = segmentations[0].max() + 1
-    n1 = segmentations[1].max() + 1
-    segmentation = (segmentations[0] + segmentations[1] * n0
-                    + segmentations[2] * n0 * n1)
-    # make segment labels consecutive numbers starting at 0
-    labels = np.unique(segmentation, return_inverse=True)[1]
-    return labels.reshape(image.shape[:2])
+    image = np.atleast_3d(image)
+    return _felzenszwalb_cython(image, scale=scale, sigma=sigma,
+                                min_size=min_size)


Test patch:
diff --git a/skimage/segmentation/tests/test_felzenszwalb.py b/skimage/segmentation/tests/test_felzenszwalb.py
--- a/skimage/segmentation/tests/test_felzenszwalb.py
+++ b/skimage/segmentation/tests/test_felzenszwalb.py
@@ -34,9 +34,7 @@ def test_minsize():
         segments = felzenszwalb(coffee, min_size=min_size, sigma=3)
         counts = np.bincount(segments.ravel())
         # actually want to test greater or equal.
-        # the construction doesn't guarantee min_size is respected
-        # after intersecting the sementations for the colors
-        assert_greater(np.mean(counts) + 1, min_size)
+        assert_greater(counts.min() + 1, min_size)
 
 
 def test_color():