Repository: scikit-image/scikit-image

Issue:
Unexpected behavior of skimage.feature.local_binary_pattern
Hi,
The local binary pattern method in skimage is producing unexpected results. For some flat regions, it correctly produces 255 (e.g. binary 11111111). But for some other flat regions in the same image, it produces noise (decimal 85, binary 01010101).

Here is a link with code example and images to reproduce the issue:
http://stackoverflow.com/questions/42322400/unexpected-behavior-in-local-binary-pattern-python-skimage

OS version: Mac OS X 10.9.5

python version: '3.5.2 |Continuum Analytics, Inc.| (default, Jul  2 2016, 17:52:12) \n[GCC 4.2.1 Compatible Apple LLVM 4.2 (clang-425.0.28)]'

skimage.__version__: '0.12.3'




Discussion:
As mentioned on the S/O question: this is because of floating point differences.  If you convert the image to ubyte first, it'll work fine.  Still, I see this as a usability bug.

Do you want to file a patch that issues a warning to the user if they do not use integer types with LBPs?
@stefanv 
Hi, I've tried to fix this issue. Please let me know if this is the correct approach.

Add these lines to the function `local_binary_pattern` in scikit-image/skimage/feature/texture.py
```
import re
from warnings import warn
match = re.search('int',str(image.dtype))
if not match:
      warn('Image data type incorrect. Use integer data types for local_binary_pattern')
```

Resolving patch:
diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py
--- a/skimage/feature/texture.py
+++ b/skimage/feature/texture.py
@@ -1,15 +1,14 @@
 """
 Methods to characterize image textures.
 """
+import warnings
 
 import numpy as np
 
 from .._shared.utils import check_nD
 from ..color import gray2rgb
 from ..util import img_as_float
-from ._texture import (_glcm_loop,
-                       _local_binary_pattern,
-                       _multiblock_lbp)
+from ._texture import _glcm_loop, _local_binary_pattern, _multiblock_lbp
 
 
 def graycomatrix(image, distances, angles, levels=None, symmetric=False,
@@ -345,6 +344,12 @@ def local_binary_pattern(image, P, R, method='default'):
         'nri_uniform': ord('N'),
         'var': ord('V')
     }
+    if np.issubdtype(image.dtype, np.floating):
+        warnings.warn(
+            "Applying `local_binary_pattern` to floating-point images may "
+            "give unexpected results when small numerical differences between "
+            "adjacent pixels are present. It is recommended to use this "
+            "function with images of integer dtype.")
     image = np.ascontiguousarray(image, dtype=np.float64)
     output = _local_binary_pattern(image, P, R, methods[method.lower()])
     return output


Test patch:
diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py
--- a/skimage/feature/tests/test_texture.py
+++ b/skimage/feature/tests/test_texture.py
@@ -2,12 +2,8 @@
 import pytest
 
 from skimage._shared.testing import expected_warnings, test_parallel
-from skimage.feature import (graycomatrix,
-                             graycoprops,
-                             greycomatrix,
-                             greycoprops,
-                             local_binary_pattern,
-                             multiblock_lbp)
+from skimage.feature import (graycomatrix, graycoprops, greycomatrix,
+                             greycoprops, local_binary_pattern, multiblock_lbp)
 from skimage.transform import integral_image
 
 
@@ -232,7 +228,7 @@ def setup(self):
                                [167, 255,  63,  40,  128, 255],
                                [  0, 255,  30,  34,  255,  24],
                                [146, 241, 255,   0,  189, 126]],
-                              dtype='double')
+                              dtype=np.uint8)
 
     @test_parallel()
     def test_default(self):
@@ -255,6 +251,20 @@ def test_ror(self):
                         [  3,   5,   0, 255,   1,   3]])
         np.testing.assert_array_equal(lbp, ref)
 
+    @pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
+    def test_float_warning(self, dtype):
+        image = self.image.astype(dtype)
+        msg = "Applying `local_binary_pattern` to floating-point images"
+        with expected_warnings([msg]):
+            lbp = local_binary_pattern(image, 8, 1, 'ror')
+        ref = np.array([[  0, 127,   0, 255,   3, 255],
+                        [ 31,   0,   5,  51,   1,   7],
+                        [119, 255,   3, 127,   0,  63],
+                        [  3,   1,  31,  63,  31,   0],
+                        [255,   1, 255,  95,   0, 127],
+                        [  3,   5,   0, 255,   1,   3]])
+        np.testing.assert_array_equal(lbp, ref)
+
     def test_uniform(self):
         lbp = local_binary_pattern(self.image, 8, 1, 'uniform')
         ref = np.array([[0, 7, 0, 8, 2, 8],
@@ -278,7 +288,9 @@ def test_var(self):
 
         # Use P=4 to avoid interpolation effects
         P, R = 4, 1
-        lbp = local_binary_pattern(image, P, R, 'var')
+        msg = "Applying `local_binary_pattern` to floating-point images"
+        with expected_warnings([msg]):
+            lbp = local_binary_pattern(image, P, R, 'var')
 
         # Take central part to avoid border effect.
         lbp = lbp[5:-5, 5:-5]