Gra sterowana kamerą - bieg po farbę
02.07.2017 - Filip Mróz
Biegnij graczu, biegnij!W tej części dodamy element interaktywności do etapu biegu tak, żeby nazwa tego etapu rzeczywiście odzwierciedlała to, co ma się w nim dziać. Jeśli gracz chce, żeby jego postać się poruszała, on sam musi się poruszać. Zastanówmy się, w jaki sposób możemy wykryć, że gracz biegnie czy w ogóle się rusza. Możemy porównać aktualny obraz z kamery z poprzednim. Następnie na podstawie różnicy tych obrazów możemy z grubsza wyznaczyć, które miejsca zostały "ruszone". Oczywiście, jeśli gracz będzie nosił ubranie koloru tła, metoda ta nie zda egzaminu, ale załóżmy, że tak nie jest. Pomysł w teorii brzmi sensownie, ale sprawdźmy jaki efekt otrzymamy w praktyce. Wyliczając różnicę dwóch kolejnych klatek, a następnie dodając do siebie składowe poszczególnych pikseli, otrzymując czarno-biały obraz: Pierwszym spostrzeżeniem jest to, że losowe szumy sprawiają, że tło nie jest czarne, a pulsuje małymi, losowymi zmianami. Standardową metodą walki z takimi błędami jest użycie filtru medianowego (o rozmiarze 5x5) (median filter) na wejściowych obrazach. Zobaczmy, jak ta operacja wpływa na wynikowy obraz różnicy: Szumy zostały zmniejszone oraz, co ważne, wygładzone (brak pojedynczych jasnych pikseli), dzięki czemu łatwiej będzie je później odfiltrować. Kolejnym spostrzeżeniem związanym z naszą metodą wykrywania ruchu jest to, że jego ślad widać w dwóch miejscach. Dzieje się tak, ponieważ wyliczamy wartość bezwzględną z różnicy. Nie ma więc znaczenia, czy ręka jest w pewnym miejscu w pierwszym obrazie czy w drugim. Dla pewnych zastosowań (np. śledzenia obiektów) taka informacja byłaby niewystarczająca, ponieważ nie znamy kierunku ruchu. W naszym przypadku to nie przeszkadza. Przyjrzyjmy się śladowi gracza podczas podskoków (które udają bieg): Na podstawie wysokości powyżej wyliczonego śladu ruchu wyliczymy chwilową prędkość gracza. Dzięki temu zmusimy gracza, by ruszał całym ciałem, od stóp po głowę, a nie tylko rękami. Implementacja wyznaczania prędkości bieguMamy już ogólny zamysł, więc możemy przejść do jego implementacji. Na początek stworzymy plik PrzetwarzanieObrazowBiegu.h z deklaracją klasy PrzetwarzanieObrazowBiegu, na którą zrzucimy całą pracę związaną z wyznaczeniem śladu ruchu: #pragma once #include "PrzetwarzanieObrazow.h" class PrzetwarzanieObrazowBiegu : public PrzetwarzanieObrazow { };
Dla porządku część wyznaczającą prędkość na podstawie podanego obrazu umieścimy w nowej funkcji należącej do #include "PrzetwarzanieObrazowBiegu.h" () class StanBiegu { () PrzetwarzanieObrazowBiegu przetwarzanieObrazow; double wyznaczPredkosc(Mat obraz);
Użyjmy tej funkcji na początku void StanBiegu::uaktualnijStan(double czas, Mat obraz) { double predkosc = wyznaczPredkosc(obraz);
Przejdźmy teraz do implementacji metody double StanBiegu::wyznaczPredkosc(Mat obraz) { Rect prostokatOtaczajacy = przetwarzanieObrazow.wykryjRuch(obraz); Interesuje nas tylko to, jak gracz przemieszcza się po osi Y, więc wykorzystamy tylko pionową składową prostokąta . Chcemy wymusić na graczu, aby ruszał jednocześnie całym ciałem, dlatego jego prędkość chwilowa będzie proporcjonalna do . Wykorzystamy zwykłą funkcję liniową wyskalowaną tak, aby przy maksymalnym , równym wysokości obrazu , gracz poruszał się z maksymalną możliwą prędkością : Implementacja powyższych rozważań jest bardzo krótka: double wysokoscSladu = prostokatOtaczajacy.height; return wysokoscSladu * MAKSYMALNA_PREDKOSC / obraz.rows; } Wykrycie ruchu w obrazieCzas zająć się najciekawszą częścią implementacji etapu biegu, czyli metodą wykrywającą ruch. Chcemy za jej pomocą wyznaczyć prostokąt otaczający wszystkie "ruszone" piksele. Schemat algorytmu będzie następujący:
Zauważmy, że tak jak w metodzie Dodajmy deklaracje wszystkich metod: class PrzetwarzanieObrazowBiegu : public PrzetwarzanieObrazow { public: Rect wykryjRuch(Mat obraz); Mat wyznaczSlad(Mat obraz); Mat usunPrzypadkowePiksele(Mat obraz); Rect otoczProstokatem(Mat obraz); };
Implementację tej klasy umieścimy w pliku PrzetwarzanieObrazowBiegu.cpp. Zacznijmy od rozłożenia #include "PrzetwarzanieObrazowBiegu.h" Rect PrzetwarzanieObrazowBiegu::wykryjRuch(Mat obraz) { Mat slady = wyznaczSlad(obraz); Mat czystySlad = usunPrzypadkowePiksele(slady); return otoczProstokatem(czystySlad); } Mat PrzetwarzanieObrazowBiegu::wyznaczSlad(Mat obraz) { return obraz; } Mat PrzetwarzanieObrazowBiegu::usunPrzypadkowePiksele(Mat obraz) { return obraz; } Rect PrzetwarzanieObrazowBiegu::otoczProstokatem(Mat obraz) { return Rect(); } Teraz zajmiemy się kolejnymi etapami. Wyznaczenie śladuPierwszy etap to wyznaczenie w obrazie pikseli, które się zmieniły. Zacznijmy od użycia wcześniej wspomnianego filtru medianowego do zmniejszenia szumów w obrazie: Mat PrzetwarzanieObrazowBiegu::wyznaczSlad(Mat obraz) { obraz.copyTo(_wyznaczSladObraz); medianBlur(_wyznaczSladObraz,_wyznaczSladObraz,5); Chcemy wyliczyć różnicę między aktualnym a poprzednim obrazem. Najpierw sprawdźmy, czy go mamy zapisany poprzedni obraz: if(_wyznaczSladPoprzedniObraz.data == NULL) { _wyznaczSladWynik = Mat(obraz.size(),CV_8U,Scalar(0)); } else {
Jeśli nie mamy poprzedniej klatki, to funkcja zwróci pusty obraz, w przeciwnym przypadku wyliczy dla każdego piksela różnicę między jego składowymi, a następnie sumę tych różnić. Jeśli czytałeś poprzedni artykuł, zabrzmi to znajomo. Okazuje się, że napisaliśmy już takie sumowanie składowych w class PrzetwarzanieObrazow { static void sumujSkladowe(Mat &obraz, Mat &wynik); } void PrzetwarzanieObrazow::sumujSkladowe(Mat &obraz, Mat &wynik) { // Wynik będzie zawierał czarno-biały obraz. wynik = Mat(obraz.size(),CV_8U); for(int i=0;i<wynik.rows;i++) for(int i2=0;i2<wynik.cols;i2++) { Vec3b piksel = obraz.at<Vec3b>(i,i2); // Dodajemy składowe pamiętając o zakresie uchar. wynik.at<uchar>(i,i2) = saturate_cast<uchar> ((int)piksel[0]+piksel[1]+piksel[2]); } }
Zastąpmy kod z absdiff(_wybierzPikseleBarwa,szukanaBarwa,_wybierzPikseleRoznica); sumujSkladowe(_wybierzPikseleRoznica,_wybierzPikseleWynik); threshold(_wybierzPikseleWynik,_wybierzPikseleWynik,wartoscGraniczna,255,THRESH_BINARY_INV);
Wróćmy do implementacji else { absdiff(_wyznaczSladObraz,_wyznaczSladPoprzedniObraz,_wyznaczSladRoznica); sumujSkladowe(_wyznaczSladRoznica,_wyznaczSladWynik); Otrzymujemy obraz przedstawiający różnice między odpowiadającymi sobie pikselami w dwóch kolejnych obrazach z kamery. Teraz musimy zdecydować o tym, jaka różnica wystarczy, by uznać piksel za "ruszony". Zrobimy to prosto za pomocą stałej wartości granicznej: // Pikselom o wartości większej niż MINIMALNA_ZMIANA ustawimy wartość 255 (bedą białe) // Całej reszcie - 0 (będą czarne) threshold(_wyznaczSladWynik,_wyznaczSladWynik,MINIMALNA_ZMIANA,255,THRESH_BINARY); }
Na koniec nie zapomnijmy zapisać przefiltrowanej wersji aktualnego obrazu, do wykorzystania w wywołaniu _wyznaczSladObraz.copyTo(_wyznaczSladPoprzedniObraz); return _wyznaczSladWynik; } Pozostaje nam jeszcze dopisać deklaracje wszystkich roboczych macierzy oraz stałej: protected: static const int MINIMALNA_ZMIANA = 60; Mat _wyznaczSladPoprzedniObraz; Mat _wyznaczSladObraz; Mat _wyznaczSladRoznica; Mat _wyznaczSladWynik;
Zobaczmy teraz, jak sprawuje się Udało nam się dość dobrze wyznaczyć piksele, które się zmieniły między kolejnymi obrazami. Niestety, widać też trochę błędnych pikseli. Z nimi powinniśmy sobie poradzić w kolejnym etapie. Usuwanie błędnych pikseliOtrzymaliśmy obraz, który przedstawiał z błędami piksele które się zmieniły. Chcemy się pozbyć błędów zachowując większość dobrych pikseli. Zauważmy, że błędy to zazwyczaj małe grupy pikseli lub wręcz pojedyncze piksele. Do ich usunięcia wykorzystamy operację erozji (patrz erosion oraz Gra sterowana kamerą - pomaluj płot), polegającą na usunięciu pikseli z obrazu, jeśli w jego okolicy jest jakiś czarny piksel. Ponieważ OpenCV dostarcza metodę wykonującą taką operację, implementacja tego etapu jest łatwa. Okolicę piksela wyznaczać będzie koło wpisane w kwadrat 5 x 5: Mat PrzetwarzanieObrazowBiegu::usunPrzypadkowePiksele(Mat obraz) { erode(obraz,_usunPrzypadkowePikseleWynik,getStructuringElement(MORPH_ELLIPSE,Size(5,5))); return _usunPrzypadkowePikseleWynik; } Dodajmy deklarację macierzy, do której zapiszemy przefiltrowany obraz: protected: () Mat _usunPrzypadkowePikseleWynik; public: Zobaczmy, czy uda nam się usunąć wszystkie błędy z poprzedniego etapu:
Jak widać udało się usunąć wszystkie błędne piksele za cenę niewielkiej ilości dobrych pikseli. Pamiętajmy, że od gracza mamy prawo oczekiwać znacznego ruchu, a do następnego etapu nie powinien przejść ani jeden błędny piksel. Dlatego, jeśli zajdzie taka potrzeba, można wzmocnić ten etap zmieniając parametry metody Wyznaczanie prostokąta otaczającegoOstatni etap algorytmu znajdzie najmniejszy prostokąt otaczający białe piksele obrazu. Ponieważ wszystkie piksele mają zawierać się w jego wnętrzu, musimy być pewni, że poprzednie etapy skutecznie wyeliminują wszystkie niepoprawne piksele. Algorytm wyznaczający prostokąt jest prosty i polega na znalezienia położenia pierwszego piksela od lewej i prawej strony oraz od góry i od dołu. Napiszemy algorytm może nie optymalnie, ale bardzo zwięźle: Rect PrzetwarzanieObrazowBiegu::otoczProstokatem(Mat obraz) { // Inicjalizujemy położenie rogów prostokąta tak, by // zostały zastąpione przez pierwszy biały piksel. int lx=obraz.cols,gy=obraz.rows; int px=-1,dy=-1; for(int y=0;y<obraz.rows;y++) for(int x=0;x<obraz.cols;x++) { if(obraz.at<uchar>(y,x)!=0) { lx = min(lx,x); px = max(px,x); gy = min(gy,y); dy = max(dy,y); } } Musimy pamiętać, że jeśli wszystkie piksele będą czarne, wyliczone wartości nie będą miały sensu. W takim przypadku zwrócimy „zerowy” prostokąt: if(px == -1) return Rect(); else return Rect(Point(lx,gy),Point(px,dy)); } Powyższa metoda poprawnie znajduje otaczający prostokąt w obrazie otrzymanym z poprzedniego etapu:
Cała metoda W ten sposób zakończyliśmy część dotyczącą wykrywania biegu gracza. Oczywiście opiera się ona na bardzo prostych technikach, ale wydaje się spełniać swoją rolę. Co więcej, podobną technikę wykrywania ruchu można zastosować w prostym systemie monitoringu – gdy wykryjemy ruch, zaczyna się nagrywanie oraz wysyłanie informacji o intruzie. |
Copyright © 2008-2010 Wrocławski Portal Informatyczny
design: rafalpolito.com