OpenCV实例(二)手势识别

1.手势识别概述1.1.获取轮廓的凸包1.2.凸缺陷1.3.凸缺陷占凸包面积比

2.手势识别过程2.1.识别流程

3.石头、剪刀、布的识别

作者: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()

输出结果:

精彩内容

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: