这次用鲁班猫 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 一起打包,推理在板子上完成,运行时只需要相机权限。

底图来自 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 的官方流程包括模型转换和运行时接入,前后处理也要重新对齐,光改一个配置没用。



