各位同学好,今天和大家分享一下如何使用 opencv+mediapipe 完成远程手势控制电脑键盘。感兴趣的可以看一下我前面一篇手势控制电脑鼠标:https://blog.csdn.net/dgvv4/article/details/122268203?spm=1001.2014.3001.5501, 把这两个结合起来去打游戏会不会很有意思呢。先放张图看效果。
这里用百度搜索栏做测试,搜索框中的内容和opencv显示窗口上的内容是同步打印出来的。
工作原理:如果检测到食指指尖关键点坐标在某个按键框的范围内部,那么该按键显示绿色;如果食指指尖和中指指尖之间的距离小于规定距离,就认为是点击食指指尖所在的按键,按键变为红色;设置1秒内同一个按键只能点击一次,避免重复出现多个相同按键值。
# 安装工具包
pip install opencv-contrib-python # 安装opencv
pip install mediapipe # 安装mediapipe
# pip install mediapipe --user #有user报错的话试试这个
pip install cvzone # 安装cvzone
pip install pynput # 键盘控制单元
# 导入工具包
import numpy as np
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部追踪方法
import mediapipe as mp
import time
from pynput.keyboard import Controller # 键盘控制模块
21个手部关键点信息如下,本节我们主要研究食指指尖"8"和中指指尖"12"的坐标信息。
参数:
mode: 默认为 False,将输入图像视为视频流。它将尝试在第一个输入图像中检测手,并在成功检测后进一步定位手的坐标。在随后的图像中,一旦检测到所有 maxHands 手并定位了相应的手的坐标,它就会跟踪这些坐标,而不会调用另一个检测,直到它失去对任何一只手的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 True,则在每个输入图像上运行手部检测,用于处理一批静态的、可能不相关的图像。
maxHands: 最多检测几只手,默认为 2
detectionCon: 手部检测模型的最小置信值(0-1之间),超过阈值则检测成功。默认为 0.5
minTrackingCon: 坐标跟踪模型的最小置信值 (0-1之间),用于将手部坐标视为成功跟踪,不成功则在下一个输入图像上自动调用手部检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 mode 为 True,则忽略这个参数,手部检测将在每个图像上运行。默认为 0.5
它的参数和返回值类似于官方函数 mediapipe.solutions.hands.Hands()
参数:
img: 需要检测关键点的帧图像,格式为BGR
draw: 是否需要在原图像上绘制关键点及识别框
flipType: 图像是否需要翻转,当视频图像和我们自己不是镜像关系时,设为True就可以了
返回值:
hands: 检测到的手部信息,包含:21个关键点坐标,检测框坐标及宽高,检测框中心坐标,检测出是哪一只手。
img: 返回绘制了关键点及连线后的图像
本节只创建键盘上的部分按键用于演示,定义类Button不需要一个一个单独绘制矩形按键。使用一个循环,分别对每个按键实例化,将实例化结果保存在 buttonList 列表中。在第(6)步绘制键盘时,逐个取出实例化对象,在窗口上绘制30个按键。
代码如下:
import cv2
from cvzone.HandTrackingModule import HandDetector # 导入手部检测模块
#(1)捕捉电脑摄像头
cap = cv2.VideoCapture(0) # 0代表自己的电脑摄像头,1代表外接摄像头
cap.set(3, 1280) # 设置显示框的宽1280
cap.set(4, 720) # 设置显示框的高720
#(2)接收手部检测方法
detector = HandDetector(mode=False, # 视频流图像
maxHands=1, # 最多检测一只手
detectionCon=0.5, # 最小检测置信度
minTrackCon=0.5) # 最小跟踪置信度
#(3)创建一个类用于构造键盘按键
class Button:
# 初始化,按键的左上坐标pos(列表类型),文本信息text(字符串类型),按键的宽高size
def __init__(self, pos:list, text:str, size=[90,90]):
# 分配属性
self.pos = pos
self.text = text
self.size = size
# 在类的内部定义方法
def draw(self, img):
x1, y1 = self.pos # 矩形框的左上角坐标
w, h = self.size # 矩形框的宽高
# img画板,矩形框左上角坐标,矩形框右下角坐标,颜色,-1代表颜色填充
cv2.rectangle(img, (x1, y1), (x1+w, y1+h), (255,0,0), -1)
# 美化一下矩形框
cv2.rectangle(img, (x1, y1), (x1+w, y1+h), (255,255,0), 4)
# 在矩形框上显示字符串信息
cv2.putText(img, self.text, (x1+25, y1+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
# 创建一个列表存放键盘上每个按键的文本信息
keys = [['Q','W','E','R','T','Y','U','I','O','P'],
['A','S','D','F','G','H','J','K','L',';'],
['Z','X','C','V','B','N','M',',','.','/']]
# 存放每一个按键的信息
buttonList = []
# 通过循环来实例化所有按键
for i in range(3): # 键盘文本列表有三行
# 分别实例化每一行的按钮信息
for x, key in enumerate(keys[i]): #返回每个元素的索引和值
# 确定每个按键的左上角坐标位置
px = 115*x + 30 + 40*i #水平方向每两个按键之间间隔115,每次换行缩进40,初始位置x=30
py = 115*i + 50 # 竖直方向每两个按键之间间隔115,初始位置y=50
# 将实例化后的对象存放在列表中
buttonList.append(Button([px, py], key))
# 每次换行后px坐标重置
x = 0
#(4)处理每一帧图像
while True:
# 返回是否读取成功,和读取的帧图像
success, img = cap.read()
# 翻转图像,让自己和摄像头呈镜像关系
img = cv2.flip(img, 1) # 1代表水平翻转,0代表上下翻转
#(5)检测手部关键点
# 检测手部关键点信息,返回手部信息hands,绘制关键点后的图像img
hands, img = detector.findHands(img, flipType=False)
#(6)绘制键盘
for i in range(3*len(keys[0])): # 一共有3行10列个按键
# 调用类中的绘图方法,显示每个键盘按键
buttonList[i].draw(img)
#(7)显示图像
cv2.imshow('video', img)
# 每帧图像滞留时间,ESC键退出
if cv2.waitKey(1) & 0xFF==27:
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
手部检测结果及虚拟键盘如下图所示。
从下面代码的第(6)步开始是锁定键盘按键的位置。通过 detector.findHands() 返回手部检测信息hands列表,由0或1或2个字典组成,如果检测到一只手就返回一个字典。字典中包含:lmList 代表21个手部关键点的像素坐标;bbox 代表检测框的左上角坐标和框的宽高;center 代表检测框的中心点的像素坐标;type 代表检测出的是左手还是右手。
只需要知道食指指尖坐标 lmList[8] 在哪个按键的范围内,并计算食指指尖和中指指尖之间的距离,距离小于某个值认为是点击按键。距离计算方法,detector.findDistance(pt1, pt2, img) 传入两个关键点坐标。返回值为 distance 代表两个关键点之间的距离, info 代表指尖连线的起点、中点、终点, img 代表绘制指尖连线后的图像。
在上述代码中补充:
import cv2
from cvzone.HandTrackingModule import HandDetector # 导入手部检测模块
#(1)捕捉电脑摄像头
cap = cv2.VideoCapture(0) # 0代表自己的电脑摄像头,1代表外接摄像头
cap.set(3, 1280) # 设置显示框的宽1280
cap.set(4, 720) # 设置显示框的高720
#(2)接收手部检测方法
detector = HandDetector(mode=False, # 视频流图像
maxHands=1, # 最多检测一只手
detectionCon=0.5, # 最小检测置信度
minTrackCon=0.5) # 最小跟踪置信度
#(3)创建一个类用于构造键盘按键
class Button:
# 初始化,按键的左上坐标pos(列表类型),文本信息text(字符串类型),按键的宽高size
def __init__(self, pos:list, text:str, size=[90,90]):
# 分配属性
self.pos = pos
self.text = text
self.size = size
# 在类的内部定义方法,默认内部深蓝色填充,边框为浅蓝色
def draw(self, img, colorIn, colorBd):
x1, y1 = self.pos # 矩形框的左上角坐标
w, h = self.size # 矩形框的宽高
# img画板,矩形框左上角坐标,矩形框右下角坐标,颜色,-1代表颜色填充
cv2.rectangle(img, (x1, y1), (x1+w, y1+h), colorIn, -1)
# 美化一下矩形框
cv2.rectangle(img, (x1, y1), (x1+w, y1+h), colorBd, 4)
# 在矩形框上显示字符串信息
cv2.putText(img, self.text, (x1+25, y1+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
# 创建一个列表存放键盘上每个按键的文本信息
keys = [['Q','W','E','R','T','Y','U','I','O','P'],
['A','S','D','F','G','H','J','K','L',';'],
['Z','X','C','V','B','N','M',',','.','/']]
# 存放每一个按键的信息
buttonList = []
# 矩形按键的颜色
colorIn = (255,0,0) # 按键内部填充的颜色
colorBd = (255,255,0) # 按键的边框颜色
# 通过循环来实例化所有按键
for i in range(3): # 键盘文本列表有三行
# 分别实例化每一行的按钮信息
for x, key in enumerate(keys[i]): #返回每个元素的索引和值
# 确定每个按键的左上角坐标位置
px = 115*x + 30 + 40*i #水平方向每两个按键之间间隔115,每次换行缩进40,初始位置x=30
py = 115*i + 50 # 竖直方向每两个按键指尖间隔115,初始位置y=50
# 将实例化后的对象存放在列表中
buttonList.append(Button([px, py], key))
# 每次换行后px坐标重置
x = 0
#(4)处理每一帧图像
while True:
# 返回是否读取成功,和读取的帧图像
success, img = cap.read()
# 翻转图像,让自己和摄像头呈镜像关系
img = cv2.flip(img, 1) # 1代表水平翻转,0代表上下翻转
#(5)绘制键盘
for i in range(3*len(keys[0])): # 一共有3行10列个按键
# 调用类中的绘图方法,显示每个键盘按键
buttonList[i].draw(img, colorIn, colorBd)
#(6)检测手部关键点
# 检测手部关键点信息,返回手部信息hands,绘制关键点后的图像img
hands, img = detector.findHands(img, flipType=False)
# 如果检测到手了,才执行下一步
if hands:
# 获取21个手部关键点信息
lmList = hands[0]['lmList']
# 获取食指指尖关键点坐标
x1, y1 = lmList[8]
# 获取中指指尖关键点坐标
x2, y2 = lmList[12]
#(7)遍历所有的按键,检查食指指尖在哪个按键的范围内
for index, button in enumerate(buttonList): # botton存放的是类实例化后的对象
# 所在矩形的左上坐标和宽高
x0, y0 = button.pos
w, h = button.size
# 如果食指指尖在某个矩形框中,改变键盘按键颜色
if x0<=x1<=x0+w and y0<=y1<=y0+h:
# 按键内部填充颜色
cv2.rectangle(img, (x0, y0), (x0+w, y0+h), (0,255,0), -1)
# 按键边框颜色
cv2.rectangle(img, (x0, y0), (x0+w, y0+h), (0,0,255), 4)
# 按键上的字符串
cv2.putText(img, button.text, (x0+25, y0+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
#(8)计算食指和中指指尖之间的距离
distance, _, img = detector.findDistance((x1,y1), (x2,y2), img)
# 如果指尖距离小于80,认为是点击按键
if distance<50:
# 点击按键改变按键颜色
cv2.rectangle(img, (x0, y0), (x0+w, y0+h), (0,0,255), -1)
# 按键上的字符串
cv2.putText(img, button.text, (x0+25, y0+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
#(9)显示图像
cv2.imshow('video', img)
# 每帧图像滞留时间,ESC键退出
if cv2.waitKey(1) & 0xFF==27:
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
显示结果如下,如果食指在某个按键矩形中,并且食指和中指之间的距离大于规定值,那么这个按键填充绿色,边界框变红色。食指在某个按键矩形中,并且食指和中指之间的距离小于规定值,那么这个按键和边界框都填充红色。
下面的第(9)步,为了验证点击按键时 opencv 画面上的点击内容和百度搜索框的输入内容是否同步,在opencv画面上创建一个矩形框用来显示字符。
通过 keyboard.press() 获得键盘响应,输入值是键盘上的某个字符,表示点击键盘上的该字符。
由于每一帧播放的非常快,可能只点击了一次按键,却打印出来很多相同的字符。使用休眠函数 time.sleep(t) ,每点击一次按键就暂停程序的执行,暂停 t 秒时间。
import cv2
from cvzone.HandTrackingModule import HandDetector # 导入手部检测模块
from time import sleep
from pynput.keyboard import Controller # 键盘控制单元
#(1)捕捉电脑摄像头
cap = cv2.VideoCapture(0) # 0代表自己的电脑摄像头,1代表外接摄像头
cap.set(3, 1280) # 设置显示框的宽1280
cap.set(4, 720) # 设置显示框的高720
#(2)接收手部检测方法
detector = HandDetector(mode=False, # 视频流图像
maxHands=1, # 最多检测一只手
detectionCon=0.5, # 最小检测置信度
minTrackCon=0.5) # 最小跟踪置信度
# 接收键盘控制单元
keyboard = Controller()
#(3)创建一个类用于构造键盘按键
class Button:
# 初始化,按键的左上坐标pos(列表类型),文本信息text(字符串类型),按键的宽高size
def __init__(self, pos:list, text:str, size=[90,90]):
# 分配属性
self.pos = pos
self.text = text
self.size = size
# 在类的内部定义方法,默认内部深蓝色填充,边框为浅蓝色
def draw(self, img, colorIn, colorBd):
x1, y1 = self.pos # 矩形框的左上角坐标
w, h = self.size # 矩形框的宽高
# img画板,矩形框左上角坐标,矩形框右下角坐标,颜色,-1代表颜色填充
cv2.rectangle(img, (x1, y1), (x1+w, y1+h), colorIn, -1)
# 美化一下矩形框
cv2.rectangle(img, (x1, y1), (x1+w, y1+h), colorBd, 4)
# 在矩形框上显示字符串信息
cv2.putText(img, self.text, (x1+25, y1+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
# 创建一个列表存放键盘上每个按键的文本信息
keys = [['Q','W','E','R','T','Y','U','I','O','P'],
['A','S','D','F','G','H','J','K','L',';'],
['Z','X','C','V','B','N','M',',','.','/']]
# 保存最终的输出结果
finalText = ''
# 存放每一个按键的信息
buttonList = []
# 矩形按键的颜色
colorIn = (255,0,0) # 按键内部填充的颜色
colorBd = (255,255,0) # 按键的边框颜色
# 通过循环来实例化所有按键
for i in range(3): # 键盘文本列表有三行
# 分别实例化每一行的按钮信息
for x, key in enumerate(keys[i]): #返回每个元素的索引和值
# 确定每个按键的左上角坐标位置
px = 115*x + 30 + 40*i #水平方向每两个按键之间间隔115,每次换行缩进40,初始位置x=30
py = 115*i + 50 # 竖直方向每两个按键指尖间隔115,初始位置y=50
# 将实例化后的对象存放在列表中
buttonList.append(Button([px, py], key))
# 每次换行后px坐标重置
x = 0
#(4)处理每一帧图像
while True:
# 返回是否读取成功,和读取的帧图像
success, img = cap.read()
# 翻转图像,让自己和摄像头呈镜像关系
img = cv2.flip(img, 1) # 1代表水平翻转,0代表上下翻转
#(5)绘制键盘
for i in range(3*len(keys[0])): # 一共有3行10列个按键
# 调用类中的绘图方法,显示每个键盘按键
buttonList[i].draw(img, colorIn, colorBd)
#(6)检测手部关键点
# 检测手部关键点信息,返回手部信息hands,绘制关键点后的图像img
hands, img = detector.findHands(img, flipType=False)
# 如果检测到手了,才执行下一步
if hands:
# 获取21个手部关键点信息
lmList = hands[0]['lmList']
# 获取食指指尖关键点坐标
x1, y1 = lmList[8]
# 获取中指指尖关键点坐标
x2, y2 = lmList[12]
#(7)遍历所有的按键,检查食指指尖在哪个按键的范围内
for index, button in enumerate(buttonList): # botton存放的是类实例化后的对象
# 所在矩形的左上坐标和宽高
x0, y0 = button.pos
w, h = button.size
# 如果食指指尖在某个矩形框中,改变键盘按键颜色
if x0<=x1<=x0+w and y0<=y1<=y0+h:
# 按键内部填充颜色
cv2.rectangle(img, (x0, y0), (x0+w, y0+h), (0,255,0), -1)
# 按键边框颜色
cv2.rectangle(img, (x0, y0), (x0+w, y0+h), (0,0,255), 4)
# 按键上的字符串
cv2.putText(img, button.text, (x0+25, y0+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
#(8)计算食指和中指指尖之间的距离
distance, _, img = detector.findDistance((x1,y1), (x2,y2), img)
# 如果指尖距离小于50,认为是点击按键
if distance<50:
# 点击按键改变按键颜色
cv2.rectangle(img, (x0, y0), (x0+w, y0+h), (0,0,255), -1)
# 按键上的字符串
cv2.putText(img, button.text, (x0+25, y0+65), cv2.FONT_HERSHEY_COMPLEX, 1.8, (255,255,255), 3)
# 点击键盘上某个按键
keyboard.press(button.text) # 键盘上的某个符号,'A'
# 在文本框中显示该字符
finalText += button.text
# 点击一次后,0.2秒之后才能再点一次
sleep(0.2)
#(9)创建虚拟文本框
# 文本框内部
cv2.rectangle(img, (100, 450), (700, 550), (255,255,255), -1)
# 文本框边框
cv2.rectangle(img, (100, 450), (700, 550), (0,0,0), 5)
# 按键上的字符串
cv2.putText(img, finalText, (110, 525), cv2.FONT_HERSHEY_COMPLEX, 1.8, (0,0,0), 3)
#(10)显示图像
cv2.imshow('video', img)
# 每帧图像滞留时间,ESC键退出
if cv2.waitKey(1) & 0xFF==27:
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
显示结果如图,当食指在某个按键的范围内,并且指尖距离大于规定值,认为是搜索按键,按键变成绿色,边界框变成红色;如果指尖距离小于规定值,认为是点击按键,按键内部和边框都变成红色。使用百度搜索框测试,能够实现同步输入。