前段时间社团布置了一个手势识别控制电脑音量的小任务,今天记录一下学习过程,将大佬作品在我的贫瘠的基础上解释一下~
项目主要由以下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.<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()