各位同学好,今天和大家分享一下如何使用opencv+mediapipe完成远程手势调节图片尺寸的案例。先放张图看效果。当拇指和食指竖起时,根据食指间的连线的长度自由缩放图片尺寸。图片的中点始终位于指尖连线的中点。16代表FPS值
这里需要用到mediapipe中的手部关键点检测方法,并且需要判断哪根手指是弯下的,哪根手指是翘起来的。手部关键点检测方法有不明白的可以看我之前的一篇文章:【MediaPipe】(1) AI视觉,手部关键点实时跟踪,附python完整代码_立同学的博客-CSDN博客,判断哪个手指朝上的方法在后面的章节会介绍。
# 安装工具包
pip install opencv-contrib-python # 安装opencv
pip install mediapipe # 安装mediapipe
# pip install mediapipe --user #有user报错的话试试这个
pip install cvzone # 安装cvzone
# 导入工具包
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部追踪方法
import mediapipe as mp
import time
21个手部关键点信息如下,本节我们主要研究食指指尖"8"的坐标信息。
参数:
mode: 默认为 False,将输入图像视为视频流。它将尝试在第一个输入图像中检测手,并在成功检测后进一步定位手的坐标。在随后的图像中,一旦检测到所有 maxHands 手并定位了相应的手的坐标,它就会跟踪这些坐标,而不会调用另一个检测,直到它失去对任何一只手的跟踪。这减少了延迟,非常适合处理视频帧。如果设置为 True,则在每个输入图像上运行手部检测,用于处理一批静态的、可能不相关的图像。
maxHands: 最多检测几只手,默认为 2
detectionCon: 手部检测模型的最小置信值(0-1之间),超过阈值则检测成功。默认为 0.5
minTrackingCon: 坐标跟踪模型的最小置信值 (0-1之间),用于将手部坐标视为成功跟踪,不成功则在下一个输入图像上自动调用手部检测。将其设置为更高的值可以提高解决方案的稳健性,但代价是更高的延迟。如果 mode 为 True,则忽略这个参数,手部检测将在每个图像上运行。默认为 0.5
它的参数和返回值类似于官方函数 mediapipe.solutions.hands.Hands()
参数:
img: 需要检测关键点的帧图像,格式为BGR
draw: 是否需要在原图像上绘制关键点及识别框
flipType: 图像是否需要翻转,当视频图像和我们自己不是镜像关系时,设为True就可以了
返回值:
hands: 检测到的手部信息,包含:21个关键点坐标,检测框坐标及宽高,检测框中心坐标,检测出是哪一只手。
img: 返回绘制了关键点及连线后的图像
代码如下
# 远程手势缩放物体尺寸
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部追踪方法
import time
#(1)获取摄像头信息
cap = cv2.VideoCapture(0) # 0代表电脑自带的摄像头
cap.set(3, 1280) # 设置显示框的宽
cap.set(4, 720) # 设置显示框的高
pTime = 0 # 处理第一帧图像的起始时间
#(2)获取手部检测方法,传入参数,手部最小检测置信度0.8,最多检测2只手
detector = HandDetector(detectionCon=0.8, maxHands=2)
#(3)处理每一帧图像
while True:
# 返回是否读取成功,已经读取的帧图像
success, img = cap.read()
#(4)检测手部信息
# 返回每只手的检测框信息hands,以及绘制后的手部图像
hands, img = detector.findHands(img, draw=True, flipType=True) # fliptype代表是否翻转图像,上面以及翻转过了
# 打印检测到的是左手还是右手,以及关键点的像素坐标
print(hands)
#(5)展示视频图像
# 计算fps
cTime = time.time() # 处理每一帧图像所需的时间
fps = 1/(cTime-pTime)
pTime = cTime # 更新处理下一帧图像的起始时间
# 把fps值显示在图像上,img画板,显示字符串,显示的坐标位置,字体,字体大小,颜色,线条粗细
cv2.putText(img, str(int(fps)), (30,50), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,0), 3)
# 显示图像,输入窗口名及图像数据
# cv2.namedWindow("img", 0) # 窗口大小可手动调整
cv2.imshow('img', img)
if cv2.waitKey(20) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
打印某一帧中检测到的手部信息,lmList 代表每只手的21个手部关键点坐标;bbox 代表检测框的左上角坐标,以及检测框的宽和高;center 代表检测框的中心坐标;type 代表检测出是哪一只手。
----------------------------------------------------
[{'lmList': [[1214, 809], [1111, 806], [1024, 769], [983, 732], [939, 727], [972, 576], [896, 461], [851, 392], [810, 331], [1052, 528], [985, 393], [943, 310], [905, 237], [1145, 514], [1109, 376], [1081, 289], [1052, 214], [1247, 525], [1256, 407], [1254, 332], [1245, 262]],
'bbox': (810, 214, 446, 595),
'center': (1033, 511),
'type': 'Right'},
{'lmList': [[174, 753], [329, 742], [469, 707], [589, 687], [695, 674], [451, 519], [539, 414], [596, 358], [653, 311], [394, 480], [488, 365], [558, 299], [628, 249], [321, 459], [409, 347], [481, 286], [555, 243], [232, 450], [293, 350], [348, 291], [411, 242]],
'bbox': (174, 242, 521, 511),
'center': (434, 497),
'type': 'Left'}]
----------------------------------------------------
显示结果如图:
首先,我们通过 cv2.imread() 导入需要缩放的图像,img[0:180, 0:320] = img1,先把图像显示在视频帧图像的固定位置。
确定缩放方法的思路是,如果检测到两只手,并且这两只手是拇指和食指朝上,那么检测到的第一帧时的两只手的食指尖之间的距离,作为初始距离 startDist = length,接下去图片在这个初始距离的基础上进行缩放。如果检测的手消失,那么就重置初始距离 startDist = None,将下一次检测到的指尖距离作为初始值。
通过 detector.findHands() 方法来检测哪个手指朝上,传入参数是每只手的所有关键点坐标。返回值是由0和1组成的长度为5的列表,0代表该手指弯曲,1代表该手指朝上。我们需要得到的是[1,1,0,0,0],即拇指和食指朝上,其他手指弯曲。
通过 detector.findDistance() 方法来检测某两个关键点之间的距离。length, info, img = distance = detector.findDistance(lmList1[8], lmList2[8], img), 返回值: length 代表两个关键点之间的距离,info 是一个6个元素组成的列表,包含关键点之间连线的起点、终点、中点坐标。img 是绘制连线后的图像。
因此,我们在上述代码中补充。
# 远程手势缩放物体尺寸
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部追踪方法
import time
#(1)获取摄像头信息
cap = cv2.VideoCapture(0) # 0代表电脑自带的摄像头
cap.set(3, 1280) # 设置显示框的宽
cap.set(4, 720) # 设置显示框的高
pTime = 0 # 处理第一帧图像的起始时间
#(2)获取手部检测方法,传入参数,手部最小检测置信度0.8,最多检测2只手
detector = HandDetector(detectionCon=0.8, maxHands=2)
startDist = None # 设置一个初始距离
#(3)处理每一帧图像
while True:
# 返回是否读取成功,已经读取的帧图像
success, img = cap.read()
#(4)检测手部信息
# 返回每只手的检测框信息hands,以及绘制后的手部图像
hands, img = detector.findHands(img, draw=True, flipType=True) # fliptype代表是否翻转图像
#(5)导入需要调节的图片,图像的(w,h)为[1280,720]
img1 = cv2.imread('C:\\GameDownload\\Deep Learning\\TF2.jpg')
# 由于我导入的图像太大了,这里把它缩小一下
img1 = cv2.resize(img1, (320,180))
# 把这张图片放在屏幕的固定位置,先指定h再指定w, 即img[h,w]
img[0:180, 0:320] = img1
# 放大和缩小步骤:当两只手的拇指和食指竖起,其他指弯下,那么就执行放大和缩小
#(6)如果检测到有两只手,进行放大缩小的操作
if len(hands) == 2:
# 检测手指是否朝上的,hands[0]代表第一只手,hands[1]代表第二只手
print('which up:', detector.fingersUp(hands[0]), detector.fingersUp(hands[1]))
# 返回值是[1,1,0,0,0]代表一只手中拇指和食指竖起,其他指都没有竖起
if detector.fingersUp(hands[0]) == [1,1,0,0,0] and detector.fingersUp(hands[1]) == [1,1,0,0,0]:
# 通过两只手食指的关键点之间的距离来缩放图片
lmList1 = hands[0]['lmList'] # 第一只手的关键点坐标信息,hands是一个字典
lmList2 = hands[1]['lmList'] # 第二只手的关键点坐标信息
# 第一次检测到食指间的距离
if startDist is None:
# 计算食指间的距离并绘图;食指的关键点索引是8;返回值:连线长度,连线的信息(起点、终点、中点坐标),绘制后的图像
length, info, img = distance = detector.findDistance(lmList1[8], lmList2[8], img)
# print('length',length,'info',info)
# 检测到的第一帧的食指间的距离作为初始距离,接下来超过这个长度就放大,小于这个长度就缩小
startDist = length
# 第一帧检测到距离之后,接下来变动的距离就是用于缩放图片大小
length, info, img = distance = detector.findDistance(lmList1[8], lmList2[8], img)
# 计算变化量,正数代表放大,负数代表缩小
scale = length - startDist
print('scale:',scale)
# 如果两只手中至少有一只消失了,重置初始距离
else:
startDist = None
#(7)展示视频图像
# 计算fps
cTime = time.time() # 处理每一帧图像所需的时间
fps = 1/(cTime-pTime)
pTime = cTime # 更新处理下一帧图像的起始时间
# 把fps值显示在图像上,img画板,显示字符串,显示的坐标位置,字体,字体大小,颜色,线条粗细
cv2.putText(img, str(int(fps)), (30,50), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,0), 3)
# 显示图像,输入窗口名及图像数据
# cv2.namedWindow("img", 0) # 窗口大小可手动调整
cv2.imshow('img', img)
if cv2.waitKey(20) & 0xFF==27: #每帧滞留20毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
打印某几帧的结果,数字1代表该指时朝上的,0代表该指是弯着的。scale代表食指间的距离。
----------------------------------------------
which up: [1, 1, 0, 0, 0] [1, 1, 0, 0, 0]
scale: 225.00591450309486
which up: [1, 1, 0, 0, 0] [1, 1, 0, 0, 0]
scale: 18.837911081298728
which up: [1, 1, 0, 0, 0] [1, 1, 0, 0, 0]
scale: -30.173165115569134
which up: [1, 1, 0, 0, 0] [1, 1, 0, 0, 0]
scale: -28.422575826465106
-----------------------------------------------
显示结果如图,需要改变尺寸的图片暂时位于左上方,当拇指和食指朝上时绘制指尖连线,并计算连线长度。
info中存放指尖连线的起点、终点、中点坐标,其中 cx, cy = info[4:] 代表连线中点的坐标。现在让图片的中点落在指尖连线的中点上,使图片的位置随手指位置而实时发生变化。
在计算中点时用到 newH//2 和 newW//2 ,这时候需要保证新的宽和高能被2整除。否则在执行 img[cy - newH//2:cy + newH//2, cx - newW//2:cx + newW//2] = img1 时,屏幕上划分给图片的shape和图片自身的shape不一致,从而导致程序报错。因此在执行之前,通过int(((h1+scale)//2)*2) 和 int(((w1+scale)//2)*2) ,提前使宽高能被整除。如果是奇数除以2,也只是忽略了一个像素,不会有影响。
当我们把图片放的过大,或图像坐标出现负数时,程序会抛出异常不能执行,因此使用 try,except 方法,当遇到异常就执行except中的内容,这里是pass,直接跳过;没发出异常就正常运行try中的内容。
因此,在上述代码中补充。
# 远程手势缩放物体尺寸
import cv2
from cvzone.HandTrackingModule import HandDetector # 手部追踪方法
import time
#(1)获取摄像头信息
cap = cv2.VideoCapture(0) # 0代表电脑自带的摄像头
cap.set(3, 1280) # 设置显示框的宽
cap.set(4, 720) # 设置显示框的高
pTime = 0 # 处理第一帧图像的起始时间
#(2)获取手部检测方法,传入参数,手部最小检测置信度0.8,最多检测2只手
detector = HandDetector(detectionCon=0.8, maxHands=2)
startDist = None # 设置一个初始距离
scale = 0 # 设置一个初始的需要缩放的大小
cx, cy = 200, 200 # 确定初始的图像中心点在屏幕上的显示位置
#(3)处理每一帧图像
while True:
# 返回是否读取成功,已经读取的帧图像
success, img = cap.read()
# 翻转图像,保证摄像机画面和人的动作是镜像
# img = cv2.flip(img, flipCode=1) #0竖直翻转,1水平翻转
#(4)检测手部信息
# 返回每只手的检测框信息hands,以及绘制后的手部图像
hands, img = detector.findHands(img, draw=True, flipType=True) # fliptype代表是否翻转图像
#(5)导入需要调节的图片,图像的(w,h)为[1280,720]
img1 = cv2.imread('C:\\GameDownload\\Deep Learning\\TF2.jpg')
# 由于我导入的图像太大了,这里把它缩小一下
img1 = cv2.resize(img1, (320,180))
# 把这张图片放在屏幕的固定位置,先指定h再指定w, 即img[h,w]
# img[0:180, 0:320] = img1
# 放大和缩小步骤:当两只手的拇指和食指竖起,其他指弯下,那么就执行放大和缩小
#(6)如果检测到有两只手,进行放大缩小的操作
if len(hands) == 2:
# 检测手指是否朝上的,hands[0]代表第一只手,hands[1]代表第二只手
# print('which up:', detector.fingersUp(hands[0]), detector.fingersUp(hands[1]))
# 返回值是[1,1,0,0,0]代表一只手中拇指和食指竖起,其他指都没有竖起
if detector.fingersUp(hands[0]) == [1,1,0,0,0] and detector.fingersUp(hands[1]) == [1,1,0,0,0]:
# 通过两只手食指的关键点之间的距离来缩放图片
lmList1 = hands[0]['lmList'] # 第一只手的关键点坐标信息,hands是一个字典
lmList2 = hands[1]['lmList'] # 第二只手的关键点坐标信息
# 第一次检测到食指间的距离
if startDist is None:
# 计算食指间的距离并绘图;食指的关键点索引是8;返回值:连线长度,连线的信息(起点、终点、中点坐标),绘制后的图像
length, info, img = distance = detector.findDistance(lmList1[8], lmList2[8], img)
# print('length',length,'info',info)
# 检测到的第一帧的食指间的距离作为初始距离,接下来超过这个长度就放大,小于这个长度就缩小
startDist = length
# 第一帧检测到距离之后,接下来变动的距离就是用于缩放图片大小
length, info, img = distance = detector.findDistance(lmList1[8], lmList2[8], img)
# 计算变化量,正数代表放大,负数代表缩小。scale的变化范围过大,除以2使它变化缓慢一些
scale = (length - startDist) // 2
#(7)按比例缩放图像
# 获取食指连线的中心点坐标,用于实时改变图像的位置
cx, cy = info[4:] # info是一个列表索引4和5存放中心点坐标
# 如果两只手中至少有一只消失了,重置初始距离
else:
startDist = None
try: # 用于处理异常,因为一旦缩放的区间变成负数,就会报错
# 确定需要缩放的图像的宽高
h1, w1, _ = img1.shape # 获取原始图像的宽高
# 如果scale是奇数,那么计算结果不能被2整除,使得img中的空出的位置的shape和img1的shape不一样
# newH, newW = h1+scale, w1+scale
newH, newW = int(((h1+scale)//2)*2), int(((w1+scale)//2)*2)
# 改变原图像的shape,先指定宽,后指定高
img1 = cv2.resize(img1, (newW, newH))
# 实时改变图像的位置,使图像中心点随着食指间的连线的中点的位置变化
# 确保newH和newW可以被2整除,不然重组后的img中的shape和img1的shape不同
img[cy - newH//2:cy + newH//2, cx - newW//2:cx + newW//2] = img1 # 先指定高,再指定宽
except: # 如果报错了的话上面try的内容不起作用
pass
#(7)展示视频图像
# 计算fps
cTime = time.time() # 处理每一帧图像所需的时间
fps = 1/(cTime-pTime)
pTime = cTime # 更新处理下一帧图像的起始时间
# 把fps值显示在图像上,img画板,显示字符串,显示的坐标位置,字体,字体大小,颜色,线条粗细
cv2.putText(img, str(int(fps)), (30,50), cv2.FONT_HERSHEY_PLAIN, 3, (255,0,0), 3)
# 显示图像,输入窗口名及图像数据
# cv2.namedWindow("img", 0) # 窗口大小可手动调整
cv2.imshow('img', img)
if cv2.waitKey(1) & 0xFF==27: #每帧滞留1毫秒后消失,ESC键退出
break
# 释放视频资源
cap.release()
cv2.destroyAllWindows()
显示结果如下: