7.3. Bonjour BlazeFace

BlazeFace est un réseau de neurones de détection de visages issu de la collection MediaPipe de Google. Un seul appel d’inférence renvoie un rectangle englobant autour de chaque visage détecté ainsi que six points de repère faciaux – œil droit, œil gauche, nez, bouche, oreille droite, oreille gauche. Chaque OpenMV Cam livrée avec la prise en charge des réseaux de neurones embarque le modèle blazeface_front_128.tflite en mémoire flash, si bien qu’exécuter un détecteur de visages de bout en bout ne prend que quelques lignes de Python.

7.3.1. Le script complet

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))

Voilà tout le détecteur de visages. Il n’y a rien de plus ; le script capture une trame, la transmet au modèle, parcourt la liste de détections renvoyée et redessine pour chaque visage le rectangle englobant ainsi que ses six points de repère dans la trame. L’aperçu de l’IDE affiche les boîtes et les points de repère en temps réel.

7.3.2. Ce que fait chaque ligne

Les trois premières lignes importent les modules dont le script a besoin. csi est l’interface du capteur de la caméra ; ml est le module d’apprentissage automatique dont traite le reste de ce chapitre ; BlazeFace est le post-processeur qui transforme les tenseurs de sortie bruts de BlazeFace en la liste de boîtes englobantes et de points de repère que le script parcourt.

Les cinq lignes suivantes configurent le capteur. La caméra est réinitialisée à un état connu, réglée sur la couleur RGB565, réglée sur la résolution VGA, puis fenêtrée sur un carré de 400 par 400. Le fenêtrage compte : BlazeFace a été entraîné sur des recadrages carrés, et lui fournir une entrée carrée aligne le rapport d’aspect attendu par le réseau avec ce qu’il voit dans la trame capturée.

La ligne de chargement du modèle ouvre le fichier du modèle :

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

ml.Model lit le fichier au chemin indiqué – /rom/ est un système de fichiers résidant en mémoire flash, traité plus loin – et renvoie un objet modèle sur lequel le script lancera des inférences. Le mot-clé postprocess= enregistre le post-processeur BlazeFace ; sans lui, predict renverrait les tenseurs de sortie bruts du réseau et l’application devrait les décoder à la main. Avec lui, predict renvoie directement le résultat décodé. L’argument threshold=0.4 du post-processeur fixe la confiance minimale que le réseau doit indiquer avant qu’une détection soit conservée ; des valeurs plus basses captent des visages plus faibles au prix de davantage de faux positifs.

Les quatre lignes restantes constituent la boucle principale. Chaque passage capture une trame et demande au modèle ce qu’il voit :

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() prend une liste d’entrées (ici, une seule image capturée) et renvoie une liste de tuples de détection. Chaque tuple contient le rectangle englobant (x, y, w, h), un score de confiance score compris entre zéro et un, et un ndarray (6, 2) de coordonnées de points de repère – l’œil droit, l’œil gauche, le nez, la bouche, l’oreille droite et l’oreille gauche dans cet ordre. L’appel de dessin utilise draw_rectangle() – la même primitive sur laquelle se terminait chaque détecteur classique du chapitre sur l’image – pour tracer le contour du visage. ml.utils.draw_keypoints() est un petit utilitaire des outils ml qui marque chaque point clé d’une croix à sa position (x, y).

7.3.3. Ce que le script ne dit pas

Le script représente sept lignes exécutables de travail d’inférence au-delà des imports et de la configuration du capteur, mais une grande quantité de calculs se déroule à l’intérieur de ces sept lignes. La trame RGB565 de 400 par 400 capturée devient un tenseur 8 bits quantifié de 128 par 128 avant d’atteindre le réseau ; le réseau exécute des centaines d’opérations sur des dizaines de milliers de poids ; les tenseurs résultants de scores de confiance et de décalages de boîtes deviennent une liste classée de boîtes englobantes non chevauchantes assorties de points de repère avant que predict ne retourne. Chacune de ces transformations est quelque chose que l’application peut contrôler si elle en a besoin, et plusieurs d’entre elles doivent être ajustées pour tout modèle autre que celui par défaut.

Les quatre sous-sections suivantes décortiquent ces transformations. Dans l’ordre :

  • Le module ml – ce que ml.Model expose une fois un modèle chargé, et où le fichier de modèle réside réellement sur la caméra.

  • La chaîne d’inférence – les quatre étapes de chaque appel predict().

  • Les moteurs d’inférence – les chemins CPU et NPU qui exécutent l’arithmétique du réseau.

  • Le décodage de la sortie – les post-processeurs qui convertissent les tenseurs de sortie bruts en les détections que ce script a parcourues.

À la fin du chapitre, le lecteur sait écrire le script équivalent pour un modèle qui n’était pas livré avec la caméra, décoder un tenseur dont le post-processeur n’existe pas encore, et comprendre pourquoi un modèle donné tourne à 30 FPS sur une caméra et à 3 FPS sur une autre.