前言
在前面的博文皮肤检测类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中 } } } };
参考资料: