CONTANDO OBJETOS ❤️ con TEMPLATE MATCHING | OpenCV con Python

Por Administrador

¡A contar corazones!. En el tutorial de hoy veremos como realizar el conteo de corazones en una baraja desde A, hasta la carta número 10, es decir que estaremos encontrando múltiples objetos con ayuda de template matching que vimos en un post anterior, así que ¡vamos a por ello!.

CONTENIDO

Contando objetos con template matching

  • ¿Cuál es el problema en el tutorial de OpenCV si decidimos usar este para el conteo de objetos?
  • Contando corazones con template matching
    • Función para borrar los puntos más cercanos o redundantes
    • Leyendo las imágenes de entrada y aplicando la función anterior
    • Creando un nuevo array para almacenar los puntos encontrados
    • Encerrando cada objeto encontrado
    • Visualizar el número total de corazones
    • Resultados del programa

Contando objetos con template matching

El objetivo del tutorial de hoy será contar el número de corazones presentes en una imagen, para ello he tomado una imagen de barajas y he recortado las correspondientes a los corazones desde A hasta el 10 de corazones, todas por separado.

Figura 1: Imagen de donde han sido obtenidas las cartas de corazones. (Fuente)

Dado que para este procedimiento vamos a emplear template matching, necesitaremos de las imágenes de entrada y una imagen template o plantilla. Y debido a que en todas las imágenes de los naipes de corazones estos tendrán los mismos tamaños, he recortado un corazón de una de ellas para tomarlo como imagen template.

Figura 2: Imagen template que usaremos en el programa de hoy.

De este modo lo que buscaremos es contar cuantas veces se repite este corazón en la imagen de entrada I.

Antes de ir más adelante, te recomiendo revisar el tutorial sobre Template matching | OpenCV con Python, ya que allí podemos profundizar más este tema junto con la programación y métodos que OpenCV nos ofrece.

¿Cuál es el problema en el tutorial de OpenCV si decidimos usar este para el conteo de objetos?

En primer lugar para la realización de este post y video, fui al tutorial que nos ofrece OpenCV sobre el template matching o emparejamiento de plantillas, allí me dirigí a la sección: Template Matching with Multiple Objects.

Figura 3: Captura del tutorial proporcionado por OpenCV para el template matching (Fuente).

Como podemos apreciar en la figura 3, dada una imagen de entrada (izquierda) y una imagen template de la moneada del juego de Mario Bros, a la derecha tenemos como resultado cada una de las monedas rodeada por un recuadro delimitador.

En mi caso voy a usar como imagen de entrada la carta de 3 corazones y como template la imagen que podemos visualizar en la figura 2. Así que tomé el mismo código del tutorial con el objetivo de realizar el conteo de objetos, y añadí un contador. En sí este fue el código que usé:

import cv2 as cv
import numpy as np
#from matplotlib import pyplot as plt
img_rgb = cv.imread('3_corazones.jpg')
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('template.jpg',0)
w, h = template.shape[::-1]
res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where( res >= threshold)
i = 0
for pt in zip(*loc[::-1]):
    i += 1
    cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)
cv.putText(img_rgb, str(i), (150, 40), 1, 2.5, (0, 255, 0), 3)
cv.imshow('res',img_rgb)
cv.waitKey(0)
cv.destroyAllWindows()

Como se puede apreciar he comentado la línea 3, añadí mis imágenes de entrada en las líneas 4 y 6, añadí un contador en la línea 11 y este se incrementará dentro de un for en la línea 13. Finalmente he visualizado el conteo final gracias a la línea 15 y terminamos visualizando la imagen. 

No voy entrar a detalle en este código, simplemente quiero dar a conocer lo que ocurrió cuando lo apliqué con el objetivo de contar objetos, corazones en este caso. Y bien el resultado, tomando como imagen de entrada el 3 de corazones y como imagen template el corazón de la figura 2 fue el siguiente:

Figura 4: Resultado de haber aplicado el programa del tutorial de OpenCV, acondicionando este para que cuente objetos dada una imagen.

Como puedes apreciar en la figura 4, ese fue el resultado de haber aplicado el programa, tenemos 2 corazones rodeados, pero nuestro contador indica que han sido rodeados 116 veces. Esta imagen indica que se han dibujado varias veces cuadros delimitadores para un mismo corazón, es decir que tenemos puntos redundantes.

Es por esta razón que a continuación vamos a ver como solucionar este problema, y poder contar los corazones sin datos redundantes.

Contando corazones con template matching

Ahora si vamos con el programa objetivo de este tutorial. Para las pruebas que voy a realizar, tomaré como imágenes de entrada a cada una de las cartas correspondientes a los corazones desde la A, hasta el 10, mientras que como imagen plantilla, la imagen de la figura 2, es decir el corazón.

Función para borrar los puntos más cercanos o redundantes

import cv2
import numpy as np

def points_template_matching(image, template):
    points = []
    threshold = 0.9

    res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
    candidates = np.where(res >= threshold)
    candidates = np.column_stack([candidates[1], candidates[0]])

    i = 0
    while len(candidates) > 0:
        if i == 0: points.append(candidates[0])
        else:
            to_delete = []
            for j in range(0, len(candidates)):
                diff = points[i-1] - candidates[j]
                if abs(diff[0]) < 10 and abs(diff[1]) < 10:
                    to_delete.append(j)
            candidates = np.delete(candidates, to_delete, axis=0)
            if len(candidates) == 0: break
            points.append(candidates[0])
        i += 1
    return points

Línea 1 y 2: Importamos los paquetes necesarios, OpenCV y numpy.

Ahora procedemos a crear una función en la cual aplicaremos template matching dadas dos imágenes, una de entrada y el template. Esta función nos devolverá un punto por cada buen emparejamiento, es decir que vamos a descartar los puntos redundantes o muy cercanos, permitiéndonos de este modo contar cada uno de los objetos presentes en la imagen, en nuestro caso los corazones.

Línea 4: Declaramos la función points_template_matching, y como argumentos de esta necesitaremos una imagen de entrada I y la imagen template T.

NOTA: Para una mejor comprensión de los procedimientos relacionados al template matching o emparejamiento de plantillas por favor dirígete a este tutorial: Template matching | OpenCV con Python

Linea 5 y 6: Declaramos una lista vacía llamada points, esta nos servirá para almacenar los puntos finales que se devolverán de cada uno de los objetos encontrados. También he definido un umbral de 0.9 (pero podrías establecer otro valor), de tal modo que todos los valores de la matriz resultante (al aplicar cv2.matchTemplate) mayores o iguales a este valor serán considerados como candidatos del objeto encontrado.

Línea 8: Aplicamos cv2.matchTemplate, e indicamos la imagen I, el template T y por último el método cv2.TM_CCOEFF_NORMED que es el que usaremos en esta ocasión. Este último método al estar normalizado, quiere decir que los valores más altos que se encontrarán en la matriz resultante res serán cercanos a la unidad, por ello tomamos un umbral de 0.9, de este modo podremos filtrar los mejores emparejamientos.

Línea 9: De la matriz res, buscamos todos los valores mayores o iguales al umbral, esto nos llevará a obtener una matriz con elementos booleanos, de donde obtendremos True cuando los valores sean mayores o iguales al umbral, caso contrario False.

Con ayuda de np.where buscamos los índices de dichos resultados booleanos, de este modo obtendremos dos arrays de las filas y otro de las columnas de cada True.

Línea 10: Concatenamos los dos arrays obtenidos de la línea 9, de tal modo que las columnas queden primeras, seguidas de las filas. Esto hará que los puntos candidatos estén ordenados en coordenadas x e y.

Línea 12: Declaramos un contador i en 0.

Línea 13: Realizamos un ciclo de repetición while, mientras el número de elementos en candidates sea mayor a 0.

Líinea 14: En la primera iteración almacenaremos en points el primer punto de candidates.

