一 、背景知识:(自己看这块的一些笔记)
(1)数字电视的色彩空间和计算机不同,不是RGB空间,而是采用一个亮度信号(Y)和两个色差信号(R-Y、B-Y)的YUV空间或者叫YCbCr空间。数字电视采用YUV(YCbCr)色彩空间的原因主要就是为了减少数据储存空间和数据传输带宽,同时又能非常方便的兼容黑白电视(R-Y和B-Y信号为零)
(2)来源上的差异
yuv色彩模型来源于rgb模型,该模型的特点是将亮度和色度分离开,从而适合于图像处理领域。
应用:basic color model used in analogue color TV broadcasting.
YCbCr模型来源于yuv模型。YCbCr is a scaled and offset version of the YUV color space.
应用:数字视频,ITU-R BT.601 recommendation
(3)YCbCr其中Y是指亮度分量,Cb指蓝色色度分量,而Cr指红色色度分量。人的肉眼对视频的Y分量更敏感,因此在通过对色度分量进行子采样来减少色度分量后,肉眼将察觉不到的图像质量的变化。主要的子采样格式有 YCbCr 4:2:0、YCbCr 4:2:2 和 YCbCr 4:4:4。
(4)皮肤检测主要是根据肤色在颜色空间上的分布特征来检测图像中的肌肤区域。利用颜色信息对皮肤检测,应为不同人的肤色在排除亮度、室环境等影响对肤色的影响,皮肤的色调基本一致。因此可以利用颜色信息来进行皮肤检测。YCbCr色彩模型被广泛应用在电视的色彩显示领域。CbCr其中Y是指亮度分量,Cb指蓝色色度分量,而Cr指红色色度分量,而且在空间将色度与亮度分离的特点。且是二维独立分布,能较好的限制肤色分布区域。通过对肤色的像素点在YCbCr空间投影,聚类得到了肤色聚类成椭圆形。
(5)首先采集肤色样本,进行颜色空间转化,并进行图像平滑处理,将噪声减小。并且对于每一个像素转化到cbcr平面,投影,转化为高斯模型,,并且对于新的一个加入点,计算其高斯分布的概率,最大似然性或其他方法来对人的皮肤进行分割。
(6)YCbCr颜色空间
YCbCr颜色空间是以演播室质量标准为目标的CCIR601编码方案中采用的色彩表示模型,被广泛应用于电视的色彩显示等领域中。YCbCr颜色空间中,Y分量表示亮度,Cb和Cr分量分别表示蓝色和红色的色度。亮度信号Y与色度信号Cb,Cr的采样比率为4:2:2,
(7)YCbCr空间有如下特点: 1)YCbCr广泛应用于图像和视频压缩标准,例如MPEG和JPEG;
2)YCbCr空间是离散空间,易于实现聚类算法;
3)YCbCr空间是感知均匀的色度空间,与人类视觉感知过程相类似。
(8)在大部分基于像素级别的皮肤识别方法为了减少光照条件对皮肤颜色的影响以及降低计算量,通常在选择颜色空间时考虑放弃亮度分量,在2D颜色空间内对皮肤像素进行建模,这似乎是合理的。因为我们的目标是对所谓的“肤色块”建模,而“肤色块”明显受色度的影响要大于受亮度的影响,并且通过丢弃亮度分量也可以简化颜色分析的计算量。可是事实并非如此,实际上放弃亮度分量并没有增加反而是减少了肤色与非肤色的可分性。
(9)要将皮肤检测与识别技术与其应用相结合来考虑颜色空间的选择问题。因为皮肤检测技术一般只是某些具体应用的中间过程,如果其用于某些实时系统,可以适当降低精度要求,这时就可以考虑放弃亮度分量,使用2D颜色空间来节省计算时间。但是如果要求很高的检测精度,最好使用传统的3D颜色空间。
(10)阈值法
在实际应用中,考虑到每个颜色空间都有自己的优势,所以有时不仅仅是在个颜色空间上进行阈值分割,而采取多个颜色空间相结合的方法进行,以提高划分的准确率直方图的自动色偏检测。采用CIE Lab颜色空间,此空间所计算出来的颜色之间的距离与实际感知上的差别基本一致将RGB先转换成xyz,再由xyz空间最终转换成CIE Lab色度空间。在CIE Lab下进行偏色图像的自动检测。
(11)K-L变换( Karhunen-Loeve Transform)是建立在统计特性基础上的一种变换,有的文献也称为霍特林(Hotelling)变换,因他在1933年最先给出将离散信号变换成一串不相关系数的方法。K-L变换的突出优点是相关性好,是均方误差(MSE,Mean Square Error)意义下的最佳变换,它在数据压缩技术中占有重要地位。
需要作修改的就是if(y<100) (*pMask)=(value<700) ? 255:0; else (*pMask)=(value<850)? 255:0; 这条做阈值判断的命令
于光照和摄像头性能的不同,这里的阈值需要根据自己的摄像头调节出最合适的效果才可以
另外的话,对于质量不是很好的WebCam 建议在输入图像上加一个小点的高斯模糊以去除噪点
关于效果图里面一些类似噪点的部分,可以通过膨胀腐蚀模糊再二值化的方法取得比较圆润的肤色图(就是可以做mask的)
- cvErode(pSkin, pSkin, NULL, 1);
- cvDilate(pSkin, pSkin, NULL, 1);
- cvSmooth(pSkin, pSkin, CV_GAUSSIAN, 21, 0, 0);
- cvThreshold(pSkin, pSkin,130, 255, CV_THRESH_BINARY);
二、皮肤检测----肤色椭圆模型
肤色区域的颜色与亮度成非线性函数关系,在低亮度条件下,YCbCr 空间中色度的聚类性会随Y 呈非线性变换降低。为了使肤色聚类不受亮度Y 的影响并将YCbCr 颜色空间中的色度Cb、Cr进行非线性变换,在研究YCbCr 颜色空间的肤色聚类情况的基础上,去掉高光阴影部分(即 Y 的最大最小值),YCbCr 空间色度非线性变换过程中,用 Cb·· Y 、 Cr·· Y 表示肤色区域的中轴线,肤色区域的宽度分别用 Vcb、Vcr 表示。
即将图像转化到YCbCr 空间并且在CbCr平面进行投影,因此我们采集了肤色的样本点,将其投影到此平面,并且投影后,我们进行了相应的非线性变换K-L变换
进而形成的的统计椭圆模型
这里参数的说明参考《基于分裂式K均值聚类的肤色检测方法 》
代码如下: (完全代码在附件中可找到)
- void cvSkinSegment(IplImage* img, IplImage* mask) //原始图像及目标图像
- {
- CvSize imageSize = cvSize(img->width, img->height);
- IplImage *imgY = cvCreateImage(imageSize, IPL_DEPTH_8U, 1);
- IplImage *imgCr = cvCreateImage(imageSize, IPL_DEPTH_8U, 1);
- IplImage *imgCb = cvCreateImage(imageSize, IPL_DEPTH_8U, 1);
- IplImage *imgYCrCb = cvCreateImage(imageSize, img->depth, img->nChannels);
- cvCvtColor(img,imgYCrCb,CV_BGR2YCrCb);
- cvSplit(imgYCrCb, imgY, imgCr, imgCb, 0); //得到每一通道的数据
- int y, cr, cb, l, x1, y1, value;
- unsigned char *pY, *pCr, *pCb, *pMask;
- pY = (unsigned char *)imgY->imageData; //Y通道的第一行数据的指针
- pCr = (unsigned char *)imgCr->imageData;
- pCb = (unsigned char *)imgCb->imageData;
- pMask = (unsigned char *)mask->imageData; //目标图像的第一行数据的指针
- cvSetZero(mask); //目标图像全部为零
- l = img->height * img->width; //图像的元素个数
- for (int i = 0; i < l; i++)
- {
- y = *pY;
- cr = *pCr;
- cb = *pCb;
- cb -= 109;
- cr -= 152;
- x1 = (819*cr-614*cb)/32 + 51;
- y1 = (819*cr+614*cb)/32 + 77;
- x1 = x1*41/1024;
- y1 = y1*73/1024;
- value = x1*x1+y1*y1; //构造椭圆的模型
- if(y<100) (*pMask)=(value<700) ? 255:0; //对齐进行值得选择,要不255,要不0 //在不同的亮度下呈现出不同的亮度
- else (*pMask)=(value<850)? 255:0; //255 is the skin, 0 is the background //255 纯白色 0 纯黑色
- pY++;
- pCr++;
- pCb++;
- pMask++;
- }
- //cvSaveImage("example.jpg",mask);
- //cvErode(mask, mask, NULL, 1);
- //cvDilate(mask, mask, NULL, 1);
- //cvSmooth(mask, mask, CV_GAUSSIAN, 21, 0, 0);
- //cvThreshold(mask, mask,130, 255, CV_THRESH_BINARY);
- cvReleaseImage(&imgY);
- cvReleaseImage(&imgCr);
- cvReleaseImage(&imgCb);
- cvReleaseImage(&imgYCrCb);
- }
三、连通域选择,从而消除光线的影响
肤色检测往往都存在的弊端是受光线的影响(图1),而且我们考虑到我们所要的区域是占很大一部分的,并非噪声点,因此我进行了连通域的统计,我这里是只要的最大的连通域,如果我们想排除那些噪声点,或光线,可以将设置一个阈值,或者选取前几大的连通域部分即可
由于检测到的部分受光线的影响很大,并且很容易误判的图样:
图1
连通域统计代码:
- //此部分找到每一个相连通的部分, 并对每一个连通的部分进行赋值,使其被赋予不同的值!
- for(int y=0;y<pic->height;y++) //图像的高度
- {
- uchar* ptr=(uchar*)(pic->imageData+y*pic->widthStep); //已检测皮肤的图像
- uchar* ptr1=(uchar*)(out->imageData+y*out->widthStep); //将要输出的图像
- for(int x=0;x<pic->width;x++)
- {
- if(ptr[x]==255) //此图像只有0和255 //皮肤检测的误差很大pic的点 //如果是皮肤
- {
- struct buck bk=get_point(out,x,y); //得到小范围的块3 * 3
- if((bk.p[0][0]+bk.p[0][1]+bk.p[0][2]+bk.p[1][0]+bk.p[1][2]+bk.p[2][0]+bk.p[2][1]+bk.p[2][2])==0) //小范围都是皮肤点
- {
- ptr1[x]=(++c_n); //那个像素点是第几个连通的皮肤点! 对输出的out图像进行赋值 //c_n记录这是第几个连通域
- }
- else //新图的此处周围已经有皮肤点了
- { //不是大范围的连通区域 即 0 0 0
- // 0 0 0
- // 0 0 0
- c_now=0;
- for(int j=0;j<3;j++)
- {
- for(int i=0;i<3;i++) //这个小范围的域块
- {
- if(bk.p[j][i]>0 && c_now==0)c_now=bk.p[j][i]; //小连通域的赋值 //背景点
- if(bk.p[j][i]>0 && bk.p[j][i]!=c_now && c_now>0) //小连通域的标前一个为0
- {
- c_xx=bk.p[j][i]; //此个小域的一点的赋值
- for(int yy=0;yy<out->height;yy++) //细细的分析一下如果是1 1 0 0 0 0
- // 0 1 0 2 2 0
- { // 0 0 ? 2 0 2
- //则利用此for循环进行相应的统一为一个数 //1 1 0 0 0 0
- uchar* ptr2=(uchar*)(out->imageData+yy*out->widthStep); //0 1 0 1 1 0
- for(int xx=0;xx<out->width;xx++) //此时的像素点 //0 0 1 1 0 1
- {
- if(ptr2[xx]==c_xx)
- {
- ptr2[xx]=c_now;
- }
- }
- }
- }
- }
- }
- ptr1[x]=c_now; //并且此点也为那个前一个连通的区域
- }
- }
- }
- }
- //此部分将最大的那个连通域进行提取,其他的删去
- for(int y=0;y<out->height;y++)
- {
- uchar* ptr=(uchar*)(out->imageData+y*out->widthStep);
- for(int x=0;x<out->width;x++)
- {
- if(ptr[x]==0)continue; //背景点则不进行处理
- num_c[ptr[x]]++; //统计每一个连通域的元素数目
- }
- }
- c_n=num_c[0]; //从第一个统计到的数开始,这并不一定是1的元素连通数,可能是第4个连通域的元素个数
- c_now=0;
- for(int i=1;i<256;i++)
- {
- if(num_c[i]>c_n) //比较得出最大的那个数目的连通域
- {
- c_now=i; //这是那个最多的的数字标识
- c_n=num_c[i];
- }
- }
- int hand_all=0,hand_x=0,hand_y=0;
- for(int y=0;y<out->height;y++)
- {
- uchar* ptr=(uchar*)(out->imageData+y*out->widthStep);
- for(int x=0;x<out->width;x++)
- {
- if(ptr[x]==c_now) //out图像中那个数是标识数时
- {
- ptr[x]=255; //赋予此值为255
- hand_all++; //一共找到的个数,即连通的最大域的元素个数
- hand_x+=x; //x坐标的和?
- hand_y+=y; //y坐标的和?
- }
- else
- {
- ptr[x]=0; //剩下的值都为0, 即生成为背景值
- }
- }
- }
(图2)
参考文献 ------《基于分裂式K均值聚类的肤色检测方法 》 、 《 皮肤检测技术的研究及改进》
附件代码:skin_detect.cpp