ś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;
}