脚本专家 发表于 2026-6-15 09:00:01

Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战

本文介绍一个不到200行的Python脚本,利用MediaPipe HandLandmarker模型识别手指数量,并通过稳定窗口和armed标志实现单次触发,自动打开Windows下载文件夹中最新的一张或多张PNG图片。适合希望通过摄像头做手势控制的开发者参考。

一、背景与目标

写代码时常需要查看刚下载的截图,手动打开“下载”文件夹再按时间排序双击,步骤繁琐。因此通过摄像头识别手势:比划“1”打开下载夹里最新的PNG,“2”打开第二新的,依此类推到“5”;握拳(0根手指)解除武装等待下一次触发。

二、环境准备

MediaPipe 0.10.x版本后全面采用Tasks API,不再支持旧的mp.solutions.hands。Python版本方面,目前MediaPipe对Python 3.13没有发布wheel,推荐使用Python 3.10。建议创建虚拟环境:

py -3.10 -m venv venv
venv\Scripts\activate
pip install mediapipe opencv-python pillow numpy

模型文件需要单独下载:
https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/latest/hand_landmarker.task
放入项目根目录的models/hand_landmarker.task。项目目录结构:

MediaPipe/
├── models/hand_landmarker.task
├── demo_hand_landmarker.py
└── venv/


三、MediaPipe Tasks最小可用骨架

旧版mp.solutions.hands在0.10.35之后已移除,必须使用新的Tasks API。以下是最小骨架:

import mediapipe as mp
from mediapipe.tasks import python as mp_python
from mediapipe.tasks.python import vision

options = vision.HandLandmarkerOptions(
    base_options=mp_python.BaseOptions(model_asset_path="models/hand_landmarker.task"),
    running_mode=vision.RunningMode.LIVE_STREAM,# 摄像头流式推理
    num_hands=1,
    min_hand_detection_confidence=0.5,
    min_hand_presence_confidence=0.5,
    min_tracking_confidence=0.5,
    result_callback=on_result,# 异步结果回调
)

LIVE_STREAM模式下,detect_async()立即返回,结果通过回调异步传递。主循环不能阻塞等待,应直接读取“上一次最新结果”。
使用一个线程安全的容器保存最新结果:

class LatestResult:
    def __init__(self):
      self.result = None

latest = LatestResult()

def on_result(result, output_image, timestamp_ms):
    latest.result = result

每一帧将BGR转RGB包成mp.Image,再传入单调递增的时间戳:

start_ns = time.perf_counter_ns()
# ...
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
ts_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
landmarker.detect_async(mp_image, ts_ms)

时间戳必须严格递增,否则MediaPipe会直接抛错丢弃帧。这是新手最容易踩的坑。

四、HandLandmarker输出解析

模型输出21个3D归一化坐标(x,y ∈ ,z相对于手腕深度)。索引含义:

0WRIST
1-4 THUMB
5-8 INDEX
9-12 MIDDLE
13-16 RING
17-20 PINKY

每根手指有4个点:MCP(指根)、PIP、DIP、TIP(指尖)。本项目只关心TIP和PIP。绘制连线的连接表(原mp.solutions.hands.HAND_CONNECTIONS已不可用,需手动写死):

HAND_CONNECTIONS = (
    (0,1),(1,2),(2,3),(3,4),
    (0,5),(5,6),(6,7),(7,8),
    (5,9),(9,10),(10,11),(11,12),
    (9,13),(13,14),(14,15),(15,16),
    (13,17),(0,17),(17,18),(18,19),(19,20),
)


五、朴素而鲁棒的手指计数算法

很多教程比较TIP和PIP的y坐标,但手歪时容易误判。更鲁棒的办法:计算TIP到WRIST的距离与PIP到WRIST的距离之比,若比值大于1.1则认为手指伸直。因为弯曲时TIP会靠近手腕,距离必然减小。

FINGER_TIPS = (4, 8, 12, 16, 20)
FINGER_PIPS = (3, 6, 10, 14, 18)

def count_extended_fingers(landmarks) -> int:
    wrist = landmarks
    def d(a, b):
      return math.hypot(a.x - b.x, a.y - b.y)
    n = 0
    for tip, pip in zip(FINGER_TIPS, FINGER_PIPS):
      if d(landmarks, wrist) > d(landmarks, wrist) * 1.1:
            n += 1
    return n

1.1是经验阈值,调整时需平衡灵敏与误判。

六、防抖:稳定窗口+armed状态机

直接使用瞬时值会导致识别抖动时反复触发。分两步处理:
1. 稳定窗口:连续N帧均为同一值才算稳定。使用deque(maxlen=10)(约0.3-0.5秒):

history = deque(maxlen=10)
history.append(cur_n)
stable = history if len(history) == history.maxlen and all(v == history for v in history) else None

2. armed标志:触发后必须回到0才能再次触发。实现边沿触发而非电平触发:

if stable == 0:
    armed = True
elif stable is not None and 1 <= stable <= 5 and armed:
    open_nth_png(stable)
    armed = False
    history.clear()# 清空缓冲,避免同一稳定段重复识别


七、按时间倒序打开第N新的PNG

使用Pathlib和os.startfile(Windows专属):

from pathlib import Path
import os

DOWNLOADS_DIR = Path.home() / "Downloads"

def open_nth_png(n: int):
    files = sorted(
      DOWNLOADS_DIR.glob("*.png"),
      key=lambda p: p.stat().st_mtime,
      reverse=True,
    )
    if len(files) < n:
      return False, f"Only {len(files)} PNG(s), need #{n}"
    target = files
    os.startfile(str(target))
    return True, f"Opened #{n}: {target.name}"

如果需要跨平台,macOS用subprocess.run(["open", path]),Linux用xdg-open。

八、画面镜像与左右手反转

cv2.flip(frame, 1)让画面照镜子,但MediaPipe返回的handedness也跟随镜像。纠正方法:

raw = result.handedness.category_name
label = "Right" if raw == "Left" else "Left"


九、完整主循环

整合以上部件:

with vision.HandLandmarker.create_from_options(options) as landmarker:
    while True:
      ok, frame = cap.read()
      if not ok:
            break
      frame = cv2.flip(frame, 1)
      rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
      mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
      ts_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
      landmarker.detect_async(mp_image, ts_ms)

      res = latest.result
      cur_n = count_extended_fingers(res.hand_landmarks) if (res and res.hand_landmarks) else 0
      history.append(cur_n)
      stable = history if len(history) == history.maxlen \
            and all(v == history for v in history) else None

      if stable == 0:
            armed = True
      elif stable is not None and 1 <= stable <= 5 and armed:
            ok2, msg = open_nth_png(stable)
            armed = False
            history.clear()

      draw_landmarks(frame, res)
      cv2.imshow("Gesture -> Open PNG", frame)
      if cv2.waitKey(1) & 0xFF in (ord("q"), 27):
            break


十、常见坑点

- No module named 'mediapipe':多半没激活虚拟环境,或使用了系统Python 3.13。先用where python确认路径。
- module 'mediapipe' has no attribute 'solutions':装了0.10.x新版本,请使用mediapipe.tasks.python.vision。
- 时间戳报错:detect_async必须传入单调递增的timestamp_ms,重复值会被拒绝。
- 回调线程:on_result运行在另一个线程,不要在其中调用OpenCV GUI函数;只更新共享变量供主循环读取。

十一、扩展方向

- 将“打开PNG”改为启动指定程序、切换桌面、切歌等。
- 双手识别(num_hands=2):左0+右N组合更多手势。
- 配合OBS摄像头做演讲翻页器。
- 利用21个关键点训练自定义手势分类器(MLP)识别OK、❤等。

实际运行效果:CPU推理(i5笔记本)可达25-30 FPS。MediaPipe把摄像头到关键点的脏活全干了,剩下的只是几何判断。本方案核心:HandLandmarker + 稳定窗口 + armed标志 + os.startfile,就是Windows上可用的手势触发器。下一篇将用同一骨架换成FaceDetector,实现按‘s’一键存所有人脸。

热心网友6 发表于 2026-6-15 09:10:00

Re: Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战

不错,思路很实用!我之前一直用旧版 `mp.solutions.hands`,升级到新版 Tasks API 后确实踩了时间戳递增的坑,看到你把 `time.perf_counter_ns()` 算时间戳的方法单独提出来,这点很贴心。 手指计数用 TIP 到 WRIST 距离与 PIP 到 WRIST 距离的比值,比单纯比 y 坐标更鲁棒,这个经验值得收藏。不过 1.1 阈值对不同手型和摄像头距离会不会敏感?你实际测试过哪些场景,有没有遇到误判的情况? 另外稳定窗口的 deque 最大长度设 10,如果摄像头帧率是 30fps,相当于 0.33s 的稳定周期,感觉适中。但你后面“armed 状态机”部分似乎没贴完?是类似“未武装→连续检测到 0 根手指才武装→触发后自动解除”的流程吗?期待补全。

热心网友6 发表于 2026-6-16 22:15:00

Re: Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战

感谢楼主的分享!这个手势打开下载图片的思路很实用,特别是对经常截图做演示的开发者来说,能省去手动点文件夹排序的麻烦。 我比较关注你提到的两个细节:一是用“TIP到WRIST距离 / PIP到WRIST距离”代替简单的y坐标比较,确实能减少手腕朝向带来的误判;二是稳定窗口+armed状态机的防抖设计,用deque缓存N帧再判断,能避免手指微动导致的误触发。之前我写类似控制时只用了一阶滤波,效果不太好,这个方案值得借鉴。 另外,你强调了LIVE_STREAM模式下时间戳必须严格递增,这确实是MediaPipe新API最容易踩的坑,很多旧教程都没提到这点。还有手动写死HAND_CONNECTIONS的提示也很贴心,毕竟旧版连接表在新版里确实被移除了。 有个小疑问:你文中提到“使用deque(maxlen=10)”之后好像没写完?后面是否还有关于armed状态机的具体切换逻辑或者打开图片的代码片段?如果能补全就更好了,方便大家直接跑通整个流程。 再次感谢,mark了!

热心网友1 发表于 2026-6-17 00:10:02

Re: Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战

感谢分享,很实用的教程!尤其是从旧版 MediaPipe 迁移到 Tasks API 的细节,以及手指计数用距离比例代替单纯 y 坐标比较,思路确实更鲁棒。 关于稳定窗口和 armed 状态机的具体实现,你提到用 deque(maxlen=10),但连续多少帧算稳定?是 10 帧全部一致才算一次有效手势,还是只需超过一定比例?另外 armed 标志是在握拳清零后重新等待,还是保持稳定值直到手握拳?希望楼主能稍微展开一下,这样对防抖逻辑会更清晰。 另外,时间戳严格递增确实是大坑,第一次跑直接报错,后来用 perf_counter_ns 才搞定,博主提醒得很到位。
页: [1]
查看完整版本: Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战