septiembre 8, 2021 10:00 am

Jesús

En su forma más básica, un video no es más que una serie de imágenes, por lo que al aprovecharnos de este hecho aparementemente simple o tirvial, podemos adaptar nuestro conocimiento sobre clasificación de imágenes para crear pipelines de procesamiento de video impulsados en deep learning.

En el artículo de hoy utilizaremos la misma fórmula ya vista cuando implementamos un detector de sonrisas y un detector de tapabocas (barbijos, máscaras faciales, cubrebocas), para crear un reconocedor de emociones.

La fórmula anteriormente mencionada se compone de dos elementos primordiales:

  1. 1
    Un detector de rostros: Usaremos el clásico detector de rostros Haar Cascade implementado en OpenCV.
  2. 2
    Un clasificador de emociones: Implementaremos nuestro propio clasificador de emociones en TensorFlow.

Al final de este artículo habrás aprendido:

  • Cómo funciona, a alto nivel, un reconocedor de emociones.
  • Cómo implementar una red neuronal convolucional en TensorFlow para la clasificación de emociones.
  • Cómo usar OpenCV para detectar rostros en imágenes.
  • Cómo usar OpenCV para crear un gráfico de barras para mostrar la distribución de probabilida del clasificador de emociones en tiempo real.

¿Estás listo? ¡Comencemos!

Cómo Funciona un Detector de Emociones

Mujer molesta

Mujer molesta

Como en muchas otras aplicaciones, un detector o reconocedor de emociones es un detector de objetos especializado en rostros, a partir de los cuales determina la emoción expresada por el mismo.

Los pasos que nuestro reconocedor llevará a cabo se resumen así:

  1. 1
    Toma una imagen de entrada y la pasa por el detector de rostros.
  2. 2
    Cada rostro detectado es pasado por el clasificador de emociones.
  3. 3
    Con base a las predicciones del modelo, creamos un gráfico de barras para mostrar la distribución de probabilidad de las emociones reconocidas en el rostro.
  4. 4
    Con base a las coordenadas producidas por el detector, así como a la predicción del clasificador (es decir, la emoción que manifiesta la persona), dibujamos los resultados en la imagen original.

Sencillo, ¿no te parece?

Creación del Entorno de Desarrollo con Anacond

Estructura del proyecto

Estructura del proyecto

  • La carpeta assets contiene dos videos de prueba que usaremos más adelante para demostrar la efectividad de nuestra solución.
  • En data se encuentra el conjunto de datos en el que entrenaremos nuestra red clasificadora de emociones implementada en TensorFlow.
  • datasmarts/emotions.py contiene un par de constantes útiles relacionadas con las emociones que aprenderemos a reconocer en breve.
  • datasmarts/recognize.py es el script que implementa la lógica del reconocimiento de emociones en video. Este programa depende del siguiente.
  • datasmarts/train_model.py, que como su nombre indica, implementa la arquitectura del clasificador y también la lógica de entrenamiento.
  • resources contiene el archivo XML usado para configurar e instanciar el detector de rostros basado en Haar Cascade que OpenCV implementa.

Para crear el ambiente de Anacona, solo corre este comando:

conda env create -f env.yml

Con esta instrucción habremos creado un ambiente llamado tf-emotion-recognizer, configurado así:

Si no tienes acceso a una GPU, puedes cambiar la línea 9 por - tensorflow. No obstante, ten en cuenta que esto ocasionará que el entrenamiento de la red demore decenas de horas.

Finalmente, para activar el ambiente, ejecuta:

conda activate tf-emotion-recognizer

¡Estamos listos para ponernos manos a la obra! ?

Implementando un Reconocedor de Emociones con OpenCV y TensorFlow

El primer paso consiste en implementar el clasificador de emociones, por lo que debemos hablar de los datos que usaremos para completar tal tarea.

Challenges in Representation Learning: Facial Expression Recognition Challenge

La data que usaremos proviene de una competición de Kaggle llamada Challenges in Representation Learning: Facial Expression Recognition Challenge. Las características generales de las imágenes son:
  • Las imágenes son de 48x48, en escala de grises.
  • Hay siete categorías de emociones: 0 = Molestia, 1 = Asco, 2 = Miedo, 3 = Felicidad, 4 = Tristeza, 5 = Sorpresa y 6 = Neutro.
  • Los datos se encuentra en un archivo CSV.
  • La columna emotion contiene un entero correspondiente a alguna de las emociones descritas arriba.
  • La columna pixels contiene una lista de 2304 números correspondientes a los píxeles de la imagen (48x48 = 2304).
Muestra de FER2013

Emociones de izquierda a derecha: tristeza, molestia, miedo, sorpresa, felicidad, neutro.

