opencv+mediapipe 手势识别控制电脑音量(详细注释解析)

       前段时间社团布置了一个手势识别控制电脑音量的小任务,今天记录一下学习过程,将大佬作品在我的贫瘠的基础上解释一下~ 

opencv+mediapipe 手势识别控制电脑音量(详细注释解析)_第1张图片

项目主要由以下4个步骤组成:

1、使用OpenCV读取摄像头视频流

2、识别手掌关键点像素坐标

3、根据拇指和食指指尖的坐标,利用勾股定理计算距离

4、将距离等比例转为音量大小,控制电脑音量

最终的效果是这样的:

库 

首先介绍一下应用的几个库

opencv  

OpenCV是Intel开源计算机视觉库。OpenCV的全称是:Open Source Computer Vision Library

对于这个,我们应该已经不再陌生了,毕竟已经学习了很久啦

mediapipe

一个新朋友! 

MediaPipe是一个用于构建机器学习管道的框架,用于处理视频、音频等时间序列数据。MediaPipe依赖OpenCV来处理视频,FFMPEG来处理音频数据。它还有其他依赖项,如OpenGL/Metal、Tensorflow、Eigen等。 在这个例子中,将使用它来进行手势的识别。

python中的一些标准库 

time 

  (1)、time库概述

         time库是Python中处理时间的标准库

         import time

         time.()

  (2)、time库包含三类函数

         - 时间获取:time()   ctime()   gmtime()
         - 时间格式化:strftime()   strptime()
         - 程序计时:sleep()   perf_counter()

 math

内置数学类函数库,math库不支持复数类型,仅支持整数和浮点数运算。
math库一共提供了:

  • 4个数字常数
  • 44个函数,分为4类:
    16个数值表示函数
    8个幂对数函数
    16个三角对数函数
    4个高等特殊函数

 这两个库都需要使用保留字import使用

numpy 

这个库也是经常使用的,它的应用如下: 

  • 创建n维数组(矩阵)
  • 对数组进行函数运算,使用函数计算十分快速,节省了大量的时间,且不需要编写循环,十分方便
  • 数值积分、线性代数运算、傅里叶变换
  • ndarray快速节省空间的多维数组,提供数组化的算术运算和高级的 广播功能。1.3 对象
  • NumPy中的核心对象是ndarray
  • ndarray可以看成数组,存放 同类元素
  • NumPy里面所有的函数都是围绕ndarray展开的

 实例分部展示

 # 导入电脑音量控制模块,实现系统与音频接口的交互, 用于控制电脑音量

from ctypes import cast, POINTER

ctypes

模块ctypes是Python内建的用于调用动态链接库函数的功能模块,一定程度上可以用于Python与其他语言的混合编程。由于编写动态链接库,使用C/C++是最常见的方式,故ctypes最常用于Python与C/C++混合编程之中。

ctypes.cast(obj,type)此函数类似于C中的强制转换运算符。它返回一个新的类型实例,该实例指向与obj相同的内存块。type必须是指针类型,obj必须是可以解释为指针的对象。

POINTER 返回类型对象,用来给 restype 和 argtypes 指定函数的参数和返回值的类型用。

from comtypes import CLSCTX_ALL

comtypes

comtypes是一个轻量级的Python COM包,基于ctypes FFI库。
comtypes允许在纯Python中定义、调用和实现自定义和基于调度的COM接口。
此程序包仅适用于Windows。

from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume

在调节音量方面,上面3行经常一起出现,记下来就好 

#导入其他库

# 导入其他辅助库
import time
import math
# 重要的科学辅助库
import numpy as np

本例中,time用于⏲计时,math用于计算根号。

 # 定义一个名为HandControlVolume的类

class HandControlVolume:
    def __init__(self):
        # 初始化 medialpipe
        # 导入MediaPipe库中的绘图工具函数,用于在图像上绘制检测结果
        self.mp_drawing = mp.solutions.drawing_utils
        # 导入MediaPipe库中的绘图样式,用于定义绘制的颜色和线条风格
        self.mp_drawing_styles = mp.solutions.drawing_styles
        # 导入MediaPipe库中的手部检测模型
        self.mp_hands = mp.solutions.hands

#主函数

# 主函数
    def recognize(self):
        # 计算刷新率
        fpsTime = time.time()
        # OpenCV读取视频流,获取一个视频流对象
        cap = cv2.VideoCapture(1)
        # 视频分辨率
        resize_w = 720
        resize_h = 640
        # 画面显示初始化参数
        rect_height = 0
        rect_percent_text = 0

如果你的电脑是自带的摄像头,别忘了把videocapture的参数调整为0 ,我的是外接摄像头,所以参数是1

