Domowe efekty specjalne - podmiana tła

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

Nałożenie obrazu na maskę

Chcemy wyczyścić wejściowy obraz zostawiając tylko pierwszy plan. By tego dokonać wystarczy wykonać operację logiczną AND między obrazem wejściowym a maską pierwszego tła. Dodajmy nową macierz na wynik tej operacji oraz na maskę pierwszego planu:
Mat foregroundMask;
Mat foregroundImage;
Operacje logiczne na obiektach typu Mat działają zgodnie z powszechną intuicją, lecz najpierw musimy przekonwertować maskę, aby miała 3 kanały (typy i ilość kanałów w operacjach na macierzach muszą się zgadzać):
cvtColor(fixedDifferenceImage,foregroundMask,CV_GRAY2BGR);
foregroundImage = foregroundMask & inputImage;
imshow("Foreground Image",foregroundImage); 
Przy okazji można usunąć (bądź wykomentować) wyświetlanie się części pośrednich wyników w celu uniknięcia chaosu przy testowaniu programu.

Potrafimy już wyliczyć pierwszy plan, zajmijmy się teraz przygotowaniem nowego tła.



Użycie statycznego tła

Jako etap pośredni użyjemy statycznego obrazka jako nowego tła. Wczytamy plik "forest.jpg" przedstawiający zdjęcie lasu i użyjemy go jako nowego tła. Potrzebujemy dwóch macierzy, dla wczytanego obrazu "forest.jpg" oraz dla przetworzonego tła (z miejscem na pierwszy plan). Dodamy też stałą ze ścieżką do pliku:
Mat currentBackgroundImage;
Mat backgroundImage;
const string staticBackgroundImagePath = "forest.jpg";
Na początku programu wczytajmy obrazek z pliku za pomocą imread:
currentBackgroundImage = imread(staticBackgroundImagePath);
Teraz, aby nałożyć tło na odpowiednie części obrazu, wykorzystamy maskę pierwszego planu, odejmując ją od obrazu tła. Oznacza to, że wszystkie punkty należące do pierwszego planu zostaną usunięte z obrazu tła. Dopiszmy ten kawałek kodu, sprawdzając przy okazji czy udało się wczytać tło:
if(currentBackgroundImage.data!=NULL) {
	backgroundImage = currentBackgroundImage - foregroundMask;
}
Mamy gotowe tło i pierwszy plan, pozostaje tylko połączyć je w jeden obraz. Zadeklarujmy obiekt na wynikowy obraz:
Mat resultImage;
Ponieważ postaraliśmy się, żeby obszary zajęte przez oba obrazy były rozłączne, możemy wyliczyć wynik jako ich sumę:
resultImage = foregroundImage | backgroundImage;
imshow("Result!",resultImage);
Po udanej kompilacji oraz odpowiednim ustawieniu parametrów i kamery okaże się, że jesteśmy w lesie:



Widać trochę nienaturalność tego złożenia (różnica w kolorach, widoczne krawędzie). Poprawmy to trochę poprzez rozmycie obrazu za pomocą metody medianBlur, która zastępuje każdy piksel medianą wartości z lokalnego kwadratu o podanym rozmiarze (w tym przypadku 3x3):
resultImage = foregroundImage | backgroundImage;
medianBlur(resultImage,resultImage,3);
imshow("Result!",resultImage);
Pierwszy plan jest teraz dużo mniej "blokowaty", płacimy za to drobnym rozmyciem:



Aktualną wersję kodu można znaleźć w paczce ze źródłami w pliku: main4_statyczne_tlo.cpp

Użycie zapętlonego wideo jako tła

Przejdźmy teraz do ostatniego etapu, czyli zamiany aktualnie statycznego tła na film. Pierwszy problem to znalezienie odpowiedniego filmu, ale załóżmy, że mamy taki filmik w rozdzielczości 640x480 o nazwie "fishie.avi". Do czytania filmu z pliku służy ta sama klasa VideoCapture co w przypadku pobierania obrazu z kamery. Musimy ją jednak trochę zmodyfikować, tak by wideo było zapętlone tzn. po zakończeniu rozpoczynało się od początku. Dzięki temu nagrywanie stanie się prostsze, bo nie będzie potrzeby ponownego uruchamiania wideo przy każdym ujęciu, wystarczy poczekać na kolejny cykl. Żeby to uwzględnić, rozszerzymy klasę VideoCapture. Przeciążymy metodę grab(), która odpowiada za pobranie nowej klatki, tak by po dotarciu do końca ustawić wskaźnik na początek:
class LoopedVideo : public VideoCapture {
public:
	LoopedVideo() {}
	virtual bool grab() {
		if(VideoCapture::grab()==false) {
			rewind();
		}
		return true;
	}
	void rewind() {
		set(CV_CAP_PROP_POS_FRAMES,0);
	}
};
Metoda grab() zwraca informację, czy udało się uzyskać następną klatkę, więc wykorzystamy to do sprawdzenia, czy jesteśmy już na końcu pliku. Zadeklarujmy ścieżkę do pliku wideo, a także obiekt naszej nowej klasy:
LoopedVideo backgroundMovie;
const string dynamicBackgroundMoviePath = "fishie.avi";
Zastąpmy teraz kod wczytujący tło linią otwierającą plik z filmem:
backgroundMovie.open(dynamicBackgroundMoviePath);
Pozostaje pytanie, jak często wczytywać klatki, by tło zmieniało się z taką samą szybkością jak przy odtwarzaniu. Nam jednak nie jest potrzebna duża dokładność, będziemy więc zmieniać klatkę w naszej głównej pętli przetwarzającej.
camera >> inputImage;
backgroundMovie >> currentBackgroundImage;
imshow("Camera Image",inputImage);
Uruchamiamy program, ustawiamy parametry i cieszymy oczy naszym efektem końcowym:



Nie jest to jeszcze efekt rodem z Hollywood, ale jak na tak prosty program i tak spisuje się nienajgorzej. Należy pamiętać, że pomieszczenie musi być odpowiednio dobrze oświetlone. W przeciwnym wypadku obraz z kamery bardzo traci na jakości - losowe fluktuacje koloru pikseli znaczenie się wtedy nasilają.
Aktualną wersję kodu można znaleźć w paczce ze źródłami w pliku: main5_dynamiczne_tlo.cpp
5
Twoja ocena: Brak Ocena: 5 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com