查看: 88|回复: 1

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

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
本文介绍一个不到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。建议创建虚拟环境:
  1. py -3.10 -m venv venv
  2. venv\Scripts\activate
  3. 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。项目目录结构:
  1. MediaPipe/
  2. ├── models/hand_landmarker.task
  3. ├── demo_hand_landmarker.py
  4. └── venv/
复制代码

三、MediaPipe Tasks最小可用骨架

旧版mp.solutions.hands在0.10.35之后已移除,必须使用新的Tasks API。以下是最小骨架:
  1. import mediapipe as mp
  2. from mediapipe.tasks import python as mp_python
  3. from mediapipe.tasks.python import vision
  4. options = vision.HandLandmarkerOptions(
  5.     base_options=mp_python.BaseOptions(model_asset_path="models/hand_landmarker.task"),
  6.     running_mode=vision.RunningMode.LIVE_STREAM,  # 摄像头流式推理
  7.     num_hands=1,
  8.     min_hand_detection_confidence=0.5,
  9.     min_hand_presence_confidence=0.5,
  10.     min_tracking_confidence=0.5,
  11.     result_callback=on_result,  # 异步结果回调
  12. )
复制代码
LIVE_STREAM模式下,detect_async()立即返回,结果通过回调异步传递。主循环不能阻塞等待,应直接读取“上一次最新结果”。
使用一个线程安全的容器保存最新结果:
  1. class LatestResult:
  2.     def __init__(self):
  3.         self.result = None
  4. latest = LatestResult()
  5. def on_result(result, output_image, timestamp_ms):
  6.     latest.result = result
复制代码
每一帧将BGR转RGB包成mp.Image,再传入单调递增的时间戳:
  1. start_ns = time.perf_counter_ns()
  2. # ...
  3. rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  4. mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
  5. ts_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
  6. landmarker.detect_async(mp_image, ts_ms)
复制代码
时间戳必须严格递增,否则MediaPipe会直接抛错丢弃帧。这是新手最容易踩的坑。

四、HandLandmarker输出解析

模型输出21个3D归一化坐标(x,y ∈ [0,1],z相对于手腕深度)。索引含义:
  1. 0  WRIST
  2. 1-4 THUMB
  3. 5-8 INDEX
  4. 9-12 MIDDLE
  5. 13-16 RING
  6. 17-20 PINKY
复制代码
每根手指有4个点:MCP(指根)、PIP、DIP、TIP(指尖)。本项目只关心TIP和PIP。绘制连线的连接表(原mp.solutions.hands.HAND_CONNECTIONS已不可用,需手动写死):
  1. HAND_CONNECTIONS = (
  2.     (0,1),(1,2),(2,3),(3,4),
  3.     (0,5),(5,6),(6,7),(7,8),
  4.     (5,9),(9,10),(10,11),(11,12),
  5.     (9,13),(13,14),(14,15),(15,16),
  6.     (13,17),(0,17),(17,18),(18,19),(19,20),
  7. )
复制代码

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

很多教程比较TIP和PIP的y坐标,但手歪时容易误判。更鲁棒的办法:计算TIP到WRIST的距离与PIP到WRIST的距离之比,若比值大于1.1则认为手指伸直。因为弯曲时TIP会靠近手腕,距离必然减小。
  1. FINGER_TIPS = (4, 8, 12, 16, 20)
  2. FINGER_PIPS = (3, 6, 10, 14, 18)
  3. def count_extended_fingers(landmarks) -> int:
  4.     wrist = landmarks[0]
  5.     def d(a, b):
  6.         return math.hypot(a.x - b.x, a.y - b.y)
  7.     n = 0
  8.     for tip, pip in zip(FINGER_TIPS, FINGER_PIPS):
  9.         if d(landmarks[tip], wrist) > d(landmarks[pip], wrist) * 1.1:
  10.             n += 1
  11.     return n
复制代码
1.1是经验阈值,调整时需平衡灵敏与误判。

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

直接使用瞬时值会导致识别抖动时反复触发。分两步处理:
1. 稳定窗口:连续N帧均为同一值才算稳定。使用deque(maxlen=10)(约0.3-0.5秒):
  1. history = deque(maxlen=10)
  2. history.append(cur_n)
  3. stable = history[0] if len(history) == history.maxlen and all(v == history[0] for v in history) else None
复制代码
2. armed标志:触发后必须回到0才能再次触发。实现边沿触发而非电平触发:
  1. if stable == 0:
  2.     armed = True
  3. elif stable is not None and 1 <= stable <= 5 and armed:
  4.     open_nth_png(stable)
  5.     armed = False
  6.     history.clear()  # 清空缓冲,避免同一稳定段重复识别
复制代码

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

使用Pathlib和os.startfile(Windows专属):
  1. from pathlib import Path
  2. import os
  3. DOWNLOADS_DIR = Path.home() / "Downloads"
  4. def open_nth_png(n: int):
  5.     files = sorted(
  6.         DOWNLOADS_DIR.glob("*.png"),
  7.         key=lambda p: p.stat().st_mtime,
  8.         reverse=True,
  9.     )
  10.     if len(files) < n:
  11.         return False, f"Only {len(files)} PNG(s), need #{n}"
  12.     target = files[n - 1]
  13.     os.startfile(str(target))
  14.     return True, f"Opened #{n}: {target.name}"
复制代码
如果需要跨平台,macOS用subprocess.run(["open", path]),Linux用xdg-open。

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

cv2.flip(frame, 1)让画面照镜子,但MediaPipe返回的handedness也跟随镜像。纠正方法:
  1. raw = result.handedness[hand_idx][0].category_name
  2. label = "Right" if raw == "Left" else "Left"
复制代码

九、完整主循环

整合以上部件:
  1. with vision.HandLandmarker.create_from_options(options) as landmarker:
  2.     while True:
  3.         ok, frame = cap.read()
  4.         if not ok:
  5.             break
  6.         frame = cv2.flip(frame, 1)
  7.         rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  8.         mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
  9.         ts_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
  10.         landmarker.detect_async(mp_image, ts_ms)
  11.         res = latest.result
  12.         cur_n = count_extended_fingers(res.hand_landmarks[0]) if (res and res.hand_landmarks) else 0
  13.         history.append(cur_n)
  14.         stable = history[0] if len(history) == history.maxlen \
  15.             and all(v == history[0] for v in history) else None
  16.         if stable == 0:
  17.             armed = True
  18.         elif stable is not None and 1 <= stable <= 5 and armed:
  19.             ok2, msg = open_nth_png(stable)
  20.             armed = False
  21.             history.clear()
  22.         draw_landmarks(frame, res)
  23.         cv2.imshow("Gesture -> Open PNG", frame)
  24.         if cv2.waitKey(1) & 0xFF in (ord("q"), 27):
  25.             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’一键存所有人脸。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

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 根手指才武装→触发后自动解除”的流程吗?期待补全。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-15 11:10 , Processed in 0.027870 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部