?‍? CONTORNOS y como DIBUJARLOS en OpenCV y Python

Por Administrador

CONTENIDO:

  • ¿Qué es un contorno?
  • Contornos en OpenCV
    • cv2.findContours
      • Argumentos de entrada
      • ¿Qué obtenemos de la función?
    • Dibujar contornos con cv2.drawContours
  • Ahora sí, vamos con la programación
    • Comparando cv2.RETR_EXTERNAL y cv2.CHAIN_APPROX_SIMPLE
    • Analicemos los modos de recuperación de contornos

En el post de hoy vamos a tocar el tema de los contornos, aquellos puntos que muchas veces nos hemos encontrado en aplicaciones de visión por computador que rodean a ciertos objetos dentro de una imagen.

¿Qué es un contorno?

Figura 1: Contorno verde que rodea una pelota

Los contornos son aquellos puntos que rodean un objeto de interés dentro de una imagen, la documentación de OpenCV los describe como: «Los contornos se pueden explicar simplemente como una curva que une todos los puntos continuos (a lo largo del límite), que tienen el mismo color o intensidad. Los contornos son una herramienta útil para el análisis de formas y la detección y reconocimiento de objetos». Un ejemplo está precisamente en la figura 1, allí el contorno se muestra de color verde y está rodeando a una pelota. 

Contornos en OpenCV

Para poder encontrar y dibujar contornos de algún objeto o región de interés, OpenCV emplea la función cv2.findContours, que es de la que vamos a explicar hoy. Mientras que para dibujar contornos se usa cv2.drawContours.

Hay algo que debemos tener muy en cuenta si queremos encontrar contornos, y es que la imagen de entrada para emplear cv2.findContours debe ser binaria, es decir que únicamente puede presentar blanco y negro. Esto es importante ya que la función rodeará las áreas de color blanco que muestre la imagen de entrada.

cv2.findContours

En esta ocasión, vamos a revisar los siguientes argumentos de la función (recuerda que si quieres más información debes visitar la documentación de OpenCV): 

Figura 2: Función cv2.findContours y la estructura que veremos en este post

Argumentos de entrada la función cv2.findContours

Se va a explicar a continuación los argumentos de entrada: image, mode y method.

Image (Imagen binaria)

Figura 3: Izq. Imagen previo a la binarización. Der. Imagen binaria.

La imagen de entrada debe ser binaria, es decir a blanco y negro. Podemos obtener esta por ejemplo a través de detección de colores cuando usábamos rangos para determinar cierto color, umbralización o detección de bordes.

Mode (Modo de recuperación del contorno)

Vamos a ver los modos: cv2.RETR_EXTERNAL, cv2.RETR_LIST, cv2.RETR_CCOMP y cv2.RETR_TREE. Pero antes de explicar brevemente la diferencia entre cada uno de ellos, debemos entender lo que es la jerarquía de contornos.

La jerarquía de contornos demuestra la relación que tienen los contornos. Podemos tener el caso en que tengamos un contorno y dentro de este se encuentre otro, y dentro otro. Entonces por ejemplo un contorno externo es padre, mientras que el interno es hijo.

Teniendo esto en cuenta veamos cada uno de los modos de recuperación de contornos:

  • cv2.RETR_EXTERNAL: Recupera los contornos extremos. Solo los mayores de la familia, no el resto.
  • cv2.RETR_LIST: Recupera todos los contornos sin establecer jerarquía (ninguna relación padre hijo).
  • cv2.RETR_CCOMP: Organiza los contornos en jerarquía de dos niveles. Es decir que si tenemos un contorno externo tendrá jerarquía 1, y uno interno jerarquía 2. Si este a su vez tiene otro contorno tendrá jerarquía 1.
  • cv2.RETR_TREE: Recupera todos los contornos con sus jerarquías.

NOTA: Estos modos afectaran directamente al argumento de salida ‘hierarchy’, ya lo veremos en el siguiente post, o puedes ver mi video de ?‍? JERARQUÍA de CONTORNOS – OpenCV y Python.

Method (Método de aproximación del contorno)

En cuanto a los métodos de aproximación del contorno veremos: cv2.CHAIN_APPROX_NONE y cv2.CHAIN_APPROX_SIMPLE. En ambos casos se procederá a almacenar los puntos x e y, correspondientes a cada contorno encontrado, la diferencia está en la cantidad de puntos que se almacenan. Veamos un ejemplo con el contorno de un rectángulo:

Figura 4: Con cv2.CHAIN_APPROX_NONE se almacenan todos los puntos que rodean al rectángulo, mientras que con cv2.CHAIN_APPROX_SIMPLE se almacenan únicamente 4.

  • cv2.CHAIN_APPROX_NONE: Almacena todos los puntos del contorno.
  • cv2.CHAIN_APPROX_SIMPLE: Comprime segmentos horizontales, verticales y diagonales y deja solo sus puntos finales, ahorrando memoria al no almacenar puntos redundantes.

¿Qué obtenemos de la función?

De esta función se obtienen 3 argumentos cuando estamos usando OpenCV 3, mientras que si estamos usando OpenCV 4, se obtendrán 2, ya que se omite «image».

Image

No entraremos en la explicación de este argumento, ya que para versiones recientes de OpenCV, como lo es la versión 4, ya no es utilizada.

Contours (Contornos encontrados)

Contornos encontrados, y cada contorno es almacenado como un vector de puntos.

Hierarchy (Jerarquía de contornos)

Como habíamos dicho antes, demuestra la relación que tienen los contornos. Podemos tener el caso en que tengamos un contorno y dentro de este se encuentre otro, y dentro otro. Entonces por ejemplo un contorno externo es padre, mientras que el interno es hijo.

Dibujar contornos con cv2.drawContours

Para dibujar un contorno en específico o todos los contornos encontrados por cv2.findContours, se utiliza cv2.drawContours.

Figura 5: Función cv2.drawContours empleada para dibujar contornos en una imagen.

Ahora sí, vamos por la programación

Ahora que hemos tratado la teoría acerca de cv2.findContours, vamos por lo que venimos, ¡la programación!, para ello usaremos la siguiente imagen:

Figura 6: Imagen de la que extraeremos los contornos.

En un principio vamos a comparar los  métodos de aproximación del contorno cv2.RETR_EXTERNAL y cv2.CHAIN_APPROX_SIMPLE. Luego pasaremos con los modos de recuperación del contorno cv2.RETR_EXTERNAL, cv2.RETR_LIST, cv2.RETR_CCOMP y cv2.RETR_TREE.

Comparando cv2.RETR_EXTERNAL y cv2.CHAIN_APPROX_SIMPLE

Vamos directamente con la programación:

import cv2
import numpy as np

imagen = cv2.imread('figContorno.png')
gray = cv2.cvtColor(imagen,cv2.COLOR_BGR2GRAY)
_,th = cv2.threshold(gray,100,255,cv2.THRESH_BINARY)

#Para versiones OpenCV3:
img1,contornos1,hierarchy1 = cv2.findContours(th, cv2.RETR_EXTERNAL,
      cv2.CHAIN_APPROX_NONE)
img2,contornos2,hierarchy2 = cv2.findContours(th, cv2.RETR_EXTERNAL,
      cv2.CHAIN_APPROX_SIMPLE)
#Para versiones OpenCV4:
#contornos1,hierarchy1 = cv2.findContours(th, cv2.RETR_EXTERNAL,
#			cv2.CHAIN_APPROX_NONE)
#contornos2,hierarchy2 = cv2.findContours(th, cv2.RETR_EXTERNAL,
#			cv2.CHAIN_APPROX_SIMPLE)

cv2.drawContours(imagen, contornos1, -1, (0,255,0), 3)
print ('len(contornos1[2])=',len(contornos1[2]))
print ('len(contornos2[2])=',len(contornos2[2]))
cv2.imshow('imagen',imagen)
cv2.imshow('th',th)
cv2.waitKey(0)
cv2.destroyAllWindows()

En las líneas 1 y 2  importamos OpenCV y numpy, en la línea 4 leemos la imagen correspondiente a la figura 6, luego en la línea 5 la transformamos a escala de grises, para en la línea 6 aplicar umbralización simple y así obtener una imagen binarizada.

ATENCIÓN: Para versiones de OpenCV 3, vamos a usar las líneas 9 a 12. Si posees OpenCV 4 comenta estas líneas y descomenta las líneas 14 a 17.

En la línea 9 estamos usando la función cv2.findContours, en ella hemos especificado la imagen binaria, cv2.RETR_EXTERNAL, y cv2.CHAIN_APPROX_NONE, mientras que en la línea 11 tenemos la misma línea pero con cv2.CHAIN_APPROX_SIMPLE. Luego en la línea 19 dibujamos todos los contornos con cv2.drawContours. En ella especificamos la imagen en donde se van a visualizar los contornos en este caso image, los contornos corresponden a contornos1 (podríamos también especificar contornos2, pero obtendríamos la misma visualización), se dibujarán todos los contornos con -1, de color verde (0,255,0) en BGR y finalmente el grosor de línea.

En la línea 20 y 21 imprimimos la cantidad de elementos almacenados en el contorno en la posición 2, tanto cuando usamos cv2.CHAIN_APPROX_NONE y cv2.CHAIN_APPROX_SIMPLE. Finalmente visualizamos la imagen binarizada y la imagen en donde se visualizaran los contornos.

Figura 7: Izq. Imagen binaria, Der. Imagen en donde están dibujados los contornos externos de color verde.

En el terminal obtenemos:

len(contornos1[2])= 1092
len(contornos2[2])= 4

En  la figura 7 podemos ver a la izquieda la imagen binaria necesaria para emplear cv2.findContours. Mientras que a la derecha los contornos creados que están rodeando al círculo y a los dos rectángulos que corresponden a los contornos externos. 

En el terminal en cambio vemos que para contornos1  el cual corresponde al uso de cv2.CHAIN_APPROX_NONE han sido alamcenados 1092 puntos, mientras que al usar cv2.CHAIN_APPROX_SIMPLE en contornos2, se han almacenado 4 puntos para ese contorno. Dados estos resultados podemos apreciar que con cv2.CHAIN_APPROX_SIMPLE no está almacenando puntos redundantes, por lo que es mejor emplearlo.

Analicemos los modos de recuperación de contornos

Vamos a visualizar los contornos obtenidos luego de aplicar cv2.findContours a una imagen binaria y analizaremos brevemente cada uno de los modos de recuperación del contorno: cv2.RETR_EXTERNAL, cv2.RETR_LIST, cv2.RETR_CCOMP y cv2.RETR_TREE. Mientras que en el siguiente post veremos como afecta cada uno de ellos en la jerarquía de contornos.

Aplicando cv2.RETR_EXTERNAL

import cv2
import numpy as np

imagen = cv2.imread('figContorno.png')
gray = cv2.cvtColor(imagen,cv2.COLOR_BGR2GRAY)
_,th = cv2.threshold(gray,100,255,cv2.THRESH_BINARY)

#Para versiones OpenCV3:
img,contornos,hierarchy = cv2.findContours(th, cv2.RETR_EXTERNAL,
      cv2.CHAIN_APPROX_SIMPLE)

#Para versiones OpenCV4:
#contornos,hierarchy = cv2.findContours(th, cv2.RETR_EXTERNAL,
#			cv2.CHAIN_APPROX_SIMPLE)

print ('hierarchy=',hierarchy)

for i in range (len(contornos)):
  cv2.drawContours(imagen, contornos, i, (0,255,0), 3)
  cv2.imshow('imagen',imagen)
  cv2.waitKey(0)

cv2.imshow('imagen',imagen)
cv2.waitKey(0)
cv2.destroyAllWindows()

Pues bien, aquí hemos hecho un programa similar al anterior, en este caso luego de aplicar la función para encontrar contornos (dependiendo si posees OpenCV 3 u OpenCV 4), imprimimos la variable hierarchy, presente en la línea 16.

De la línea 18 a la 21 usamos un for, para que se dibujen los contornos conforme vayas presionando una tecla. ¡Vamos pruébalo!

Como resultado obtendremos:

Figura 8: Izq. Se dibujan los contornos (usando cv2.RETR_EXTERNAL) de color verde. Der. En el terminal tenemos la información de la variable hierarchy.

En la figura 8 a la izquierda se observa que se han dibujado solo los contornos externos, mientras que a la derecha podemos observar que la variable hierarchy presenta tres listas correspondientes a los tres contornos encontrados.

Aplicando cv2.RETR_LIST

Aquí usaremos cv2.RETR_LIST, para ello debemos reemplazar las líneas 8 a 14 (dependiendo  de tu versión de OpenCV) por:

#Para versiones OpenCV3:
img,contornos,hierarchy = cv2.findContours(th, cv2.RETR_LIST,
      cv2.CHAIN_APPROX_SIMPLE)

#Para versiones OpenCV4:
#contornos,hierarchy = cv2.findContours(th, cv2.RETR_LIST,
#			cv2.CHAIN_APPROX_SIMPLE)

El resultado sería:

Figura 9: Izq. Se dibujan los contornos (usando cv2.RETR_LIST) de color verde. Der. En el terminal tenemos la información de la variable hierarchy.

Entonces en la figura 9 a la izquierda podemos ver que se han dibujado 10 contornos, mientras que a la derecha tenemos la información de la variable hierarchy, con una lista de 10 listas que corresponden a la relación de cada contorno.

Aplicando cv2.RETR_CCOMP

Aquí usaremos cv2.RETR_CCOMP, para ello debemos reemplazar las líneas 8 a 14 (dependiendo  de tu versión de OpenCV) por:

#Para versiones OpenCV3:
img,contornos,hierarchy = cv2.findContours(th, cv2.RETR_CCOMP,
      cv2.CHAIN_APPROX_SIMPLE)

#Para versiones OpenCV4:
#contornos,hierarchy = cv2.findContours(th, cv2.RETR_CCOMP,
#			cv2.CHAIN_APPROX_SIMPLE)

Teniendo como resultado:

Figura 10: Izq. Se dibujan los contornos (usando cv2.RETR_CCOMP) de color verde. Der. En el terminal tenemos la información de la variable hierarchy.

Al igual que cv2.RETR_LIST, obtenemos 10 contornos, la diferencia está en la variable hierarchy, si lo notaste, existen elementos que difieren con los del anterior modo.

Aplicando cv2.RETR_TREE

Aquí usaremos cv2.RETR_TREE, para ello debemos reemplazar las líneas 8 a 14 (dependiendo  de tu versión de OpenCV) por:

#Para versiones OpenCV3:
img,contornos,hierarchy = cv2.findContours(th, cv2.RETR_TREE,
      cv2.CHAIN_APPROX_SIMPLE)

#Para versiones OpenCV4:
#contornos,hierarchy = cv2.findContours(th, cv2.RETR_TREE,
#			cv2.CHAIN_APPROX_SIMPLE)

Veamos:

Figura 11: Izq. Se dibujan los contornos (usando cv2.RETR_TREE) de color verde. Der. En el terminal tenemos la información de la variable hierarchy.

Como podrás darte cuenta, aquí también se han dibujado 10 contornos, y de nuevo la diferencia entre con los modos anteriores está en la variable hierachy.

Recuerda que en el próximo post veremos como es que funciona la jerarquía de contornos, y porque obtenemos distintos valores cuando usamos los 4 modos de recuperación de contornos que hemos visto hoy. Te dejo con las imágenes resumen. ¡Te espero en el siguiente post!

Referencias:

#infoOmes