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

poniedziałek, 6 lipca 2009

Trackbar

Dzisiaj kolejny post związany z HighGUI. Trackbar jest to rodzaj suwaka zmieniający wartości danej liczby całkowitej od 0 do określonego przez nas maksimum z krokiem 1. Ostatnio często z tego korzystam, bo jest to rozwiązanie naprawdę wygodne :)

Za utworzenie trackbara odpowiada funkcja
int cvCreateTrackbar(
const char* trackbar_name, // nazwa wyswietlona przy suwaku
const char* window_name, // okno na ktorym chcemy utworzyc suwak
int* value, // wskaznik na zwiazana z suwakiem wartosc
int count, // maksymalna wartosc (minimalna to zero)
CvTrackbarCallback on_change // funkcja wyzwalacza, analogiczna jak dla myszki, moze byc NULL
);

Jeszcze dwie uwagi: funkcja cvCreateTrackbar musi być wywołana PO utworzeniu okna funkcją cvNamedWindow (w przeciwnym wypadku suwaka nie będzie). Nagłówek funkcji wyzwalacza to
void foo(int);

Okno z suwakiem wygląda tak:

piątek, 3 lipca 2009

Learning OpenCV. Computer Vision with the OpenCV Library

Witam po dłuższej nieobecności spowodowanej sesją, nawałem pracy itp., itd. Dzisiaj minirecenzja książki "Learning OpenCV". Autorami książki są Gary Bradski oraz Adrian Kaehler. Pierwszy z nich jest bardzo mocno związany z OpenCV i uczestniczy w jej powstawaniu od samego początku.

Ksiązka ta prowadzi wprowadza nas krok po kroku w świat wizji komputerowej i OpenCV. Na początku poznajemy bibliotekę, sposób jej instalacji. Dalej kolejno mamy:
  • podstawy OpenCV - zapoznajemy się z podstawowymi strukturami,
  • HighGUI - zapis/odczyt obrazu, wyświetlanie okien,
  • przetważanie obrazów - filtry, morfologia obrazu, sploty, detektory, DFT itp.,
  • histogramy - budowanie, analiza, porównywanie,
  • kontury,
  • segmentacja,
  • ruch i jego śledzenie,
  • kalibracja kamery,
  • wizja 3D,
  • uczenie maszynowe - opis biblioteki Machine Learning, która jest obecnie częścią OpenCV
Książka opisuje wszystko przejrzyście i przystępnie. Zawiera sporo przykładów użycia. Najważniejsze funkcje opisane są osobno, a każdy parametr wytłumaczony. Pozwala to szybko zrozumieć jak dany parametr wpływa na wynik. Książka bogata jest w schematy i obrazy wynikowe.

Na początku książkę czyta się lekko i szybko, później trzeba się już niestety bardziej skupić, ponieważ poziom skomplikowania wzrasta (dla mnie było to mniej więcej od rozdziału o śledzeniu ruchu...). Mimo wszystko trzeba przyznać, że autorom udaje się podtrzymać poziom i od książki oderwać się nie mogłem ;-) Przekrój materiału jest odpowiedni dla początkującego, właściwie można ją czytać nie mając wcześniej styczności z wizją komputerową, gdyż każdy proces jest tłumaczony, także jego możliwe zastosowanie. Zaraz po tym autorzy przedstawiają nam jak taki efekt uzyskać w OpenCV, mamy więc dwie pieczenie na jednym ogniu.

Jako wadę można uznać ponownie... zakres materiału. Trzeba przyznać, że książka skupia się na podstawach. Bardzo wiele rzeczy, już bardziej zaawansowanych, nie jest nawet wspomniana pomimo tego, że w OpenCV są zaimplementowane. Oczywiście nie można mieć do autorów o to pretensji. Książka jest swoistym wstępem i tylko w takich kategoriach należy ją brać pod uwagę, chociaż niedosyt zostaje. Jest to jedyna pozycja na rynku o OpenCV, na kolejne się nie zanosi. Poza tym, nowa wersja biblioteki przechodzi w obiektowość (z C w C++). Algorytmy są przystosowywane. Wsteczna kompatybilność ma być zachowana, ale w niedługim czasie książkę będzie można uznać za lekko nieaktualną.

Podsumowując polecam ją jak najbardziej osobom, które chcą poznać podstawy OpenCV wraz z podstawami wizji komputerowej :)

Pozdrwaiam,
Ratix :)

P.S. Mam zamiar jeszcze napisać tego typu minirecenzje innych książek które wpadną mi w ręce. Ponadto, jak już kiedyś wspomniałem, staram się tworzyć bardziej "praktyczne" programy, idzie niestety trochę opornie...

poniedziałek, 8 czerwca 2009

Inpainting


Czasami, z różnych powodów, na obrazie pojawiają się wady, które możemy łatwo dostrzec. Wady takie, mogą przyczynić się do pogorszenia wyników (np. wykrycie nadmiarowych krawędzi). W OpenCV możemy takie wady usunąć (musimy jednak je określić...). Za wszystko odpowiedzialna jest funkcja cvInpaint.
void cvInpaint( const CvArr* src, const CvArr* inpaint_mask, CvArr* dst, double inpaintRange, int flags );

Obrazy wejsciowy i źródłowy muszą być tego samego typu i rozmiaru. Maska musi posiadać ten sam rozmiar co obraz, ponadto musi posiadać 8-bitową głębię i 1 kanał. inpaintRange określa w jakim promień obszaru przetwarzania dla danego piksela. Za duży może powodować uwzględnianie pikseli dosyć odległych i dla małych wad powodować rozmywanie obrazu. Flaga oznacza wybór metody. Są dwie do wyboru: CV_INPAINT_NS oraz CV_INPAINT_TELEA.

Poniższe obrazy przedstawiają obraz oryginalny, a dalej obrazy z wadą oraz obrazy o po naprawianiu i różnica pomiędzy obrazem oryginalnym i naprawionym. Jak będzie można zauważyć, dla małej wady wynik jest zadowalający. Dla większych, już nie jest tak dobrze. Niemniej, może być to pomocne, gdy chcemy usunać wadę wpływającą niekorzystnie na wynik programu.

Obraz oryginalny

