附加题part4:图像预处理(肤色检测及二值化处理)
tis1:该部分属于图像预处理第三、四步,关于图像预处理的完整内容请跳转至part3;同时再次鸣谢基于OpenCV的手势识别完整项目(Python3.7),本文基于其做一些补充
tis2:该部分代码整体逻辑框架是一致的,后续不再赘述
- 颜色空间转换
- 通过cv2.split()函数将图像拆分为多个通道
- 设置掩膜数组(可能是初始化,也可能直接用区间函数设值)
- 图像与运算:对色深进行判断,并将肤色部分提取出来
一、RGB颜色空间
1、基本理论学习
- 以下是在论文中的原文内容
附上原文链接:Human Skin Colour Clustering for Face Detection
- 总结一下就是:
在均匀光照下满足(1)
在侧向照明下满足(2)
- 可以看到共同的条件是R > G和R > B
所以写成代码就是:
if (R > G) and (R > B):
if (R > 95) and (G > 40) and (B > 20) and (max(R,G,B) - min(R,G,B) > 15) and (abs(R - G) > 15):
skin = 1
# print 'Condition 1 satisfied!'
elif (R > 220) and (G > 210) and (B > 170) and (abs(R - G) < 15):
skin = 1
# print 'Condition 2 satisfied!'
2、完整代码书写
def skinMask_RGB(roi):
rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
(R, G, B) = cv2.split(rgb)
skin = np.zeros(R.shape, dtype=np.uint8)
(x, y) = R.shape # 获取像素点坐标范围
for i in range(0, x):
for j in range(0, y):
if (R[i][j] > G[i][j]) and (R[i][j] > B[i][j]):
if (abs(R[i][j] - G[i][j]) > 15) and (R[i][j] > 95) and (G[i][j] > 40) and (B[i][j] > 20) and (max(R[i][j], G[i][j], B[i][j]) - min(R[i][j], G[i][j], B[i][j]) > 15):
skin[i][j] = 255
elif (abs(R[i][j] - G[i][j]) <= 15) and (R[i][j] > 220) and (G[i][j] > 210) and (B[i][j] > 170):
skin[i][j] = 255
res = cv2.bitwise_and(roi, roi, mask=skin)
return res
3、一个小提醒
skin = np.zeros(R.shape, dtype=np.uint8) #正确
skin = np.zeros(R.shape) # 错误(会报错)
- 可以试着把掩膜图像和原图像print出来,之后你就会发现两种图像的数字类型并不同
- 所以这里可不能偷懒哦,必须加类型转换dtype=np.uint8
4、效果呈现
二、HSV颜色空间
1、基本理论学习
(1)判断条件:0<=H<=20,S>=48,V>=50
- 是不是很简单?
- 博主没有在网上找到这组数据的原出处,但很多人都在用…
(2)openCV范围函数:cv2.inRange()
- 一个很有意思的函数,很适合这种判断条件单一的掩膜操作
- 能同时判断出三个通道(是不是很省事)
- 举个例子:
- 低于lower_red的值或高于upper_red的值,图像值变为0
- 在lower_red~upper_red之间的值变成255(三通道同时满足)
hsv = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2HSV)
lower_red = np.array([20, 20, 20])
upper_red = np.array([200, 200, 200])
mask = cv2.inRange(hsv, lower_red, upper_red)
2、代码书写
def skinMask_HSV(roi):
low = np.array([0, 48, 50]) # 最低阈值
high = np.array([20, 255, 255]) # 最高阈值
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, low, high)
res = cv2.bitwise_and(roi, roi, mask=mask)
return res
3、效果呈现
三、椭圆肤色检测模型
1、YUV,YCrCb颜色空间
(1)什么是YUV颜色空间
- YUV是北美NTSC系统和欧洲PAL系统中模拟电视信号编码的基础
包含:一个亮度信号Y,两个色度信号U和V - 核心原理:
- 使用RGB的信息,从全彩色图像中产生一个黑白图像
- 提取出三个主要的颜色变成两个额外的信号来描述颜色
- 把这三个信号组合回来就可以产生一个全彩色图像
- 两个色度信号对应的颜色
- RGB与YUV间的转换
(2)什么是YCrCb颜色空间
- 简单点说就是YUV的变种,其在数字电视和图像压缩(比如JPEG)方面都有应用
- 对照YUV中的Y,YCrCb中的Y也表示亮度值
Cr为图像红色浓度偏移量,Cb为蓝色浓度偏移量
上图中四部分依次- 原图
- 只有Y成分的图(基本等同于灰度图,体现亮暗程度)
- 只有Cb成分的图
- 只有Cr成分的图
- Cr和Cb对应坐标对应颜色见下
- RGB与YCrCb间的转换
2、利用椭圆检测的原理
-
下图展示的是肤色样本点从RGB转化到YCbCr空间并且在CbCr平面进行投影的结果,可以看到其大部分在一个椭圆区间内
-
这个模型博主也没有找到原出处…但确实其应用很广泛
-
包装到函数里为(这里连参数都不用改了,直接用都行)
cv2.ellipse(skinCrCbHist, (113,155),(23,25), 43, 0, 360, (255,255,255), -1)
注意:这里的最后一个参数-1,是将绘出图形内部进行填充(这里相当于对椭圆内部填充为白色)
3、代码书写
def elliptic(roi):
CrCb_standard = np.zeros((256, 256), dtype=np.uint8)
cv2.ellipse(CrCb_standard, (113, 155), (23, 25), 43, 0, 360, (255, 255, 255), -1)
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB)
(y, Cr, Cb) = cv2.split(YCrCb)
skin = np.zeros(y.shape, dtype=np.uint8)
(m, n) = skin.shape
for i in range(0, m):
for j in range(0, n):
if CrCb_standard[Cr[i][j], Cb[i][j]] > 0:
skin[i][j] = 255
res = cv2.bitwise_and(roi, roi, mask=skin)
return res
4、效果呈现
四、YCrCb颜色空间的Cr分量+Otsu法阈值分割算法
def Cr_otsu(roi):
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB)
(y, Cr, Cb) = cv2.split(YCrCb)
Cr1 = cv2.GaussianBlur(Cr, (5, 5), 0) # 高斯滤波
_, skin = cv2.threshold(Cr1, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
res = cv2.bitwise_and(roi, roi, mask=skin)
return res
1、什么是Otsu法
-
最大类间方差法,一种确定阈值的算法
-
原理:主要是通过阈值进行前后背景分割(按图像的灰度特性,将图像分成背景和前景两部分)
-
读者可以通过Otsu算法——最大类间方差法(大津算法)详细了解算法内容(很好理解的)
-
缺点:对噪声和目标大小十分敏感,目标与背景的灰度有较大的重叠时,效果不不是很理想
2、代码注解
(1)一个注意点
- 该算法只对CR通道单独进行Otsu处理!
(2)cv2.threshold ()函数注解
-
原型:cv2.threshold (src, thresh, maxval, type)
一个用于图像二值化的函数(直接构造好掩膜) -
函数参数:
-
src:源图像矩阵(单通道,8位或32位浮点数据)
-
thresh:阈值,取值范围0~255
代码中设置的为0,是因为在cv2.THRESH_OTSU之下有自适应阈值,所以前面不管设多少都会被自动忽略 -
maxval:可理解为填充色,取值范围0~255
代码中设置的为255,就是将超过阈值的部分设为白色(就是在构造掩膜) -
type:阈值类型
选项 像素值>thresh 其他情况 cv2.THRESH_BINARY maxval 0 cv2.THRESH_BINARY_INV 0 maxval cv2.THRESH_TRUNC thresh 当前灰度值 cv2.THRESH_TOZERO 当前灰度值 0 cv2.THRESH_TOZERO_INV 0 当前灰度值 其它取值:
- cv2.THRESH_OTSU:使用最小二乘法处理像素点
- cv2.THRESH_TRIANGLE:使用三角算法处理像素点
-
-
我们的代码是cv2.THRESH_BINARY + cv2.THRESH_OTSU,即使用Otsu自适应阈值化算法
-
注意:函数返回值有两个!!!
- ret:即我们设置的阈值(自适应阈值会自动返回的)
- dst:二值化后的像素矩阵,与原像素矩阵同规格
所以我们前面需要一个占位将值传出,否则就会出现错误(_用于占位)
3、一个小问题(希望你没遇见)
-
博主的运行结果是一团黑(没有正常分离)…
-
首先不应该是代码的问题,因为它能准确地把矿泉水瓶标签的红白色前后景分离
难道说是我拿手检测时手和背景间的色差太小了?!!然后他把我的手当成了背景之一?(这个想法不是空穴来风,寝室里的床板颜色和手的颜色真的很接近)
-
博主认为应该是手和背景间的色差太小导致的(博主的背景色大多是寝室床板的颜色,与手的颜色很接近)
-
于是乎博主尝试着把背景变得“干净”一点,比如背景改成白色,就像这样:
但是结果还是不尽如人意。。。 -
这里博主最终没有想明白原因,希望成功的uu能给博主一些指导(。◕ˇ∀ˇ◕)
五、Cr,Cb范围筛选法
1、判断条件:133<=Cr<=173 77<=Cb<=127
- 所以这里有两种写法,即:不等式和使用cv2.inRange()函数
2、代码书写
# 方法一:不等式
def Cr_Cb_1(roi):
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB)
(y, Cr, Cb) = cv2.split(YCrCb)
skin = np.zeros(Cb.shape, dtype=np.uint8)
(x, y) = Cr.shape
for i in range(0, x):
for j in range(0, y):
if(Cr[i][j] > 133) and (Cr[i][j] < 173) and (Cb[i][j] > 77) and (Cb[i][j] < 127):
skin[i][j] = 255
res = cv2.bitwise_and(roi, roi, mask=skin)
return res
# 方法二:使用cv2.inRange()函数
def Cr_Cb_2(roi):
low = np.array([0, 130, 77])
high = np.array([255, 175, 127])
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB)
mask = cv2.inRange(YCrCb, low, high)
res = cv2.bitwise_and(roi, roi, mask=mask)
return res
3、效果展示
六、OpenCV自带AdaptiveSkinDetector
1、相关内容学习
- 网络上关于其原理的解释有很多,博主总结一下:
- 特点:基于HSV,把皮肤阈值分割和运动检测相结合
- 流程:
- 具体内容可以参照基础学习笔记之opencv(17):皮肤检测类CvAdaptiveSkinDetector的使用学习
2、代码书写
- 以下代码并没有在博主的电脑上跑起来,只是逻辑上应该这样写,请慎重取用
import cv2
# 加载图像
image = cv2.imread('your_image.jpg')
# 创建AdaptiveSkinDetector对象
detector = cv2.ximgproc.createAdaptiveSkinDetector()
# 调整参数
detector.set("minArea", 1000) # 设置最小面积
detector.set("maxArea", 20000) # 设置最大面积
# 获得肤色掩码
skin_mask = detector.detect(image)
# 显示原始图像和肤色掩码
cv2.imshow('Original Image', image)
cv2.imshow('Skin Mask', skin_mask)
# 等待按下任意按键退出
cv2.waitKey(0)
cv2.destroyAllWindows()
- 设置最小面积:可以过滤掉噪声或不相关的区域(排除小面积区域)
设置最大面积:过滤背景或不相关的物体(排除大面积区域)
3、为啥跑不了?
-
核心原因:博主的cv2库中没有createAdaptiveSkinDetector()这个函数
-
我看到网上也有其他博主也遇到了这个问题,他的解释说可能是因为openCV的版本问题(而且他也没解决)
-
但博主的openCV是最新版本,所以很奇怪
4、当然也有好消息
- 网上利用其它语言实现后得出的结论是:该模型考虑了很多背景因素进来而导致效果很不好,所以也不会用它做处理
- 网上的结果都是类似于这样的
七、对上述六种处理方法的看法
1、在我的背景下
- 个人感觉RGB对背景的过滤程度是最好的
椭圆模型和CrCb阈值的两个方法对背景处理得都不太好 - 代码部分最简单的当属CrCb阈值法和HSV阈值法,运行速度很快
- 综合下来,结合代码和处理结果,我觉得RGB效果最好
2、结合其它博主的结论
- 其他博主认为HSV和RGB的效果不如YCrCb空间下的运行结果
- YCrCb是一个单独把亮度分离开来的颜色模型->肤色不会受到光线亮度而发生改变
- 而HSV和RGB没有单独分离亮度,在曝光条件下效果会更差
- 当然你的结论也可能与我的不同
3、对不同结论的解释
-
我认为结论不同的原因在于:拍摄背景
- 我的背景相较博主的背景更“苛刻”:背景中的大部分都与手的颜色相似
- 而参考文档里博主的背景则是更接近于黑/白色
-
HSV和RGB的三个参数都用于表示色彩
∴能反映的色彩特性更加精确
∴更能有效地将与手颜色相似的背景色剔除当然这也可以解释为啥RGB提取出的手不完整
因为手的边缘可能因为光照偏白或偏黑,而RGB对颜色极度敏感,故容易导致误判 -
YCrCb只有两个参数表示颜色
∴一定程度上降低了色彩精确度
∴在背景色接近于手的颜色时效果很差
3、总结一下
- 在背景接近于手的颜色时,HSV和RGB效果更好(更能有效剔除背景)
在背景和手掌颜色差距较大时,YCrCb效果更好(更能完整提取出手的形态) - 带着这套理论在纯白背景拍了一张
可以看到HSV和RGB确实把手的边缘吞了很多,相比之下YCrCb保留了完整的手部信息,更有利于后续处理 - 因此,实际选取情况应当依据不同场景而定(背景颜色与手的颜色的相似度),并没有哪种方法是通用的
看到这儿了?现在该跳回part3了哦ヾ(๑╹◡╹)ノ"
特别声明:以上的图片部分来自于网络,感谢CSDN、知乎等平台上各位博主的分享,本文用作交流学习予以引用,在此一并表示感谢!