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...