基于骨架模型的手势分类识别
基于MediaPipe的手部关键点检测
MediaPipe最初是为了实时分析视频和音频而开发的,它是一个用于构建机器学习管道的框架,2019年,Google对MediaPipe框架进行了公开发布,并开源了手部检测和跟踪模型MediaPipeHands。MediaPipeHands提供了一种手部和手指的高保真追踪方案,如图,它可从视频单帧中推断出手部手势的21个3D坐标。
当我们有了手势关键点的坐标之后,我们就可以根据坐标来构建向量,从而利用向量对手势进行特征构建。利用向量进行特征构建的优点是向量不仅具有大小还有方向,这对后续动态手势识别的特征构建比较做了一定铺垫,此外向量的大小角度等特征由坐标计算得到但不依赖于坐标系而改变也是它进行特征构建的一大优势。
判断手指发生弯曲的特征构建
对于常见的一些手势我们可以利用手势中的手指是否发生了弯曲来进行识别。如当一幅手势图像中的拇指、食指发生了弯曲而另外三根手指未发生弯曲,我们就判定该手势为OK手势。
如图,我们可以依据关键点0、关键点6和关键点8构建出向量*v
06和向量v08。显然我们可以很自然的想到,当向量v06的2范数||v06||大于向量v08*的2范数||v08||时,我们就说食指发生了弯曲。同理我们可以对中指、无名指和小拇指进行弯曲定义。手指弯曲角度的特征构建
而对于大拇指的弯曲定义则相对麻烦一些,由于人类骨骼的影响,我们无法简单的利用上述向量2范数的大小比较来判定大拇指是否发生了弯曲。为此我们需要对手指向量加上另外一个特征,即角度特征。我们可以由关键点0、关键点3和关键点4的坐标,通过余弦定理来计算∠034的大小,经测试,当其角度小于130°时,我们一般就可判定大拇指发生了弯曲。
基于时空数据特征的动态手势识别
在动态手势识别领域,针对动态手势识别的算法有隐马尔可夫模型(HMM)、支持向量机(SVM)以及动态时间规整(DTW)等等,而笔者在尝试使用骨架模型进行静态手势识别时受图像处理帧差法启发,借用手势关键点以及视频帧本身所具有的特性,尝试了一种简单且有效的动态手势识别算法,经测试,该算法还可以很好的与静态手势识别算法进行结合。在介绍该算法前,我们首先介绍该算法所需的一些动态识别特征。
手势位移的提取
所谓手势位移,即手势的某一点在视频相邻帧之间的位移。设上图中关键点9为点P,记点P在摄像机中的坐标为(x,y),xi表示点P在第i帧图像中的横坐标,yi表示点P在第i帧图像中的纵坐标,记手势位移x,则:
$$
X=(x_{i+1}-x_i,y_{i+1}-y_i)\
||X||=\sqrt{(x_{i+1}-x_i)^2+(y_{i+1}-y_i)^2}
$$
手势运动方向的提取
在物理学中,位移是一个矢量(向量),既有大小又有方向,显然根据本文需求中,我们仅需判定xi+1−xi>0时,手势在向右运动,同理当xi+1−xi<0时,我们就判定手势正在向左运动。
手势运动角度范围的提取
记x轴水平向量为l=[0,1],则手势位移x与水平向量l的夹角为:
$$
\theta=arccos\frac{x\cdot l}{||x|| ||l||}
$$
为避免手势关键点抖动产生的不必要的误差,我们需要对手势位移的大小设定一个阈值,只有当手势位移的大小大于该阈值时我们才能判定手势在产生运动行为。同时为防止不必要的误触行为,我们也需要对手势的运动角度设定一个阈值范围.
综上所述,我们对手势向右运动的数学定义如下:
$$
\left{
\begin{array}{c}
||X||>180\x_{i+1}-x_i>0\0°<\theta<30°
\end{array}
\right.
$$
手势向左运动的数学定义如下:
$$
\left{
\begin{array}{c}
||X||<180\x_{i+1}-x_i<0\150°<\theta<180°
\end{array}
\right.
$$
代码实例
# -*- codeing=utf-8 -*-
# @Author:姜磊
# 人间烟火气,最抚凡人心
import math
# 计算向量2范数
def vectorSize(p1,p2):
return math.sqrt((p1[0]-p2[0])**2+(p1[1]-p2[1])**2)
# 余弦定理计算向量夹角
def vectorAngle(p1,p2,p3):
b = math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2)
c = math.sqrt((p2[0] - p3[0]) ** 2 + (p2[1] - p3[1]) ** 2)
a = math.sqrt((p3[0] - p1[0]) ** 2 + (p3[1] - p1[1]) ** 2)
if ((2 * b * c)>1e-10):
angle = math.acos(((b ** 2 + c ** 2 - a ** 2) / (2 * b * c)))
return math.degrees(angle)
# 由坐标点构造向量
def mkVector(p1,p2):
return [(p1[0]-p2[0]),(p1[1]-p2[1])]
# 计算两个向量的夹角
def vectorAngle2(v1,v2):
nor=0.0
a=0.0
b=0.0
for x,y in zip(v1,v2):
nor+=x*y#向量内积
a+=x**2
b+=y**2
if a==0 or b==0:
return None
cosTheta=nor/math.sqrt(a*b)
angle=math.acos(cosTheta)
return math.degrees(angle)
# 判断手指是否张开
def fingersUp(landmarks):
fingers=[]
# 大拇指
if vectorAngle(landmarks[0],landmarks[3],landmarks[4])>130:
fingers.append(1)
else:
fingers.append(0)
# 食指
if vectorSize(landmarks[0],landmarks[8])>vectorSize(landmarks[0],landmarks[6]):
fingers.append(1)
else:
fingers.append(0)
# 中指
if vectorSize(landmarks[0],landmarks[12])>vectorSize(landmarks[0],landmarks[10]):
fingers.append(1)
else:
fingers.append(0)
# 无名指
if vectorSize(landmarks[0],landmarks[16])>vectorSize(landmarks[0],landmarks[14]):
fingers.append(1)
else:
fingers.append(0)
# 小拇指
if vectorSize(landmarks[0],landmarks[20])>vectorSize(landmarks[0],landmarks[18]):
fingers.append(1)
else:
fingers.append(0)
return fingers
# -*- codeing=utf-8 -*-
# @Author:姜磊
# 人间烟火气,最抚凡人心
from fingersVector import fingersUp,vectorSize,vectorAngle,mkVector,vectorAngle2
import cv2 as cv
def staticGestureRec(landmark):
command = 0
fingers=fingersUp(landmark)
if (fingers[0] == 0 and fingers[1] == 0 and fingers[2] == 1 and fingers[3] == 1 and fingers[4] == 1):
command=1
# print("OK")
if(fingers[0]==1 and fingers[1]==0 and fingers[2]==0 and fingers[3]==0 and fingers[4]==0):
command=2
print("大拇指")
if(fingers[0]==0 and fingers[1]==0 and fingers[2]==0 and fingers[3]==0 and fingers[4]==0):
command=3
print("拳头")
if (fingers[0] == 0 and fingers[1] == 0 and fingers[2] == 1 and fingers[3] == 0 and fingers[4] == 0):
command=4
# print("中指")
if (fingers[0] == 1 and fingers[1] == 1 and fingers[2] == 0 and fingers[3] == 0 and fingers[4] == 1):
command=5
# print("spider")
if (fingers[0] == 1 and fingers[1] == 1 and fingers[2] == 0 and fingers[3] == 0 and fingers[4] == 0
and vectorSize(landmark[3],landmark[6])<20 and vectorAngle(landmark[4],landmark[6],landmark[8])<90):
command=6
# print("比心")
return command
def gestureRecognition(frame,landmarks,preCenter):
# 静态手势识别
command = staticGestureRec(landmarks)
# 动态手势识别
curCenter = landmarks[9]
cv.circle(frame, curCenter, 10, (0, 255, 255), -1)
centerVector = mkVector(curCenter, preCenter) # 构造帧差向量
curPreSize = vectorSize(preCenter, curCenter) # 控制手势帧差大小区域
xAxis = [1, 0]
angle = vectorAngle2(centerVector, xAxis) # 控制手势帧差角度区域
if centerVector[0] > 0 and curPreSize > 180 and angle < 30:
command = 7
# print("right")
if centerVector[0] < 0 and curPreSize > 180 and angle > 150:
command = 8
# print("left")
return curCenter,command
效果: