好久没写博客了,因为最近都忙着赶项目和打比赛==| 好吧,今天我打算写一篇关于使用opencv做皮肤检测的技术总结。那首先列一些现在主流的皮肤检测的方法都有哪些:

  1. RGB color space
  2. Ycrcb之cr分量+otsu阈值化
  3. YCrCb中133<=Cr<=173 77<=Cb<=127
  4. HSV中 7<H<20 28<S<256 50<V<256
  5. 基于椭圆皮肤模型的皮肤检测
  6. opencv自带肤色检测类AdaptiveSkinDetector

那我们今天就来一一实现它吧!

方法一:基于RGB的皮肤检测

根据RGB颜色模型找出定义好的肤色范围内的像素点,范围外的像素点设为黑色。

查阅资料后可以知道,前人做了大量研究,肤色在RGB模型下的范围基本满足以下约束:

在均匀光照下应满足以下判别式:

R>95 AND G>40 B>20 AND MAX(R,G,B)-MIN(R,G,B)>15 AND ABS(R-G)>15 AND R>G AND R>B

在侧光拍摄环境下:

R>220 AND G>210 AND B>170 AND ABS(R-G)<=15 AND R>B AND G>B

既然判别式已经确定了,所以按照判别式写程序就很简单了。

  1. /*基于RGB范围的皮肤检测*/
  2. Mat RGB_detect(Mat& img)
  3. {
  4. /*
  5. R>95 AND G>40 B>20 AND MAX(R,G,B)-MIN(R,G,B)>15 AND ABS(R-G)>15 AND R>G AND R>B
  6. OR
  7. R>220 AND G>210 AND B>170 AND ABS(R-G)<=15 AND R>B AND G>B
  8. */
  9. Mat detect = img.clone();
  10. detect.setTo(0);
  11. if (img.empty() || img.channels() != 3)
  12. {
  13. return detect;
  14. }
  15. for (int i = 0; i < img.rows; i++)
  16. {
  17. for (int j = 0; j < img.cols; j++)
  18. {
  19. uchar *p_detect = detect.ptr<uchar>(i, j);
  20. uchar *p_img = img.ptr<uchar>(i, j);
  21. if ((p_img[2] > 95 && p_img[1]>40 && p_img[0] > 20 &&
  22. (MAX(p_img[0], MAX(p_img[1], p_img[2])) - MIN(p_img[0], MIN(p_img[1], p_img[2])) > 15) &&
  23. abs(p_img[2] - p_img[1]) > 15 && p_img[2] > p_img[1] && p_img[1] > p_img[0]) ||
  24. (p_img[2] > 200 && p_img[1] > 210 && p_img[0] > 170 && abs(p_img[2] - p_img[1]) <= 15 &&
  25. p_img[2] > p_img[0] && p_img[1] > p_img[0]))
  26. {
  27. p_detect[0] = p_img[0];
  28. p_detect[1] = p_img[1];
  29. p_detect[2] = p_img[2];
  30. }
  31. }
  32. }
  33. return detect;
  34. }

检测效果如下:

从检测结果可以看出,皮肤的检测效果并不好,首先皮肤检测的完整性并不高,一些稍微光线不好的区域也没法检测出皮肤来。第二,这种基于RBG范围来判定皮肤的算法太受光线的影响了,鲁棒性确实不好。

方法二:基于椭圆皮肤模型的皮肤检测

经过前人学者大量的皮肤统计信息可以知道,如果将皮肤信息映射到YCrCb空间,则在CrCb二维空间中这些皮肤像素点近似成一个椭圆分布。因此如果我们得到了一个CrCb的椭圆,下次来一个坐标(Cr, Cb)我们只需判断它是否在椭圆内(包括边界),如果是,则可以判断其为皮肤,否则就是非皮肤像素点。

  1. /*基于椭圆皮肤模型的皮肤检测*/
  2. Mat ellipse_detect(Mat& src)
  3. {
  4. Mat img = src.clone();
  5. Mat skinCrCbHist = Mat::zeros(Size(256, 256), CV_8UC1);
  6. //利用opencv自带的椭圆生成函数先生成一个肤色椭圆模型
  7. ellipse(skinCrCbHist, Point(113, 155.6), Size(23.4, 15.2), 43.0, 0.0, 360.0, Scalar(255, 255, 255), -1);
  8. Mat ycrcb_image;
  9. Mat output_mask = Mat::zeros(img.size(), CV_8UC1);
  10. cvtColor(img, ycrcb_image, CV_BGR2YCrCb); //首先转换成到YCrCb空间
  11. for (int i = 0; i < img.cols; i++) //利用椭圆皮肤模型进行皮肤检测
  12. for (int j = 0; j < img.rows; j++)
  13. {
  14. Vec3b ycrcb = ycrcb_image.at<Vec3b>(j, i);
  15. if (skinCrCbHist.at<uchar>(ycrcb[1], ycrcb[2]) > 0) //如果该落在皮肤模型椭圆区域内,该点就是皮肤像素点
  16. output_mask.at<uchar>(j, i) = 255;
  17. }
  18. Mat detect;
  19. img.copyTo(detect,output_mask); //返回肤色图
  20. return detect;
  21. }

检测效果:

这种基于肤色椭圆模型的算法的皮肤检测较上面算法在效果上有着较大的提升,基本上改检测的皮肤都检测到了,对光线的抗干扰能力也是比较强的,检测出来的图像都比较干净,背景杂质较少。

法三:YCrCb颜色空间Cr分量+Otsu法阈值分割

这里先简单介绍YCrCb颜色空间。

YCrCb即YUV,其中“Y”表示明亮度(Luminance或Luma),也就是灰阶值;而“U”和“V” 表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。“亮度”是透过RGB输入信号来建立的,方法是将RGB信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面─色调与饱和度,分别用Cr和Cb来表示。其中,Cr反映了RGB输入信号红色部分与RGB信号亮度值之间的差异。而Cb反映的是RGB输入信号蓝色部分与RGB信号亮度值之间的差异。

该方法的原理也很简单:

a.将RGB图像转换到YCrCb颜色空间,提取Cr分量图像

b.对Cr做自二值化阈值分割处理(Otsu法)

  1. /*YCrCb颜色空间Cr分量+Otsu法*/
  2. Mat YCrCb_Otsu_detect(Mat& src)
  3. {
  4. Mat ycrcb_image;
  5. cvtColor(src, ycrcb_image, CV_BGR2YCrCb); //首先转换成到YCrCb空间
  6. Mat detect;
  7. vector<Mat> channels;
  8. split(ycrcb_image, channels);
  9. Mat output_mask = channels[1];
  10. threshold(output_mask, output_mask, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
  11. src.copyTo(detect, output_mask);
  12. return detect;
  13. }

检测效果:

法四:基于YCrCb颜色空间Cr,Cb范围筛选法

这个方法跟法一其实大同小异,只是颜色空间不同而已。据资料显示,正常黄种人的Cr分量大约在133至173之间,Cb分量大约在77至127之间。大家可以根据自己项目需求放大或缩小这两个分量的范围,会有不同的效果。

  1. /*YCrCb颜色空间Cr,Cb范围筛选法*/
  2. Mat YCrCb_detect(Mat & src)
  3. {
  4. Mat ycrcb_image;
  5. int Cr = 1;
  6. int Cb = 2;
  7. cvtColor(src, ycrcb_image, CV_BGR2YCrCb); //首先转换成到YCrCb空间
  8. Mat output_mask = Mat::zeros(src.size(), CV_8UC1);
  9. for (int i = 0; i < src.rows; i++)
  10. {
  11. for (int j = 0; j < src.cols; j++)
  12. {
  13. uchar *p_mask = output_mask.ptr<uchar>(i, j);
  14. uchar *p_src = ycrcb_image.ptr<uchar>(i, j);
  15. if (p_src[Cr] >= 133 && p_src[Cr] <= 173 && p_src[Cb] >= 77 && p_src[Cb] <= 127)
  16. {
  17. p_mask[0] = 255;
  18. }
  19. }
  20. }
  21. Mat detect;
  22. src.copyTo(detect, output_mask);;
  23. return detect;
  24. }

检测效果:

法五:HSV颜色空间H范围筛选法

同样地,也是在不同的颜色空间下采取相应的颜色范围将皮肤分割出来。

  1. /*HSV颜色空间H范围筛选法*/
  2. Mat HSV_detector(Mat& src)
  3. {
  4. Mat hsv_image;
  5. int h = 0;
  6. int s = 1;
  7. int v = 2;
  8. cvtColor(src, hsv_image, CV_BGR2HSV); //首先转换成到YCrCb空间
  9. Mat output_mask = Mat::zeros(src.size(), CV_8UC1);
  10. for (int i = 0; i < src.rows; i++)
  11. {
  12. for (int j = 0; j < src.cols; j++)
  13. {
  14. uchar *p_mask = output_mask.ptr<uchar>(i, j);
  15. uchar *p_src = hsv_image.ptr<uchar>(i, j);
  16. if (p_src[h] >= 0 && p_src[h] <= 20 && p_src[s] >=48 && p_src[v] >=50)
  17. {
  18. p_mask[0] = 255;
  19. }
  20. }
  21. }
  22. Mat detect;
  23. src.copyTo(detect, output_mask);;
  24. return detect;
  25. }

检测效果:

法六:opencv自带肤色检测类AdaptiveSkinDetector

opencv提供了下面这个好用的皮肤检测函数:

CvAdaptiveSkinDetector(int samplingDivider = 1, int morphingMethod = MORPHING_METHOD_NONE);

这个函数的第二个参数表示皮肤检测过程时所采用的图形学操作方式,其取值有3种可能:

  • 如果为MORPHING_METHOD_ERODE,则表示只进行一次腐蚀操作;
  • 如果为MORPHING_METHOD_ERODE_ERODE,则表示连续进行2次腐蚀操作;
  • 如果为MORPHING_METHOD_ERODE_DILATE,则表示先进行一次腐蚀操作,后进行一次膨胀操作。
  1. /*opencv自带肤色检测类AdaptiveSkinDetector*/
  2. Mat AdaptiveSkinDetector_detect(Mat& src)
  3. {
  4. IplImage *frame;
  5. frame = &IplImage(src); //Mat -> IplImage
  6. CvAdaptiveSkinDetector filter(1, CvAdaptiveSkinDetector::MORPHING_METHOD_ERODE_DILATE);
  7. IplImage *maskImg = cvCreateImage(cvSize(src.cols, src.rows), IPL_DEPTH_8U, 1);
  8. IplImage *skinImg = cvCreateImage(cvSize(src.cols, src.rows), IPL_DEPTH_8U, 3);
  9. cvZero(skinImg);
  10. filter.process(frame, maskImg); // process the frame
  11. cvCopy(frame, skinImg, maskImg);
  12. Mat tmp(skinImg); //IplImage -> Mat
  13. Mat detect = tmp.clone();
  14. cvReleaseImage(&skinImg);
  15. cvReleaseImage(&maskImg);
  16. return detect;
  17. }

从效果图看来,背景多了很多白色的杂质,单从直观效果来看,貌似还不如我上面写的几个算法,难道跟场景有关系?当然,opencv自带的这个api在皮肤检测上确实相当不错的。

总结

今天花了将近7个小时才撸了这篇文章出来==!这篇文章对各大主流的皮肤检测算法做了个总结和实现。其实说白了,每个算法的思想都是大同小异的,都是根据总结出来的一些经验,设定皮肤颜色的范围,再将其过滤出来,不同的只是过滤的过程在不同的颜色空间下进行而已。我们可以根据自己的应用场景,适当地修改这些范围,以获得满意的结果。可以改善的方向就是,我们可以用合适的滤波器或者形态学处理一些噪声,来使得提取出来的皮肤更为干净。

完整代码可以访问我的github来获取~

OpenCV探索之路(二十七):皮肤检测技术的更多相关文章

  1. OpenCV颜色转换和皮肤检测

    本笔记重点记录OpenCV中的颜色转换和利用色彩空间的特性进行皮肤检测 颜色转换 实现原理 之所以要引入色调/饱和度/亮度的色彩空间概念,是因为人们喜欢凭直觉分辨各种颜色,而它与这种方式吻合.实际上, ...

  2. OpenCV探索之路(十七):Mat和IplImage访问每个像素的方法总结

    在opencv的编程中,遍历访问图像元素是经常遇到的操作,掌握其方法非常重要,无论是Mat类的像素访问,还是IplImage结构体的访问的方法,都必须扎实掌握,毕竟,图像处理本质上就是对像素的各种操作 ...

  3. OpenCV探索之路(十七):Mat和IplImage访问像素的方法总结

    在opencv的编程中,遍历访问图像元素是经常遇到的操作,掌握其方法非常重要,无论是Mat类的像素访问,还是IplImage结构体的访问的方法,都必须扎实掌握,毕竟,图像处理本质上就是对像素的各种操作 ...

  4. OpenCV探索之路(二十四)图像拼接和图像融合技术

    图像拼接在实际的应用场景很广,比如无人机航拍,遥感图像等等,图像拼接是进一步做图像理解基础步骤,拼接效果的好坏直接影响接下来的工作,所以一个好的图像拼接算法非常重要. 再举一个身边的例子吧,你用你的手 ...

  5. OpenCV探索之路(十六):图像矫正技术深入探讨

    刚进入实验室导师就交给我一个任务,就是让我设计算法给图像进行矫正.哎呀,我不太会图像这块啊,不过还是接下来了,硬着头皮开干吧! 那什么是图像的矫正呢?举个例子就好明白了. 我的好朋友小明给我拍了这几张 ...

  6. WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模拟程序)

    原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于HTTP-GET的实现](提供模拟程序) 基于HTTP-GET的元数据发布方式与基于WS-MEX原理类似,但是ServiceMetad ...

  7. WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于WS-MEX的实现](提供模拟程序)

    原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[基于WS-MEX的实现](提供模拟程序) 通过<如何将一个服务发布成WSDL[编程篇]>的介绍我们知道了如何可以通过编程或者配 ...

  8. WCF技术剖析之二十七: 如何将一个服务发布成WSDL[编程篇]

    原文:WCF技术剖析之二十七: 如何将一个服务发布成WSDL[编程篇] 对于WCF服务端元数据架构体系来说,通过MetadataExporter将服务的终结点导出成MetadataSet(参考< ...

  9. 基于深度学习的病毒检测技术无需沙箱环境,直接将样本文件转换为二维图片,进而应用改造后的卷积神经网络 Inception V4 进行训练和检测

    话题 3: 基于深度学习的二进制恶意样本检测 分享主题:全球正在经历一场由科技驱动的数字化转型,传统技术已经不能适应病毒数量飞速增长的发展态势.而基于沙箱的检测方案无法满足 APT 攻击的检测需求,也 ...

随机推荐

  1. IDL 使用数组

    1.下标方式 IDL> array=indgen(8) IDL> print,array 0 1 2 3 4 5 6 7 IDL> print,array[3] IDL> ar ...

  2. 浅谈script标签中的async和defer

    script标签用于加载脚本与执行脚本,在前端开发中可以说是非常重要的标签了.直接使用script脚本的话,html会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的DOM渲染 ...

  3. 解决网络通信中外网和内网之间的通信问题(NAT转换)

    本文原址 http://www.cnblogs.com/lidabo/p/3828846.html 在网络编码中会发现程序在局域网中是可以适用的,但是在外网与内网之间和内网与内网之间就不可行.问题就在 ...

  4. Spring IOC(四)总结

    目录 1.spring容器中Bean生命周期 2.IOC容器中核心接口 3.IOC容器启动流程 4.IOC依赖注入流程 =============正文分割线================== Spr ...

  5. 容器与Docker简介(三)Docker相关术语——微软微服务电子书翻译系列

    本节列出了在更加深入Docker之前应该熟悉的术语和定义. 有关详细的定义,请参阅Docker提供的术语表. 容器镜像(Container image):具有创建容器所需要的所有依赖和信息的包. 镜像 ...

  6. Windows搭建wnmp

    1. 下载安装nginx: nginx官网下载地址:http://nginx.org/en/download.html 下载任一版本(我下载的是stable1.12.1版本)解压到D:\wnmp\ng ...

  7. LeetCode 53. Maximum Subarray(最大的子数组)

    Find the contiguous subarray within an array (containing at least one number) which has the largest ...

  8. 实现ajax的步骤

    实现ajax的步骤 1.创建xmlHttprequest对象 function createXmlHttpRequest(){ var xmlHttp; try{ //Firefox, Opera 8 ...

  9. angular-utils-ui-breadcrumbs使用心得

    angular-utils-ui-breadcrumbs是一个用来自动生成面包屑导航栏的一个插件,需要依赖angular.UIRouter和bootstrap3.css.生成的界面截图如下,点击相应的 ...

  10. POJ 3468 A Simple Problem with Integers(树状数组区间更新)

    A Simple Problem with Integers Time Limit: 5000MS   Memory Limit: 131072K Total Submissions: 97217   ...