Python + MediaPipe 手势绘画高级应用:从基础到创意交互

在计算机视觉与手势交互领域,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种方式防抖:

  1. 时间防抖 :触发指令后延迟0.5~1秒,期间不响应同指令(如 time.sleep(0.5));

  2. 多帧验证:连续2~3帧检测到同一手势,才执行指令(进阶优化,代码可扩展)。

5. 技巧5:镜像显示与视觉反馈

为提升用户体验,增加2个细节优化:

  • 镜像显示 :用 cv2.flip(frame, 1) 水平翻转摄像头帧,让手势操作与屏幕显示"同步"(就像照镜子);

  • 视觉反馈

    • 切换颜色时,在屏幕左上角画一个彩色圆点,提示当前颜色;

    • 保存图片时,显示"Saved!"文字;

    • 绘制时,实时显示画笔轨迹(画布与帧叠加)。

四、效果演示与优化方向

1. 效果展示(图文说明)

|--------|-----------------------------------------|----------------------------|
| 功能场景 | 实际效果描述 | 示意图(文字描述) |
| 动态笔触绘画 | 拇指与食指捏合,移动时绘制蓝色轨迹;手指张开一点,笔触变粗;靠近一点,笔触变细 | 屏幕显示:蓝色线条,粗细随手指距离变化 |
| 切换颜色 | 保持绘画姿势,伸直中指 → 颜色从蓝色切换为绿色,左上角出现绿色圆点提示 | 左上角:绿色圆点;画布轨迹:从蓝变绿 |
| 清空画布 | 五指完全张开 → 画布瞬间变黑(所有轨迹清除) | 画布:从有图案变为纯黑色 |
| 保存图片 | 握拳 → 控制台打印保存路径,屏幕显示"Saved!",画布图片保存到本地 | 屏幕:"Saved!"文字;本地:新增 PNG 文件 |

2. 进阶优化方向

如果想进一步提升应用体验,可以尝试以下方向:

  • 多手协同 :支持左手控制颜色/粗细,右手绘画(修改 HandDetectormax_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()

六、运行说明

  1. 确保电脑已连接摄像头;

  2. 运行代码:python hand_drawing.py

  3. 操作指南:

    1. 拇指+食指捏合 → 移动绘制;

    2. 保持捏合,伸直中指 → 切换颜色;

    3. 五指张开 → 清空画布;

    4. 握拳 → 保存图片;

    5. q 键退出程序。

通过本文的高级技巧,你不仅能实现"隔空绘画",更能理解 MediaPipe 手势识别的核心逻辑------在此基础上,还可以扩展出更多创意应用(如手势控制PPT、游戏交互等)。动手试试吧!

相关推荐
会跑的葫芦怪3 小时前
Go tool pprof 与 Gin 框架性能分析完整指南
开发语言·golang·gin
MoRanzhi12033 小时前
12. NumPy 数据分析与图像处理入门
大数据·图像处理·人工智能·python·矩阵·数据分析·numpy
爱学习的小道长3 小时前
Python调用优云智算安装的ComfyUI服务器
服务器·开发语言·python
要做朋鱼燕3 小时前
解析UART空闲中断与DMA接收机制
开发语言·笔记·单片机·嵌入式硬件·rtos·嵌入式软件
Dream achiever3 小时前
11.WPF 的命令处理事件--参数介绍
开发语言·c#·wpf
databook3 小时前
让YOLO飞起来:从CPU到GPU的配置指南
人工智能·python·图像识别
_bong3 小时前
python语言中的常用容器(集合)
开发语言·python
布伦鸽4 小时前
C# WPF DataGrid 数据绑定时的单元格编辑类型模板
开发语言·c#·wpf
tqs_123454 小时前
分sheet写入excel
开发语言·python·算法