👁️ CONTADOR DE PARPADEOS 👁️ | Python – MediaPipe Face Mesh – OpenCV

Por Administrador

¡Hola, hola Omesitos!. Bienvenidos a un nuevo tutorial en el que vamos a estar construyendo un contador de parpadeos con ayuda de una de las soluciones que habíamos visto de Mediapipe, FaceMesh. 😄

CONTENIDO:

  • Contador de Parpadeos con Mediapipe Facemesh
  • Instalación de packages
  • ¡Vamos con la programación!
  • Referencias

Contador de Parpadeos con Mediapipe Facemesh en Python

Antes de pasar con el contenido de este post, hablemos un poquito de lo que vamos a hacer.

 

Figura 1: (Izq) Mallado facial, (Der) 6 puntos que tomaremos para cada ojo.

Vamos a aplicar MediaPipe Face Mesh, de ella obtendremos 468 puntos distribuídos en el rostro de la persona detectada. De todos estos puntos únicamente tomaremos 6 del ojo izquierdo y 6 del ojo derecho. Pero ¿por qué?, bueno esto debido a que estaremos usando la ecuación de la relación de aspecto del ojo que nos provee este artículo, que por cierto encontré en el blog de Pyimagesearch, en donde abordan este mismo tema, solo que en ese post utilizan la librería dlib y nosotros emplearemos MediaPipe para obtener los puntos de referencia.

Figura 2: Eye Aspect Ratio / Relación de Aspecto del Ojo. (Fuente)

Una vez que obtengamos los 6 puntos tendremos que calcular ciertas distancias para obtener el Eye Aspect Ratio o la Relación de Aspecto del Ojo como lo vemos en la figura 2. A partir de allí tendremos que añadir una serie de procesos para realizar el conteo de parpadeos. 

Figura 3: Visualización en tiempo real de la relación de aspecto de los ojos.

Además construiremos una gráfica en tiempo real que nos mostrará los cambios que obtenemos en la relación de aspecto de los ojos, cuando estos están abiertos o cerrados, como lo podemos visualizar en la figura 3. 

Instalación de packages

Para este proyecto únicamente tendremos que usar pip install mediapipe ya que además de este módulo, se instalarán OpenCV, Numpy, Matplotlib, entre otros.

¡Vamos con la programación!

Para una explicación más detallada del programa que veremos a continuación, por favor dirígete a la serie de videos que he subido en mi canal. En total son 3 videos en los que realizó el programa paso a paso. ¡Anímate a verlos todos!. 😉

📹 👁️ CONTADOR DE PARPADEOS 👁️ (Parte 1) | Python – MediaPipe Face Mesh – OpenCV

📹 👁️ CONTADOR DE PARPADEOS 👁️ (Parte 2) | Python – MediaPipe Face Mesh – OpenCV

📹 👁️ CONTADOR DE PARPADEOS 👁️ (Parte 3) | Python – MediaPipe Face Mesh – OpenCV

Empezamos importando todos los paquetes que vamos a usar.

import cv2
import mediapipe as mp
import numpy as np
import matplotlib.pyplot as plt
from collections import deque

Luego vamos con la declaración de distintas funciones.

def drawing_output(frame, coordinates_left_eye, coordinates_right_eye, blink_counter):
     aux_image = np.zeros(frame.shape, np.uint8)
     contours1 = np.array([coordinates_left_eye])
     contours2 = np.array([coordinates_right_eye])
     cv2.fillPoly(aux_image, pts=[contours1], color=(255, 0, 0))
     cv2.fillPoly(aux_image, pts=[contours2], color=(255, 0, 0))
     output = cv2.addWeighted(frame, 1, aux_image, 0.7, 1)

     cv2.rectangle(output, (0, 0), (200, 50), (255, 0, 0), -1)
     cv2.rectangle(output, (202, 0), (265, 50), (255, 0, 0),2)
     cv2.putText(output, "Num. Parpadeos:", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
     cv2.putText(output, "{}".format(blink_counter), (220, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (128, 0, 250), 2)
     
     return output

La función drawing_output nos permitirá aplicar cierto color al área de los ojos, y además la visualización de los resultados del conteo.

def eye_aspect_ratio(coordinates):
     d_A = np.linalg.norm(np.array(coordinates[1]) - np.array(coordinates[5]))
     d_B = np.linalg.norm(np.array(coordinates[2]) - np.array(coordinates[4]))
     d_C = np.linalg.norm(np.array(coordinates[0]) - np.array(coordinates[3]))

     return (d_A + d_B) / (2 * d_C)

La siguiente función es eye_aspect_ratio que es la que nos ayudará a calcular las 3 distancias (figura 4) que intervienen en el cálculo de la relación de aspecto de ojo, y nos devolverá el resultado de la ecuación descrita en la figura 2.

Figura 4: Distancias verticales y horizontal, necesarias para el cálculo de la relación de aspecto del ojo

def plotting_ear(pts_ear, line1):
     global figure
     pts = np.linspace(0, 1, 64)
     if line1 == []:
          plt.style.use("ggplot")
          plt.ion()

          figure, ax = plt.subplots()
          line1, = ax.plot(pts, pts_ear)
          plt.ylim(0.1, 0.4)
          plt.xlim(0, 1)
          plt.ylabel("EAR", fontsize=18)
     else:
          line1.set_ydata(pts_ear)
          figure.canvas.draw()
          figure.canvas.flush_events()
     
     return line1

Finalmente tenemos a la función plotting_ear, la cual nos ayudará a generar la gráfica de la relación de aspecto de los ojos en tiempo real.

Una vez que contamos con estas funciones, vamos con el programa principal.

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

mp_face_mesh = mp.solutions.face_mesh
index_left_eye = [33, 160, 158, 133, 153, 144]
index_right_eye = [362, 385, 387, 263, 373, 380]
EAR_THRESH = 0.26
NUM_FRAMES = 2
aux_counter = 0
blink_counter = 0
line1 = []
pts_ear = deque(maxlen=64)
i = 0

with mp_face_mesh.FaceMesh(
     static_image_mode=False,
     max_num_faces=1) as face_mesh:

     while True:
          ret, frame = cap.read()
          if ret == False:
               break
          frame = cv2.flip(frame, 1)
          height, width, _ = frame.shape
          frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
          results = face_mesh.process(frame_rgb)

          coordinates_left_eye = []
          coordinates_right_eye = []

          if results.multi_face_landmarks is not None:
               for face_landmarks in results.multi_face_landmarks:
                    for index in index_left_eye:
                         x = int(face_landmarks.landmark[index].x * width)
                         y = int(face_landmarks.landmark[index].y * height)
                         coordinates_left_eye.append([x, y])
                         cv2.circle(frame, (x, y), 2, (0, 255, 255), 1)
                         cv2.circle(frame, (x, y), 1, (128, 0, 250), 1)
                    for index in index_right_eye:
                         x = int(face_landmarks.landmark[index].x * width)
                         y = int(face_landmarks.landmark[index].y * height)
                         coordinates_right_eye.append([x, y])
                         cv2.circle(frame, (x, y), 2, (128, 0, 250), 1)
                         cv2.circle(frame, (x, y), 1, (0, 255, 255), 1)

En primer lugar indicamos que vamos a estar realizando un videostreaming. Luego declaramos algunas constantes y variables que nos servirán para el conteo de parpadeos, como el número mínimo de fotogramas para los ojos cerrados, el umbral a comparar con la relación de aspecto del ojo, el contador propiamente y también los puntos que estaremos tomando para los ojos izquierdo y derecho de la malla facial, a estos los tenemos en las líneas 51 y 52.

En la línea 61, establecemos las opciones de configuración que usaremos para el mallado facial con Mediapipe, y procedemos a leer los fotogramas que tendremos que transformar de BGR a RGB para aplicar esta solución como tal.

Entonces tendremos que asegurarnos de que obtengamos datos de la malla facial, es decir que se haya podido detectar algún rostro, y que de este se hayan extraído sus puntos. Si es así tomamos las 6 coordenadas x e y de cada ojo.

               ear_left_eye = eye_aspect_ratio(coordinates_left_eye)
               ear_right_eye = eye_aspect_ratio(coordinates_right_eye)
               ear = (ear_left_eye + ear_right_eye)/2

               # Ojos cerrados
               if ear < EAR_THRESH:
                    aux_counter += 1
               else:
                    if aux_counter >= NUM_FRAMES:
                         aux_counter = 0
                         blink_counter += 1                
               frame = drawing_output(frame, coordinates_left_eye, coordinates_right_eye, blink_counter)
               pts_ear.append(ear)
               if i > 70:
                    line1 = plotting_ear(pts_ear, line1)
               i +=1
               #print("pts_ear:", pts_ear)
          cv2.imshow("Frame", frame)
          k = cv2.waitKey(1) & 0xFF
          if k == 27:
               break
cap.release()
cv2.destroyAllWindows()

Finalmente tendremos que calcular la relación de aspecto de los ojos, compararlos con el umbral que habíamos establecido previamente, identificar el número de frames en que los ojos se encuentran cerrados y detectar el parpadeo para visualizarlo en el contador. Esta sección usa todas las funciones que habíamos declarado en primer lugar.

Figura 5: Conteo de parpadeos y gráfica de la relación de aspacto de los ojos en tiempo real sobre un videostreaming.

Y bien, esto ha sido todo por el tutorial de hoy. ¡Espero que te haya gustado! 🙃 Nos vemos en el siguiente…

Referencias:

📎 http://vision.fe.uni-lj.si/cvww2016/proceedings/papers/05.pdf

📎 https://pyimagesearch.com/2017/04/24/eye-blink-detection-opencv-python-dlib/