这篇文章是自己跟着油管大神做的,(有条件的建议自己搭梯子跟着去做一遍Hand Tracking 30 FPS using CPU | OpenCV Python (2021) | Computer Vision - YouTube)由于基础不太好,我就自己做了一遍,里面做了大量注释,有opencv的,也有python基础语法的,希望对做计算机视觉的新手有所帮助,大佬就可以略过啦。有什么问题欢迎指出。
首先做一个小model,用于满足手部追踪的最低要求handTrackingMin.py,后面的模块在此基础上增加功能
import cv2
import mediapipe as mp
import time
cap = cv2.VideoCapture(1)
mpHands = mp.solutions.hands
hands = mpHands.Hands()
mpDraw = mp.solutions.drawing_utils # 直接调用mediapipe的坐标绘制方法
preTime = 0
curTime = 0 # 先定义好前一帧的时间和当前时间,用以描述帧率
while True:
success, img = cap.read()
imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 图像色彩转换,因为hands = mpHands.Hands()这个对象只能读入RGB图像信息
results = hands.process(imgRGB)
# print(reults.multi_hand_landmarks) # 打印输出landmark,若未检测到,就输出None,检测到了就输出坐标值
if results.mult_hand_landmarks:
for handLms in results.mult_hand_landmarks: # 遍历每只手的坐标 共21个坐标
for id, lm in enumerate(handLms.landmarks): # 将其中一只手的每个关键点的id(序号)和坐标进行列举出来
# enumerate是python内置函数,用于枚举、列举,同时对列出的内容进行编号,因此可以同时获得索引和值,索引从0开始,
# 其有两个参数,第一个参数为要枚举的序列,第二个参数为起始序号,默认为0开始
# print(id, lm)
h, w, c = img.shape # 获取图像尺寸
cx, cy = int(lm.x * w), int(lm.y * h) # 坐标乘以相应尺寸,并转换成整数
print(id, cx, cy)
# """但现在的问题是,我们有了坐标值,但这些坐标值是小数,对于图片像素点来说是不好映射的,它实际上是相对于图像的比例值,因此为了能映射到
# 实际图像上,需要乘以图像的尺寸"""
cv2.circle(img, (cx, cy), 25, (255, 255, 0)) # 为了能明显区分每一个landmark,对其进行标注
mpDraw.draw_landmarks(img, handLms,
mpHands.HAND_CONNECTIONS) # 第一个参数是传入的图片,第二个参数是传入的坐标,这个方法可以将各个landmark绘制在画面中,
# 第三个参数将各个点连在一起
"""现在的问题在于,但我们不知道如果运用这些值。管他有没有用, 先将序号和坐标存入一个列表中,需要时就调用"""
curTime = time.time() # 获取当前帧的时间
fps = 1 / (curTime - preTime) # 帧率
preTime = curTime # 时间更新
cv2.putText(img, str(int(fps)), (255, 10), cv2.FONT_HERSHEY_SIMPLEX,
3, (255, 0, 255), None) # 在画面上显示帧率,并四舍五入为整数
cv2.imshow(img)
cv2.waitKey(1)
正式创建了一个手部跟踪模块handTrackingModule.py
import cv2
import mediapipe as mp
import time
class handDetector: # 创建一个手部检测器类,他是根据mediapipe中的hands模块来创建的
def __init__(self, static_image_mode = False, # 是否将输入图像看作静态图像,False代表当作视频
max_num_hands = 2, # 最大检测数量
model_complexity = 1, #模型复杂度(0或1),landmark准确率和推理延迟通常会随着复杂度升高而升高
min_detection_confidence = 0.5, # 最小检测置信度
min_tracking_confidence = 0.5 ): # 最小追踪置信度
self.mode = static_image_mode
self.maxHands = max_num_hands
self.detectionCon = min_detection_confidence
self.trackCon = min_tracking_confidence
self.mpHands = mp.solutions.hands
self.hands = self.mpHands.Hands(self.mode, self.maxHands, self.detectionCon, self.trackCon)
self.mpDraw = mp.solutions.drawing_utils # 直接调用mediapipe的坐标绘制方法
def findHands(self, img, draw=True): # 其中draw=True可以决定是否想要画出landmark及其连接线
imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 图像色彩转换,因为hands = mpHands.Hands()这个对象只能读入RGB图像信息
self.results = self.hands.process(imgRGB) # 这里定义了一个实例变量
# print(reults.multi_hand_landmarks) # 打印输出landmark,若未检测到,就输出None,检测到了就输出坐标值
if self.results.mult_hand_landmarks:
for handLms in self.results.mult_hand_landmarks: # 遍历每只手的坐标 共21个坐标
if draw:
self.mpDraw.draw_landmarks(img, handLms,
self.mpHands.HAND_CONNECTIONS) # 第一个参数是传入的图片,第二个参数是传入的坐标,这个方法可以将各个landmark绘制在画面中,
# 第三个参数将各个点连在一起
return img # 返回已经绘制好连接点的图像
"""现在的问题在于,但我们不知道如果运用这些值。管他有没有用, 先将序号和坐标存入一个列表中,需要时就调用.因此,我们需要创建一个findPosition函数来将"""
def findPosition(self, img, handNumber, draw=True): # 在这里,我们并不需要图像的各项参数,只要它的尺寸,用来确定位置信息,其中handNumber表示手的序号
lmList = [] # 定义一个空列表,用于接收后面产生的landmark序号及坐标
# 判断是否检测到有手部信息,如果有,则输出序号及位置
if self.results.mult_hand_landmarks:
myHand = self.results.mult_hand_landmarks[handNumber] # 将手的序号传给myHand
for id, lm in enumerate(myHand.landmarks): # 将其中一只手的每个关键点的id(序号)和坐标进行列举出来
# enumerate是python内置函数,用于枚举、列举,同时对列出的内容进行编号,因此可以同时获得索引和值,索引从0开始,
# 其有两个参数,第一个参数为要枚举的序列,第二个参数为起始序号,默认为0开始
# print(id, lm)
h, w, c = img.shape # 获取图像尺寸
cx, cy = int(lm.x * w), int(lm.y * h) # 坐标乘以相应尺寸,并转换成整数
# print(id, cx, cy)
lmList.append([id, cx, cy ]) # 注意:这里lmList追加的是一个列表,追加之后就成了两级列表,如:[[id1, cx1, cy1], [id2, cx2, cy2], [id3, cx3, cy3]......]
# """但现在的问题是,我们有了坐标值,但这些坐标值是小数,对于图片像素点来说是不好映射的,它实际上是相对于图像的比例值,因此为了能映射到
# 实际图像上,需要乘以图像的尺寸"""
if draw:
cv2.circle(img, (cx, cy), 25, (255, 255, 0), cv2.FILLED) # 为了能明显区分每一个landmark,对其进行标注
return lmList
def main(): # 以下代码可以用在不同项目中
preTime = 0
curTime = 0 # 先定义好前一帧的时间和当前时间,用以描述帧率
cap = cv2.VideoCapture(1)
detector = handDetector() # 创建一个类外实例,这里无需参数,因为已经默认设置好了,注意:创建类外实例时无需self
while True:
success, img = cap.read()
img = detector.findHands(img)
lmList =detector.findPosition(img) # 接收序号及位置信息
if len(lmList) != 0: # 判断列表是否为空,若不为空则打印,如果没有此判断句,当程序运行时没有检测到手,lmList就没有值,就会报错(Index out of range),因此一定要加
print(lmList[4]) # 这里我们尝试打印4号位置(大拇指尖)的序号及坐标信息
curTime = time.time() # 获取当前帧的时间
fps = 1 / (curTime - preTime) # 帧率
preTime = curTime # 时间更新
cv2.putText(img, str(int(fps)), (255, 10), cv2.FONT_HERSHEY_SIMPLEX,
3, (255, 0, 255), None) # 在画面上显示帧率,并四舍五入为整数
cv2.imshow(img)
cv2.waitKey(1)
if __name__ == "__main__": # 在本py文件中,这段话表明程序的入口,当单独执行本py文件的时候,if __name__ == "__main__":下面的
# 语句会自动执行,但本py文件被当作模块导入时,if __name__ == "__main__":下面的语句不会被执行
main()
最后实现鼠标追踪AiVirtualMouse.py,其中调用了上面的手部追踪模块
import numpy as np
import handTrackModule as htm
import cv2
import numpy
import time
import autopy # 它包括用于控制键盘和鼠标,在屏幕上查找颜色和位图以及显示警报的功能
###################################
wCam, hCam = 640, 480 # 定义画面尺寸
framR = 100 # 限定的尺寸
smoothening = 7 # 平滑度
###################################
cap = cv2.VideoCapture(1)
cap.set(3, wCam)
cap.set(4, hCam) # 定义画面尺寸
pTime = 0
preLocaX, preLocaY = 0, 0 # 先前的坐标值
curLocaX, curLocaY = 0, 0 # 当前的坐标值
detector = htm.handDetector(maxHands=1) # 检测器,检测最大数量
wScr, hScr = autopy.screen.size() # 自动获取屏幕尺寸
while True:
# 1. find hand landmarks
success, img = cap.read() # 读取视频内容
img = detector.findHands(img) # 如果想要画出landmark及其连线,加入draw=True
lmlist, bbox = detector.findPosition(img) # 获取整只手的坐标,存入lmlist,bbox为边界框数组;findPosition(img)中有两个元组,一个代表lmlist,一个代表bbox
# 如果想要画出●圆圈作为标记点,在后面加draw=True
# 2. 获得食指和中指的指尖
if len(lmlist) != 0:
x1, y1 = lmlist[8][1:] # 食指指尖坐标
x2, y2 = lmlist[12][1:] # 中指指尖坐标
print(x1, y1, x2, y2)
# 3. 检查哪个指尖向上
fingers = detector.fingersUp() # 将5个值作为数组传给fingers[1,1,1,1,1] 其中1代表指尖向上,0表示无指尖,一次代表大拇指到小拇指
cv2.rectangle(img, (framR, framR), (wCam - framR, hCam - framR), (255, 255, 0), 2) # 画框,设置框尺寸 rectangle参数:图像源,左上顶点坐标,右下对角线顶点坐标,颜色,粗细,线条类型
# 4. 仅检测到食指:移动模式
if fingers[1] == 1 and fingers[2] == 0: # 食指为1,中指为0
# 5. 转换坐标,用来检测食指移动到哪里,然后将坐标发送给鼠标
x3 = np.interp(x1, (framR, wCam - framR), (0, wScr)) # 线性插值,将手指相对于摄像头画面的位置转换为鼠标相对于屏幕的位置
y3 = np.interp(y1, (framR, hCam - framR), (0, hScr))
# 6. 设置平滑值
curLocaX = preLocaX + (x3 - preLocaX) / smoothening # 平滑处理,如果smoothening越大,动作越平滑,但数值大会产生运动过慢和运动滞后的问题,
curLocaY = preLocaY + (y3 - preLocaY) / smoothening
# 7. 移动鼠标
autopy.mouse.move(wScr - curLocaX, curLocaY) # 将转换过的坐标发送给鼠标
cv2.circle(img, (x1, y1), 15, (255, 0, 255), cv2.FILLED) # 以当前坐标为圆心画圆圈,用以表示手指位置
preLocaX, preLocaY = curLocaX, curLocaY # 对位置坐标进行迭代
"""但出现了一个问题,当手往下移动的时候,由于检测不到手的全貌,导致鼠标无法停留在屏幕最下方,因此需要划定一个操作区域,于是设置 framR=100 """
# 8. 食指和中指都向上,点击模式
if fingers[1] == 1 and fingers[2] == 1: # 食指为1,中指也为1
# 9. 检测指间距离
length, img, infoLine, = detector.findDistance(8, 12, img) # infoLine表示信息线
# 10. 如果距离短,点击鼠标
if length < 40: # 设定一个阈值,当两指指尖小于这个阈值,才被视为点击模式
cv2.circle(img, (infoLine[4], infoLine[5]), 15, (0, 255, 255), cv2.FILLED) # 在两指之间画圈,其中(infoLine[4], infoLine[5])表示两指之间圆心坐标
autopy.mouse.click() # 点击
"""但问题在于,由于是一帧一帧进行检测,坐标会发生抖动,最终会使点击不准确,于是设置平滑度,第6步"""
# 11. 帧率
cTime = time.time() # 获取当前时间
fps = 1 / (cTime - pTime) # 获取帧率
pTime = cTime
cv2.putText(img, str(int(fps)), (40, 70), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 255, 0), 3) # 设置文字及其格式
# 12. 显示
cv2.namedWindow()
cv2.imshow("Image", img)
cv2.waitKey(1)