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:
- 1Un detector de rostros: Usaremos el clásico detector de rostros Haar Cascade implementado en OpenCV.
- 2Un clasificador de emociones: Implementaremos nuestro propio clasificador de emociones en TensorFlow.
Al final de este artículo habrás aprendido:
¿Estás listo? ¡Comencemos!
Cómo Funciona un Detector de Emociones
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í:
- 1Toma una imagen de entrada y la pasa por el detector de rostros.
- 2Cada rostro detectado es pasado por el clasificador de emociones.
- 3Con 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.
- 4Con 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
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: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 ☕
¡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:
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:
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%.
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.
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!