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

相关推荐
AI攻城狮34 分钟前
RAG Chunking 为什么这么难?5 大挑战 + 最佳实践指南
人工智能·云原生·aigc
yiyu07161 小时前
3分钟搞懂深度学习AI:梯度下降:迷雾中的下山路
人工智能·深度学习
掘金安东尼1 小时前
玩转龙虾🦞,openclaw 核心命令行收藏(持续更新)v2026.3.2
人工智能
demo007x1 小时前
万字长文解读ClaudeCode/KiloCode 文件处理技术
人工智能·claude·trae
aircrushin2 小时前
OpenClaw开源生态与AI执行能力的产业化路径
人工智能
是糖糖啊2 小时前
OpenClaw 从零到一实战指南(飞书接入)
前端·人工智能·后端
踩着两条虫2 小时前
从设计稿到代码:VTJ.PRO 的 AI 集成系统架构解析
前端·vue.js·人工智能
孤烟2 小时前
吓瘫!我用1行代码攻破公司自研AI权限系统,数据裸奔一整夜(附攻击payload+防御源码)
人工智能·ai编程
掘金一周2 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了 | 掘金一周 3.5
前端·人工智能·agent
CoovallyAIHub2 小时前
Moonshine:比 Whisper 快 100 倍的端侧语音识别神器,Star 6.6K!
深度学习·算法·计算机视觉