OpenCV实例(二)手势识别
OpenCV实例(二)手势识别
作者:Xiou
1.手势识别概述
手势识别的范围很广泛,在不同场景下,有不同类型的手势需要识别,例如:
● 识别手势所表示的数值。
● 识别手势在特定游戏中的含义,如“石头、剪刀、布”等。
● 识别手势在游戏中表示的动作,如前进、跳跃、后退等。
● 识别特定手势的含义,如表示“OK”的手势、表示胜利的手势等。
理论基础
凸包和凸缺陷在图像处理中具有非常重要的意义,被广泛地用于图像识别等领域。
逼近多边形是轮廓的高度近似,但是有时候,我们希望使用一个多边形的凸包来简化它。凸包和逼近多边形很像,只不过凸包是物体最外层的凸多边形。凸包指的是完全包含原有轮廓,并且仅由轮廓上的点构成的多边形。凸包的每一处都是凸的,即连接凸包内任意两点的直线都在凸包内部。在凸包内,任意连续三个点构成的面向内部的角的角度都小于180°。
OpenCV提供的函数cv2.convexHull()用于获取轮廓的凸包,其语法格式为:
hull=cv2.convexHull(points[,clockwise[,returnPoints]])
其中,返回值hull为凸包角点。该函数中的参数如下:
● points表示轮廓。
● clockwise为布尔型值;在该值为True时,凸包角点按顺时针方向排列;在该值为False时,凸包角点按逆时针方向排列。
● returnPoints为布尔型值,默认值是True,此时,函数返回凸包角点的坐标值;当该参数为False时,函数返回轮廓中凸包角点的索引。
1.1.获取轮廓的凸包
代码实例:使用函数cv2.convexHull()获取轮廓的凸包。
# -*- coding: utf-8 -*-
import cv2
# --------------读取并绘制原始图像------------------
o = cv2.imread('hand.bmp')
cv2.imshow("original",o)
# --------------提取轮廓------------------
gray = cv2.cvtColor(o,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
# --------------寻找凸包,得到凸包的角点------------------
hull = cv2.convexHull(contours[0])
# --------------绘制凸包------------------
cv2.polylines(o, [hull], True, (0, 255, 0), 2)
# --------------显示凸包------------------
cv2.imshow("result",o)
cv2.waitKey()
cv2.destroyAllWindows()
输出结果:
1.2.凸缺陷
凸包与轮廓之间的部分称为凸缺陷。凸缺陷示意图如图8-4所示,图中的白色四角星是前景,显然,其边缘就是其轮廓,连接四个顶点构成的四边形是其凸包。
通常情况下,使用如下四个特征值来表示凸缺陷:
● 起点:该特征值用于说明当前凸缺陷的起点位置。需要注意的是,起点值用轮廓索引表示。也就是说,起点一定是轮廓中的一个点,并且用其在轮廓中的序号来表示。例如,图8-4中的点A是凸缺陷1的起点。
● 终点:该特征值用于说明当前凸缺陷的终点位置。该值也是使用轮廓索引表示的。
● 轮廓上距离凸包最远的点。例如,图8-4中的点C是凸缺陷1中的轮廓上距离凸包最远的点。
● 最远点到凸包的近似距离。例如,图8-4中的距离D是凸缺陷1中的最远点到凸包的近似距离。OpenCV提供了函数cv2.convexityDefects()用来获取凸缺陷,其语法格式如下:
convexityDefects=cv2.convexityDefects(contour,convexhull)
需要说明的是,返回结果中[起点,终点,轮廓上距离凸包最远的点,最远点到凸包的近似距离]的前三个值是轮廓点的索引,所以需要从轮廓点集中找它们。
上述函数的参数如下:
● contour是轮廓。
● convexhull是凸包。
值得注意的是,用函数cv2.convexityDefects()计算凸缺陷时,要使用凸包作为参数。在查找该凸包时,函数cv2.convexHull()所使用的参数returnPoints的值必须是False。
为了更直观地观察凸缺陷点集,尝试将凸缺陷点集在一幅图内显示出来。实现方式为,将起点和终点用一条线连接,在最远点处绘制一个圆点。下面通过一个例子来展示上述操作。
代码实例:使用函数cv2.convexityDefects()计算凸缺陷。
# -*- coding: utf-8 -*-
import cv2
#----------------原图--------------------------
img = cv2.imread('hand.bmp')
cv2.imshow('original',img)
#----------------构造轮廓--------------------------
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 127, 255,0)
contours, hierarchy = cv2.findContours(binary,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
#----------------凸包--------------------------
cnt = contours[0]
hull = cv2.convexHull(cnt,returnPoints = False)
defects = cv2.convexityDefects(cnt,hull)
print("defects=\n",defects)
#----------------构造凸缺陷--------------------------
for i in range(defects.shape[0]):
s,e,f,d = defects[i,0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
cv2.line(img,start,end,[0,0,255],2)
cv2.circle(img,far,5,[255,0,0],-1)
#----------------显示结果,释放图像--------------------------
cv2.imshow('result',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
输出结果:
1.3.凸缺陷占凸包面积比
当有0个凸缺陷时,手势既可能表示数值1,也可能表示数值0。因此,不能根据凸缺陷的个数判定此时的手势到底表示的是数值0还是数值1,需要寻找二者的其他区别。
代码实例:编写程序,利用表示数值0的手势和表示数值1的手势的凸缺陷面积差异,对二者进行识别。
# -*- coding: utf-8 -*-
import cv2
# 手势识别函数
def reg(x):
#=================找出轮廓===============
#查找所有轮廓
x=cv2.cvtColor(x,cv2.COLOR_BGR2GRAY)
contours,h = cv2.findContours(x,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#从所有轮廓中找到最大的,作为手势的轮廓
cnt = max(contours,key=lambda x:cv2.contourArea(x))
areacnt = cv2.contourArea(cnt) #获取轮廓面积
#===========获取轮廓的凸包=============
hull = cv2.convexHull(cnt) #获取轮廓的凸包,用于计算面积,返回坐标
areahull = cv2.contourArea(hull) #获取凸包的面积
#===========获取轮廓面积、凸包面积,二者的比值=============
arearatio = areacnt/areahull
#通常情况下,手势0,轮廓和凸包大致相等,该值大于0.9.
# 手势1,轮廓要比凸包小一些,该值小于等于0.9
# 需要注意,这个不是特定值,因人而异,有的人手指长,有的人手指短
# 所以,该值存在一定的差异
if arearatio>0.9: #轮廓面积/凸包面积>0.9,二者面积近似,识别为0
result='fist:0'
else:
result='finger:1' #对应:轮廓面积/凸包面积<=0.9,较大凸缺陷,识别为1
return result
# 读取两幅图像识别
x = cv2.imread('zero.jpg')
y = cv2.imread('one.jpg')
# 分别识别x和y
xtext=reg(x)
ytext=reg(y)
# 输出识别结果
org=(0,80)
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale=2
color=(0,0,255)
thickness=3
cv2.putText(x,xtext,org,font,fontScale,color,thickness)
cv2.putText(y,ytext,org,font,fontScale,color,thickness)
# 显示识别结果
cv2.imshow('zero',x)
cv2.imshow('one',y)
cv2.waitKey()
cv2.destroyAllWindows()
输出结果:
由图可知,程序能够准确地识别出表示数值0(fist:0)和表示数值1(finger:1)手势的图像。
2.手势识别过程
2.1.识别流程
手势识别基本流程图如图所示:
下面,对各个步骤进行程序介绍。
● Step 1:获取图像。本步骤的主要任务是读取摄像头、划定识别区域。划定识别区域的目的在于仅识别特定区域内的手势,简化识别过程。
● Step 2:识别皮肤。本步骤的主要任务是色彩空间转换、在新的色彩空间内根据颜色范围值识别出皮肤所在区域。
色彩空间转换的目的在于将图像从BGR色彩空间转换到HSV色彩空间,以进行皮肤检测。通过皮肤颜色的范围值确定手势所在区域。
● Step 3:图像预处理。图像预处理主要是为了去除图像内的噪声,以便后续处理。这里的图像预处理包含膨胀操作和高斯滤波
● Step 4:获取轮廓。本步骤的主要任务在于获取图像的轮廓信息,并获取其面积。
● Step 5:获取凸包。本步骤的主要任务是获取轮廓的凸包信息,并获取其面积。
● Step 6:计算轮廓和凸包的面积比。本步骤的主要任务是计算轮廓和凸包的面积比。
● Step 7:获取凸缺陷。本步骤的主要任务是获取手势的凸缺陷。
● Step 8:计算并绘制有效凸缺陷。本步骤的主要任务是计算有效凸缺陷的个数,并绘制凸包、凸缺陷的最远点。
● Step 9:使用凸缺陷识别手势。本步骤的主要任务是根据凸缺陷的个数、凸缺陷与凸包的面积比进行手势识别。本步骤先对凸缺陷的个数进行判断,然后根据凸缺陷的个数判定当前手势的形状。有一个特例是,当凸缺陷的个数为0时,需要再对轮廓与凸包面积比进行判断,才能决定具体手势。
● Step 10:显示结果。本步骤的主要任务是将识别结果显示出来。
代码实例:
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import math
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
#==============主程序======================
while(cap.isOpened()):
ret,frame = cap.read() # 读取摄像头图像
# print(frame.shape) #获取窗口大小
frame = cv2.flip(frame,1) #沿着y轴转换下方向
#===============设定一个固定区域作为识别区域=============
roi = frame[10:210,400:600] # 将右上角设置为固定识别区域
cv2.rectangle(frame,(400,10),(600,210),(0,0,255),0) # 将选定的区域标记出来
#===========在hsv色彩空间内检测出皮肤===============
hsv = cv2.cvtColor(roi,cv2.COLOR_BGR2HSV) #色彩空间转换
lower_skin = np.array([0,28,70],dtype=np.uint8) #设定范围,下限
upper_skin = np.array([20, 255, 255],dtype=np.uint8) #设定范围,上限
mask = cv2.inRange(hsv,lower_skin,upper_skin) #确定手所在区域
#===========预处理===============
kernel = np.ones((2,2),np.uint8) #构造一个核
mask = cv2.dilate(mask,kernel,iterations=4) #膨胀操作
mask = cv2.GaussianBlur(mask,(5,5),100) #高斯滤波
#=================找出轮廓===============
#查找所有轮廓
contours,h = cv2.findContours(mask,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#从所有轮廓中找到最大的,作为手势的轮廓
cnt = max(contours,key=lambda x:cv2.contourArea(x))
areacnt = cv2.contourArea(cnt) #获取轮廓面积
#===========获取轮廓的凸包=============
hull = cv2.convexHull(cnt) #获取轮廓的凸包,用于计算面积,返回坐标
# hull = cv2.convexHull(cnt,returnPoints=False)
areahull = cv2.contourArea(hull) #获取凸包的面积
#===========获取轮廓面积、凸包的面积比=============
arearatio = areacnt/areahull
# 轮廓面积/凸包面积 :
# 大于0.9,表示几乎一致,是手势0
# 否则,说明凸缺陷较大,是手势1.
#===========获取凸缺陷=============
hull = cv2.convexHull(cnt,returnPoints=False) #使用索引,returnPoints=False
defects = cv2.convexityDefects(cnt,hull) #获取凸缺陷
#===========凸缺陷处理==================
n=0 #定义凹凸点个数初始值为0
#-------------遍历凸缺陷,判断是否为指间凸缺陷--------------
for i in range(defects.shape[0]):
s,e,f,d, = defects[i,0]
start = tuple(cnt[s][0])
end = tuple(cnt[e][0])
far = tuple(cnt[f][0])
a = math.sqrt((end[0]-start[0])**2+(end[1]-start[1])**2)
b = math.sqrt((far[0] - start[0]) ** 2 + (far[1] - start[1]) ** 2)
c = math.sqrt((end[0]-far[0])**2+(end[1]-far[1])**2)
#--------计算手指之间的角度----------------
angle = math.acos((b**2 + c**2 -a**2)/(2*b*c))*57
#-----------绘制手指间的凸包最远点-------------
#角度在[20,90]之间的认为是不同手指所构成的凸缺陷
if angle<=90 and d>20:
n+=1
cv2.circle(roi,far,3,[255,0,0],-1) #用蓝色绘制最远点
#----------绘制手势的凸包--------------
cv2.line(roi,start,end,[0,255,0],2)
#============通过凸缺陷个数及面积比判断识别结果=================
if n==0: #0个凸缺陷,可能为0,也可能为1
if arearatio>0.9: #轮廓面积/凸包面积>0.9,判定为拳头,识别为0
result='0'
else:
result='1' #轮廓面积/凸包面积<=0.9,说明存在很大的凸缺陷,识别为1
elif n==1: #1个凸缺陷,对应2根手指,识别为2
result='2'
elif n==2: #2个凸缺陷,对应3根手指,识别为3
result='3'
elif n==3: #3个凸缺陷,对应4根手指,识别为4
result='4'
elif n==4: #4个凸缺陷,对应5根手指,识别为5
result='5'
#============设置与显示识别结果相关的参数=================
org=(400,80)
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale=2
color=(0,0,255)
thickness=3
#================显示识别结果===========================
cv2.putText(frame,result,org,font,fontScale,color,thickness)
cv2.imshow('frame',frame)
k = cv2.waitKey(25)& 0xff
if k == 27: # 键盘Esc键退出
break
cv2.destroyAllWindows()
cap.release()
输出结果:
3.石头、剪刀、布的识别
“石头、剪刀、布”是一种猜拳游戏,受到全世界人们的喜爱。该游戏如此流行,主要是因为它并非是纯靠运气的游戏,而是一种靠策略和智慧取胜的博弈。
形状匹配OpenCV提供了函数cv2.matchShapes()用来对两个对象的Hu矩进行比较。这两个对象可以是轮廓,也可以是灰度图像。函数cv2.matchShapes()的语法格式为:
retval=cv2.matchShapes(contour1,contour2,method,parameter)
其中,retval是返回值。该函数有如下4个参数。
● contour1:第1个轮廓或者灰度图像。
● contour2:第2个轮廓或者灰度图像。
● method:比较两个对象的Hu矩的方法.
● parameter:应用于method的特定参数,该参数为扩展参数,截至OpenCV 4.5.3-pre版本,暂不支持该参数,因此将该值设置为0。
代码实例:使用函数cv2.matchShapes()识别手势。
# -*- coding: utf-8 -*-
import cv2
def reg(x):
o1 = cv2.imread('paper.jpg',1)
o2 = cv2.imread('rock.jpg',1)
o3 = cv2.imread('scissors.jpg',1)
gray1 = cv2.cvtColor(o1,cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(o2,cv2.COLOR_BGR2GRAY)
gray3 = cv2.cvtColor(o3,cv2.COLOR_BGR2GRAY)
xgray = cv2.cvtColor(x,cv2.COLOR_BGR2GRAY)
ret, binary1 = cv2.threshold(gray1,127,255,cv2.THRESH_BINARY)
ret, binary2 = cv2.threshold(gray2,127,255,cv2.THRESH_BINARY)
ret, binary3 = cv2.threshold(gray3,127,255,cv2.THRESH_BINARY)
xret, xbinary = cv2.threshold(xgray,127,255,cv2.THRESH_BINARY)
contours1, hierarchy = cv2.findContours(binary1,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
contours2, hierarchy = cv2.findContours(binary2,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
contours3, hierarchy = cv2.findContours(binary3,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
xcontours, hierarchy = cv2.findContours(xbinary,
cv2.RETR_LIST,
cv2.CHAIN_APPROX_SIMPLE)
cnt1 = contours1[0]
cnt2 = contours2[0]
cnt3 = contours3[0]
x = xcontours[0]
ret=[]
ret.append(cv2.matchShapes(x,cnt1,1,0.0))
ret.append(cv2.matchShapes(x,cnt2,1,0.0))
ret.append(cv2.matchShapes(x,cnt3,1,0.0))
max_index = ret.index(min(ret)) #计算最大值索引
if max_index==0:
r="paper"
elif max_index==1:
r="rock"
else:
r="sessiors"
return r
t1=cv2.imread('test1.jpg',1)
t2=cv2.imread('test2.jpg',1)
t3=cv2.imread('test3.jpg',1)
# print(reg(t1))
# print(reg(t2))
# print(reg(t3))
# ===========显示处理结果==================
org=(0,60)
font = cv2.FONT_HERSHEY_SIMPLEX
fontScale=2
color=(255,255,255)
thickness=3
cv2.putText(t1,reg(t1),org,font,fontScale,color,thickness)
cv2.putText(t2,reg(t2),org,font,fontScale,color,thickness)
cv2.putText(t3,reg(t3),org,font,fontScale,color,thickness)
cv2.imshow('test1',t1)
cv2.imshow('test2',t2)
cv2.imshow('test3',t3)
cv2.waitKey()
cv2.destroyAllWindows()
输出结果: