Author:qyan.li
Date:2022.6.16
Topic:简单记录一次MediaPipe手势识别的过程(附算法思想和问题解决办法)
Reference:https://zhuanlan.zhihu.com/p/391844369
一、写在前面
~~~~~~~~
前段时间借助于树莓派(Raspberry
)构建手势控制系统,需要实现手势识别功能。由于之前使用MediaPipe
实现过姿态识别的功能,效果不错。因此,此次决定还是借助于MediaPipe
实现手势识别,调用实现的过程问题不断,写篇博文记录一下心路历程,供大家学习参考。
~~~~~~~~
树莓派智能小车的博文也已经更新,参考连接:https://blog.csdn.net/DALEONE/article/details/125241860?spm=1001.2014.3001.5501
二、准备工作及问题解决
~~~~~~~~
首先,对于MediaPipe
手势识别功能进行简要说明,一句话概括:MediaPipe
手势识别可以检测出手部的21个关键结点,速度快,效果佳。
~~~~~~~~
MediaPipe
的安装类似于其他python
模块的安装,没有什么特殊的地方,借助于命令:pip install MediaPipe
~~~~~~~~
安装完成之后,可以借助于如下代码验证安装下来的MediaPipe
能否正常使用,测试的代码放置在下面(这其实也是我们后面手势识别的代码)
import cv2
import mediapipe as mp
import time
import traceback
import socket
import sys
commandDict = {'right':'10','left':'01','forward':'11','backforward':'00','unKnown':'-1'}
cap = cv2.VideoCapture(0)
# cap = cv2.VideoCapture(0,cv2.CAP_DSHOW)
mpHands = mp.solutions.hands
hands = mpHands.Hands(static_image_mode=False,
max_num_hands=2,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
mpDraw = mp.solutions.drawing_utils
pTime = 0
cTime = 0
# 全局变量
frameNum = 0
commandLst = []
commandSending = ''
flag = 0
while True:
# 初始化0关键点的坐标
lst = [0, 0, 0]
# 初始化字典
distanceDict = {}
success, img = cap.read()
imgRGB= cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
results = hands.process(imgRGB)
# print(results.multi_hand_landmarks)
if results.multi_hand_landmarks:
for handLms in results.multi_hand_landmarks:
for id, lm in enumerate(handLms.landmark):
## 存储0关键点的三个坐标
if id == 0:
lst = [lm.x,lm.y,lm.z]
h, w, c = img.shape
cx, cy = int(lm.x *w), int(lm.y*h)
## 绘制手部关键结点
cv2.circle(img, (cx,cy), 3, (255,0,255), cv2.FILLED)
## 分别检测4,8,12,20四个关键结点与0结点间的距离判断手指指向
if id == 4 or id == 8 or id == 12 or id == 20:
distanceSum = 0
distanceSum = (lst[0]-lm.x)**2 + (lst[1]-lm.y)**2 + (lst[2]-lm.z)**2
distanceDict[id] = distanceSum
## mediapipe中首部关键结点的连线
mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS)
command = 'UnKnown'
if distanceDict == {}:
MaxId = 0
else:
MaxId = [key for key,value in distanceDict.items() if value == max(distanceDict.values())][0]
## 判断哪个结点距离根节点最远,并由此给出相应的命令
if MaxId == 4:
command = 'right'
if MaxId == 8:
command = 'left'
if MaxId == 12:
command = 'forward'
if MaxId == 20:
command = 'backforward'
if MaxId == 0:
command = 'unKnown'
cv2.putText(img,command,(10,150), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,255), 3)
## 计算并显示帧率
cTime = time.time()
fps = 1/(cTime-pTime)
pTime = cTime
cv2.putText(img,str(int(fps)), (10,70), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,255), 3)
cv2.imshow("Image", img)
## 连续判断十帧图像
frameNum += 1
commandLst.append(command)
if frameNum == 5:
if max(commandLst) == min(commandLst):
commandSending = commandDict[command]
else:
commandSending = commandDict['unKnown']
## 数据重新置零
frameNum = 0
commandLst = []
print(commandSending)
else:
continue
## 此处设置退出键esc,按下esc退出窗口
key = cv2.waitKey(1)
if key == 27:
#通过esc键退出摄像
cv2.destroyAllWindows()
break
~~~~~~~~
建立新的项目文件,拷贝上述代码运行(前提Python
已经成功配置OpenCV
的环境)
-
如果你的程序成功运行(摄像头窗口正常调用,
python
程序输出正常)且没有出现任何报错,那么恭喜你,你的环境配置成功,接下来的问题解决环节你已经可以跳过。 -
如果你的程序报错,无法执行,那么同样恭喜你,接下来会将拯救你于苦海:
程序的报错可能分为三种情况:
-
FileNotFoundError: The path does not exist.**
报错,出现此类错误是由于python
解释器的安装路径中存在中文字符导致。小
Tips:
注意此处为解释器的安装路径,而非项目文件路径
-
[ WARN:0@3.614] global D:\a\opencv-python\opencv-python\opencv\modules\videoio\src\cap_msmf.cpp (539) `anonymous-namespace'::SourceReaderCB::~SourceReaderCB terminating async callback
警告,由于Opencv版本不兼容导致的问题,由于不影响程序的正常运行,可以不同管 -
cv2.error: OpenCV(4.5.5) D:\a\opencv-python\opencv-python\opencv\modules\imgproc\src\color.cpp:182: error: (-215:Assertion failed) !_src.empty() in function 'cv::cvtColor'
此种错误网络上说法不一,但是自己是由于电脑本身的摄像头连接导致的错误
~~~~~~~~ OK,我们已经发现问题,下面开始介绍上述问题解决办法:
-
python解释器中文路径问题
~~~~~~~~ 不清楚大家什么状况,自己解释器含有中文路径是由于系统名称为中文,导致python安装的位置中含有中文,可能的解决办法:
- 修改系统的名称为中文—一劳永逸,但是可能存在系统崩溃,文件丢失的风险,权衡过后被我pass掉
- 修改python解释器的安装位置—个人感觉结尾复杂,同时涉及环境变量以及诸多问题,最终也被pass
~~~~~~~~ 那自己是采取的什么措施,安装一个新的python解释器,放置在全英文字符路径下,重新配置下环境,MediaPipe项目使用新的解释器即可,现在python已经出到3.10。
-
警告问题:
~~~~~~~~ 这个警告其实可以不用管,只要不影响程序的运行即可,但是也可以将
cap = cv2.VideoCapture(0)
改为cap = cv2.VideoCapture(0,cv2.CAP_DSHOW)
,会发现此时项目运行的警告已经消失-
cv2.error报错问题
~~~~~~~~ 这个问题网络上的解释比较多,比如路径,通道数等等,我都尝试过,但都不太符合自己运行环境的实际情况。折磨很久,检查很多遍,最终发现是摄像头连接的问题,那怎么检查你的报错情况是否是摄像头的问题呢?很简单,手动打开电脑的摄像头,看看能否正常工作,如果不能,那大概率就是我的这种情况。
~~~~~~~~ 那如果真的是,如何解决呢?那你就可以检索windows下摄像头无法正常工作的文章,提供几个思路,应该是可以解决的:
-
重启大法,重启下电脑试试,没准真的可以解决
-
调整下屏幕倾角,按一下键盘与屏幕连接处(考虑接触不良问题,尤其是动过电脑的同学)
自己就是这种情况,由于之前换过电脑屏幕,可能接触不是特别好
-
更新下摄像头的驱动
-
其他方法参考其他博文即可
-
-
~~~~~~~~ OK,问题解决完,代码可以正常运行,我们来看一下代码是怎样实现手势识别的,思想非常简单,用一句话概括:计算指尖到手掌根部的距离实现手势的识别。手势识别算法设计是需要和你的需求相匹配,自己是利用手势控制小车的前进,停止,左转,右转。命令简单,且仅具有4中分类,所以,上述的算法可以很好的进行适配。但是假设你的需求较为复杂,就需要更高阶的算法实现手势识别。
~~~~~~~~
先放一张MediaPipe手部21关键节点的图片,方便大家后续参考:
for id, lm in enumerate(handLms.landmark):
## 存储0关键点的三个坐标
if id == 0:
lst = [lm.x,lm.y,lm.z]
h, w, c = img.shape
cx, cy = int(lm.x *w), int(lm.y*h)
## 绘制手部关键结点
cv2.circle(img, (cx,cy), 3, (255,0,255), cv2.FILLED)
## 分别检测4,8,12,20四个关键结点与0结点间的距离判断手指指向
if id == 4 or id == 8 or id == 12 or id == 20:
distanceSum = 0
distanceSum = (lst[0]-lm.x)**2 + (lst[1]-lm.y)**2 + (lst[2]-lm.z)**2
distanceDict[id] = distanceSum
上述代码展示如何计算四个手指的指尖到掌根的距离:
- id代表手掌21个不同的关键结点
- lm存储每个结点的xyz坐标,可以通过lm.x lm.y lm.z的方式进行获取
- id=4,8,12,20分别代表手掌的大拇指,中指,食指,小拇指,id=0代表掌根
- 将四个手指到指尖的距离存入distanceDict字典,方便后续调用
command = 'UnKnown'
if distanceDict == {}:
MaxId = 0
else:
MaxId = [key for key,value in distanceDict.items() if value == max(distanceDict.values())][0]
## 判断哪个结点距离根节点最远,并由此给出相应的命令
if MaxId == 4:
command = 'right'
if MaxId == 8:
command = 'left'
if MaxId == 12:
command = 'forward'
if MaxId == 20:
command = 'backforward'
if MaxId == 0:
command = 'unKnown'
cv2.putText(img,command,(10,150), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,255), 3)
这段代码展示借助于distanceDict确定命令:
- 在字典不为零的情况下,取出字典中距离最远的手指id
- 根据实现建立的映射关系,转化为相应的控制命令
## 连续判断十帧图像
frameNum += 1
commandLst.append(command)
if frameNum == 5:
if max(commandLst) == min(commandLst):
commandSending = commandDict[command]
else:
commandSending = commandDict['unKnown']
## 数据重新置零
frameNum = 0
commandLst = []
print(commandSending)
else:
continue
这段代码表示连续判断十帧图像,生成最终的命令:
(连续判断10帧图像的目的在于防止手指切换过程中可能造成的误差)
- commandLst用于存储10帧图像的命令
- max(commandLst)==min(commandLst)判断10帧图像命令是否相同,相同命令即确定,不同即为unKnown
三、写在最后
~~~~~~~~ 写到这里,今天的主要内容已经完成,最后写一点题外话,是由上面python解释器安装引申来的。假设有的同学真的采用上述方法下载新的解释器,那么自己的本机上就应该不止有一个解释器,那么如何管理他们呢?这其实在自己刚玩python时是困扰自己挺长时间的一个问题。
~~~~~~~~ 拿我自己的电脑来说,python解释器有三个版本:3.7,3.9,3.10,且按照教程添加环境变量,此时在终端中输入python显示的为3.10即你最近下载的版本,使用pip install安装模块时默认也肯定会安装在3.10的解释器下,那么如何访问3.7和3.9的解释器呢?尤其时在pip install 安装模块时。
- 找到python37和39的安装路径
- 将python文件夹下python.exe分别改为python37.exe和python39.exe
~~~~~~~~ OK,经过上述操作,当你想通过终端利用pip安装模块时,就可以借助于如下命令:python37 -m pip install numpy或者python39 -m pip install numpy,其他类似的命令也可以采取上述方式。