用 MediaPipe 手势数字识别一键打开下载夹里的图片(Python + OpenCV 实战)

关键词:MediaPipe Tasks、HandLandmarker、21 关键点、手势数字识别、LIVE_STREAM 异步、OpenCV、os.startfile

一、想要做的事

我电脑屏幕上经常一边写代码一边盯刚下载的截图。每次都要打开"下载"文件夹、按时间排序、双击 ------这个动作太小却很烦。

那干脆让摄像头来干:

  • 比划"1" → 自动打开下载夹里最新的那张 PNG;
  • 比划"2" → 打开第二新的那张;
  • ......一直到 5;
  • 握拳(0 根手指)= 解除武装,准备下一次触发。

最终效果是一个 200 行不到的 Python 脚本,靠 MediaPipe 的 HandLandmarker 模型识别 21 个关键点,再用一条非常朴素的几何规则数手指。

本文从 0 到 1 拆开它,讲清四件事:

  1. MediaPipe Tasks 在 Python 里到底怎么用?LIVE_STREAM 模式和回调长什么样?
  2. 给定 21 个关键点,怎么数"伸出几根手指"?
  3. 怎样防止识别抖动一下就疯狂连发?
  4. Windows 下用 os.startfile 调起默认看图程序的小坑。
    "C:\Users\86182\Desktop\MediaPipe\demo_hand_landmarker.py"

二、环境准备

MediaPipe 当前对 Python 3.13 还没有发布的 wheel,必须用 3.10(3.11/3.12 也行,但 3.10 是最稳的)。我直接装在虚拟环境里:

bash 复制代码
py -3.10 -m venv venv
venv\Scripts\activate
pip install mediapipe opencv-python pillow numpy

模型文件自己下一次到本地(直连 Google CDN 即可,国内大部分网络都通):

复制代码
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 的最小可用骨架

新版 MediaPipe(0.10.x)已经全面切到 Tasks API,老博客里的 mp.solutions.hands 在 0.10.35 之后不存在了。所以请直接用:

python 复制代码
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() 立刻返回,结果通过你注册的 result_callback 异步给你。这意味着主循环千万不要在调用之后阻塞等结果------直接读"上一次最新结果"就好:

python 复制代码
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,再带一个单调递增 的时间戳调 detect_async

python 复制代码
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 在 0,1,z 是相对于手腕的深度)。索引按官方文档:

复制代码
0   WRIST
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 拿,现在自己写死):

python 复制代码
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 会被卷向手腕,距离一定变小。

python 复制代码
FINGER_TIPS = (4, 8, 12, 16, 20)
FINGER_PIPS = (3, 6, 10, 14, 18)

def count_extended_fingers(landmarks) -> int:
    wrist = landmarks[0]
    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[tip], wrist) > d(landmarks[pip], wrist) * 1.1:
            n += 1
    return n

20 行不到,对所有方向都鲁棒。1.1 是经验阈值,太小会把弯指误判为伸直,太大会漏掉刚伸出的手指。

六、抖动处理:稳定窗口 + armed 状态机

如果直接拿 count_extended_fingers 的瞬时值去开图,识别抖一下立刻连开五张图,体验崩溃。两步处理:

第一步:连续 N 帧都是同一个值才算"稳定" 。我用 deque(maxlen=10),10 帧 ≈ 0.3-0.5 秒:

python 复制代码
history = deque(maxlen=10)
history.append(cur_n)
stable = history[0] if len(history) == history.maxlen and all(v == history[0] for v in history) else None

第二步:触发后必须先回到 0 才能再触发 ------一个标志位 armed

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

这就是 Game UI 里常说的"边沿触发",不是"电平触发"。代价只有一句 armed = False,效果极好。

七、按时间倒序定位"第 N 新"的 PNG

Python 标准库就够:

python 复制代码
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[n - 1]
    os.startfile(str(target))    # Windows 专属:相当于双击
    return True, f"Opened #{n}: {target.name}"

os.startfile 在 Linux/macOS 没有,要写跨平台的话用 subprocess.run(["open", path])(macOS)/xdg-open(Linux),不过我这是个人 Windows 工具就懒得抽象了。

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

cv2.flip(frame, 1) 让画面像照镜子一样自然,但 MediaPipe 拿到的是镜像后的图,它返回的 Left/Right 也跟实际相反。一句话补回来:

python 复制代码
raw = result.handedness[hand_idx][0].category_name
label = "Right" if raw == "Left" else "Left"

handedness 这个字段名我每次都拼错,提醒自己。

九、完整主循环

把上面零件拼起来:

python 复制代码
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[0]) if (res and res.hand_landmarks) else 0
        history.append(cur_n)
        stable = history[0] if len(history) == history.maxlen \
                 and all(v == history[0] 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

十、几个一定会遇到的坑

  1. No module named 'mediapipe' :99% 是没激活虚拟环境,或者用的是系统 Python 3.13。先 where python 确认。
  2. module 'mediapipe' has no attribute 'solutions' :你装的是 0.10.x,老 API 已经下线,请用 mediapipe.tasks.python.vision 那一套。
  3. 时间戳报错detect_async 必须传单调递增的 timestamp_ms,重复值会被拒。
  4. 回调线程on_result 是另一个线程跑的,不要在里面调 OpenCV GUI 函数;只更新共享变量给主循环读。

十一、还能往哪儿玩

  • 把"打开 PNG"换成"启动指定程序"、"切歌"、"切桌面"------一切快捷键能干的事都行;
  • 双手(num_hands=2):左 0 + 右 N,组合表达式更多;
  • 配合 OBS 虚拟摄像头,做不需要触屏的演讲翻页器;
  • 21 个关键点也足以训练自定义手势分类器,喂给一个 MLP 就能识别"OK"、"❤"等更花哨的姿势。

代码不到 200 行,跑起来流畅 25-30 fps(CPU 推理,i5 笔记本)。MediaPipe 这一套真正的价值是把"摄像头到关键点"的脏活全干了,剩下的判断都只是几何题。

小结 :HandLandmarker + 一个稳定窗口 + 一个 armed 标志 + os.startfile,就是 Windows 上一个能用的手势触发器。下一篇我们会用同一套骨架,把 HandLandmarker 换成 FaceDetector,做一个"按 's' 一键存所有人脸"的小工具。

相关推荐
Jooolin5 小时前
从 DeepSeek、Qwen 到 GPT:一次企业级 AI 知识库项目的模型选型复盘
人工智能·云原生·ai编程
不羁的木木6 小时前
HarmonyOS AI开发提效工具:DevEco Code & DevEco CLI - 实战:端侧AI文字识别应用
人工智能·华为·harmonyos
某人辛木6 小时前
Web自动化测试
前端·python·pycharm·pytest
蓝速科技6 小时前
蓝速科技 AI 数字人导办能力实测与人机协同价值评估
人工智能·科技
云和数据.ChenGuang6 小时前
T5大模型
人工智能·机器人·pandas·数据预处理·数据训练
C+++Python6 小时前
详细介绍一下Java泛型的通配符
java·windows·python
哈哈,柳暗花明6 小时前
人工智能专业术语详解(O)
人工智能·专业术语
不羁的木木6 小时前
HarmonyOS AI开发提效工具:DevEco Code & DevEco CLI - 初识与配置指南
人工智能·华为·harmonyos
Kagol6 小时前
Superpowers GSD gstack AgentSkills深度测评
前端·人工智能