一、技术栈
1. OpenCV(Open Source Computer Vision Library)
-
性质 :开源计算机视觉库(Library)
-
主要功能:
-
图像/视频的基础处理(读取、裁剪、滤波、色彩转换等)
-
特征检测(边缘、角点等)
-
摄像头标定、目标跟踪等
-
-
在项目中的作用:
-
负责视频流的捕获(
cv2.VideoCapture
) -
图像格式转换(
cv2.cvtColor
) -
最终结果的渲染显示(
cv2.imshow
)
-
2. MediaPipe
-
性质 :由Google开发的跨平台机器学习框架(Framework)
-
主要功能:
-
提供预训练的端到端模型(如手部关键点、人脸网格、姿态估计等)
-
专注于实时感知任务(低延迟、移动端优化)
-
-
在项目中的作用:
-
调用
mediapipe.solutions.hands
模型实现21个手部关键点检测 -
输出关键点坐标,并通过
mpDraw
可视化
-
二、手部关键点检测
(一)初始化
python
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
(二)关键点检测
python
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]
重要代码解释:
python
result = hands.process(imgRGB)
- 底层过程:
图像输入MediaPipe手部模型
模型输出包含:
multi_hand_landmarks:21个关键点的归一化坐标(0~1之间)
multi_handedness:左右手判断
- result 数据结构:
类型:List(列表)
内容:每个元素代表一只手的21个关键点数据(因此result.multi_hand_landmarks最多 有两个元素)
层级关系:
python
result.multi_hand_landmarks[0] # 第1只手
.landmark[0] # 第1个关键点
.x # 归一化x坐标 (0.0~1.0)
.y # 归一化y坐标 (0.0~1.0)
.z # 相对深度(值越小越靠近摄像头)

(三)可视化
python
# 关键点可视化
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) # 用深蓝色实心圆高亮标记拇指尖
重要代码解释:
python
mpDraw.draw_landmarks(img, handLms, mpHands.HAND_CONNECTIONS, handLmsStyle, handConStyle)
-
功能:绘制手部关键点和骨骼连线
-
参数详解:
-
img
:目标图像(OpenCV格式) -
handLms
:当前手的关键点数据 -
mpHands.HAND_CONNECTIONS
:预定义的关键点连接关系(如点0-1相连,点1-2相连等) -
handLmsStyle
:关键点绘制样式(红色圆点,厚度5) -
handConStyle
:连接线样式(绿色线条,厚度10)
-
python
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
python
cv2.putText(img, str(i), (xPos - 25, yPos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255),
2) # 在关键点旁标注索引数字

(四)完整代码
python
import cv2
import mediapipe as mp
import time
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
while 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 = cTime
cv2.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. 四指的判断
python
# 处理食指到小指
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. 拇指的判断
python
# 镜像翻转修正左右手问题
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内部已自动处理坐标转换,开发者直接使用检测到的归一化坐标即可,无需额外计算原始坐标。
(二)完整代码
python
import cv2
import mediapipe as mp
import time
cap = 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.shape
if 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].label
handLabel = "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)
# 计算 FPS
cTime = time.time()
fps = 1 / (cTime - pTime)
pTime = cTime
cv2.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'):
break
cap.release()
cv2.destroyAllWindows()