PiscCode:用 MediaPipe 实现手势控制输入尝试之俄罗斯方块

这是一次偏探索性质的实验:尝试用摄像头识别手势,并将其映射成真实键盘输入,用于简单游戏或交互控制。

目标不是做完整产品,而是验证几个问题:

  • MediaPipe 手部关键点是否足够稳定?

  • 能否在不加复杂滤波的情况下实现可用输入?

  • 如何避免误触与抖动?


🎯 实验目标

实现最小可用交互模型:

手势 行为
食指指向方向 方向键单击
食指 + 中指 方向键长按
手收回 松键

🔧 技术选型

  • MediaPipe Hand Landmarker:手部关键点检测

  • OpenCV:摄像头输入与画面展示

  • pynput:模拟系统键盘输入

  • Python 状态机逻辑:抑制抖动与重复触发


🧠 核心思路

整体流程非常直接:

复制代码
摄像头 → 手部关键点 → 手指方向判断 → 输入状态机 → 键盘事件

重点不在识别算法,而在:

  • 如何把连续视觉信号变成离散输入事件

  • 如何保证按键只触发一次或稳定长按


🧩 实验代码

复制代码
import cv2
import numpy as np
import mediapipe as mp
from mediapipe import solutions
from mediapipe.framework.formats import landmark_pb2
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
from pynput.keyboard import Controller, Key


class HANDPLAY:
    def __init__(self, model_path="hand_landmarker.task",
                 num_hands=2,
                 mirror=True):
        base_options = python.BaseOptions(model_asset_path=model_path)
        options = vision.HandLandmarkerOptions(
            base_options=base_options,
            num_hands=num_hands
        )
        self.detector = vision.HandLandmarker.create_from_options(options)

        self.keyboard = Controller()
        self.key_map = {
            "Up": Key.up,
            "Down": Key.down,
            "Left": Key.left,
            "Right": Key.right
        }

        self.mode = "idle"   # idle | click | hold
        self.active_dir = None
        self.mirror = mirror

    # -------------------------
    # 工具函数
    # -------------------------
    def _finger_direction(self, lm, tip_id, base_id):
        tip = lm[tip_id]
        base = lm[base_id]

        dx = tip.x - base.x
        dy = tip.y - base.y

        if self.mirror:
            dx = -dx

        if abs(dx) > abs(dy):
            return "Right" if dx > 0 else "Left"
        else:
            return "Up" if dy < 0 else "Down"

    def _finger_extended(self, lm, tip_id, pip_id):
        return lm[tip_id].y < lm[pip_id].y

    # -------------------------
    # 输入状态机
    # -------------------------
    def _handle_input(self, lm):
        index_ext = self._finger_extended(lm, 8, 6)
        middle_ext = self._finger_extended(lm, 12, 10)

        if not index_ext:
            self._reset()
            return

        direction = self._finger_direction(lm, 8, 5)
        key = self.key_map[direction]

        # 长按
        if middle_ext:
            if self.mode != "hold" or self.active_dir != direction:
                self._reset()
                self.keyboard.press(key)
                print(f"[HOLD] {direction}")
                self.mode = "hold"
                self.active_dir = direction
            return

        # 单击
        if self.mode != "click" or self.active_dir != direction:
            self._reset()
            self.keyboard.press(key)
            self.keyboard.release(key)
            print(f"[CLICK] {direction}")
            self.mode = "click"
            self.active_dir = direction

    def _reset(self):
        if self.mode == "hold" and self.active_dir:
            self.keyboard.release(self.key_map[self.active_dir])
            print(f"[RELEASE] {self.active_dir}")
        self.mode = "idle"
        self.active_dir = None

    # -------------------------
    # 绘制 + 主流程
    # -------------------------
    def _draw_landmarks_on_image(self, rgb_image, detection_result):
        annotated = np.copy(rgb_image)

        if detection_result.hand_landmarks:
            for lm in detection_result.hand_landmarks:
                proto = landmark_pb2.NormalizedLandmarkList()
                proto.landmark.extend([
                    landmark_pb2.NormalizedLandmark(x=p.x, y=p.y, z=p.z)
                    for p in lm
                ])

                solutions.drawing_utils.draw_landmarks(
                    annotated,
                    proto,
                    mp.solutions.hands.HAND_CONNECTIONS,
                    solutions.drawing_styles.get_default_hand_landmarks_style(),
                    solutions.drawing_styles.get_default_hand_connections_style()
                )

                self._handle_input(lm)
        else:
            self._reset()

        return annotated

    def do(self, frame, device=None):
        if frame is None:
            return None

        mp_image = mp.Image(
            image_format=mp.ImageFormat.SRGB,
            data=cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        )

        result = self.detector.detect(mp_image)
        annotated = self._draw_landmarks_on_image(mp_image.numpy_view(), result)

        return cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR)

