środa, 29 grudnia 2010

Ankieta

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

wtorek, 6 kwietnia 2010

OpenCV 2.1

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

niedziela, 4 kwietnia 2010

Intel IPP i OpenCV

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

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

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

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

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

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


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

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

środa, 24 marca 2010

Segmentacja skóry oparta o reguły Bayesa

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

Reguły Bayesa

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

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

Twarz

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

Wathershed

Za ten algorytm odpowiada metoda

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

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

Histogram

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

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

Uczenie:

Mapa prawdopodobieństwa:

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

Próg 60%

Próg 90%

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

wtorek, 2 marca 2010

Śledzenie blobów

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

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

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

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

virtual IplImage* GetMask()
{
return maska;
}

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

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

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

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

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

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

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

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

using namespace std;

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

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

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

virtual IplImage* GetMask()
{
return maska;
}

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

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

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

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

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

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

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

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

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

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

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

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

CvBlobTrackerAutoParam1 params;
CvBlobTrackerAuto* tracker;

SimpleSkinDetector skd;

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

tracker = cvCreateBlobTrackerAuto1(¶ms);

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

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

tracker->Process(_img2, NULL);

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

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

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

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

return 0;
}

poniedziałek, 15 lutego 2010

Prosty detektor skóry oparty o wektor RGB

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

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

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

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