Segmentacja obrazu oraz wykrywanie i śledzenie punktów

Transkrypt

Segmentacja obrazu oraz wykrywanie i śledzenie punktów
Segmentacja obrazu oraz wykrywanie i śledzenie
punktów charakterystycznych przy pomocy technologii CUDA
Piotr Ćwieczkowski
Wrocław, 24.06.2009
Wstęp
Celem projektu było zaimplementowanie algorytmów związanych z zagadnieniami obserwacji
wideo (video surveillance). W tym celu skupiłem się na dwóch algorytmach – jednym związanym z
segmentacją obrazu na tło oraz ruchome obszary ([1]) oraz metodą wykrywania oraz śledzenia
punktów charakterystycznych. Implementacja korzysta (m.in. do wczytywania filmów) z biblioteki
OpenCV ([11]). W szczególności algorytm segmentacji został zaimplementowany tak, aby mógł być
bezpośrednio używany w programach korzystających z innych algorytmów istniejących w OpenCV.
Segmentacja
Algorytm
Jako część projektu zaimplementowałem algorytm segmentacji tła/ruchomych obiektów
opisany w [1] (pliki GPUBGStatModel.h, GPUBGStatModel.cpp, GPUBGStatModel.cu).
Na podstawie obrazu referencyjnego, przedstawiającego statyczne tło (w tym przypadku jedna z
pierwszych klatek filmów testowych) oblicza on maskę pikseli, które zostały sklasyfikowane jako
ruchome.
W tym celu algorytm tworzy dla każdego piksela macierz złożoną z norm wektorów (kolory
RGB traktujemy jako wektory trójelementowe) zsumowanych na obszarze 3x3 dookoła piksela.
Są to dwie pierwsze fazy przetwarzania klatki: obliczanie norm (jądro calculate_norms) oraz
sumowanie (sum_all).
Następnie obliczany jest próg Mmin, według którego piksele będą klasyfikowane.
W szczególności jeśli dla danego piksela wartość Mmin<0 to jest on automatycznie uznawany za
ruchomy. W przeciwnym wypadku jego przynależność jest (iteracyjnie) obliczana na podstawie jego
otoczenia (piksele, które znajdują się obok ruchomych obszarów mają większą szansę zostać tak
sklasyfikowane).
W tym celu nowa maska jest liczona na podstawie wyniku z poprzedniej klatki. Dla każdego
piksela, jeśli (liczba 'białych' sąsiadów) > Mmin to jest on zaznaczony jako ruchomy. Następnie wynik jest
poprawiany przez iteracyjne propagowanie obszarów ruchomych. W związku z tym obliczamy dwie
wartości Mmin – jedną dla pierwszej iteracji oraz inną dla kolejnych iteracji. Dzięki temu możemy
kontrolować wpływ poprzednich klatek na aktualny wynik. Obie wartości zależą od parametrów
Odc, B, Ts, które można podać przy tworzeniu odpowiednich struktur. Obliczanie progu oraz kolejne
iteracje są wykonywane odpowiednio przez funkcje compute_seg_coef oraz mrf_iteration.
Poprawianie wyników
Po zakończeniu obliczania maski może ona zawierać wiele szumu (pikseli źle rozpoznanych jako
ruch) lub przerw w obszarach ruchomych (niepoprawnie rozpoznanych jako tło). W celu pozbycia się
takich błędów program wykonuje dwukrotnie erozję (erode) oraz dylację (dilate) aby usunąć szum.
Następnie jeszcze kilkukrotnie dylację oraz później tę samą liczbę razy erozję w celu zamknięcia 'dziur'.
W ostatniej (opcjonalnej) fazie algorytmu wypełniane są większe przerwy. Jest to przydatne, gdy
zależy nam na spójnych obszarach. Najpierw jądro blob_distance oblicza odległość każdego piksela
od najbliższego piksela 'ruchomego' od lewej oraz prawej strony. Jądro fill_gaps 'zapala' te piksele,
których suma odległości jest mniejsza od pewnej ustalonej wartości. W ten sposób wszystkie przerwy o
tej długości są wypełniane.
Wyniki
Algorytm tu zaimplementowany daje bardzo dobre wyniki w przypadku użycia statycznego tła
oraz niezmieniającego się oświetlenia. Podczas przetwarzania pliku video2-vlong.avi algorytm
rozpoznaje elementy oświetlone światłem w korytarzu jako ruchome. Istnieje wiele algorytmów (np. te
zaimplementowane w OpenCV), które dostosowują się z czasem do zmian oświetlenia.
Opisana tu metoda nie sprawdza się także przy filmach poza budynkami. Poruszające się liście
drzew są cały czas rozpoznawane jako ruchome. Wiele innych algorytmów przechowuje 'historię'
poprzednich kolorów danego piksela dzięki czemu może wykluczyć takie przypadki.
Możliwe usprawnienia:
− adaptacja obrazu referencyjnego (uaktualnianie go w każdej klatce),
− usprawnienie jądra sum_all,
− automatyczne ustalanie parametrów (np. Odc , Ts),
− optymalizacja pamięci ([1] opisuje sposób znacznego zmniejszenia wymagań pamięciowych na
rzecz utraty przejrzystości algorytmu oraz implementacji),
− uogólnienie procedur erozji oraz dylacji, zmniejszy liczbę ich wywołań (jednak może zwiększyć
liczbę dostępów do pamięci),
− rozszerzenie jądra blob_distance tak aby obliczało także wysokość w pionie.
Porównanie z innymi metodami
Liczba klatek na sekundę
0.57
70
0.36
60
0.23
50
0.14
40
0.09
fps
czas obl i czeń (s )
Czas obliczania segmentacji
0.06
30
20
0.04
10
0.02
0
0.01
1
41
81 121 161 201 241 281 321 361 401
CUDA
MoG
FGD
1
41
81 121 161 201 241 281 321 361 401
CUDA
MoG
FGD
Wykresy przedstawiają czas obliczania segmentacji oraz liczbę klatek na sekundę podczas przetwarzania pliku wideo
test-compressed.avi. Wyniki opisanego tu algorytmu są przedstawione niebieskim kolorem (CUDA).
Są one porównane z dwoma algorytmami (MoG – opisany w [2], FGD - [3]) zaimplementowanymi w bibliotece OpenCV.
System testowy z kartą NVIDIA GeForce 8800GT 512MB oraz procesorem Athlon X2 3800+, 2048 MB RAM.
Algorytm KLT (Kanade-Lucas-Tomasi)
Filtr bilateralny
Algorytmy wyszukiwania punktów charakterystycznych są bardzo czułe na szum występujący
w obrazie. Może on wynikać przede wszystkim z cech kamery, którą film został nagrany.
Szum można usunąć przez zastosowanie np. rozmycia Gaussa. Z drugiej strony metoda wykrywania
punktów opiera się na wyszukiwaniu krawędzi oraz rogów. Rozmywanie obrazów może usunąć ważne
dla nas informacje. Z tego powodu zaimplementowałem filtr bilateralny (pliki BilateralFilter.h oraz
BilateralFilter.cu). Wszelkie informacje na ten temat (zarówno teoretyczne, jak i metody
implementacji, zastosowania, etc.) można znaleźć w [4].
Zaimplementowałem dwie metody rozmywania (o promieniu r): działającą w czasie O(r2)
(bilateral_bf) oraz metodę O(r) (bilateral_Or) opisaną np. w [5].
Pierwsza metoda pomimo złożoności czasowej dla niewielkich promieni (r<15) jest
wystarczająco szybka i w większości przypadków (taka wielkość jest wystarczająca do usunięcia
szumu) właśnie ona jest używana. Rozpatrywałem także możliwość użycia separowalnego filtru. Takie
podejście nie daje poprawnych wyników, jednak w niektórych źródłach jest stosowane (np. do
wygładzania SSAO w NVIDIA DirectX 10 SDK). Niestety przy r rzędu 3-6 artefakty (smugi) stają się
bardzo widoczne.
Metoda O(r) jest o wiele atrakcyjniejsza pod względem złożoności, jednak nie jest łatwa do
zaimplementowania przy użyciu CUDA. Głównym problemem jest zbyt mała ilość pamięci lokalnej dla
wątku. Według algorytmu dla każdego wiersza zostaje wywołany oddzielny wątek. Każdy z nich
przechowuje histogram dla aktualnie obliczanego okna wokół piksela (histogram możemy uaktualnić
za pomocą właśnie O(r) odwołań do tekstury). Wszelkie obliczenia wykonujemy oddzielnie dla
każdego kanału. Musimy zatem przechować 256*3 wartości dla każdego wątku (można także zbadać
czy wyrównanie histogramów do 4 bajtów nie będzie szybsze). Taka liczba danych nie zmieści się w
rejestrach, przez co CUDA automatycznie będzie się do nich odwoływać z tzw. pamięci lokalnej.
Niestety dostęp do tej pamięci (który w tym algorytmie jest całkowicie losowy) nie jest cachowany, a
opóźnienie jest takie samo jak przy użyciu pamięci globalnej (400-600 cykli). Takie rozwiązanie jest
nieoptymalne.
Dane każdego wątku możemy przechować w pamięci wspólnej bloku (shared memory).
Jest ona niezwykle szybka, jednak każdy blok ma do dyspozycji tylko 16KB. Oznacza to że możemy
uruchomić najwyżej 16 wątków w bloku (256*3*16 = 12KB). To także nie jest optymalne rozwiązanie,
ponieważ liczba ta powinna być wielokrotnością rozmiaru warpu (która na większości kart graficznych
wynosi 32 wątki).
Według obliczeń z CUDA Occupancy Calculator przy takiej wielkości bloku multiprocesory (np.
na GeForce 8800GT) są obciążone tylko w 20%. Przy blokach wielkości 128-256 wątków ta wartość
wzrasta powyżej 60%. Innym zagadnieniem kluczowym dla wydajności jądra jest sam schemat
odwołań do pamięci dzielonej.
Należy jednak zauważyć, że algorytm KLT operuje nie na kolorowym obrazie, ale na
intensywnościach pikseli (czyli na obrazie jednokanałowym). Histogram ma zatem łączny rozmiar 256
bajtów. Przez to możemy użyć bloków o rozmiarze np. 32 wątków (możemy nawet 63, ale już nie 64,
ponieważ część pamięci dzielonej jest zarezerwowana). Pomimo tego nie udało mi się uzyskać
zadowalających wyników przy użyciu tego algorytmu.
Należy także wspomnieć o metodzie, która została zaprojektowana z myślą o kartach
graficznych, opisanej w [6] – Bilateral Grid. Niestety nie miałem możliwości jej zaimplementować,
jednak wydaje się być ona najlepszym algorytmem wykonującym filtr bilateralny przy użyciu GPU.
Wykrywanie punktów
Zarówno wykrywanie jak i śledzenie punktów są wykonywane według sposobu opisanego
w [7] oraz [8] (w plikach MotionDetector.h oraz MotionDetector.cu).
Proces ten składa się z kolejnych faz. Najpierw obliczane są intensywności pikseli (jądro
calculate_intensity) oraz gradienty (compute_gradients). Następnie, za pomocą dwóch przejść
(sumujących poziomo oraz pionowo), obliczane są odpowiednie macierze oraz współczynnik
cornerness (sumH oraz sumV_and_compute_cornerness). Po tym obszary wokół już znalezionych
punktów są zerowane (fill_around_points), aby nie znajdować tam niepotrzebnych punktów.
Ostatecznie za pomocą jądra local_maximum pozostawiamy tylko te piksele, których współczynnik
jest w otoczeniu 15x15 największy.
W wyniku tych obliczeń otrzymujemy obraz, w którym niewielka liczba pikseli jest 'zapalona'.
Są to piksele rozpoznane jako punkty charakterystyczne. Aby móc je dalej przetwarzać (np. śledzić)
musimy stworzyć listę zawierającą ich współrzędne. Jest to zadanie, które łatwo można wykonać przy
użyciu procesora. Koszt przesyłania całego obrazu z pamięci GPU jest duży, a istnieją algorytmy
umożliwiające wykonanie tego bezpośrednio na karcie. W tym celu zaimplementowana jest struktura
HistoPyramids opisana w [9] (pliki HistoPyramid.h oraz HistoPyramid.cu). Sam algorytm w bardzo
prostu sposób może być zapisany przy pomocy CUDA, jedynym problemem może być brak obsługi
tablic tekstur, co można łatwo obejść korzystająć z konstrukcji switch (jednak zmniejsza to
elastyczność tej metody – inne możliwości to użycie wolnej pamięci globalnej, lub tekstur 3D).
W ten sposób otrzymujemy listę współrzędnych punktów charakterystycznych. Algorytm ten
można rozszerzyć o wybór punktów, których parametr cornerness spełnia pewne warunki (jest
większy od ustalonego progu). W wielu przypadkach (kiedy część punktów obrazu śledzimy) zależy
nam jedynie na pewnej liczbie punktów dla których ta wartość jest największa. Można to wykonać na
karcie graficznej sortując dane lub wykonując algorytm stream compaction (lub wykonać obliczenia
przy pomocy procesora – przesłanie z karty stosunkowo niewielkiej liczby punktów (<2000) nie
powinno wpływać na wydajność programu).
Śledzenie punktów
Pierwszym etapem śledzenia punktów jest zbudowanie piramidy obrazu dla aktualnej klatki
(przechowujemy także piramidę dla poprzedniej klatki). Obliczamy 3-4 poziomy piramidy, włącznie z
gradientami dla każdego poziomu (jest to wykonywane przez jądra downsize2x2 oraz
compute_gradients).
Następnie zaczynając od najniższego poziomu (najmniejszego obrazu) przybliżana jest nowa
pozycja każdego punktu (update_fpoints). Pozycje są przechowywane według współrzędnych
obrazu wejściowego, więc muszą być przeskalowane odpowiednio do każdego poziomu. Następnie
metodą Newtona przez porównanie obrazów oraz gradientów z poprzedniej klatki (z tego samego
poziomu) obliczane jest optymalne przesunięcie punktu. Przesunięcia obliczone na niższym poziomie
są używane jako początkowe przybliżenia na wyższych poziomach. W przypadku gdy przesunięcia nie
można obliczyć, punkt jest przesuwany poza obraz. Należy zauważyć, że punkty są przechowywane w
pamięci globalnej, przez co możemy je dowolnie czytać lub zapisywać. Dzięki temu nie musimy
wykorzystywać metody 'ping-pong' popularnej przy często używanej w programach GPGPU.
Ostatecznie lista punktów jest kopiowana do pamięci RAM i przy pomocy zwykłego kodu
procesora usuwane są punkty poza obrazem. To zadanie można wykonać przy pomocy GPU (stream
compaction – tak jak opisane wyżej), lecz wyniki przedstawione w [10] pokazują, że algorytm scan jest
wydajniejszy przy pomocy CUDA dopiero przy 36k danych (górna granica na liczbę punktów to 2048).
Wnioski
Implementacja algorytmu KLT przy użyciu technologii CUDA (tak jak implementacja autorów
przy użyciu Cg) jest znacznie szybsza od tych samych algorytmów wykonywanych przy użyciu CPU.
Pomimo błędów przy śledzeniu (wiele punktów jest niepoprawnie 'gubionych'), przez co należy często
wykonywać stosunkowo kosztowne wyszukiwanie punktów, program przetwarza więcej niż 25 klatek
na sekundę (co można zwiększyć przez np. zamianę filtru bilateralnego na szybszy algorytm).
Wydaje mi się jednak, że kluczowym aspektem metod opierających się na punktach
charakterystycznych jest samo wyszukiwanie i rozpoznawanie tych punktów. Te znalezione przez
opisany algorytm mają pewne ważne cechy (np. to że można je łatwo śledzić), jednak jest ich bardzo
dużo, przez co trudno jest je scharakteryzować (użyć np. do rozpoznawania kształtów). Istnieje wiele
prac rozważających bardziej 'przemyślane' wybieranie punktów, np. przez wcześniejsze wycięcie
jedynie obszarów o kolorze skóry, lub wykrywanie części ciała takich jak oczy, dziurki nosa, itd.
Takie rozszerzenia zmniejszają elastyczność i ogólność programu, jednak umożliwiają uzyskiwanie o
wiele lepszych (bardziej wartościowych) wyników.
Podsumowanie
Zarówno podczas implementacji jak i podczas szukania odpowiednich algorytmów w znacznej
mierze poszerzyłem swoją wiedzę na temat wykrywania ruchu oraz zagadnień związanych
z Computer Vision. Przedstawione algorytmu (w szczególności segmentacja) oferują ciekawe wyniki,
zarówno pod względem wydajności jak i jakości rozwiązań. Ponadto pozostawiają one wiele
możliwości przyszłych usprawnień oraz rozszerzeń.
Implementacja przy pomocy CUDA jest bardzo pouczającym doświadczeniem. Pomimo moich
starań w kodzie napisanym przeze mnie wiele procedur (kerneli) można wciąż zoptymalizować.
Programowanie kart graficznych stawia wiele wyzwań związanych chociażby z całkiem nowym
sposobem myślenia o algorytmach oraz metodach przyspieszeń wymuszanych przez architekturę
GPU. Pomimo tego praca włożona w dopracowywanie poszczególnych jąder jest wynagradzana
przez ogromne przyspieszenia w stosunku do kodu CPU.
Biorąc to pod uwagę jestem zadowolony z wyników projektu. Zarówno zdobyte
doświadczenia i wiedza związane z Computer Vision jak i z programowaniem kart graficznych są
bardzo wartościowe oraz dają dużo możliwości dalszej pracy i rozwoju w tych dziedzinach.
Przedstawiony program jest tylko przykładem użycia funkcji, które mogą zostać zastosowane w
innych aplikacjach. W szczególności algorytm segmentacji, który został napisany tak, aby
współpracować z biblioteką OpenCV oraz filtr bilateralny, napisany w 'C for CUDA', który nie wymaga
żadnych innych bibliotek i może służyć w wielu programach zajmujących się przetwarzaniem
obrazów.
Bibliografia
[1] - Real-Time, GPU-based Foreground-Background Segmentation, Andreas Griesser
http://oscar.vision.ee.ethz.ch/gpuseg/
[2] - An improved adaptive background mixture model for real-time tracking with shadow detection,
P. KadewTraKuPong and R. Bowden; http://personal.ee.surrey.ac.uk/Personal/R.Bowden/
[3] - Foreground Object Detection from Videos Containing Complex Background, Liyuan Li, Weimin Huang,
Irene Y.H. Gu, and Qi Tian, ACM MM2003 9p
pewne informacje można znaleźć np. tu: http://opencv.willowgarage.com/wiki/VideoSurveillance
[4] - A Gentle Introduction to Bilateral Filtering and its Applications, Sylvian Paris
http://people.csail.mit.edu/sparis/bf_course/
[5] - Fast Median and Bilateral Filtering, Ben Weiss, Shell & Slate Software Corp.
http://www.shellandslate.com/fastmedian.html
[6] - Real-time Edge-Aware Image Processing with the Bilateral Grid, Jiawen Chen, Sylvain Paris, Frédo Durand
http://groups.csail.mit.edu/graphics/bilagrid/
[7] - GPU-based Video Feature Tracking And Matching, Sudipta N. Sinha, Jan-Michael Frahm, Marc Pollefeys,
Yakup Genc; http://www.cs.unc.edu/~ssinha/Research/GPU_KLT/
[8] - Pyramidal Implementation of the Lucas Kanade Feature Tracker: Description of the algorithm,
Jean-Yves Bouguet; można znaleźć np. w odnośnikach tu: http://www.ces.clemson.edu/~stb/klt/
[9] - GPU Point List Generation through Histogram Pyramids, Gernot Ziiegller, Art Tevs, Chriistiian Theoballt,
Hans-Peter Seiidell; http://www.mpi-inf.mpg.de/~gziegler/
[10] - Parallel Prefix Sum (Scan) with CUDA, Mark Harris
dostępne z NVIDIA CUDA SDK; http://www.nvidia.com/object/cuda_home.html
[11] - OpenCV - http://opencv.willowgarage.com/wiki/

Podobne dokumenty