【机器视觉案例】(5) AI视觉,远程手势控制虚拟计算器,附python完整代码

各位同学好,今天和大家分享一下如何使用MediaPipe+Opencv完成虚拟计算器,先放张图看效果。FPS值为29,食指和中指距离小于规定阈值则认为点击按键,为避免重复数字出现,规定每20帧可点击一次。

手部关键点检测的方法我之前已经详细写过,这里就直接使用,有不明白的可看我的这篇文章【MediaPipe】(1) AI视觉,手部关键点实时跟踪,附python完整代码


1. 导入工具包

# 安装工具包
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"的坐标信息。


2. 相关函数介绍

(1)cvzone.HandTrackingModule.HandDetector()    手部关键点检测方法

是已经编写好的检测方法,它的具体原理和我写的第一篇手部关键点检测的方法相同,想知道具体实现的方法可以去看一下。链接在文章开头。

参数:

mode: 默认为 False,将输入图像视为视频流。它将尝试在第一个输入图像中检测手,并在成功检测后进一步定位手的坐标。在随后的图像中,一旦检测到所有 max_num_hands 手并定位了相应的手的坐标,它就会跟踪这些坐标,而不会调用另一个检测,直到它失去对任何一只手的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 True,则在每个输入图像上运行手部检测,用于处理一批静态的、可能不相关的图像。

maxHands: 最多检测几只手,默认为2

detectionCon: 手部检测模型的最小置信值(0-1之间),超过阈值则检测成功。默认为 0.5

minTrackingCon: 坐标跟踪模型的最小置信值 (0-1之间),用于将手部坐标视为成功跟踪,不成功则在下一个输入图像上自动调用手部检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 static_image_modeTrue,则忽略这个参数,手部检测将在每个图像上运行。默认为 0.5

它的参数和返回值类似于官方函数 mediapipe.solutions.hands.Hands()

(2)cvzone.HandTrackingModule.HandDetector.findHands()    找到手部关键点并绘图

参数:

img: 需要检测关键点的帧图像,格式为BGR

draw: 是否需要在原图像上绘制关键点及识别框

flipType: 图像是否需要翻转,当视频图像和我们自己不是镜像关系时,设为True就可以了

返回值:绘制关键点后的img帧图像;以及21个关键点的坐标信息(列表形式)


3. 检测手部信息

这里设置 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()

效果图如下:


4. 创建虚拟计算器

接下来我们需要在屏幕上创建一个计算器界面,定义一个类方法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. 激活按钮完成数学计算

补充上述代码,从第(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()

没点击按钮时:

点击按钮时:

  • 7
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

立Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值