前言
在前面的博文皮膚檢測類CvAdaptiveSkinDetector的使用中,已經介紹過了這個皮膚檢測類的使用方法,因為本人對算法比較好奇,又繼續閱讀了下該算法的源碼,所以這篇文章是對該源碼做的一個簡單分析。
基礎
本算法內容是來自論文 An adaptive real-time skin detector based on Hue thresholding: A comparison on two motion tracking methods。一般的皮膚檢測都是統計大量不同光照不同環境下的皮膚特性,然后利用皮膚的這些統計信息來分割。當然了本算法的前面也是利用這些統計信息來預處理檢測皮膚的。但是由於這些統計特性只是皮膚的共性,在處理具體某個人的皮膚時未必是最好的,因為有可能把很多背景也當成了皮膚而檢測出來了。讀文該論文后總結了下該算法的核心思想:當對人進行皮膚檢測時,由於人一般是會動的,所以在使用共性的皮膚統計信息對輸入圖像進行皮膚過濾后,再在這些過濾后的皮膚像素上檢測運動信息(因為動的部分的皮膚信息更具有特性,可以代表這個人的皮膚信息),這樣得到的皮膚就可以更准確的適應這個人了,上面是個人對源碼的一些理解,理解時可以參考論文的算法流程,如下:
因為程序涉及到很多直方圖的操作,需要用到不少OpenCV中的函數,下面是其中2個函數的使用說明。
double cvGetReal1D(const CvArr* arr, int idx0)
該函數的作用是返回1D數組中下標為idx0的元素值,所以參數1為存儲元素的數組,該數組必須是單通道的;參數2為所需返回元素在數組中的下標,該下標是zero-based的。
void cvGetMinMaxHistValue(const CvHistogram* hist, float* min_value, float* max_value, int* min_idx=NULL, int*max_idx=NULL )
該函數的作用是得到直方圖hist中最小最大值,最小最大值的地址分別保存在min_value和max_value中,且其對應在hist中的小標保存在min_idx和max_idx中。
另外在讀源碼的過程中可以發現:本程序中利用直方圖這個工具只是為了好求出占直方圖前后指定百分比的橫坐標而已。
C/c++知識點總結:
一般類的構造函數中初始化私有變量的值,而初始化函數中一般是定義一些全局變量並賦值什么的。
類中關於函數中的默認參數是在函數聲明部分體現的。
程序的流程圖如下所示:
源碼注釋
Adaptiveskindetector.h:
class CV_EXPORTS CvAdaptiveSkinDetector { private: enum { GSD_HUE_LT = 3, GSD_HUE_UT = 33, GSD_INTENSITY_LT = 15, GSD_INTENSITY_UT = 250 }; class CV_EXPORTS Histogram { private: enum { HistogramSize = (GSD_HUE_UT - GSD_HUE_LT + 1) }; protected: int findCoverageIndex(double surfaceToCover, int defaultValue = 0); public: CvHistogram *fHistogram; Histogram(); virtual ~Histogram(); void findCurveThresholds(int &x1, int &x2, double percent = 0.05); void mergeWith(Histogram *source, double weight); }; int nStartCounter, nFrameCount, nSkinHueLowerBound, nSkinHueUpperBound, nMorphingMethod, nSamplingDivider; double fHistogramMergeFactor, fHuePercentCovered; Histogram histogramHueMotion, skinHueHistogram; IplImage *imgHueFrame, *imgSaturationFrame, *imgLastGrayFrame, *imgMotionFrame, *imgFilteredFrame; IplImage *imgShrinked, *imgTemp, *imgGrayFrame, *imgHSVFrame; protected: void initData(IplImage *src, int widthDivider, int heightDivider); void adaptiveFilter(); public: enum { MORPHING_METHOD_NONE = 0, MORPHING_METHOD_ERODE = 1, MORPHING_METHOD_ERODE_ERODE = 2, MORPHING_METHOD_ERODE_DILATE = 3 }; //類函數中的默認參數是在函數聲明部分體現的 CvAdaptiveSkinDetector(int samplingDivider = 1, int morphingMethod = MORPHING_METHOD_NONE); virtual ~CvAdaptiveSkinDetector(); virtual void process(IplImage *inputBGRImage, IplImage *outputHueMask); };
Adaptiveskindetector.cpp:
#include "precomp.hpp" //將pointer像素處的點的值設置為qq #define ASD_INTENSITY_SET_PIXEL(pointer, qq) {(*pointer) = (unsigned char)qq;} //判斷pointer像素處的值與v之間的差是否大於閾值,如果是則表示該點處於運動狀態 #define ASD_IS_IN_MOTION(pointer, v, threshold) ((abs((*(pointer)) - (v)) > (threshold)) ? true : false) void CvAdaptiveSkinDetector::initData(IplImage *src, int widthDivider, int heightDivider) { CvSize imageSize = cvSize(src->width/widthDivider, src->height/heightDivider); imgHueFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); //色調分量,此時不在為NULL imgShrinked = cvCreateImage(imageSize, IPL_DEPTH_8U, src->nChannels); //收縮 imgSaturationFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); //飽和度分量 imgMotionFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); imgTemp = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); //中間圖形學操作轉換用的 imgFilteredFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); imgGrayFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); //亮度分量 imgLastGrayFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 1); imgHSVFrame = cvCreateImage(imageSize, IPL_DEPTH_8U, 3); }; CvAdaptiveSkinDetector::CvAdaptiveSkinDetector(int samplingDivider, int morphingMethod) { nSkinHueLowerBound = GSD_HUE_LT; //3 nSkinHueUpperBound = GSD_HUE_UT; //33 fHistogramMergeFactor = 0.05; // empirical result fHuePercentCovered = 0.95; // empirical result nMorphingMethod = morphingMethod; //傳進來的參數2 nSamplingDivider = samplingDivider; //傳進來的參數1 //這個是內部的計數器,int類型,在類內部代碼貌似沒有用到,可能是供外部調用的 nFrameCount = 0; nStartCounter = 0; imgHueFrame = NULL; imgMotionFrame = NULL; imgTemp = NULL; imgFilteredFrame = NULL; imgShrinked = NULL; imgGrayFrame = NULL; imgLastGrayFrame = NULL; imgSaturationFrame = NULL; imgHSVFrame = NULL; }; CvAdaptiveSkinDetector::~CvAdaptiveSkinDetector() { cvReleaseImage(&imgHueFrame); cvReleaseImage(&imgSaturationFrame); cvReleaseImage(&imgMotionFrame); cvReleaseImage(&imgTemp); cvReleaseImage(&imgFilteredFrame); cvReleaseImage(&imgShrinked); cvReleaseImage(&imgGrayFrame); cvReleaseImage(&imgLastGrayFrame); cvReleaseImage(&imgHSVFrame); }; void CvAdaptiveSkinDetector::process(IplImage *inputBGRImage, IplImage *outputHueMask) { IplImage *src = inputBGRImage; int h, v, i, l; //該標志表示的是imgHueFrame這個變量(其實同時也表示一些列的圖像變量)是否分配內存了, //如果是則為true bool isInit = false; nFrameCount++; //為NULL,表示沒有分配內存 if (imgHueFrame == NULL) { isInit = true; initData(src, nSamplingDivider, nSamplingDivider); //nSamplingDivider是由構造函數傳進來的 } //分別指向各圖像的指針 unsigned char *pShrinked, *pHueFrame, *pMotionFrame, *pLastGrayFrame, *pFilteredFrame, *pGrayFrame; pShrinked = (unsigned char *)imgShrinked->imageData; pHueFrame = (unsigned char *)imgHueFrame->imageData; pMotionFrame = (unsigned char *)imgMotionFrame->imageData; pLastGrayFrame = (unsigned char *)imgLastGrayFrame->imageData; pFilteredFrame = (unsigned char *)imgFilteredFrame->imageData; pGrayFrame = (unsigned char *)imgGrayFrame->imageData; if ((src->width != imgHueFrame->width) || (src->height != imgHueFrame->height)) { //如果大小有壓縮,則imgShrinked中村的是壓縮版的原圖像,其數據內容頁復制過來了 cvResize(src, imgShrinked); cvCvtColor(imgShrinked, imgHSVFrame, CV_BGR2HSV); //理所當然,imgHSVFrame是3通道的 } else { cvCvtColor(src, imgHSVFrame, CV_BGR2HSV); } //HSV3通道打開 cvSplit(imgHSVFrame, imgHueFrame, imgSaturationFrame, imgGrayFrame, 0); cvSetZero(imgMotionFrame); //這2幅圖像清0 cvSetZero(imgFilteredFrame); l = imgHueFrame->height * imgHueFrame->width; //圖像中總像素點的個數 for (i = 0; i < l; i++) { v = (*pGrayFrame); //取出亮度分量 //GSD_INTENSITY_LT = 15, GSD_INTENSITY_UT = 250 if ((v >= GSD_INTENSITY_LT) && (v <= GSD_INTENSITY_UT)) //亮度分量滿足一定條件(15,250) { h = (*pHueFrame); if ((h >= GSD_HUE_LT) && (h <= GSD_HUE_UT)) //色調分量滿足一定條件(3,33) { //第一次進入process()函數時,這個if條件一定滿足 if ((h >= nSkinHueLowerBound) && (h <= nSkinHueUpperBound)) //pFilteredFrame為單通道的,存儲的是滿足一定皮膚條件的色調值 //如果不滿足,則說明其內部值對應為0(前面有清0操作) ASD_INTENSITY_SET_PIXEL(pFilteredFrame, h); if (ASD_IS_IN_MOTION(pLastGrayFrame, v, 7)) //前后的亮度值相差7的話,則說明該點是運動的,且很像皮膚點,同理設置 //pMotionFrame為色調值 ASD_INTENSITY_SET_PIXEL(pMotionFrame, h); } } pShrinked += 3; //pShrinked是3通道的 pGrayFrame++; pLastGrayFrame++; pMotionFrame++; pHueFrame++; pFilteredFrame++; } if (isInit) //skinHueHistogram本身是類中的一個變量,即類Histogram對象 //計算色調分量的直方圖,並保存在skinHueHistogram的直方圖變量中 //因為isInit只有一次機會等於true,所以只計算第一幀圖像的imgHueFrame,因為以后每幀的imgHueFrame //會間接利用融合后的直方圖體現出來 cvCalcHist(&imgHueFrame, skinHueHistogram.fHistogram); cvCopy(imgGrayFrame, imgLastGrayFrame); //imgLastGrayFrame保存的是上一次的圖像亮度圖 //腐蝕,消除由攝像機引起的離散的點,這里默認采用3*3的小矩形當腐蝕和膨脹的結構元素 cvErode(imgMotionFrame, imgTemp); // eliminate disperse pixels, which occur because of the camera noise cvDilate(imgTemp, imgMotionFrame); //膨脹后結果保存在imgMotionFrame中 //因為histogramHueMotion.fHistogram只計算3~33的那些直方圖,所以那些histogramHueMotion中 //為0的就沒有被計算進來 cvCalcHist(&imgMotionFrame, histogramHueMotion.fHistogram); //fHistogramMergeFactor = 0.05,這里是將直方圖skinHueHistogram和直方圖histogramHueMotion融合,其中 //histogramHueMotion占有fHistogramMergeFactor的比例(經驗值為5%,只占小部分) //直方圖融合結果保存在skinHueHistogram中 skinHueHistogram.mergeWith(&histogramHueMotion, fHistogramMergeFactor); //fHuePercentCovered = 0.95;nSkinHueLowerBound和nSkinHueUpperBound是int類型,如果以經驗值來計算的話 //nSkinHueLowerBound里存的是5%的色調直方圖的對應的色調值,nSkinHueUpperBound存的是95%色調直方圖中的色調值 //由源碼可知,nSkinHueLowerBound>=3,nSkinHueUpperBound<=33 skinHueHistogram.findCurveThresholds(nSkinHueLowerBound, nSkinHueUpperBound, 1 - fHuePercentCovered); //因為最后的輸出值直接來源於imgFilteredFrame,所以這里的形態學操作直接是針對imgFilteredFrame的 switch (nMorphingMethod) { case MORPHING_METHOD_ERODE : //進行1次腐蝕操作 cvErode(imgFilteredFrame, imgTemp); cvCopy(imgTemp, imgFilteredFrame); break; case MORPHING_METHOD_ERODE_ERODE : //進行2次腐蝕操作 cvErode(imgFilteredFrame, imgTemp); cvErode(imgTemp, imgFilteredFrame); break; case MORPHING_METHOD_ERODE_DILATE : //先腐蝕和膨脹操作 cvErode(imgFilteredFrame, imgTemp); cvDilate(imgTemp, imgFilteredFrame); break; } //所以一定要先給outputHueMask分配內存 if (outputHueMask != NULL) //輸出滿足皮膚條件的像素圖像 cvCopy(imgFilteredFrame, outputHueMask); }; //------------------------- Histogram for Adaptive Skin Detector -------------------------// //該函數就是創建一個滿足要求的直方圖而已 CvAdaptiveSkinDetector::Histogram::Histogram() { // HistogramSize = (GSD_HUE_UT - GSD_HUE_LT + 1),是一個枚舉類型 int histogramSize[] = { HistogramSize }; float range[] = { GSD_HUE_LT, GSD_HUE_UT }; float *ranges[] = { range }; //創建一個直方圖,bin是從3到33,總共31個bin,CV_HIST_ARRAY代表直方圖數據類型 //為multi-dimensional dense array fHistogram = cvCreateHist(1, histogramSize, CV_HIST_ARRAY, ranges, 1); cvClearHist(fHistogram); }; CvAdaptiveSkinDetector::Histogram::~Histogram() { cvReleaseHist(&fHistogram); }; int CvAdaptiveSkinDetector::Histogram::findCoverageIndex(double surfaceToCover, int defaultValue) { double s = 0; for (int i = 0; i < HistogramSize; i++) { //對所以bins的值的累加(因為一個直方圖其實就是一個vector而已,所以這里可以直接獲取它的值) s += cvGetReal1D( fHistogram->bins, i ); //得到第i個值 if (s >= surfaceToCover) { return i; //返回收斂處的下標索引,也就是說返回直方圖累計值值為surfaceToCover的下標 } } return defaultValue; //如果沒有滿足條件的下標, //則直接返回默認的下標,由源碼可知,該下標其實就是-1 }; //該函數的作用是返回直方圖總值的percent比例和(1-percent)比例的x坐標值,分別保存在x1和x2這2個數中 void CvAdaptiveSkinDetector::Histogram::findCurveThresholds(int &x1, int &x2, double percent) { double sum = 0; for (int i = 0; i < HistogramSize; i++) { sum += cvGetReal1D( fHistogram->bins, i ); //總的bin處的值 } x1 = findCoverageIndex(sum * percent, -1); //返回前百分比為percent處的下標索引 x2 = findCoverageIndex(sum * (1-percent), -1); //返回前百分比為(1-percent)處的下標索引 if (x1 == -1) //個人感覺x1不可能等於-1 x1 = GSD_HUE_LT; //GSD_HUE_LT = 3 else x1 += GSD_HUE_LT; if (x2 == -1) x2 = GSD_HUE_UT; //GSD_HUE_UT = 33 else x2 += GSD_HUE_LT; }; void CvAdaptiveSkinDetector::Histogram::mergeWith(CvAdaptiveSkinDetector::Histogram *source, double weight) { float myweight = (float)(1-weight); float maxVal1 = 0, maxVal2 = 0, *f1, *f2, ff1, ff2; //cvGetMinMaxHistValue()函數為求直方圖source的最大最小值以及它們對應的位置, //這里只求出其最大值,保存在maxVal2中 cvGetMinMaxHistValue(source->fHistogram, NULL, &maxVal2); if (maxVal2 > 0 ) { //求直方圖fHistogram最大值,保存在maxVall中 cvGetMinMaxHistValue(fHistogram, NULL, &maxVal1); if (maxVal1 <= 0) { for (int i = 0; i < HistogramSize; i++) { //cvPtr1D()函數為返回對應索引值處的指針 f1 = (float*)cvPtr1D(fHistogram->bins, i); f2 = (float*)cvPtr1D(source->fHistogram->bins, i); (*f1) = (*f2); } } else { for (int i = 0; i < HistogramSize; i++) { f1 = (float*)cvPtr1D(fHistogram->bins, i); f2 = (float*)cvPtr1D(source->fHistogram->bins, i); ff1 = ((*f1)/maxVal1)*myweight; //直方圖1歸一化后取出myweight比例的值,准備和直方圖2融合 if (ff1 < 0) //有小於0的情況嗎? ff1 = -ff1; ff2 = (float)(((*f2)/maxVal2)*weight); if (ff2 < 0) //直方圖2歸一化后取出weight比例的值,准備和直方圖1融合 ff2 = -ff2; (*f1) = (ff1 + ff2); //最終融合結果放到直方圖1中 } } } };
參考資料:
皮膚檢測類CvAdaptiveSkinDetector的使用