Para descargarte los datos, necesitas tener una cuenta de Kaggle e iniciar sesión. Como el proceso es medio engorroso, te recomiendo que te bajes el código de este post en el formulario de abajo, el cual ya contiene los datos comprimidos, así como las instrucciones de uso.

¿Quién se preocupa por ti como yo? ?

¡ATENTO!

Aunque originalmente el conjunto de datos tiene 7 emociones, nosotros usaremos solo 6, pues combinaremos las emociones 0 y 1 (es decir, Molestia y Asco, respectivamente) en una sola, ya que las expresiones faciales se parecen, y así le facilitamos un poco la vida al clasificador.

El primer archivo que veremos es datasmarts/emotions.py:

Como puedes ver, la constante EMOTIONS lista las emociones que aprenderemos a clasificar, mientras que COLORS asocia cada emoción a un color (en formato BGR). Estos colores serán útiles más adelante cuando implementemos el reconocedor.

Acto seguido, abre datasmarts/train_model.py e inserta las siguientes líneas para traerte las dependencias del programa:

La función auxiliar build_network() construye la arquitectura de la red que vamos a entrenar. Esta red se compone de seis bloques de convoluciones (dos de 32 filtros, dos de 64 filtros y dos de 128 filtros), seguida de tres capas densas.

Como se trata de una red relativamente profunda, añadimos numerosas capas Dropout() para escudarnos frente al overfitting:

La siguiente función auxiliar, load_dataset(), carga los conjuntos de entrenamiento, validación y pruebas a partir de un único archivo CSV. Primero, definimos la firma de la misma, y las listas que contendrán las imágenes y etiquetas de cada subconjunto de datos:

Después, abrimos el archivo CSV en modo de lectura, e iteramos sobre cada una de sus líneas. La columna emotion contendrá la emoción asociada a la imagen:

Como combinaremos las emociones 0 y 1 (Molestia y Asco) en la emoción 0 (Molestia). Asímismo, restamos 1 a todas las emociones para asegurarnos de que la secuencia se conserve y empiece desde 0 (recuerda que estamos deshaciéndonos de la emoción 1):

Parseamos los píxeles de la imagen presentes en la columna pixels, y convertimos la lista a una matriz 48x48:

La columna Usage nos dice a qué subconjunto de datos pertenece la imagen y la etiqueta:

Convertimos las listas de imágenes en arreglos de NumPy y aplicamos one-hot encoding a las etiquetas:

Cerramos la función load_dataset() devolviendo los conjuntos de datos:

Definimos el menú del programa, que se compone únicamente del parámetro -d/--dataset, la ruta a la carpeta donde se encuentra el CSV con los datos:

Asumimos que los datos se encuentran en un archivo llamado fer2013.csv dentro de la carpeta indicada en el flag -d/--dataset. También aprovechamos para determinar el número de clases a clasificar: 

Cargamos los datos usando load_dataset():

Construimos la red neuronal, la compilamos e imprimimos un resumen textual de su estructura:

Definimos un callback para guardar checkpoints de los mejores modelos encontrados a lo largo del entrenamiento:

Procesaremos grupos de 128 imágenes a la vez. De una vez definimos esquemas de aumento de datos para los conjuntos de entrenamiento y validación:

Entrenamos el modelo durante 300 epochs:

Definimos el aumento de datos para el conjunto de pruebas, y evaluamos el mejor modelo (es decir, el correspondiente al checkpoint del epoch más alto):

Uff, ese programa estuvo largo, ¿no crees? Tomémonos un descanso ☕

Café

Yo en este momento 🙂

¡Ya estoy de vuelta! 

El siguiente archivo en el que trabajaremos es datasmarts/recognize.py. Ábrelo y coloca estas líneas allí para importar las dependencias:

La función rectangle_area() calcula el área de un rectángulo con base en las coordenadas de sus superior izquierda e inferior derecha:

La siguiente función auxliar (plot_emotion()) la usamos para crear un gráfico de barras con la distribución de probabilidad de las prediccciones hechas por el reconocedor de emociones:

plot_face() dibuja un rectángulo del color correspondiente a la emoción reconocida alrededor de un rostro detectado en la imagen de entrada:

predict_emotion() corre el clasificador de emociones sobre una región de interés en una imagen, asociada a un rostro hallado por el detector facial:

Habiendo implementado todas las funciones auxiliares, pasamos a definir los flags de entrada del programa:

  • -m/--model: Ruta a la carpeta donde se encuentran los checkpoints del clasificador de emociones.
  • -v/--video: Ruta al video de entrada sobre el que ejecutaremos el reconocimiento de emociones.
  • -d/--detector: Ruta al archivo de configuración del detector de rostros.

Cargamos el clasificador, en caso de existir; si no hay ningún checkpoint, imprimimos un mensaje informativo y detenemos la ejecución del programa:

