Domowe efekty specjalne - podmiana tła

28.09.2011 - Filip Mróz
TrudnośćTrudność

Zapamiętywanie tła

Nasza metoda separacji pierwszego planu opiera się na porównaniu aktualnego obrazu z wcześniej zapisanym tłem. Potrzebny będzie więc pewien interfejs do wybrania momentu pobrania tła, będzie on też użyteczny w przyszłym ustawianiu parametrów przetwarzania. Jako że chcemy by program był jak najprostszy, pozostaniemy przy wykorzystaniu metody waitKey(time) do komunikacji. Należy pamiętać, że reaguje ona tylko wtedy, gdy jedno z okien OpenCV ma fokus. Najpierw zmodyfikujmy pętlę tak, by wykorzystywała wciśnięty klawisz:
key = waitKey(1000/FPS);
processKey(key);
Dla przejrzystości wyświetlimy też aktualną klawiszologię:
printf("Connection established!\n");
showUsage();
Będziemy musieli też przenieść deklarację inputImage z procedury main tak, by processKey(key) mógł się do niej odwołać. Dodamy też deklaracje tła, przez co zewnętrzne deklaracje przybiorą postać:
const int FPS = 30;
VideoCapture camera;
Mat inputImage;
Mat savedBackgroundImage;
Pozostaje nam uzupełnić nowe funkcje:
void showUsage() {
	printf("Press 'b' to set background\n");
	printf("Press 'k' to terminate\n");
}
 
void processKey(char key) {
	switch(key) {
		case 'b': 
			inputImage.copyTo(savedBackgroundImage);
			imshow("Background",savedBackgroundImage);
			break;
	}
}
Funkcja processKey(key) dodatkowo wyświetla aktualnie zapisane tło w nowym oknie, tak by można było sprawdzić czy w ogóle działa. Możemy teraz przejść do wyliczenia różnicy między zapisanym tłem a aktualnym obrazem.

Wyznaczenie różnicy obrazów

Klasa Mat dostarcza wielu użytecznych operacji na macierzach, takich jak operacje arytmetyczne i logiczne. Wykorzystamy operacje absDiff do wyliczenia bezwzględnej wartości z różnicy pomiędzy odpowiadającymi sobie elementami dwóch macierzy: tłem i aktualnym obrazem. Dodajmy najpierw nową macierz na przechowanie wyliczonej różnicy:
Mat differenceImage;
W głównej pętli wyliczmy interesującą nas różnicę i pokażmy ją (sprawdzając wcześniej czy tło zostało już zapisane):
if(savedBackgroundImage.data!=NULL) {
	absdiff(inputImage, savedBackgroundImage,differenceImage);
	imshow("Difference Image",differenceImage);
}
Po uruchomieniu i zapisaniu tła, powinniśmy otrzymać wynik podobny (oczywiście w innej scenerii) do tego:



Tutaj warto omówić parę przeszkód związanych z naszym zadaniem, ale również często obserwowanych w innych zagadnieniach dotyczących przetwarzania obrazów. Głównym problemem podczas przetwarzania obrazów jest różnego rodzaju szum (image noise), wpływający na dane wejściowe. Jest on związany zarówno z kamerą (jakość obrazu, automatyczne ustawienia), materiałami (odbicia, refleksy, zależność koloru od światła), umiejscowieniem źródła światła (cienie). W przypadku naszej metody wyodrębniania pierwszego planu, najwięcej problemów sprawia automatyczne wyrównanie jasności obrazu przez kamerę (przed wysłaniem go do komputera) oraz cienie rzucane przez pierwszy plan. Pierwszy problem pojawia się, gdy zmienia się średnia jasność obrazu (np. zasłonimy jego znaczną część) wtedy jasność obrazu jest automatycznie podwyższana i nasze zapisane tło przestaje być podobne do aktualnego obrazu:



Cienie są również niepożądane, gdyż zmieniają one część obrazu, która nie należy do pierwszego planu, utrudniając prawidłowe jego rozpoznanie:



Oba problemy mają związek ze zmianą jasności tła (ogólna zmiana lub lokalne przyciemnienie) i mogą być ograniczone poprzez przekonwertowanie kolorów obrazów do HSV zamiast domyślnego BGR (to samo co RGB, tylko w innej kolejności). Wtedy zmiana jasności ma dużo mniejszy wpływ na trójkę reprezentującą kolor (dla RGB są to trzy kanały, a w HSV maksymalnie dwa). Mimo to w naszym przypadku dużo prościej i skuteczniej jest uniknąć problemów poprzez odpowiednie ustawienie kamery i otoczenia. Większość, nawet prostych kamer, ma możliwość wyłączenia wyrównywania jasności obrazu i ustawienia go na wybrany, stały poziom. Drugi problem, czyli cienie, można wyeliminować unikając punktowego źródła światła, a także umieszczając kamerę w taki sposób, by cień rzucany przez obiekty na pierwszym planie był poza obszarem obserwowanym przez kamerę.

Zakładając, że potrafimy już sprostać tym problemom, przejdźmy do następnej części naszego programu, czyli decyzji które piksele należą do tła.
Aktualną wersję kodu można znaleźć w paczce ze źródłami w pliku: main2_roznica.cpp

Ustalanie wartości granicznej

Aktualnie mamy obrazek BGR reprezentujący różnicę między obrazami i chcemy zdecydować, dla których pikseli ta różnica jest wystarczająca, aby móc uznać go za pierwszy plan. By program pozostał prosty, ustalimy jedną wartość graniczną foreground_threshold dla wszystkich pikseli obrazu. Wcześniej przekonwertujemy obraz BGR na obraz czarno-biały, korzystając z metody cvtColor (uzyskany obraz nie jest zwykłą średnią z trzech kanałów BGR, więcej, ale się tym nie przejmujmy), dzięki czemu foreground_threshold staje się po prostu liczbą. cvtColor znajduje się w pliku imgproc.hpp, więc musimy go dołączyć:
#include "opencv2/imgproc/imgproc.hpp"
Dodajmy deklarację potrzebnych zmiennych:
Mat grayscaleDifferenceImage;
Mat thresholdedDifferenceImage;
int foregroundThreshold;
By wyznaczyć piksele, których wartość jest większa od foreground_threshold, użyjemy metody threshold:
cvtColor(differenceImage,grayscaleDifferenceImage,CV_BGR2GRAY);
threshold(grayscaleDifferenceImage,thresholdedDifferenceImage, foregroundThreshold,255,THRESH_BINARY);
imshow("Foreground Mask", thresholdedDifferenceImage);
Wartość foreground_threshold musi być odpowiednio dobrana, więc potrzebna będzie możliwość jej modyfikacji. Dodajmy opis nowych klawiszy w showUsage():
printf("Press 'a' to increase foregroundThreshold\n");
printf("Press 'z' to decrease foregroundThreshold\n");
Następnie obsługę odpowiednich klawiszy w processKey(key):
case 'a':
foregroundThreshold+=1;
printf("foregroundThreshold = %d\n", foregroundThreshold);
	break;
case 'z':
	if(foregroundThreshold>1) foregroundThreshold -=1;
printf("foregroundThreshold = %d\n", foregroundThreshold);
	break;
Powinniśmy być teraz w stanie uzyskać możliwość separacji tła (nazwijmy ten obraz maską pierwszego planu).

Ponieważ metoda jest bardzo prosta, by nie rzec prymitywna, efekt zależy od wyboru tła (w zasadzie zawsze zależy). Należy pamiętać, że kolor tła powinien być inny niż kolor pierwszego planu, tło powinno być statyczne (ruszająca się roślinność nie jest dobrym wyborem), powinna być zapewniona dostateczna ilość światła. Obrazek poniżej przedstawia efekt, jaki da się uzyskać. Widać, że nie jest on doskonały, występują dziury w pierwszy planie, widać też losowe punkty wynikające z drgania obrazu kamery. Drganie to polega na losowych zmianach wartości pikseli wokół „prawdziwych wartości”, często zachodzących zgodnie z rozkładem Gaussa (Gaussian noise). Dla niskich wartości granicznych taka zmiana tła wystarcza, by zakwalifikować piksele do pierwszego planu. Część błędów można jednak skorygować bardzo prostymi metodami.



Poprawa jakości maski

Uzyskana maska często nie jest doskonała. Można wyróżnić dwa główne rodzaje błędów: wynikające z podobieństwa pierwszego planu do drugiego (pojawiają się dziury – patrz obrazek) albo też z szumu w obrazie pochodzącym od kamery (małe losowe punkty). Do usunięcia obydwu efektów można wykorzystać złożenie dwóch niezwykle prostych operacji:
  • erozji (erode)
  • rozszerzenia (dilate)
Parametrem używanym przez nie jest rozmiar sąsiedztwa (zazwyczaj jest to kwadrat). Dla ustalonego sąsiedztwa wartość piksela jest zastępowana minimalną wartością sąsiadów (erozja) lub maksymalną (rozszerzenie), powodując odpowiednio zmniejszenie lub rozrośnięcie się obiektów. Złożenie tych operacji nie jest przemienne i zależnie od kolejności otrzymamy:
  • erozja $ \circ $ rozszerzenia – operacja otwierania (opening)
  • rozszerzenia $ \circ $ erozja – operacja zamykania (closing)
Otwieranie służy do usuwania małych obiektów i rozdzielania sklejonych, natomiast zamykanie, jak wskazuje nazwa, do zamykania i sklejania dziur. By poprawić jakość naszej maski, najpierw wykonamy otwieranie, a następnie zamykanie. Wykorzystamy do tego celu metodę morphologyEx, która pozwala łatwo wykonać te operacje. Jak zwykle, najpierw dodajmy macierz, która będzie przechowywać obraz po tych operacjach:
Mat fixedDifferenceImage;
Następnie rozszerzmy ciąg działań na obrazie o wspomniane operacje morfologiczne:
morphologyEx(thresholdedDifferenceImage,fixedDifferenceImage,MORPH_OPEN,Mat(),Point(-1,-1),1);
morphologyEx(fixedDifferenceImage,fixedDifferenceImage,MORPH_CLOSE,Mat(),Point(-1,-1),2);
imshow("Fixed Foreground Mask", fixedDifferenceImage);
Czwarty argument morphologyEx (macierz) wyznacza sąsiedztwo użyte w operacjach (Mat() oznacza domyślny kwadrat 3x3). Ostatni argument wyznacza z kolei liczbę wykonanych iteracji każdej z operacji. Efekt tej zmiany przedstawia:



W tym przypadku wygląda to bardzo dobrze (w przeciwnym wypadku można pobawić się z inną ilością iteracji, kolejnością), czasami jednak trzeba się uciekać do bardziej skomplikowanych operacji (patrz koniec artykuły)

Jeśli wszystko się udało, mamy teraz dobrze działający program wyznaczający pierwszy plan. Pora nałożyć na niego obraz z kamery.
Aktualną wersję kodu można znaleźć w paczce ze źródłami w pliku: main3_poprawiona_maska.cpp
5
Twoja ocena: Brak Ocena: 5 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com