Obraz z cienką krzywą
Obraz oryginalny

Po naprawie

Różnica


Obraz z grubą krzywą
Obraz oryginalny

Po naprawie

Różnica


Obraz z półprzezroczystą plamą
Obraz oryginalny

Po naprawie

Różnica


Przykład wykorzystania. Jest to program z poprzedniego wpisu (o myszce), tym razem rysujący większe punkty (okresla to zmienna promien). Po naciśnięciu klawisza "i", w miejscach zaznaczonych myszką, wykonany zostanie inpaintig. Z linii poleceń należy przekazać ścieżkę do pliku z obrazem.
bool wcisniety = false;
CvScalar kolor;
char* nazwa = "Rysowanie";
IplImage* maska; // maska okreslajaca piksele do zmiany
double promien = 3.0; // promien rysowanych kolek

void obsluga_myszki(int e, int x, int y, int fl, void* par)
{
IplImage * obraz = (IplImage*) par;

// zmiany zapisujemy na obrazie i w masce
switch (e)
{
case CV_EVENT_LBUTTONDOWN:
cvCircle(obraz, cvPoint(x, y), promien, kolor, -1);
cvCircle(maska, cvPoint(x, y), promien, kolor, -1);
wcisniety = true;
break;
case CV_EVENT_LBUTTONUP:
wcisniety = false;
break;
case CV_EVENT_MOUSEMOVE:
if (wcisniety)
{
cvCircle(obraz, cvPoint(x, y), promien, kolor, -1);
cvCircle(maska, cvPoint(x, y), promien, kolor, -1);
}
break;
}
}

void inpaint(IplImage * obraz)
{
IplImage* naprawiony = cvCreateImage(cvGetSize(obraz), obraz->depth, obraz->nChannels);

// funkcja naprawiajaca obraz
cvInpaint(obraz, maska, naprawiony, 3, CV_INPAINT_TELEA);

cvReleaseImage(&obraz);
obraz = cvCloneImage(naprawiony);
cvReleaseImage(&naprawiony);
}

void program_gl(char * sciezka)
{
cvNamedWindow(nazwa, CV_WINDOW_AUTOSIZE);
IplImage* wys = cvLoadImage(sciezka, CV_LOAD_IMAGE_ANYCOLOR);
// maska zawsze musi byc 8-bitowa i 1-kanalowa
maska = cvCreateImage(cvGetSize(wys), 8, 1);
cvZero(maska);
kolor = cvScalarAll(1.0);
cvSetMouseCallback(nazwa, obsluga_myszki, wys);

while (true)
{
cvShowImage(nazwa, wys);
int key = cvWaitKey(10);
if (key == 'k')
break;
if (key == 'i')
{
inpaint(wys);
cvZero(maska);
}
}

cvReleaseImage(&wys);
cvReleaseImage(&maska);
cvDestroyWindow(nazwa);

}

/*
* Inpainting
* autor: ratixu.blogspot.com
*/
int main(int argc, char** argv)
{
program_gl(argv[1]);
return (EXIT_SUCCESS);
}

wtorek, 2 czerwca 2009

Mysz w OpenCV


Kolejny post niezwiązany stricte z wizją komputerową, ale opisujący przydatną w pracy nad programem/algorytmem funkcjonalność. W OpenCV brakuje zaawansowanych elementów GUI, biblioteka ma inny cel. Jednak interakcja pozwala na znaczne przyspieszenie testowania programu. OpenCV udostępnia kilka podstawowych operacji w części zwanej HighGUI (związany z nią jest plik highgui.h). Jedną z nich jest tworzenie okien. Dzisiaj opiszę inną: obsługę myszy.

Obsługa myszki rozwiązana jest czymś na wzór "callbacku". Aby jej użyć musimy napisać funkcję o określonym nagłówku (ważna jest tylko kolejność i typ parametrów):
void obsluga_myszki(int zdarzenie, int x, int y, int flagi, void* parametry);

Proste jej użycie pokzauje poniższy przykład:
// czy przycisk jest wcisniety
bool wcisniety = false;
// kolor ktorym rysujemy
CvScalar kolor;
// nazwa okna
char* nazwa = "Rysowanie";

void obsluga_myszki(int e, int x, int y, int fl, void* par)
{
// rzutujemy parametr na wskaznik na obraz
IplImage * obraz = (IplImage*) par;

switch (e)
{
case CV_EVENT_LBUTTONDOWN:
cvSet2D(obraz, y, x, kolor);
wcisniety = true;
break;
case CV_EVENT_LBUTTONUP:
wcisniety = false;
break;
case CV_EVENT_MOUSEMOVE:
if (wcisniety)
{
cvSet2D(obraz, y, x, kolor);
}
break;
}
}

void program_gl()
{

// tworzymy okno
cvNamedWindow(nazwa, CV_WINDOW_AUTOSIZE);

// wyswietlany obraz
IplImage* wys = cvCreateImage(cvSize(500, 500), 8, 3);
cvZero(wys);

// ustawiamy funkcje obslugi myszy
// kolejno podajemy nazwe okna, nazwe funkcji obslugi
// nasze dane (jako wskaznik), ktore otrzymamy w funkcji
// obslugi w parametrze void* par
cvSetMouseCallback(nazwa,obsluga_myszki,wys);

while(true)
{

cvShowImage(nazwa,wys);

if(cvWaitKey(10) == 'k')
break;
}

cvReleaseImage(&wys);
cvDestroyWindow(nazwa);

}

/*
* Rysowanie
* autor: ratixu.blogspot.com
*/
int main(int argc, char** argv)
{
kolor = cvScalarAll(255.0);
program_gl();
return (EXIT_SUCCESS);
}

Po uruchomieniu możemy rysować białym kolorem na czarnym tle.

Oczywiście zastosowań jest tyle, ile wyobraźnia podpowie :)

czwartek, 28 maja 2009

XML i YAML w OpenCV

Czasu ostatnio niewiele (zaliczenia i te sprawy), a na blogu zastój. W OpenCV pracuję nad czymś "grubszym" dlatego od czasu do czasu zarzucę jakąś krótszą notką, żeby sprawić wrażenie, że blog jeszcze żyje :P

Dzisiaj będzie o XML/YAML. Najczęściej wykorzystujemy tego typu pliki do przechowywania parametrów lub ustawień programu. Zapis do pliku wygląda następująco:

// otwieramy plik do zapisu
CvFileStorage * fs = cvOpenFileStorage("conf.xml",0,CV_STORAGE_WRITE);

// zapisujemy dane
cvWriteInt(fs,"wysokosc", 480);
cvWriteInt(fs,"szerokosc", 640);

// zwalniamy zasoby
cvReleaseFileStorage(&fs);


i w ten sposób otrzymujemy plik conf.xml:



480
640



lub, zmieniając rozszerzenie, plik conf.yml:

%YAML:1.0
wysokosc: 480
szerokosc: 640


Odczyt danych wygląda podobnie do zapisu:
// otwieramy plik do odczytu
CvFileStorage * fs = cvOpenFileStorage("conf.xml", 0, CV_STORAGE_READ);

// odczytujemy dane
int wysokosc = cvReadIntByName(fs, 0, "wysokosc");
int szerokosc = cvReadIntByName(fs, 0, "szerokosc");

// zwalniamy zasoby
cvReleaseFileStorage(&fs);


To właściwie tyle z podstaw :) Oczywiście OpenCV daje więcej możliwości manipulowania danymi. Zamiast zapisywać wartości oddzielnie, możemy to zrobić w ciągu:
cvWriteInt(fs,"fps",30);
// zapisujemy strukture
cvStartWriteStruct(fs,"rozmiar",CV_NODE_SEQ);
cvWriteInt(fs,0, 480);
cvWriteInt(fs,0, 640);
cvEndWriteStruct(fs);


Otrzymamy pliki:


30

480 640


%YAML:1.0
fps: 30
rozmiar:
- 480
- 640


taką strukturę odczytujemy tak:

// otrzymujemy sekwencje - podobnie jak w detekcji twarzy
CvSeq* seq = cvGetFileNodeByName(fsr,0,"rozmiar")->data.seq;

int wysokosc = cvReadInt((CvFileNode*)cvGetSeqElem(seq,0));
int szerokosc = cvReadInt((CvFileNode*)cvGetSeqElem(seq,1));


Do plików XML/YAML możemy zapisywać także inne rzeczy:
CvMat* macierz = cvCreateMat(5,6,CV_32F);
cvZero(macierz);

// zapisujemy komentarz...
cvWriteComment(fs,"Komentarz",0);
// ...napis...
cvWriteString(fs,"napis","zapisany lancuch znakow");
// ...liczbe rzeczywista...
cvWriteReal(fs,"liczba",3.14);
// ...strukture (np. macierz)...
cvWrite(fs,"macierz",macierz);




"zapisany lancuch znakow"
3.1400000000000001

5
6
f
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.

%YAML:1.0
# Komentarz
napis: zapisany lancuch znakow
liczba: 3.1400000000000001
macierz: !!opencv-matrix
rows: 5
cols: 6
dt: f
data: [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0. ]


odczyt:
CvMat* macierz = (CvMat*) cvReadByName(fs,0,"macierz");
double liczba = cvReadRealByName(fs,0,"liczba",0.0);
const char* napis = cvReadStringByName(fs,0,"napis","domyslny");


Do plików tych można zapisywać także skomplikowane struktury drzewiaste. Wspomnę o tym gdy będę opisywał takie struktury :)

wtorek, 26 maja 2009

Linux Mint 7 wydany

Wydano najnowszą wersję Linuksa Mint o numerze 7 i nazwie "Gloria". Więcej informacji na oficjalnym blogu dystrybucji.

niedziela, 10 maja 2009

Detekcja twarzy

Detekcja twarzy jest obecnie powszechnie wykorzystywana (np. w ustawianiu ostrości aparatów cyfrowych). Gdy dowiemy się gdzie na obrazie znajdują się twarze, możemy wykonać wiele ciekawych operacji. Najpierw jednak musimy je znaleźć :) Za proces ten w OpenCV odpowiadna jedna funkcja... Kod:

CvCapture* vid = cvCreateCameraCapture(0);
cvNamedWindow("detekcja twarzy", CV_WINDOW_AUTOSIZE);
cvQueryFrame(vid);
double fps = 25;
int odstep_miedzy_klatkami = 1000 / fps;

// kolory do zaznaczania twarzy
static CvScalar kolory[] = {
{{19.0,69.0,139.0,0.0}},
{{63.0,133.0,205.0,0.0}},
{{96.0,164.0,244.0,0.0}},
};

// deklarujemy pamiec na obliczenia
CvMemStorage * storage = cvCreateMemStorage(0);

// tworzymy klasyfikator
// jako agrument musimy podac siezke do pliku z efektem treningu klasyfikatora
CvHaarClassifierCascade * haar = (CvHaarClassifierCascade*) cvLoad("/usr/local/share/opencv/haarcascades/haarcascade_frontalface_alt.xml");

while (true)
{
IplImage* ramka = cvQueryFrame(vid);

if (ramka == 0)
break;

// czyscimy bufor
cvClearMemStorage(storage);

// skala
double skala = 1.5;

// przygotowujemy obrazy posrednie
IplImage *temp = cvCreateImage(cvSize(ramka->width, ramka->height), 8, 1);
IplImage *temp2 = cvCreateImage(cvSize(cvRound(ramka->width / skala), cvRound(ramka->height / skala)), 8, 1);

// zamieniamy kolory na skale szarosci
cvConvertImage(ramka, temp, CV_BGR2GRAY);

// zmniejszamy czarno-bialy obraz
cvResize(temp, temp2, CV_INTER_LINEAR);

// szukamy twarzy w danej ramce
CvSeq *wynik = cvHaarDetectObjects(temp2, haar, storage, 1.1, 3, CV_HAAR_DO_CANNY_PRUNING, cvSize(30,30));

// iterujemy po wszystkich wynikach
for (int i = 0; i < (wynik ? wynik->total : 0); i++)
{
// pobieramy prostokat z pozycja wskazujaca twarz
CvRect * twarz = (CvRect*) cvGetSeqElem(wynik, i);


// ustalamy dwa punkty po przekatnej prostokata
CvPoint punkt1 = cvPoint(cvRound(twarz->x * skala),cvRound(twarz->y * skala));
CvPoint punkt2 = cvPoint(cvRound((twarz->x + twarz->width)* skala),cvRound((twarz->y + twarz->height) * skala));

// rysujemy prostokat zaznaczajacy twarz na obrazie
cvRectangle(ramka,punkt1,punkt2,kolory[i%3],2);

}


cvShowImage("detekcja twarzy", ramka);

cvReleaseImage(&temp);
cvReleaseImage(&temp2);

int c = cvWaitKey(odstep_miedzy_klatkami);
if (c == 'k')
break;

}

