Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GazeCtrl / Gaze-Grid

Sterowanie komputerem wzrokiem przy użyciu zwykłej kamery RGB laptopa — bez sprzętu IR (Tobii i podobne) i bez ograniczeń licencyjnych. Kamera obserwuje użytkownika i rozpoznaje, na które z 12 pól ekranu (siatka 3×4) patrzy; zatrzymanie wzroku na polu przez zadany czas aktywuje je.

Projekt powstaje z myślą o wsparciu osób niepełnosprawnych — docelowo jako warstwa wejściowa dla aplikacji typu AAC.

Cały stos jest open source: MediaPipe (Apache 2.0), OpenCV (Apache 2.0 / BSD), scikit-learn (BSD). Działa lokalnie i offline — obraz z kamery nie opuszcza komputera.

Stan projektu

Wczesny prototyp, ale przetestowany na żywo — cały pipeline działa z prawdziwą twarzą i ma zmierzoną skuteczność.

Cztery kalibracje, trafność liczona uczciwie (całe odłożone rundy, szczegóły niżej):

Sesja Wariant domyślny Po wyborze wariantu per sesja
19.07 popołudnie 82,5% 86,9%
19.07 wieczór 72,1% 87,9%
19.07 noc (słabe światło) 55,6% 63,1%
07.08 biuro 66,2% 86,5% (±6,5)

Rozrzut między sesjami jest większy niż jakakolwiek zmiana w kodzie — warunki zbierania próbek decydują o wyniku bardziej niż model. Sesja nocna pokazuje, ile traci się przy złym świetle.

Sesja biurowa pokazuje coś innego. Przy dobrym świetle rozpoznawanie kolumny jest w zasadzie rozwiązane (96,7%), ale wiersz zatrzymał się na 68,5% — dopóki nie okazało się, że głowa osunęła się w trakcie kalibracji o prawie cały odstęp między rzędami. Odjęcie tego dryfu podniosło trafność wiersza do 88,8% i całą sesję do 86,5%. Oś pionowa pozostaje wąskim gardłem: sygnał odróżniający rzędy jest tak mały, że zwykłe osunięcie się na krześle potrafi go przykryć.

Element Stan
Detekcja twarzy i tęczówek (MediaPipe) działa, zweryfikowane
Ekstrakcja cech (4 wartości: pozycja tęczówki w obu oczach) działa, weryfikowane na żywo
Kalibracja 12 pól (4 rundy, losowa kolejność) + raport trafności działa, walidacja leave-one-round-out
Zespół 5 sieci + wybór wariantu per sesja działa
Klasyfikacja pola + dwell activation działa
Akcja po aktywacji pola nie zaimplementowane — on_zone_activated() tylko wypisuje numer pola

Jak to działa

kamera RGB
   ↓
MediaPipe Face Landmarker      478 punktów twarzy, w tym tęczówki (468–477)
   ↓
ekstrakcja cech                pozycja tęczówki względem kącików oka (oba oczy),
                               4 wartości — yaw/pitch głowy NIE są cechą
   ↓
korekta dryfu głowy            opcjonalna; czy działa i na których cechach —
                               rozstrzygane osobno w każdej kalibracji
   ↓
zespół 5 sieci MLP             trenowany per-użytkownik podczas kalibracji;
                               głowica klasyfikująca (12 pól) albo regresyjna
                               (ciągłe kolumna/wiersz) — wybór per sesja
   ↓
wygładzanie czasowe            głosowanie większościowe w oknie 7 klatek
                               + próg pewności 0,55
   ↓
dwell activation               0,7 s stabilnego patrzenia → zdarzenie aktywacji

Dlaczego klasyfikacja pól, a nie współrzędne kursora

Appearance-based gaze estimation ze zwykłej kamery RGB daje błąd rzędu kilku stopni kąta, co przy typowej odległości od ekranu przekłada się na kilka centymetrów. To wystarcza, żeby rozpoznać obszar ekranu, ale nie do wskazania pojedynczego piksela — do tego potrzebna byłaby kamera IR z emiterami (dokładność <1°).

Dlatego model uczy się bezpośrednio klasyfikacji 12 dużych pól, zamiast regresji współrzędnych x/y. Pomija to pośredni krok szacowania wektora spojrzenia w stopniach (jak w L2CS-Net czy ETH-XGaze), który i tak wymagałby kalibracyjnego mapowania na ekran.

Z tego samego powodu wygładzanie to głosowanie większościowe, a nie filtr Kalmana — przy dyskretnych polach nie ma ciągłej trajektorii do wygładzania. Kalman miałby sens dopiero przy przejściu na predykcję współrzędnych.

Dlaczego trzy decyzje zapadają przy każdej kalibracji

Trzy pomysły okazały się pomagać w jednych sesjach i szkodzić w innych, więc zamiast ustalać je raz na zawsze, program sprawdza je przy każdej kalibracji i wybiera to, co działa dla tej konkretnej sesji:

  • Typ głowicy. Klasyfikator traktuje 12 pól jako nieporównywalne etykiety i nie wie, że pole 5 leży między 1 a 9. Regresor przewiduje kolumnę i wiersz jako liczby ciągłe, więc korzysta z uporządkowania siatki — pomaga tam, gdzie sygnału brakuje (+7,5 punktu w najsłabszej sesji), i lekko szkodzi tam, gdzie go starcza (−3,3 w najlepszej).
  • Czy korekta dryfu głowy w ogóle działa. Głowa osuwa się przez całą kalibrację i przesuwa cechy o kilkanaście procent sygnału odróżniającego pola. Odjęcie tego wpływu dało +4,4 i +8,7 punktu w dwóch sesjach, ale −11,8 w trzeciej — tam dryf nie był napędzany ruchem głowy i korekta odejmowała wielkość niezwiązaną z problemem.
  • Na których cechach korekta ma działać. Osobna decyzja, bo dryf poziomy i pionowy zachowują się niezależnie. W sesji biurowej korekta samego poziomu dała −2 punkty, a objęcie nią cech pionowych +20 punktów; w sesji nocnej odwrotnie — każda maska szkodziła i wartownik odrzucił wszystkie.

Osiem kombinacji jest ocenianych na odłożonych rundach, a wariant domyślny (klasyfikator bez korekty) jest uprzywilejowany — kandydat musi wygrać o co najmniej 2 punkty procentowe, żeby go zastąpić. Ten próg jest tym ważniejszy, im więcej kandydatów: przy ośmiu wariantach na czterech rundach łatwo wybrać zwycięzcę szumu. Wybrany wariant i wyniki wszystkich ośmiu wypisywane są po kalibracji.

Zespół 5 sieci różniących się tylko inicjalizacją nie służy głównie poprawie trafności: na 480 próbkach pojedynczy MLP potrafi wahać wynikiem o 12 punktów zależnie od ziarna. Uśrednienie zbija tę wariancję i sprawia, że raportowana liczba dotyczy dokładnie tego modelu, który zostaje zapisany.

Instalacja

Wymagany Python 3.12 — MediaPipe nie publikuje wheeli dla 3.14, więc instalacja na systemowym Pythonie Fedory się nie powiedzie.

uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -r requirements.txt

Bez uv — dowolny Python 3.12 w systemie:

python3.12 -m venv .venv
.venv/bin/pip install -r requirements.txt

Model MediaPipe (3,6 MB, pobierany raz, potem działa offline):

wget -O face_landmarker.task \
  https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task

Pierwsze uruchomienie

Kolejność ma znaczenie — każdy krok weryfikuje założenie następnego.

1. Znajdź właściwą kamerę. Laptopy często mają kilka urządzeń wideo (kamera IR, urządzenie metadata), z których część otwiera się, ale nie oddaje obrazu. Poniższe polecenie pokazuje tylko te, które realnie zwracają klatki:

.venv/bin/python gaze_grid.py --list-cameras

2. Sprawdź, czy tęczówki są wykrywane poprawnie. Ten krok jest istotny: przypisanie oko↔tęczówka w stałych IRIS_A/IRIS_B opiera się na powszechnej konwencji tutoriali MediaPipe, ale warto potwierdzić je wizualnie.

.venv/bin/python gaze_grid.py --debug

Grupa A rysowana jest na żółto, B na niebiesko — punkty tęczówki jako kropki, kąciki oka jako krzyżyki. Jeśli żółte kropki są na innym oku niż żółte krzyżyki, zamień IRIS_A z IRIS_B w sekcji KONFIGURACJA w gaze_grid.py. Bez tego kalibracja uczy się na przemieszanych cechach i wychodzi słabo bez widocznej przyczyny.

3. Kalibracja — 12 pól × 40 próbek, w 4 rundach po 10. Patrz w kropkę pojawiającą się kolejno w środku każdego pola; po każdej zmianie masz 1,5 s na przeniesienie wzroku (z odliczaniem w kropce), zanim zacznie się zbieranie próbek. Jeśli to za szybko, zwiększ --settle 2.5 — lepiej kalibrować dłużej niż zbierać próbki, w których wzrok jest jeszcze w drodze:

.venv/bin/python gaze_grid.py --calibrate

Podczas kalibracji patrzysz w kropkę w środku pola, a nie na podświetlony kwadrat. Kwadrat pozwala wzrokowi błądzić po całym polu (przy 1920×1080 to obszar 480×360 px), więc próbki jednej klasy rozjeżdżają się po dużym kącie i klasy zachodzą na siebie. Punkt ściąga wzrok w jedno miejsce. Odliczanie wyświetla się dokładnie na środku pola, żeby czytanie go nie odciągało wzroku od celu w momencie startu zbierania.

Pola zapalają się w losowej kolejności, innej w każdej rundzie. Przy stałej kolejności 1→12 wszystko, co dryfuje w czasie kalibracji — osuwająca się głowa, zmiana światła, zmęczenie oczu — byłoby skorelowane z numerem pola, a model mógłby uczyć się dryfu zamiast spojrzenia.

Na koniec wypisywana jest trafność, wybrany wariant wraz z wynikami wszystkich czterech, trzy najsłabsze pola i z czym każde z nich jest mylone. Kierunek pomyłek mówi więcej niż sama trafność: pomyłki w pionie (pole mylone z tym nad nim) wskazują na kąt kamery i słabszy sygnał pitch, pomyłki w poziomie — na zbyt wąskie kolumny, a brak wyraźnego kierunku na ogólny szum (światło, odbicia w okularach). Poniżej ~70% warto zadziałać zgodnie z tą podpowiedzią i powtórzyć.

Surowe próbki lądują w calibration_data.npz — nie tylko gotowe cechy, ale też znormalizowane landmarki i macierze głowy. Nieudaną kalibrację można więc analizować bez powtarzania jej, a nowy pomysł na wektor cech przeliczyć na zapisanych próbkach, zamiast siadać przed kamerą przy każdej zmianie.

Trafność liczona jest na całych odłożonych rundach, nie na losowych próbkach. Próbki w obrębie jednej rundy to kolejne, niemal identyczne klatki — losowy podział rozdzielałby ich duplikaty między zbiór treningowy i testowy, a model rozpoznawałby klatki już widziane. Ta liczba jest więc niższa niż przy losowym podziale, ale jest uczciwym oszacowaniem zachowania na żywo.

Każda z 4 rund służy po kolei jako zbiór testowy, a wynik to średnia z podanym rozrzutem. Pojedyncza odłożona runda daje wynik obarczony rozrzutem rzędu ±11 punktów, więc łatwo wziąć szczęśliwą rundę za poprawę. Model finalny uczy się potem na wszystkich próbkach — odłożone też są cenne.

4. Praca:

.venv/bin/python gaze_grid.py --run

ESC kończy każdy tryb. Wszystkie tryby przyjmują --camera N.

Kalibracja i praca wyświetlają siatkę na pełnym ekranie, a rozmiar ekranu wykrywany jest automatycznie. Jest to istotne dla poprawności: pola siatki wyznaczają kąty spojrzenia, więc kalibracja w małym oknie nauczyłaby klasyfikator innego rozkładu niż ten, który wystąpi przy pracy. Z tego samego powodu oba tryby muszą używać tego samego trybu wyświetlania — jeśli kalibrujesz z --windowed, pracuj też z --windowed (i tym samym --width/--height).

Ograniczenia

  • Kalibracja jest per-użytkownik i per-ustawienie. Zmiana pozycji względem kamery, oświetlenia albo samej kamery wymaga powtórzenia kalibracji.
  • Duży wpływ mają: oświetlenie, ruchy głowy, okulary, kąt kamery. Różnica między kalibracją przy dobrym świetle a wieczorną sięga 20 punktów trafności.
  • Pion jest słabszy niż poziom. Najczęstsza pomyłka to pole leżące nad lub pod właściwym; sygnał pionowy jest słabszy i mniej powtarzalny między sesjami.
  • Dokładność nie pozwala na precyzyjne wskazywanie — patrz wyżej.

Podpięcie własnej akcji

on_zone_activated(zone_idx) w gaze_grid.py jest wywoływane raz na każdą aktywację pola (numeracja 0–11, wypisywana 1–12). Tam podpina się docelowe zachowanie — odtworzenie słowa lub dźwięku, kliknięcie, zdarzenie do aplikacji AAC.

Pliki

gaze_grid.py całość — detekcja, kalibracja, klasyfikacja, pętla robocza
requirements.txt zależności
face_landmarker.task model MediaPipe, pobierany osobno (poza repo)
calibration_model.pkl wynik kalibracji, tworzony lokalnie (poza repo)
calibration_data.npz surowe próbki kalibracyjne, do analizy (poza repo)

Model i kalibracja są celowo poza repozytorium: model jest pobieralny, a kalibracja dotyczy konkretnej osoby i konkretnego ustawienia kamery.

Licencja

MIT — patrz LICENSE.

About

Webcam-only eye-gaze tracking mapped to a 12-cell screen grid with dwell-based activation — an open-source accessibility tool, no dedicated eye tracker required.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages