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