Línea 15 y 16: En las iteraciones siguientes, declaramos una lista vacía llamada to_delete, que almacenará los índices de candidates que se desean borrar.

EL objetivo de este procedimiento es almacenar el primer buen emparejamiento y comparar este con el resto de puntos usando una resta. Si esta fuera menor a 10 en sus compoenente x e y, entonces dichos puntos se almacenarían en to_delete para ser borrados de candidates, y de este modo borrar los puntos redundates, mientras que irán quedando los que se haya obtenido un valor mayor a 10 pixeles de diferencia. ¡Veremos este proceso a continuación!.

Línea 17 y 18: Creamos un ciclo de repetición for, que se repetirá según el número de elementos de candidates. Dentro de este aplicaremos la diferencia entre el punto almacenado en points y cada uno de los elementos de candidates.

Línea 19 y 20: Si el valor absoluto de diff tanto en el índice 0 y 1 son menores a 10, entonces almacenaremos en to_delete los índices a ser borrados, esto debido a la cercanía que tienen entre sí, y por lo tanto son redundantes.

Línea 21: Con ayuda de np.delete borramos de candidates, los índices to_delete y especificamos axis=0 para que borre por filas.

Línea 22: Controlamos que el número de elementos en candidates no sea cero, ya que si ese fuera el caso usamos break para que salga del ciclo while, ya que no tendríamos elementos con los cuales trabajar.

Línea 23: Una vez que hemos borrado los puntos redundantes de candidates en la línea 21, procedemos a tomar el primer elemento restante de candidates y almacenarlo en points, para de este modo realizar el mismo procedimiento que vimos anteriormente y seguir borrando los puntos demasiado cercanos.

Línea 24: Incrementamos en 1 al contador i.

Línea 25: Retornamos los puntos obtenidos luego el procedimiento, es decir points.

Leyendo las imágenes de entrada y aplicando la función anterior

Figura 5: A la izquierda tenemos las imágenes plantillas que nos servirían para emparejarlas con la imagen de la carta 3 de corazones. El primer corazón permitiría encontrar los dos primeros corazones, mientras que el corazón invertido permitiría encontrar el tercer corazón de la carta.

Como podemos apreciar en la figura 5, para detectar los dos primeros corazones de la carta 3, necesitamos de la imagen plantilla que está ubicada en la parte superior izquierda. Mientras que para detectar el tercer corazón invertido de la carta, tendríamos que voltear la imagen template para que podamos obtener un buen valor de emparejamiento.

Entonces, una vez que hemos realizado la función para eliminar los puntos redundantes, procederemos a leer la imagen de entrada junto con los dos templates, y aplicaremos la función points_template_matching. De este modo podremos encontrar los 3 corazones presentes en la imagen de la carta.

image = cv2.imread("3_corazones.jpg")
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

template1 = cv2.imread("template.jpg", 0)
points1 = points_template_matching(image_gray, template1)
print("points1 = ",points1)

template2 = cv2.flip(template1, -1)
points2 = points_template_matching(image_gray, template2)
print("points2 = ",points2)

Línea 28 y 29: Leemos la imagen de entrada, en este programa estoy leyendo la imagen de corazones número 3, a continuación la pasamos de bgr a escala de grises.

Línea 31 a 33: Leemos la imagen del template que es la del corazón de la figura 2 directamente en escala de grises. Y en la línea 32 procedemos a aplicar la función points_template_matching, y le damos como argumentos image_graytemplate1. De ella obtendremos los puntos correspondientes a los dos primeros corazones, los cuales podemos imprimir.

points1 =  [array([82, 10], dtype=int64), array([ 81, 121], dtype=int64)]

Línea 35 a 37: Para obtener el corazón invertido tomaremos la imagen del template1 y la voltearemos. Seguido de ello podremos aplicar la función points_template_matching y adicinalmente podremos imprimir los o el punto encontrado.

points2 =  [array([ 81, 235], dtype=int64)]

Creando un nuevo array para almacenar los puntos encontrados

Hasta aquí hemos encontrado 2 puntos en points1 y un punto en points2. El siguiente paso sería concatenar ambos para obtener un solo array, pero hay que tomar en cuenta que puede que uno de estos dos array esté vacío o incluso ambos, es por esta razón que consideraremos estos casos a continuación:

if len(points1) > 0 and len(points2) > 0:
    points = np.concatenate((points1, points2))
elif len(points1) == 0 and len(points2) == 0:
    points = []
elif len(points1) == 0 and len(points2) > 0:
    points = points2
elif len(points1) > 0 and len(points2) == 0:
    points = points1

Línea 39 y 40: En caso de que existan elementos tanto en points1 como en points2, se procederá a concatenar estos dos y almacenar este resultado en una nueva variable llamada points

Línea 41 y 42: Si no se tiene elementos ni en points1, ni en points2, entonces points será una lista vacía.

Línea 43 y 44: Si solo existiesen elementos en points2, entonces igualaremos points2 a points.

Línea 45 y 46: Si solo existiesen elementos en points1, entonces igualaremos points1 a points.

Encerrando cada objeto encontrado

Una vez que tenemos nuestros puntos listos en points, vamos a recorrer cada uno de ellos con un for y dibujaremos un rectángulo verde que rodee a cada corazón.

for point in points:
    x1, y1 = point[0], point[1]
    x2, y2 = point[0] + template1.shape[1], point[1] + template1.shape[0]
    cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)

Línea 48: Recorremos cada uno de los puntos almacenados en points.

A continuación vamos a obtener cada una de las coordenadas necesarias para dibujar un rectángulo por cada objeto.

Línea 49: Ya que previamente habíamos especificado que points estaría en coordenadas x e y, lo único que tendríamos que hacer es igualarlo a nuevas variables (aunque podríamos no hacerlo y usar la variable points). 

Línea 50: Para obtener los puntos finales que dibujarán el rectángulo tendremos que sumar los puntos iniciales más el número de columnas y filas respectivamente de la imagen del tempalte.

Línea 51: Dibujamos el rectángulo con las coordenadas dadas en la línea 49 y 50.

Visualizar el número total de corazones

Para terminar, vamos a visualizar la totalidad de objetos encontrados en la imagen y además visualizaremos los resultados al aplicar este programa.

cv2.putText(image, str(len(points)), (95, 35), 1, 3, (0, 255, 0), 2)
cv2.imshow("Image", image)
cv2.imshow("Template1", template1)
cv2.imshow("Template2", template2)
cv2.waitKey(0)
cv2.destroyAllWindows()

Línea 53: Vamos a ubicar la cantidad de elementos que tiene points, que corresponderían al número de objetos encontrados.

Línea 54 a 58: Visualizamos la imagen de entrada y los dos templates usados. Esta visualización se dentendrá una vez que se haya presionado cualquier tecla y se cerrarán las ventanas.

Resultados del programa

Bien, ahora estamos listos para probar nuestro programa, vamos a ir cambiando la imagen a leer en la línea 28, de tal modo que probemos los rsultados con todas las cartas de corazones desde la A hasta el 10.

Figura 6: Resultados del programa realizado para contar el número de corazones por cada imagen de cartas de corazones, usando template matching.

En la figura número 6 tenemos los resultados de cada una de las imágenes usadas correspondientes a las cartas de corazones. En cada una de ellas vemos que se han contado correctamente los corazones, sin embargo hay un caso especial en la carta 10 de corazones.

Como vemos en la última carta, nuestro programa ha contado 11 corazones si embargo se trata de una carta de 10 corazones. Pero si prestamos más atención y contamos todos los corazones de dicha carta, tenemos que en realidad hay 11 corazones, es decir que quien diseñó la carta por equivocación añadió un corazón demás.

Por lo tanto nuestro programa está contando correctamente todos los corazones de cada imagen. ¡Lo hemos conseguido!.

Y hemos llegado al final de este post, espero que te haya gustado. Nos vemos en un siguiente tutorial. ?