2

Buscar el centro de un Contorno con OpenCV Python

¡Buenas, queridos humanos! Una pregunta que me habéis hecho muchas veces es como encontrar el centro de un objeto o contorno en una imagen. La verdad es que es muy fácil y puede conseguirse con pocas líneas de código.

Para el tutorial de hoy he dibujado esta imagen que será perfecta para practicar. Intentaremos encontrar los centros y los contornos de las bolas que hay en esta supuesta mesa de billar:

 

Escribiremos un script con Python y OpenCV que nos dé este resultado:

 

¿Qué pasos vamos a seguir?

Nuestro programa seguirá estos cinco pasos:

  1. Extraer las bolas: aplicaremos detección de color verde para extraer el fondo, y luego invertiremos la máscara para quedarnos con las bolas.
  2. Filtrar el ruido: aunque la imagen es artificial y ya está muy limpia, haremos un Open+Close para eliminar el ruido y la línea blanca.
  3. Detección de contornos: con la función cv2.findContours() encontraremos los contornos de las bolas.
  4. Buscar los centros: usaremos los momentos de cada contorno para calcular el centroide de cada bola. También escribiremos su posición (x,y).
  5. Mostrar la imagen

¡Y no nos olvidemos de poner música! Hoy trabajaremos con algo relajante de fondo.


Código paso a paso

Vamos a construir el programa paso a paso. Al final de la página está el código completo.

En primer lugar importamos las librerías (hoy sólo necesitamos cv2 y numpy) y una fuente estándar que usaremos para escribir las coordenadas de los centros.

import numpy as np
import cv2

#Cargamos una fuente de texto
font = cv2.FONT_HERSHEY_SIMPLEX

 

Después, cargamos la imagen con la función cv2.imread() y comprobamos que se haya leído correctamente. ¡No os olvidéis de descargar la imagen que hay al principio del tutorial y guardarla en el mismo directorio que el script de Python!

#Abrimos la imagen y la convertimos a hsv
imagen = cv2.imread('billar.png')
if(imagen is None):
	print("Error: no se ha podido encontrar la imagen")
	quit()
hsv = cv2.cvtColor(imagen, cv2.COLOR_BGR2HSV)

 

Queremos conseguir una máscara con el tapete verde del fondo. Convertimos la imagen al espacio de color HSV, definimos los rangos máximos y minimos y aplicamos detección de colores para extraer el tapete:

#Convertimos la imagen a HSV
hsv = cv2.cvtColor(imagen, cv2.COLOR_BGR2HSV)

#Extraemos el fondo verde
verde_bajos = np.array([50,1,1])
verde_altos = np.array([100, 254, 254])
fondo = cv2.inRange(hsv, verde_bajos, verde_altos)

Un apunte antes de continuar: ¿por qué estamos utilizando el espacio de color HSV en vez del típico RGB? El espacio HSV (Hue-Saturation-Value) es el más útil para hacer detección de color. La primera componente indica la Tonalidad (rojo, azul, verde, amarillo…), la segunda es la Saturación (más o menos gris) y la tercera es el Valor (intensidad/luminosidad).

¿Por qué el HSV es el espacio más útil? Imaginémonos por un momento que queremos detectar algún color extraño, como ocres. El ocre es un amarillo deslucido, y en el espacio HSV podríamos establecer el rango H: 35-50 para coger la tonalidad amarilla y S:150-200 y L: 150-200 para coger los tonos más apagados. Sin embargo, trabajando con RGB ya no está tan claro cuál debería ser el rango para detectar ocres. ¿Cuál de las tres componentes hay que subir para conseguir más saturación? ¿Y para abarcar todas las tonalidades de amarillo…?

 

De momento hemos extraído el tapete del fondo, pero lo que nos interesa son las bolas. La máscara que hemos obtenido al aplicar la función cv2.inRange() sobre la imagen HSV tiene este aspecto:

La parte blanca de la imagen son los píxeles cuyo valor está dentro del rango que hemos definido

Las máscaras son imágenes binarias cuyos píxeles sólo pueden tomar los valores 0 ó 1. Podemos aplicar la operación booleana not a la máscara para invertirla y quedarnos con las bolas.

 
#Invertimos la mascara para obtener las bolas 
bolas = cv2.bitwise_not(fondo) 

Esta es la máscara que se obtiene tras aplicar cv2.bitwise_not()

 

Siempre que se hace detección de colores hay que filtrar la máscara para eliminar el ruido. La imagen con la que estamos trabajando la he dibujado yo misma, por tanto está muy preparada y hemos obtenido una máscara limpia… excepto por esa línea blanca que nos molesta mucho.

Definiremos un kernel de 3×3 y aplicaremos un Open seguido de un Close para eliminar la línea.

#Eliminamos ruido
kernel = np.ones((3,3),np.uint8)
bolas = cv2.morphologyEx(bolas,cv2.MORPH_OPEN,kernel)
bolas = cv2.morphologyEx(bolas,cv2.MORPH_CLOSE,kernel)

La línea blanca ha desaparecido. ¡Hooray!

 

El siguiente paso es encontrar los contornos de las bolas. Para hacerlo llamamos la función cv2.findContours() que nos devolverá una lista con todos los contornos que encuentre en la máscara, y después los pintamos con cv2.drawContours().

#Buscamos los contornos de las bolas y los dibujamos en verde
contours,_ = cv2.findContours(bolas, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(imagen, contours, -1, (0,255,0), 2)

¿Qué significan los parámetros que hemos pasado a la función cv2.findContours()? El parámetro cv2.RETR_LIST indica que los contornos pueden calcularse sin guardar la información de la jerarquía (i.e. qué contornos están dentro de otros), pues aquí no nos sirve.

cv2.CHAIN_APPROX_SIMPLE es el método de aproximación de contorno. Este método intenta aproximar el contorno por el mínimo de puntos posible, eliminando aquellos que sean redundantes. Este método es especialmente útil cuando se detectan figuras poligonales porque sólo se guardan las esquinas, ahorrando mucha memoria.

 

Ahora viene la parte que verdaderamente nos interesa: ¡encontrar el centro de cada bola! Para ello usaremos los momentos de cada contorno. Los momentos son una medida particular que indica la dispersión de una nube de puntos, y matemáticamente se definen como:

donde x,y son las coordenadas de un píxel y la función I(x,y) indica su intensidad. Estamos trabajando sobre una máscara binaria, por tanto la función I(x,y) sólo puede valer 0 ó 1.

Bien, ¿cómo diablos puede ayudarnos esta fórmula extraña a saber el centro? Hay tres momentos que nos interesan: M00, M01 y M10. Fijémonos que si estamos calculando M00, la fórmula anterior se transforma en:

(recordad que a⁰ = 1, y aquí definimos 0⁰ = 1)
Como I(x,y) sólo puede dar 0 ó 1, la fórmula de M00 es equivalente al número de píxeles cuyo valor es 1. Por tanto M00 es el área en píxeles de la región blanca.

Para M10, la fórmula original se transforma en esta:

Esto es igual a la suma de las coordenadas x de los píxeles cuya intensidad sea 1. Por tanto, dividiendo M10 por M00 obtendríamos el centroide para la componente x:

Y lo mismo con M01:

Por tanto: para obtener los centroides calcularemos los momentos M00, M10 y M01 de cada contorno y haremos las divisiones anteriores. Después los pintaremos (dibujando un círculo pequeño en su posición) y escribiremos las cordenadas sobre la imagen:

#Buscamos el centro de las bolas y lo pintamos en rojo
for i in contours:
	#Calcular el centro a partir de los momentos
	momentos = cv2.moments(i)
	cx = int(momentos['m10']/momentos['m00'])
	cy = int(momentos['m01']/momentos['m00'])

	#Dibujar el centro
	cv2.circle(imagen,(cx, cy), 3, (0,0,255), -1)

	#Escribimos las coordenadas del centro
	cv2.putText(imagen,"(x: " + str(cx) + ", y: " + str(cy) + ")",(cx+10,cy+10), font, 0.5,(255,255,255),1)

 

Por último mostramos la imagen y esperamos a que el usuario pulse ESC para salir:

#Mostramos la imagen final
cv2.imshow('Final', imagen)
 
#Salir con ESC
while(1):
    tecla = cv2.waitKey(5) & 0xFF
    if tecla == 27:
        break

#Destruir la ventana y salir
cv2.destroyAllWindows()
quit()

¡Hecho! Si ahora ejecutamos este script veremos la ventana con la imagen del billar y las bolas marcadas:


Código completo

El código final quedará así:

"""
	TUTORIAL: buscar centros de objetos con OpenCV+Python

	Escrito por Glare
	www.robologs.net

"""

import numpy as np
import cv2

#Cargamos una fuente de texto
font = cv2.FONT_HERSHEY_SIMPLEX

#Abrimos la imagen
imagen = cv2.imread('billar.png')
if(imagen is None):
	print("Error: no se ha podido encontrar la imagen")
	quit()

#Convertimos la imagen a HSV
hsv = cv2.cvtColor(imagen, cv2.COLOR_BGR2HSV)

#Extraemos el fondo verde
verde_bajos = np.array([50,1,1])
verde_altos = np.array([100, 254, 254])
fondo = cv2.inRange(hsv, verde_bajos, verde_altos)

#Invertimos la mascara para obtener las bolas
bolas = cv2.bitwise_not(fondo)


#Eliminamos ruido
kernel = np.ones((3,3),np.uint8)
bolas = cv2.morphologyEx(bolas,cv2.MORPH_OPEN,kernel)
bolas = cv2.morphologyEx(bolas,cv2.MORPH_CLOSE,kernel)


#Buscamos los contornos de las bolas y los dibujamos en verde
contours,_ = cv2.findContours(bolas, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(imagen, contours, -1, (0,255,0), 2)

#Buscamos el centro de las bolas y lo pintamos en rojo
for i in contours:
	#Calcular el centro a partir de los momentos
	momentos = cv2.moments(i)
	cx = int(momentos['m10']/momentos['m00'])
	cy = int(momentos['m01']/momentos['m00'])

	#Dibujar el centro
	cv2.circle(imagen,(cx, cy), 3, (0,0,255), -1)

	#Escribimos las coordenadas del centro
	cv2.putText(imagen,"(x: " + str(cx) + ", y: " + str(cy) + ")",(cx+10,cy+10), font, 0.5,(255,255,255),1)

#Mostramos la imagen final
cv2.imshow('Final', imagen)
 
#Salir con ESC
while(1):
    tecla = cv2.waitKey(5) & 0xFF
    if tecla == 27:
        break

#Destruir la ventana y salir
cv2.destroyAllWindows()
quit()

Conclusión

Hoy habéis aprendido cómo calcular los centroides de un Contorno mediante momentos, y dibujarlos sobre una imagen estática. Esto tiene mucha utilidad para saber la posición de un objeto sobre una fotografía. Además, si se está trabajando sobre un vídeo es posible calcular la trayectoria de un cuerpo usando algún método como el de Lucas Kanade.

Si tenéis alguna duda o hay alguna incorrección en el código, escribidme un comentario y os responderé lo más rápido que pueda. Y si os gustan mis tutoriales siempre podéis dejar un like a nuestra página de Facebook o Twitter para estar al día de nuestras publicaciones.

¡Hasta la próxima! ^-^

Gl4r3

Brillante, luminosa y cegadora a veces, Glare es tan artista como técnica. Le encanta dar rienda suelta a sus módulos de imaginación y desdibujar los ya de por si delgados límites que separan el mundo de la electrónica y el arte. Su mayor creación hasta la fecha es un instrumento capaz de convertir los colores y la luz en música. Cuándo sus circuitos no están trabajando en una nueva obra electrónica, le gusta dedicar sus ciclos a la lectura o a documentar sus invenciones para beneficio de los humanos. Sus artilugios favoritos son aquellos que combinan una funcionalidad práctica con un diseño elegante y artístico.

2
Deja un comentario

avatar
2 Hilos iniciados
0 Respuestas a hilos
0 Followers
 
Most reacted comment
Hottest comment thread
2 Nº autores comentarios
Christian SarriaOliver J. Autores de comentarios recientes
más nuevos primero más antiguos primero
Christian Sarria
Humano
Christian Sarria

Si tengo usar el código con una cámara, qué puedo a hacr

Oliver J.
Humano
Oliver J.

Muchas gracias Gl4r3 por este artículo!