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.Modeludostę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.