Q:

Why does the local_binary_pattern function in scikit-image provide same value for different patterns?

I am using the local_binary_pattern function in the scikit-image package. I would like to compute the rotation invariant uniform LBP of 8 neighbors within radius 1. Here is my Python code:
import numpy as np
from skimage.feature import local_binary_pattern

image = np.array([[150, 137, 137, 146, 146, 148],
                  [145, 144, 144, 144, 142, 144],
                  [149, 144, 144, 143, 153, 147],
                  [145, 144, 147, 150, 145, 150],
                  [146, 146, 139, 148, 144, 148],
                  [129, 139, 142, 150, 146, 140]]).astype(np.uint8)

lbp = local_binary_pattern(image, 8, 1, "uniform")

print("image =")
print(image)
print("lbp =")
print(lbp)

And here is the output
image =
[[150 137 137 146 146 148]
 [145 144 144 144 142 144]
 [149 144 144 143 153 147]
 [145 144 147 150 145 150]
 [146 146 139 148 144 148]
 [129 139 142 150 146 140]]
lbp =
[[ 0.  5.  5.  1.  9.  0.]
 [ 9.  6.  9.  9.  8.  9.]
 [ 0.  8.  6.  8.  0.  3.]
 [ 9.  7.  1.  0.  7.  0.]
 [ 1.  1.  8.  9.  7.  1.]
 [ 3.  4.  9.  0.  2.  3.]]

What confuses me is that some same values in lbp do not correspond to the same uniform pattern. E.g., lbp[1, 1] and lbp[2, 2] are both 6, but the LBP of image[1, 1] is:
1 0 0
1 x 1
1 1 1

The LBP of image[2, 2] is:
1 1 1
1 x 0
1 1 1

where based on the values in lbp, I assume the local_binary_pattern function uses 'greater or equal to' to compare with neighbors.
The LBPs of image[1, 1] and image[2, 2] are both uniform. But how could image[1, 1] and image[2, 2] have the same LBP value?

A:

In order to improve robustness against rotation of the LBP descriptor the square neighbourhood is replaced by a circular one. In a circular neighbourhood formed by eight pixels the four neighbours on the diagonals do not coincide with pixel centers. The intensity values of those neighbours are commonly computed through bilinear interpolation. The following figure graphically explains why in your sample image some LBP3×3 patterns are different to the LBP8,1 patterns.

Code
w_cen  =  (1-1/np.sqrt(2))**2  # Weights
w_diag  =  (1/np.sqrt(2))**2
w_orto  =  (1-1/np.sqrt(2))*(1/np.sqrt(2))

def bilinear_interpoplation(i_cen, i_diag, i_hor, i_ver):
    return i_cen*w_cen + i_diag*w_diag + i_hor*w_orto + i_ver*w_orto

def circular_neighbourhood(x):
    [I7, I6, I5] = x[0, :]
    [I0, Ic, I4] = x[1, :]
    [I1, I2, I3] = x[2, :]
    I7i = bilinear_interpolation(Ic, I7, I0, I6)
    I5i = bilinear_interpolation(Ic, I5, I4, I6)
    I3i = bilinear_interpolation(Ic, I3, I4, I2)
    I1i = bilinear_interpolation(Ic, I1, I0, I2)
    interpolated = np.array([[I7i, I6, I5i], 
                             [ I0, Ic,  I4], 
                             [I1i, I2, I3i]])
    return interpolated

def binary_pattern(x):
    return np.where(x >= x[1, 1], 1, 0)

def display_lbps(patch):
    interpolated = circular_neighbourhood(patch)
    print('Patch =')
    print(patch)
    print('LBP of patch =')
    print(binary_pattern(patch))
    print('Interpolated patch =')
    print(interpolated)
    print('LBP of interpolated patch =')
    print(binary_pattern(interpolated))

display_lbps(image[0:3, 0:3])
display_lbps(image[1:4, 1:4])

A:

The rotation-invariant LBP does not use the pixel values of neighbors directly, but rather values interpolated on a circle (for the rotation invariance). See https://github.com/scikit-image/scikit-image/blob/master/skimage/feature/_texture.pyx#L156
Also see the original LBP paper http://vision.stanford.edu/teaching/cs231b_spring1415/papers/lbp.pdf, which mentions "The gray values of neighbors which do not fall
exactly in the center of pixels are estimated by interpolation."

