各位同学好,今天和大家分享一下如何使用 opencv+Mediapipe 通过手势识别来演示PPT,先放张图看效果。
当只有大拇指翘起时,向左翻页;当只有小拇指翘起时,向右翻页;当食指和中指翘起时,表示鼠标指针移动,如图中黄色指针;当只有食指翘起时,红色线条绘制板书;当所有手指都弯曲时,擦除板书。
1. 安装工具包
pip install opencv_python==4.2.0.34 # 安装opencv
pip install mediapipe # 安装mediapipe
# pip install mediapipe --user #有user报错的话试试这个
pip install cvzone # 安装cvzone
# 导入工具包
import cv2
import cvzone
import numpy as np
from cvzone.HandTrackingModule import HandDetector # 导入手部检测模块
import os
21个手部关键点信息如下
2. 手部关键点检测
(1) cvzone.HandTrackingModule.HandDetector() 是手部关键点检测方法
参数:
mode: 默认为 False,将输入图像视为视频流。它将尝试在第一个输入图像中检测手,并在成功检测后进一步定位手的坐标。在随后的图像中,一旦检测到所有 maxHands 手并定位了相应的手的坐标,它就会跟踪这些坐标,而不会调用另一个检测,直到它失去对任何一只手的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 True,则在每个输入图像上运行手部检测,用于处理一批静态的、可能不相关的图像。
maxHands: 最多检测几只手,默认为 2
detectionCon: 手部检测模型的最小置信值(0-1之间),超过阈值则检测成功。默认为 0.5
minTrackingCon: 坐标跟踪模型的最小置信值 (0-1之间),用于将手部坐标视为成功跟踪,不成功则在下一个输入图像上自动调用手部检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 mode 为 True,则忽略这个参数,手部检测将在每个图像上运行。默认为 0.5
它的参数和返回值类似于官方函数 mediapipe.solutions.hands.Hands()
MULTI_HAND_LANDMARKS: 被检测/跟踪的手的集合,其中每只手被表示为21个手部地标的列表,每个地标由x, y, z组成。x和y分别由图像的宽度和高度归一化为[0,1]。Z表示地标深度。
MULTI_HANDEDNESS: 被检测/追踪的手是左手还是右手的集合。每只手由label(标签)和score(分数)组成。 label 是 'Left' 或 'Right' 值的字符串。 score 是预测左右手的估计概率。
(2)cvzone.HandTrackingModule.HandDetector.findHands() 找到手部关键点并绘图
参数:
img: 需要检测关键点的帧图像,格式为BGR
draw: 是否需要在原图像上绘制关键点及识别框
flipType: 图像是否需要翻转,当视频图像和我们自己不是镜像关系时,设为True就可以了
返回值:
hands: 检测到的手部信息,由0或1或2个字典组成的列表。如果检测到两只手就是由两个字典组成的列表。字典中包含:21个关键点坐标(x,y,z),检测框左上坐标及其宽高,检测框中心点坐标,检测出是哪一只手。
img: 返回绘制了关键点及连线后的图像
(3)处理PPT图片
将ppt都导出成图片,放在一个文件夹中。pathImage变量中保存所有图片文件的文件名。pathFullImage中保存所有图片的路径。
代码如下:
import cv2
import cvzone
from cvzone.HandTrackingModule import HandDetector # 导入手部跟踪模块
import os
#(1)视频捕获
cap = cv2.VideoCapture(0) # 获取电脑摄像头
cap.set(3, 1280) # 读入图像的宽度
cap.set(4, 720) # 图像的高度
# 获取ppt图片所在文件夹的路径
folderpath = 'ppt'
# 列表,存放每个文件的名称。根据文件名排序后存放到变量pathImage中
pathImage = sorted(os.listdir(folderpath), key=len)
# 设置当前展示的ppt的编号
imgNumeber = 0
# 相机的帧图显示在ppt上的size
alpha = 0.8 # 缩放比例
ws, hs = int(320*alpha), int(180*alpha) # 16:9的图像显示比例
#(2)手部识别模块配置
detector = HandDetector(maxHands=1, detectionCon=0.8) # 最多检测1只手,检测置信度0.8
#(3)处理每一帧图像
while True:
# 图像是否读取成功success,读取的帧图像img
success, img = cap.read() # 每次执行读取一帧
# 文件路径拼接,如果各组件名首字母不包含'/',则函数会自动加上,如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
pathFullImage = os.path.join(folderpath, pathImage[imgNumeber])
# 获取指定路径的图片文件
imgCurrent = cv2.imread(pathFullImage)
#(4)手部关键点检测
# 翻转图像,使电脑图像和我们自己呈镜像关系
img = cv2.flip(img, flipCode=1) # flipCode=0上下翻转,1左右翻转
# 返回手部信息hands,绘制关键点后的图像img
hands, img = detector.findHands(img)
print(hands) # 查看手部信息
#(5)显示图像
# 在ppt图片旁边接一张摄像机的帧图像
imgSmall = cv2.resize(img, (ws, hs)) # 将摄像机帧图片缩小
h, w, _ = imgCurrent.shape # 获取ppt图像的高,宽,不要通道数
# ppt图片的右下角位置上显示视频图像,[h,w]
imgCurrent[h-hs:h, w-ws:w] = imgSmall
cv2.imshow('imgCurrent', imgCurrent) # 显示ppt图像
cv2.imshow('img', img) # 显示摄像头图像
k = cv2.waitKey(1) # 每帧滞留1毫秒后消失
if k & 0xFF == 27: # ESC键退出程序
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
输出手部信息结果如下:
[{'lmList': [[440, 446, 0], [498, 408, -30], [540, 348, -37], [561, 290, -42], [567, 241, -46], [495, 280, -6], [502, 211, -20], [501, 169, -35], [499, 133, -45], [461, 277, -3], [467, 206, -15], [468, 159, -31], [468, 122, -43], [426, 286, -5], [429, 218, -20], [430, 174, -33], [433, 139, -42], [388, 303, -10], [385, 250, -26], [385, 213, -35], [388, 177, -40]],
'bbox': (385, 122, 182, 324),
'center': (476, 284),
'type': 'Right'}]
效果图如下:
3. PPT换页
接下来对手指关键点进行处理,判断那些手指是翘起的,使用 detector.fingersUp() 函数将手部信息 hands 传进去,判断哪几个手指是翘起的,返回一个列表,1代表翘起,0代表弯曲,如[1,0,0,0,0],就代表只有大拇指翘起。
给换页操作限制区域,只有当手部中心点(cx, cy)在屏幕中上方时才能激活换页操作。cx, cy = hand['center']。
为了避免每一帧都执行换页操作从而使换页过于快,设置延时器buttonDelay 规定每30帧才能执行一次换页操作。使用 buttonCounter 记录当前帧距离上一次换页已经过去多少帧了。如果执行了换页操作那么 buttonPressed=True,接下来的30帧不能换页。
在上面的代码中补充。
import cv2
import cvzone
from cvzone.HandTrackingModule import HandDetector # 导入手部跟踪模块
import os
#(1)视频捕获
cap = cv2.VideoCapture(0) # 获取电脑摄像头
cap.set(3, 1280) # 读入图像的宽度
cap.set(4, 720) # 图像的高度
# 获取ppt图片所在文件夹的路径
folderpath = 'ppt'
# 列表,存放每个文件的名称。根据文件名排序后存放到变量pathImage中
pathImage = sorted(os.listdir(folderpath), key=len)
# 设置当前展示的ppt的编号
imgNumeber = 0
# 相机的帧图显示在ppt上的size
alpha = 0.8 # 缩放比例
ws, hs = int(320*alpha), int(180*alpha) # 16:9的图像显示比例
# 在相机上画一条线,手在线条以上做手势才能触发
threshold = 350 # 线条的高度阈值
# 处理换页过快的问题
buttonPressed = False # 只有当False才能换一次页
buttonCounter = 0 # 记录距离上一次换页已经过去多少帧了
buttonDelay = 30 # 每30帧才能执行一次换页
#(2)手部识别模块配置
detector = HandDetector(maxHands=1, detectionCon=0.8) # 最多检测1只手,检测置信度0.8
#(3)处理每一帧图像
while True:
# 图像是否读取成功success,读取的帧图像img
success, img = cap.read() # 每次执行读取一帧
# 文件路径拼接,如果各组件名首字母不包含'/',则函数会自动加上,如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
pathFullImage = os.path.join(folderpath, pathImage[imgNumeber])
# 获取指定路径的图片文件
imgCurrent = cv2.imread(pathFullImage)
#(4)手部关键点检测
# 翻转图像,使电脑图像和我们自己呈镜像关系
img = cv2.flip(img, flipCode=1) # flipCode=0上下翻转,1左右翻转
# 返回手部信息hands,绘制关键点后的图像img
hands, img = detector.findHands(img)
# 在相机上画一条线,线以上做手势才能触发
cv2.line(img, (0,threshold), (1280,threshold), (0,255,0), 3)
#(5)关键点处理
if hands and buttonPressed is False: # 如果检测到了手,并且距离上一次换页已经过去了规定时间
# 获取一只手的所有信息
hand = hands[0] # hands是一个由多个字典组成的列表,只有第一个字典有信息,其他都是空
# 统计多少根手指翘起,返回一个列表,1表示手指是翘起的,0表示弯曲
fingers = detector.fingersUp(hand) # 最好手掌正对摄像机
print(fingers) #[0,1,1,1,1]
# 手部中心点坐标,hand是一个字典
cx, cy = hand['center'] # 返回中心点坐标
# 如果手的中心点坐标在线条以上就能继续操作
if cy < threshold: # 在图像上,坐标y向下为正,x向右为正
# 手掌正对摄像机,只有大拇指翘起,执行向左换页操作
if fingers == [1,0,0,0,0]:
print('to left')
# 如果当前的ppt不是第一张才能再向前移动一张
if imgNumeber > 0:
# 完成一次操作后,下次就不能再换页操作了
buttonPressed = True
# 当前展示的ppt编号向前移动一个,编号减一
imgNumeber -= 1 # pathImage[imgNumeber]指向的图片文件名改变
# 手掌正对摄像机,只有小拇指翘起,执行向右换页操作
if fingers == [0,0,0,0,1]:
print('to right')
# 如果当前的ppt不是最后一张才能再向后移动一张
if imgNumeber < len(pathImage)-1: # pathImage列表,存放所有的图片文件名
# 完成一次操作后,下次就不能再换页操作了
buttonPressed = True
# 当前展示的ppt编号向后移动一个,编号加一
imgNumeber += 1 # pathImage[imgNumeber]指向的图片文件名改变
#(6)设置延时器
if buttonPressed is True: # 此时已经换过页了
buttonCounter += 1 # 延时器计数加一
if buttonCounter > buttonDelay: # 如果延时器超过规定的帧数
buttonPressed = False # 下一帧可以换页
buttonCounter = 0 # 延时计时器重置
#()显示图像
# 在ppt图片旁边接一张摄像机的帧图像
imgSmall = cv2.resize(img, (ws, hs)) # 将摄像机帧图片缩小
h, w, _ = imgCurrent.shape # 获取ppt图像的高,宽,不要通道数
# ppt图片的右下角位置上显示视频图像,[h,w]
imgCurrent[h-hs:h, w-ws:w] = imgSmall
cv2.imshow('imgCurrent', imgCurrent) # 显示ppt图像
cv2.imshow('img', img) # 显示摄像头图像
k = cv2.waitKey(1) # 每帧滞留1毫秒后消失
if k & 0xFF == 27: # ESC键退出程序
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
效果图如下,当大拇指翘起,向左翻页。
4. PPT板书
当食指和中指都竖起时,食指指尖位置就相当于激光笔位置。由于ppt图片每一帧都在输入,因此激光笔前几帧的位置不会被保留下来,每帧只有一个激光笔位置。只需要在每帧ppt上画个圈就行。
当只有食指竖起时,相当于写板书,ppt图像上需要绘制当前帧之前的很多帧的食指指尖坐标,并把点与点之间的连线绘制出来。为了记录下每一次绘制板书的起点和终点的这段绘图点集合,定义二维列表 annotations = [[]],每个子列表存放每个一次完整绘图的曲线点坐标集合。annotationNumber 变量用来指定当前的一次完整绘图保存在哪个索引对应的子列表中。
为了避免手部在相机边缘框位置移动时,关键点检测效果很差的问题。手指在一个小范围的矩形框中移动,将这个小范围映射到整个电脑屏幕的大范围。使用 np.interp() 函数,将食指的指尖坐标(lmList[8][0],lmList[8][1])的移动范围做映射。
如果 annotationStart == False 表示当前帧是绘制板书的起点位置,将当前帧的指尖保存在二维列表annotations的索引annotationNumber对应子列表中,annotations[annotationNumber].append((xval, yval))。每次画完板书,即不是只有食指之间翘起时,annotationStart = False 停止保存指尖坐标。
在绘制板书时,遍历annotations中的每个子列表,再遍历每个子列表的所有坐标元素,就能得出所有的坐标点,绘制两两坐标点之间的连线即可。
在上述代码中补充:
import cv2
import cvzone
from cvzone.HandTrackingModule import HandDetector # 导入手部跟踪模块
import numpy as np
import os
#(1)视频捕获
cap = cv2.VideoCapture(0) # 获取电脑摄像头
cap.set(3, 1280) # 读入图像的宽度
cap.set(4, 720) # 图像的高度
# 获取ppt图片所在文件夹的路径
folderpath = 'ppt'
# 列表,存放每个文件的名称。根据文件名排序后存放到变量pathImage中
pathImage = sorted(os.listdir(folderpath), key=len)
# 设置当前展示的ppt的编号
imgNumeber = 0
# 相机的帧图显示在ppt上的size
alpha = 0.8 # 缩放比例
ws, hs = int(320*alpha), int(180*alpha) # 16:9的图像显示比例
# 在相机上画一条线,手在线条以上做手势才能触发
threshold = 350 # 线条的高度阈值
# 处理换页过快的问题
buttonPressed = False # 只有当False才能换一次页
buttonCounter = 0 # 记录距离上一次换页已经过去多少帧了
buttonDelay = 30 # 每30帧才能执行一次换页
# 保存板书的每一个坐标点
annotations = [[]] # 二维列表,每个列表保存连续绘制一次后的坐标点
annotationNumber = -1 # 当前使用的是第几个列表中的一次绘图后的关键点
annotationStart = False # 开始绘图后,需要知道一次绘图的终点和起点
#(2)手部识别模块配置
detector = HandDetector(maxHands=1, detectionCon=0.8) # 最多检测1只手,检测置信度0.8
#(3)处理每一帧图像
while True:
# 图像是否读取成功success,读取的帧图像img
success, img = cap.read() # 每次执行读取一帧
# 文件路径拼接,如果各组件名首字母不包含'/',则函数会自动加上,如果最后一个组件为空,则生成的路径以一个’/’分隔符结尾
pathFullImage = os.path.join(folderpath, pathImage[imgNumeber])
# 获取指定路径的图片文件
imgCurrent = cv2.imread(pathFullImage)
#(4)手部关键点检测
# 翻转图像,使电脑图像和我们自己呈镜像关系
img = cv2.flip(img, flipCode=1) # flipCode=0上下翻转,1左右翻转
# 返回手部信息hands,绘制关键点后的图像img
hands, img = detector.findHands(img)
# 在相机上画一条线,线以上做手势才能触发
cv2.line(img, (0,threshold), (1280,threshold), (0,255,0), 3)
# 在相机上画一个手指移动映射区域
cv2.rectangle(img, (300, 0), (1100,360), (255,0,0), 3)
#(5)关键点处理
if hands and buttonPressed is False: # 如果检测到了手,并且距离上一次换页已经过去了规定时间
# 获取一只手的所有信息
hand = hands[0] # hands是一个由多个字典组成的列表,只有第一个字典有信息,其他都是空
# 统计多少根手指翘起,返回一个列表,1表示手指是翘起的,0表示弯曲
fingers = detector.fingersUp(hand) # 最好手掌正对摄像机
print(fingers) #[0,1,1,1,1]
# 将手指移动的边界限制在一个框中,框的大小映射到屏幕大小
lmList = hand['lmList'] # 获取21个关键点的xyz坐标
# indexFinger = lmList[8][0], lmList[8][1] # 获取食指指尖的xy坐标
# 设置映射区域
xval = int(np.interp(lmList[8][0], [300, 1280], [0, 1280])) # x的映射区域
yval = int(np.interp(lmList[8][1], [0, 360], [0, 720])) # y的映射区域
# 手部中心点坐标,hand是一个字典
cx, cy = hand['center'] # 返回中心点坐标
# 如果手的中心点坐标在线条以上就能继续操作
if cy < threshold: # 在图像上,坐标y向下为正,x向右为正
# ① 手掌正对摄像机,只有大拇指翘起,执行向左换页操作
if fingers == [1,0,0,0,0]:
print('to left')
# 如果当前的ppt不是第一张才能再向前移动一张
if imgNumeber > 0:
# 完成一次操作后,下次就不能再换页操作了
buttonPressed = True
# 当前展示的ppt编号向前移动一个,编号减一
imgNumeber -= 1 # pathImage[imgNumeber]指向的图片文件名改变
# ② 手掌正对摄像机,只有小拇指翘起,执行向右换页操作
if fingers == [0,0,0,0,1]:
print('to right')
# 如果当前的ppt不是最后一张才能再向后移动一张
if imgNumeber < len(pathImage)-1: # pathImage列表,存放所有的图片文件名
# 完成一次操作后,下次就不能再换页操作了
buttonPressed = True
# 当前展示的ppt编号向后移动一个,编号加一
imgNumeber += 1 # pathImage[imgNumeber]指向的图片文件名改变
# ③ 指针设置,如果食指和中指同时竖起就绘制一个圈,不需要在线条以上
if fingers == [0,1,1,0,0]:
print('circle')
# 在ppt图片上的食指指尖绘制圆圈
cv2.circle(imgCurrent, (xval, yval), 10, (0,255,255), -1) # -1代表颜色填充
cv2.circle(imgCurrent, (xval, yval), 11, (0,0,255), 3)
# ④ 板书设置,如果只有食指竖起就按食指轨迹移动绘制线条
if fingers == [0,1,0,0,0]:
# 如果之前没绘制过图那么annotationStart=False
if annotationStart == False:
annotationStart = True # 那么当前帧开始绘图
annotationNumber += 1 # 将当前绘图结果到保存在该索引指向的列表中
annotations.append([]) # 在二维列表中添加一个子列表用来保存坐标
cv2.circle(imgCurrent, (xval, yval), 10, (0,255,255), -1)
# 将食指的每一帧坐标都保存在指定的索引列表中
annotations[annotationNumber].append((xval, yval))
# 如果不绘制板书了,当前一次绘图的坐标都保存好
else:
annotationStart = False # 不绘制了
# ⑤ 删除前一次的板书,不全删
if fingers == [0,1,1,1,0]:
if annotations:
annotations.pop(-1) # 删除最后一次绘图的坐标
annotationNumber -= 1 # 绘图索引向前移动一个
# ⑥ 擦除全部板书,如果没有手指竖起就删除所有板书
if fingers == [0,0,0,0,0]:
annotations = [[]] # 二维列表重置
annotationNumber = -1 # 重置当前使用的列表索引
annotationStart = False # 重置绘图过程
#(6)设置延时器
if buttonPressed is True: # 此时已经换过页了
buttonCounter += 1 # 延时器计数加一
if buttonCounter > buttonDelay: # 如果延时器超过规定的帧数
buttonPressed = False # 下一帧可以换页
buttonCounter = 0 # 延时计时器重置
#(7)绘制板书,将第④步保存的坐标点都绘制出来
for i in range(len(annotations)): # 取出绘图列表
# 遍历每个列表,绘制一次绘图的点
for j in range(len(annotations[i])):
# 绘制每两个点之间的线条
if j != 0:
cv2.line(imgCurrent, annotations[i][j-1], annotations[i][j], (0,0,255),5)
#(8)显示图像
# 在ppt图片旁边接一张摄像机的帧图像
imgSmall = cv2.resize(img, (ws, hs)) # 将摄像机帧图片缩小
h, w, _ = imgCurrent.shape # 获取ppt图像的高,宽,不要通道数
# ppt图片的右下角位置上显示视频图像,[h,w]
imgCurrent[h-hs:h, w-ws:w] = imgSmall
cv2.imshow('imgCurrent', imgCurrent) # 显示ppt图像
cv2.imshow('img', img) # 显示摄像头图像
k = cv2.waitKey(1) # 每帧滞留1毫秒后消失
if k & 0xFF == 27: # ESC键退出程序
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
效果图如下: