Extracción de características de imagen: Patrones Binarios Locales con Cython

Introducción

El objetivo común de la extracción de características es representar los datos crudos como un conjunto reducido de características que describan mejor sus atributos principales [1]. De esta forma, podemos reducir la dimensionalidad de la entrada original y usar las nuevas características como entrada para entrenar técnicas de reconocimiento de patrones y clasificación.

Aunque hay varias características que podemos extraer de una imagen, los Patrones Binarios Locales (LBP) son un enfoque teóricamente sencillo, pero eficiente, para la clasificación de texturas en escala de grises e invariante a la rotación. Funcionan porque los patrones más frecuentes corresponden a microcaracterísticas primitivas como bordes, esquinas, puntos y regiones planas [2].

En [2], Ojala et al. mostraron que el histograma de ocurrencias discretas de los patrones uniformes es una característica de textura muy poderosa. La textura de una imagen se define como un fenómeno bidimensional caracterizado por dos propiedades: (1) estructura espacial (patrón) y (2) contraste.

Fig 1. Imagen usada para probar la metodología de patrones binarios locales

Metodología

Conjunto de vecinos circularmente simétrico

Un conjunto de vecinos circularmente simétrico para un píxel dado gc se define por los puntos con coordenadas (i, j) que rodean al punto central en un círculo de radio R, con un número de elementos P.

Textura

Definimos una textura T como la colección de píxeles en una imagen en escala de grises

donde gp corresponde al valor de gris del vecino local p.

Interpolación

Cuando un vecino no se encuentra en el centro de un píxel, ese valor de gris del vecino debe calcularse por interpolación. Por lo tanto, necesitamos definir una función que, dada una coordenada, devuelva el valor de gris interpolado.

Logrando invarianza a la escala de grises

Considerando una posible pérdida de información, es posible convertir la textura en la diferencia conjunta. Para calcularla, restamos el valor de gris del píxel central a todo el conjunto de vecinos. La distribución de diferencias conjuntas es un operador de textura altamente discriminativo. Registra las ocurrencias de varios patrones en el vecindario de cada píxel en un histograma P-dimensional.

donde gp es el valor de gris del vecino p. Esta distribución es invariante ante desplazamientos de escala de grises.

Patrón Binario Local

El operador LBP_{P,R} es, por definición, invariante ante cualquier transformación monótona de la escala de grises. Mientras el orden de los valores de gris se mantenga igual, la salida del operador LBP_{P,R} permanece constante.

donde

Patrones Binarios Locales Uniformes

En [2], Ojala menciona que, en su experiencia práctica, LBP no es un buen discriminador. Proponen seleccionar únicamente el conjunto de patrones binarios locales tal que el número de transiciones espaciales (cambios de bit 0/1) no supere 2. Por ejemplo, el patrón ‘1111’ tiene 0 transiciones espaciales, el patrón ‘1100’ tiene 1 transición espacial y el patrón ‘1101’ tiene 2 transiciones espaciales. A cada patrón uniforme se le asocia un índice único. La fórmula para crear el índice se tomó de aquí.

Ahora podemos calcular los patrones binarios locales para un píxel central. El siguiente paso es calcular los patrones binarios locales para todos los píxeles.

Pista: Por simplicidad, no estoy considerando el caso en que un índice seleccionado sea negativo (es decir, img_gray[-1][0] devuelve el último píxel de la primera columna). Si quisiéramos tener un cálculo más preciso, deberíamos considerar este caso y tratarlo.

Código en Cython

El código anterior no es perfecto; sin embargo, lo que realmente lo hace lento es que iteramos sobre todos los píxeles de la imagen. Esperar 1 minuto y 10 segundos para calcular nuestras características es mucho si tomamos en cuenta que además tenemos que entrenar una técnica de reconocimiento de patrones. Por lo tanto, necesitamos una implementación alternativa que sea mucho más rápida para los bucles. En este caso, usaremos Cython. El código se presenta en la siguiente imagen; es un bloque de código grande. Algunas partes podrían mejorarse, pero ya es mucho más rápido. Siéntete libre de dejar comentarios si no entiendes algo del código.

El código está escrito de tal forma que la mayor parte se ejecuta enteramente en la API de C. Esta estrategia acelera considerablemente la ejecución, pero también nos permite aprovechar el módulo paralelo de Cython. Vamos a repartir el trabajo entre varios núcleos de la CPU.

from libc.math cimport sin, cos, pi, ceil, floor, pow
from libc.stdlib cimport abort, malloc, free
import numpy as np
cimport numpy as np
cimport cython
from cython.parallel import prange, parallel
cimport openmp


cdef double get_pixel2d(
    double *image,
    Py_ssize_t n_rows, 
    Py_ssize_t n_cols,
    long x,
    long y) nogil:
    
    if (y < 0) or (y >= n_rows) or (x < 0) or (x >= n_cols):
        return 0
    else:
        return image[y * n_cols + x]

    
cdef double bilinear_interpolation(
    double *image,
    Py_ssize_t n_rows,
    Py_ssize_t n_cols,
    double x,
    double y) nogil:
    
    cdef double d_y, d_x, top_left, top_right, bottom_left, bottom_right
    cdef long min_y, min_x, max_y, max_x

    min_y = <long>floor(y)
    min_x = <long>floor(x)
    max_y = <long>ceil(y)
    max_x = <long>ceil(x)
    
    d_y = y - min_y
    d_x = x - min_x
    
    top_left = get_pixel2d(image, n_rows, n_cols, min_x, min_y)
    top_right = get_pixel2d(image, n_rows, n_cols, max_x, min_y)
    bottom_left = get_pixel2d(image, n_rows, n_cols, min_x, max_y)
    bottom_right = get_pixel2d(image, n_rows, n_cols, max_x, max_y)
    
    top = (1 - d_x) * top_left + d_x * top_right
    bottom = (1 - d_x) * bottom_left + d_x * bottom_right

    return (1 - d_y) * top + d_y * bottom


cdef double *joint_difference_distribution(
    double *image,
    Py_ssize_t n_rows,
    Py_ssize_t n_cols,
    int x0,
    int y0,
    int P,
    int R
) nogil:
    cdef Py_ssize_t p
    cdef double *T = <double *> malloc(sizeof(double) * P)
    cdef double x, y, gp, gc
    
    if T is NULL:
        abort()
        
    gc = get_pixel2d(image, n_rows, n_cols, x0, y0)
    
    for p in range(P):
        x = x0 + R * cos(2 * pi * p / P)
        y = y0 - R * sin(2 * pi * p / P)
        gp = bilinear_interpolation(image, n_rows, n_cols, x, y)
        T[p] = gp - gc
    
    return T


cdef int *binary_joint_distribution(double *T, Py_ssize_t T_size) nogil:
    cdef int *s_T = <int *> malloc(sizeof(int) * T_size)
    cdef Py_ssize_t i = 0
    
    for t in range(T_size):
        if T[t] >= 0.0:
            s_T[t] = 1
        else:
            s_T[t] = 0
    
    return s_T


cdef long LBP(double *T, int *s_T, Py_ssize_t T_size) nogil:
    cdef long LBP_pr = 0
    cdef Py_ssize_t i = 0
    
    for i in range(0, T_size):
        LBP_pr = LBP_pr + 2 ** i * s_T[i]
        
    return LBP_pr


cdef int is_uniform_pattern(int *s_T, Py_ssize_t s_T_size) nogil:
    cdef Py_ssize_t i = 0
    cdef int counter = 0
    
    for i in range(s_T_size - 1):
        if s_T[i] != s_T[i + 1]:
            counter += 1
        
        if counter > 2:
            return 0
    return 1


cdef int create_index(int *s_T, Py_ssize_t s_T_size) nogil:
    cdef int n_ones = 0
    cdef int rot_index = -1
    cdef int first_one = -1
    cdef int first_zero = -1
    cdef int lbp = -1

    cdef Py_ssize_t i
    for i in range(s_T_size):
        if s_T[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 == s_T_size:
        lbp = s_T_size * (s_T_size - 1) + 1
    else:
        if first_one == 0:
            rot_index = n_ones - first_zero
        else:
            rot_index = s_T_size - first_one
        lbp = 1 + (n_ones - 1) * s_T_size + rot_index
    return lbp


cdef int LBP_uniform(int *s_T, Py_ssize_t s_T_size) nogil:
    cdef int LBP_pru = 0
    cdef Py_ssize_t i = 0
    
    if is_uniform_pattern(s_T, s_T_size):
        LBP_pru = create_index(s_T, s_T_size)
    else:
        LBP_pru = 2 + s_T_size * (s_T_size - 1)
        
    return LBP_pru


@cython.boundscheck(False)
@cython.wraparound(False)
def local_binary_patterns(
    double[:, ::1] image,
    int P,
    int R,
    int num_threads=1
):
    
    cdef Py_ssize_t x = 0
    cdef Py_ssize_t y = 0
    cdef int n_rows = image.shape[0]
    cdef int n_cols = image.shape[1]
    cdef int[:, ::1] lbp = np.zeros([n_rows, n_cols], dtype=np.int32) 
    
    with nogil, parallel(num_threads=num_threads):
        for y in prange(n_rows, schedule='static'):
            for x in prange(n_cols, schedule='static'):
                T = joint_difference_distribution(&image[0][0], n_rows, n_cols, x, y, P, R)
                s_T = binary_joint_distribution(T, P)
                lbp[y, x] = LBP_uniform(s_T, P)
    
    return np.asarray(lbp)

Usando 4 hilos, pudimos calcular los patrones binarios locales para todos los píxeles en menos de 150 ms. Es tanto más rápido que ni siquiera me voy a molestar en calcular cuántas veces.

Comparación con una imagen similar

Tomemos otra imagen de ladrillos, pero esta tendrá una textura distinta.

Ambos histogramas son muy similares, y deberían serlo, ya que al final ambos son ladrillos. Sin embargo, las características del 20 al 40 son muy distintas en ambas imágenes. Esto significa que, con un buen algoritmo de machine learning, podríamos clasificarlas correctamente.

Conclusión

Los patrones binarios locales son características simples pero eficientes. La teoría detrás de ellos no es difícil de entender y son fáciles de programar. Sin embargo, si los programamos enteramente en Python, tendremos algunos problemas de rendimiento. Abordamos el problema con Cython y obtuvimos resultados muy impresionantes. El siguiente paso es recolectar distintas imágenes de textura y entrenar tu algoritmo de machine learning favorito para clasificarlas.

Notebook de Jupyter

https://github.com/ocampor/notebooks/blob/master/notebooks/image/features/local-binary-patterns.ipynb

Bibliografía

[1] Marques, O. (2011). Practical image and video processing using MATLAB. John Wiley & Sons.

[2] Ojala, T., Pietikäinen, M., & Mäenpää, T. (2002). Multiresolution gray-scale and rotation invariant texture classification with local binary patterns. IEEE Transactions on Pattern Analysis and Machine Intelligence, 24(7), 971–987.

← Volver al blog