Detectando FIGURAS GEOMÉTRICAS (??⬛) con OpenCV – Python
Te doy la bienvenida a un nuevo post, esta ves para tratar el tema de detección de figuras geométricas simples. Así que si este es un tema que te interesa y deseas realizar alguna aplicación, te invito a que leas este post o que veas el video que he preparado para ti. Recuerda que puedes encontrar este código en mi respositorio de GitHub y al final de este post ¡Empecemos!
El proceso a seguir será el siguiente:
- Leer la imagen de entrada y transformarla a escala de grises
- Obtener una imagen binarizada
- Encontrar los contornos
- cv2.approxPolyDP
- Determinando las figuras geométricas
- Triángulo
- Cuadrado y rectángulo
- Aspect Ratio
- Pentágono
- Hexágono
- Círculo
- Determinando las figuras geométricas
Leer la imagen de entrada y transformarla a escala de grises
Para empezar, vamos a leer la imagen que vamos a utilizar, más adelante probaremos una imagen con más figuras. En esta imagen aparecen: un círculo, un triángulo, un cuadrado, un pentágono, un hexágono y un rectángulo.
import cv2 image = cv2.imread('figurasColores.png')
Si visualizamos la imagen leída, veremos lo siguiente:

Figura 1: Imagen de entrada.
Ahora vamos a transformarla a escala de grises, para ello usamos la siguiente línea:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
Obtener una imagen binarizada
Como necesitamos encontrar cada uno de los contornos correspondientes a cada figura geométrica, debemos obtener una imagen binarizada, para ello puedes hacerlo a través de la detección de bordes como con cv2.Canny, o a través de umbralización simple con cv2.threshold, por ejemplo. En esta ocasión vamos a emplear canny. Luego de ello será necesario mejorar la imagen binara por ello usaremos dilatación y erosión.
canny = cv2.Canny(gray, 10, 150) canny = cv2.dilate(canny, None, iterations=1) canny = cv2.erode(canny, None, iterations=1) #_, th = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)
Línea 5: Aplicamos detección de bordes con cv2.Canny
.
Línea 6 y 7: Aplicamos dilatación y erosión para mejorar la imagen binaria obtenida.
Línea 8: Como te había comentado, en vez de detección de bordes hubiera podido utilizar umbralización simple. Si deseas probarlo puedes descomentar esta línea y comentar las correspondientes a la detección de bordes.
Veamos la imagen almacenada en canny
:

Figura 2: Imagen al ser aplicada la detección de bordes, dilatación y erosión.
Encontrar los contornos
El siguiente paso sería encontrar todos los contornos correspondientes a la imagen binaria, para ello usamos la función cv2.findContours.
#_,cnts,_ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# OpenCV 3 cnts,_ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# OpenCV 4
Puedes usar cualquiera de estas líneas de código dependiendo de la versión de OpenCV que tengas instalado.
Dentro de cv2.findContours
, en sus argumentos especificaremos la imagen binaria canny
en este caso, seguido de cv2.RETR_EXTERNAL y cv2.CHAIN_APPROX_SIMPLE, para poder encontrar los contornos externos.
Si deseas puedes dibujar todos los contornos encontrados con la siguiente línea:
#cv2.drawContours(image, cnts, -1, (0,255,0), 2)
Ahora para poder analizar cada uno de los contornos encontrados, y poder determinar de qué figura geométrica se trata usaremos un for.
for c in cnts:
Dentro de este emplearemos la función cv2.approxPolyDP, que nos ayudará a identificar cada una de las figuras de la imagen.
cv2.approxPolyDP
Según OpenCV, la función cv2.approxPolyDP aproxima curvas o una curva poligonal con una precisión especificada. En otro documento nos dice que aproxima una forma de contorno en otra con menos número de vértices, dependiendo de la precisión que le especifiquemos.
Lo que se me viene a la mente después de esta explicación que nos ofrece OpenCV, es por ejemplo el círculo, al que si aplicáramos esta función se representaría como un polígono con muchísimos lados y vértices. Algo más o menos así:

