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()

输出结果:

在这里插入图片描述

posted @ 2023-04-12 21:19  小幽余生不加糖  阅读(277)  评论(0编辑  收藏  举报  来源