-
Notifications
You must be signed in to change notification settings - Fork 0
Progreso febrero 2022
SEMANA 1 Y 2 (2/02/2022 - 16/02/2022)
- Librería dlib
- Threads para mejorar latencia
- Librería dlib con PiVideoStream (test 4 dlib)
- MediaPipe
- MediaPipe vs dlib
SEMANA 3 (16/02/2022 - 23/02/2022)
- Optimización 1 (uso de threads)
- Optimización 2 (cambio del dibujo de la malla)
- Optimización 3 (no dibujar la malla)
SEMANA 4 (23/02/2022 - 3/03/2022)
- Estudio de los rasgos faciales principales en una expresión
- Creación del dataset
- Primer entrenamiento
Objetivo: buscar y analizar que algoritmos de reconocimiento de puntos faciales son más rápidos y eficientes en una Raspberry Pi 4.
Esta librería contiene un detector de puntos faciales preentrenado capaz de detectar 68 puntos de referencia.
El procedimiento se divide en dos fases:
- Detección de la cara (HOG + Linear SVM o MMOD CNN)
- Detección de los puntos de referencia (Árboles de regresión, más info aquí)
A continuación realizaré unos test para comprobar el rendimiento de este algoritmo en nuestra Raspberry Pi 4. Para ello programaré unos scripts en los que ejecutaré el algoritmo y calcularé la media de FPS para mostrarlo por pantalla en tiempo de ejecución. A su vez, si el script se ejecuta con el flag --savefps, se guardarán los datos de FPS en un fichero .csv durante el tiempo indicado en el argumento --time. El argumento --savebugs lo explicaré más adelante.
usage: test1.py [-h] [--savefps] [--savebugs] [--time TIME]
optional arguments:
-h, --help show this help message and exit
--savefps Activa el guardado de fps en fichero .csv
--savebugs Activa el guardado de fallos del algoritmo en fichero .csv
--time TIME Duracion en segundos del guardado de datos (int). Default: 30s
Un ejemplo de fichero es fps_dlib_test3.csv, como podemos ver los datos se guardan de la forma: fps_average, execution_time
Además he creado un script plotDataFPS.py que dibuja los datos de estos ficheros .csv en una gráfica, de esta manera podremos comparar el rendimiento de los distintos test.
El script recibe como argumento los ficheros .csv a plottear (mínimo 1)
plotData.py file1.csv [file2.csv ...]
Sin agregar nada de carga computacional, sólo mostrando la imagen recibida de la picamera por pantalla usando opencv, obtenemos una media de unos 20fps con picos de 30fps. Por lo tanto partimos desde esa cifra y estudiaremos ahora cuánto perdemos.
El código de este test se puede encontrar en test1.py
Aquí realizaré una prueba general utilizando dlib tal como se recomienda, usando el detector de caras incorporado y el detector de puntos faciales incorporado. Como detector de caras dlib te da a escoger entre uno de los siguientes métodos:
- HOG + Linear SVM: dlib.get_frontal_face_detector()
- MMOD CNN: dlib.cnn_face_detection_model_v1(modelPath)
Yo escogeré el primero de los dos porque es el más eficiente computacionalmente (aunque sea menos preciso). Y por último usamos dlib.shape_predictor(modelPath) como detector de puntos faciales.
Si ejecutamos lo mencionado anteriormente obtenemos una media de 0.6fps y podemos ver que le cuesta mucho arrancar.
Partir desde este punto para seguir desarrollando nuestro detector de expresiones me parecía desastroso porque ya no conseguíamos que esto corriera en tiempo real, entonces intenté mejorar su desempeño.
El código de este test se puede encontrar en test2.py
Estudiando qué es lo que relentizaba el funcionamiento del test1, descubrí que era el detector de caras de dlib. El detector de puntos faciales apenas merbaba el rendimiento del programa. Por lo tanto decidí cambiar dicho detector de caras por otro más rápido, el que viene incorporado en OpenCv (Haar cascades)
Haciendo este cambio en el script ya conseguí un rendimiento de 2.5fps de media. A continuación podemos ver una comparativa entre el test 1 y 2.
A parte de no tener un buen rendimiento computacionalmente hablando, tenía muchos fallos en las detecciones.
El código de este test se puede encontrar en test3.py
Jugando con los parámetros de la función detectMultiScale() del detector de caras de OpenCv me di cuenta de que podía mejorar todavía más el rendimiento de nuestra aplicación. Cambiando el scaleFactor de 1.05 a 1.2 conseguí multiplicar por 2 el rendimiento, obteniendo una media de 5.5fps. Además se detectaban menos falsos positivos.
Aquí podemos ver una comparativa entre el test 1, 2 y 3.
6fps como máximo me parecían muy pocos, y sobre todo me parecía poco que únicamente mostrando la imagen por pantalla el rendimiento del programa fuese de 20fps de media. Entonces, investigando por internet descubrí que se podían usar threads para mejorar la latencia leyendo fotogramas de la PiCamera, aumentando así los fps. Más información en este tutorial.
El tutorial indica como crear una clase en la que se encapsula toda la lógica, la clase será PiVideoStream que tendrá los siguientes métodos:
- start(): lanza el thread para leer frames
- read(): devuelve el último frame leído
- stop(): detiene el thread
Gracias a esta técnica, ahora sin carga computacional conseguimos partir de una media de 90fps. A continuación podemos ver una comparativa entre usar la técnica y no, observamos una gran diferencia.
Hemos comprobado en el apartado anterior que leer frames usando threads mejora la tasa de fotogramas por segundo en crudo, lo incorporaremos ahora en el último test de dlib para ver si existe una mejora.
El código se encuentra en el test4.py
En la siguiente gráfica podemos ver una comparativa entre el test 3 usando la PiCamera de la forma tradicional y el test 4 leyendo con threads. Observamos que el rendimiento mejora de 5.5fps a 7fps.
En este apartado estudiaré el funcionamiento de MediaPipe Face Mesh (más información aquí) en la Raspberry Pi 4. Este método es capaz de estimar 468 puntos 3D en la cara.
Para instalar mediapipe en una Raspberry Pi 4 he tenido que instalar una versión especial.
pip3 install mediapipe-rpi4
El código de este test se puede encontrar en test1.py
Aquí realizaré una prueba general tal cómo se explica en el tutorial de la página oficial de Google. Podemos ver que obtenemos un rendimiento de unos 5.5fps de media, pero una detección de puntos faciales muy robusta.
Ahora probaré el mismo código pero usando la clase PiVideoStream para leer los frames de la Picamera, podemos ver que el rendimiento aumenta un poco. El código está en test2.py.
Hemos analizado las dos librerías más importantes de reconocimiento de puntos faciales y hemos sacado en claro que los test4 (dlib) y test2 (MediaPipe) son los que mejores resultados en cuanto a rendimiento han obtenido. En este apartado los enfrentaremos para ver con cual de ellos finalmente nos quedamos.
Comparando el rendimiento de ambos podemos ver que el de MediaPipe es ligeramente superior (1 o 2 fps por arriba)
En este apartado partiremos de la hipótesis de que siempre hay una cara en la imagen, entonces compararemos los siguientes fallos:
- Falsos positivos (sólo puede haber una detección)
- No detección de ningún rostro (tiene que haber uno)
Para ello los scripts de test tienen un flag --savebugs que lo que activará será guardar en un fichero .csv un 1 si en dicho frame se encuentra algún fallo de los descritos anteriormente o un 0 si no hay ningún fallo, esto lo realizará a lo largo del tiempo.
En el test de MediaPipe sólo tendré en cuenta el segundo fallo de los dos, porque el primero se puede controlar indicando que únicamente detecte 1 cara.
usage: test1.py [-h] [--savefps] [--savebugs] [--time TIME]
optional arguments:
-h, --help show this help message and exit
--savefps Activa el guardado de fps en fichero .csv
--savebugs Activa el guardado de fallos del algoritmo en fichero .csv
--time TIME Duracion en segundos del guardado de datos (int). Default: 30s
Analizaremos los algoritmos en las siguientes situaciones:
- Buenas condiciones lumínicas
- Malas condiciones lumínicas
- Rostros parcialmente cubiertos
- Rostros girados
Podemos observar que MediaPipe sale claramente victorioso en todas las situaciones.
Objetivo: aprovechar los 4 núcleos de la Raspberry Pi 4 usando threads y de esta manera aumentar el rendimiento de MediaPipe.
Actualmente nuestro programa está compuesto por dos hilos. El primero de ellos se encarga de leer frames de la PiCamera y el segundo se encarga de todo lo demás en nuestro main.
La nueva propuesta de mejora es crear otro hilo que se encargue de toda la interfaz gráfica, para ello he creado la clase GraphicInterface.py que tiene los siguientes métodos:
- start(): lanza el hilo que se encargará de mostrar por pantalla la interfaz gráfica continuamente.
- put_image(image): incluye la imagen pasada como argumento en la interfaz gráfica.
- put_text(text, location, color, size, thickness): incluye el texto pasado como argumento en la interfaz gráfica con los parámetros de configuración que se indican.
- stop(): para la ejecución de la interfaz gráfica
El nuevo código de este test se encuentra en test3.py.
Veamos ahora una comparativa entre el último test 2 y este nuevo test 3 con la mejora del hilo para la interfaz gráfica.
Al ver los resultados nos llevamos una gran sorpresa viendo que el rendimiento disminuye, cosa que ahora mismo no logro entender.
Otra de las propuestas de mejora es crear un hilo que se encargue del procesamiento de la malla de la cara, cosa que en esta semana no he conseguido y no sé si finalmente se podrá conseguir.
Viendo este panorama en el que en vez de mejorar el rendimiento lo hemos disminuido, me puse a buscar posibles mejoras.
La primera optimización consiste en buscar la forma de dibujar la malla de la cara que menos recursos consuma. Para ello además creé una clase FaceMesh en la que encapsularemos todo el código referente a la malla de la cara a partir de ahora. El código final de esta optimización lo podéis encontrar en test4.py
Veamos ahora una comparativa entre el test 2 y este nuevo test 4 con la optimizaión del dibujo de la malla de la cara.
Podemos ver que el rendimiento ha aumentado con esta nueva representación de la malla más liviana.
Pensando en el futuro esta sería una clara optimización, ya que nuestro objetivo es mostrar una predicción de la expresión de la cara de la persona y no la malla.
Veamos cuánta mejora hay comparándolo con la optimización anterior.
Vemos una mejora de unos 2 fps, aunque esperaba una mejora superior.
Objetivo: entrenamiento de un modelo de SVM que sea capaz de detectar expresiones faciales basándose en la información obtenida de la malla de MediaPipe.
Empezaré estudiando cuáles son los rasgos faciales que más influyen a la hora de expresar emociones con la cara, para ello nos ayudaremos de la siguiente imagen.
Fuente: ABC
Nuestro entrenamiento se va a basar en la distancia entre puntos faciales característicos, por lo tanto debemos observar cuáles son los puntos de toda la malla que más nos ayudarán en esta tarea. Al ejecutarse en un sistema de bajo coste no podemos elegir todos los puntos (que sería los más óptimo) porque tendríamos que calcular en cada iteración 219024 distancias.
Observando la imagen obtenemos la siguiente información útil:
- Cara 1 (sorpresa): cejas en forma de "U" y muy separadas de los ojos, nariz estirada, no hay arrugas en la zona de los pómulos, la boca está abierta y los ojos también están muy abiertos.
- Cara 2 (enfado): cejas muy cercanas a los ojos y muy cercanas entre ellas, hay mucha frente, los ojos no están tan abiertos, existen arrugas encima de la boca, la nariz está mas alargada horizontalmente, la boca tiene una forma característica.
- Cara 3 (disgusto): ojos casi cerrados, nariz encogida, arrugas encima de la boca características, boca medio abierta con forma característica.
- Cara 4 (alegría): ojos bastante abiertos, arrugas encima de la boca características, boca sonriente.
Procesando las información anterior, concluimos con que necesitamos obtener las coordenadas de las siguientes zonas de la cara:
- Cejas
- Ojos
- Nariz
- Labios
- Arrugas de los pómulos
Lo primero que hice fue buscar un mapa que contenga todos los índices de cada uno de los puntos de la malla, encontré el siguiente en el repositorio oficial de MediaPipe
Es un mapa muy denso con los 468 puntos, por lo tanto diseñaré mi propia versión del mapa con los índices esenciales que necesitaremos. Observando las fotos en las que yo mismo tengo puesta la malla, consigo diferenciar cuáles de los puntos del mapa son los que identifican las zonas de la cara buscadas.
Gracias a este nuevo mapa personalizado tendremos a nuestra disposición, de una forma fácil y rápida, cada uno de los índices que necesitaremos. Acabaré probando con un script a dibujar cada uno de esos puntos en sus respectivas coordenadas para comprobar que he hecho bien el mapa. El script es testIndexMediapipe.py.
El método que estamos siguiendo se basa en usar como datos de entrenamiento las distancias entre puntos faciales, por lo tanto debemos crear nuestro propio dataset en el que incluyamos dichos datos. Para ello partiremos de un dataset de imagénes con distintas expresiones faciales, este será FER-2013.
Empezaré creando un mapa visual como el del apartado anterior en el que representaré las distintas distancias que he elegido para entrenar nuestro modelo. Esto nos facilitará el trabajo al programar el script que genere nuestro dataset.
Podemos ver que hemos pasado de tener que calcular 219024 distancias a calcular únicamente 32, esto aumentará notablemente el rendimiento.
Ahora que ya tenía la idea planteada me puse manos a la obra con el script que generá nuestro dataset, este será datasetGeneratorFER.py. Lo que realizará será recorrerse los distintos directorios del dataset FER-2013 y procesar cada una de las imágenes obteniendo cada una de las 32 distancias mencionadas anteriormente y guardándolas en dataset.csv con sus respectivas clases.
El formato del fichero .csv será el siguiente:
distancia1,distancia2,distancia3,...,distancia32,clase
Las distancias estarán ordenadas de la siguiente manera:
- Ceja izquierda: distancia1 - distancia5
- Ceja derecha: distancia6 - distancia10
- Ojo izquierdo: distancia11 - distancia12
- Ojo derecho: distancia13 - distancia14
- Arruga izquierda: distancia15 - distancia19
- Arruga derecha: distancia20 - distancia24
- Boca exterior: distancia25 - distancia29
- Boca interior: distancia30 - distancia32
Las clases estarán numeradas de la siguiente manera:
- Angry: 0
- Fear: 1
- Happy: 2
- Neutral: 3
- Sad: 4
- Surprise: 5
Como último apunte: para generar dataset.csv he procesado las imágenes de la carpeta "train" de FER-2013 porque he pensado que con esa cantidad ya sería necesario, posteriormente ya las dividiré en "train" y "test". Además no he usado las imágenes de "disgust" porque había muy pocas y eso iba a dar problemas a la hora de entrenar nuestros modelos.
Nuestro primer dataset finalmente entonces estará constituido por 24429 muestras con 32 características cada una de ellas y 6 posibles clases cómo salida.
X1 X2 X3 X4 ... X30 X31 X32 y
0 0.483621 0.454554 0.415042 0.358517 ... 0.001151 0.001953 0.001074 0.0
1 0.398594 0.384428 0.355201 0.310171 ... 0.002006 0.001670 0.001179 0.0
2 0.366459 0.377436 0.372099 0.348960 ... 0.001562 0.001250 0.002051 0.0
3 0.510924 0.505214 0.479804 0.435491 ... 0.001836 0.001739 0.001730 0.0
4 0.520141 0.510795 0.482674 0.438302 ... 0.112723 0.139697 0.113980 0.0
... ... ... ... ... ... ... ... ... ...
24424 0.620357 0.614406 0.579932 0.517440 ... 0.002563 0.004015 0.003343 5.0
24425 0.481895 0.471618 0.446489 0.404800 ... 0.049052 0.062796 0.045624 5.0
24426 0.411678 0.411802 0.392324 0.353545 ... 0.169462 0.219663 0.173645 5.0
24427 0.539622 0.529736 0.502103 0.457206 ... 0.006736 0.008460 0.006759 5.0
24428 0.416384 0.411002 0.388876 0.349536 ... 0.132737 0.159057 0.132353 5.0
[24429 rows x 33 columns]
Una vez creado nuestro dataset ya estamos listos para hacer nuestros primeros entrenamientos, en esta primera aproximación he usado las siguientes herramientas:
- Librería de Python: sklearn
- Método de aprendizaje automático: SVM (kernel rbf)
El script en el que llevo a cabo el entrenamiento es train.py. El dataset lo divido en: 80% datos para entrenamiento, 20% para datos de test.
# Matriz de confusión
[[ 92 14 148 284 85 45]
[ 37 39 106 260 116 114]
[ 18 15 938 225 73 37]
[ 13 21 69 662 87 24]
[ 15 19 119 371 245 22]
[ 20 16 54 108 20 355]]
precision recall f1-score support
0 0.47 0.14 0.21 668
1 0.31 0.06 0.10 672
2 0.65 0.72 0.68 1306
3 0.35 0.76 0.48 876
4 0.39 0.31 0.35 791
5 0.59 0.62 0.61 573
accuracy 0.48 4886
macro avg 0.46 0.43 0.40 4886
weighted avg 0.48 0.48 0.44 4886
Podemos observar que hemos tenido unos resultados bastante malos (accuracy = 48%), a partir de ahora deberemos pensar en soluciones para mejorar el porcentaje de acierto.