1 编写背景
在2024年的高效智能创意大赛中,有一个手势识别的任务,需要通过四足机器人的摄像头识别手势然后做出相应动作。在这我将介绍我的手势识别的思路和相关代码,已记录我那2024.7.21死去的比赛。
2 任务要求
比赛规则如下:
![](https://i-blog.csdnimg.cn/direct/bebc417b1997433ea332bd8153379f26.png)
3 问题分析与解决
3.1 MediaPipe介绍
MediaPipe 是一款由 Google Research 开发并开源的机器学习模型应用框架,专门用于实时流媒体的多模态处理。Mediapipe hand库内置了已经训练好的手部检测和关键点识别模型,这些模型由Mediapipe团队使用大规模标注数据进行训练和优化,确保了高精度和高效能,开发者在使用时无需再进行模型训练,只需调用相关API即可实现手部检测功能。
此外,Mediapipe还训练了获取手部21个地标的模型。这21个地标是预定义好的,具体位置如下:
(1)手腕(1个地标):
0: 手腕
(2)拇指(4个地标):
1: 拇指根部
2: 拇指第一个关节
3: 拇指第二个关节
4: 拇指指尖
(3)食指(4个地标):
5: 食指根部
6: 食指第一个关节
7: 食指第二个关节
8: 食指指尖
(4)中指(4个地标):
9: 中指根部
10: 中指第一个关节
11: 中指第二个关节
12: 中指指尖
(5****)无名指(4个地标):
13: 无名指根部
14: 无名指第一个关节
15: 无名指第二个关节
16: 无名指指尖
(6**) 小指(4个地标)**:
17: 小指根部
18: 小指第一个关节
19: 小指第二个关节
20: 小指指尖
21个手部地标
3.2 MediaPipe的使用
此框架最大的好处是不需要自己去训练,模型已经被训练并封装好在python软件包里面,我们可以通过调用他的API很容易实现手部信息获取。
3.2.1 下载安装
要使用此包,则需要在python3.8环境(建议,比3.8低了没有此包,好像最高可以3.10?)下
pip install mediapipe
3.2.2 调用方法和代码
话不多说,我直接给出完整的代码和详细注释:
# -*- coding:utf-8 -*-
"""
@ By: ZhengXuan
@ Date: 2024-4-21
"""
import cv2
import mediapipe as mp
import numpy as np
class HandDetector:
"""
使用mediapipe库查找手。导出地标像素格式。添加了额外的功能。
如查找方式,许多手指向上或两个手指之间的距离。而且提供找到的手的边界框信息。
"""
def __init__(self, mode=False, maxHands=2, detectionCon=0.5, minTrackCon=0.5):
"""
:param mode: 在静态模式下,对每个图像进行检测
:param maxHands: 要检测的最大手数
:param detectionCon: 最小检测置信度
:param minTrackCon: 最小跟踪置信度
"""
self.results = None
self.mode = mode
self.maxHands = maxHands
self.modelComplex = False
self.detectionCon = detectionCon
self.minTrackCon = minTrackCon
# 初始化手部识别模型
self.mpHands = mp.solutions.hands
self.hands = self.mpHands.Hands(self.mode, self.maxHands, self.modelComplex,
self.detectionCon, self.minTrackCon)
self.mpDraw = mp.solutions.drawing_utils # 初始化绘图器
self.tipIds = [4, 8, 12, 16, 20] # 指尖列表
self.fingers = [] # 存储手的状态
self.lmList = [] # 储检测到的手部的每个关键点的坐标
def findHands(self, img, draw=True):
"""
从图像(BRG)中找到手部。
:param img: 用于查找手的图像。
:param draw: 在图像上绘制输出的标志。
:return: 带或不带图形的图像
"""
imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 将传入的图像由BGR模式转标准的Opencv模式——RGB模式,
self.results = self.hands.process(imgRGB) # 处理图像,返回包含检测到的手部信息的结果。这个结果通常包含了手部的关键点坐标
# 画出手的关键点和线条
if self.results.multi_hand_landmarks:
for handLms in self.results.multi_hand_landmarks:
if draw:
self.mpDraw.draw_landmarks(img, handLms,
self.mpHands.HAND_CONNECTIONS)
return img
def findPosition(self, img, handNo=0, draw=True):
"""
查找单手的地标并将其放入列表中像素格式。还可以返回手部周围的边界框。
:param img: 要查找的主图像
:param handNo: 如果检测到多只手,则为手部id
:param draw: 在图像上绘制输出的标志。(默认绘制矩形框)
:return: 像素格式的手部关节位置列表;手部边界框
"""
# 保存关键点的像素坐标
xList = []
yList = []
bbox = []
bboxInfo = [] # 保存首部检测框信息
self.lmList = []
if self.results.multi_hand_landmarks:
myHand = self.results.multi_hand_landmarks[handNo]
for id, lm in enumerate(myHand.landmark): # 遍历手部关键点,id表示关键点下标,lm表示关键点对象
h, w, c = img.shape
# lm是存储的是关键点归一化(0~1)的相对位置,
px, py = int(lm.x * w), int(lm.y * h) # 转换为图像中的像素坐标
xList.append(px)
yList.append(py)
self.lmList.append([px, py])
if draw:
cv2.circle(img, (px, py), 5, (255, 0, 255), cv2.FILLED) # 用红点标记关键点
# 获取手关键点的左上点和右下点
xmin, xmax = min(xList), max(xList)
ymin, ymax = min(yList), max(yList)
# 边界框信息存储
boxW, boxH = xmax - xmin, ymax - ymin
bbox = xmin, ymin, boxW, boxH
# 边界框中心坐标
cx, cy = bbox[0] + (bbox[2] // 2), \
bbox[1] + (bbox[3] // 2)
# id含义是指的手部最后一个关键点的下标
bboxInfo = {"id": id, "bbox": bbox, "center": (cx, cy)}
if draw:
cv2.rectangle(img, (bbox[0] - 20, bbox[1] - 20),
(bbox[0] + bbox[2] + 20, bbox[1] + bbox[3] + 20),
(0, 255, 0), 2)
return self.lmList, bboxInfo
def fingcurved(self):
"""
查找除拇指外的四个手指弯曲状态
计算方式:
取出除了大拇指以外的四个手指指尖坐标a1、a2、a3、a4(对应地标8,12,16,20),
然后取出地标为6,10,14,18的坐标b1、b2、b3、b4(即每个指尖以下第二个关节),
通过比较指尖(a1、a2、a3、a4)到手腕地标(0)和指关节(b1、b2、b3、b4)到地标0的欧几里得距离,
即可区分手指是否弯曲
:return: 弯曲手指的列表
"""
finger = []
for id in range(1, 5):
point1 = np.array((self.lmList[self.tipIds[id]][0], self.lmList[self.tipIds[id]][1]))
point2 = np.array((self.lmList[self.tipIds[id] - 2][0], self.lmList[self.tipIds[id] - 2][1]))
point3 = np.array((self.lmList[0][0], self.lmList[0][1]))
if np.linalg.norm(point1 - point3) < np.linalg.norm(point2 - point3): # 计算两点之间的距离
finger.append(1)
else:
finger.append(0)
return finger
def okgesture(self):
"""
特殊手势处理:判断是否手势为ok
判断方式:
ok手势,其拇指指尖地标a0和食指指尖地标a1十分接近,于是我们这样处理:如果中指、无名
指、小拇指伸直并且食指指尖到大拇指指尖的距离小于食指指尖到中指指尖距离则断定为ok手
势。
"""
f1, f2, f3, f4 = self.fingcurved()
if (f2 == 0 and f3 == 0 and f4 == 0):
point1 = np.array((self.lmList[8][0], self.lmList[8][1]))
point2 = np.array((self.lmList[4][0], self.lmList[4][1]))
point3 = np.array((self.lmList[12][0], self.lmList[12][1]))
if np.linalg.norm(point1 - point2) < np.linalg.norm(point1 - point3):
return True
def handType(self):
"""
检查传入的手部是左还是右
:return: "Right" 或 "Left"
"""
if self.results.multi_hand_landmarks:
if self.lmList[17][0] < self.lmList[5][0]:
return "Right"
else:
return "Left"
我将mediapipe检测手和获取手部信息封装在了HandDetect类中,下面给出调用代码:
detector = HandDetector()
camera = cv2.VideoCapture(0, cv2.CAP_DSHOW)
while True:
ret, img = camera.read()
if ret:
img = detector.findHands(img) # 获取你的手部的关键点信息
cv2.imshow('hand',img)
检测效果如下图所示:
21个地标获取效果图
3.2.3 手势分类
在获取到这21个地标后,我们通过判断手指弯曲与否和弯曲数量就可以判断大部分手势:
比如:
在比赛规则中的后退手势,当四个手指全伸直就是后退;
前进手势,食指伸直,另外三个手指弯曲就是前进:
但是也可能遇上一些不能简单通过弯曲和伸直来判断的手势:
例如我遇到的右平移手势,看起来是食指弯曲,后三个手指伸直,但是在我自己比画的时候,他很容易将食指识别为伸直,那这样就和上面的后退手势混淆了,所以我写了一个特殊手势处理,在上面代码中我给出了!:
最后我附上实现调用的完整代码:
# -*- coding:utf-8 -*-
"""
@ By: ZhengXuan
@ Date: 2024-4-21
"""
import cv2
from HandTrackingModule import HandDetector
class Main:
def __init__(self):
self.detector = None
self.camera = cv2.VideoCapture(0, cv2.CAP_DSHOW) # 以视频流传入
self.camera.set(3, 1280) # 设置分辨率
self.camera.set(4, 720)
def Gesture_recognition(self):
self.detector = HandDetector()
gesture_buffer = [None] * 3 # 只有连续三帧都为同一手势,才输出该手势,提高识别鲁棒性
while True:
ret, img = self.camera.read()
if ret:
img = self.detector.findHands(img) # 获取你的手部的关键点信息
cv2.imshow('hand',img)
lmList, bbox = self.detector.findPosition(img) # 获取你手部的关键点的像素坐标和边界框
if lmList:
x_1, y_1 = bbox["bbox"][0], bbox["bbox"][1]
f1, f2, f3, f4 = self.detector.fingcurved()
# 根据手指弯曲状态识别手势并在图像上显示相应文本
if f1 == 0 and f2 == 1 and f3 == 1 and f4 == 0:
gesture = "twist"
elif f1 == 0 and f2 == 1 and f3 == 1 and f4 == 1:
gesture = "forward"
elif self.detector.okgesture():
gesture = "right_move"
elif f1 == 0 and f2 == 0 and f3 == 0 and f4 == 0:
gesture = "back"
elif f1 == 0 and f2 == 0 and f3 == 1 and f4 == 1:
gesture = "left_move"
elif f1 == 1 and f2 == 1 and f3 == 1 and f4 == 0:
gesture = "left_twist"
elif f1 == 0 and f2 == 1 and f3 == 0 and f4 == 0:
gesture = "right_twist"
else:
gesture = None
if gesture:
cv2.putText(img, gesture, (x_1, y_1), cv2.FONT_HERSHEY_PLAIN, 3, (0, 0, 255), 3)
gesture_buffer.insert(0, gesture)
gesture_buffer.pop()
cv2.imshow("camera", img)
if gesture_buffer[0] is not None and all(ges == gesture_buffer[0] for ges in gesture_buffer):
self.print_gesture(gesture_buffer[0])
if cv2.getWindowProperty('camera', cv2.WND_PROP_VISIBLE) < 1:
break
# 通过关闭按钮退出程序
cv2.waitKey(1)
# if cv2.waitKey(1) & 0xFF == ord("q"):
# break # 按下q退出
def print_gesture(self, gesture):
if gesture == "twist":
print("原地扭身")
elif gesture == "forward":
print("前进")
elif gesture == "right_move":
print("右平移")
elif gesture == "back":
print("后退")
elif gesture == "left_twist":
print("左旋转")
elif gesture == "right_twist":
print("右旋转")
elif gesture == "left_move":
print("左平移")
if __name__ == '__main__':
main = Main()
main.Gesture_recognition()
4 测试效果
运行上面代码,可以得到如下所示的测试效果:
后退
前进
右平移
原地转身
左旋转
右平移
左旋转
5 结束语
虽然比赛没有进入国赛,但我相信不是我的问题,而是比赛主办方的问题,在这我将给出一个宝贵的建议:那就是线上答辩比赛,如果裁判规定了时间,比如我的答辩规定只有6分钟,那么一定一定要提前排练控制在时间内,不然会输的很难看。我希望将技术分享出来帮助到今后有需要的人来完成他们的梦想,希望我今天写的博客能为你的比赛、项目或者学习提供绵薄之力,如果觉得有用,请为我点上一个小小的赞,让更多人看到这篇博客!一起加油!
版权归原作者 徐岸轩 所有, 如有侵权,请联系我们删除。