cvReleaseHaarClassifierCascade(&haar);
cvDestroyAllWindows();
cvReleaseCapture(&vid);


Powyższy kod to modyfikacja z jednego z wcześniejszych wipsów. Na uzyskany z kamery obraz dokładane są prostokąty zaznaczające twarze.

Parę słów komentarza do kodu: pierwsza sprawa to plik xml z danymi z treningu. Plik ten zawiera dane do klasyfikatora cech Haara. Same klasyfikatory to dość rozbudowana dziedzina związana ze sztuczną inteligencją. Nas interesuje to, że klasyfikator zbudowany w oparciu o dany plik pozwoli nam wykryć twarze skierowane frontalnie do kamery. Użycie innego pliku pozwoli nam wykryć np. twarze uchwycone z profilu czy ludzką postać. W kolejnych wersjach biblioteki ma pojawić się ich więcej. Można je znaleźć w odpowiednim katalogu OpenCV.

Przygotowanie obrazu przed detekcją. Detekcja twarzy odbywa się w różnej skali, co pozwala znaleźć twarze ludzi zarówno bliżej jak i dalej obiektywu. Sam proces jest dość złożony. Podanie do wykrywania mniejszego obrazu powoduje znaczne przyspieszenie detekcji, przy niewielkim spadku jego skuteczności (jednak z utratą możliwości wykrywania mocno oddalonych twarzy). Podobnie jest z zamianą kolorów na skalę szarości.

Najważniejszym aspektem w detekcji frontalnych twarzy są oczy. Kiedy jedno oko jest przysłonięte, twarz w większości przypadków nie powinna zostać wykryta. Można też pobawić się w drugą stronę i "spreparować" twarz jak na filmiku ;-)



Zachęcam do modyfikacji parametrów. U mnie, niestety, szybkość nie powala, ale nad tym jeszcze popracuję :)

niedziela, 3 maja 2009

Dzień z kulturą japońską w Bytomiu

16.05.2009 odbędzie się w Bytomiu Dzień Kwitnącej Wiśni. Organizatorem imprezy jest Polsko-Japońska Wyższa Szkoła Technik Komputerowych WZI w Bytomiu. Poszczególne atrakcje odbywać się będą w jej budynkach.

W programie przewidziano pokazy i konkursy związane z m.in. japońskimi sztukami walki, konsolami, anime/mangą, karaoke czy japońską kuchnią. Więcej informacji na stronie organizatora. Dla każdego fana kultury dalekiego wschodu wydarzenie obowiązkowe :)

Do zobaczenia w Bytomiu za 13 dni :)

środa, 29 kwietnia 2009

Quo vadis?

Zastanawiając się nad tematyką kolejnego posta doszedłem do wniosku, że nie mam szansy zrobić regularnego kursu, ponieważ za małą mam wiedzę w tej dziedzinie ;-) A co za tym idzie, nie będzie już kolejnych postów pt. "OpenCV cz.X". Nie mam zamiaru jednak zawieszać bloga. Dalej będę opisywać zmagania z biblioteką, ale już w luźniejszej formie i bez konkretnego kierunku.To co sprawi mi problemy lub wyda mi się ciekawe zostanie tutaj opisane. Na głównej stronie biblioteki napisano:
The Open Computer Vision Library has > 500 algorithms, documentation and sample code for real time computer vision.
I na tych algorytmach chciałbym się skupić. Jak wyjdzie w praniu, to się okaże...

Pozdrawiam,
Ratix ;-)

niedziela, 19 kwietnia 2009

OpenCV cz. 4. Splot

Splot (ang. convolution) jest jedną z najczęściej używanych operacji w wizji (widzeniu) komputerowym. Po definicję matematyczną splotu odsyłam do wikipedii. Nie należy się zrażać widząc całki. W przetwarzaniu obrazów cyfrowych mamy doczynienia z funkcjami dyskretnymi, więc całki jako takie nie mają racji bytu (dotyczy to też wielu innych matematycznych narzędzi). Więc czym jest splot? Można to określić jako modyfikację jednej funkcji inną. U nas będzie to modyfikacja obrazu pewną maską. W ten sposób zmodyfikujemy obraz, aby ułatwić sobie dalszą z nim pracę. Jak to działa w praktyce?

Powyżej widzimy maskę i wycinek obrazka. Ciemniejszym kolorem zaznaczono odpowiednio środek maski i aktualnie przetwarzany piksel. "Nakładamy" maskę tak aby zaciemnione pola się nakryły i wykonujemy mnożenie pikseli obrazka z pikselami maski które się nad nimi znajdują:
0.1 * 32 + 0.1 * 12 + 0.1 * 54 + 0.1 * 43 + 0.2 * 32 + 0.1 * 32 + 0.1 * 12 + 0.1 * 73 + 0.1 * 43 = 36.5.
Wynik zaokrąglamy, otrzymujemy 36. Wartość tą wstawiamy w obraz wynikowy na pozycji odpowiadającej aktualnie przetwarzanemu pikselowi. Tak uzyskany obraz wynikowy nazywany jest potocznie splotem.

Poniżej przedstawione zdjęcie (tablica z parku Vigelanda w Oslo):
przetwarzamy, w wersji w odcieniach szarości, wcześniej pokazaną maską
0.1 0.1 0.1
0.1 0.2 0.1
0.1 0.1 0.1

i otrzymujemy:
Jak można zauważyć (lub nie można ;-) ), uzyskany obraz jest wygładzony. Omawiana maska jest tzw. filtrem wygładzającym. Jest to przykładowe wykorzystanie splotu. Samych filtrów wygładzających jest sporo. Często maski prezentuje się w takiej formie:
1 1 1
1 2 1
1 1 1

dodając przed maską współczynnik skali, w tym przypadku wynosi on 1/10. Ok, czas na kod:
// otwieramy obraz na ktorym bedziemy operowac
IplImage* obraz = cvLoadImage("/home/lukasz/blog/vigeland.png", CV_LOAD_IMAGE_GRAYSCALE);

// tworzymy maske
CvMat* maska = cvCreateMat(3,3,CV_32F);

// nasza maska wyglada nastepujaco
// 0.1 0.1 0.1
// 0.1 0.2 0.1
// 0.1 0.1 0.1
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
cvSet2D(maska,j,i,cvScalarAll(0.1));

cvSet2D(maska,1,1,cvScalarAll(0.2));

// splot
// kolejno przekazujemy: obraz zrodlowy, obraz docelowy, maska i opcjonalnie
// srodek maski
// Przekazywanie jako obrazu zrodlowego i docelowego tej samej struktury nie
// zawsze jest wskazane. Wspomne o tym w odpowiednim momencie.
cvFilter2D(obraz,obraz,maska);

cvNamedWindow("splot", CV_WINDOW_AUTOSIZE);
cvShowImage("splot", obraz);
while (true)
{
if (cvWaitKey(100) == 'k') break;
}

// zapisujemy obraz wynikowy
cvSaveImage("/home/lukasz/blog/splot.png",obraz);

cvDestroyAllWindows();
cvReleaseImage(&obraz);
cvReleaseMat(&maska);
Splot służy nie tylko do wygładzania. Jedyne czego nam potrzeba to odpowiednie maski! Zobaczmy filtr Sobela. Maska wygląda nastepująco:
-1 -2 -1
0  0  0
1  2  1
w odpowienie miejsce w kodzie wstawiamy:
cvSet2D(maska,2,0,cvScalarAll(1.0));
cvSet2D(maska,2,1,cvScalarAll(2.0));
cvSet2D(maska,2,2,cvScalarAll(1.0));
cvSet2D(maska,1,1,cvScalarAll(0.0));
cvSet2D(maska,1,1,cvScalarAll(0.0));
cvSet2D(maska,1,1,cvScalarAll(0.0));
cvSet2D(maska,0,0,cvScalarAll(-1.0));
cvSet2D(maska,0,1,cvScalarAll(-2.0));
cvSet2D(maska,0,2,cvScalarAll(-1.0));
i otrzymujemy: Filtr sobela realizuje coś na wzór pierwszej pochodnej. Przedstawiona maska operuje wzdłuż osi pionowej. Tego typu filtry wykorzystuje się np. do wykrywania krawędzi na obrazie. Transponując macierz, otrzymamy filtr dla osi poziomej:
-1  0 -1
-2  0 -2
-1  0 -1
Ten sam efekt można uzyskać też inaczej, zdając się na bilbiotekę:
// otwieramy obraz na ktorym będziemy operowac
IplImage* obraz = cvLoadImage("/home/lukasz/blog/vigeland.png", CV_LOAD_IMAGE_GRAYSCALE);

// tworzymy maske
CvMat* maska = cvCreateMat(3,3,CV_32F);

// stosujemy filtr sobela rzedu 0 dla poziomu i 1 dla pionu
cvSobel(obraz,obraz,0,1);

cvNamedWindow("splot", CV_WINDOW_AUTOSIZE);
cvShowImage("splot", obraz);
while (true)
{
if (cvWaitKey(100) == 'k') break;
}

// zapisujemy obraz wynikowy
cvSaveImage("/home/lukasz/blog/splot.png",obraz);

cvDestroyAllWindows();
cvReleaseImage(&obraz);
Wynikiem jest: Wynik działania powinien być w miarę podobny. Czasami rozbieżności wynikają z faktu, że dla danego filtru istnieje więcej niż jedna maska. Na przyszłość polecam oczywiście metodę cvSobel :) Splot jest bardzo często jedną z początkowych operacji w przetwarzaniu wizyjnym. Zobaczmy laplasjan:
// otwieramy obraz na ktorym bedziemy operowac
IplImage* obraz = cvLoadImage("/home/lukasz/blog/vigeland.png", CV_LOAD_IMAGE_GRAYSCALE);

// tworzymy macierz na wynik
CvMat* lap = cvCreateMat(obraz->height,obraz->width,CV_32F);
// uzycie operatora Laplace'a 
cvLaplace(obraz,lap);

cvNamedWindow("splot", CV_WINDOW_AUTOSIZE);
cvShowImage("splot", lap);
while (true)
{
if (cvWaitKey(100) == 'k') break;
}

// zapisujemy obraz wynikowy
cvSaveImage("/home/lukasz/blog/splot.png",lap);

cvDestroyAllWindows();
cvReleaseMat(&lap);
Chciałem tu zwrócić uwagę na dwie sprawy. Po pierwsze, do wyświetlania wyniku posłużyliśmy się tym razem strukturą CvMat, jak widać nie ma z tym problemu, o czym pisałem już kiedyś (nie jest to oczywiście bezwarunkowa operacja). Druga to właśnie fakt, że do cvLaplace nie mogliśmy podać jako źródła i wyniku tego samego obrazu. cvLaplace wymaga by jako wynik podać strukturę przechowującą 16-bitowe liczby ze znakiem lub 32-bitowe bez znaku. Nasz obraz wczytany z pliku nie spełnia tych wymogów i jego użycie jako źródła i przeznaczenia spowoduje wystąpienie błędu. Po wykonaniu otrzymujemy: Uzyskany wynik przedstawia drugą pochodną cząstkową zsumowaną w pionie i poziomie. Nie może być traktowany jako obraz, gdyż nim nie jest (występują tam np. wartości ujemne). Dla laplasjanu także istnieją maski, np.
1   1   1   1  1
1   1   1   1  1
1   1  -24  1  1
1   1   1   1  1
1   1   1   1  1
Wykonując ją uzyskamy: Podobnie jak w przypadku Sobela, istnieje kilka masek dla laplasjanu. Chciałem zademonstrować sposób działania splotu, bo być może przyjdzie nam korzystać z naszej lub innej z góry zadanej maski. Sam problem wykonania splotu jest bardziej złożony, niż może się wydawać na pierwszy rzut oka (co zrobić np. z pikselami na krawędziach obrazu?). Jednak mając odpowiednią maskę możemy się zdać na bibliotekę :)