Figura 3: Representación de un círculo como un polígono. (Fuente)
Bien, pues veamos como se utiliza esta función para emplearla. En este caso se necesita de los siguientes tres argumentos :
Curve: Corresponde al contorno que estemos analizando en ese momento.
Epsilon: Es el parámetro que especifica la precisión de aproximación. Para obtener este parámetro necesitaremos de la ayuda de la función cv2.arcLength que a su vez calculará el perímetro del contorno o la longitud de curva (dado el caso).
Como argumentos de cv2.arcLength, estarán el contorno que estemos analizando seguido de true o false que corresponde al parámetro closed para indicar que la curva es cerrada o no. Finalmente se tendrá que multiplicar por cierto porcentaje (que hay que tener muy en cuenta) para obtener epsilon.
Closed: Es true cuando la curva aproximada es cerrada, es decir que tanto el primer como el último vértice están conectados. False, lo contrario.
Ahora retomemos el código, dentro del for que habíamos determinado, vamos a digirar lo siguiente:
epsilon = 0.01*cv2.arcLength(c,True) approx = cv2.approxPolyDP(c,epsilon,True) #print(len(approx)) x,y,w,h = cv2.boundingRect(approx)
Línea 14: Lo primero que haremos es determinar epsilon
(que será uno de los argumentos empleados por cv2.approxPolyDP
), para ello pondremos un porcentaje de 1% mutiplicado por la función cv2.arcLength
. Debo aclarar que el porcentaje que aquí he puesto ha sido determinado por prueba y error, así que al final del proceso este fue un valor que me daba buenos resultados. Aclaro esto ya que para tu aplicación debes experimentar con distintos porcentajes hasta encontrar el que mejor te funcione.
Línea 15: Empleamos la función cv2.approxPolyDP
, allí especificaremos el contorno, epsilon y true en el parámetro closed ya que todos los vértices iniciales y finales de las figuras están conectados.
Línea 16: Si descomentamos esta línea, vamos a obtener la impresión de longitud de approx
. Dicho de otro modo, imprimirá la cantidad de vertices de cada contorno que esté analizando y que nos servirá para diferenciar cada figura.
Línea 17: Usamos la función cv2.boundingRect
para obtener los puntos x, y el ancho y alto del contorno actual, estos datos nos serán de ayuda cuando estemos tratando de diferenciar entre cuadrado y rectángulo. Ya lo veremos más adelante.
Determinando las figuras geométricas
Bien, ahora vamos a usar la información dada por approx
. Por ejemplo si en la longitud de esta variable obtenemos 3 estaríamos en presencia de un triángulo, si obtenemos 4 estaríamos en presencia de un cuadrado o rectángulo (algo que debemos diferenciar), si tenemos 5 sería un pentágono y así sucesivamente. En el caso de círculo veremos que se representará como un polígono con muchos vértices.
Triángulo
if len(approx)==3: cv2.putText(image,'Triangulo', (x,y-5),1,1.5,(0,255,0),2)
Línea 19 y 20: Si la longitud de approx
(que representa el número de vértices) es 3, vamos a visualizar “Triángulo” en la parte superior izquierda del mismo con cv2.putText
.
NOTA: Pongo la palabra sin tilde ya que OpenCV no la visualiza. Si quieres más información sobre cv2.putText
por favor da clic aquí.
Cuadrado y Rectángulo
Para determinar cuando se trata de un cuadrado o rectángulo (dado que ambas figuras poseen 4 vértices), vamos a ayudarnos de la información obtenida por cv2.boundingRect
en la línea 17 y emplearemos su aspect ratio.
Aspect Ratio
El aspect ratio se obtiene de la relación del ancho y alto del objeto (Aquí te dejo la infomación que nos ofrece OpenCV). Veamos la siguiente imagen que nos ayudará con la explicación:

Figura 4: Determinando el aspect ratio de un cuadrado y dos rectángulos.
En la figura 4 vemos algunos ejemplos de los resultados obtenidos al aplicar aspect ratio a un cuadrado y dos rectángulos. Si el cuadrado tiene un ancho y alto de 50 por ejemplo, su aspect ratio sería 1. Seguido tenemos un rectángulo que tiene ancho de 40 mientras que 100 de alto, su aspect ratio sería 0.4. El otro rectángulo posee un ancho de 100 mientras una altura de 40, así que el aspect ratio resultante sería 2.5.
Entonces ya que el cuadrado tiene sus lados iguales y obtendremos 1 en el aspect ratio, podemos tomar esto para diferenciarlo del rectángulo, que por otro lado podría obtener valores menores o mayores a 1.
if len(approx)==4: aspect_ratio = float(w)/h print('aspect_ratio= ', aspect_ratio) if aspect_ratio == 1: cv2.putText(image,'Cuadrado', (x,y-5),1,1.5,(0,255,0),2) else: cv2.putText(image,'Rectangulo', (x,y-5),1,1.5,(0,255,0),2)
Línea 22: Comparamos si approx
es igual a 4, entonces tendremos que diferenciar entre un cuadrado y un rectángulo.
Línea 23: Determinamos el aspect ratio con ayuda del ancho y alto del contorno que habíamos obtenido en la línea 17.
Línea 24: Imprimimos el valor que obtenemos en aspect ratio, esto únicamente para darnos cuenta de lo que obtenemos cuando la figura es un cuadrado o rectángulo y para que veas la diferencia entre estos dos casos.
Línea 25 y 26: Si aspect_ratio
es igual a 1, entonces se tratará de un cuadrado así que vamos a visualizar “Cuadrado” en la parte superior izquierda del mismo con cv2.putText
.
Línea 27 y 28: Si el aspect_ratio
es diferente de 1, entonces se tratará de un rectángulo y usaremos cv2.putText
para poder visualizarlo.
NOTA: Aquí he comparado con 1, pero podrías ponerle en cierto rango, como por ejemplo entre 0.95 y 1.05, como lo hacen en el blog de pyimagesearch.
Pentágono
if len(approx)==5: cv2.putText(image,'Pentagono', (x,y-5),1,1.5,(0,255,0),2)
Línea 30 y 31: Si la longitud de approx
es 5, se visualiza “Pentágono” en la parte superior izquierda del mismo con cv2.putText
.
Hexágono
if len(approx)==6: cv2.putText(image,'Hexagono', (x,y-5),1,1.5,(0,255,0),2)
Línea 33 y 34: Si la longitud de approx
es 6 corresponderá a un hexágono así que seguiremos con el mismo procedimiento.
Círculo
Si imprimiste len(approx)
te pudiste haber dado cuenta que en el círculo obtuvimos una cantidad de 14 vértices. Entonces podemos especificar que si el contorno presenta más de 10 vértices, será considerado como un círculo. (Esta consideración puede cambiar de acuerdo a tu aplicación) .
if len(approx)>10: cv2.putText(image,'Circulo', (x,y-5),1,1.5,(0,255,0),2) cv2.drawContours(image, [approx], 0, (0,255,0),2) cv2.imshow('image',image) cv2.waitKey(0)
Línea 36 y 37: Si la longitud de approx
es mayor a 10 corresponderá a un círculo y continuamos con el mismo procedimiento.
Línea 39: Dibujamos cada contorno con cv2.drawContours
.
Línea 40 y 41: Visualizamos la imagen hasta que una tecla sea presionada.
Programa completo
import cv2 image = cv2.imread('figurasColores2.png') gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) canny = cv2.Canny(gray, 10, 150) canny = cv2.dilate(canny, None, iterations=1) canny = cv2.erode(canny, None, iterations=1) #_, th = cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) #_,cnts,_ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# OpenCV 3 cnts,_ = cv2.findContours(canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# OpenCV 4 #cv2.drawContours(image, cnts, -1, (0,255,0), 2) for c in cnts: epsilon = 0.01*cv2.arcLength(c,True) approx = cv2.approxPolyDP(c,epsilon,True) #print(len(approx)) x,y,w,h = cv2.boundingRect(approx) if len(approx)==3: cv2.putText(image,'Triangulo', (x,y-5),1,1.5,(0,255,0),2) if len(approx)==4: aspect_ratio = float(w)/h print('aspect_ratio= ', aspect_ratio) if aspect_ratio == 1: cv2.putText(image,'Cuadrado', (x,y-5),1,1.5,(0,255,0),2) else: cv2.putText(image,'Rectangulo', (x,y-5),1,1.5,(0,255,0),2) if len(approx)==5: cv2.putText(image,'Pentagono', (x,y-5),1,1.5,(0,255,0),2) if len(approx)==6: cv2.putText(image,'Hexagono', (x,y-5),1,1.5,(0,255,0),2) if len(approx)>10: cv2.putText(image,'Circulo', (x,y-5),1,1.5,(0,255,0),2) cv2.drawContours(image, [approx], 0, (0,255,0),2) cv2.imshow('image',image) cv2.waitKey(0)
Veamos los resultados
Ahora probemos con una imagen con más figuras:
Como pudiste apreciar estas figuras han podido ser etiquetadas correctamente, esto depende de la función cv2.approxPolyDP y las demás consideraciones que tomes en cuanto a la aplicación. Nos vemos en el siguiente post donde veremos como etiquetar no solo a las figuras sino también su color.
Referencias
- https://docs.opencv.org/3.4/d1/d32/tutorial_py_contour_properties.html
- https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html
- https://docs.opencv.org/2.4/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html
- https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/
Excelente Gaby, me servirá de mucho
De nada Jorge, espero que así sea 🙂
Muchas gracias Gabriela,
Es un gran trabajo y que nos puede ayudar mucho.
Saludos,
Muchas gracias Jorge, saludos para ti también.
Últimamente he estado viendo mas tutoriales de mujeres, parece que entieneden el mundo más facíl.
Me ayudo bastante tu trabajo, gracias 😀
Muchas gracias Camilo, eres muy amable. 🙂
ES MUY BUENA LA INFORMACIÓN Y GRACIAS, no se i tal ves me puede ayudar como le puedo poner
image = cv2.imread(‘figurasColores2.png’), cuando no quiero ver desde una imagen si no desde un entorno de la camara mismo. en tiempo real como podria hacer
Hola JESS, podrías preparar un escenario de pruebas por ejemplo, en donde tengas distintas figuras geométricas. Entonces para poder realizarlo a través de un video ya no lees la imagen, sino los fotogramas. Puedes ver este tutorial por ejemplo: http://omes-va.com/basicvideo/
Hola muy buena tu información, te quería consulta como se realiza la eroción y dilatación con None.
canny = cv2.dilate(canny, None, iterations=1)
canny = cv2.erode(canny, None, iterations=1)
Muchas gracias!
Saludos
Hola Emanuel, dada una imagen binaria (blanco y negro), con cv2.dilate harás más grande el área blanca, mientras que con cv2.erode la disminuirás. Con None, estoy indicando que no he especificado un kernel.
Hola muy buena la información brindada. Te queria consultar como funciona la erocion y dilatacion con None.
canny = cv2.dilate(canny, None, iterations=1)
canny = cv2.erode(canny, None, iterations=1)
Muchas gracias
Saludos
Muy buenos tutoriales de los mejor en español, sigue trabajando.
Muchas gracias Antonio! 🙂
Hola Gaby, estoy intentando replicar esto mismo pero en matlab y se me ha dificultado un poco el tema, no se si puedas echarme una mano con esto.
Quedo atenta.
gracias.
Hola Gabriela como estas, tengo una pregunta si fuera no con foto sino con video ? que opcion existe ?
Hola Sebastian, podrías leer un video o videostreaming, puedes ver este video por ejemplo: https://youtu.be/iAnCUJvNCGY
Hola gracias por compartir acabo de leer tu entrada, aunque el enlace de ‘ blog de pyimagesearch’ no me ha funcionado, si podrías verlo por favor.
Hola Jhon, muchas gracias. Este es el link: https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/ 🙂
Quisiera saber como agregar una función que cuente las formas que encuentro, ejemplo 5 cuadrados, 2 triángulos, etc!
Gracias!
Hola Federico, podrías usar un contador para cada caso. 🙂
Hola. Estoy haciendo una entrega para clase en la que tengo que encontrar la forma rectangular de mi DNI, hasta ahí bien gracias a lo que publicaste aquí. El problema está en que nos pide sacar la información en imágenes no en texto por consola, sabes como puedo hacer para dividir la foto del DNI en otras más pequeñas, es decir, una para el nombre, otra para apellidos, otra para la foto, etc.
Muchas gracias!
Que tal, gracias por tu dedicación!!
Hay alguna forma de usar esta detección de figuras pero para para js o java?
Exelente tutorial.
Lo estoy aplicando en la detección de rectángulos con fondo oscuro que contienen texto color blanco en la imagen de una factura.
¿Cómo puedo invertir esas áreas para lograr tener un fondo totalmente blanco y las letras en color negro?
Hola me gustaría saber si es posible detectar 4 círculos que componen un rectángulo y entre esos 4 puntos de colores poner una imagen en perspectiva.
pregunta.. ¿Dónde debo guardar la imagen? que ruta?
graciasss…..
Traceback (most recent call last):
File «C:/Users/Sistemas/Desktop/Figurasgeometricas.py», line 4, in
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv2.error: OpenCV(4.6.0) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\color.cpp:182: error: (-215:Assertion failed) !_src.empty() in function ‘cv::cvtColor’
Hola Gaby, corrí el último código (que para mi caso funcionó con python 3.9) y solo pude detectar el hexágono ¿Esto está mal no? Solo copié y pegue :D. ¿Alguna idea?
Por cierto, excelente trabajo.
hola me sale este error [ WARN:0@76.214] global loadsave.cpp:244 cv::findDecoder imread_(‘figurasColores.png’): can’t open/read file: check file path/integrity
Gabi hola necesito ayuda estoy en un proyecto con una cámara RealSense y tengo que detectar cuadrados triángulos y cilindros y ubicarlos en el espacio con coordenadas x e y pero no son las mismas configuraciones para las formas geométricas para esta cámara ya es es 3D gracias gabi de parte de todos los estudiantes de robotica de Argentina saludos
Hola Gabi, tengo el sigueinte mensaje de error al tratar de importar la librería
ImportError: DLL load failed while importing cv2: No se puede encontrar el módulo especificado.