Cargamos el video de entrada:

Cargamos el detector de rostros:

Iteramos sobre cada fotograma del video. Redimensionamos los frames para que el procesamiento sea más veloz; también creamos un lienzo donde iremos construyendo el diagrama de barras de las emociones:

Pasamos el detector de rostros sobre una copia en escala de grises del frame actual:

SI hay detecciones, nos quedamos con la que ocupe la mayor área. Esto lo hacemos para disminuir la posibilidad de falsos positivos:

Pasamos el clasificador de emociones sobre la región de interés asociada al rostro detectado:

Ensamblamos el gráfico de barras con la distribución de probabilidad de las emociones:

Y dibujamos la detección del color correspondiente a la emoción predominante, junto con su probabilidad:

Mostramos el resultado:

Si en algún momento el usuario presiona la tecla q, rompemos el ciclo:

Una vez agotado el video, liberamos los recursos que ocupamos:

Estamos listos para correr los scripts.

Para entrenar el clasificador, asegúrate de haber descomprimido el archivo fer2013.zip ubicado dentro de la carpeta data. Luego, corre este comando:
python datasmarts/train_model.py -d data/fer2013

Dependiendo de la capacidad de tu hardware, deberás esperar unas cuantas horas. Sin embargo, antes de empezar a entrenar, veremos la arquitectura de la red:

Como podemos notar, se trata de un modelo grandecito, compuesto de más de medio millón de parámetros.

Una vez finalizado el entrenamiento, veremos en la consola el accuracy del mejor modelo, evaluado sobre el conjunto de pruebas:

Aunque 65.35% no es para echar cohetes, veremos que es más que suficiente para crear un buen reconocedor de emociones.

Para correr el pipeline de reconocimiento de emociones, usamos este comando:
python ./datasmarts/recognize.py -v ./assets/emotions.mp4 -d ./resources/haarcascade_frontalface_default.xml

En uno de los frames, vemos que la red reconoce con claridad que la mujer se encuentra feliz:

Mujer feliz

Mujer feliz

En otro fotograma, la expresión de la modelo muestra desconcierto o preocupación, lo cual se refleja en la distribución de probabildad a mano derecha, ya que aunque la clase asustado tiene un 70.59% de probabilidad, la clase triste tiene un 19.51%.

Mujer asustada

Mujer asustada

Si queremos correr el reconocedor sobre el segundo video de ejemplo, usamos esta instrucción:

python ./datasmarts/recognize.py -v ./assets/emotions2.mp4 -d ./resources/haarcascade_frontalface_default.xml

En el fotograma de abajo vemos que la modelo claramente está molesta, y la red exitosamente la etiqueta como molesto con un apabullante 93.21% de probabilidad.

Mujer molesta

Mujer molesta

En la segunda parte de este video puedes ver el resultado completo de correr el reconocedor:

Yo diría que nuestro programa es bastante competente, ¿no te parece?

Resumen

En este artículo implementemos un reconocedor de emociones en video bastante capaz.

Empezamos parseando el conjunto de datos FER 2013 que, a diferencia de la mayoría de los datasets de imágenes, estaba en format CSV. Posteriormente, entrenamos un clasificador de emociones sobre sus imágenes, alcanzando un accuracy decente de 65.35%.

Tenemos que tener en cuenta que las expresiones faciales son complicadas de interpretar, incluso para nosotros los seres humanos. En cualquier momento, podemos mostrar emociones mixtas. Adicionalmente, existen muchas emociones con rasgos similares, como la molestia y el disgusto, o el miedo y la sorpresa, entre otras.

El último paso consistió en pasar cada frame del video a un detector de rostros implementado en OpenCV, y una vez obtenidas las caras, clasificar sus emociones mediante la red neuronal que creamos con TensorFlow.

Cabe mencionar que aunque nuestra estrategia funciona para este problema en particular, estamos asumiendo que cada frame es independiente. De manera más sencilla, tratamos cada frame del video como una imagen independiente, pero la realidad es que ese no es el caso cuando lidiamos con videos, ya que hay una dimensión adicional, el tiempo, que cuando se toma en cuenta, produce aún mejores resultados.

¿Y a ti qué te pareció el post? ¿Te gustó?

¿Por qué no descargas el proyecto y experimentas con otros videos o, mejor aún, con tu propia webcam?

¡Hasta luego!

Sobre el Autor

Jesús Martínez es el creador de DataSmarts, un lugar para los apasionados por computer vision y machine learning. Cuando no se encuentra bloggeando, jugando con algún algoritmo o trabajando en un proyecto (muy) cool, disfruta escuchar a The Beatles, leer o viajar por carretera.

Paso 2/2: Celebra tu NUEVO EMPLEO en Machine Learning ?

A %d blogueros les gusta esto: