typescript and javascript logo
author avatar

Grzegorz Dubiel

29-10-2025

Sztuczka z uruchamianiem rozpoznawania twarzy w Web Workerze w React

Kiedy chcemy wykonać proste zadania rozpoznawania twarzy na naszych zdjęciach, prawdopodobnie będziemy musieli, użyć aplikacji, która w tle wysyła zapytania do zewnętrznego API. Co w sytuacji gdy chemy zachować prywatność naszych danych i przetwarzać je lokalnie na komputerze? Sam byłem w takiej sytuacji kilka razy, ponieważ mam lekką obsesję na punkcie prywatności danych. Z punktu widzenia programisty JavaScript, jesli chodzi o budowanie takich aplikacji, zwykle myślimy o integrowaniu zewnętrznych usług lub prosimy kolegów znających Pythona o napisanie API (swoją drogą, to dobra okazja, żeby nauczyć się Pythona). Na szczęście istnieje dość stara, ale wciąż użyteczna biblioteka — face-api.js.

Czym jest face-api.js

Podczas wybierania biblioteki do użycia w projekcie jej wiek i zaangażowanie użytkowników w jej utrzymanie to zwykle jedne z głównych czynników. W tym przypadku jednak nie ma to większego znaczenia — face-api.js wydaje się być kompletną biblioteką, gotową do użycia w projekcie.

To rozwiązanie nie korzysta z LLM ani żadnych innych transformerów. face-api.js jest nakładką na tensorflow.js i współpracuje głównie z modelami: SSD Mobilenet V1, ResNet-34-like Face Recognition Model oraz MTCNN (experimental).

SSD (Single Shot MultiBox Detector) to model zaprojektowany do wykrywania wielu obiektów w jednym przebiegu, wytrenowany i dostosowany do zadań wykrywania twarzy. Model dobrze sprawdza się w zadaniach detekcji twarzy w czasie rzeczywistym. Jego rozmiar jest stosunkowo niewielki (około 5,4 MB), ponieważ został skwantyzowany.

ResNet-34-like Face Recognition Model został zaprojektowany do zadań rozpoznawania twarzy. Model ten potrafi generować deskryptory twarzy, które mogą zostać użyte do porównywania dwóch twarzy poprzez mierzenie ich podobieństwa z wykorzystaniem algorytmu Euclidean distance, którego implementacja jest również dostępna w face-api.js.

MTCNN (Multi-task Cascaded Convolutional Neural Networks) jest dołączony do face-api.js głównie w celach eksperymentalnych, ale może być bardzo przydatny. Model ten jest szczególnie wyspecjalizowany w zadaniach wykrywania twarzy. Jego główną cechą jest wykrywanie punktów charakterystycznych — na przykład oczu, ust i nosa. Wykrywanie odbywa się symultanicznie, Rozmiar tego modelu to około 2 MB.

Oto najbardziej, wedgług mnie, interesujące modele dostępne w face-api.js, ale jest ich więcej — na przykład lekka wersja do zadań rozpoznawania twarzy oraz model rozpoznawania ekspresji twarzy.

Oczywiście możliwe jest również użycie własnych modeli z face-api.js.

Z jakiego powodu używać face-api.js w Web Workerze?

Zanim zagłębimy się w implementację inferencjii w Web Workerze, warto zrozumieć, dlaczego opłaca się poświęcić wysiłek na zarządzanie komunikacją między głównym wątkiem a workerem.

Załóżmy, że mamy komponent, który pozwala użytkownikom przesłać przykładowe zdjęcie twarzy do przeglądarki, a następnie zestaw innych obrazów. Zadaniem komponentu jest anonimizacja każdej twarzy z podanego zestawu, która należy do osoby z przykładowego zdjęcia.

Najpierw musimy załadować modele:

typescript

Modele możemy przechowywać w katalogu ./public. Po zamontowaniu komponentu wywoływana jest funkcja odpowiedzialna za ładowanie modeli.

Następnie zdefiniujemy naszą główną funkcję do inferencji. Na razie umieścimy całą logikę w hooku:

typescript

