深入理解OpenCV:实用计算机视觉项目解析(原书第3版)
上QQ阅读APP看书,第一时间看更新

1.3 皮肤变色器的实现

无须去检测皮肤颜色和所在的区域,只需使用OpenCV的floodFill()函数即可,该函数类似于大多数图像处理软件中的颜料桶工具。我们知道屏幕中间区域就是皮肤像素(因为要求用户将他们的脸放在中间)。为了将整个人脸变成绿色皮肤,只需对图像中心位置的像素进行绿色漫水填充,这样做总能让人脸的某些部分变成绿色。实际上,颜色、饱和度和亮度在脸部的不同部位可能会有所不同,这会使漫水填充很少覆盖面部的所有像素,除非将其阈值设为很低以至于覆盖人脸以外的不想要的像素。为了解决这个问题,可在图像中心区域不采用单个的漫水填充,而是对人脸区域中6个不同的皮肤像素点进行漫水填充。

OpenCV的floodFill()函数有一个很好的特性,它会将漫水填充的效果绘制到新的图像中而不(直接)修改输入图像。这一特性可得到一幅掩码图像,该图像用来调整皮肤像素的颜色而不必改变亮度和饱和度,产生比所有皮肤都为绿色像素的图像(这会导致重要脸部细节丢失)更为逼真的图像。

在RGB颜色空间改变皮肤颜色效果并不好,因为改变皮肤颜色需要改变脸部图像的亮度,但皮肤颜色不允许变化太大,而RGB无法从色彩中分离亮度。解决该问题的方法之一是采用HSV颜色空间,因为它能将亮度与色彩(色调)以及色度(饱和度)分开。不幸的是,HSV将色调值包裹在红色周围,又因为皮肤大多是红色的,这就意味着需要同时使用<10%和>90%的色调值,因为在这里它们都是红色。所以,我们可使用Y’CrCb颜色空间(OpenCV中的YUV空间的变种)来解决此问题。Y’CrCb颜色空间不仅能将颜色和亮度分开,而且对于通常的皮肤颜色,其取值只有一种。注意,实际上对于大多数相机而言,其图像和视频在转换成RGB前都使用某种YUV类型作为颜色空间,所以在很多情形下可直接得到YUV图像,无须自己转换。

由于我们想获得像卡通画一样的外星人模式,可对图像进行卡通化之后再用外星人滤波器。换言之,可使用由双边滤波器产生的缩小颜色图像和全尺寸的边缘掩码。皮肤检测通常在低分辨率下工作较好,因为它与分析高分辨率像素的近邻的平均值等价(或者可看作是用低频信号代替高频噪声信号)。接下来的工作是在对图像进行缩放后进行的,其缩放大小与用双边滤波器处理图像时一样(即只取图像一半的宽度和高度)。下面先将绘画图像转换为YUV:

我们还需要缩小边缘掩码,使其与绘画图像的比例相同。当存储一幅单独的掩码图像时,若用OpenCV的floodFill()函数会遇到困难,即掩码图像应在整个图像周围用1个像素作边界,若输入图像大小为W×H个像素,则单独的掩码图像大小为(W运算符2)×(H运算符2)个像素。但floodFill()函数也允许初始化边缘掩码来确保漫水填充算法不会越界。使用这一特性,可防止漫水填充的区域扩展到人脸外面。因此,需要提供两幅掩码图像:一幅的边缘掩码大小为W×H,另一幅则因为包含了图像的边界,所以大小为(W运算符2)×(H运算符2),边缘掩码相同。可让多个cv::Mat对象(或头部)引用同一数据,或者甚至可让一个cv::Mat对象引用另一个cv::Mat图像的区域。因此,无须分配两个分离的图像,然后复制边缘掩码给它们,只需分配一个包含边界的掩码图像并创建一个额外的大小为W×H的cv::Mat头部(这仅是在没有边界的情况下引用漫水填充掩码中的感兴趣区域)。换句话说,仅仅只有一个(W运算符2)×(H运算符2)大小的像素数组,但有两个cv::Mat对象,其中一个引用整个(W运算符2)×(H运算符2)大小的图像,另一个引用图像中间大小为W×H的区域:

整个边缘掩码有强边缘也有弱边缘(如下图的左图所示),但我们只想要强边缘,所以将采用二值化阈值法来过滤(效果见下图的中图)。为了在边缘之间加入一些间隙,我们将结合形态算子dilate()和erode()来消除一些间隙(也称为关闭算子),结果见下图的右图:

我们可以在下图中看到应用阈值化和形态算子的结果,第一幅图是输入边缘图,第二幅是经过阈值化滤波器得到的图像,最后一幅是经过膨胀和侵蚀形态滤波器的图像:

如前所述,我们希望对脸部周围的许多像素点使用漫水填充算法,从而确保包含了整个人脸图像的各种颜色和色调。我们选择鼻子、脸颊和前额周围的六个点,如下面屏幕截图的左侧所示。注意,这些值取决于前面绘制的脸部轮廓:

现在,仅需要为漫水填充找到一些好的下界和上界。注意,漫水填充算法基于Y’CrCb颜色空间,因此基本上可以决定亮度、红色分量、蓝色分量变化多少。我们希望允许包括阴影、高亮、反射在内的亮度变化较大,但不希望颜色变化很大:

调用floodFill()函数时,除了存储外部掩码需要指定参数FLOODFILL_MASK_ONLY外,其他参数默认:

下图左边有六个漫水填充的位置(见小圆圈),右边图像显示生成的外部掩码,其中皮肤为灰色,边缘为白色。注意右图已针对本书进行了修改,以便使皮肤像素(值为1)清晰可见:

图像变量mask(上面的右图为它对应的图像)包含以下的值:

●值为255的边缘像素

●值为1的皮肤像素

●其余值为0的像素

其间,变量edgeMask仅包含边缘像素(值为255)。因此为了得到皮肤像素,可从变量中移除边缘:

变量mask现在仅包含值为1的皮肤像素和值为0的非皮肤像素。为了改变原图的皮肤颜色和亮度,可使用带有皮肤掩码的cv::add()函数来增加原始BGR图像中的绿色成分:

下图展示了左边的原图和右边最终的外星人卡通图,其中脸部至少有六个部分是绿色的了!

请注意,我们已经使皮肤看起来更绿,但也更亮(看起来像一个在黑暗中发光的外星人)。如果你只想改变肤色而不使它更亮,可以使用其他颜色更改方法,例如将绿色分量加到70,同时红色和蓝色分量减少70,或者使用cvtColor(src,dst,"CV_BGR2HSV_FULL")将图像转换成HSV颜色空间,并调整色度和饱和度。

降低素描图像的随机椒盐噪声

大多数智能手机的微型相机、树莓派相机模块和一些摄像头都有明显的图像噪声。这通常可以接受,但对于5×5的Laplacian边缘滤波器则影响很大。边缘掩码(素描模式显示)经常会有很多黑色小斑点,它们被称为椒盐噪声,由白色背景上相邻的几个黑色像素组成。我们已经在使用中值滤波器了,通常强度足以去除椒盐噪声,但现有情况下则可能不够强。因为边缘掩码大多为带着一些黑色边缘(值为0)的纯白色背景(值为255)和噪点(值也为0)。我们可使用标定的闭形态算子,但那会消除很多边缘。因此,本项目采用一个自定义滤波器,用于删除被白色像素完全包围的小黑色区域像素。这样可以消除很多噪声,同时对实际的边缘影响不大。

我们将扫描图像中的黑色像素,在每个黑色像素处,我们将检查它周围5×5正方形区域的边界像素是否为白色,如果它们都为白色,则说明有一个黑色噪声小岛,可用白色像素来填充整个块以去除黑色小岛。为了简化5×5滤波器,可忽略掉图像周围边框的两个像素,并保持其原样。

下图左边的原始图像来自Android平板电脑,中间图像为素描模式,带有椒盐噪声的小黑点,右图显示了使用上述方法删除椒盐噪声的结果,皮肤看起来干净了很多:

为方便起见,我们封装了名为removePepperNoise()的函数来处理图像,其代码如下:

就这样了!反复测试你的应用程序,直到准备好移植到嵌入式设备!