MediaPipe 手势识别的几个坑

这次用鲁班猫 3 做了个离线手势识别,系统是 Android 14。摄像头看到手就画出骨架,比个 OK,自动保存一张带骨架的截图。

模型直接用 Google 的 MediaPipe。它已经能找出手部的 21 个关键点,还带七种静态手势分类。自己要处理的是相机、画面坐标,以及识别结果怎么变成实际操作。

模型用现成的

Android 用 Kotlin 写,相机接 CameraX,识别接 MediaPipe Tasks。依赖用这几项。

def cameraxVersion = '1.4.2'
implementation "androidx.camera:camera-core:$cameraxVersion"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation 'com.google.mediapipe:tasks-vision:1.0.0'

官方模型放进 app/src/main/assets,创建识别器时指定这个文件,运行模式设为 LIVE_STREAM,通过回调拿结果。官方 Android 示例已经有完整接法,照着看比从空白工程硬猜快。

模型跟着 APK 一起打包,推理在板子上完成,运行时只需要相机权限。

点赞手势的 21 个关键点与骨架连线

底图来自 MediaPipe 官方测试图片,这是本项目的板端测试输出。红点是关节点,蓝线标出拇指,其余连线用绿色。

相机别开两路

CameraX 常见的接法是 Preview 显示画面,ImageAnalysis 给模型送帧。但在我这块 RK3576 板子的固件上,两路一起开会卡住。

最后只保留一条 ImageAnalysis,预览和识别共用它。拿到 1920×1080 的图像后,处理行步幅,把最长边缩到 640 像素,再统一旋转和镜像,送进模型。

帧策略用 KEEP_ONLY_LATEST。处理不过来就丢旧帧,别让识别排队。当前代码还限制了至少 100 ms 才提交一帧,所以提交速率最高约每秒 10 帧,实际显示速度还得看推理回调。

ImageProxy 用完一定要 close,跳过帧和异常分支也一样。 缓冲区占着不放,相机后面就可能没帧了。

另外,板子上的 CAM0、Android 的 Camera ID 0 和 /dev/video0 要分别核对。这个项目同时检查物理 m00_ 模块、逻辑 ID 0 和当前固件的 FRONT 属性,条件不满足就停。这套映射只针对已经验证过的板子和固件。

骨架为什么会飘

这个问题得从坐标和时间两头查。

坐标上,模型给的是归一化关键点,预览用的却是 centerCrop。图片会先放大,再裁掉超出显示区域的部分。直接拿关键点乘屏幕宽高,当然对不上。

把关键点投到屏幕上,要和预览用同一个缩放比例和裁切偏移。下面的 x、y 是模型返回的归一化坐标,srcW、srcH 是模型输入图像的宽高。

scale = max(viewW / srcW, viewH / srcH)
offsetX = (viewW - srcW * scale) / 2
offsetY = (viewH - srcH * scale) / 2

screenX = x * srcW * scale + offsetX
screenY = y * srcH * scale + offsetY

这些除法要用浮点数。旋转和镜像也统一在推理前处理,别让预览和模型各算各的方向。

时间上,手已经移走了,上一帧的结果才回来。把旧骨架叠到新画面,多少有点灵魂出窍。

我的处理是从结果回调里取出对应的输入图片,和这次关键点一起更新到界面。画面和骨架必须属于同一帧。 代价是显示会等识别结果,预览流畅度也跟着受限。

还有一个容易漏的判断。分类结果是 None,只代表没认出支持的手势。只要关键点还在,骨架就继续画;没检测到手、结果过期或者切到后台时,再清掉。

OK 截图要防连拍

官方这七种分类没有 OK,我用现有关键点补了一层几何判断。拇指和食指指尖靠近,食指弯曲,另外三根手指伸直,才算匹配。

距离按掌部尺寸归一化,再结合关节角度判断。用固定像素距离的话,手往前伸一点,阈值就不合适了。这里用的是按输入宽高还原后的 2D 坐标,和画面里的骨架保持一致。

然后才是触发控制。视频每帧都可能认出 OK,你一直举着手,就一直存图,相册受不了这个。

判断 当前条件
确认手势 最近最多 5 帧中至少 4 帧强匹配,当前帧也满足,窗口持续至少 300 ms
触发后锁定 保持 OK 只拍一张
允许再拍 离开 OK 至少 600 ms,两次触发至少间隔 1.5 秒

触发条件严一点,保持条件宽一点,减少边界抖动。采样断开超过 400 ms,就清掉旧的观察窗口。这些是当前配置,实际触发时间还受帧率影响。

截图直接合成当前画面和同帧骨架,再用 MediaStore 存成 PNG。只画这两层,状态栏和提示框就不会混进去。编码和写盘放到独立线程,别堵着相机识别。

保存的是当前显示画面的合成图,输入已经缩小过,不能当成传感器原图。

这版配置的还是 Delegate.CPU。RK3576 有 NPU,但应用得接入 RKNN 才用得上。RKNN 的官方流程包括模型转换和运行时接入,前后处理也要重新对齐,光改一个配置没用。

听歌