使用Python基于OpenCV+MediaPipe追踪手势并控制音量

Posted WakingHours

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Python基于OpenCV+MediaPipe追踪手势并控制音量相关的知识,希望对你有一定的参考价值。

写在前面

说明

  下文中所有代码均没有封装到函数或者类(模块)中, 因为笔者发现将代码封装到类的后, 再通过main(测试)调用类时,这样代码会更加美观, 可读性更强, 但是事实上效果却不尽如意, 在笔者测试的过程中手部识别追踪手部的效果都不如直接写在一个main函数中来的稳定.

  并且调用类所产生的追踪手部图像(img)和标号(mark)变得及其不稳定, 并且有极大的概率出现追踪(tracking)手部的时候失效的情况.

笔者也不知道出现该现象的原因, 希望有大佬可以指明方向.

简介

1.OpenCV简介

OpenCV是一个基于BSD许可(开源)发行的跨平台的计算机视觉机器学习的软件库, 它实现了图像处理计算机视觉方向的很多通用算法,它由C++语言编写, 它具有C++, Java, Python和MATLAB接口,可以运行在Linux, Windows, android和Mac OS操作系统上运行.
它提供了很多图像操作,并且是标准的计算机视觉API

OpenCv中文官方文档: http://www.woshicver.com/

2.MediaPipe简介

MediaPipe是谷歌开源的多面体机器学习框架, 里面包含了很多各种各样的模型, 谷歌都已经训练好了, 我们只需调用即可.
MediaPipe 的核心框架由 C++ 实现,主要概念包括Packet、Stream、Calculator、Graph以及子图Subgraph。数据包是最基础的数据单位,一个数据包代表了在某一特定时间节点的数据,例如一帧图像或一小段音频信号;数据流是由按时间顺序升序排列的多个数据包组成,一个数据流的某一特定Timestamp只允许至多一个数据包的存在;而数据流则是在多个计算单元构成的图中流动。MediaPipe 的图是有向的——数据包从Source Calculator或者 Graph Input Stream流入图直至在Sink Calculator 或者 Graph Output Stream离开。

3.配置环境

开发环境

Python 3.8.5
VSCode或pycharm

所需的库

opencv-python 		4.5.3.56
mediapipe			0.8.7.3
pycaw 				20181226 
numpy				1.19.5			
PyAutoGUI 			0.9.52		[可选].用于做一些自动化操作

安装库文件只需要使用 pip install xxx 命令即可.
当然, 在最后的GitHub链接部分, 我也会给出所需的requestment.txt

最终效果演示

手部21节点说明

    通过标号,我们就可以实时追踪确定的手指





开始开发

初始化部分

初始化流,初始化变量,初始化声音模块,以及后期映射的部分

#################################
# 相机参数
wCam, hCam = 640, 480

videoCap = cv2.VideoCapture(0)
videoCap.set(3, wCam)  # 设置摄像头高度
videoCap.set(4, hCam)  # 设置这项头高度

#################################
# 显示FPS
cTime = 0
pTime = 0

##################################
# mediapipe模块初始化
mode = False
maxHands = 2
detectionCon = 0.5
trackCon = 0.5
mpHands = mp.solutions.hands
hands = mpHands.Hands(mode, maxHands, detectionCon, trackCon)
mpDraw = mp.solutions.drawing_utils

################################
# 音量模块初始化
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(
    IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))
# volume.GetMute() # 静音
# volume.GetMasterVolumeLevel() # 主音量等级
# volume.GetVolumeRange() # 主音量范围
# volume.SetMasterVolumeLevel(-20.0, None) # 设置主音量
# print(volume.GetVolumeRange()) #  (-63.5, 0.0, 0.5)

#################################
# 获取当前音量范围(range)和最大最小音量
volumeRange = volume.GetVolumeRange()  # 获取当前主音量范围
minVol = volumeRange[0]
maxVol = volumeRange[1]
# 后面映射部分需要用到
vol = 0
volBar = 400
volPer = 0
#################################

findHands函数

思路

对获取的摄像头,通过cv2.read()读取每一帧图像,然后对其加工,若存在手,则遍历,最后通过画图工具对img进行画出.

实现

    # 用来追踪发现并且追踪手部
    def findHands(self, img, draw=True):  # 对传入的图像, 是否draw
        self.results = self.hands.process(img) # 对每帧图像进行加工
        if self.results.multi_hand_landmarks:  # 检测到手, 并且返回标号
            for oneHand in self.results.multi_hand_landmarks: # 遍历所有手(maxHand)
                if draw: # 是否标记出
                    self.mpDraw.draw_landmarks(img, oneHand, self.mpHands.HAND_CONNECTIONS)
        return img  # 返回加工好的图像

getLmList函数

思路

遍历枚举后的标号, 返回的x,y是针对整个画幅的比例, 所以乘以画幅, 就可以得到具体的位置坐标

实现

def getMarkList(result_):
    mark_list = []  # 初始化空列表
    if result_.multi_hand_landmarks: # 如果有标号(检测到手)
        oneHand = result_.multi_hand_landmarks[0]  # 只检测一个手
        for num, local in enumerate(oneHand.landmark):  # 遍历枚举
            h, w, c = img.shape # 获取画幅
            local_x, local_y = int(local.x * w), int(local.y * h) # 比例放大, 得到位置
            mark_list.append([num, local_x, local_y]) # 添加到mark_List
    return mark_list

volumeControl函数