Tutaj dzieje się wiele rzeczy. Najważniejszą funkcją jest handleFace, która organizuje cały proces rozpoznawania, porównywania i blurrowania docelowych twarzy.

Najpierw musimy wyodrębnić wszystkie twarze z przykładowego zdjęcia. Twarze te posłużą jako przykłady do określenia, które twarze w docelowym zestawie należy rozmyć.

typescript

Następnie musimy wyodrębnić wszystkie twarze jako obiekty zwane deskryptorami z docelowego zestawu zdjęć, używając tego samego utilsa (extractAllFaces), co w poprzednim kroku.

typescript

Następnie porównujemy twarze, używając algorytmu Euclidean distance:

typescript

face-api.js udostępnia również metodę do obliczania deskryptorów przy użyciu tego algorytmu.

Po pomyślnym porównaniu mamy wszystko, czego potrzebujemy, aby rozmyć odpowiednie twarze. Wykonujemy więc bardzo proste blurrowanie twarzy:

typescript

Na koniec możemy wywołać hooki w komponencie React'owym:

JSX

Wygląda świetnie, więc…

…gdzie jest problem?

Problem polega na tym, że po kliknięciu przez użytkowników przycisku „Process”, interfejs użytkownika się zawiesza. Dzieje się tak, ponieważ środowisko uruchomieniowe JavaScriptu jest jednowątkowe. Domyślnie wszystko działa na jednym wątku procesora — tak zwanym głównym wątku — mimo że moglibyśmy mieć dostępnych nawet osiem lub więcej.

Mimo że modele są lekkie, połączenie zadań inferencji, renderowania interfejsu oraz rozmywania twarzy to zbyt duże obciążenie, by obsłużył je tylko główny wątek.

Rzowiązywanie problemu

Na szczęście istnieje rozwiązanie — Web Workery. Plan jest prosty: możemy przekazać kod odpowiedzialny za inferencję i blurrowanie do Web Workera. W ten sposób nasz interfejs pozostaje responsywny, ponieważ główny wątek będzie odciążony. W środowisku przeglądarki istnieje kilka typów workerów: Dedicated Worker, Shared Worker i Service Worker. Skupimy się na Dedicated Worker, który, według MDN, jest dostępny dla jednego dedykowanego skryptu, który go wywołał.

Aby z niego skorzystać, należy utworzyć plik zawierający kod workera:

typescript

Następnie, po stronie głównego wątku, należy utworzyć instancję klasy Worker, podając ścieżkę do pliku workera:

typescript

Komunikacja odbywa się za pomocą wiadomości.

Migracja logiki inferencji

Teraz, gdy problem został zidentyfikowany, logika inferencji musi zostać przeniesiona z głównego wątku do workera.

Do obsługi komunikacji użyjemy biblioteki, ponieważ natywne rozwiązania nie są zbyt wygodne. Narzędzie, którego użyjemy, nazywa się Comlink. Dzięki tej paczce wystarczy zdefiniować klasę, która będzie abstrakcją naszego workera; metody tej klasy są wywoływane, gdy odpowiednia wiadomość przychodzi z głównego wątku.

Oto przykład workera napisanego bez użycia jakichkolwiek bibliotek w celu zobrazowania :

typescript

Oto wersja wykorzystująca Comlink:

typescript

Czytelniej, prawda?

Wszystko, co musimy zrobić, to przenieść naszą logikę inferencji do workera, prawda? Cóż... nie do końca.

Kolejny problem...

Jest jeden problem, który musimy rozwiązać, jeśli chcemy, aby nasza inferencja po stronie klienta była użyteczna.

Potrzebujemy canvas api, aby przetworzyć obraz, ale nie jest on dostępny w web workerze.

Problem z canvas jest również związany z face-api.js, ponieważ biblioteka korzysta z niego pod maską. Na szczęście istnieje zamiennik dla canvas, czyli OffscreenCanvas. Powinien również rozwiązać pozostałe problemy związane z blurrowaniem obrazu w web workerze.

Wreszcie możemy migrować

Najpierw stworzymy kod dla naszego workera. W ten sposób będziemy mogli zobaczyć, jak problem został rozwiązany.

Zdefiniujmy utilsy:

typescript

Musimy zserializować właściwości obiektu, ponieważ obiekty face-api.js (detections, landmarks, descriptors) są złożonymi instancjami klas (obiektami z prototypami i metodami), za pomocą postMessage można przesyłać tylko serializowalne, proste typy danych JavaScript. Mówiąc prościej: jeśli nie zserializujemy tych obiektów, w głównym wątku otrzymalibyśmy brakujące lub niepoprawne właściwości. Dla przykładu box._y zamiast box.y.

Następnie konfigurujemy face-api.js w workerze i dodajemy również kilka utilsów:

typescript

Pierwszą rzeczą, którą należy zrobić, aby face-api.js działał w web workerze, jest jawne podanie alternatywy dla canvas. Mamy także funkcję do tworzenia canvas'a z danych obrazów, która będzie używana w wykrywaniu i rozpoznawaniu twarzy.

Teraz możemy zdefiniować naszą klasę, która dzięki Comlink będzie pełnić rolę „syntactic sugar” dla API workera:

typescript

Podejście proponowane przez Comlink jest bardzo wygodne. Łatwo sobie wyobrazić, że nasze metody będą wywoływane w miejscach, gdzie worker odbiera wiadomości.

Przenieśliśmy również logikę obliczania porównań obrazów i twarzy, która wcześniej była wykonywana w funkcji handleFace oraz funkcji compareImages wywoływanej w useFaceHook. Teraz logika ta została zamknięta w metodzie detectMatchingFaces. Funkcja blurrowania również została przeniesiona i dostosowana.

Ostatnią rzeczą, którą musimy zrobić po stronie workera, jest załadowanie modeli i udostępnienie instancji naszej klasy workera:

typescript

Przepisywanie logiki po stronie głównego wątku

Teraz musimy nieco dostosować logikę z hooka.

Pierwszą rzeczą, którą musimy dodać, jest inicjalizacja workera. Zainicjalizujemy workera w hooku useEffect i przypiszemy go do ref.

typescript

Następnie, w naszym handlerze handleFace, musimy opakować worker, używając metody wrap z biblioteki Comlink.

typescript

Dzięki temu otrzymujemy przyjazne i łatwo dostępne API, które pozwala nam płynnie komunikować się z workerem, tak jakby był klasą.

typescript

Po wykryciu wszystkich przykładowych twarzy na zdjęciu przykładowym możemy w końcu wykrywać i rozmywać twarze w docelowych obrazach.

typescript

Musimy utworzyć funkcję do uzyskiwania obrazu z wykrytymi twarzami, ponieważ nie chcemy zagnieżdżać zbyt dużo kodu w pętli — szybko stałoby się to chaotyczne. Funkcja przyjmuje wszystko, co potrzebne do uzyskania twarzy do rozmycia: przykładowe twarze, docelowy obraz, w którym szukamy twarzy pasujących do przykładów, oraz funkcję do wykrywania pasujących twarzy.

Cały moduł z hookiem useFace wygląda następująco:

typescript

Podsumowanie

Po kilku trudnych krokach udało nam się rozwiązać problem, z którym wielu devów by się poddało i zwyczajnie skorzystało z zewnętrznych usług. W dzisiejszych czasach, gdy często jesteśmy połączeni z usługami w chmurze i zdalnymi serwerami, warto myśleć o użytkownikach i ich prywatności, również z naszej perspektywy może to być korzystne. Nie zawsze trzeba uruchamiać serwer albo funkcję serverless, przetwarzając dane użytkowników lokalnie na ich maszynie, nie trzeba się martwić o koszty wykorzystania zasobów. Ponadto nie ponosimy takiej samej odpowiedzialności za wrażliwe dane użytkowników przetwarzanych na ich maszynie, jak w przpadku przechowywania ich w naszej bazie danych na naszych serwerach.

Dzięki za przeczytanie! Stay tuned! 🫡

typescript and javascript logogreg@aboutjs.dev

©Grzegorz Dubiel | 2026