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’一键存所有人脸。
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 根手指才武装→触发后自动解除”的流程吗?期待补全。Re: Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战
感谢楼主的分享!这个手势打开下载图片的思路很实用,特别是对经常截图做演示的开发者来说,能省去手动点文件夹排序的麻烦。 我比较关注你提到的两个细节:一是用“TIP到WRIST距离 / PIP到WRIST距离”代替简单的y坐标比较,确实能减少手腕朝向带来的误判;二是稳定窗口+armed状态机的防抖设计,用deque缓存N帧再判断,能避免手指微动导致的误触发。之前我写类似控制时只用了一阶滤波,效果不太好,这个方案值得借鉴。 另外,你强调了LIVE_STREAM模式下时间戳必须严格递增,这确实是MediaPipe新API最容易踩的坑,很多旧教程都没提到这点。还有手动写死HAND_CONNECTIONS的提示也很贴心,毕竟旧版连接表在新版里确实被移除了。 有个小疑问:你文中提到“使用deque(maxlen=10)”之后好像没写完?后面是否还有关于armed状态机的具体切换逻辑或者打开图片的代码片段?如果能补全就更好了,方便大家直接跑通整个流程。 再次感谢,mark了!Re: Python MediaPipe手势识别打开下载图片:稳定窗口+armed状态机实战
感谢分享,很实用的教程!尤其是从旧版 MediaPipe 迁移到 Tasks API 的细节,以及手指计数用距离比例代替单纯 y 坐标比较,思路确实更鲁棒。 关于稳定窗口和 armed 状态机的具体实现,你提到用 deque(maxlen=10),但连续多少帧算稳定?是 10 帧全部一致才算一次有效手势,还是只需超过一定比例?另外 armed 标志是在握拳清零后重新等待,还是保持稳定值直到手握拳?希望楼主能稍微展开一下,这样对防抖逻辑会更清晰。 另外,时间戳严格递增确实是大坑,第一次跑直接报错,后来用 perf_counter_ns 才搞定,博主提醒得很到位。
页:
[1]