思路

用getMarkList函数获取的标号列表, 通过索引获取想要检测的标号, 然后检测两指间的距离, 映射到音量模块上, 从而实现控制音量的效果.

注意: cv中的色彩是GBR

实现

def volumeControl(img):
    if len(markList) != 0:  # 如果检测出点了
        # print(lmList[4], lmList[8d10, (122, 255, 0), cv2.FILLED)
        thumb_x, thumb_y = markList[4][1], markList[4][2]
        index_x, index_y = markList[8][1], markList[8][2]
        little_x, little_y = markList[20][1], markList[20][2]
        cx, cy = (thumb_x + index_x) // 2, (thumb_y + index_y) // 2  # 找到拇指和食指的中心

        # 高亮我们想要检测的标号
        cv2.circle(img, (thumb_x, thumb_y), 10, (122, 255, 0), cv2.FILLED)
        cv2.circle(img, (index_x, index_y), 10, (122, 255, 0), cv2.FILLED)
        cv2.circle(img, (little_x, little_y), 10, (0, 255, 0), cv2.FILLED)
        cv2.circle(img, (cx, cy), 6, (122, 255, 0), cv2.FILLED)

        # 然后我们来在我们想标记的中间画根线
        cv2.line(img, (thumb_x, thumb_y), (little_x, little_y), (0, 255, 255), 2)
        cv2.line(img, (thumb_x, thumb_y), (index_x, index_y), (255, 0, 255), 3)  # 参数分别是: 要显示到的图像, 坐标1, 坐标2, BGR, 厚度

        # 那么我们如何获取长度呢, 很简单嘛, 空间中的欧几里得范数:sqrt(x*x+y*y), math.hypot()就可以直接计算出
        thumb_index_distance = math.hypot(index_x - thumb_x, index_y - thumb_y)
        little_thumb_distance = math.hypot(little_x - thumb_x, little_y - thumb_y)
        # print(thumb_index_distance)  # 打印长度, 看两根手指最大和最小的范围

        # 接下来我们就要改变系统音量了: pycaw在github中可以找到
        # 手指的长度范围: Hand range: 20~200
        # 声音的范围: Volume Range: -65~0 ( 0为最大音量)
        # 我们需要一个映射, 这里就用到了numpy.interp()
        vol = np.interp(thumb_index_distance, [20, 160], [minVol, maxVol])  # 音量的转换
        volBar = np.interp(thumb_index_distance, [20, 160], [400, 150])  # 音量条的转换
        volPer = np.interp(thumb_index_distance, [20, 160], [0, 100])  # 转换百分比

        # print(int(thumb_index_distance), vol)

        # 此时我们就可以控制我们的音量了
        volume.SetMasterVolumeLevel(vol, None)  # 设置主音量

        # 我们最后一件事情能够做的就是显示音量:
        # cv2.putText(img,f"volume: {vol}",(0,80),cv2.FONT_HERSHEY_COMPLEX,1,(0,255,255))

        cv2.rectangle(img, (30, 150), (60, 400), (255, 255, 20), 3)  # 画一个矩形
        cv2.rectangle(img, (30, int(volBar)), (60, 400), (255, 255, 20), cv2.FILLED)  # 填充矩形

        cv2.putText(img, f"{str(int(volPer))}%", (20, 430), cv2.FONT_HERSHEY_PLAIN, 2,  # 位置, 字体, 比例
                    (255, 255, 20), 2)  # BGR颜色, 线的宽度

        # 根据你的检测精度和距离, 合适的设定你的阈值
        if thumb_index_distance <= 25:  # 当长度<=25, 我认为食指和拇指一块了
            cv2.circle(img, (cx, cy), 10, (0, 50, 255), cv2.FILLED)  # 当检测到合在一起了, 就改变颜色
            pyautogui.hotkey('ctrl', 'alt', 'right')  # 网易云热键热键:切歌
            # time.sleep(1) # 等待, 不要重复切换热键
            cv2.waitKey(200)
        # 暂停
        if little_thumb_distance <= 25:  # 过小时, 就在暂停图像,等待恢复
            # cv2.waitKey(0)
            time.sleep(2)

dispFPS函数

显示FPS的函数,除以当前时间帧,显示FPS

def dispFPS(img):
    global cTime, pTime
    # 显示FPS
    cTime = time.time()
    fps = 1 / (cTime - pTime)
    pTime = cTime
    cv2.putText(img, f"FPS: {str(int(fps))}", (0, 30), cv2.FONT_HERSHEY_PLAIN, 2,  # 位置, 字体, 比例
                (10, 255, 0), 2)  # BGR颜色, 线的宽度

最后

我仍然在优化代码,希望感兴趣的朋友可以一起交流
请多给我的GitHub点点star,我将感激不尽


GitHub连接: https://github.com/ComputerVision

本次代码在 VolumeControl中可以找到

联系方式:WakingHoursHUC@outlook.com

以上是关于使用Python基于OpenCV+MediaPipe追踪手势并控制音量的主要内容,如果未能解决你的问题,请参考以下文章

Python/OpenCV - 基于机器学习的 OCR(图像到文本)

使用Python基于OpenCV+MediaPipe追踪手势并控制音量

基于Opencv-python人脸口罩检测

基于Opencv-python人脸口罩检测(附完整代码)

基于Opencv-python人脸口罩检测(附完整代码)

opencv系列之基于NVIDIA显卡的opencv-python硬解方案