7.3. Hello BlazeFace

BlazeFace to sieć neuronowa do wykrywania twarzy z kolekcji MediaPipe firmy Google. Pojedyncze wywołanie wnioskowania zwraca prostokąt ograniczający wokół każdej wykrytej twarzy wraz z sześcioma punktami charakterystycznymi twarzy – prawe oko, lewe oko, nos, usta, prawe ucho, lewe ucho. Każda OpenMV Cam dostarczana ze wsparciem dla sieci neuronowych niesie model blazeface_front_128.tflite w pamięci flash, więc uruchomienie kompletnego detektora twarzy zajmuje kilka linii Pythona.

7.3.1. Pełny skrypt

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

To cały detektor twarzy. Nie ma w nim nic więcej; skrypt przechwytuje ramkę, przekazuje ją do modelu, przechodzi zwróconą listę wykryć i rysuje prostokąt ograniczający każdej twarzy oraz jej sześć punktów charakterystycznych z powrotem na ramce. Podgląd w IDE pokazuje ramki i punkty charakterystyczne w czasie rzeczywistym.

7.3.2. Co robi każda linia

Pierwsze trzy linie importują moduły, których skrypt potrzebuje. csi to interfejs sensora kamery; ml to moduł uczenia maszynowego, o którym jest reszta tego rozdziału; BlazeFace to post-procesor, który przekształca surowe tensory wyjściowe BlazeFace w listę ramek ograniczających i punktów charakterystycznych, po której iteruje skrypt.

Kolejnych pięć linii konfiguruje sensor. Kamera zostaje zresetowana do znanego stanu, ustawiona na kolor RGB565, ustawiona na rozdzielczość VGA, a następnie kadrowana (windowed) do kwadratu 400 na 400. Kadrowanie ma znaczenie: BlazeFace został wytrenowany na kwadratowych wycinkach, a podanie mu kwadratowego wejścia dopasowuje oczekiwany przez sieć współczynnik proporcji do tego, co widzi w przechwyconej ramce.

Linia ładowania modelu otwiera plik modelu:

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

ml.Model odczytuje plik pod podaną ścieżką – /rom/ to system plików rezydujący we flashu, omówiony później – i zwraca obiekt modelu, względem którego skrypt będzie uruchamiał wnioskowania. Słowo kluczowe postprocess= rejestruje post-procesor BlazeFace; bez niego predict zwróciłby surowe tensory wyjściowe sieci, a aplikacja musiałaby dekodować je ręcznie. Z nim predict zwraca zdekodowany wynik bezpośrednio. Argument threshold=0.4 na post-procesorze ustawia minimalną pewność, jaką sieć musi zgłosić, zanim wykrycie zostanie zachowane; niższe wartości wychwytują słabsze twarze kosztem większej liczby fałszywych pozytywów.

Pozostałe cztery linie to główna pętla. Każde jej przejście przechwytuje jedną ramkę i pyta model, co widzi:

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() przyjmuje listę wejść (tutaj jeden przechwycony obraz) i zwraca listę krotek wykryć. Każda krotka zawiera prostokąt ograniczający (x, y, w, h), pewność score między zerem a jedynką oraz tablicę ndarray o kształcie (6, 2) ze współrzędnymi punktów charakterystycznych – prawe oko, lewe oko, nos, usta, prawe ucho i lewe ucho w tej kolejności. Wywołanie rysujące używa draw_rectangle() – tego samego prymitywu, którym kończył każdy klasyczny detektor w rozdziale o obrazie – aby obrysować twarz. ml.utils.draw_keypoints() to mały pomocnik z narzędzi ml, który zaznacza każdy punkt kluczowy krzyżykiem w jego pozycji (x, y).

7.3.3. Czego skrypt nie mówi

Skrypt to siedem wykonywalnych linii pracy wnioskowania poza importami i konfiguracją sensora, ale wewnątrz tych siedmiu linii dzieje się sporo arytmetyki. Przechwycona ramka RGB565 400 na 400 staje się skwantyzowanym 8-bitowym tensorem 128 na 128, zanim dotrze do sieci; sieć wykonuje setki operacji względem dziesiątek tysięcy wag; powstałe tensory pewności i przesunięć ramek stają się uszeregowaną listą nienachodzących na siebie ramek ograniczających z dołączonymi punktami charakterystycznymi, zanim predict powróci. Każda z tych transformacji jest czymś, co aplikacja może kontrolować, jeśli zajdzie taka potrzeba, a kilka z nich trzeba dostroić dla dowolnego niestandardowego modelu.

Kolejne cztery podrozdziały rozkładają te transformacje na czynniki pierwsze. Po kolei:

  • Moduł ml – co ml.Model udostępnia po załadowaniu modelu i gdzie plik modelu faktycznie znajduje się na kamerze.

  • Potok wnioskowania – cztery etapy każdego wywołania predict().

  • Silniki wnioskowania – ścieżki CPU i NPU, które wykonują arytmetykę sieci.

  • Dekodowanie wyjścia – post-procesory, które przekształcają surowe tensory wyjściowe w wykrycia, po których iterował ten skrypt.

Do końca rozdziału czytelnik potrafi napisać równoważny skrypt dla modelu, który nie był dostarczony z kamerą, zdekodować tensor, którego post-procesor jeszcze nie istnieje, oraz wyjaśnić, dlaczego konkretny model działa z prędkością 30 FPS na jednej kamerze, a 3 FPS na innej.