Detecta FIGURAS GEOMÉTRICAS y sus COLORES ❤️???? OpenCV – Python

Por Administrador

En el post anterior vimos como detectar algunas figuras geométricas simples, y además en otros posts y videos te he mostrado como hago para detectar colores usando OpenCV y Python. Así que en este video voy a unir estos dos en una sola aplicación. Acompáñame y recuerda que todo el código usado para esta aplicación estará al final de este post y en mi repositorio de github.

Hoy veremos lo siguiente:

  • Creando función para detectar los colores: rojo, naranja, amarillo, verde, violeta y rosa.
  • Creando función para detectar las figuras geométricas: triángulo, cuadrado, rectángulo, pentágono, hexágono y círculo.
  • Leer la imagen de entrada, aplicar escala de grises, detección de bordes y encontrar sus contornos.
    • Convertir imagen de entrada de BGR a HSV.
    • Analizando cada contorno.
    • Extrayendo cada contorno.
    • Combinando la imagen binaria auxiliar y la de HSV.
    • Detectando figuras geométricas y su color
  • Programa completo
  • Referencias

Creando función para detectar los colores: rojo, naranja, amarillo, verde, violeta y rosa

Vamos en un principio a desarrollar una función que nos ayude a detectar los colores de las figuras geométricas presentes en la siguiente imagen:

Figura 1: Imagen de entrada

import cv2
import numpy as np

def figColor(imagenHSV):
  # Rojo
  rojoBajo1 = np.array([0, 100, 20], np.uint8)
  rojoAlto1 = np.array([10, 255, 255], np.uint8)
  rojoBajo2 = np.array([175, 100, 20], np.uint8)
  rojoAlto2 = np.array([180, 255, 255], np.uint8)

  # Naranja
  naranjaBajo = np.array([11, 100, 20], np.uint8)
  naranjaAlto = np.array([19, 255, 255], np.uint8)

  #Amarillo
  amarilloBajo = np.array([20, 100, 20], np.uint8)
  amarilloAlto = np.array([32, 255, 255], np.uint8)

  #Verde
  verdeBajo = np.array([36, 100, 20], np.uint8)
  verdeAlto = np.array([70, 255, 255], np.uint8)

  #Violeta
  violetaBajo = np.array([130, 100, 20], np.uint8)
  violetaAlto = np.array([145, 255, 255], np.uint8)

  #Rosa
  rosaBajo = np.array([146, 100, 20], np.uint8)
  rosaAlto = np.array([170, 255, 255], np.uint8)

  # Se buscan los colores en la imagen, según los límites altos 
  # y bajos dados
  maskRojo1 = cv2.inRange(imagenHSV, rojoBajo1, rojoAlto1)
  maskRojo2 = cv2.inRange(imagenHSV, rojoBajo2, rojoAlto2)
  maskRojo = cv2.add(maskRojo1, maskRojo2)
  maskNaranja = cv2.inRange(imagenHSV, naranjaBajo, naranjaAlto)
  maskAmarillo = cv2.inRange(imagenHSV, amarilloBajo, amarilloAlto)
  maskVerde = cv2.inRange(imagenHSV, verdeBajo, verdeAlto)
  maskVioleta = cv2.inRange(imagenHSV, violetaBajo, violetaAlto)
  maskRosa = cv2.inRange(imagenHSV, rosaBajo, rosaAlto)
  
  cntsRojo = cv2.findContours(maskRojo, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3	
  cntsNaranja = cv2.findContours(maskNaranja, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsAmarillo = cv2.findContours(maskAmarillo, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsVerde = cv2.findContours(maskVerde, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsVioleta = cv2.findContours(maskVioleta, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsRosa = cv2.findContours(maskRosa, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3

  if len(cntsRojo)>0: color = 'Rojo'
  elif len(cntsNaranja)>0: color = 'Naranja'
  elif len(cntsAmarillo)>0: color = 'Amarillo'
  elif len(cntsVerde)>0: color = 'Verde'
  elif len(cntsVioleta)>0: color = 'Violeta'
  elif len(cntsRosa)>0: color = 'Rosa'

  return color

Línea 1 y 2: Importamos los paquetes necesarios que vamos a usar, en este caso OpenCV y Numpy.

Línea 4: Declaramos la función figColor, como entrada necesita de una imagen en HSV que veremos luego.

Línea 6 a 29: Aquí establecemos los límites iniciales y finales de los colores que vamos a detectar, esto ya lo vimos antes en otro post, pero si es la primera vez que te encuentras con mi blog te los dejo aquí para más información: Detección de colores en OpenCV – Python (En 4 pasos) y DETECCIÓN DE COLORES Y Tracking en OpenCV – Parte2.

Línea 33 a 40: Se crean imágenes binarias, es decir imágenes a blanco  y negro, donde el  blanco representará la presencia de dicho color en la imagen, mientras que en negro la no presencia del mismo. Estas imágenes nos servirán para poder encontrar los contornos correspondientes a cada color.

Línea 42 a la 47: Ahora necesitamos encontrar los contornos de cada imagen binaria por cada color. Como te habrás dado cuenta aquí estoy empleando una alternativa en su uso, ubicando al final [0] para indicar que se está tomando el primer argumento que nos devuelve cv2.findContours, esto para OpenCV 4. Si tuvieses OpenCV3, en vez de 0 deberías poner 1 ya que los contornos se encontrarán en la posición 1.

Línea 49 a 54: Luego, lo que hice fue contar cuantos contornos hay por cada color. Si por ejemplo la cantidad de contornos en color rojo es mayor a 0, se asignará ‘Rojo’ a la variable color. En cambio si la cantidad de contornos  en naranja es mayor a 0, la variable color será ‘Naranja’, y así para todos los colores restantes. Esto nos servirá, para que esta función nos devuelva el nombre del color que ha sido detectado. 

Línea 56: Esta función nos devolverá la variable color que contiene el nombre del color detectado.

Creando función para detectar las figuras geométricas: triángulo, cuadrado, rectángulo, pentágono, hexágono y círculo

Ahora desarrollaremos una función para poder determinar de que figura geométrica se trata y que nos devuelva el nombre de la misma. Para ello vamos a usar el procedimiento que vimos en el post anterior: Detectando FIGURAS GEOMÉTRICAS (??⬛) con OpenCV – Python. Por lo que si tienes alguna duda te recomiendo que leas primero ese post.

def figName(contorno,width,height):
  epsilon = 0.01*cv2.arcLength(contorno,True)
  approx = cv2.approxPolyDP(contorno,epsilon,True)

  if len(approx) == 3:
    namefig = 'Triangulo'

  if len(approx) == 4:
    aspect_ratio = float(width)/height
    if aspect_ratio == 1:
      namefig = 'Cuadrado'
    else:
      namefig = 'Rectangulo'

  if len(approx) == 5:
    namefig = 'Pentagono'

  if len(approx) == 6:
    namefig = 'Hexagono'

  if len(approx) > 10:
    namefig = 'Circulo'

  return namefig

Línea 58: A la función figName se le entrega un contorno, su ancho y su altura (que nos servirá en el caso que se tenga que diferenciar entre cuadrado y rectángulo), y procederá a distinguir si se trata de un triángulo, un cuadrado o un rectángulo, un pentágono, un hexágono o un círculo. 

Línea 59 a la 79: Aquí necesitamos emplear la función cv2.approxPolyDP para obtener approx y a su vez calculamos su longitud, para diferenciar de entre las distintas figuras geométricas. Por ello en la línea 62 si len(approx) es igual a 3 se tratará de un triángulo, en la línea 65 si es 4 puede tratarse de un cuadrado o un rectángulo (que diferenciamos calculando su aspect ratio), y así sucesivamente. En el caso del círculo en la línea 78 he establecido que si len(approx) es mayor a 10 sea considerado un círculo (como un polígono con 10 lados), pero esta consideración puede cambiar dependiendo de tu aplicación.

Si quieres una explicación más extensa del procedimiento para detectar figuras geométricas  puedes visitar mi anterior post.

Línea 81: Esta función nos devolverá la variable namefig que corresponde al nombre de la figura geométrica detectada.

Una vez que se tienen estas dos funciones:

  • figColor, para que nos devuelva el color de la figura geométrica.
  • figName, que nos devolverá el nombre correspondiente a la figura geométrica.

Podremos proceder a aplicarlas en una imagen. El procedimiento que vamos a seguir es similar al que vimos en el video/post anterior. 

Leer la imagen de entrada, aplicar escala de grises, detección de bordes y encontrar sus contornos

Este procedimiento es igual al que  usamos  anteriormente  por lo que lo explicaré brevemente.

imagen = cv2.imread('figurasColores2.png')
gray = cv2.cvtColor(imagen, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 10,150)
canny = cv2.dilate(canny,None,iterations=1)
canny = cv2.erode(canny,None,iterations=1)
_,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

Leemos la imagen con cv2.imread. Luego transformamos la imagen de BGR a escala de grises para después aplicar detección de bordes con cv2.Canny y aplicamos transformaciones morfológicas con cv2.dilate y cv2.erode para poder mejorar la imagen binaria obtenida. 

Después aplicamos cv2.findContours para encontrar los contornos. Recuerda usar la línea 88 u 89 dependiendo de la versión de OpenCV que tengas.

Convertir imagen de entrada de BGR a HSV

También vamos a transformar la imagen de entrada de BGR a HSV, que nos servirá para alimentar a la función figColor y que permitirá detectar los colores de las figuras.

imageHSV = cv2.cvtColor(imagen, cv2.COLOR_BGR2HSV)

Analizando cada contorno

for c in cnts:
  x, y, w, h = cv2.boundingRect(c)

Línea 92: Vamos a tratar cada contorno con un for.

Línea 93: Se emplea la función cv2.boundingRect para obtener los puntos x, y, ancho y alto del contorno (estos dos últimos nos ayudarán en la función figName para diferenciar entre rectángulo y cuadrado). 

Extrayendo cada contorno

Vamos a crear una imagen axuliar del tamaño de la imagen de entrada para tratar cada contorno veamos:

imAux = np.zeros(imagen.shape[:2], dtype="uint8")
imAux = cv2.drawContours(imAux, [c], -1, 255, -1)

NOTA: Esta técnica es usada en pyimagesearch.

Línea 94: Creamos una imagen auxiliar de negro de dos dimensiones con el mismo ancho y alto de la imagen de entrada, para ello usamos np.zeros y especificaremos su tamaño con imagen.shape[:2].

Línea 95:  Se dibuja un contorno en la imagen auxiliar imAux, toma en cuenta que el contorno estará lleno (ya que en el tercer argumento de cv2.drawContours hemos colocado -1), es decir que veremos un área en blanco que corresponderá a cada figura.

Combinando la imagen binaria auxiliar y la de HSV

Ahora será necesario con ayuda de la imagen auxiliar realizada, plasmar en sus zonas blancas la imagen en HSV.

maskHSV = cv2.bitwise_and(imageHSV,imageHSV, mask=imAux)

Aquí estamos usando cv2.bitwise_and (da clic aquí para más información de esta función) para poder visualizar en las zonas blancas de la imagen auxiliar a la imagen en HSV. Puedes ver este proceso en el video que subí a yotube justo en este minuto.

Pero te preguntarás ¿Por qué es necesario este paso?, pues bien esto es simplemente para poder detectar el color de cada figura geométrica.

Detectando figuras geométricas y su color

Bien, ahora solo nos queda llamar a las funciones que habíamos creado en un principio y etiquetar a cada figura geométrica con su nombre y color.

name = figName(c,w,h)
color = figColor(maskHSV)
nameColor = name + ' ' + color
cv2.putText(imagen,nameColor,(x,y-5),1,0.8,(0,255,0),1)
cv2.imshow('imagen',imagen)
cv2.waitKey(0)

Línea 97: Llamamos a la función figName dándole el contorno c, ancho w y alto h. Entonces nos devolverá name que es el nombre de la figura geométrica.

Línea 98: En esta llamamos a la función figColor, a ella le entregamos maskHSV y nos devolverá el color de la figura.

Línea 99: Ahora para imprimir el texto con la infomación de la figura necesitamos concatenar name y color, almacenando esto en una nueva variable llamada nameColor.

Línea 100: Visualizamos nameColor en la imagen de entrada.

Línea 101 y 102: Visualizamos la imagen y esperamos a que una tecla sea presionada para seguir el proceso con cada figura de la imagen. 

Programa completo

import cv2
import numpy as np

def figColor(imagenHSV):
  # Rojo
  rojoBajo1 = np.array([0, 100, 20], np.uint8)
  rojoAlto1 = np.array([10, 255, 255], np.uint8)
  rojoBajo2 = np.array([175, 100, 20], np.uint8)
  rojoAlto2 = np.array([180, 255, 255], np.uint8)

  # Naranja
  naranjaBajo = np.array([11, 100, 20], np.uint8)
  naranjaAlto = np.array([19, 255, 255], np.uint8)

  #Amarillo
  amarilloBajo = np.array([20, 100, 20], np.uint8)
  amarilloAlto = np.array([32, 255, 255], np.uint8)

  #Verde
  verdeBajo = np.array([36, 100, 20], np.uint8)
  verdeAlto = np.array([70, 255, 255], np.uint8)

  #Violeta
  violetaBajo = np.array([130, 100, 20], np.uint8)
  violetaAlto = np.array([145, 255, 255], np.uint8)

  #Rosa
  rosaBajo = np.array([146, 100, 20], np.uint8)
  rosaAlto = np.array([170, 255, 255], np.uint8)

  # Se buscan los colores en la imagen, segun los límites altos 
  # y bajos dados
  maskRojo1 = cv2.inRange(imagenHSV, rojoBajo1, rojoAlto1)
  maskRojo2 = cv2.inRange(imagenHSV, rojoBajo2, rojoAlto2)
  maskRojo = cv2.add(maskRojo1, maskRojo2)
  maskNaranja = cv2.inRange(imagenHSV, naranjaBajo, naranjaAlto)
  maskAmarillo = cv2.inRange(imagenHSV, amarilloBajo, amarilloAlto)
  maskVerde = cv2.inRange(imagenHSV, verdeBajo, verdeAlto)
  maskVioleta = cv2.inRange(imagenHSV, violetaBajo, violetaAlto)
  maskRosa = cv2.inRange(imagenHSV, rosaBajo, rosaAlto)
  
  cntsRojo = cv2.findContours(maskRojo, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3	
  cntsNaranja = cv2.findContours(maskNaranja, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsAmarillo = cv2.findContours(maskAmarillo, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsVerde = cv2.findContours(maskVerde, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsVioleta = cv2.findContours(maskVioleta, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3
  cntsRosa = cv2.findContours(maskRosa, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] #Reemplaza por 1, si tienes OpenCV3

  if len(cntsRojo)>0: color = 'Rojo'
  elif len(cntsNaranja)>0: color = 'Naranja'
  elif len(cntsAmarillo)>0: color = 'Amarillo'
  elif len(cntsVerde)>0: color = 'Verde'
  elif len(cntsVioleta)>0: color = 'Violeta'
  elif len(cntsRosa)>0: color = 'Rosa'

  return color
    
def figName(contorno,width,height):
  epsilon = 0.01*cv2.arcLength(contorno,True)
  approx = cv2.approxPolyDP(contorno,epsilon,True)

  if len(approx) == 3:
    namefig = 'Triangulo'

  if len(approx) == 4:
    aspect_ratio = float(width)/height
    if aspect_ratio == 1:
      namefig = 'Cuadrado'
    else:
      namefig = 'Rectangulo'

  if len(approx) == 5:
    namefig = 'Pentagono'

  if len(approx) == 6:
    namefig = 'Hexagono'

  if len(approx) > 10:
    namefig = 'Circulo'

  return namefig
  
imagen = cv2.imread('figurasColores2.png')
gray = cv2.cvtColor(imagen, cv2.COLOR_BGR2GRAY)
canny = cv2.Canny(gray, 10,150)
canny = cv2.dilate(canny,None,iterations=1)
canny = cv2.erode(canny,None,iterations=1)
#_,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
imageHSV = cv2.cvtColor(imagen, cv2.COLOR_BGR2HSV)

for c in cnts:
  x, y, w, h = cv2.boundingRect(c)
  imAux = np.zeros(imagen.shape[:2], dtype="uint8")
  imAux = cv2.drawContours(imAux, [c], -1, 255, -1)
  maskHSV = cv2.bitwise_and(imageHSV,imageHSV, mask=imAux)
  name = figName(c,w,h)
  color = figColor(maskHSV)
  nameColor = name + ' ' + color
  cv2.putText(imagen,nameColor,(x,y-5),1,0.8,(0,255,0),1)
  cv2.imshow('imagen',imagen)
  cv2.waitKey(0)

Finalmente obtendremos lo siguiente:

Y hemos llegado al final de este tutorial, espero que te haya sido de ayuda y que te animes a probarlo. Si tienes alguna duda, sugerenia, corrección o mensaje, puedes hacérmelo saber en el área de comentarios.

Referencias