毕设的题目是手势识别,刚开始接触计算机视觉这方面,感谢CSDN和GitHub上的大佬,网上类似项目很多,方法也有很多,自己顺带进行了整理,边做毕设边分享一下自己学习心得吧,也算是梳理一下所学知识,各大佬有什么好的建议还请指出,不吝赐教。后期会把完整的项目上传。
参考资料:
https://blog.csdn.net/ifruoxi/article/details/78091954(获取手势,基于Python)
https://blog.csdn.net/qq_22527639/article/details/81501565(肤色检测:方法全,理论介绍的也很全面,基于C++)
https://blog.csdn.net/shadow_guo/article/details/43602051(基于RGB空间肤色检测,基于Python)
https://blog.csdn.net/weixin_40893939/article/details/84527037(基于HSV空间和YCrCb空间肤色检测,基于Python)
https://blog.csdn.net/Eastmount/article/details/83581277(腐蚀膨胀理论介绍,基于Python)
https://blog.csdn.net/dz4543/article/details/80655067(轮廓提取,基于Python)
环境:Win10 + Python3.7 + OpenCV3.4.5
各个库的安装就不多说了,其他话也不多说,开始正题。
1.获取手势
主要是调用OpenCV,创建main.py和 picture.py
main.py 当前负责录像,picture负责处理图像
main.py
import cv2
import picture as pic
font = cv2.FONT_HERSHEY_SIMPLEX #设置字体
size = 0.5 #设置大小
width, height = 300, 300 #设置拍摄窗口大小
x0,y0 = 300, 100 #设置选取位置
cap = cv2.VideoCapture(0) #开摄像头
if __name__ == "__main__":
while(1):
ret, frame = cap.read() #读取摄像头的内容
frame = cv2.flip(frame, 2)
roi = pic.binaryMask(frame, x0, y0, width, height) #取手势所在框图并进行处理
key = cv2.waitKey(1) & 0xFF#按键判断并进行一定的调整
#按'j''l''u''j'分别将选框左移,右移,上移,下移
#按'q'键退出录像
if key == ord('i'):
y0 += 5
elif key == ord('k'):
y0 -= 5
elif key == ord('l'):
x0 += 5
elif key == ord('j'):
x0 -= 5
if key == ord('q'):
break
cv2.imshow('frame', frame) #播放摄像头的内容
cap.release()
cv2.destroyAllWindows() #关闭所有窗口
2.图像预处理
预处理在picture.py中完成。
预处理的主要步骤为:去噪 -> 肤色检测 -> 二值化 -> 形态学处理 -> 轮廓提取,其中最麻烦的两项为肤色检测和轮廓提取。
2.1去噪
即滤波,主要是为了实现对图像噪声的消除,增强图像的效果,其实个人感觉这里滤波的作用不是很明显,也可以选择不滤波,在肤色检测后会有二次滤波。
#以3*3的模板进行均值滤波
blur = cv2.blur(roi, (3,3))
#以3*3的模板进行高斯滤波,最后一个参数表示x与y方向的标准差,给0的话,函数会自己运算
blur = cv2.GaussianBlur(roi, (3,3), 0)
#中值滤波
blur = cv2.medianBlur(roi,5)
#双边滤波,9为区域的直径,后面两个参数是空间高斯函数标准差和灰度值相似性高斯函数标准差
blur = cv2.bilateralFilter(img,9,75,75)
均值滤波器、高斯滤波器、中值滤波器、双边滤波器都可以进行使用。推荐使用双边滤波器,该滤波器考虑了图像的空间关系,也考虑图像的灰度关系。双边滤波同时使用了空间高斯权重和灰度相似性高斯权重,确保了边界不会被模糊掉。不过我在处理中直接省去了去噪这个过程。
2.2 肤色检测 + 二值化处理
picture.py
方法一:基于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
import cv2
import numpy as np
def binaryMask(frame, x0, y0, width, height):
cv2.rectangle(frame,(x0,y0),(x0+width, y0+height),(0,255,0)) #画出截取的手势框图
roi = frame[y0:y0+height, x0:x0+width] #获取手势框图
cv2.imshow("roi", roi) #显示手势框图
res = skinMask(roi) #进行肤色检测
cv2.imshow("res", res) #显示肤色检测后的图像
return res
##########方法一###################
##########BGR空间的手势识别#########
def skinMask(roi):
rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) #转换到RGB空间
(R,G,B) = cv2.split(rgb) #获取图像每个像素点的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):
#判断条件,不在肤色范围内则将掩膜设为黑色,即255
if (abs(R[i][j] - G[i][j]) > 15) and (R[i][j] > G[i][j]) and (R[i][j] > B[i][j]):
if (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 (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
效果图:
方法二:基于HSV颜色空间
判断条件:0<=H<=20,S>=48,V>=50
肤色检测的方式不同影响的是skinMask,之后的代码只是修改skinMask函数,picture.py中其他代码不需要改动。
##########方法二###################
########HSV颜色空间H范围筛选法######
def skinMask(roi):
low = np.array([0, 48, 50]) #最低阈值
high = np.array([20, 255, 255]) #最高阈值
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) #转换到HSV空间
mask = cv2.inRange(hsv,low,high) #掩膜,不在范围内的设为255
res = cv2.bitwise_and(roi,roi, mask = mask) #图像与运算
return res
效果图:
方法三:椭圆肤色检测模型
在YCrCb空间,肤色像素点会聚集到一个椭圆区域。先定义一个椭圆模型,然后将每个RGB像素点转换到YCrCb空间比对是否在椭圆区域,是的话判断为皮肤。
##########方法三###################
#########椭圆肤色检测模型##########
def skinMask(roi):
skinCrCbHist = np.zeros((256,256), dtype= np.uint8)
cv2.ellipse(skinCrCbHist, (113,155),(23,25), 43, 0, 360, (255,255,255), -1) #绘制椭圆弧线
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB) #转换至YCrCb空间
(y,Cr,Cb) = cv2.split(YCrCb) #拆分出Y,Cr,Cb值
skin = np.zeros(Cr.shape, dtype = np.uint8) #掩膜
(x,y) = Cr.shape
for i in range(0, x):
for j in range(0, y):
if skinCrCbHist [Cr[i][j], Cb[i][j]] > 0: #若不在椭圆区间中
skin[i][j] = 255
res = cv2.bitwise_and(roi,roi, mask = skin)
return res
效果图:
方法四:YCrCb颜色空间的Cr分量+Otsu法阈值分割算法
针对YCrCb中Cr分量的处理,对CR通道单独进行Otsu处理,Otsu方法opencv里用threshold,Otsu算法是对图像的灰度级进行聚类。
################方法四####################
####YCrCb颜色空间的Cr分量+Otsu法阈值分割算法
def skinMask(roi):
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB) #转换至YCrCb空间
(y,cr,cb) = cv2.split(YCrCb) #拆分出Y,Cr,Cb值
cr1 = cv2.GaussianBlur(cr, (5,5), 0)
_, skin = cv2.threshold(cr1, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) #Ostu处理
res = cv2.bitwise_and(roi,roi, mask = skin)
return res
效果图:
方法五:Cr,Cb范围筛选法
该方法与方法一、二类似,不同的只是颜色空间不相同
判断条件:133<=Cr<=173 77<=Cb<=127
##########方法五###################
########Cr,Cb范围筛选法###########
def skinMask(roi):
YCrCb = cv2.cvtColor(roi, cv2.COLOR_BGR2YCR_CB) #转换至YCrCb空间
(y,cr,cb) = cv2.split(YCrCb) #拆分出Y,Cr,Cb值
skin = np.zeros(cr.shape, dtype = np.uint8)
(x,y) = cr.shape
for i in range(0, x):
for j in range(0, y):
#每个像素点进行判断
if(cr[i][j] > 130) and (cr[i][j] < 175) and (cb[i][j] > 77) and (cb[i][j] < 127):
skin[i][j] = 255
res = cv2.bitwise_and(roi,roi, mask = skin)
return res
效果图:
方法六:OpenCV自带AdaptiveSkinDetector
关于该函数的使用可以参考http://www.cnblogs.com/tornadomeet/archive/2012/11/20/2778740.html
最终方案选择:在几种方式中选择效果比较好的,RGB和HSV的效果一般,而且曝光的话,效果更差,YCrCb是一个单独把亮度分离开来的颜色模型,使用这个颜色模型的话,像肤色不会受到光线亮度而发生改变,方法三和四均可。
2.3 形态学处理
即便是比较好的肤色检测算法,分割出来的手势,也难免有黑点,或者背景有白点,这时候需要对分割出来的手势图进行进一步处理,主要是腐蚀膨胀两个操作。
腐蚀和膨胀是针对白色部分(高亮部分而言)。从数学角度来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,称之为A)与核(称之为B)进行卷积。
膨胀就是求局部最大值操作,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素,这样就会使图像中的高亮区域逐渐增长。
腐蚀就是求局部最小值操作,即计算核B覆盖的区域的像素点的最小值,并把这个最小值赋值给参考点指定的像素,这样就会使图像中的高亮区域逐渐减少。
开运算:先腐蚀后膨胀,去除孤立的小点,毛刺
闭运算:先膨胀后腐蚀,填平小孔,弥合小裂缝
在binaryMask函数中return前面添加以下代码,进行开运算
kernel = np.ones((3,3), np.uint8) #设置卷积核
erosion = cv2.erode(res, kernel) #腐蚀操作
cv2.imshow("erosion",erosion)
dilation = cv2.dilate(erosion, kernel)#膨胀操作
cv2.imshow("dilation",dilation)
效果如图:
可以看到背景杂质点去掉了
2.4 轮廓提取
在binaryMask函数中return前面添加以下代码,对肤色检测后的图像提取手势区域
binaryimg = cv2.Canny(res, 50, 200) #二值化,canny检测
h = cv2.findContours(binaryimg,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE) #寻找轮廓
contours = h[1] #提取轮廓
ret = np.ones(res.shape, np.uint8) #创建黑色幕布
cv2.drawContours(ret,contours,-1,(255,255,255),1) #绘制白色轮廓
cv2.imshow("ret", ret)