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

avatar
作者
筋斗云
阅读量:0

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

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

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

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

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

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

最终的效果是这样的:

库 

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

opencv  

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

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

mediapipe

一个新朋友! 

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

python中的一些标准库 

time 

  (1)、time库概述

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

         import time

         time.<b>()

  (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 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()

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!