1、RGB color space
2、Ycrcb之cr分量+otsu阈值化
3、YCrCb中133<=Cr<=173 77<=Cb<=127
4、HSV中 7
6、opencv自带肤色检测类AdaptiveSkinDetector
不过经检验,用RGB的方法受光线影响比较大,鲁棒性太低了,所以我们这次就不实验它了,留下一个判别式就好。
此处主要说第五项,其他项请参考 https://blog.csdn.net/qq_22527639/article/details/81501565
经过前人学者大量的皮肤统计信息可以知道,如果将皮肤信息映射到YCrCb空间,则在CrCb二维空间中这些皮肤像素点近似成一个椭圆分布。因此如果我们得到了一个CrCb的椭圆,下次来一个坐标(Cr, Cb)我们只需判断它是否在椭圆内(包括边界),如果是,则可以判断其为皮肤,否则就是非皮肤像素点。
看看这个YCrCb是个啥:
肤色YCbCr颜色空间是一种常用的肤色检测的色彩模型,其中Y代表亮度,Cr代表光源中的红色分量,Cb代表光源中的蓝色分量。人的肤色在外观上的差异是由色度引起的,不同人的肤色分布集中在较小的区域内。肤色的YCbCr颜色空间CbCr平面分布在近似的椭圆区域内,通过判断当前像素点的CbCr是否落在肤色分布的椭圆区域内,就可以很容易地确认当前像素点是否属于肤色。将图像转换到YCbCr空间并且在CbCr平面进行投影,可以采集到肤色的样本点。
将CbCr平面均分为许多小区域,将每个区域的中心点CbCr色度值作为当前区域的特征值,对肤色区域像素值进行遍历,如果当前像素值落在该区域内则替换当前区域特征值。
这么一看,和HSV模型有点像哈。
3、YUV和RGB互相转换的公式如下(RGB取值范围均为0-255)︰
Y = 0.299R + 0.587G + 0.114B
U = -0.147R - 0.289G + 0.436B
V = 0.615R - 0.515G - 0.100B
R = Y + 1.14V
G = Y - 0.39U - 0.58V
B = Y + 2.03U
大概知道了,YUV(YCrCb)就是一个单独把亮度分离开来的颜色模型,使用这个颜色模型的话,像肤色不会受到光线亮度而发生改变。
在实际代码中,该椭圆是采用绘画函数绘制到图片上的,一句代码而已:(参数固定)
ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);
void ellipse(Mat& img, Point center, Size axes, double angle, double startAngle, double endAngle, const Scalar&color, int thickness=1, int lineType=8, int shift=0)
该函数是用来在指定图片上绘制椭圆弧线的。 参数image为需要绘制椭圆的图像;参数center是该椭圆的中心点坐标;参数axes是该椭圆的长半轴和短半轴;参数angle是该椭圆和水平方向上的旋转夹角;参数startAngle表示绘制椭圆弧线相对该椭圆自己的水平轴的起始角度;参数endAngel表示绘制椭圆弧线相对该椭圆自己的水平轴的终止角度;后面的参数比较普通就不介绍了。
即将图像转化到YCbCr 空间并且在CbCr平面进行投影,因此我们采集了肤色的样本点,将其投影到此平面,并且投影后,我们进行了相应的非线性变换K-L变换
长短半轴的长度,以及角度是怎么得出来的,根据这个图形,大概就可以看出来角度是43~45之间了。这个参数应该是固定的。
#include
#include
using namespace cv;
using namespace std;
void ellipse_detect(Mat &src, Mat& detected_one);
int main(int argc, char** argv)
{
Mat src = imread("song.jpg");
if (src.empty()){
cerr << "could not load image..." << endl;
return -1;
}
namedWindow("Pre Image", CV_WINDOW_AUTOSIZE);
imshow("Pre Image", src);
//需要处理一下尺寸
resize(src, src, Size(src.cols, src.rows));
//肤色检测
Mat detectImg;
ellipse_detect(src, detectImg);
imshow("肤色检测图", detectImg);
waitKey(0);
return 0;
}
void ellipse_detect(Mat &src, Mat& detected_one)
{
Mat img = src.clone();
Mat skinCrCbHist = Mat::zeros(Size(256, 256), CV_8UC1); //256*256的矩阵,相当于CrCb分量的横纵坐标
//利用Opencv自带的椭圆生成函数生成一个肤色椭圆模型
ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);
Mat ycrcb_image;
Mat output_mask = Mat(img.size(), CV_8UC1);
output_mask = Scalar::all(0);
cvtColor(img, ycrcb_image, COLOR_BGR2YCrCb);//转换为YCrCb模型
for (int row = 0; row < ycrcb_image.rows; row++){
uchar *currentRow = ycrcb_image.ptr(row);//ycrcb_image指针
uchar *maskRow = output_mask.ptr(row);//mask指针
for (int col = 0; col < ycrcb_image.cols; col++){
uchar y, cr, cb;
y = *currentRow++;
cr = *currentRow++;
cb = *currentRow++;
Vec3b ycrcb = Vec3b(y, cr, cb);
if (skinCrCbHist.at(cr, cb)>0){
*maskRow = 255;
}
else{
*maskRow = 0;
}
maskRow++;
}
}
img.copyTo(detected_one, output_mask);//返回肤色图
//表示img和output_mask与操作后赋给detected_one
}
这段代码的大致流程是这样的,首先先设置一个单通道的256*256大小的二维矩阵,全0填充完也就是黑色的了。
Mat skinCrCbHist = Mat::zeros(Size(256, 256), CV_8UC1);//256*256的矩阵,相当于CrCb分量的横纵坐标。
然后再上面画一个椭圆,具体位置大小角度等是固定的了,这是前人实验总结出来的。
ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);//画椭圆,-1表示全填充,也就是说这个椭圆是全白的
接下来把获取的图像转为YCrCb颜色模型的图像,由于用这个模型就是为了排除Y(亮度)的影响,所以横纵坐标分别是Cb,Cr,看上面的图就知道了。转完后,遍历转完图像的像素点,看其后两个通道的值,也就是Cb,Cr的值,转到前面的掩模上就是它的横纵坐标,那我们之前根据这个坐标,对应到的位置如果在椭圆里面,之前椭圆是白色的嘛,底是黑色的,那么说明这个点是肤色点了。
也就是在这个图阴影的位置。
我们新建了一个掩模,Mat output_mask = Mat::zeros(img.size(), CV_8UC1);,跟原图一样大小,不过是黑色的。然后如果原图上哪一个点是肤色像素点,那么我们就把掩模上的这个点设为255,之后用copyTo函数的时候,会把这两个图img、output_mask进行掩模运算(与操作)再赋值detected_one过去,就是只显示肤色像素点的图了。
说起掩模,可以去这个地方看:http://www.cnblogs.com/skyfsm/p/6894685.html