niedziela, 29 marca 2009

OpenCV cz. 3. Otwieranie i zapis obrazu wideo

OpenCV posiada możliwość odczytywania i zapisywania obrazu w wielu popularnych formatach. Funkcjonalność ta udostępniona jest w zestawie HighGui. Dopuszczalne są (podano rozszerzenia plików):bmp, dib, jpeg, jpg, jpe, png, pbm, pgm, ppm, sr, ras, tiff, tif, exr oraz jp2. Funkcje, które pozwalają przeprowadzić te operacje pojawiały się już w poprzednich wpisach. Dla przypomnienia poniżej zaprezentowano przykładowy kawałek kodu:
// wczytanie obrazu w odcieniach szarosci
IplImage*img=cvLoadImage("/home/lukasz/Pulpit/rysowanie.png",CV_LOAD_IMAGE_GRAYSCALE);
// zapisanie obrazu do pliku
cvSaveImage("/home/lukasz/Pulpit/rysowanie.png",img);

Trochę bardziej złożonym zagadnieniem jest odczyt i zapis strumienia wideo. OpenCV pozawala domyślnie na obsługę plików dvix (patrz: źródło). Z obsługą obrazu wideo związana jest analogiczna do IplImage struktura CvCapture. Wyznaję zasadę, że lepszy okomentowany kod niż długie opisy działania ;-) Poniżej przykład pokazujący odczyt i wyświetlanie pliku avi:
// odczytanie pliku avi
CvCapture* vid = cvCreateFileCapture("/home/lukasz/Pulpit/sample.avi");

// tworzymy okno wyswietlajace obraz
cvNamedWindow("plik wideo", 0);

// odczytanie pierwszej klatki - niezbedne do prawidlowego odczytania wlasciwosci pliku
// przy uzyciu funkcji cvGetCaptureProperty
cvQueryFrame(vid);

// odczytujemy z wlasciwosci pliku liczbe klatek na sekunde
double fps =  cvGetCaptureProperty(vid, CV_CAP_PROP_FPS);

// wyliczamy czas potrzebny do odtwarzania pliku z prawidlowa prędkoscia
int odstep_miedzy_klatkami = 1000 / fps;

while (true)
{
// pobranie kolejnej ramki
IplImage* ramka = cvQueryFrame(vid);

// jezeli nie jest pusta to wyswietlamy
if (ramka != 0)
cvShowImage("plik wideo", ramka);
else
break;

// czekamy przez okreslony czas
int c = cvWaitKey(odstep_miedzy_klatkami);

// jezeli nacisnieto klawisz 'k', konczymy wyswietlanie
if (c == 'k')
break;

}

// zwolnienie zasobów
cvDestroyAllWindows();
cvReleaseCapture(&vid);
Kolejnym poruszanym zagadnieniem jest odczyt i zapis obrazu z kamery. Zasadniczo nie różni się on bardzo od obsługi plików dvix. Dalej mamy do czynienia ze strukturą CvCapture. Zmienia się jedynie metoda zwracająca do niej wskaźnik. Przykład:
// pobranie zasobu kamery, parametr mowi o tym ktora kamere wybieramy
// w przypadku gdy w systemie dostepna jest wiecej niz jedna
CvCapture* vid = cvCreateCameraCapture(0);

// tworzymy okno wyswietlajace obraz
cvNamedWindow("plik wideo", CV_WINDOW_AUTOSIZE);

// odczytanie pierwszej klatki - niezbedne do prawidlowego odczytania wlasciwosci pliku
// przy uzyciu funkcji cvGetCaptureProperty
cvQueryFrame(vid);

// ustalamy wartosc fps - najlepiej wieksza niz maksymalna deklarowana przez producenta
// Jezeli wartosc bedzie za mala, obraz bedzie pojawial sie z opoznieniem
double fps =  50;

int odstep_miedzy_klatkami = 1000 / fps;

while (true)
{
// pobranie kolejnej klatki
IplImage* ramka = cvQueryFrame(vid);

// jezeli nie jest pusta to wyswietlamy
if (ramka != 0)
cvShowImage("plik wideo", ramka);
else
break;

// czekamy przez okreslony czas
int c = cvWaitKey(odstep_miedzy_klatkami);

// jezeli nacisnieto klawisz 'k', konczymy wyswietlanie
if (c == 'k')
break;

}

// zwolnienie zasobow
cvDestroyAllWindows();
cvReleaseCapture(&vid);
W powyższych kodach pojawia się funkcja cvQueryFrame. Jak wspomniano, służy ona do pobierania ramek z CvCapture. Ten sam efekt można osiągnąć posługując się kolejno funkcjami cvGrabFrame i cvRetrieveFrame. Pierwsza z nich ściąga ramkę z kamery i pozostawia ją w stanie surowym w pamięci, a druga pobiera taką ramkę i zamienia ją na IplImage.

Ostatnią funkcjonalnością dzisiaj opisaną będzie zapis wideo. Zapisywać możemy kolejne obrazy IplImage. Nie musi być to zatem obraz z kamery czy przetworzony plik wideo. Przykład:

// pobranie zasobu kamery, parametr mowi o tym ktora kamere wybieramy
// w przypadku gdy w systemie dostepna jest wiecej niz jedna
CvCapture* vid = cvCreateCameraCapture(0);

// tworzymy okno wyswietlajace obraz
// jako parametr przekazujemy CV_WINDOW_AUTOSIZE, ktory utworzy okno o wielkości
// odpowiadajacej wyswietlanemu obrazowi
cvNamedWindow("plik wideo", CV_WINDOW_AUTOSIZE);

// odczytanie pierwszej klatki - niezbedne do prawidlowego odczytania wlasciwosci pliku
// przy uzyciu funkcji cvGetCaptureProperty
cvQueryFrame(vid);

// ustalamy wartosc fps
double fps =  30;
int odstep_miedzy_klatkami = 1000 / fps;

