?? CONTORNOS y como DIBUJARLOS en OpenCV y Python
CONTENIDO:
- ¿Qué es un contorno?
- Contornos en OpenCV
- cv2.findContours
- Argumentos de entrada
- ¿Qué obtenemos de la función?
- Dibujar contornos con cv2.drawContours
- cv2.findContours
- 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?
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):
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)
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:
- 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.
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:
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.
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:
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:
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:
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:
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:
- https://docs.opencv.org/master/d4/d73/tutorial_py_contours_begin.html
- https://docs.opencv.org/3.4/d3/dc0/group__imgproc__shape.html#ga17ed9f5d79ae97bd4c7cf18403e1689a
- https://docs.opencv.org/3.4/d6/d6e/group__imgproc__draw.html#ga746c0625f1781f1ffc9056259103edbc
Mu buena explicación, muchas gracias 🙂
Muchas gracias David 😀
Excelente trabajo Gabriela, tu forma de explicar es clara, amena y muy completa, felicitaciones!
Hola Juan Gabriel, te lo agradezco mucho 🙂
hola , a la hora de hacer los contornos me los cuenta mal , en vez de 11 sale 25 , en mi imagen tengo 11 monedas, y me contabliza 25 , puedes ayudarme porfavor
Hola Jair, puede que se estén encontrando varios contornos. Puedes ayudarte de cv2.contourArea, para descartar algunos.