附加题part4:图像预处理(肤色检测及二值化处理)

附加题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、效果呈现

RGB35545afc7da9a519.png

二、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_BINARYmaxval0
      cv2.THRESH_BINARY_INV0maxval
      cv2.THRESH_TRUNCthresh当前灰度值
      cv2.THRESH_TOZERO当前灰度值0
      cv2.THRESH_TOZERO_INV0当前灰度值

      其它取值:

      • cv2.THRESH_OTSU:使用最小二乘法处理像素点
      • cv2.THRESH_TRIANGLE:使用三角算法处理像素点
  • 我们的代码是cv2.THRESH_BINARY + cv2.THRESH_OTSU,即使用Otsu自适应阈值化算法

  • 注意:函数返回值有两个!!!

    • ret:即我们设置的阈值(自适应阈值会自动返回的)
    • dst:二值化后的像素矩阵,与原像素矩阵同规格
      所以我们前面需要一个占位将值传出,否则就会出现错误(_用于占位)

3、一个小问题(希望你没遇见)

  • 博主的运行结果是一团黑(没有正常分离)…

  • 首先不应该是代码的问题,因为它能准确地把矿泉水瓶标签的红白色前后景分离
    73a6927f90a6fa3c236ef96a1648a391.png
    难道说是我拿手检测时手和背景间的色差太小了?!!然后他把我的手当成了背景之一?

    (这个想法不是空穴来风,寝室里的床板颜色和手的颜色真的很接近)

  • 博主认为应该是手和背景间的色差太小导致的(博主的背景色大多是寝室床板的颜色,与手的颜色很接近)

  • 于是乎博主尝试着把背景变得“干净”一点,比如背景改成白色,就像这样:
    c1dc90f99f0ee24b36b10886bea7032c.png
    但是结果还是不尽如人意。。。

  • 这里博主最终没有想明白原因,希望成功的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、效果展示

fbf479688b7bdd30d9067681a935bbba.png

六、OpenCV自带AdaptiveSkinDetector

1、相关内容学习

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、当然也有好消息

  • 网上利用其它语言实现后得出的结论是:该模型考虑了很多背景因素进来而导致效果很不好,所以也不会用它做处理
  • 网上的结果都是类似于这样的
    9c078483ec719242e03c30e0aea60c3a.png

七、对上述六种处理方法的看法

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、知乎等平台上各位博主的分享,本文用作交流学习予以引用,在此一并表示感谢!

  • 32
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值