Detectando FIGURAS GEOMÉTRICAS (??⬛) con OpenCV – Python

Por Administrador

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:

  1. Leer la imagen de entrada y transformarla a escala de grises
  2. Obtener una imagen binarizada
  3. Encontrar los contornos
  4. cv2.approxPolyDP
    1. Determinando las figuras geométricas
      1. Triángulo
      2. Cuadrado y rectángulo
        1. Aspect Ratio
      3. Pentágono
      4. Hexágono
      5. Círculo

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