// tworzymy strukture zapisujaca klatki do pliku
// kolejno podajemy: sciezke do pliku, format, fps i rozmiar ramki (opcjonalnie czy obraz ma byc w kolorze)
CvVideoWriter* vr = cvCreateVideoWriter("/home/lukasz/Pulpit/zapis.mpg",CV_FOURCC_DEFAULT,fps,cvSize(640,480));

while (true)
{
// pobranie kolejnej klatki
IplImage* ramka = cvQueryFrame(vid);

// jezeli nie jest pusta to wyswietlamy
if (ramka != 0)
{
cvShowImage("plik wideo", ramka);

// zapisujemy ramke do pliku
cvWriteFrame(vr,ramka);
}
else
break;

// czekamy przez okreslony czas
int c = cvWaitKey(odstep_miedzy_klatkami);

// jezeli nacisnięto klawisz 'k', konczymy wyswietlanie
if (c == 'k')
break;

}

// zwolnienie zasobow
cvDestroyAllWindows();
cvReleaseCapture(&vid);
cvReleaseVideoWriter(&vr);

Jakieś wady? Niestety tak. Nie da się ukryć, że kamerki komputerowe mają zmienny (zależny np. od oświetlenia) współczynnik fps. W CvVideoWriter nie zmienimy tego po utowrzeniu, więc film może zachowywać się nieprzewidywalnie w trakcie nagrywania. Problem nie dotyczy oczywiście zapisu wcześniej przygotowanych statycznych obrazów. Jeżeli ktoś posiada lepszy pomysł na synchronizację obrazu z kamery z nagrywaniem to zapraszam do komentarzy ;-)

poniedziałek, 23 lutego 2009

OpenCV cz. 2. Rysujemy i piszemy

Do rysowania prostych figur przygotowano odpowiednie funkcje. Podobnie ma się rzecz z wyświetlaniem tekstu. Aby wyświetlić linię prostą skorzystamy z funkcji cvLine. Jako parametry przyjmuje ona wskaźnik na CvArr, dwa punkty oznaczające końce linii oraz kolor. Ponadto można ustawić kilka parametrów opcjonalnych pozwalających ustawić grubość linii czy sposób jej wyświetlania (np. z antyaliasingiem). Przy ustawionym ROI, linia zostanie wyświetlona tylko w jego obszarze.
// tworzy linię o końcach [1,1] oraz [100,100], koloru białego, o grubości 3 pikseli
cvLine(img,cvPoint(1,1),cvPoint(100,100),cvScalarAll(255.0),3);

Jak widać, jest ona podobna w nazewnictwie do innych funkcji tej biblioteki. Idąc dalej mamy możliwość umieszczania innych obiektów geometrycznych: prostokąta, elipsy itd. Przykładowy kod na końcu notki (aby lepiej zrozumieć działanie funkcji cvEllipse zachęcam do zapoznania się z tym obazkiem). Inne funkcje wysujące (np. wielokąty) działają podobnie, w razie potrzeby odsyłam, a jakże inaczej, do dokumentacji.

Pisanie jest czynnością podobną do rysowania, ale wcześniej należy zainicjować czcionkę.
// zainicjowanie czcionki
CvFont font = cvFont(1.0,1);
cvInitFont(&font,CV_FONT_HERSHEY_SIMPLEX,1.0,1.0);

a same napisy tworzymy funkcją cvPutText.
Poniżej kod na podsumowanie:
#include "cv.h"
#include "cxcore.h"
#include "cxtypes.h"
#include "highgui.h"

/*
* Rysowanie i pisanie
* autor: ratixu.blogspot.com
*/
int main(int argc, char** argv)
{
IplImage* img = cvCreateImage(cvSize(700, 700), IPL_DEPTH_32F, 3);

// tworzenie linii o końcach w punktach [5,1] oraz [200,100]
// o kolorze białym, grubości 5 pkseli z antyaliasingiem
cvLine(img,cvPoint(5,1),cvPoint(200,100),cvScalarAll(255.0),5,CV_AA);

// tworzenie prostokąta o przekątnej o końcach w punktach [5,110]
// oraz [200,210] grubości 4 pksele
cvRectangle(img,cvPoint(5,110),cvPoint(200, 210),cvScalar(122,121,43),4);

// tworzenie elipsy - jedna z metod
CvPoint srodek = cvPoint(200,350);
CvSize osie = cvSize(200,100);
double kat = 360.0;
double katStartowy = 0.0;
double katKoncowy = kat;
CvScalar kolor = cvScalarAll(255.0);
int grubosc = 5;
cvEllipse(img,srodek,osie,kat,katStartowy,katKoncowy,kolor,grubosc);

// tworzenie okręgu o promieniu 75 pikseli i środku w punkcie [200,600]
// oraz grubości 5 pikseli
cvCircle(img,cvPoint(200,600),75,cvScalarAll(255.0),5);

// zainicjowanie czcionki:
// skala rozciągnięcia, przyjumjąc 1 za 100%
double skala = 2.0;
// pochylenie czcionki, przyjmując 1.0 za 45 stopni
double kursywa = 0.2;
CvFont font = cvFont(skala,grubosc);
// włączamy antyalliasing, CV_FONT_XXX to typy dostępnych czcionek
cvInitFont(&font,CV_FONT_HERSHEY_SIMPLEX,skala,skala,kursywa,grubosc,CV_AA);


//podpisujemy figury
cvPutText(img,"Odcinek",cvPoint(300,100),&font,cvScalarAll(255.0));
cvPutText(img,"Prostokat",cvPoint(300,200),&font,cvScalarAll(255.0));
cvPutText(img,"Elipsa",cvPoint(450,350),&font,cvScalarAll(255.0));
cvPutText(img,"Okrag",cvPoint(300,650),&font,cvScalarAll(255.0));

cvNamedWindow("img",0);
cvShowImage("img", img);
while(true)
{
if(cvWaitKey(100) == 'k') break;
}

// zapisujemy wynik do pliku
cvSaveImage("/home/lukasz/Desktop/rysowanie.png",img);
cvDestroyWindow("img");
cvReleaseImage(&img);
return (EXIT_SUCCESS);
}

a tutaj zapisany obraz:

piątek, 13 lutego 2009

OpenCV cz. 1. Podstawy

Dzisiaj będzie trochę o strukturach w OpenCV. Podstawą jest struktura o nazwie CvArr. Jak sama nazwa wskazuje jest to jednowymiarowa tablica przechowująca dane określonego typu. Dalej znajdziemy CvMat czyli tablicę wielowymiarową oraz IplImage który jest odzwierciedleniem obrazu. Struktury wymieniono w takiej kolejności nie bez powodu: można ich generalnie używać jakby istniał schemat dziedziczenia Arr -> Mat -> Image. Sama biblioteka to w większości funkcje napisane w C i kilka klas w C++. Mamy tu zatem do czynienia z klasycznymi strukturami i ciężko mówić o dziedziczeniu obiektowym. Piszę o tym ponieważ w większości funkcji bibliotecznych które przyjmują jako parametr CvArr możemy spokojnie posłać w tym miejscu CvMat lub IplImage i dalej, jeżeli przyjmuje CvMat to przyjmuje także IplImage. Nie dotyczy to jednak metod napisanych samodzielnie... W skład tych struktur, oprócz wskaźnika na dane, wchodzą inne przydatne informacje takie jak liczba kanałów, wysokość, szerokość itp. Poniżej trochę kodu:
// macierz o 30 wierszach i 40 kolumnach, składająca się z elementów 32 bitowych zmiennoprzecinkowych
CvMat* mat = cvCreateMat(30,40,CV_32F);

// utworzenie obrazka o rozdzielczości 800x600, głębi 32 bitowej i 4 kanałach
IplImage* img = cvCreateImage(cvSize(800,600),IPL_DEPTH_32F,4);

// wierna kopia obrazka, macierz posiada analogiczną metodę cvCloneMat
IplImage* img2 = cvCloneImage(img);
CvMat* mat2 = cvCloneMat(mat);

// oczywiście sprzątamy po sobie
cvReleaseImage(&img);
cvReleaseImage(&img2);
cvReleaseMat(&mat);
cvReleaseMat(&mat2);

Kolejnymi strukturami o których warto wspomnieć są: CvSize, CvScalar i CvRect. Pojawiają się one dosyć często do określania parametrów funkcji (jak np. w w/w konstruktorach). CvSize służy do określenia wysokości i szerokości, CvScalar do przetrzymywania 4 wartości typu dobule (np. jako kolor w formacie RGBA) a CvRect do określania prostokątów (wysokość,szerokość,x,y).
// prostokąt o wymiarach 40x30 kórego pozycja startowa (lewy górny róg)
// jest w punkcie [0,0]
CvRect rect = cvRect(0,0,40,30);
// podobne do CvRect ale bez punktu zaczepienia
CvSize size = cvSize(40,30);
// Prosty skalar
CvScalar scal = cvScalar(23.2,34.2,45.7,23.5);
// odpowiada to funkcji cvScalar(0.3,0.3,0.3,0.3);
CvScalar scal2 = cvScalarAll(0.3);

Takich struktur jest jeszcze trochę. W razie problemów najlepiej sięgnąć do dokumentacji i API (znajdziecie je w dziale linki - wizja komputerowa). Pisanie ułatwia oczywiście dobre IDE. Podpowiada one składnię i można szybko zobaczyć jakie parametry posiada funkcja. Wiele struktur ma gotowe funkcje tworzące, które ułatwiają poszukiwania ;-)

Czas wrócić do macierzy i obrazów. Wypadałoby jeszcze wspomnieć jak dostać się do ich elementów. Na początek najlepiej posłużyć się cvGet2D przyjmującym dwie współrzędne i, oczywiście, wskaźnik do CvArr dzięki czemu działa na macierzach i obrazkach. Podobnie przedstawia się ustawianie wartości za pomocą funkcji cvSet2D
// pobranie wartości spod komórki [1,1]
// przyjęte parametry to: wskaźnik na strukturę, współrzędna Y, współrzędna X
CvScalar s1 = cvGet2D(img,1,1);
// działa tak jak powyższa metoda, ale zwraca tylko wartość double
double s2 = cvGetReal2D(mat,1,1);

// ustawianie wartości
// przyjęte parametry to: wskaźnik na strukturę, współrzędna Y, współrzędna X,
// CvScalar z wartością
cvSet2D(mat,1,1,cvScalarAll(0.5));
cvSet2D(img,1,1,,cvScalarAll(0.5));

Oczywiście sami musimy zadbać w tym przypadku czy nie pobieramy danych spoza zakresu. Opisana metoda nie jest najszybszą (można wręcz powiedzieć że najwolnieszją) metodą dostępu to pikseli. Później poznamy szybsze ;-)

Inną użyteczną rzeczą są tzw. ROI (Region of Interest) oraz COI (Color of Interest). Pozwalają one ograniczyć obszar (lub kolor) który jest przetwarzany. Wiele funkcji sprawdza czy podany obraz ma ustawiony ROI i stosuje algorytm tylko na nim. Z COI jest już trochę gorzej ale powiedzmy że także działa :)

// ustawienie ROI na kwadrat o boku 10, zaczynający się w [0,0]
cvSetImageROI(img,cvRect(0,0,10,10));
// ustawienie kanału COI
cvSetImageCOI(img,1);

Podsumowując:
#include "cxtypes.h"
#include "cxcore.h"
#include "highgui.h"

/*
* Manipulowanie danymi
* autor: ratixu.blogspot.com
*/
int main(int argc, char** argv)
{

IplImage* img = cvCreateImage(cvSize(300,300),IPL_DEPTH_32F,3);

for(int i = 0; i < img->width; i++)
for(int j = 0; j < img->height; j++)
{
if(i==j)
cvSet2D(img,i,j,cvScalar(255,0,255,0));
else if (i < j)
cvSet2D(img,i,j,cvScalar(0,255,255,0));
else
cvSet2D(img,i,j,cvScalar(255,255,0,0));
}

cvNamedWindow("okienko", 1);
// metoda ta wyświetla obraz. Sama dostosuje rozmiar okna
cvShowImage("okienko",img);
while(true)
{
if(cvWaitKey(100) == 'q') break;
}
cvDestroyWindow("okienko");
cvReleaseImage(&img);
return (EXIT_SUCCESS);
}

i efekt wywołania: