niedziela, 20 lutego 2011

Splot 2

Na blogu pojawił się już kiedyś post o splocie. Odsyłam do niego, jeżeli chcecie przypomnieć sobie jak to działa. Poniżej przedstawię trochę bardziej zaawansowane techniki dokonywania splotu. Ich celem jest zwiększenie prędkości wykonania. Jest to ważne, gdyż taka operacja jest w niektórych sytuacjach wykorzystywana często, chociażby wyostrzając każdą klatkę obrazu z kamery. Każde zaoszczędzone cykle procesora w wizji się przydadzą :) 
FFT
FFT (Fast Fourier Transform, szybka transformacja Fouriera) jest operacją matematyczną pozwalającą przyspieszyć proces uzyskania sygnału w dziedzinie częstotliwościowej. Operację splotu można opisać następująco: wyznaczenie transformat Fouriera obrazu i maski, wymnożenie odpowiednich częstotliwości, wyznaczenie transformaty odwrotnej obrazu. Samo wyznaczenie sygnału w dziedzinie częstotliwościowej ma podobną złożoność obliczeniową co splot. Dzięki FFT można przyspieszyć ten uciążliwy proces. Dochodzi jednak mnożenie częstotliwości. Aby splot z FFT przyspieszył wykonywany kod, maska musi mieć odpowiednio duży rozmiar. Inną sprawą jest fakt, że do FFT najefektywniejsze są obrazy o rozmiarach będących potęgą dwójki. Niestety wykorzystanie FFT nie pozostaje bez wpływu na wyniki, które różnią się od tych uzyskanych stosując wprost wzór splotu. Są tutaj trochę inne problemy niż przy klasycznym splocie, zainteresowanych odsyłam do stosownej literatury. Kod można znaleźć w dokumentacji OpenCV.
Rozbicie operacji na mniejsze
Inną operacją pozwalającą zmniejszyć liczbę operacji jest robicie ich na mniejsze. Pewna grupa filtrów umożliwia rozbicie maski na dwa wektory. Przykładem jest filtr Sobela, którego maska wygląda następująco:
Macierz ta jest wynikiem mnożenia dwóch wektorów:

W takim przypadku można najpierw zastosować splot z jednym z wektorów a następnie na jego wyniku wykonać splot z drugim wektorem znacząco redukując przy tym liczbę mnożeń i dodawań. Do tego celu przeznaczona jest w OpenCV specjalna funkcja o nazwie sepFilter2D, bardzo podobna do filter2D z tym, że przyjmuje dwie maski, kolejno poziomą i pionową. Trzeba jednak wziąć po uwagę ewentualne pośrednie wyniki. Przy klasycznej masce Sobela mogą wystąpić wartości ujemne, natomiast w przypadku wektora [1,2,1] wykroczyć poza dopuszczalny zakres.
GPU
Inną metodą przyspieszenia splotu jest zaprzęgnięcie do obliczeń karty graficznej. OpenCV w wersji 2.2 wykorzystuje możliwości udostępnione przez firmę NVDIA w CUDA. Na marginesie sama NVIDIA zadeklarowała pomoc w rozwijaniu OpenCV. Aby dowiedzieć się więcej o programowaniu GPU polecam artykuł na wikipedii. W skrócie chodzi o fakt, że obliczenia te są mocno równoległe co znacznie przyspiesza pewien zakres operatorów matematycznych. Poza tym, odciąża ona CPU, który można wykorzystać do innych obliczeń. Samo programowanie GPU nie należy do łatwych, trzeba przestawić się na trochę inne myślenie, inaczej projektować algorytmy. Na szczęście OpenCV udostępnia uproszczone metody do działania na karcie graficznej. Znajdują się one w przestrzeni nazw gpu a zadeklarowane są w pliku nagłówkowym gpu.h. I tak mamy dwie metody dokonujące splotu:
void filter2D(const GpuMat& src,
GpuMat& dst,
int ddepth,
const Mat& kernel,
Point anchor=Point(-1,-1));
void sepFilter2D(const GpuMat& src,
GpuMat& dst,
int ddepth,
const Mat& kernelX,
const Mat& kernelY,
Point anchor = Point(-1,-1),
int rowBorderType = BORDER DEFAULT,
int columnBorderType = -1);
Najistotniejsza zmiana to wykorzystanie GpuMat zamiast Mat. Jest to spowodowane innym rodzajem pamięci dostępnej w GPU. Dla nas najważniejsze jest to, że można łatwo zamieniać jedną na drugą. Dostępny jest konstruktor GpuMat przyjmujący jako parametr referencję do Mat, oraz dwie metody pozwalające na przesyłanie danych i pobieranie ich:
void upload(const cv::Mat& m); // pobranie wartości z m
void download(cv::Mat& m) const; // zapisanie wartości do m
Same metody fiter2d oraz sepFilter2D działają tak samo jak ich "normalne" odpowiedniki. Trzeba tylko pamiętać, że na karcie graficznej dostępne są jedynie tablice floatów, więc przesłanie w GpuMat wartości typu uchar spowoduje błąd. Podobno autorzy mają usunąć to ograniczenie w przyszłości. Ponadto samo przesyłanie danych, ich odczyt a nawet samo zainicjowanie korzystania z GPU ma pewien narzut, więc przydaje się ono w często wykorzystywanych splotach, lub tych z wykorzystaniem wielki obrazów i masek. Na koniec małe porównanie czasu wykonania stu splotów z obrazem 800 na 600 pikseli  pomiędzy CPU a GPU (testowano na Intel C2D 2,4 GHz i GeForce 9600 GT)
CPU:
                filter2D   sepFilter2D
maska 3x3:       1,04s        0,40s
maska 15x15:     3,90s        2,99s  
GPU:
                filter2D   sepFilter2D
maska 3x3:       0,01s        0,28s
maska 15x15:     0,06s        0,69s 

sobota, 5 lutego 2011

Instalacja OpenCV 2.2

Zaczynając nowy cykl powrót do korzeni, czyli instalacja OpenCV :) Tym razem skupię się na najnowszej wersji tej biblioteki w wersji 2.2. Przynosi ona kilka istotnych zmian. Pierwszą z nich jest nowy podział biblioteki na moduły. Tym razem jest ich znacznie więcej i są bardziej wyspecjalizowane.:
  • core - podstawowe struktury, algebra, obsługa XML/YAML,
  • imgproc - operacje na obrazach (filtracje, konwersje kolorów, zmiana rozmiarów itp.),
  • highgui - obsługa I/O i GUI,
  • ml - uczenie maszynowe (sztuczne sieci neuronowe i inne statystyczne),
  • features2d - detektory punktów i obszarów charakterystycznych na płaskich obrazach,
  • video - analiza ruchu, śledzenie obiektów,
  • objdetect - detekcja obiektów (np. twarzy),
  • calib3d - kalibracja kamer, stereowizja,
  • flann - po prostu FLANN ;-),
  • contrib - kod wstępnie zatwierdzony, ale nie stabilny na rozwinięty na tyle by stanowił integralną część biblioteki,
  • legacy - zapewnia wsteczną kompatybilność,
  • gpu - część algorytmów z wykorzystaniem wsparcia obliczeń na kartach graficznych.
Jak widać, trochę się to rozrosło, ale będzie chyba łatwiej ograniczyć się podczas tworzenia własnego projektu do niezbędnych części OpenCV.
Linux
OpenCV na linuksie zainstalować można na dwa sposoby. Pierwszy banalny (na przykładzie Ubuntu):
Uruchamiamy menadżer pakietów Synaptic, szukamy "opencv", zaznaczamy i instalujemy (niestety obecnie jest to wersja 2.1, trzeba poczekać na aktualizację).
Metoda druga pozwala na większą kontrolę nad tym co i jak instalujemy. Pobieramy źródła opencv, rozpakowujemy, a następnie uruchamiamy cmake (można doinstalować w synapticu, polecam wersję z interfejsem graficznym ;-).  Uruchamiamy cmake-gui i wybieramy katalog ze źródłami oraz miejsce do zbudowania wersji binarnej (przydatne także do usuwania OpenCV):
Klikamy przycisk Configure i dostajemy wynik pierwszego przebiegu
Na czerwono podświetlone są pozycje które wymagają weryfikacji (na początku wszystkie), tutaj można zaznaczyć ewentualne opcje których potrzebujemy (np. obsługa okien z wykorzystaniem Qt)
W ten sposób można też wybrać wiele innych rzeczy, np. czytanie niektórych formatów pilków, które wymaga dodatkowych bibliotek w wersji dev do zainstalowania z synaptica. Tutaj można wskazać też takie dodatkowe rzeczy jak wykorzystanie GPU czy IPP. Kiedy już wszystko zostanie poprawione ponownie klikamy Configure. Na czerwono zostaną zaznaczone tylko te pozycje, które wymagają ponownej weryfikacji lub mają niespełnione zależności:
Tutaj tylko weryfikujemy i klikamy ponownie Configure (dodam tylko, że Qt musi być doinstalowane ręcznie, np. z Synaptica). Teraz pozostaje tylko kliknięcie Generate i jeżeli wszysko było ok, otrzymaliśmy wersję gotową do kompilacji.
Uruchamiamy konsolę i wchodzimy do katalogu w którym została ona utworzona. Wpisujemy make (z parametrem -j 2 jeżeli posiadamy dwurdzeniowy procesor, -j 4 jeżeli cztery rdzenie itd.) a następnie jeżeli wszystko przebiegło bez problemów sudo make install i cieszymy się nowym OpenCV :) W ten sposób można też instalować najświeższą wersję z SVNa. Trzeba pamiętać by odinstalować starą wersję przed instalacją innej poleceniem w konsoli sudo make uninstall.

Przykładowy program na linuksie
Do pisania programów w C++ używam ostatnio Qt Creatora. Tworzymy nowy projekt, wybieramy aplikację konsolową, ustawiamy nazwę projektu i jego położenie.
Początkowo mamy dwa pliki: *.pro z opcjami projektu oraz *.cpp z kodem. Plik pro przetrzymuje opcje projektu, takie jak dołączone biblioteki. Zmieniamy jego zawartość na następującą 
TARGET = pierwszy
CONFIG   += console
CONFIG   -= app_bundle
TEMPLATE = app
SOURCES += main.cpp
INCLUDEPATH += /usr/local/include/opencv2
LIBS += -L/ur/local/lib \
    -lopencv_highgui
gdzie  to nazwa pliku pro będąca też nazwą projektu. Zwrócić uwagę należy na INCLUDEPAH gdzie wskazano katalog z plikami nagłówkowymi OpenCV oraz LIBS gdzie deklaruje się użyte biblioteki. Znacznikiem -L wskazujemy katalog z bibliotekami (należy zwrócić uwagę na brak odstępu pomiędzy -L a początkiem ścieżki). Znak \ wskazuje, że kontynuujemy zawartość LIBS w kolejnej linii. Bibliotekę wskazujemy znacznikiem -l (także bez spacji) zgodnie z konwencją uniksową. Jeżeli plik biblioteki nazywa się libXXX.so to omijamy przedrostek lib. Do main.cpp wsawiamy przykładowy kod użyty w dokumentacji
#include <highgui/highgui.hpp>
#include <opencv.hpp>

using namespace cv;

int main(int, char**)
{
    VideoCapture cap(0);
    if(!cap.isOpened())
        return -1;
    Mat edges;
    namedWindow("edges",1);
    for(;;)
    {
        Mat frame;
        cap >> frame;
        cvtColor(frame, edges, CV_BGR2GRAY);
        GaussianBlur(edges, edges, Size(7,7), 1.5, 1.5);
        Canny(edges, edges, 0, 30, 3);
        imshow("edges", edges);
        if(waitKey(30) >= 0) break;
    }
 
    return 0;
}
Klikamy Uruchom (skrót klawiszowy Ctrl+R) i jeżeli wszystko jest ok, oglądamy obraz z kamery z wykrytymi krawędziami

Windows
W Windowsie ściągamy plik instalacyjny z wersją binarną (OpenCV-2.2.0-win32-vs2010.exe) i instalujemy w standardowy windowsowy sposób pamiętając o zaznaczeniu po drodze o dodaniu do PATHa
Inną metodą jest instalacja ze źródła. Podobnie jak w linuksie wykorzystujemy do tego cmake, do ściągnięcia ze strony autorów. Należy także zaznaczyć dodanie CMake do PATHa i uruchamiania go z uprawnieniami administratora. Do instalacji ze źródeł ściągamy plik OpenCV-2.2.0-win.zip. Dalej postępujemy tak jak w przypadku linuksa, z tym że wybieramy po drodze odpowiedni kompilator (u mnie Visual Studio 10):
Dzięki takiemu wyborowi dostajemy odpowiednie pliki projektów do Visual Studio 10 (np. INSTALL).
Przykładowy program na windowsie
Uruchamiamy Visual Studio 10 i tworzymy nowy projekt C++
Przechodzimy do opcji. W zakładce C/C++ dodajemy folder z plikami nagłówkowymi
Następnie w zakładce Linker/General należy dodać katalog z bibliotekami
a w Linker/Input biblioteki z których korzystamy
Dopisujemy kod programu i uruchamiamy :) Na windowsie miałem z tym masę problemów, do teraz nie wiem czemu to raz działa, raz nie (bo 64-bity, bo sterownik do kamery dziwny itd.), więc tymczasowo podziękuję systemowi Microsoftu...

środa, 29 grudnia 2010

Ankieta

Blog został trochę odświeżony. Mam nadzieję, że znajdę teraz trochę więcej czasu do pisania notek :) Jak można zauważyć, po prawej dodałem BLIPa, na którym przekazywał będę co ciekawsze linki do artykułów czy blogów z okolic wizji komputerowej i jej zastosowań. Ponadto pojawiły się ankiety, zachęcam do głosowania. Zauważyłem, że najczęściej na bloga wchodzi się z wyszukiwarek pod hasłem "kurs opencv" oraz "opencv". Najpopularniejsze posty także dotyczą wyłącznie OpenCV, stąd pomysł ankiety i poznania preferencji czytelników :)
Przez te ponad pół roku od ostatniego wpisu dostałem ogromną liczbę maili z prośbą o pomoc. Nie wszystkim udało mi się odpisać (nawet nie połowie...), co wynikało z braku czasu. Bardzo dużo osób pisało w sprawie kompilacji biblioteki jak i samych programów. Widzę, że to poważny problem, postaram się go trochę pociągnąć na bardzo podstawowym poziomie.

wtorek, 6 kwietnia 2010

OpenCV 2.1

Dzisiaj pojawiła się nowa wersja OpenCV oznaczona numerem 2.1. Najważniejsze zmiany od poprzedniej wersji 2.0 to:
  • poprawa ogromnej ilości błędów (o których już kiedyś przy okazji 2.0 wspominałem...)
  • zamiana OpenMP na TBB
  • nowy interfejs dla Pythona
  • lepsze wsparcie 64-bitowych aplikacji dla systemów Windows i MacOS
  • całkowite przejście na obsługiwanie błędów poprzez wyjątki
  • całkowite przejście na CMake dla wszystkich systemów
  • implementacja algorytmu GrabCut
  • poprawa algorytmów związanych ze stereografią
Pozostaje pobrać i przetestować ;-)

niedziela, 4 kwietnia 2010

Intel IPP i OpenCV

Ostatnio mam potrzebę zastosowania innej wizyjnej biblioteki - Intel® Integrated Performance Primitives (w skrócie IPP). Jak nazwa wskazuje, podobnie jak OpenCV, powstała ona przy udziale firmy Intel. Jest to biblioteka o wysokiej optymalizacji dzięki czemu osiąga ona niskie czasy wykonania. Istnieje możliwość przyspieszenia OpenCV gdy wcześniej zainstalowano IPP. Obecnie ma to już marginalne znaczenie. Wspieranych jest jedynie kilka funkcji, m.in. klasyfikator Harra czy DFT. Więcej o bibliotece powinno pojawić się na blogu w przyszłości, teraz skupię się na jednym aspekcie.

Pomimo że w IPP zawarto pokaźną liczbę niskopoziomowych funkcji, brakuje w niej odpowiednika highgui z OpenCV. Powoduje to, że wszystkie dane musimy alokować samodzielnie, samodzielnie też przetwarzać odpowiednie formaty plików graficznych. Z pomocą może nam jednak przyjść OpenCV ;-). Możemy wykorzystać cvLoadImage i operować na IplImage. Spójrzmy na przykładowy kod wyznaczający laplasjan obrazu:
#include < highgui.h >
#include < ippi.h >

int main() {
const char * str = "{ścieżka do pliku}";
IplImage * wczytanyObraz = cvLoadImage(str,CV_LOAD_IMAGE_GRAYSCALE);

IppiSize size;
size.height = wczytanyObraz->height;
size.width = wczytanyObraz->width;
int step = 0;

Ipp8u *poFiltracji = ippiMalloc_8u_C1(size.width,size.height,&step);
ippiFilterLaplace_8u_C1R((Ipp8u*)wczytanyObraz->imageData,wczytanyObraz->widthStep,poFiltracji,step,size,ippMskSize5x5);
IplImage * doWyswietlenia = cvCreateImageHeader(cvGetSize(wczytanyObraz),8,1);
cvSetData(doWyswietlenia,(void*)poFiltracji,step);

cvShowImage("okno",doWyswietlenia);
while(true)
{
int key = cvWaitKey(100);
if(key == (int) ' ')
break;
}
ippiFree(poFiltracji);
cvReleaseImage(&wczytanyObraz);
return 0;
}


W linii 6. wczytujemy z dysku plik. Robimy to z użyciem HighGui. w liniach 8-11 przygotowujemy dane potrzebne w funkcji IPP obliczającej laplasjan. W linii 13. alokujemy pamięć na obraz wynikowy. Funkcja ta alokuje obraz 8-bitowy o jednym kanale na co wskazuje jej nazwa (8u i C1). Więcej o nazewnictwie IPP w przyszłości. W linii 14. obliczamy właściwy wynik. I tutaj najważniejsze: jako źródło (w tym przypadku wskaźnik na Ipp8u) używamy imageData z IplImage, z kolei jako przeznaczenie używamy naszego zaalokowanego Ipp8u. Druga ważna rzecz dzieje się w liniach 15-16. Zamiast tworzyć obraz tworzymy tylko jego nagłówek. Dane natomiast ustawiamy korzystając ze wskaźnika na Ipp8u. Można jeszcze zwrócić uwagę na linię 25., gdzie zwalniamy zaalokowaną przez IPP strukturę.

Powyżej pokazano jak mieszać OpenCV i IPP. Przyznam, że powyższy kod to efekt moich prób jak uzyskać płynne przejście między OpenCV i IPP bez kopiowania danych między strukturami. Metoda może więc nie być idealna, lub co gorzej, mieć jakieś ukryte błędy... Jak ktoś zna lepsze podejście to proszę o podzielenie się w komentarzach :)

środa, 24 marca 2010

Segmentacja skóry oparta o reguły Bayesa

Dzisiaj będzie trochę o prostej segmentacji skóry związanej z regułami Bayesa. Metody które opisywałem wcześniej, opierały się jedynie o wektor w pewnej przestrzeni kolorów. Powodowało to, że w różnych warunkach oświetleniowych czy nawet przy różnych kamerach wyniki znacząco się różniły. Uzyskiwaliśmy jednak wynik w postaci binarnej. Dzięki regułom Bayesa możemy w pewien sposób zapobiec wspomnianym różnicom, ale wynik będzie w zbiorze rozmytym - każdy piksel będzie na X % skórą. Poniżej opiszę krótko metodę, zaprezentuję kilka obrazów wynikowych a na końcu zamieszczę kod.

Reguły Bayesa

O regułach Bayesa można przeczytać np. na wikipedii (na angielskiej jest bardziej rozbudowany artykuł). W skrócie chodzi o ustalenie prawdopodobieństwa warunkowego. Dobrze ilustruje to przykład z wikipedii angielskiej którego sens wygląda mniej więcej tak:

Do pewnej szkoły uczęszcza 10 chłopców i 12 dziewczyn. Połowa dziewczyn nosi spodnie, połowa spódnice. Wszyscy chłopcy noszą spodnie. W oddali widzisz ucznia idącego do szkoły, widzisz tylko, że na pewno ma spodnie. Jaka jest szansa, że jest to dziewczyna?
Odpowiedź można uzyskać z reguł Bayesa. Sytuację tą przenosimy na detekcję skóry...

Twarz

Twarz można wykryć używając różnych metod (ja zastosuję tą znaną z bloga). Jest ona obszarem pokrytym skórą (zazwyczaj ;-)), więc może być dla nas źródłem informacji. Zakładamy, że wszystko co jest w obszarze twarzy jest skórą, a wszystko co poza nim, skórą nie jest (ważne jest zatem, aby inne części ciała lub twarze innych osób nie znalazły się w kadrze). Sama ramka nie rozdziela wyraźnie obszaru twarzy od otoczenia. Większą precyzję można uzyskać korzystając z algorytmu watershed. Działa on podobnie do zaznaczania różdżką w popularnych programach graficznych

Wathershed

Za ten algorytm odpowiada metoda

void cvWatershed( const CvArr* image, CvArr* markers );

Pierwszy parametr to kolorowy obraz, a drugi to 32-bitowa macierz. o identycznych jak image rozmiarach. W parametrze markers zapisujemy liczby 1,2,3 ... oznaczające, że dany piksel jest klasy 1.,2.,3. itd a 0 dla pikseli dla których to algorytm ma przydzielić klasę. Po wykonaniu funkcji w parametrze markers piksele będą miały już tylko wartości klas bądź -1 dla granic pomiędzy klasami. Dzięki temu uzyskamy piksele skóry i pozostałe z większą dokładnością niż prostokątna ramka. Zdarzają się oczywiście przekłamania, ale nic nie jest idealne, a metoda ta zdecydowanie poprawia uzyskiwane wyniki. Piksele klasyfikujemy przez kilkadziesiąt klatek a dane zbieramy do histogramów

Histogram

Potrzebne są dwa trójwymiarowe histogramy, jeden dla skóry i jeden dla pozostałych pikseli. Każdy z wymiarów odpowiada kanałowi obrazu kolorowego. Rozmiar każdego z wymiarów może być 256 co dopowiada wszystkim dopuszczalnym wartościom, ale lepiej sprawuje się mniejsza liczba wymiarów np. 64. Wówczas minimalne różnice nie wpływają aż tak na wynik, a uzyskany model skóry jest bardziej ogólny. Histogramy następnie normalizujemy i używamy w kolejnych klatkach do klasyfikowania czy dany piksel jest skórą czy nie.

Metoda jest szybka, wymaga jedynie wstępnego procesu "uczenia". Gdy zmienią się warunki oświetleniowe najlepiej algorytm rozpocząć od nowa. Poniżej wspomniane obrazy wynikowe

Uczenie:

Mapa prawdopodobieństwa:

Próg 30% (piksele o prawdopodobieństwie bycia skórą 30% i wyższej)

Próg 60%

Próg 90%

Kod dostępny jest na GitHubie. Program ma kilka parametrów opcjonalnych, wypisują się one w konsoli. Aby rozpocząć uczenie należy nacisnąć klawisz 'n'. Najlepiej poczekać chwilę, aż ustabilizuje się obraz. Później widzimy jak zaznaczane są kolejne obszary twarzy a na końcu widzimy działanie algorytmu dla kolejnych klatek z kamery. Program kończymy klawiszem 'k'.

wtorek, 2 marca 2010

Śledzenie blobów

Dzisiaj trochę o śledzeniu blobów. OpenCV udostępnia tracker o budowie modułowej do śledzenia takich struktur. Niestety nie jest on za dobrze opisany, jedyne co na jego temat znalazłem to ta nieoficjalna dokumentacja. Tracker to klasa CvBlobTrackerAuto. Jako parametr przyjmuje strukturę CvBlobTrackerAutoParam1. Struktura ta przechowuje moduły trackera. Jakie to moduły? Segmentator tła, detektor blobów, tracker blobów, moduł wygładzania trajektorii ruchu i inne. Ładny schemat blokowy znajduje się w opisie na "nieoficjalnej" dokumentacji. Struktura parametrów wygląda następująco:
struct CvBlobTrackerAutoParam1
{
int FGTrainFrames;
CvFGDetector* pFG; // segmentator tła
CvBlobDetector* pBD; // detektor blobów
CvBlobTracker* pBT; // tracker blobów
CvBlobTrackGen* pBTGen; 
CvBlobTrackPostProc* pBTPP; 
int UsePPData;
CvBlobTrackAnalysis* pBTA;
};

Komentarz dodałem obok najważniejszych modułów (reszta jest opcjonalna). Tracker tworzymy za pomocą funkcji CvBlobTrackerAuto* cvCreateBlobTrackerAuto1(CvBlobTrackerAutoParam1* param = NULL);, gdzie, jak widać, przekazujemy nasze parametry. Dla standardowo dostępnych modułów mamy funkcje budujące odpowiednie obiekty. I tak np. dla detektora blobów mamy funkcję cvCreateBlobDetectorXXX gdzie XXX to Simple lub CC. Moduły są klasami wirtualnymi, nic nie stoi na przeszkodzie pisania własnych. Tak też uczynimy, żeby móc śledzić skórę. Użyłem prostego algorytmu segmentacji z poprzedniego wpisu. Klasa CvFGDetector wygląda następująco:
class CV_EXPORTS CvFGDetector: public CvVSModule
{
public:
CvFGDetector(){SetTypeName("FGDetector");};
virtual IplImage* GetMask() = 0;
/* Process current image: */
virtual void    Process(IplImage* pImg) = 0;
/* Release foreground detector: */
virtual void    Release() = 0;
};

Trzy metody do przedefiniowania :) Uzyskujemy nową klasę SimpleSkinDetector :
class SimpleSkinDetector : public CvFGDetector
{
IplImage * maska;
public:

SimpleSkinDetector()
{
maska = 0;
SetTypeName("SSD");
};

virtual IplImage* GetMask()
{
return maska;
}

/* Process current image: */
virtual void Process(IplImage* img)
{
if (!maska)
maska = cvCreateImage(cvGetSize(img), 8, 1);

for (int y = 0; y < img->height; y++)
{
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
uchar* ptrRet = (uchar*) (maska->imageData + y * maska->widthStep);
for (int x = 0; x < img->width; x++)
{
double b, g, r;

b = ptr[3 * x];
g = ptr[3 * x + 1];
r = ptr[3 * x + 2];
double min = b;
double max = b;
if (min > g)
min = g;
if (min > r)
min = r;

if (max < g)
max = g;
if (max < r)
max = r;

if (r <= 95 ||
g <= 40 ||
b <= 20 ||
max - min <= 15 ||
fabs(r - g) <= 15 ||
r <= g ||
r <= b)
{
ptrRet[x] = 0;
}
else
{
ptrRet[x] = 255;
}
}
} 
cvErode(maska, maska, NULL, 1);
cvDilate(maska, maska, NULL, 1);
};

/* Release foreground detector: */
virtual void Release()
{
if (maska)
cvReleaseImage(&maska);
}
};

Wynik działania trackera wygląda następująco

Poniżej kod programu
#include < vector >
#include < cv.h >
#include < cvaux.h >
#include < highgui.h >
#include < iostream >
#include < list >

using namespace std;

void rysujPunkty(IplImage * obr, int x, int y, CvScalar col)
{
int x1 = x - 5;
int x2 = x + 5;
int y1 = y - 5;
int y2 = y + 5;
if (x1 < 0)
x1 = 0;
if (x2 > obr->width - 1)
x2 = obr->width - 1;
if (y1 < 0)
y1 = 0;
if (y2 > obr->height - 1)
y2 = obr->height - 1;
cvLine(obr, cvPoint(x1, y), cvPoint(x2, y), col);
cvLine(obr, cvPoint(x, y1), cvPoint(x, y2), col);
}

class SimpleSkinDetector : public CvFGDetector
{
IplImage * maska;
public:

SimpleSkinDetector()
{
maska = 0;
SetTypeName("SSD");
};

virtual IplImage* GetMask()
{
return maska;
}

/* Process current image: */
virtual void Process(IplImage* img)
{
if (!maska)
maska = cvCreateImage(cvGetSize(img), 8, 1);

for (int y = 0; y < img->height; y++)
{
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
uchar* ptrRet = (uchar*) (maska->imageData + y * maska->widthStep);
for (int x = 0; x < img->width; x++)
{
double b, g, r;

b = ptr[3 * x];
g = ptr[3 * x + 1];
r = ptr[3 * x + 2];
double min = b;
double max = b;
if (min > g)
min = g;
if (min > r)
min = r;

if (max < g)
max = g;
if (max < r)
max = r;

if (r <= 95 ||
g <= 40 ||
b <= 20 ||
max - min <= 15 ||
fabs(r - g) <= 15 ||
r <= g ||
r <= b)
{
ptrRet[x] = 0;
}
else
{
ptrRet[x] = 255;
}
}
}
cvErode(maska, maska, NULL, 1);
cvDilate(maska, maska, NULL, 1);
};

/* Release foreground detector: */
virtual void Release()
{
if (maska)
cvReleaseImage(&maska);
}
};

CvScalar kolorki[] = {
CV_RGB(255, 255, 255),
CV_RGB(255, 0, 255),
CV_RGB(0, 255, 255),
CV_RGB(255, 255, 0),
CV_RGB(0, 0, 255),
CV_RGB(255, 0, 0),
CV_RGB(0, 255, 0)
};

void rysujObszar(IplImage*dst, CvBlob* blob, CvScalar kol)
{
CvRect rect = CV_BLOB_RECT(blob);
cvSetImageROI(dst, rect);
cvAddS(dst, kol, dst);
cvResetImageROI(dst);
}

int main(int argc, char** argv)
{

CvCapture* cam = NULL;
if (argc > 1)
cam = cvCreateCameraCapture(atoi(argv[1]));
else
cam = cvCreateCameraCapture(0);

cvNamedWindow("Oryginał", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Skóra", CV_WINDOW_AUTOSIZE);
cvNamedWindow("Bloby", CV_WINDOW_AUTOSIZE);

//+++++++++++++++++++++++++++++++++++++++

CvBlobTrackerAutoParam1 params;
CvBlobTrackerAuto* tracker;

SimpleSkinDetector skd;

params.pFG = &skd;
params.FGTrainFrames = 0;
params.pBD = cvCreateBlobDetectorSimple();
params.pBT = cvCreateBlobTrackerMS();
params.pBTA = cvCreateModuleBlobTrackAnalysisHistPVS();
params.pBTGen = cvCreateModuleBlobTrackGen1();
params.pBTPP = cvCreateModuleBlobTrackPostProcKalman();

tracker = cvCreateBlobTrackerAuto1(¶ms);

//+++++++++++++++++++++++++++++++++++++++

IplImage * _img = cvQueryFrame(cam);
while (true)
{
_img = cvQueryFrame(cam);
// optymalizacja przez zmniejszenie analizowanego obszaru
CvSize polowa = cvSize(_img->width / 2.0, _img->height / 2.0);
IplImage* _img2 = cvCreateImage(polowa, 8, 3);
IplImage * _skinImg = cvCreateImage(polowa, 8, 1);
IplImage* _skinImgTemp = skd.GetMask()
cvResize(_skinImgTemp, _skinImg);
cvResize(_img, _img2);
IplImage * _cross = cvCreateImage(polowa, 8, 3);
cvZero(_cross);

tracker->Process(_img2, NULL);

if (tracker->GetBlobNum() > 0)
{
for (int i = tracker->GetBlobNum(); i > 0; i--)
{
CvScalar kolor = kolorki[i % 7]; // ustalamy kolor
CvBlob* pB = tracker->GetBlob(i - 1); // pobieramy bloba
CvPoint p = cvPointFrom32f(CV_BLOB_CENTER(pB)); // makra CV_BLOB_XXX pozwalaja uzyskac dane o blobach
// rysujemy wyniki
rysujPunkty(_cross, p.x, p.y, kolor);
CvSize s = cvSize(MAX(1, cvRound(CV_BLOB_RX(pB))), MAX(1, cvRound(CV_BLOB_RY(pB))));
rysujObszar(_img2, pB, cvScalar(255, 0, 0));
cvRectangle(_cross, cvPoint(p.x - s.width, p.y - s.height), cvPoint(p.x + s.width, p.y + s.height), kolor, 2, CV_AA);
} 
}

cvShowImage("Oryginał", _img2);
cvShowImage("Skóra", _skinImg);
cvShowImage("Bloby", _cross);
cvReleaseImage(&_skinImg);
cvReleaseImage(&_skinImgTemp);
cvReleaseImage(&_cross);
cvReleaseImage(&_img2);

int key = cvWaitKey(3);
if (key == ' ')
break;
}

cvDestroyAllWindows();
// sprzatamy
if (cam)cvReleaseCapture(&cam);
if (params.pBT)cvReleaseBlobTracker(¶ms.pBT);
if (params.pBD)cvReleaseBlobDetector(¶ms.pBD);
if (params.pBTGen)cvReleaseBlobTrackGen(¶ms.pBTGen);
if (params.pBTA)cvReleaseBlobTrackAnalysis(¶ms.pBTA);
if (params.pFG)cvReleaseFGDetector(¶ms.pFG);
if (tracker)cvReleaseBlobTrackerAuto(&tracker);

return 0;
}

poniedziałek, 15 lutego 2010

Prosty detektor skóry oparty o wektor RGB

Witam po długiej przerwie. Już wiele razy pisałem o tym co planuję, wiele pomysłów na tego bloga przechodziło przez myśl. Ostatecznie wyszło tak, że nie pisałem nic. Więc porzucam te wszystkie pomysły i pisać będę krótsze notki, może częściej, może rzadziej, ale przynajmniej blog będzie dawał czasami znak życia :)

Dzisiaj powraca temat detekcji skóry, ponieważ ostatnio dość mocno nad tym pracuję. Jak kiedyś o tym wspomniałem, najprostsze techniki opierają się o proste zależności między wartościami odpowiednich kanałów obrazu. Dzisiaj jedna z takich metod dająca zaskakująco dobre wyniki (zwłaszcza po zastosowaniu erozji ;). Jej opis znajdziemy tutaj pod tytułem "Human Skin Colour Clustering for Face Detection". Nie owijając w bawełnę, poniżej kod.
IplImage* process(IplImage* img)
{
IplImage * toRet = cvCreateImage(cvGetSize(img), 8, 1);

for (int y = 0; y < img->height; y++)
{
uchar* ptr = (uchar*) (img->imageData + y * img->widthStep);
uchar* ptrRet = (uchar*) (toRet->imageData + y * toRet->widthStep);
for (int x = 0; x < img->width; x++)
{
double b, g, r;

b = ptr[3 * x];
g = ptr[3 * x + 1];
r = ptr[3 * x + 2];
double min = b;
double max = b;
if (min > g)
min = g;
if (min > r)
min = r;
if (max < g)
max = g;
if (max < r)
max = r;
if (r <= 95 ||
g <= 40 ||
b <= 20 ||
max - min <= 15 ||
fabs(r - g) <= 15 ||
r <= g ||
r <= b)
{
ptrRet[x] = 0;
}
else
{
ptrRet[x] = 255;
}
}
}
cvErode(toRet,toRet,NULL,1);
return toRet;
}
W komentarzach możecie podzielić się "wrażaniami" :) U mnie na jednej kamerce działa świetnie, na drugiej gorzej niż średnio :) Wynik działania:
Światło Kamera 1 Kamera 2
Naturalne
Sztuczne
Naturalne + Sztuczne

poniedziałek, 21 września 2009

Podstawowe klasy w OpenCV


Ostatnio wspomniałem o nowym interfejsie w OpenCV, a dzisiaj przedstawię jego podstawowe klasy. Przede wszystkim nie będzie już struktur CvArr, CvMat i IplImage - wszystkie one zastąpione zostają klasą Mat. Tak jak reszta klas i funkcji, znajduje się ona w przestrzeni nazw cv. Nie chcę tutaj przepisywać dokumentacji, więc zademonstruję tylko na przykładzie "szumu" z telewizora ;-) Kod źródłowy (na githubie) oraz efekt:

Na początku zaznaczamy użycie przestrzeni nazw cv. Spowodowało to zniknięcie prefiksu "cv" z nazw funkcji. I tak nie ma już cvNamedWindow ale namedWindow. Parametry są niemal identyczne (jedyna zmiana w tej i podobnych funkcjach to użycie obiektu string zamiast wskaźnika char*). Dalej mamy obiekt klasy Mat. Posiada on wiele konstruktorów. W przykładowym kodzie pokazano tworzenie macierzy liczb typu double. Analogicznie można stworzyć np. macierz wartości boolowskich podając DataType< bool>::type itd. Można także deklarować według starego typu podając jako parametr np.CV_32F. Przykładowo, tak stworzymy obraz RGB o rozdzielczości 640x480:

Mat obr(640,480,CV_8UC3);

lub z wykorzystaniem Size:

Mat obr(Size(640,480),CV_8UC3);

Dalej mamy odpowiednik starego CvScalar czyli klasę Scalar. Obiekty tej klasy można zadeklarować tak jak w przykładowym kodzie Scalar_<typ>, lub z użyciem funkcji np.

Scalar scl = Scalar::all(0);

Kolejna funkcja to randu która zapełnia macierz wartościami losowymi z zakresu [0:1]. Ostatnia nowość to funkcja imshow - odpowiednik cvShowImage. I na koniec zauważyć można brak cvRelease :). Wszystkie klasy mają odpowiednio zaimplementowane destruktory, więc program sprząta po sobie sam :)

czwartek, 17 września 2009

OpenCV 2.0

Kilka dni temu pojawiła się nowa wersja biblioteki OpenCV oznaczona numerem 1.2.0 (a nazywana potocznie "dwójką"). Co zatem nowego? Przede wszystkim nowy , obiektowy interfejs C++ zamiast C (który nadal jest wspierany, będzie zachowana wsteczna kompatybilność). Dzięki niemu można praktycznie zapomnieć o alokacji i czyszczeniu pamięci, z czasem zobaczymy jak to wyjdzie "w praniu" ;-) (żegnajcie cvCreateXXX i cvReleaseXXX). Także takie operacje jak dodawanie macierzy będzie uproszczone (mając macierze A,B,C będzie można zrobić prostą operację C = A + B zamiast cvAdd(A,B,C)). Przeciążenia operatorów, zakresy i wszystkie inne dobrodziejstwa C++ dostępne, może twórcy OpenGLa w końcu postąpią podobnie... Od dawien dawna dostępna jest wersja dla Pythona, ale wersja C++ na pewno ucieszy wielu użytkowników, nie ukrywam, że mnie bardzo :).
Kontynuując, mamy kilkanaście nowych algorytmów (m.in. związanych z detekcją), przejście na CMake, dokumentację w LaTeXu, mocniejszą optymalizację (np. użycie SSE3, większą integrację z OpenMP) itd. Więcej w ChangeLogu ;-) Miłego korzystania :)

poniedziałek, 24 sierpnia 2009

Detekcja skóry

Dzisiaj wybiegnę trochę do przodu i pokażę detekcję skóry (będzie w zbliżającej się wersji OpenCV). Jak w wielu przypadkach, detekcję skóry przeprowadza się jedną funkcją. Na początku tworzymy odpowiedni obiekt
CvAdaptiveSkinDetector sd();

Konstruktor może przyjąć dwa parametry, poniżej wartości domyślne
CvAdaptiveSkinDetector sd(1, CvAdaptiveSkinDetector::MORPHING_METHOD_NONE);

Pierwszy z nich mówi o skali obrazu wyjściowego (np. 2 powoduje, że wynik ma dwukrotnie mniejszą szerokość i wysokość od obrazu przetwarzanego). Drugi to technika morfologiczna. Do wyboru mamy
  • MORPHING_METHOD_NONE - brak
  • MORPHING_METHOD_ERODE - erozja
  • MORPHING_METHOD_ERODE_ERODE - podwójna erozja
  • MORPHING_METHOD_ERODE_DILATE - otwarcie

Morfologia może być przydatna, gdyż całkiem dobrze redukuje szum w postaci pojedynczych porozrzucanych pikseli (zakładamy, że takie nie stanowią obszaru skóry). Analizę obrazu przeprowadzamy funkcją process
void process(IplImage *inputBGRImage, IplImage *outputHueMask);

Pierwszy parametr to obraz wejściowy, a drugi wyjściowy. Co uzyskujemy? Jest to obraz na którym wszystkie wartości powyżej zera spełniają kilka zależności i które są prawdopodobnie pikselami skóry. Tego typu detekcja opiera się na analizie kolorów obrazu (często w HSL/HSV wspieranym wstępną analizą w RGB). Powoduje to, że na wynik niekorzystnie wpływa np. niejednolite oświetlenie. Zainteresowanych techniką tego typu algorytmów odsyłam np. tutaj lub do googla, gdzie pod hasłem "skin detection" znajdziemy wiele artykułów na ten temat. Poniżej wynik działania przykładowego programu adaptiveskindetector_sample.cpp
Obraz oryginalny

Obraz wynikowy

Jak widać, działanie nie jest idealne. Przyznam, że ogólnie jestem zawiedziony "skutecznością" metody, ale może jeszcze coś poprawią :) Fajnie byłoby porównać działanie innych algorytmów znalezionych w sieci. Może coś tam skrobnę ale nie ukrywam, że ma to na razie niski priorytet :) Uaktualniłem przykład bloby.cpp na githubie, wybierając kanał "3" można się pobawić w detekcję skóry.

czwartek, 20 sierpnia 2009

Detekcja blobów

Dzisiaj o obszarach spójnych (ang. blob). Detekcja blobów jest istotną częścią wielu algorytmów wizyjnych (zwłaszcza ich początkowej fazy). Czym jest blob? Trudno na to pytanie jednoznacznie odpowiedzieć... Można powiedzieć, że jest to obszar obrazu odróżniający się pewną cechą od otoczenia. W większości przypadków będzie to jego jasność. Generalnie obraz cyfrowy jest zbiorem liczb, więc może to być nawet coś w stylu czasu od ostatniej zmiany wartości piksela. Spójrzmy na przykładowe bloby:

Są to proste przypadki. Pierwszy z lewej ma stały rozkład wartości, przypadek idealny, świetnie odróżnia się od otoczenia :). Dalej jest już gorzej, środkowy przypadek nie ma stałej wartości, jednak maksimum jest w środku, a wartości spadają równomiernie w każdym kierunku wraz z oddalaniem od środka. Ostatni nie ma ani stałej wartości, ani równomiernego jej spadku/wzrostu z oddalaniem od środka. Bloby to właśnie takie "plamy" na obrazie, mają, jak już wspomniałem, dość duże znaczenie (blobami mogą być np. gwiazdy na obrazie nieba, czy samochody na zdjęciu lotniczym). Często mają nieregularny kształt, co utrudnia znajdowanie ich granic.
Do detekcji blobów wydzielono specjalną część z OpenCV o nazwie cvBlobsLib. Właściwie do zapoznania się z jej podstawami wystarczy przeczytać przykładowy kod. Ten wpis wybiega niewiele poza niego :) Na początku poszukujemy blobów przy pomocy obiektu CBlobResult
CBlobResult bloby = CBlobResult(obraz, NULL, 0);

W konstruktorze podajemy kolejno przetwarzany obraz, maskę i wartość tła. Dalej możemy bloby przefiltrować, odrzucając je na podstawie ich cech, np.
bloby.Filter(bloby, B_EXCLUDE, CBlobGetArea(), B_LESS, min_pow);
bloby.Filter(bloby, B_EXCLUDE, CBlobGetArea(), B_GREATER, max_pow);

W powyższym kodzie odrzucamy bloby, których powierzchnia nie mieści się pomiędzy wartościami min_pow i max_pow. Ilość znalezionych blobów łatwo uzyskać
bloby.GetNumBlobs();

Bloba o pobieramy w następujący sposób
CBlob *aktualny_blob = bloby.GetBlob(0);

Z ciekawszych rzeczy, możemy uzyskać "przylądki" bloba :)
aktualny_blob->MinX(); // lewy
aktualny_blob->MinY(); // gorny
aktualny_blob->MaxX(); // prawy
aktualny_blob->MaxY(); // dolny

Możemy także zmienić wszystkie jego piksele na określoną wartość
aktualny_blob->FillBlob(obraz, kolor);

CvBlobLib nie jest obszerna, wszystkie możliwe parametry czy funkcje łatwo znaleźć przeglądając kody źródłowe lub autouzupełnianie kodu w IDE :) Napisałem niewielki program do zabawy parametrami. Powstał też film pokazujący jego działanie, ale po jego obejrzeniu stwierdziłem, że jest nudny (trwa 2 min.) i ostatecznie go nie umieszczę. Poniżej zrzut ekranu
oraz działanie dla obrazka z trzema przykładowymi blobami z początku wpisu

Najlepiej popróbować samemu. Założyłem repozytorium, gdzie będę wrzucał gotowe do użycia kody. Plik z tego wpisu nazywa się bloby.cpp. Miłej zabawy :)

piątek, 24 lipca 2009

Kontury


Dzisiaj trochę o konturach. Są one reprezentowane w OpenCV przez strukturę CvSeq (dokładnie taką samą, jak ta, która pojawiła się przy detekcji twarzy). Dzięki konturom możemy w prosty sposób klasyfikować zawartość obrazu. Klasyfikować możemy proste kształty, np. figury geometryczne. Rozpoznawanie twarzy itp. nie wchodzą w grę, to inna liga :) Niemniej proces ten jest przydatny w rozwiązywaniu wielu problemów. Żeby dalej nie zanudzać, poniżej kod prostej funkcji wyszukującej konturu:
CvSeq * znajdz_kontury(char* sciezka)
{
// czytamy obraz, jezeli nie istnieje, to zwracamy NULL
IplImage * obraz = cvLoadImage(sciezka, CV_LOAD_IMAGE_GRAYSCALE);
if (obraz == NULL)
return NULL;

// nasz kontur
CvSeq * kontur;
// pamiec na obliczenia
CvMemStorage * mem = cvCreateMemStorage(0);
// operacja progrowania
cvThreshold(obraz, obraz, 100, 255, CV_THRESH_BINARY_INV);
// szukanie konturow
cvFindContours(obraz, mem, &kontur);
// aproksymacja konturow
kontur = cvApproxPoly(kontur, sizeof (CvContour), mem, CV_POLY_APPROX_DP, cvContourPerimeter(kontur) * 0.035);
// sprzatanie
cvReleaseImage(&obraz);
// powinnio sie jeszcze zwolnic memstora ale na potrzeby przykladu tego nie robimy
// (poniewaz zwracamy wskaznik na sekwencje do dalszego uzycia)

return kontur;
}

Jak już wspomniałem, kontur jest strukturą CvSeq. CvMemStorage ma podobne znaczenie jak w notce o detekcji twarzy. Dalej mamy operację progowania.
PROGOWANIE
double cvThreshold(
const CvArr* src, 
CvArr* dst, 
double threshold, 
double max_value, 
int threshold_type)

Jako src i dst można bez problemu podać ten sam obraz. Trzeci parametr mówi o wartości naszego progu. Znaczenie pozostałych dwóch bardzo ładnie (z odpowiednimi przebiegami) wytłumaczono w wiki OpenCV. Do metody możemy dołączyć opcjonalnie zastosowanie algorytmu Otsu, w postaci CV_THRESH_XXX | CV_THRESH_OTSU. W naszym prostym przypadku nic on nie poprawi, ale może być pomocny w złożonych obrazach. Należy pamiętać, że wskazane są obrazy w odcieniach szarości (dla metody Otsu jest to warunek konieczny). Oprócz tej funkcji dostępna jest także inna, cvAdaptiveThreshold:
void cvAdaptiveThreshold(
const CvArr* src, 
CvArr* dst, 
double max_value, 
int adaptive_method=CV_ADAPTIVE_THRESH_MEAN_C, 
int threshold_type=CV_THRESH_BINARY, 
int block_size=3, 
double param1=5)

Pierwsze trzy parametry są identyczne jak w zwykłym cvThreshold. Po znaczenie pozostałych odsyłam ponownie do wiki OpenCV. Ten typ progowania zamiast używać stałego progu dla całego obrazu, oblicza go dynamicznie biorąc pod uwagę otoczenie analizowanego piksela. Dzięki temu wyniki mogą znacząco się poprawić, ale koszt obliczeniowy jest wyższy. Do zapoznania się z zaletami i wadami polecam tę stronę. Oprócz wymienionych metod można zastosować inne, np. detekcję krawędzi cvCanny (jednak nie jest to już progowanie!). Generalnie chcemy mieć w dalszym przetwarzaniu obraz binarny. Jeżeli podamy inny (np. pełny zakres odcieni szarości) to i tak będzie on traktowany jako obraz dwu wartościowy, ale z niekorzystnym progiem 0.
Szukanie konturów odbywa się poprzez funkcję cvFindContours:
int cvFindContours(CvArr* image, 
CvMemStorage* storage, 
CvSeq** first_contour, 
int header_size=sizeof(CvContour), 
int mode=CV_RETR_LIST, 
int method=CV_CHAIN_APPROX_SIMPLE, 
CvPoint offset=cvPoint(0, 0))

Kolejno podajemy obowiązkowo obraz na którym szukamy konturów i referencję do wskaźnika na pierwszy kontur. Na razie tyle wystarczy. Kolejną ważną operacją jest aproksymacja konturu. Uzyskujemy ją za pomocą funkcji cvApproxPoly:
CvSeq* cvApproxPoly(
const void* src_seq, 
int header_size, 
CvMemStorage* storage, 
int method, 
double parameter, 
int parameter2=0)

Co do parametrów to zasadniczo najważniejsze jest to, że jako pierwszy podajemy nasz kontur, kolejne 3 tak jak w przykładowym kodzie, możliwości manewru właściwie brak :) Dociekliwym polecam OpenCV wiki. Warty omówienia jest jeszcze parameter. Jako wartość przekazuję tutaj wynik makra cvContourPerimeter pomnożony przez stałą. cvContourPerimeter zwrwaca długość całego konturu. To tyle z podstaw szukania konturów. Przejdźmy do kolejnego przykładu:
void wyswietl_kontury(char * sciezka)
{
// czytamy obraz, jezeli nie istnieje, to zwracamy NULL
IplImage * obraz = cvLoadImage(sciezka, CV_LOAD_IMAGE_COLOR);
if (obraz == NULL)
return;

// przystowowywanie obrazu
IplImage * do_analizy = cvCreateImage(cvSize(obraz->width, obraz->height), 8, 1);
cvCvtColor(obraz, do_analizy, CV_BGR2GRAY);

// nasz kontur
CvSeq * kontur;
// pamiec na obliczenia
CvMemStorage * mem = cvCreateMemStorage(0);
// operacja progrowania
cvThreshold(do_analizy, do_analizy, 100, 255, CV_THRESH_BINARY_INV);
// szukanie konturow
cvFindContours(do_analizy, mem, &kontur, sizeof (CvContour), CV_RETR_TREE);

for (; kontur != NULL; kontur = kontur->h_next)
{
// aproksymacja konturu
CvSeq* temp_kontur = cvApproxPoly(kontur, sizeof (CvContour), mem, CV_POLY_APPROX_DP, cvContourPerimeter(kontur) * 0.035);
// zaznaczanie konturow na obrazie
cvDrawContours(obraz, temp_kontur, cvScalar(255.0, 0.0, 0.0, 0.0), cvScalar(0.0, 255.0, 0.0, 0.0), 100, 2, CV_AA, cvPoint(0, 0));
}
cvNamedWindow("kontury", CV_WINDOW_AUTOSIZE);
cvShowImage("kontury", obraz);

while (1)
{
int l = cvWaitKey(100);
if (l == 'k')
break;

}
}

Jest on podobny do poprzedniej funkcji. Jedna ze zmian to inny przykład wywołania cvFindContours: CV_RETR_TREE (drzewo) zamiast domyślnego CV_RETR_LIST (listy). Zanim przejdę do dalszego omawiania spójrzmy na obraz, który będziemy dalej analizować

Jak można zauważyć, są na nim dwa rodzaje figur, nazwijmy je głównymi (czarne na białym tle) i wewnętrznymi (tutaj: białe na czarnym tle). Teraz spójrzmy na poniższy schemat

Dzięki opcji drzewa możemy zobaczyć które dokładnie kontury zawierają się wewnątrz innych. Lista pozwala nam za to na łatwiejsze iterowanie po wszystkich konturach, nie zważając na ich wzajemne relacje. h_next to wskaźnik na następny kontur w tej samej płaszczyźnie, a v_next na kontur znajdujący się wewnątrz aktualnie wskazywanego konturu. Stąd taka a nie inna konstrukcja pętli
for (; kontur != NULL; kontur = kontur->h_next)
{
... inny kod...
}

Dzięki niej i wybraniu CV_RETR_TREE przechodzimy tylko po konturach "głównych". Nową funkcją jest cvDrawContours:
void cvDrawContours(
CvArr *img, 
CvSeq* contour, 
CvScalar external_color, 
CvScalar hole_color, 
int max_level, 
int thickness=1, 
int line_type=8)

pierwsze dwa parametry to obraz na którym rysujemy i kontur który zaznaczamy. Dalej mamy dwa kolory, odpowiednio dla aktualnego konturu i dla konturów wewnątrz niego. max_level to poziom do którego mamy zaznaczać wewnętrzne kontury. 0 oznacza brak zaznaczania, 1 zaznaczanie wewnętrznych konturów, 2 zaznaczanie kolejnych poziomów zagłębienia (wewnętrzne dla wewnętrznych) itd. Kolejne parametry są podobne do tych z cvLine (patrz wpis z lutego). W przykładzie wyswietl_kontury(char * sciezka) użyłem poziomu 100 i uzyskano następujący obraz

ustalenie poziomu na 0 spowoduje, że wynik będzie następujący:

DOPASOWYWANIE DO WZORCA
Po tym wszystkim czas na najważniejsze czyli dopasowywanie konturów do wzorców. Proces ten polega na określeniu podobieństwa dwóch konturów i zdecydowaniu który z wzorcowych konturów jest najbardziej "podobny" do analizowanego konturu. Na początek warto dodać, dlaczego aproksymowaliśmy kontury. Otóż, kontury mają różną wielkość i kształty, ponadto są to wartości dyskretne. Spójrzmy na przykład zaznaczenia konturów bez aproksymacji

dla porównania jeszcze raz wersja z aproksymacją

Jak można zauważyć, kontury bez aproksymacji mają na krawędziach "schody". Algorytmy badające podobieństwo opierają się m.in. na długościach krawędzi i kątami między nimi. Poszarpanie krawędzi może wpływać niekorzystnie na wyniki. Warto jeszcze dodać, że należy starannie dobrać parametr aproksymacji. Za mały powoduje wspomniane przed chwilą problemy, z kolei za duży zbytnie uproszczenie konturów

Wracając do tematu dopasowywania :) Za nasze wzorce ustalimy sobie kwadrat

i gwiazdę pięcioramienną

poniżej kod klasyfikujący figury
void okresl_kontury1(char * sciezka, CvSeq * kontur1, CvSeq * kontur2, CvScalar kolor1 = cvScalar(0.0, 255.0, 255.0, 0.0), CvScalar kolor2 = cvScalar(255.0, 0.0, 255.0, 0.0))
{
// czytamy obraz, jezeli nie istnieje, to zwracamy NULL
IplImage * obraz = cvLoadImage(sciezka, CV_LOAD_IMAGE_COLOR);
if (obraz == NULL)
return;

// przystowowywanie obrazu
IplImage * do_analizy = cvCreateImage(cvSize(obraz->width, obraz->height), 8, 1);
cvCvtColor(obraz, do_analizy, CV_BGR2GRAY);

// nasz kontur
CvSeq * kontur;
// pamiec na obliczenia
CvMemStorage * mem = cvCreateMemStorage(0);
// operacja progrowania
cvThreshold(do_analizy, do_analizy, 100, 255, CV_THRESH_BINARY_INV);
// szukanie konturow
cvFindContours(do_analizy, mem, &kontur);

for (; kontur != NULL; kontur = kontur->h_next)
{
CvSeq* temp_kontur = cvApproxPoly(kontur, sizeof (CvContour), mem, CV_POLY_APPROX_DP, cvContourPerimeter(kontur) * 0.035);

// badanie podobieństwa konturow
double match1 = cvMatchShapes(temp_kontur, kontur1, CV_CONTOURS_MATCH_I1);
double match2 = cvMatchShapes(temp_kontur, kontur2, CV_CONTOURS_MATCH_I1);
// im mniejsza wartosc, tym bardziej kontury sa do siebie podobne
if (match1 < match2)
cvDrawContours(obraz, temp_kontur, kolor1, kolor1, 0, 2, CV_AA);
else
cvDrawContours(obraz, temp_kontur, kolor2, kolor2, 0, 2, CV_AA);
}

cvNamedWindow("kontury", CV_WINDOW_AUTOSIZE);
cvShowImage("kontury", obraz);

while (1)
{
int l = cvWaitKey(100);
if (l == 'k')
break;

}
cvDestroyWindow("kontury");
cvReleaseImage(&do_analizy);
cvReleaseImage(&obraz);
cvReleaseMemStorage(&mem);
}
w tym przykładzie mamy jedną nową funkcję - cvMatchShapes
double cvMatchShapes(
const void* object1, 
const void* object2, 
int method, 
double parameter=0)
jako object1 i objet2 przekazujemy porównywane kontury. Jako method możemy podać CV_CONTOURS_MATCH_IX gdzie X to 1,2 lub 3. Dla nas najważniejsze jest, że wyniku działania funkcji, niezależnie od metody, dostajemy liczbę zmiennoprzecinkową. Im jest ona mniejsza tym kontury są bardziej do siebie podobne. Spójrzmy na wynik wywołania funkcji dla naszych przykładowych wzorców Kolorem żółtym zaznaczono bardziej podobne do gwiazdy, różowym do kwadratu. Jak widać, jest prawie idealnie :) prostokąt w lewym górnym rogu przydzielilibyśmy raczej do kwadratu, ale komputer zadecydował inaczej. No nic, próbujemy dalej :)
void okresl_kontury2(char * sciezka, CvSeq * kontur1, CvSeq * kontur2, CvScalar kolor1 = cvScalar(0.0, 255.0, 255.0, 0.0), CvScalar kolor2 = cvScalar(255.0, 0.0, 255.0, 0.0))
{
// czytamy obraz, jezeli nie istnieje, to zwracamy NULL
IplImage * obraz = cvLoadImage(sciezka, CV_LOAD_IMAGE_COLOR);
if (obraz == NULL)
return;

// przystowowywanie obrazu
IplImage * do_analizy = cvCreateImage(cvSize(obraz->width, obraz->height), 8, 1);
cvCvtColor(obraz, do_analizy, CV_BGR2GRAY);

// nasz kontur
CvSeq * kontur;
// pamiec na obliczenia
CvMemStorage * mem = cvCreateMemStorage(0);
// operacja progrowania
cvThreshold(do_analizy, do_analizy, 100, 255, CV_THRESH_BINARY_INV);
// szukanie konturow
cvFindContours(do_analizy, mem, &kontur);

// tworzymy histogramy
int rozmiary[2] = {2,2};
CvHistogram * hist_analiz = cvCreateHist(2, rozmiary, CV_HIST_ARRAY, NULL);
CvHistogram * hist_kontur1 = cvCreateHist(2, rozmiary, CV_HIST_ARRAY, NULL);
CvHistogram * hist_kontur2 = cvCreateHist(2, rozmiary, CV_HIST_ARRAY, NULL);
// obliczamy histogramy geometryczne
cvCalcPGH(kontur1, hist_kontur1);
cvCalcPGH(kontur2, hist_kontur2);



for (; kontur != NULL; kontur = kontur->h_next)
{
CvSeq* temp_kontur = cvApproxPoly(kontur, sizeof (CvContour), mem, CV_POLY_APPROX_DP, cvContourPerimeter(kontur) * war);
cvCalcPGH(temp_kontur, hist_analiz);

cvCompareHist(hist_analiz,hist_kontur1,CV_COMP_CORREL);
// badanie podobieństwa konturow
double match1 = cvCompareHist(hist_analiz,hist_kontur1,CV_COMP_CORREL);
double match2 = cvCompareHist(hist_analiz,hist_kontur2,CV_COMP_CORREL);
// badanie korelacji, jezeli histogramy sa podobne to korelacja jest wieksza (1,0 dla identycznych i -1,0 calkowicie roznych)
if (match1 > match2)
cvDrawContours(obraz, temp_kontur, kolor1, kolor1, 0, 2, CV_AA);
else
cvDrawContours(obraz, temp_kontur, kolor2, kolor2, 0, 2, CV_AA);
}

cvNamedWindow("kontury", CV_WINDOW_AUTOSIZE);
cvShowImage("kontury", obraz);

while (1)
{
int l = cvWaitKey(100);
if (l == 'k')
break;

}
cvDestroyWindow("kontury");
cvReleaseImage(&do_analizy);
cvReleaseImage(&obraz);
cvReleaseMemStorage(&mem);
}
Z rzeczy nowych: na początek tworzymy histogramy (patrz: poprzedni wpis). Rozmiary {2,2} nie są obowiązkowe, pierwszy rozmiar nie musi być równy drugiemu. Najważniejsze, żeby histogram był dwuwymiarowy. Proponuję pozmieniać wartości i sprawdzić jak wpływa to na wyniki (ale nie na przykładzie z tego wpisu, bo jest to bardzo prosty przypadek i nic się nie zmieni), oczywiście nie licząc przypadku gdzie podamy jeden z rozmiarów jako 1, gdyż nie ma on sensu. Kontynuując, kolejną nowością jest cvCalcPGH
void cvCalcPGH(
const CvSeq* contour, 
CvHistogram* hist)
funkcja ta odpowiednio zapełnia histogram danymi. Możemy później porównać te histogramy funkcją cvCompareHist
double cvCompareHist(
const CvHistogram* hist1, 
const CvHistogram* hist2, 
int method)
odnośnie możliwych metod odsyłam do OpenCV wiki. Ja wybrałem tutaj korelację. Ważne jest to, że nie zawsze wynik interpretowany jest tak samo. Np. dla chi-kwadrat kontury identyczne są dla wartości 0.0 a najbardziej odbiegają od siebie przy 2.0. Czas na wynik Jak widać, wyniki można uznać za bardziej satysfakcjonujące :) Nie znaczy to, że poprzednia metoda jest gorsza. Należy popróbować dla danego zestawu danych i wybrać bardziej optymalną (odwieczny problem dokładność/szybkość). To by było na tyle z konturów :) Dla bardziej dociekliwych, chcących się dowiedzieć "jak to działa", polecam zainteresować się takimi rzeczami jak kody łańcuchowe (kody Freemana), momenty geometryczne, momenty Hu, a w przypadku drugiej metody pairwise geometric histograms (stąd skrót PGH).

sobota, 11 lipca 2009

Histogram

Histogram jest dość istotny w analizie obrazu. Pozwala nam on określić pewne cechy obrazu i np. ustalić wartości progowania. Histogram w OpenCV wiąże się ze strukturą CvHistogram. Poniżej przykładowa funkcja tworząca histogram na podstawie obrazu (w odcieniach szarości):
CvHistogram* rysuj_histogram1(IplImage * obraz)
{
// tworzenie histogramu z obrazka
int rozm = 256;
// pierwszy parametr mowi o liczbie wymiarow histogramu
// drugi to wskaznik na liczbe przedzialow w poszczegolnych wymiarach
// my chcemy tylko jeden histogram o liczbie przedzialow 256
// trzeci parmater to rodzaj histogramu
CvHistogram * hist = cvCreateHist(1, &rozm, CV_HIST_ARRAY);

for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
int wartosc = (int) cvGet2D(obraz, y, x).val[0];
// uzyskiwanie wskaznika na wartosc z danego przedzialu
// dzieki niemu mozemy ta wartosc modyfikowac
(*cvGetHistValue_1D(hist, wartosc))++;
}
wyswietl_histogram(hist);
}
Funkcja tworzącą histogram posiada jeszcze dwa opcjonalne parametry float** ranges = NULL oraz int uniform = 1. Uniform ustawione na 1 mówi o tym, że jako ranges podajemy tablicę par liczb ograniczających kolejne przedziały, a uniform == 0, że jako ranges podamy wartości graniczne pomiędzy kolejnymi przedziałami. W pierwszym przypadku podajemy N par, a w drugim N+1 wartości, gdzie N to liczba przedziałów. Parametry te możemy początkowo pominąć, a później ustawić je za pomocą funkcji
void cvSetHistBinRanges(CvHistogram* histogram,float** ranges, int uniform = 1);
Drugą nową funkcją w przykładzie jest
float* cvGetHistValue_1D(CvHistogram * h, int ind);
która zwraca nam wskaźnik do wybranego przedziału. Dzięki temu możemy zarówno odczytać jak i nadpisać wartość z nim związaną. Funkcja ta ma jeszcze kilka odmian. Z końcówką 2D oraz 3D pozawala odczytać z dwóch i trzech wymiarów (wtedy podajmy odpowiednio int ind1, int ind2 oraz int ind1, int ind2, int ind3 zamiast pojedynczego indeksu), a z końcówką nD z większej liczby wymiarów (zamiast pojedynczego indeksu przekazujemy tablicę indeksów int* ind). Podobną funkcją jest cvQueryHistValue_XD, gdzie w miejsce X wstawiamy 1,2,3 lub n. Parametry są identyczne jak w cvGetHistValue_XD, ale 'Query' zwraca wartość typu double i służy wyłącznie do odczytu. Obliczanie histogramu można sobie uprościć ;-) Przykład drugi
CvHistogram * rysuj_histogram2(IplImage * obraz)
{
int rozm = 256;
CvHistogram * hist = cvCreateHist(1, &rozm, CV_HIST_ARRAY);
// tutaj latwiejsza metoda ;-)
// obraz musi posiadac jeden kanal
cvCalcHist(&obraz, hist);
wyswietl_histogram(hist);
}
Dla formalności, poniżej kod wyświetlający histogramy
void wyswietl_histogram(CvHistogram * hist)
{
int wysokosc = 200;
IplImage * wynik = cvCreateImage(cvSize(255, wysokosc), 8, 1);

// pobieranie wartosci maksymalnej i minimalnej
float min, max = 0;
cvGetMinMaxHistValue(hist, &min, &max);
double skala = (double) wysokosc / max;
int i = 0;
for (i = 0; i < 255; i++)
{
// pobieranie ilosci w danym przedziale, tym razem tylko do odczytu
double wartosc = cvQueryHistValue_1D(hist, i);
cvLine(wynik, cvPoint(i, wysokosc - 1), cvPoint(i, wysokosc - (int) (wartosc * skala) + 1), cvScalarAll(150.0));
}

cvNamedWindow("histogram", CV_WINDOW_AUTOSIZE);

cvShowImage("histogram", wynik);

while (true)
{
if (cvWaitKey(250) == 'k')
break;
}

cvReleaseImage(&wynik);
cvReleaseHist(&hist);
}
Działanie funkcji wyswietl_histogram pokazują przykłady (ponownie wracamy do Parku Vigelanda): Zdjęcie Histogram Zostawiając na boku strukturę CvHistogram (w kolejnych wpisach do niej wrócę ;-) warto przedstawić jedną z ważnych operacji jaką jest wyrównanie histogramu. Pozawala ono poprawić zdjęcia, które mają bardzo zły kontrast, przez co nasz algorytm może niepoprawnie interpretować zawartość obrazu. Rzeźba z poprzedniego zdjęcia jest właśnie przykładem zdjęcia z nierównym histogramem. Na zdjęciu tym np. nie można rozróżnić chmur (chyba, że ktoś posiada naprawdę wprawne oko :) ). Histogram wyrówujemy poleceniem
cvEqualizeHist(const CvArr* src,CvArr* dst);
Jako źródło i wynik możemy podać tą samą strukturę. Na koniec zdjęcie i histogram po wyrównaniu: Zdjęcie Histogram