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 :)