7.3. Hola BlazeFace

BlazeFace es una red neuronal de detección de rostros de la colección MediaPipe de Google. Una sola llamada de inferencia devuelve un rectángulo delimitador alrededor de cada rostro detectado junto con seis puntos de referencia faciales: ojo derecho, ojo izquierdo, nariz, boca, oreja derecha, oreja izquierda. Toda OpenMV Cam que se entrega con soporte para redes neuronales lleva el modelo blazeface_front_128.tflite en la memoria flash, de modo que ejecutar un detector de rostros de principio a fin requiere unas pocas líneas de Python.

7.3.1. El script completo

import csi
import ml
from ml.postprocessing.mediapipe import BlazeFace

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.VGA)
csi0.window((400, 400))

model = ml.Model("/rom/blazeface_front_128.tflite",
                 postprocess=BlazeFace(threshold=0.4))

while True:
    img = csi0.snapshot()
    for (x, y, w, h), score, keypoints in model.predict([img]):
        img.draw_rectangle((x, y, w, h), color=(0, 255, 0))
        ml.utils.draw_keypoints(img, keypoints, color=(255, 0, 0))

Ese es el detector de rostros completo. No hay nada más en él; el script captura un fotograma, se lo entrega al modelo, recorre la lista de detecciones devuelta y dibuja el rectángulo delimitador de cada rostro junto con sus seis puntos de referencia de nuevo en el fotograma. La vista previa del IDE muestra los cuadros y los puntos de referencia en tiempo real.

7.3.2. Qué hace cada línea

Las tres primeras líneas importan los módulos que el script necesita. csi es la interfaz del sensor de la cámara; ml es el módulo de aprendizaje automático sobre el que trata el resto de este capítulo; BlazeFace es el posprocesador que convierte los tensores de salida en bruto de BlazeFace en la lista de cuadros delimitadores y puntos de referencia que el script itera.

Las cinco líneas siguientes configuran el sensor. La cámara se reinicia a un estado conocido, se establece en color RGB565, se establece en resolución VGA y luego se enventana a un cuadrado de 400 por 400. El enventanado importa: BlazeFace se entrenó con recortes cuadrados, y darle una entrada cuadrada alinea la relación de aspecto que la red espera con lo que ve en el fotograma capturado.

La línea de carga del modelo abre el archivo del modelo:

model = ml.Model("/rom/blazeface_front_128.tflite",
                 postprocess=BlazeFace(threshold=0.4))

ml.Model lee el archivo en la ruta indicada (/rom/ es un sistema de archivos residente en flash que se cubre más adelante) y devuelve un objeto de modelo contra el que el script ejecutará inferencias. La palabra clave postprocess= registra el posprocesador de BlazeFace; sin ella, predict devolvería los tensores de salida en bruto de la red y la aplicación tendría que decodificarlos a mano. Con ella, predict devuelve el resultado decodificado directamente. El argumento threshold=0.4 del posprocesador establece la confianza mínima que la red debe informar antes de conservar una detección; valores más bajos captan rostros más tenues a costa de más falsos positivos.

Las cuatro líneas restantes son el bucle principal. Cada pasada por él captura un fotograma y le pregunta al modelo qué ve:

img = csi0.snapshot()
for (x, y, w, h), score, keypoints in model.predict([img]):
    img.draw_rectangle((x, y, w, h), color=(0, 255, 0))
    ml.utils.draw_keypoints(img, keypoints, color=(255, 0, 0))

predict() toma una lista de entradas (aquí, una imagen capturada) y devuelve una lista de tuplas de detección. Cada tupla contiene el rectángulo delimitador (x, y, w, h), una confianza score entre cero y uno, y un ndarray de (6, 2) con las coordenadas de los puntos de referencia: el ojo derecho, el ojo izquierdo, la nariz, la boca, la oreja derecha y la oreja izquierda en ese orden. La llamada de dibujo usa draw_rectangle() (la misma primitiva con la que terminaba cada detector clásico del capítulo de imagen) para delinear el rostro. ml.utils.draw_keypoints() es un pequeño ayudante de las utilidades de ml que marca cada punto clave con una cruz en su posición (x, y).

7.3.3. Lo que el script no dice

El script consta de siete líneas ejecutables de trabajo de inferencia más allá de las importaciones y la configuración del sensor, pero dentro de esas siete líneas ocurre una gran cantidad de aritmética. El fotograma RGB565 de 400 por 400 capturado se convierte en un tensor cuantizado de 8 bits de 128 por 128 antes de llegar a la red; la red ejecuta cientos de operaciones contra decenas de miles de pesos; los tensores resultantes de puntuaciones de confianza y desplazamientos de cuadros se convierten en una lista clasificada de cuadros delimitadores no solapados con puntos de referencia adjuntos antes de que predict retorne. Cada una de esas transformaciones es algo que la aplicación puede controlar si lo necesita, y varias de ellas tienen que ajustarse para cualquier modelo que no sea el predeterminado.

Las cuatro subsecciones siguientes desarrollan esas transformaciones. En orden:

  • El módulo ml: qué expone ml.Model una vez cargado un modelo, y dónde reside realmente el archivo del modelo en la cámara.

  • El pipeline de inferencia: las cuatro etapas de cada llamada a predict().

  • Los motores de inferencia: las rutas de CPU y NPU que ejecutan la aritmética de la red.

  • Decodificación de la salida: los posprocesadores que convierten los tensores de salida en bruto en las detecciones que este script iteró.

Al final del capítulo el lector puede escribir el script equivalente para un modelo que no venía con la cámara, decodificar un tensor cuyo posprocesador todavía no existe, y razonar sobre por qué un modelo en concreto se ejecuta a 30 FPS en una cámara y a 3 FPS en otra.