7.3. Hello BlazeFace

BlazeFace é uma rede neural de detecção de faces da coleção MediaPipe do Google. Uma única chamada de inferência retorna um retângulo delimitador em torno de cada face detectada, junto com seis marcos faciais – olho direito, olho esquerdo, nariz, boca, orelha direita, orelha esquerda. Toda OpenMV Cam que vem com suporte a redes neurais carrega o modelo blazeface_front_128.tflite na flash, então executar um detector de faces de ponta a ponta leva algumas linhas de Python.

7.3.1. O 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))

Esse é o detector de faces inteiro. Não há mais nada nele; o script captura um quadro, o entrega ao modelo, percorre a lista de detecções retornada e desenha o retângulo delimitador de cada face mais seus seis marcos de volta no quadro. A pré-visualização da IDE mostra as caixas e os marcos em tempo real.

7.3.2. O que cada linha faz

As três primeiras linhas importam os módulos de que o script precisa. csi é a interface do sensor da câmera; ml é o módulo de aprendizado de máquina sobre o qual trata o restante deste capítulo; BlazeFace é o pós-processador que transforma os tensores de saída brutos do BlazeFace na lista de caixas delimitadoras e marcos que o script itera.

As cinco linhas seguintes configuram o sensor. A câmera é resetada para um estado conhecido, definida para a cor RGB565, definida para a resolução VGA e então janelada para um quadrado de 400 por 400. A janela importa: o BlazeFace foi treinado com recortes quadrados, e dar a ele uma entrada quadrada alinha a proporção esperada pela rede com o que ela vê no quadro capturado.

A linha de carregamento do modelo abre o arquivo do modelo:

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

ml.Model lê o arquivo no caminho fornecido – /rom/ é um sistema de arquivos residente na flash coberto mais adiante – e retorna um objeto de modelo contra o qual o script executará inferências. A palavra-chave postprocess= registra o pós-processador BlazeFace; sem ela, predict retornaria os tensores de saída brutos da rede e a aplicação teria que decodificá-los manualmente. Com ela, predict retorna o resultado decodificado diretamente. O argumento threshold=0.4 no pós-processador define a confiança mínima que a rede deve relatar antes que uma detecção seja mantida; valores menores capturam faces mais fracas ao custo de mais falsos positivos.

As quatro linhas restantes são o laço principal. Cada passagem por ele captura um quadro e pergunta ao modelo o que ele vê:

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() recebe uma lista de entradas (aqui, uma imagem capturada) e retorna uma lista de tuplas de detecção. Cada tupla contém o retângulo delimitador (x, y, w, h), uma confiança score entre zero e um, e um ndarray (6, 2) de coordenadas de marcos – o olho direito, olho esquerdo, nariz, boca, orelha direita e orelha esquerda nessa ordem. A chamada de desenho usa draw_rectangle() – a mesma primitiva com que todo detector clássico no capítulo de imagem terminou – para contornar a face. ml.utils.draw_keypoints() é um pequeno auxiliar dos utilitários de ml que marca cada ponto-chave com uma cruz em sua posição (x, y).

7.3.3. O que o script não diz

O script tem sete linhas executáveis de trabalho de inferência além das importações e da configuração do sensor, mas uma grande quantidade de aritmética acontece dentro dessas sete linhas. O quadro RGB565 de 400 por 400 capturado se torna um tensor quantizado de 8 bits de 128 por 128 antes de chegar à rede; a rede executa centenas de operações contra dezenas de milhares de pesos; os tensores resultantes de pontuações de confiança e deslocamentos de caixa se tornam uma lista ordenada de caixas delimitadoras não sobrepostas com marcos anexados antes que predict retorne. Cada uma dessas transformações é algo que a aplicação pode controlar se precisar, e várias delas têm que ser ajustadas para qualquer modelo que não seja o padrão.

As quatro subseções seguintes destrincham essas transformações. Em ordem:

  • O módulo ml – o que ml.Model expõe assim que um modelo é carregado, e onde o arquivo do modelo de fato reside na câmera.

  • O pipeline de inferência – os quatro estágios de toda chamada de predict().

  • Motores de inferência – os caminhos de CPU e NPU que executam a aritmética da rede.

  • Decodificando a saída – os pós-processadores que convertem os tensores de saída brutos nas detecções que este script iterou.

Ao final do capítulo, o leitor consegue escrever o script equivalente para um modelo que não veio com a câmera, decodificar um tensor cujo pós-processador ainda não existe, e raciocinar sobre por que um modelo específico roda a 30 FPS em uma câmera e a 3 FPS em outra.