一、技术栈
1. OpenCV(Open Source Computer Vision Library)
-
性质:开源计算机视觉库(Library)
-
主要功能:
-
图像/视频的基础处理(读取、裁剪、滤波、色彩转换等)
-
特征检测(边缘、角点等)
-
摄像头标定、目标跟踪等
-
-
在项目中的作用:
-
负责视频流的捕获(
cv2.VideoCapture
) -
图像格式转换(
cv2.cvtColor
) -
最终结果的渲染显示(
cv2.imshow
)
-
2. MediaPipe
-
性质:由Google开发的跨平台机器学习框架(Framework)
-
主要功能:
-
提供预训练的端到端模型(如手部关键点、人脸网格、姿态估计等)
-
专注于实时感知任务(低延迟、移动端优化)
-
-
在项目中的作用:
-
调用
mediapipe.solutions.hands
模型实现21个手部关键点检测 -
输出关键点坐标,并通过
mpDraw
可视化
-
二、手部关键点检测
(一)初始化
cap = cv2.VideoCapture(0) # 通过OpenCV调用摄像头设备。参数0:默认摄像头(笔记本内置摄像头)。
mpHands = mp.solutions.hands # MediaPipe的手部关键点检测模型(21个关键点)
hands = mpHands.Hands() # 创建模型实例
mpDraw = mp.solutions.drawing_utils # MediaPipe提供的绘图工具,用于在图像上绘制关键点和连线。
handLmsStyle = mpDraw.DrawingSpec(color=(0, 0, 255), thickness=5) # 点的样式
handConStyle = mpDraw.DrawingSpec(color=(0, 255, 0), thickness=10) # 线的样式
pTime = 0
cTime = 0
(二)关键点检测
ret, img = cap.read() # 从摄像头持续读取视频帧。OpenCV默认格式为BGR格式if ret:imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 格式转换,MediaPipe模型要求输入为RGB格式# 手部关键点检测result = hands.process(imgRGB)# print(result.multi_hand_landmarks)imgHeight = img.shape[0]imgWidth = img.shape[1]
重要代码解释:
result = hands.process(imgRGB)
- 底层过程:
图像输入MediaPipe手部模型
模型输出包含:
multi_hand_landmarks:21个关键点的归一化坐标(0~1之间)
multi_handedness:左右手判断
- result 数据结构:
类型:List(列表)
内容:每个元素代表一只手的21个关键点数据(因此result.multi_hand_landmarks最多 有两个元素)
层级关系:
result.multi_hand_landmarks[0] # 第1只手.landmark[0] # 第1个关键点.x # 归一化x坐标 (0.0~1.0).y # 归一化y坐标 (0.0~1.0).z # 相对深度(值越小越靠近摄像头)
(三)可视化
# 关键点可视化if result.multi_hand_landmarks:for handLms in result.multi_hand_landmarks: # 遍历每只检测到的手mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle,handConStyle) # 绘制手部关键点和骨骼连线for i, lm in enumerate(handLms.landmark): # 遍历21个关键点xPos = int(lm.x * imgWidth) # 将归一化x坐标转换为像素坐标yPos = int(lm.y * imgHeight) # 将归一化y坐标转换为像素坐标cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),2) # 在关键点旁标注索引数字if i == 4:cv2.circle(img, (xPos, yPos), 20, (166, 56, 56), cv2.FILLED)print(i, xPos, yPos) # 用深蓝色实心圆高亮标记拇指尖
重要代码解释:
mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle, handConStyle)
-
功能:绘制手部关键点和骨骼连线
-
参数详解:
-
img
:目标图像(OpenCV格式) -
handLms
:当前手的关键点数据 -
mpHands.HAND_CONNECTIONS
:预定义的关键点连接关系(如点0-1相连,点1-2相连等) -
handLmsStyle
:关键点绘制样式(红色圆点,厚度5) -
handConStyle
:连接线样式(绿色线条,厚度10)
-
xPos = int(lm.x * imgWidth) # 将归一化x坐标转换为像素坐标
yPos = int(lm.y * imgHeight) # 将归一化y坐标转换为像素坐标
- 坐标转换公式
像素坐标 = 归一化坐标 × 图像尺寸
示例:
若图像宽度imgWidth=640,某点lm.x=0.5 → xPos=320
若图像高度imgHeight=480,某点lm.y=0.25 → yPos=120
cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),2) # 在关键点旁标注索引数字
(四)完整代码
import cv2
import mediapipe as mp
import timecap = cv2.VideoCapture(0) # 通过OpenCV调用摄像头设备。参数0:默认摄像头(笔记本内置摄像头)。
mpHands = mp.solutions.hands # MediaPipe的手部关键点检测模型(21个关键点)
hands = mpHands.Hands() # 创建模型实例
mpDraw = mp.solutions.drawing_utils # MediaPipe提供的绘图工具,用于在图像上绘制关键点和连线。
handLmsStyle = mpDraw.DrawingSpec(color=(0, 0, 255), thickness=5) # 点的样式
handConStyle = mpDraw.DrawingSpec(color=(0, 255, 0), thickness=10) # 线的样式
pTime = 0
cTime = 0while True:ret, img = cap.read() # 从摄像头持续读取视频帧。OpenCV默认格式为BGR格式if ret:imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 格式转换,MediaPipe模型要求输入为RGB格式# 手部关键点检测result = hands.process(imgRGB)# print(result.multi_hand_landmarks)imgHeight = img.shape[0]imgWidth = img.shape[1]# 关键点可视化if result.multi_hand_landmarks:for handLms in result.multi_hand_landmarks: # 遍历每只检测到的手mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle,handConStyle) # 绘制手部关键点和骨骼连线for i, lm in enumerate(handLms.landmark): # 遍历21个关键点xPos = int(lm.x * imgWidth) # 将归一化x坐标转换为像素坐标yPos = int(lm.y * imgHeight) # 将归一化y坐标转换为像素坐标cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),2) # 在关键点旁标注索引数字if i == 4:cv2.circle(img, (xPos, yPos), 20, (166, 56, 56), cv2.FILLED)print(i, xPos, yPos) # 用深蓝色实心圆高亮标记拇指尖cTime = time.time()fps = 1 / (cTime - pTime)pTime = cTimecv2.putText(img, f"FPS:{int(fps)}", (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)cv2.imshow('img', img)if cv2.waitKey(1) == ord('q'):break
三、识别手指个数
(一)识别原理
1. 四指的判断
# 处理食指到小指
for i in range(1, 5):if handLms.landmark[fingerTips[i]].y < handLms.landmark[fingerTips[i] - 2].y:fingerState.append(1) # 伸出else:fingerState.append(0) # 弯曲
关键点:当食指远端指间关节(DIP,索引点8)在图像坐标系中的垂直位置高于近端指间关节(PIP,索引点6)时,即满足:y8<y6。
2. 拇指的判断
# 镜像翻转修正左右手问题img = cv2.flip(img, 1)...# 处理拇指(默认掌心朝镜头)if handType == 'Right': # 对于右手if handLms.landmark[fingerTips[0]].x < handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 右手拇指伸出else:fingerState.append(0) # 右手拇指弯曲else: # 对于左手if handLms.landmark[fingerTips[0]].x > handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 左手拇指伸出else:fingerState.append(0) # 左手拇指弯曲
镜像翻转的必要性:
MediaPipe基于深度卷积神经网络(CNN)架构,通过学习手部关键点的空间分布模式来区分左手和右手。因此会将拇指在图像左侧的手识别为物理右手。而摄像头原始画面中物理右手拇指实际位于右侧,因此必须通过cv2.flip(img, 1)水平镜像翻转图像,才能使MediaPipe正确识别手型。
坐标判断的底层逻辑:
所有关键点坐标均基于镜像翻转后的图像空间,物理右手在翻转后的坐标系中表现为thumb_tip.x < thumb_ip.x。MediaPipe内部已自动处理坐标转换,开发者直接使用检测到的归一化坐标即可,无需额外计算原始坐标。
(二)完整代码
import cv2
import mediapipe as mp
import timecap = cv2.VideoCapture(0)
mpHands = mp.solutions.hands
hands = mpHands.Hands()
mpDraw = mp.solutions.drawing_utils
handLmsStyle = mpDraw.DrawingSpec(color=(0, 0, 255), thickness=5) # 关键点样式
handConStyle = mpDraw.DrawingSpec(color=(0, 255, 0), thickness=10) # 连接线样式
pTime = 0# 定义手指关键点
fingerTips = [4, 8, 12, 16, 20] # 拇指、食指、中指、无名指、小指的指尖关键点索引while True:ret, img = cap.read()if ret:# 镜像翻转修正左右手问题img = cv2.flip(img, 1)imgRGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)result = hands.process(imgRGB)imgHeight, imgWidth, _ = img.shapeif result.multi_hand_landmarks:for handLms, handInfo in zip(result.multi_hand_landmarks, result.multi_handedness):mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle, handConStyle)# 获取手的类型:左手还是右手handType = handInfo.classification[0].labelhandLabel = "Right Hand" if handType == 'Right' else "Left Hand"# 手势计数fingerState = [] # 记录每根手指是否伸出# 处理食指到小指for i in range(1, 5):if handLms.landmark[fingerTips[i]].y < handLms.landmark[fingerTips[i] - 2].y:fingerState.append(1) # 伸出else:fingerState.append(0) # 弯曲# 处理拇指(默认掌心朝镜头)if handType == 'Right': # 对于右手if handLms.landmark[fingerTips[0]].x < handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 右手拇指伸出else:fingerState.append(0) # 右手拇指弯曲else: # 对于左手if handLms.landmark[fingerTips[0]].x > handLms.landmark[fingerTips[0] - 1].x:fingerState.append(1) # 左手拇指伸出else:fingerState.append(0) # 左手拇指弯曲# 计算伸出的手指数量fingerCount = sum(fingerState)# 在图像上显示手指数量cv2.putText(img, f"{handLabel}: {fingerCount} Fingers", (50, 100),cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 3)# 计算 FPScTime = time.time()fps = 1 / (cTime - pTime)pTime = cTimecv2.putText(img, f"FPS:{int(fps)}", (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 3)cv2.imshow('Hand Tracking', img)if cv2.waitKey(1) == ord('q'):breakcap.release()
cv2.destroyAllWindows()