diff --git a/skimage/feature/_texture.pyx b/skimage/feature/_texture.pyx index 9daa215d..6caa7ea3 100644 --- a/skimage/feature/_texture.pyx +++ b/skimage/feature/_texture.pyx @@ -90,12 +90,13 @@ def _local_binary_pattern(double[:, ::1] image, the angular space). R : float Radius of circle (spatial resolution of the operator). - method : {'D', 'R', 'U', 'V'} + method : {'D', 'R', 'U', 'N', 'V'} Method to determine the pattern. * 'D': 'default' * 'R': 'ror' * 'U': 'uniform' + * 'N': 'nri_uniform' * 'V': 'var' Returns @@ -125,6 +126,9 @@ def _local_binary_pattern(double[:, ::1] image, cdef double lbp cdef Py_ssize_t r, c, changes, i + cdef Py_ssize_t rot_index, n_ones + cdef cnp.int8_t first_zero, first_one + for r in range(image.shape[0]): for c in range(image.shape[1]): for i in range(P): @@ -141,24 +145,83 @@ def _local_binary_pattern(double[:, ::1] image, lbp = 0 # if method == 'uniform' or method == 'var': - if method == 'U' or method == 'V': + if method == 'U' or method == 'N' or method == 'V': # determine number of 0 - 1 changes changes = 0 for i in range(P - 1): changes += abs(signed_texture[i] - signed_texture[i + 1]) + if method == 'N': + # Uniform local binary patterns are defined as patterns + # with at most 2 value changes (from 0 to 1 or from 1 to + # 0). Uniform patterns can be caraterized by their number + # `n_ones` of 1. The possible values for `n_ones` range + # from 0 to P. + # Here is an example for P = 4: + # n_ones=0: 0000 + # n_ones=1: 0001, 1000, 0100, 0010 + # n_ones=2: 0011, 1001, 1100, 0110 + # n_ones=3: 0111, 1011, 1101, 1110 + # n_ones=4: 1111 + # + # For a pattern of size P there are 2 constant patterns + # corresponding to n_ones=0 and n_ones=P. For each other + # value of `n_ones` , i.e n_ones=[1..P-1], there are P + # possible patterns which are related to each other through + # circular permutations. The total number of uniform + # patterns is thus (2 + P * (P - 1)). + # Given any pattern (uniform or not) we must be able to + # associate a unique code: + # 1. Constant patterns patterns (with n_ones=0 and + # n_ones=P) and non uniform patterns are given fixed + # code values. + # 2. Other uniform patterns are indexed considering the + # value of n_ones, and an index called 'rot_index' + # reprenting the number of circular right shifts + # required to obtain the pattern starting from a + # reference position (corresponding to all zeros stacked + # on the right). This number of rotations (or circular + # right shifts) 'rot_index' is efficiently computed by + # considering the positions of the first 1 and the first + # 0 found in the pattern. - if changes <= 2: - for i in range(P): - lbp += signed_texture[i] - else: - lbp = P + 1 - - if method == 'V': - var = np.var(texture) - if var != 0: - lbp /= var + if changes <= 2: + # We have a uniform pattern + n_ones = 0 # determies the number of ones + first_one = -1 # position was the first one + first_zero = -1 # position of the first zero + for i in range(P): + if signed_texture[i]: + n_ones += 1 + if first_one == -1: + first_one = i + else: + if first_zero == -1: + first_zero = i + if n_ones == 0: + lbp = 0 + elif n_ones == P: + lbp = P * (P - 1) + 1 + else: + if first_one == 0: + rot_index = n_ones - first_zero + else: + rot_index = P - first_one + lbp = 1 + (n_ones - 1) * P + rot_index + else: # changes > 2 + lbp = P * (P - 1) + 2 + else: # method != 'N' + if changes <= 2: + for i in range(P): + lbp += signed_texture[i] else: - lbp = np.nan + lbp = P + 1 + + if method == 'V': + var = np.var(texture) + if var != 0: + lbp /= var + else: + lbp = np.nan else: # method == 'default' for i in range(P): diff --git a/skimage/feature/tests/test_texture.py b/skimage/feature/tests/test_texture.py index d48a14f7..e4fb6acb 100644 --- a/skimage/feature/tests/test_texture.py +++ b/skimage/feature/tests/test_texture.py @@ -199,5 +199,16 @@ class TestLBP(): np.testing.assert_array_almost_equal(lbp, ref) + def test_nri_uniform(self): + lbp = local_binary_pattern(self.image, 8, 1, 'nri_uniform') + ref = np.array([[ 0, 54, 0, 57, 12, 57], + [34, 0, 58, 58, 3, 22], + [58, 57, 15, 50, 0, 47], + [10, 3, 40, 42, 35, 0], + [57, 7, 57, 58, 0, 56], + [ 9, 58, 0, 57, 7, 14]]) + np.testing.assert_array_almost_equal(lbp, ref) + + if __name__ == '__main__': np.testing.run_module_suite() diff --git a/skimage/feature/texture.py b/skimage/feature/texture.py index 7549cfdd..f9bf6c9f 100644 --- a/skimage/feature/texture.py +++ b/skimage/feature/texture.py @@ -248,6 +248,8 @@ def local_binary_pattern(image, P, R, method='default'): * 'uniform': improved rotation invariance with uniform patterns and finer quantization of the angular space which is gray scale and rotation invariant. + * 'nri_uniform': non rotation-invariant uniform patterns variant + which is only gray scale invariant [2]. * 'var': rotation invariant variance measures of the contrast of local image texture which is rotation but not gray scale invariant. @@ -263,12 +265,17 @@ def local_binary_pattern(image, P, R, method='default'): Timo Ojala, Matti Pietikainen, Topi Maenpaa. http://www.rafbis.it/biplab15/images/stories/docenti/Danielriccio/\ Articoliriferimento/LBP.pdf, 2002. + .. [2] Face recognition with local binary patterns. + Timo Ahonen, Abdenour Hadid, Matti Pietikainen, + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851, + 2004. """ methods = { 'default': ord('D'), 'ror': ord('R'), 'uniform': ord('U'), + 'nri_uniform': ord('N'), 'var': ord('V') } image = np.ascontiguousarray(image, dtype=np.double)