各位同学好,今天和大家分享一下如何使用MediaPipe+Opencv完成虚拟计算器,先放张图看效果。FPS值为29,食指和中指距离小于规定阈值则认为点击按键,为避免重复数字出现,规定每20帧可点击一次。
手部关键点检测的方法我之前已经详细写过,这里就直接使用,有不明白的可看我的这篇文章:【MediaPipe】(1) AI视觉,手部关键点实时跟踪,附python完整代码
# 安装工具包
pip install opencv-contrib-python # 安装opencv
pip install mediapipe # 安装mediapipe
# pip install mediapipe --user #有user报错的话试试这个
pip install cvzone # 安装cvzone
# 导入工具包
import cv2
from cvzone.HandTrackingModule import HandDetector
import mediapipe as mp
import time
21个手部关键点信息如下,本节我们主要研究食指指尖"8"和中指指尖"12"的坐标信息。
是已经编写好的检测方法,它的具体原理和我写的第一篇手部关键点检测的方法相同,想知道具体实现的方法可以去看一下。链接在文章开头。
参数:
mode: 默认为 False,将输入图像视为视频流。它将尝试在第一个输入图像中检测手,并在成功检测后进一步定位手的坐标。在随后的图像中,一旦检测到所有 max_num_hands 手并定位了相应的手的坐标,它就会跟踪这些坐标,而不会调用另一个检测,直到它失去对任何一只手的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 True,则在每个输入图像上运行手部检测,用于处理一批静态的、可能不相关的图像。
maxHands: 最多检测几只手,默认为2
detectionCon: 手部检测模型的最小置信值(0-1之间),超过阈值则检测成功。默认为 0.5
minTrackingCon: 坐标跟踪模型的最小置信值 (0-1之间),用于将手部坐标视为成功跟踪,不成功则在下一个输入图像上自动调用手部检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 static_image_mode 为 True,则忽略这个参数,手部检测将在每个图像上运行。默认为 0.5
它的参数和返回值类似于官方函数 mediapipe.solutions.hands.Hands()
参数:
img: 需要检测关键点的帧图像,格式为BGR
draw: 是否需要在原图像上绘制关键点及识别框
flipType: 图像是否需要翻转,当视频图像和我们自己不是镜像关系时,设为True就可以了
返回值:绘制关键点后的img帧图像;以及21个关键点的坐标信息(列表形式)
这里设置 maxHand=1 最多检测一只手,我们只需要一只手点计算器就可以了。
使用 cv2.flip() 函数翻转读取的摄像机图像,因为我们自己的右边相当于摄像机的左边,需要统一一下,变成镜像关系。指定参数 flipCode=0 竖向翻转,flipCode=1 水平翻转。
#(1)捕获摄像头
cap = cv2.VideoCapture(0)
cap.set(3, 1080) # 显示框的宽1080
cap.set(4, 720) # 显示框的高720
pTime = 0 # 设置第一帧开始处理的起始时间
# 手部检测方法,置信度为0.8,最多检测一只手
detector = HandDetector(detectionCon=0.8, maxHands=1)
#(2)处理每一帧图像
while True:
# 接收图片是否导入成功、帧图像
success, img = cap.read()
# 翻转图像,保证摄像机画面和人的动作是镜像
img = cv2.flip(img, flipCode=1) #0竖直翻转,1水平翻转
#(3)检测手部关键点,返回所有关键点的坐标和绘制后的图像
hands, img = detector.findHands(img, flipType=False)
# 查看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(20) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
效果图如下:
接下来我们需要在屏幕上创建一个计算器界面,定义一个类方法Button。计算器一共有16个按键,我们在 buttonListvalues 中存放每个按键的文本。利用两个for循环,让16个按键都经过button类初始化,暂时不绘图,把按键信息存放到 buttonList 中。初始化方法是在while循环开始之前就已经进行了,button类方法中的绘图方法draw()是在读取每一帧图像时进行,通过for循环绘制buttonList列表中每个按键元素。
因此我们在上述代码中补充。
# 创建按键类
class Button:
# 初始化,传入pos按键位置,每个矩形框的宽高,矩形框上的数字value
def __init__(self, pos, width, height, value):
# 初始化在while循环之前完成
self.pos = pos
self.width = width
self.height = height
self.value = value
# 绘图方法在while循环之后完成
def draw(self, img):
# 绘制计算器轮廓,img画板,起点坐标,终点坐标,颜色填充
cv2.rectangle(img, self.pos, (self.pos[0]+self.width, self.pos[1]+self.height),
(225,225,225), cv2.FILLED)
# 给计算器添加边框
cv2.rectangle(img, self.pos, (self.pos[0]+self.width, self.pos[1]+self.height),
(50,50,50), 3)
# 按键添加文本,img画板,文本内容,坐标,字体,字体大小,字体颜色,线条宽度
cv2.putText(img, self.value, (self.pos[0]+30,self.pos[1]+70),
cv2.FONT_HERSHEY_COMPLEX, 2, (50,50,50), 2)
#(1)捕获摄像头
cap = cv2.VideoCapture(0)
cap.set(3, 1280) # 显示框的宽1280
cap.set(4, 720) # 显示框的高720
pTime = 0 # 设置第一帧开始处理的起始时间
# ==1== 手部检测方法,置信度为0.8,最多检测一只手
detector = HandDetector(detectionCon=0.8, maxHands=1)
# ==2== 创建计算器按键
# 创建按钮内容列表
buttonListvalues = [['7', '8', '9', '*'],
['4', '5', '6', '-'],
['1', '2', '3', '+'],
['0', '/', '.', '=']]
buttonList = [] #存放每个按键的信息
# 创建4*4个按键
for x in range(4): # 四列
for y in range(4): # 四行
xpos = x * 100 + 800 #得到四块宽为100的矩形的起点x坐标,从x=800开始
ypos = y * 100 + 150 #起点y坐标
# 传入起点坐标及宽高
button1 = Button((xpos,ypos), 100, 100, buttonListvalues[y][x])
buttonList.append(button1) # 将确定坐标的矩形框信息存入列表中
#(2)处理每一帧图像
while True:
# 接收图片是否导入成功、帧图像
success, img = cap.read()
# 翻转图像,保证摄像机画面和人的动作是镜像
img = cv2.flip(img, flipCode=1) #0竖直翻转,1水平翻转
#(3)检测手部关键点,返回所有绘制后的图像
hands, img = detector.findHands(img, flipType=False)
#(4)绘制计算器
# 绘制计算器显示结果的部分,四个按键的宽合起来是400
cv2.rectangle(img, (800, 50), (800+400, 70+100), (225,225,225), cv2.FILLED)
# 结果框轮廓
cv2.rectangle(img, (800, 50), (800+400, 70+100), (50,50,50), 3)
# 遍历列表,调用类中的draw方法,绘制每个按键
for button in buttonList:
button.draw(img)
# 查看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(20) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
结果如下:
补充上述代码,从第(5)部分开始,detector.findDistance() 函数传入两个关键点的xy坐标和img画板,返回两点之间的长度length,绘制后的图像img。在这里主要研究食指,如果食指在某个按键框内,并且食指和中指的距离小于50,那么就认为是点击按键。
因此我们需要新建一个类方法 checkClick 来判断,食指和中指接触时,食指在哪个位置。依次遍历 buttonList 中的16个按键框信息,和当前食指所在位置(x,y),如果 (x, y) 在某个按键框内,即 x1 < x < x1 + width 且 y1 < y < y1 + height,那么就改变这个按键的颜色,表明已经点击,类方法 checkClick 返回True。因此,通过按键 buttonListvalues 的索引,我们就找到了我们点下去的是哪一个字符。
我们每帧图像点击按键,得到的字符串的各种组合,存放在 myEquation 变量中,如 '5 * 6 - 2'。当我们点击 '=' 号时,得到的应该是一个长的像数值的字符串,而不是数值表达式组成的字符串。这时,使用 eval() 函数,它会将长得像数值运算的字符串当作数值来计算,返回一个数值。再把它变成字符串文本显示出来就可以了。
由于图像每一帧的刷新的很快,可能我们点了一次2,就显示出来十几个2。因此我们需要设置一个延时器 delayCounter,只有当它为0时我们才能再点击,避免出现每一帧我们都点击按键,这里设置为50帧,每50帧点击一次。if delayCounter > 50: delayCounter = 0
如果想清空结果框怎么办呢,方法如下,只要在英文模式下点击键盘上的c键就可以了,key = cv2.waitKey(1); if key == ord('c'): myEquation = ' '
import cv2
from cvzone.HandTrackingModule import HandDetector
import mediapipe as mp
import time
# 创建按键类
class Button:
# 初始化,传入pos按键位置,每个矩形框的宽高,矩形框上的数字value
def __init__(self, pos, width, height, value):
# 初始化在while循环之前完成
self.pos = pos
self.width = width
self.height = height
self.value = value
# 绘图方法在while循环之后完成
def draw(self, img):
# 绘制计算器轮廓,img画板,起点坐标,终点坐标,颜色填充
cv2.rectangle(img, self.pos, (self.pos[0]+self.width, self.pos[1]+self.height),
(225,225,225), cv2.FILLED)
# 给计算器添加边框
cv2.rectangle(img, self.pos, (self.pos[0]+self.width, self.pos[1]+self.height),
(50,50,50), 3)
# 按键添加文本,img画板,文本内容,坐标,字体,字体大小,字体颜色,线条宽度
cv2.putText(img, self.value, (self.pos[0]+30,self.pos[1]+70),
cv2.FONT_HERSHEY_COMPLEX, 2, (50,50,50), 2)
# 点击按钮
def checkClick(self, x, y): #传入食指尖坐标
# 检查食指x坐标在哪一个按钮框内,x1 < x < x1 + width ,控制一列
# 检查食指y坐标在哪一个按钮框内,y1 < y < y1 + height ,控制一行
if self.pos[0] < x < self.pos[0] + self.width and \
self.pos[1] < y < self.pos[1] + self.height: # '\'用来换行
# 如果点击按钮就改变按钮颜色
cv2.rectangle(img, self.pos, (self.pos[0]+self.width, self.pos[1]+self.height),
(0,255,0), cv2.FILLED)
# 边框还是原来的不变
cv2.rectangle(img, self.pos, (self.pos[0]+self.width, self.pos[1]+self.height),
(50,50,50), 3)
# 按键文本变颜色,面积变化
cv2.putText(img, self.value, (self.pos[0]+30, self.pos[1]+70),
cv2.FONT_HERSHEY_COMPLEX, 2, (0,0,255), 5)
# 如果成功点击按钮就返回True
return True
else:
return False
#(1)捕获摄像头
cap = cv2.VideoCapture(0)
cap.set(3, 1280) # 显示框的宽1280
cap.set(4, 720) # 显示框的高720
pTime = 0 # 设置第一帧开始处理的起始时间
# ==1== 手部检测方法,置信度为0.8,最多检测一只手
detector = HandDetector(detectionCon=0.8, maxHands=1)
# ==2== 创建计算器按键
# 创建按钮内容列表
buttonListvalues = [['7', '8', '9', '*'],
['4', '5', '6', '-'],
['1', '2', '3', '+'],
['0', '/', '.', '=']]
buttonList = [] #存放每个按键的信息
# 创建4*4个按键
for x in range(4): # 四列
for y in range(4): # 四行
xpos = x * 100 + 800 #得到四块宽为100的矩形的起点x坐标,从x=800开始
ypos = y * 100 + 150 #起点y坐标
# 传入起点坐标及宽高
button1 = Button((xpos,ypos), 100, 100, buttonListvalues[y][x])
buttonList.append(button1) # 将确定坐标的矩形框信息存入列表中
# ==3== 初始化结果显示框
myEquation = ''
# eval('5'+'5') ==> 10,eval()函数将数字字符串转换成数字计算
delayCounter = 0 #添加计数器,一次点击触发一次按钮,避免重复
#(2)处理每一帧图像
while True:
# 接收图片是否导入成功、帧图像
success, img = cap.read()
# 翻转图像,保证摄像机画面和人的动作是镜像
img = cv2.flip(img, flipCode=1) #0竖直翻转,1水平翻转
#(3)检测手部关键点,返回所有绘制后的图像
hands, img = detector.findHands(img, flipType=False)
#(4)绘制计算器
# 绘制计算器显示结果的部分,四个按键的宽合起来是400
cv2.rectangle(img, (800, 50), (800+400, 70+100), (225,225,225), cv2.FILLED)
# 结果框轮廓
cv2.rectangle(img, (800, 50), (800+400, 70+100), (50,50,50), 3)
# 遍历列表,调用类中的draw方法,绘制每个按键
for button in buttonList:
button.draw(img)
#(5)检测手按了哪个键
if hands: #如果手部关键点返回的列表不为空,证明检测到了手
# 0代表第一只手,由于我们设置了只检测一只手,所以0就代表检测到的那只
lmlist = hands[0]['lmList']
# 获取食指和中指的指尖距离并绘制连线
# 返回指尖连线长度,线条信息,绘制后的图像
length, _, img = detector.findDistance(lmlist[8], lmlist[12], img)
# print(length)
x, y = lmlist[8] # 获取食指坐标
# 如果指尖距离小于50,找到按下了哪个键
if length < 50:
for i, button in enumerate(buttonList): # 遍历所有按键,找到食指尖在哪个按键内
# 点击按键,按键颜色面积发生变化,返回True。并且延时器为0才能运行
if button.checkClick(x,y) and delayCounter==0:
#(6)数值计算
# 找到点击的按钮的编号i,i是0-15,
# 如"4",索引为4,位置[1][0],等同于[i%4][i//4]
# print(buttonListvalues[i%4][i//4])
myValue = buttonListvalues[i%4][i//4]
# 如果点的是'='号
if myValue == '=':
# eval()使字符串数字和符号直接做计算, eval('5 * 6 - 2')
myEquation = str(eval(myEquation)) #eval返回一个数值
else:
# 第一次点击"5",第二次点击"6",需要显示的是56
myEquation += myValue # 字符串直接相加
# 避免重复,方法一,不推荐:
# time.sleep(0.2)
delayCounter = 1 # 启动计数器,一次运行点击了一个键
#(7)避免点一次出现多个相同数,方法二:
# 点击一个按钮之后,delayCounter=1,20帧后才能点击下一个
if delayCounter != 0:
delayCounter += 1 # 延迟一帧
if delayCounter > 50: # 10帧过去了才能再点击
delayCounter = 0
#(8)绘制显示的计算表达式
cv2.putText(img, myEquation, (800+10,100+20), cv2.FONT_HERSHEY_PLAIN,
3, (50,50,50), 3)
# 查看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)
# 每帧滞留时间
key = cv2.waitKey(1)
# 清空计算器框
if key == ord('c'):
myEquation = ''
# 退出显示
if key & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
没点击按钮时:
点击按钮时: