本文最后更新于37 天前,其中的信息可能已经过时,如有错误请发送邮件到
sunrisetsn@gmail.com
〇 前言
一直想做点”隔空操控”的东西——不用碰到屏幕,对着摄像头挥挥手就能写字。于是花了一周把想法落地成了这个程序:它借助 MediaPipe 实时识别手部关键点,捏合拇指和食指即“下笔”,指尖在空中的移动轨迹会变成屏幕上带着颗粒感的粉笔笔迹,从起点一步到可打包分发的单文件 exe。
期间踩了一堆坑,最离谱的是“中文用户名让 exe 黑屏”的玄学 bug,后面会讲。
一 功能
- 摄像头实时画面 + 虚拟画布一体叠加
- 拇指+食指捏合 = 落笔,移动指尖即是书写
- 屏幕内工具条(捏合点击或鼠标点击):7 种粉笔颜色 / 橡皮开关 / 一键清空
- 粗细滑杆:按住拖动连续调节(6~80)
- 颗粒质感粉笔笔迹
- 鼠标模式与手势模式互不干扰,可混用
- PyInstaller 单文件 exe,模型内嵌、开箱即用
二 技术栈
| 技术 | 用途 |
|---|---|
| Python 3 | 开发语言 |
| OpenCV (cv2) | 摄像头采集、帧翻转缩放、画布渲染与图像合成 |
| MediaPipe Tasks | 手部 21 关键点实时检测(HandLandmarker) |
| PySide6 (Qt) | GUI 界面、视频帧显示、鼠标交互 |
| PyInstaller | 打包为单文件 exe |
三 使用到的主要模块
①OpenCV
负责采集与渲染:
cv2.VideoCapture() | |
cv2.flip() | 摄像头镜像,书写方向更自然 |
cv2.resize() | 统一 1280×720 工作区 |
cv2.addWeighted() | 画布 82% + 视频 18% 半透明合成 |
cv2.circle/rectangle | 屏幕内工具条绘制 |
②MediaPipe(核心)
新版本移除了旧的 mp.solutions,改用 Tasks API 的 HandLandmarker(float16 模型),一次 detect 返回 21 个归一化关键点:
捏合判定 = 拇指尖(4)↔食指尖(8) 距离 / 掌宽(0–9) ratio < 0.33 → 落笔;带 0.46 滞回 → 抬笔
笔尖取拇指与食指指尖的中点,更贴近“握笔”的感觉。
③PySide6
QThread摄像头工作线程,Signal(np.ndarray)每帧把合成画面发给主线程QLabel显示画面,鼠标事件坐标等比换算到画布坐标并喂给同一套笔处理逻辑- 窗口缩放、居中、坐标往返不漏
四 实现原理
摄像头
│ 读取帧
▼
翻转(镜像) → 缩放 1280×720
▼
HandLandmarker 检测 21 关键点
▼
捏合判定 → 工具条命中 / 写线
▼
粉笔圆章(颗粒噪声) 绘制到画布
▼
画布(0.82) ⊕ 视频(0.18) 合成
▼
Qt 线程显示
状态机很简单:每帧维护“上一笔点 last”,捏合拖动时从 last 到新点插值画线;抬起清空。按下瞬间只判定一次工具按钮,杜绝连发。
五 粉笔颗粒效果
笔迹不是直线,而是沿线每 2.5px 盖一个“噪声圆章”:像素亮度随机扰动、随机留空洞、0.62 透明度混合进画布。于是笔画越叠越浓,颗粒感就像真的粉笔:
mask[rnd > speckle] = 0 | 随机空洞(颗粒) |
jit = 0.55 + 0.45 * rand(h,w,1) | 亮度随机扰动 |
canvas[y0:y1,x0:x1] = sub*(1-a) + color*jit*a |
六 踩过的坑(重点)
- 新版 MediaPipe 没了
mp.solutions:老教程全是mp.solutions.hands,新版直接 ImportError。改成tasks.HandLandmarker还得自带.task模型文件,需要一起捆绑进 exe。 - 中文用户名导致 exe 黑屏:源码跑得好好的,打包后一片漆黑。排查到 PyInstaller 把内嵌模型解压到
%TEMP%\_MEIxxxx\assets\,而我的系统用户名路径带中文,MediaPipe 的 C++ 原生文件 IO 打不开中文路径 → 模型加载失败 → 识别线程静默死亡。修复:启动时检测路径是否含非 ASCII 字符,是则把模型复制到 exe 旁边的 ASCII 目录再加载,并写app.log兜底日志。 - 工具条“按了没反应”:捏合保持时每帧都会命中按钮,导致动作被反复触发(橡皮开了又关)。改成按下瞬间只触发一次。
- 快速点击丢失:按下+松开发生在一帧内(<33ms)就轮询不到,点击全部丢失。改为“按下事件即时处理 + 拖动事件采样”,彻底解决。
- 点击滑条崩溃:
hit()返回值结构不一致导致对 list 调用startswith()抛 AttributeError,修复返回值并给交互区加了 try/except 兜底。
七 开发记录
这是第一个从零到一再用“摄像头 + 手势 + 桌面 GUI”的组合,把图像处理、神经网络推理、线程和界面串起来的完整项目。体会到最关键的是调试手段:源码跑不通一眼就看到了,但从打包好的黑窗里捞原因,比写代码本身麻烦得多。
目前是 v1.0 够用了,之后想加:画布导出 PNG、笔画截图、笔迹平滑、多手势指令(双指拖屏等),把它一步步变得更接近“能挂在墙上代替白板”的样子。