#调用mediapipe的Hands函数,输入手指关节检测的置信度和上一帧跟踪的置信度,输入最多检测手的数目,进行关节点检测 

 

# 调用mediapipe的Hands函数,输入手指关节检测的置信度和上一帧跟踪的置信度,输入最多检测手的数目,进行关节点检测
with self.mp_hands.Hands(min_detection_confidence=0.7,
                                 min_tracking_confidence=0.5,
                                 max_num_hands=2) as hands:
            # 只要摄像头保持打开,则循环运行程序
            while cap.isOpened():
                success, image = cap.read()#获取一帧当前图像,返回是否获取成功和图像数组(用numpy矩阵存储的照片)
                image = cv2.resize(image, (resize_w, resize_h))#修改图像大小

                if not success:#如果获取图像失败,则进入下一次循环
                    print("空帧.")
                    continue
                # 将图片格式设置为只读状态,可以提高图片格式转化的速度
                image.flags.writeable = False
                # 将BGR格式存储的图片转为RGB
                image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
                # 镜像处理
                image = cv2.flip(image, 1)
                # 将图像输入手指检测模型,得到结果
                results = hands.process(image)
                # 重新设置图片为可写状态,并转化会BGR格式
                image.flags.writeable = True
                image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

#当画面中检测到手掌

# 当画面中检测到手掌,results.multi_hand_landmarks值不为false
if results.multi_hand_landmarks:
    # 遍历每个手掌,注意是手掌,意思是可能存在多只手
    for hand_landmarks in results.multi_hand_landmarks:
        # 用最开始初始化的手掌画图函数及那个手指关节点画在图像上
        self.mp_drawing.draw_landmarks(
            image,#图像
            hand_landmarks,#手指信息
            self.mp_hands.HAND_CONNECTIONS,# 手指之间的连接关系
            self.mp_drawing_styles.get_default_hand_landmarks_style(), #手指样式
            self.mp_drawing_styles.get_default_hand_connections_style())#连接样式

        # 解析手指,存入各个手指坐标
        landmark_list = []#初始化一个列表来存储
        for landmark_id, finger_axis in enumerate(
                hand_landmarks.landmark):#便利某个手的每个关节
            landmark_list.append([
                landmark_id, finger_axis.x, finger_axis.y,
                finger_axis.z
            ])#将手指序号,像素点横、纵、深度坐标打包为一个列表,共同存入列表中

#检测到手指后:

# 列表非空,意为检测到手指
if landmark_list:
    # 获取大拇指指尖坐标,序号为4
    thumb_finger_tip = landmark_list[4]
    # 向上取整,得到手指坐标的整数
    thumb_finger_tip_x = math.ceil(thumb_finger_tip[1] * resize_w)#thumb_finger_tip[1]里存储的x值范围是0-1,乘以分辨率宽,便得到在图像上的位置
    thumb_finger_tip_y = math.ceil(thumb_finger_tip[2] * resize_h) #thumb_finger_tip[2]里存储的x值范围是0-1,乘以分辨率高,便得到在图像上的位置
    # 获取食指指尖坐标,序号为4,操作同理
    index_finger_tip = landmark_list[8]
    index_finger_tip_x = math.ceil(index_finger_tip[1] * resize_w)
    index_finger_tip_y = math.ceil(index_finger_tip[2] * resize_h)
    # 得到食指和拇指的中间点
    finger_middle_point = (thumb_finger_tip_x + index_finger_tip_x) // 2, (
            thumb_finger_tip_y + index_finger_tip_y) // 2
    # print(thumb_finger_tip_x)
    thumb_finger_point = (thumb_finger_tip_x, thumb_finger_tip_y)
    index_finger_point = (index_finger_tip_x, index_finger_tip_y)
    # 用opencv的circle函数画图,将食指、拇指和中间点画出
    image = cv2.circle(image, thumb_finger_point, 10, (255, 0, 255), -1)
    image = cv2.circle(image, index_finger_point, 10, (255, 0, 255), -1)
    image = cv2.circle(image, finger_middle_point, 10, (255, 0, 255), -1)
    # 用opencv的line函数将食指和拇指连接在一起
    image = cv2.line(image, thumb_finger_point, index_finger_point, (255, 0, 255), 5)
    # math.hypot为勾股定理计算两点长度的函数,得到食指和拇指的距离
    line_len = math.hypot((index_finger_tip_x - thumb_finger_tip_x),
                            (index_finger_tip_y - thumb_finger_tip_y))

 #获取电脑最大最小音量

    min_volume = self.volume_range[0]
    max_volume = self.volume_range[1]

  # 将指尖长度映射到音量上

  # np.interp为插值函数,简而言之,看line_len的值在[50,300]中所占比例,然后去[min_volume,max_volume]中线性寻找相应的值,作为返回值 

vol = np.interp(line_len, [50, 300], [min_volume, max_volume])
    # 将指尖长度映射到矩形显示上
    rect_height = np.interp(line_len, [50, 300], [0, 200])
    # 同理,通过line_len与[50,300]的比较,得到音量百分比     
    rect_percent_text = np.interp(line_len, [50, 300], [0, 100])
    # 用之前得到的vol值设置电脑音量

 #将音量显示在屏幕上

# 通过opencv的putText函数,将音量百分比显示到图像上
cv2.putText(image, str(math.ceil(rect_percent_text)) + "%", (10, 350),
            cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
# 通过opencv的rectangle函数,画出透明矩形框
image = cv2.rectangle(image, (30, 100), (70, 300), (255, 0, 0), 3)              # 通过opencv的rectangle函数,填充举行实心比例
image = cv2.rectangle(image, (30, math.ceil(300 - rect_height)), (70, 300), (255, 0, 0), -1)

# 显示刷新率FPS,cTime为程序一个循环截至的时间 

# 显示刷新率FPS,cTime为程序一个循环截至的时间
cTime = time.time()
fps_text = 1 / (cTime - fpsTime)# 计算频率
fpsTime = cTime# 将下一轮开始的时间置为这一轮循环结束的时间

 #音量显示的设置

# 显示帧率
cv2.putText(image, "FPS: " + str(int(fps_text)), (10, 70),
            cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
# 用opencv的函数显示摄像头捕捉的画面,以及在画面上写的字,画的框
cv2.imshow('MediaPipe Hands', image)
# 每次循环等待5毫秒,如果按下Esc或者窗口退出,这跳出循环
if cv2.waitKey(5) & 0xFF == 27 or cv2.getWindowProperty('MediaPipe Hands', cv2.WND_PROP_VISIBLE) < 1:
    break
# 释放对视频流的获取
cap.release()

#主程序

# 主程序,先初始化一个手掌获取实例,然后启动recognize函数即可
control = HandControlVolume()
control.recognize()

OK啦! 

opencv+mediapipe 手势识别控制电脑音量(详细注释解析)_第2张图片 

 完整代码如下:

# 导入OpenCV
import cv2

# 导入mediapipe,用于手部关键点检测和手势识别
'''
    敲桌子!!!(核心关键库)
'''
# 无法使用GPU加速,因为此库不支持该操作
import mediapipe as mp

# 导入电脑音量控制模块,实现系统与音频接口的交互
from ctypes import cast, POINTER
from comtypes import CLSCTX_ALL

# 用于控制电脑音量
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume

# 导入其他辅助库
import time
import math
# 重要的科学辅助库
import numpy as np


class HandControlVolume:
    def __init__(self):
        # 初始化 medialpipe
        # 导入MediaPipe库中的绘图工具函数,用于在图像上绘制检测结果
        self.mp_drawing = mp.solutions.drawing_utils
        # 导入MediaPipe库中的绘图样式,用于定义绘制的颜色和线条风格
        self.mp_drawing_styles = mp.solutions.drawing_styles
        # 导入MediaPipe库中的手部检测模型
        self.mp_hands = mp.solutions.hands

        # 获取电脑音量范围

        # 获取系统的音频输出设备(扬声器)
        devices = AudioUtilities.GetSpeakers()
        # 激活音频输出设备上的音量控制接口
        interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
        # 将激活的音量控制接口转换为指针类型,并赋给实例变量volume,以方便后续使用
        self.volume = cast(interface, POINTER(IAudioEndpointVolume))
        # 将音量控制对象的静音状态设置为关闭(0表示关闭,1表示打开)
        self.volume.SetMute(0, None)
        # 通过音量控制接口的GetVolumeRange()方法获取音量控制对象的音量范围(最小值和最大值)
        self.volume_range = self.volume.GetVolumeRange()
# 主函数
    def recognize(self):
        # 计算刷新率
        fpsTime = time.time()
        # OpenCV读取视频流,获取一个视频流对象
        cap = cv2.VideoCapture(1)
        # 视频分辨率
        resize_w = 720
        resize_h = 640
        # 画面显示初始化参数
        rect_height = 0
        rect_percent_text = 0
        # 使用MediaPipe库中的Hands模型进行手部检测和跟踪。
        with self.mp_hands.Hands(min_detection_confidence=0.7,
                                 min_tracking_confidence=0.5,
                                 max_num_hands=2) as hands:
            # 循环读取视频帧,直到视频流结束
            while cap.isOpened():
                success, image = cap.read()
                # 将图像调整为指定的分辨率
                image = cv2.resize(image, (resize_w, resize_h))

                # 防止摄像头掉线出现报错
                if not success:
                    print("空帧.")
                    continue
                # 提高性能
                image.flags.writeable = False
                # BGR转为RGB
                image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
                # 镜像
                image = cv2.flip(image, 1)
                # mediapipe模型处理
                results = hands.process(image)

                # 将图像的可写标志image.flags.writeable设置为True,以重新启用对图像的写入操作
                image.flags.writeable = True
                # RGB转为BGR
                image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
                # 判断是否有手掌
                if results.multi_hand_landmarks:
                    # 遍历每个手掌
                    for hand_landmarks in results.multi_hand_landmarks:
                        # 在画面标注手指
                        self.mp_drawing.draw_landmarks(
                            image,
                            hand_landmarks,
                            # 指定要绘制的手部关键点之间的连接线
                            self.mp_hands.HAND_CONNECTIONS,
                            # 获取默认的手部关键点绘制样式
                            self.mp_drawing_styles.get_default_hand_landmarks_style(),
                            # 获取默认的手部连接线绘制样式
                            self.mp_drawing_styles.get_default_hand_connections_style())
                        # 解析手指,存入各个手指坐标
                        landmark_list = []
                        # 遍历每个手部关键点的索引和对应的坐标值
                        for landmark_id, finger_axis in enumerate(hand_landmarks.landmark):
                            landmark_list.append([landmark_id, finger_axis.x, finger_axis.y, finger_axis.z])
                        if landmark_list:
                            # 获取大拇指指尖坐标
                            thumb_finger_tip = landmark_list[4]
                            thumb_finger_tip_x = math.ceil(thumb_finger_tip[1] * resize_w)
                            thumb_finger_tip_y = math.ceil(thumb_finger_tip[2] * resize_h)
                            # 获取食指指尖坐标
                            index_finger_tip = landmark_list[8]
                            index_finger_tip_x = math.ceil(index_finger_tip[1] * resize_w)
                            index_finger_tip_y = math.ceil(index_finger_tip[2] * resize_h)
                            # 中间点
                            finger_middle_point = (thumb_finger_tip_x + index_finger_tip_x) // 2, (thumb_finger_tip_y + index_finger_tip_y) // 2

                            thumb_finger_point = (thumb_finger_tip_x, thumb_finger_tip_y)
                            index_finger_point = (index_finger_tip_x, index_finger_tip_y)
                            # 画指尖2点
                            image = cv2.circle(image, thumb_finger_point, 10, (255, 0, 255), -1)
                            image = cv2.circle(image, index_finger_point, 10, (255, 0, 255), -1)
                            image = cv2.circle(image, finger_middle_point, 10, (255, 0, 255), -1)
                            # 画2点连线
                            image = cv2.line(image, thumb_finger_point, index_finger_point, (255, 0, 255), 5)
                            # 勾股定理计算长度
                            line_len = math.hypot((index_finger_tip_x - thumb_finger_tip_x), (index_finger_tip_y - thumb_finger_tip_y))

                            # 获取电脑最大最小音量
                            min_volume = self.volume_range[0]
                            max_volume = self.volume_range[1]
                            # 将指尖长度映射到音量上
                            vol = np.interp(line_len, [50, 300], [min_volume, max_volume])
                            # 将指尖长度映射到矩形显示上
                            rect_height = np.interp(line_len, [50, 300], [0, 200])
                            rect_percent_text = np.interp(line_len, [50, 300], [0, 100])

                            # 设置电脑音量
                            self.volume.SetMasterVolumeLevel(vol, None)
                # 显示矩形
                cv2.putText(image, str(math.ceil(rect_percent_text)) + "%", (10, 350),
                            cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
                image = cv2.rectangle(image, (30, 100), (70, 300), (255, 0, 0), 3)
                image = cv2.rectangle(image, (30, math.ceil(300 - rect_height)), (70, 300), (255, 0, 0), -1)

                # 显示刷新率FPS
                cTime = time.time()
                fps_text = 1 / (cTime - fpsTime)
                fpsTime = cTime
                cv2.putText(image, "FPS: " + str(int(fps_text)), (10, 70),
                            cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
                # 显示画面
                cv2.imshow('MediaPipe Hands', image)
                if cv2.waitKey(5) & 0xFF == 27 or cv2.getWindowProperty('MediaPipe Hands', cv2.WND_PROP_VISIBLE) < 1:
                    break
            cap.release()


# 开始程序
control = HandControlVolume()
control.recognize()

你可能感兴趣的:(opencv,人工智能,计算机视觉)