🔍 关键技术尝试点

1️⃣ 如何判断方向?

用食指指尖(8)与指根(5)构成向量:

复制代码
dx = tip.x - base.x dy = tip.y - base.y

再比较水平和垂直分量大小来分类方向。


2️⃣ 如何判断手指是否伸直?

复制代码
lm[tip].y < lm[pip].y

在 MediaPipe 坐标中,y 越小表示越靠上,因此可用于检测伸展状态。


3️⃣ 为什么需要状态机?

如果直接在每一帧触发按键,会导致:

  • 每秒几十次点击

  • 长按无法稳定释放

  • 手抖造成方向跳变

于是引入:

复制代码
idle → click → hold

状态流转模型来稳定输入。


4️⃣ 为什么要镜像修正?

摄像头画面通常左右翻转,如果不处理:

👉 你向右指,系统认为你向左。

所以加入:

复制代码
if self.mirror: dx = -dx

🧪 实验结果

在普通笔记本摄像头下:

项目 表现
延迟 ≈30ms
稳定度 可连续控制游戏角色
误触 偶发,主要来自遮挡

整体已经达到"可玩"的实验级效果。


🧠 一些未解决 / 可改进点

  • ❌ 没做时间滤波,快速抖动仍可能误触

  • ❌ 没区分左右手

  • ❌ 没做手势组合识别(如拳头/OK/张掌)


🏁 总结

这是一次偏 技术验证型实验

不追求产品级完整度,而是验证

👉 MediaPipe + Python 能否快速搭建可用的隔空输入系统。

事实证明:

  • 算法层不复杂

  • 输入稳定性关键在状态机设计

  • 小量工程控制就能达到可用体验

对 PiscTrace or PiscCode感兴趣?更多精彩内容请移步官网看看~🔗 PiscTrace

相关推荐
九.九13 小时前
ops-transformer:AI 处理器上的高性能 Transformer 算子库
人工智能·深度学习·transformer
春日见13 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
恋猫de小郭13 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
deephub13 小时前
Agent Lightning:微软开源的框架无关 Agent 训练方案,LangChain/AutoGen 都能用
人工智能·microsoft·langchain·大语言模型·agent·强化学习
大模型RAG和Agent技术实践13 小时前
从零构建本地AI合同审查系统:架构设计与流式交互实战(完整源代码)
人工智能·交互·智能合同审核
老邋遢13 小时前
第三章-AI知识扫盲看这一篇就够了
人工智能
互联网江湖13 小时前
Seedance2.0炸场:长短视频们“修坝”十年,不如AI放水一天?
人工智能
PythonPioneer14 小时前
在AI技术迅猛发展的今天,传统职业该如何“踏浪前行”?
人工智能
冬奇Lab14 小时前
一天一个开源项目(第20篇):NanoBot - 轻量级AI Agent框架,极简高效的智能体构建工具
人工智能·开源·agent
阿里巴巴淘系技术团队官网博客15 小时前
设计模式Trustworthy Generation:提升RAG信赖度
人工智能·设计模式