在计算机视觉与手势交互领域,MediaPipe 凭借其轻量、高效的姿态/手势识别能力,成为开发创意应用的热门工具。本文将带大家突破"基础手势跟踪"的局限,掌握 **手势绘画的高级技巧**------包括动态笔触控制、多手势协同交互、画布分层管理,最终实现一个可通过手势"隔空作画"的交互式应用。文中附完整代码与效果演示,零基础也能快速上手。
一、核心技术栈与原理
在开始前,先明确我们用到的工具和核心逻辑,避免后续开发"知其然不知其所以然"。
1. 技术栈选型
-
Python 3.8+:主开发语言,生态丰富且易上手;
-
MediaPipe Hands:谷歌开源的手势识别库,可实时检测21个手部关键点(如指尖、指节);
-
OpenCV-Python:负责摄像头捕获、图像渲染与画布绘制;
-
NumPy:处理手部关键点坐标计算(如距离、角度)。
2. 手势绘画核心原理
MediaPipe Hands 会将每个手部关键点映射为屏幕坐标系中的 (x, y) 坐标。我们通过**解析关键点的位置关系**,判断用户的"绘画意图":
-
例1:当拇指与食指指尖距离小于阈值 → 判定为"选中画笔",移动时绘制轨迹;
-
例2:当五指张开 → 判定为"清空画布";
-
例3:当无名指弯曲、其他手指伸直 → 判定为"切换画笔颜色"。
通过这种"关键点关系→手势指令→画布操作"的映射,实现"无接触绘画"。
二、基础准备:环境搭建与核心函数
首先完成环境配置,并封装2个核心工具函数(手部检测、坐标转换),为后续高级功能打基础。
1. 环境安装
打开终端,执行以下命令安装依赖:
bash
pip install mediapipe opencv-python numpy
2. 核心工具函数封装
创建 hand_utils.py
文件,封装手部检测和坐标转换逻辑(避免主代码冗余):
python
import mediapipe as mp
import numpy as np
# 初始化 MediaPipe Hands
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
class HandDetector:
def __init__(self, max_num_hands=1, min_detection_confidence=0.7, min_tracking_confidence=0.7):
"""
初始化手势检测器
:param max_num_hands: 最大检测手数(默认1,避免多手干扰)
:param min_detection_confidence: 检测置信度阈值
:param min_tracking_confidence: 跟踪置信度阈值
"""
self.hands = mp_hands.Hands(
max_num_hands=max_num_hands,
min_detection_confidence=min_detection_confidence,
min_tracking_confidence=min_tracking_confidence
)
def detect_hands(self, frame):
"""
检测图像中的手部关键点
:param frame: OpenCV 读取的 BGR 图像(需转 RGB)
:return: 处理后的图像、手部关键点列表(含坐标)
"""
# BGR → RGB(MediaPipe 要求输入为 RGB)
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 禁用写操作(提升性能)
rgb_frame.flags.writeable = False
# 检测手部
results = self.hands.process(rgb_frame)
# 恢复写操作(后续绘制用)
rgb_frame.flags.writeable = True
# 存储关键点坐标(屏幕坐标系)
hand_landmarks = []
if results.multi_hand_landmarks:
for hand_lm in results.multi_hand_landmarks:
# 绘制关键点(可选,调试用)
mp_drawing.draw_landmarks(
frame, hand_lm, mp_hands.HAND_CONNECTIONS,
mp_drawing.DrawingSpec(color=(0,255,0), thickness=2, circle_radius=2),
mp_drawing.DrawingSpec(color=(0,0,255), thickness=2)
)
# 提取关键点坐标(转换为屏幕像素值)
h, w, _ = frame.shape
lm_list = [(int(lm.x * w), int(lm.y * h)) for lm in hand_lm.landmark]
hand_landmarks.append(lm_list)
return frame, hand_landmarks
def calculate_distance(point1, point2):
"""计算两点间欧氏距离(用于判断手指开合)"""
return np.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2)
三、高级技巧实战:5个核心功能实现
基础工具就绪后,我们逐一实现手势绘画的高级功能。最终效果是:**通过拇指+食指控制画笔,中指切换颜色,五指张开清空画布,握拳保存图片**。
1. 技巧1:动态笔触(根据手指距离调整画笔粗细)
传统手势绘画的笔触粗细固定,体验生硬。我们可以通过**拇指与食指的距离**动态调整画笔粗细------距离越远,笔触越粗;距离越近,笔触越细。
核心逻辑
-
计算拇指尖(关键点4)与食指尖(关键点8)的距离
dist
; -
将距离映射到
[2, 20]
的画笔粗细范围(避免过细或过粗); -
移动时,根据实时距离更新画笔粗细。
代码实现(片段)
python
# 假设 hand_lm 是当前手部关键点列表(来自 HandDetector)
thumb_tip = hand_lm[4] # 拇指尖
index_tip = hand_lm[8] # 食指尖
dist = calculate_distance(thumb_tip, index_tip)
# 距离映射为画笔粗细(dist范围:20~200 → 粗细2~20)
brush_thickness = int(np.interp(dist, [20, 200], [2, 20]))
2. 技巧2:多手势协同(绘画/切换/清空/保存)
通过解析不同手指的状态,实现多指令协同------避免依赖键盘/鼠标,纯手势操作更自然。
手势指令定义
先明确4个核心手势的判定规则(基于 MediaPipe 21个关键点):
|------|-----------------------------------------|
| 功能 | 手势判定规则 |
| 绘画 | 拇指与食指距离 < 30("捏合"状态),且其他手指弯曲(避免误触) |
| 切换颜色 | 中指伸直(关键点12的y坐标 < 关键点10的y坐标),其他手指保持绘画姿势 |
| 清空画布 | 五指均伸直(拇指尖y < 指节y,其他指尖y < 对应指节y) |
| 保存图片 | 握拳(所有指尖y > 对应指节y,且拇指与食指距离 > 50) |
代码实现(片段)
python
def is_drawing(hand_lm):
"""判定是否为"绘画手势":拇指+食指捏合,其他手指弯曲"""
thumb_tip = hand_lm[4]
index_tip = hand_lm[8]
# 拇指与食指距离 < 30(捏合)
if calculate_distance(thumb_tip, index_tip) > 30:
return False
# 中指、无名指、小指弯曲(指尖y > 指节y)
middle_tip = hand_lm[12]
middle_mcp = hand_lm[10] # 中指掌指关节
ring_tip = hand_lm[16]
ring_mcp = hand_lm[14]
pinky_tip = hand_lm[20]
pinky_mcp = hand_lm[18]
if (middle_tip[1] < middle_mcp[1]) or (ring_tip[1] < ring_mcp[1]) or (pinky_tip[1] < pinky_mcp[1]):
return False
return True
def is_switch_color(hand_lm):
"""判定是否为"切换颜色手势":中指伸直"""
if not is_drawing(hand_lm):
return False
middle_tip = hand_lm[12]
middle_mcp = hand_lm[10]
return middle_tip[1] < middle_mcp[1] # 中指指尖高于掌指关节(伸直)
def is_clear_canvas(hand_lm):
"""判定是否为"清空画布手势":五指伸直"""
# 拇指伸直(指尖x < 指节x,因拇指方向与其他手指不同)
thumb_tip = hand_lm[4]
thumb_ip = hand_lm[3] # 拇指指间关节
if thumb_tip[0] > thumb_ip[0]:
return False
# 其他四指伸直(指尖y < 掌指关节y)
fingers = [(8,10), (12,14), (16,18), (20,22)] # (指尖, 掌指关节)
for tip_idx, mcp_idx in fingers:
if hand_lm[tip_idx][1] > hand_lm[mcp_idx][1]:
return False
return True
def is_save_image(hand_lm):
"""判定是否为"保存图片手势":握拳"""
# 所有指尖y > 对应掌指关节y(弯曲)
fingers = [(4,2), (8,6), (12,10), (16,14), (20,18)]
for tip_idx, mcp_idx in fingers:
if hand_lm[tip_idx][1] < hand_lm[mcp_idx][1]:
return False
# 拇指与食指距离 > 50(避免与"绘画捏合"混淆)
if calculate_distance(hand_lm[4], hand_lm[8]) < 50:
return False
return True
3. 技巧3:画布分层管理(避免画面混乱)
如果直接在摄像头帧上绘画,画面会随摄像头移动而"抖动"。我们可以创建一个**独立的画布层**,将绘画轨迹保存在画布上,再与摄像头帧叠加显示------实现"固定画布+动态手势"的效果。
代码实现(片段)
python
import cv2
import time
from hand_utils import HandDetector, calculate_distance
# 初始化画布(与摄像头分辨率一致,黑色背景)
cap = cv2.VideoCapture(0) # 0表示默认摄像头
ret, frame = cap.read()
if not ret:
raise Exception("无法打开摄像头")
h, w, _ = frame.shape
canvas = np.zeros((h, w, 3), dtype=np.uint8) # 黑色画布
# 初始化画笔参数
current_color = (255, 0, 0) # 初始颜色:蓝色
color_list = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255)] # 颜色列表
color_idx = 0
prev_pos = None # 上一帧画笔位置(用于绘制连续轨迹)
detector = HandDetector()
# 主循环
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
# 水平翻转帧(镜像效果,操作更直观)
frame = cv2.flip(frame, 1)
# 检测手部关键点
frame, hand_landmarks = detector.detect_hands(frame)
if hand_landmarks:
hand_lm = hand_landmarks[0] # 只处理第一只手
current_pos = hand_lm[8] # 画笔位置:食指尖
# 1. 清空画布
if is_clear_canvas(hand_lm):
canvas = np.zeros((h, w, 3), dtype=np.uint8)
time.sleep(0.5) # 防抖(避免误触多次清空)
prev_pos = None
# 2. 保存图片
elif is_save_image(hand_lm):
save_path = f"hand_drawing_{time.time()}.png"
cv2.imwrite(save_path, canvas)
print(f"图片已保存至:{save_path}")
# 在帧上显示提示
cv2.putText(frame, "Saved!", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)
time.sleep(1) # 防抖
# 3. 切换颜色
elif is_switch_color(hand_lm):
color_idx = (color_idx + 1) % len(color_list)
current_color = color_list[color_idx]
# 在帧上显示当前颜色
cv2.circle(frame, (50, 100), 20, current_color, -1)
time.sleep(0.5) # 防抖
# 4. 绘画(连续轨迹)
elif is_drawing(hand_lm):
# 计算动态画笔粗细
dist = calculate_distance(hand_lm[4], hand_lm[8])
brush_thickness = int(np.interp(dist, [20, 200], [2, 20]))
# 绘制线段(上一位置→当前位置)
if prev_pos is not None:
cv2.line(canvas, prev_pos, current_pos, current_color, brush_thickness)
prev_pos = current_pos # 更新上一位置
# 非绘画状态:重置上一位置
else:
prev_pos = None
# 叠加画布与摄像头帧(半透明效果,更美观)
combined_frame = cv2.addWeighted(frame, 0.7, canvas, 0.3, 0)
# 显示操作提示
cv2.putText(combined_frame, "Pinch: Draw | Open Hand: Clear | Fist: Save",
(10, h-20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 1)
# 显示窗口
cv2.imshow("Hand Drawing (Press 'q' to quit)", combined_frame)
cv2.imshow("Canvas", canvas) # 单独显示画布(可选)
# 按 'q' 退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放资源
cap.release()
cv2.destroyAllWindows()
4. 技巧4:防抖动处理(避免误操作)
手势识别中,手指轻微抖动可能导致"误触发"(如误判"清空画布")。我们通过2种方式防抖:
-
时间防抖 :触发指令后延迟0.5~1秒,期间不响应同指令(如
time.sleep(0.5)
); -
多帧验证:连续2~3帧检测到同一手势,才执行指令(进阶优化,代码可扩展)。
5. 技巧5:镜像显示与视觉反馈
为提升用户体验,增加2个细节优化:
-
镜像显示 :用
cv2.flip(frame, 1)
水平翻转摄像头帧,让手势操作与屏幕显示"同步"(就像照镜子); -
视觉反馈:
-
切换颜色时,在屏幕左上角画一个彩色圆点,提示当前颜色;
-
保存图片时,显示"Saved!"文字;
-
绘制时,实时显示画笔轨迹(画布与帧叠加)。
-
四、效果演示与优化方向
1. 效果展示(图文说明)
|--------|-----------------------------------------|----------------------------|
| 功能场景 | 实际效果描述 | 示意图(文字描述) |
| 动态笔触绘画 | 拇指与食指捏合,移动时绘制蓝色轨迹;手指张开一点,笔触变粗;靠近一点,笔触变细 | 屏幕显示:蓝色线条,粗细随手指距离变化 |
| 切换颜色 | 保持绘画姿势,伸直中指 → 颜色从蓝色切换为绿色,左上角出现绿色圆点提示 | 左上角:绿色圆点;画布轨迹:从蓝变绿 |
| 清空画布 | 五指完全张开 → 画布瞬间变黑(所有轨迹清除) | 画布:从有图案变为纯黑色 |
| 保存图片 | 握拳 → 控制台打印保存路径,屏幕显示"Saved!",画布图片保存到本地 | 屏幕:"Saved!"文字;本地:新增 PNG 文件 |
2. 进阶优化方向
如果想进一步提升应用体验,可以尝试以下方向:
-
多手协同 :支持左手控制颜色/粗细,右手绘画(修改
HandDetector
的max_num_hands=2
); -
形状识别:通过关键点识别"圆形""方形"手势,自动绘制对应图形;
-
撤销功能:记录画布历史状态,通过"双击手势"实现撤销上一步;
-
背景虚化:用 MediaPipe Selfie Segmentation 虚化摄像头背景,突出手势与画布;
-
导出视频:用 OpenCV 录制绘画过程,生成 MP4 视频。
五、完整代码汇总
将上述代码整合为一个完整文件 hand_drawing.py
,直接运行即可:
python
import cv2
import time
import numpy as np
import mediapipe as mp
# ---------------------- 初始化 MediaPipe Hands ----------------------
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
class HandDetector:
def __init__(self, max_num_hands=1, min_detection_confidence=0.7, min_tracking_confidence=0.7):
self.hands = mp_hands.Hands(
max_num_hands=max_num_hands,
min_detection_confidence=min_detection_confidence,
min_tracking_confidence=min_tracking_confidence
)
def detect_hands(self, frame):
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
rgb_frame.flags.writeable = False
results = self.hands.process(rgb_frame)
rgb_frame.flags.writeable = True
hand_landmarks = []
if results.multi_hand_landmarks:
for hand_lm in results.multi_hand_landmarks:
mp_drawing.draw_landmarks(
frame, hand_lm, mp_hands.HAND_CONNECTIONS,
mp_drawing.DrawingSpec(color=(0,255,0), thickness=2, circle_radius=2),
mp_drawing.DrawingSpec(color=(0,0,255), thickness=2)
)
h, w, _ = frame.shape
lm_list = [(int(lm.x * w), int(lm.y * h)) for lm in hand_lm.landmark]
hand_landmarks.append(lm_list)
return frame, hand_landmarks
# ---------------------- 辅助函数 ----------------------
def calculate_distance(point1, point2):
return np.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2)
def is_drawing(hand_lm):
thumb_tip = hand_lm[4]
index_tip = hand_lm[8]
if calculate_distance(thumb_tip, index_tip) > 30:
return False
middle_tip = hand_lm[12]
middle_mcp = hand_lm[10]
ring_tip = hand_lm[16]
ring_mcp = hand_lm[14]
pinky_tip = hand_lm[20]
pinky_mcp = hand_lm[18]
if (middle_tip[1] < middle_mcp[1]) or (ring_tip[1] < ring_mcp[1]) or (pinky_tip[1] < pinky_mcp[1]):
return False
return True
def is_switch_color(hand_lm):
if not is_drawing(hand_lm):
return False
middle_tip = hand_lm[12]
middle_mcp = hand_lm[10]
return middle_tip[1] < middle_mcp[1]
def is_clear_canvas(hand_lm):
thumb_tip = hand_lm[4]
thumb_ip = hand_lm[3]
if thumb_tip[0] > thumb_ip[0]:
return False
fingers = [(8,10), (12,14), (16,18), (20,22)]
for tip_idx, mcp_idx in fingers:
if hand_lm[tip_idx][1] > hand_lm[mcp_idx][1]:
return False
return True
def is_save_image(hand_lm):
fingers = [(4,2), (8,6), (12,10), (16,14), (20,18)]
for tip_idx, mcp_idx in fingers:
if hand_lm[tip_idx][1] < hand_lm[mcp_idx][1]:
return False
if calculate_distance(hand_lm[4], hand_lm[8]) < 50:
return False
return True
# ---------------------- 主程序 ----------------------
if __name__ == "__main__":
# 初始化摄像头与画布
cap = cv2.VideoCapture(0)
ret, frame = cap.read()
if not ret:
raise Exception("无法打开摄像头,请检查设备连接")
h, w, _ = frame.shape
canvas = np.zeros((h, w, 3), dtype=np.uint8)
# 画笔参数
color_list = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255)]
color_idx = 0
current_color = color_list[color_idx]
prev_pos = None
detector = HandDetector()
# 主循环
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
frame, hand_landmarks = detector.detect_hands(frame)
if hand_landmarks:
hand_lm = hand_landmarks[0]
current_pos = hand_lm[8]
# 清空画布
if is_clear_canvas(hand_lm):
canvas = np.zeros((h, w, 3), dtype=np.uint8)
time.sleep(0.5)
prev_pos = None
# 保存图片
elif is_save_image(hand_lm):
save_path = f"hand_drawing_{int(time.time())}.png"
cv2.imwrite(save_path, canvas)
cv2.putText(frame, "Saved!", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,255,0), 2)
time.sleep(1)
# 切换颜色
elif is_switch_color(hand_lm):
color_idx = (color_idx + 1) % len(color_list)
current_color = color_list[color_idx]
cv2.circle(frame, (50, 100), 20, current_color, -1)
time.sleep(0.5)
# 绘画
elif is_drawing(hand_lm):
dist = calculate_distance(hand_lm[4], hand_lm[8])
brush_thickness = int(np.interp(dist, [20, 200], [2, 20]))
if prev_pos is not None:
cv2.line(canvas, prev_pos, current_pos, current_color, brush_thickness)
prev_pos = current_pos
else:
prev_pos = None
# 叠加画布与帧
combined_frame = cv2.addWeighted(frame, 0.7, canvas, 0.3, 0)
# 显示提示
cv2.putText(combined_frame, "Pinch:Draw | Open:Clear | Fist:Save | Middle:Color",
(10, h-20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 1)
# 显示窗口
cv2.imshow("Hand Drawing (Press 'q' to quit)", combined_frame)
cv2.imshow("Canvas", canvas)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 释放资源
cap.release()
cv2.destroyAllWindows()
六、运行说明
-
确保电脑已连接摄像头;
-
运行代码:
python hand_drawing.py
; -
操作指南:
-
拇指+食指捏合 → 移动绘制;
-
保持捏合,伸直中指 → 切换颜色;
-
五指张开 → 清空画布;
-
握拳 → 保存图片;
-
按
q
键退出程序。
-
通过本文的高级技巧,你不仅能实现"隔空绘画",更能理解 MediaPipe 手势识别的核心逻辑------在此基础上,还可以扩展出更多创意应用(如手势控制PPT、游戏交互等)。动手试试吧!