各位同学好,今天和大家分享一下如何使用 MediaPipe+Opencv 通过手势识别来控制电脑鼠标的移动和点击,如果有兴趣的话,可以代替鼠标去打游戏。先放图看效果。用画图板来测试
黄框代表电脑屏幕的范围,将黄框的宽高映射到电脑屏幕的宽高。食指竖起并且中指弯下时,移动鼠标。食指和中指都竖起,并且两个指尖距离小于50时,认为是点击鼠标。左上角30代表FPS值
移动鼠标:移动时,食指指尖有淡蓝色圆点,表明鼠标在移动,如右图的绿色线条是鼠标移动轨迹
点击鼠标:当食指和中指间的距离小于50,食指指尖圆点变成绿色,点击鼠标,如画图板上的两个点,就是点击两下实现的。
1. 导入工具包
# 安装工具包
pip install opencv-contrib-python # 安装opencv
pip install mediapipe # 安装mediapipe
# pip install mediapipe --user #有user报错的话试试这个
pip install cvzone # 安装cvzone
pip install autopy # 鼠标控制单元
# 导入工具包
import numpy as np
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部追踪方法
import mediapipe as mp
import time
import autopy
21个手部关键点信息如下,本节我们主要研究食指指尖"8"和中指指尖"12"的坐标信息。
2. 手部关键点检测
(1) cvzone.HandTrackingModule.HandDetector() 手部关键点检测方法
参数:
mode: 默认为 False,将输入图像视为视频流。它将尝试在第一个输入图像中检测手,并在成功检测后进一步定位手的坐标。在随后的图像中,一旦检测到所有 maxHands 手并定位了相应的手的坐标,它就会跟踪这些坐标,而不会调用另一个检测,直到它失去对任何一只手的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 True,则在每个输入图像上运行手部检测,用于处理一批静态的、可能不相关的图像。
maxHands: 最多检测几只手,默认为 2
detectionCon: 手部检测模型的最小置信值(0-1之间),超过阈值则检测成功。默认为 0.5
minTrackingCon: 坐标跟踪模型的最小置信值 (0-1之间),用于将手部坐标视为成功跟踪,不成功则在下一个输入图像上自动调用手部检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 mode 为 True,则忽略这个参数,手部检测将在每个图像上运行。默认为 0.5
它的参数和返回值类似于官方函数 mediapipe.solutions.hands.Hands()
(2)cvzone.HandTrackingModule.HandDetector.findHands() 找到手部关键点并绘图
参数:
img: 需要检测关键点的帧图像,格式为BGR
draw: 是否需要在原图像上绘制关键点及识别框
flipType: 图像是否需要翻转,当视频图像和我们自己不是镜像关系时,设为True就可以了
返回值:
hands: 检测到的手部信息,包含:21个关键点坐标,检测框坐标及宽高,检测框中心坐标,检测出是哪一只手。
img: 返回绘制了关键点及连线后的图像
手部检测的代码如下:
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部检测方法
import time
#(1)导数视频数据
cap = cv2.VideoCapture(0) # 0代表自己电脑的摄像头
cap.set(3, 1280) # 设置显示框的宽度1280
cap.set(4, 720) # 设置显示框的高度720
pTime = 0 # 设置第一帧开始处理的起始时间
#(2)接收手部检测方法
detector = HandDetector(mode=False, # 视频流图像
maxHands=1, # 最多检测一只手
detectionCon=0.8, # 最小检测置信度
minTrackCon=0.5) # 最小跟踪置信度
#(3)处理每一帧图像
while True:
# 图片是否成功接收、img帧图像
success, img = cap.read()
# 翻转图像,使自身和摄像头中的自己呈镜像关系
img = cv2.flip(img, flipCode=1) # 1代表水平翻转,0代表竖直翻转
#(4)手部检测方法
# 传入每帧图像, 返回手部关键点的坐标信息(字典构成的列表hands),绘制关键点后的图像img
hands, img = detector.findHands(img, flipType=False) # 上面反转过了,这里就不用再翻转了
# print(hands)
#(5)显示图像
# 查看FPS
cTime = time.time() #处理完一帧图像的时间
fps = 1/(cTime-pTime)
pTime = cTime #重置起始时间
# 在视频上显示fps信息,先转换成整数再变成字符串形式,文本显示坐标,文本字体,文本大小
cv2.putText(img, str(int(fps)), (70,50), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,0), 3)
# 显示图像,输入窗口名及图像数据
cv2.imshow('image', img)
if cv2.waitKey(1) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
打印每帧图像检测到的手部信息hands列表,由字典组成。lmList 代表21个手部关键点的像素坐标;bbox 代表检测框的左上角坐标和框的宽高;center 代表检测框的中心点的像素坐标;type 代表检测出的是左手还是右手。
----------------------------------------------------------------------------
[{'lmList': [[522, 755], [621, 761], [709, 724], [765, 675], [794, 615], [705, 629], [761, 588], [749, 643], [715, 686], [676, 599], [743, 565], [713, 637], [664, 684], [634, 565], [710, 543], [668, 622], [613, 666], [576, 533], [657, 519], [640, 580], [597, 620]],
'bbox': (522, 519, 272, 242),
'center': (658, 640),
'type': 'Left'}]
[{'lmList': [[520, 763], [620, 774], [716, 753], [779, 707], [816, 650], [716, 655], [781, 619], [767, 677], [727, 721], [689, 627], [759, 595], [731, 667], [683, 710], [649, 594], [727, 579], [680, 653], [620, 689], [593, 558], [674, 549], [655, 608], [608, 642]],
'bbox': (520, 549, 296, 225),
'center': (668, 661),
'type': 'Left'}]
----------------------------------------------------------------------------
检测结果如图所示:
2. 移动鼠标
移动鼠标的思路是:如果检测到食指竖起,并且中指弯下,那么就认为是移动鼠标,鼠标的位置坐标是食指指尖所在的位置坐标。
检测哪个手指是竖起的方法是 detector.fingersUp() ,传入检测到的某只手的手部信息hands[0];返回值是由5个元素构成的列表,元素为1代表该手指竖起,0代表手指弯下,例如:[0,1,1,0,0] 就代表食指和中指竖起,其他手指弯下。
当手指在摄像头画面的下半部分移动时,由于摄像头界限范围问题,手掌部分会在摄像头画面中消失,导致检测不到手部关键点,因此,在屏幕画面的偏上半部分绘制一个黄色的矩形框,手指只能在矩形框中移动,避免手部关键点的消失。
由于我们设置的矩形框大小明显要小于电脑屏幕的大小,导致手控的鼠标无法在整个电脑屏幕上移动。因此,需要将矩形框的宽和高映射到电脑屏幕的宽和高。使用线性插值方法 np.interp(x, xp, fp) 简单来说就是将变量x的范围从原来的xp映射到fp。如:np.interp(x1, (pt1[0], pt2[0]), (0, wScr)),就是将x坐标的范围从原来的 pt1[0] 到 pt1[0]+w,映射到整个电脑屏幕 0 到 wScr。
返回电脑屏幕的宽和高: autopy.screen.size()
移动鼠标的位置到坐标(x,y): autopy.mouse.move(x, y)
autopy具体使用方法见下文:https://blog.csdn.net/qq_30462003/article/details/100130472
因此,我们在上述代码中补充:
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector # 手部检测方法
import time
# pip install autopy #鼠标控制单元
import autopy
#(1)导数视频数据
wScr, hScr = autopy.screen.size() # 返回电脑屏幕的宽和高(1920.0, 1080.0)
wCam, hCam = 1280, 720 # 视频显示窗口的宽和高
pt1, pt2 = (100,100), (1100, 500) # 虚拟鼠标的移动范围,左上坐标pt1,右下坐标pt2
cap = cv2.VideoCapture(0) # 0代表自己电脑的摄像头
cap.set(3, wCam) # 设置显示框的宽度1280
cap.set(4, hCam) # 设置显示框的高度720
pTime = 0 # 设置第一帧开始处理的起始时间
#(2)接收手部检测方法
detector = HandDetector(mode=False, # 视频流图像
maxHands=1, # 最多检测一只手
detectionCon=0.8, # 最小检测置信度
minTrackCon=0.5) # 最小跟踪置信度
#(3)处理每一帧图像
while True:
# 图片是否成功接收、img帧图像
success, img = cap.read()
# 翻转图像,使自身和摄像头中的自己呈镜像关系
img = cv2.flip(img, flipCode=1) # 1代表水平翻转,0代表竖直翻转
# 在图像窗口上创建一个矩形框,在该区域内移动鼠标
cv2.rectangle(img, pt1, pt2, (0,255,255), 5)
#(4)手部关键点检测
# 传入每帧图像, 返回手部关键点的坐标信息(字典),绘制关键点后的图像
hands, img = detector.findHands(img, flipType=False) # 上面反转过了,这里就不用再翻转了
# print(hands)
# 如果能检测到手那么就进行下一步
if hands:
# 获取手部信息hands中的21个关键点信息
lmList = hands[0]['lmList'] # hands是由N个字典组成的列表,字典包每只手的关键点信息
# 获取食指指尖坐标,和中指指尖坐标
x1, y1 = lmList[8] # 食指尖的关键点索引号为8
x2, y2 = lmList[12] # 中指指尖索引12
#(5)检查哪个手指是朝上的
fingers = detector.fingersUp(hands[0]) # 传入
# print(fingers) 返回 [0,1,1,0,0] 代表 只有食指和中指竖起
# 如果食指竖起且中指弯下,就认为是移动鼠标
if fingers[1] == 1 and fingers[2] == 0:
# 开始移动时,在食指指尖画一个圆圈,看得更清晰一些
cv2.circle(img, (x1,y1), 15, (255,255,0), cv2.FILLED) # 颜色填充整个圆
#(6)确定鼠标移动的范围
# 将食指的移动范围从预制的窗口范围,映射到电脑屏幕范围
x3 = np.interp(x1, (pt1[0], pt2[0]), (0, wScr))
y3 = np.interp(y1, (pt1[1], pt2[1]), (0, hScr))
#(7)移动鼠标
autopy.mouse.move(x3, y3) # 给出鼠标移动位置坐标
#(8)显示图像
# 查看FPS
cTime = time.time() #处理完一帧图像的时间
fps = 1/(cTime-pTime)
pTime = cTime #重置起始时间
# 在视频上显示fps信息,先转换成整数再变成字符串形式,文本显示坐标,文本字体,文本大小
cv2.putText(img, str(int(fps)), (70,50), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,0), 3)
# 显示图像,输入窗口名及图像数据
cv2.imshow('image', img)
if cv2.waitKey(1) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
效果图如下:
3. 点击鼠标
点击鼠标的思路是:如果食指和中指同时竖起,并且食指指尖和中指指尖之间的像素距离小于50时,那么就认为是点击鼠标。
检测哪个手指是竖起的方法是上面已经解释过的 detector.fingersUp() 方法,检测指尖距离的方法是: detector.findDistance(pt1, pt2, img), pt1 和 pt2 是两个点的坐标,传入img来绘制指尖连线图。
点击鼠标的函数,autopy.mouse.click()
由于用手指控制鼠标时,每一帧的坐标位置的变化幅度较大,导致电脑鼠标在手指控制下很容易晃动,很难准确定位到一个目标。因此需要平滑每一帧的坐标变化,使坐标变化更缓慢一些。
例如:cLocx = pLocx + (x3 - pLocx) / smooth,式中:前帧的鼠标位置的x坐标 cLocx;前一帧的鼠标位置的x坐标 pLocx;当前鼠标位置的x坐标 x3;自定义平滑系数smooth,值越大鼠标移动就越慢,平稳性就越高。
因此,在上述代码中补充:
import cv2
import numpy as np
from cvzone.HandTrackingModule import HandDetector # 手部检测方法
import time
import autopy
#(1)导数视频数据
wScr, hScr = autopy.screen.size() # 返回电脑屏幕的宽和高(1920.0, 1080.0)
wCam, hCam = 1280, 720 # 视频显示窗口的宽和高
pt1, pt2 = (100,100), (1100, 500) # 虚拟鼠标的移动范围,左上坐标pt1,右下坐标pt2
cap = cv2.VideoCapture(0) # 0代表自己电脑的摄像头
cap.set(3, wCam) # 设置显示框的宽度1280
cap.set(4, hCam) # 设置显示框的高度720
pTime = 0 # 设置第一帧开始处理的起始时间
pLocx, pLocy = 0, 0 # 上一帧时的鼠标所在位置
smooth = 4 # 自定义平滑系数,让鼠标移动平缓一些
#(2)接收手部检测方法
detector = HandDetector(mode=False, # 视频流图像
maxHands=1, # 最多检测一只手
detectionCon=0.8, # 最小检测置信度
minTrackCon=0.5) # 最小跟踪置信度
#(3)处理每一帧图像
while True:
# 图片是否成功接收、img帧图像
success, img = cap.read()
# 翻转图像,使自身和摄像头中的自己呈镜像关系
img = cv2.flip(img, flipCode=1) # 1代表水平翻转,0代表竖直翻转
# 在图像窗口上创建一个矩形框,在该区域内移动鼠标
cv2.rectangle(img, pt1, pt2, (0,255,255), 5)
#(4)手部关键点检测
# 传入每帧图像, 返回手部关键点的坐标信息(字典),绘制关键点后的图像
hands, img = detector.findHands(img, flipType=False) # 上面反转过了,这里就不用再翻转了
# print(hands)
# 如果能检测到手那么就进行下一步
if hands:
# 获取手部信息hands中的21个关键点信息
lmList = hands[0]['lmList'] # hands是由N个字典组成的列表,字典包每只手的关键点信息
# 获取食指指尖坐标,和中指指尖坐标
x1, y1 = lmList[8] # 食指尖的关键点索引号为8
x2, y2 = lmList[12] # 中指指尖索引12
#(5)检查哪个手指是朝上的
fingers = detector.fingersUp(hands[0]) # 传入
# print(fingers) 返回 [0,1,1,0,0] 代表 只有食指和中指竖起
# 如果食指竖起且中指弯下,就认为是移动鼠标
if fingers[1] == 1 and fingers[2] == 0:
# 开始移动时,在食指指尖画一个圆圈,看得更清晰一些
cv2.circle(img, (x1,y1), 15, (255,255,0), cv2.FILLED) # 颜色填充整个圆
#(6)确定鼠标移动的范围
# 将食指的移动范围从预制的窗口范围,映射到电脑屏幕范围
x3 = np.interp(x1, (pt1[0], pt2[0]), (0, wScr))
y3 = np.interp(y1, (pt1[1], pt2[1]), (0, hScr))
#(7)平滑,使手指在移动鼠标时,鼠标箭头不会一直晃动
cLocx = pLocx + (x3 - pLocx) / smooth # 当前的鼠标所在位置坐标
cLocy = pLocy + (y3 - pLocy) / smooth
#(8)移动鼠标
autopy.mouse.move(cLocx, cLocy) # 给出鼠标移动位置坐标
# 更新前一帧的鼠标所在位置坐标,将当前帧鼠标所在位置,变成下一帧的鼠标前一帧所在位置
pLocx, pLocy = cLocx, cLocy
#(9)如果食指和中指都竖起,指尖距离小于某个值认为是点击鼠标
if fingers[1] == 1 and fingers[2] == 1: # 食指和中指都竖起
# 计算食指尖和中指尖之间的距离distance,绘制好了的图像img,指尖连线的信息info
distance, info, img = detector.findDistance((x1, y1), (x2, y2), img)
# print(distance)
# 当指间距离小于50(像素距离)就认为是点击鼠标
if distance < 50:
# 在食指尖画个绿色的圆,表示点击鼠标
cv2.circle(img, (x1,y1), 15, (0,255,0), cv2.FILLED)
# 点击鼠标
autopy.mouse.click()
#(10)显示图像
# 查看FPS
cTime = time.time() #处理完一帧图像的时间
fps = 1/(cTime-pTime)
pTime = cTime #重置起始时间
# 在视频上显示fps信息,先转换成整数再变成字符串形式,文本显示坐标,文本字体,文本大小
cv2.putText(img, str(int(fps)), (70,50), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,0), 3)
# 显示图像,输入窗口名及图像数据
cv2.imshow('image', img)
if cv2.waitKey(1) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
结果图像展示,在绘图板中每点击一次就绘制一个圆圈。