Visión artificial: Detección de colores y contornos con nuestra Raspberry Pi !!

Este es el primer proyecto de visión artificial que desarrollamos en este blog y tiene como finalidad darnos una introducción bastante general al principal paquete que vamos a emplear en el desarrollo de proyectos con esta temática: OpenCV, el cual es bastante potente. Veremos algunas de sus funciones principales, así como la configuración necesaria para poder emplear de manera correcta nuestra cámara e ir escalando poco a poco junto a ustedes en el desarrollo de los principales algoritmos de visión artificial en la actualidad (reconocimiento facial, malla facial, reconocimiento de imágenes en movimiento, mediapipe, etc…).

Este post consiste en la detección de los 3 colores primarios: rojo, azul y amarillo, así como la gráfica de una línea de contorno que encierre a objetos con dichos colores, además se incluye un punto de coordenadas en el centro de cada objeto identificado y algoritmos que permiten modificar la escala del objeto que deseamos identificar, lo último es súper útil si queremos identificar objetos a una distancia específica.

Si bien es cierto, este proyecto está desarrollado en una placa Raspberry Pi, puede ser replicado en cualquier computador que cumpla los requisitos mínimos necesarios de los softwares empleados.

Lo primero que recomendamos hacer es conectar la cámara, en esta oportunidad se emplea un módulo de cámara focal ajustable con visión nocturna infrarroja, la cual pueden encontrar en Aliexpress.

Cámara focal visión nocturna infrarroja

Luego de tener nuestro módulo de cámara listo, pasamos a conectarnos a nuestra Raspberry Pi ya sea mediante el uso de periféricos o de manera remota, en este caso nos conectaremos de manera remota a una Raspberry Pi 3B+. Si aún no tienes muy en claro como acceder a tu Raspberry Pi o la instalación de un sistema operativo para éste, te recomiendo este post, en el cual explicamos de manera detallada todos puntos mencionados.

Ya en la interfaz gráfica de nuestra Raspberry Pi empezamos actualizando todos los paquetes y programas de nuestro OS, esto lo hacemos con los tan famosos comandos:

  • sudo apt-get update
  • sudo apt-get upgrade

Limpiamos terminal y el siguiente comando que vamos a utilizar es sudo raspi-config, el cual nos permitirá acceder a un menú de configuración.

Seleccionamos opciones de interfaz.

Habilitamos la opción de Cámara.

Reiniciamos Raspbian y ¡listo! ya podemos utilizar el módulo cámara con nuestra Raspberry Pi.

Respecto al entorno de programación se va a emplear Thonny Python IDE, entorno que viene instalado por defecto en Raspbian.

Instalación de OpenCV

Para instalar OpenCV vamos a utilizar la página piwheels, la cual nos brindará diversas versión de este paquete junto a sus características para la versión de python y arquitectura de microprocesador que tengamos. Así que nos dirigimos a dicha página y seleccionamos la opción de packages.

Buscamos opencv y seleccionamos el paquete opencv-contrib-pyhton.

Seremos redirigidos a una nueva página, donde obtendremos una línea o serie de comandos a ejecutar dependiendo la versión de OpenCV que deseamos instalar. En este caso la línea de comandos que aparece es para instalar la última versión de este paquete, así que lo copiamos y pegamos en nuestra terminal.

Si deseamos instalar otra versión, simplemente vamos a la tabla de Releases, columna Files y seleccionamos el cuadro de color azul correspondiente a la versión que deseamos, si realizamos esto obtendremos otra seria de comandos para la instalación del paquete.

Pasamos a nuestra terminal y ejecutamos el código indicado.

Y listo, con esto ya tendríamos instalado OpenCV en nuestra Raspberry Pi, para verificar su correcta instalación podemos ingresar al entorno de Python y ejecutar las siguientes líneas para obtener la versión de OpenCV instalada.

Si tienes algún inconveniente al instalar OpenCV puedes dejar un comentario referente al error que obtienes para que nosotros te podamos brindar una asesoría personalizada, recuerda que la instalación de paquetes varía mucho dependiendo de la versión de éstos y la versión, en este caso, de Python y Raspbian que tengamos instalado.

¡A codificar!

"""
 * Detección de colores y contornos con nuestra Raspberry Pi
 * Carlos Andrés Saldaña Amézquita
 * 
 * ▬▬▬▬▬▬▬▬▬ SÍGUEME TODOMAKER ▬▬▬▬▬▬▬▬▬▬
 * Sitio web: https://todomaker.com/
 * Instagram: https://bit.ly/3v1NmXp
 * Facebook: https://bit.ly/34IWbuH
 * YouTube: https://bit.ly/3LJRtx6
"""
# Empezamos incluyendo las librerías a emplear
import cv2
import numpy as np

# Definimos una función para detectar colores y graficar contornos
def draw(mask, color):
    contornos,_ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    for c in contornos:
        area = cv2.contourArea(c)
        
        # Si el área de interés es mayor a
        if area > 5000:    
            M = cv2.moments(c)
            if (M["m00"] == 0):
                M["m00"] = 1
                
            x = int(M["m10"]/M["m00"])
            y = int(M["m01"]/M["m00"])
            
            #--- Dibujamos los contornos seleccionados ---
            newContorno = cv2.convexHull(c)
             
            #--- Dibujamos un punto de r=7 y color verde ---
            cv2.circle(frame, (x, y), 7, (0, 255, 0), -1)
                                 
            # Puntos, ubicación, fuente, grosor, color, tamaño
            cv2.putText(frame, '{}, {}'.format(x, y), (x+10, y), font, 0.75, (0, 255, 0), 1, cv2.LINE_AA)
            cv2.drawContours(frame, [newContorno], 0, color, 3)

# Definimos el puerto de la cámara a emplear         
cap = cv2.VideoCapture(0)

# Máscara de colores a detectar
azulBajo = np.array([100, 100, 20], np.uint8)
azulAlto = np.array([125, 255, 255], np.uint8)

amarilloBajo = np.array([15, 100, 20], np.uint8)
amarilloAlto = np.array([45, 255, 255], np.uint8)

rojoBajo1 = np.array([0, 100, 20], np.uint8)
rojoAlto1 = np.array([5, 255, 255], np.uint8)

rojoBajo2 = np.array([175, 100, 20], np.uint8)
rojoAlto2 = np.array([179, 255, 255], np.uint8)

# Mientras está activada la captura de video
while True:
    # Obtenemos un valor booleano e imagen
    ret, frame = cap.read()

    # Si hay imagen capturada
    if ret == True:

        # Pasamos de BGR a HSV
        frameHSV = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        
        maskAzul = cv2.inRange(frameHSV, azulBajo, azulAlto)

        maskAmarillo = cv2.inRange(frameHSV, amarilloBajo, amarilloAlto)
        
        maskRojo1 = cv2.inRange(frameHSV, rojoBajo1, rojoAlto1)
        maskRojo2 = cv2.inRange(frameHSV, rojoBajo2, rojoAlto2)
        maskRojo = cv2.add(maskRojo1, maskRojo2)
        
        draw(maskAzul, (255, 0, 0))
        draw(maskAmarillo, (0, 255, 255))
        draw(maskRojo, (0, 0, 255))
        
        # Mostramos la ventana de captura
        cv2.imshow('Captura de video', frame)
        
        # Detenemos la visualización con la tecla 's'
        if cv2.waitKey(1) & 0xFF == ord('s'):
            break

# Detenemos la captura de video
cap.release()
# Cerramos todas las ventanas
cv2.destroyAllWindows()

Explicación del código

Ya en el entorno de Thonny Python IDE lo primero que debemos hacer es importar las librerías que necesitamos, en este caso son 2: cv2 para el procesamiento de nuestras imágenes y numpy para poder trabajar con matrices.

# Empezamos incluyendo las librerías a emplear
import cv2
import numpy as np

Vamos a definir una función para hacer el código más ordenado, en este caso la función llevará el nombre draw y recibirá 2 parámetros: la máscara de colores (mask) y el color del contorno (color). En la siguiente línea tenemos el comando cv2.findContours que nos va a permitir encontrar los contornos de todos los objetos detectados y cuyos parámetros son:

  • mask = Imagen a la cual vamos a aplicar el contorno.
  • cv2.RETR_EXTERNAL = Representa un modo de recuperación del contorno, en este caso se va a detectar únicamente el contorno exterior.
  • cv2.CHAIN_APPROX_SIMPLE = Método aproximado del contorno, en este caso se comprime los elementos en las direcciones horizontal, vertical y diagonal, esto permite retener las coordenadas finales en dicha dirección. Por ejemplo, un contorno rectangular solo necesita 4 puntos para guarda la información del contorno.

Si desea profundizar más, lo invitamos a revisar la documentación de opencv.

Lo siguiente que tenemos es un ciclo for, en el cual vamos a recorrer todos los contornos detectados y mostrar únicamente aquellos contornos que encierren un área mayor a 1000. Si esta condición se cumple pasamos a graficar el contorno y graficar el centro y coordenadas del objeto detectado.

# Definimos una función para detectar colores y graficar contornos
def draw(mask, color):
    contornos,_ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # Dibujamos nuestros contornos detectados
    for c in contornos:
        area = cv2.contourArea(c)
        
        # Si el área de interés es mayor a
        if area > 5000:    
            # Buscamos el punto central del objeto identificado (x, y)
            M = cv2.moments(c)
            
            # Evitamos las divisiones entre 0
            if (M["m00"] == 0):
                M["m00"] = 1
                
            x = int(M["m10"]/M["m00"])
            y = int(M["m01"]/M["m00"])

Seguimos dentro de la función draw y ahora sí dibujamos los contornos seleccionados, además del punto central de cada objeto junto a sus respectivas coordenadas, tanto el punto como la letra son de color verde en el formato RGB. Las demás características tales como la ubicación, fuente, grosor y tamaño del texto y contorno también se definen acá, con la función cv2.putText y cv2.drawContours.

Para cv2.putText tenemos lo siguiente:

  • frame = Imagen sobre la cual vamos a dibujar el texto.
  • ‘{}, {}’.format(x, y) = Cadena de texto a mostrar.
  • (x+10, y) = Coordenadas de la esquina inferior izquierda de la cadena de texto.
  • font = Tipo de fuente empleado.
  • 0.75 = Tamaño de la fuente empleada.
  • (0, 255, 0) = Color en formato RGB del texto.
  • 1 = Grosor del texto empleado en px.
  • cv2.LINE_AA = Tipo de línea a emplear.

Para v2.drawContours tenemos lo siguiente:

  • frame = Imagen que vamos a encerrar con el contorno.
  • [newContorno] = Acá indicamos el contorno a graficar obtenido de la función findContours().
  • 0 = Coordenadas en px de los puntos del contorno a graficar.
  • color = Color del contorno a graficar.
  • 3 = Grosor de la línea del contorno en px.
            #--- Dibujamos los contornos seleccionados ---
            newContorno = cv2.convexHull(c)
             
            #--- Dibujamos un punto de r=7 y color verde ---
            cv2.circle(frame, (x, y), 7, (0, 255, 0), 1)
                                
            # Puntos, ubicación, fuente, grosor, color, tamaño
            cv2.putText(frame, '{}, {}'.format(x, y), (x+10, y), font, 0.75, (0, 255, 0), 1, cv2.LINE_AA)
            cv2.drawContours(frame, [newContorno], 0, color, 3)

Aquí únicamente definimos el puerto de la cámara que vamos a utilizar con ayuda del comando cv2.VideoCapture, en mi caso la cámara está conectada en el puerto 0. Ustedes pueden tener la cámara conectada en otro puerto (-1, 0, 1, 2,…).

# Definimos el puerto de la cámara a emplear         
cap = cv2.VideoCapture(0)

Aquí definimos nuestras matrices de colores en formato HSV (Hue, Saturation, Value o en español Matiz, Saturación, Brillo o valor) cuyos valores van así:

  • Hue = de 0 a 179.
  • Saturation = de 0 a 255.
  • Value = de 0 a 255.
# Máscara de colores a detectar
azulBajo = np.array([100, 100, 20], np.uint8)
azulAlto = np.array([125, 255, 255], np.uint8)

amarilloBajo = np.array([15, 100, 20], np.uint8)
amarilloAlto = np.array([45, 255, 255], np.uint8)

rojoBajo1 = np.array([0, 100, 20], np.uint8)
rojoAlto1 = np.array([5, 255, 255], np.uint8)

rojoBajo2 = np.array([175, 100, 20], np.uint8)
rojoAlto2 = np.array([179, 255, 255], np.uint8)

En este sección se realiza la última etapa de la detección de colores y empezamos obtenido la imagen a procesar, en esta ocasión la imagen a procesar es frame, uno de los parámetros obtenidos del comando cap.read().

El siguiente paso a realizar es pasar nuestra imagen en formato BGR(Blue, Green, Red. Éste es el formato que OpenCV lee por defecto) a HSV con el comando cv2.cvtColor.

En tercer lugar creamos máscaras con los colores definidos en el script anterior (azul, amarillo y rojo) evaluamos nuestra imagen en formato HSV (frameHSV) con dichos colores.

Por último pasamos a visualizar la detección de colores con ayuda de la función definida previamente (draw), mostramos la ventana de captura y declaramos una tecla para poder cerrar dicha ventana.

# Mientras está activada la captura de video
while True:
    # Obtenemos un valor booleano e imagen
    ret, frame = cap.read()

    # Si hay imagen capturada
    if ret == True:

        # Pasamos de BGR a HSV
        frameHSV = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        
        maskAzul = cv2.inRange(frameHSV, azulBajo, azulAlto)
        maskAmarillo = cv2.inRange(frameHSV, amarilloBajo, amarilloAlto)
        
        maskRojo1 = cv2.inRange(frameHSV, rojoBajo1, rojoAlto1)
        maskRojo2 = cv2.inRange(frameHSV, rojoBajo2, rojoAlto2)
        maskRojo = cv2.add(maskRojo1, maskRojo2)
        
        draw(maskAzul, (255, 0, 0))
        draw(maskAmarillo, (0, 255, 255))
        draw(maskRojo, (0, 0, 255))
        
        # Mostramos la ventana de captura
        cv2.imshow('Captura de video', frame)
        
        # Detenemos la visualización con la tecla 's'
        if cv2.waitKey(1) & 0xFF == ord('s'):
            break

Finalmente nos aseguramos de detener la captura de video y cerrar todas las posibles ventanas que puedan quedar abiertas.

# Detenemos la captura de video
cap.release()
# Cerramos todas las ventanas
cv2.destroyAllWindows()

Resultados

Conclusiones

En este post se pudo apreciar la configuración correcta de nuestra cámara en una Raspberry Pi 3B+ y la instalación de OpenCV, paquete clave en el desarrollo de proyectos enfocados a visión artificial. Se logró implementar el algoritmo correcto para la detección de los 3 colores primarios, así como la grafica de sus contornos y el punto de coordenadas central de cada objeto, dicho objeto debe tener un área mayor a 5000px para poder ser identificado.

¡Y ya sabes! Mantente al pendiente de nuestras publicaciones y síguenos en nuestras redes sociales, estoy seguro que encontrarán mucha información de su interés. Si desean aprender algo en concreto referente a estas tecnologías lo pueden dejar en los comentarios.

Previous Post
Next Post