1. 项目缘起与整体设计思路
1.1 为什么要在RK3588上跑MediaPipe手势识别
先说背景。MediaPipe Hands 是谷歌推出的一套轻量级手部关键点检测方案,能在移动端和桌面端实时输出21个手部关键点坐标,做手势交互、手语识别、体感控制都够用。它的原始模型是 TFLite 格式,在手机和PC上跑得很欢,但一旦要落到嵌入式板子上,尤其是 RK3588 这类带 NPU 的国产芯片平台,事情就没那么顺了。
RK3588 这颗芯片大家应该不陌生,8核CPU(4×A76 + 4×A55),内置 6 TOPS 算力的 NPU,支持 INT8/INT16 量化推理,视频编解码能力也很强。用它做边缘端视觉推理,性价比很高。但问题在于,RKNN 工具链对 TFLite 模型的支持并不完美,尤其是 MediaPipe 这种带自定义算子的模型,直接转过去大概率会报错或者精度崩掉。
我这次项目的目标很明确:把 MediaPipe 的手部检测+关键点回归模型,完整迁移到 RK3588 的 NPU 上,做到单帧推理时间控制在 15ms 以内,关键点精度损失不超过 3 个像素。这个指标在交互场景里已经够用了。
1.2 整体技术路线选型
摆在面前的路有三条:
- 路线A:TFLite 模型直接通过 RKNN-Toolkit2 转换,能转就转,不能转的算子用 CPU 兜底。
- 路线B:把 TFLite 先转成 ONNX,再从 ONNX 转 RKNN,中间做算子替换和裁剪。
- 路线C:放弃原始模型结构,用 MediaPipe Model Maker 重新训练一个简化版,再走 ONNX 到 RKNN 的链路。
我三条路都试过。路线A最省事,但 MediaPipe 的 TFLite 模型里用了 TFLite 自定义算子(比如某些版本的CONV_2D带特殊 padding 模式),RKNN-Toolkit2 直接报Unsupported op。路线C最干净,但重新训练需要标注数据,周期太长。最后我选了路线B,核心原因是 ONNX 作为中间表示,算子替换和图优化都更灵活,而且 RKNN 对 ONNX 的支持明显比 TFLite 好。
具体链路是:MediaPipe TFLite → ONNX(通过 tflite2onnx)→ 算子替换与图简化 → RKNN 量化转换 → RK3588 板端部署。
注意:tflite2onnx 这个工具对 TFLite 版本有要求,建议用 TFLite 2.8 以下的模型文件,新版本的自定义算子更多,转换成功率会下降。
1.3 模型拆分策略
MediaPipe Hands 实际上包含两个模型:一个是手掌检测模型(Palm Detection),输入 192×192,输出手掌框;另一个是手部关键点模型(Hand Landmark),输入 224×224,输出21个关键点。两个模型是级联关系,先检测再回归。
在 RK3588 上,我把两个模型都转成了 RKNN,但做了不同的量化策略。手掌检测模型对精度要求相对低,用 INT8 量化就够了;关键点模型对坐标精度敏感,我用了混合量化,卷积层 INT8,全连接层保留 FP16。这样在 NPU 上跑,整体精度损失控制在可接受范围内。
2. 环境搭建与工具链配置
2.1 开发主机环境准备
我用的开发主机是 Ubuntu 20.04,Python 3.8。RKNN-Toolkit2 对 Python 版本比较挑,3.9 以上会有依赖冲突,建议老老实实用 3.8。先装基础依赖:
sudo apt update sudo apt install -y python3.8 python3.8-venv python3-pip python3.8 -m venv rknn_env source rknn_env/bin/activate pip install --upgrade pip然后装 RKNN-Toolkit2。这里有个坑,官方提供的 whl 包分版本,RK3588 要用 1.5.0 以上的版本,我实测 1.6.0 最稳:
pip install rknn_toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl装完之后验证一下:
from rknn.api import RKNN print(RKNN().version)能打印出版本号就说明环境没问题。
2.2 TFLite 转 ONNX 的实操细节
MediaPipe 的 TFLite 模型可以从官方模型库下载,也可以自己用 Model Maker 训练。我这次用的是官方预训练模型,文件名叫hand_landmark_full.tflite和palm_detection_full.tflite。
转换工具用tflite2onnx,安装很简单:
pip install tflite2onnx转换命令:
tflite2onnx convert palm_detection_full.tflite palm_detection.onnx tflite2onnx convert hand_landmark_full.tflite hand_landmark.onnx转完之后用 Netron 打开看看图结构。这里要注意,MediaPipe 的 TFLite 模型里有一些DEQUANTIZE和QUANTIZE节点,这些在 ONNX 里会变成DequantizeLinear和QuantizeLinear。RKNN 对这两个算子的支持还行,但如果量化参数不对,推理结果会全错。
我遇到的一个典型问题是:手掌检测模型的输出层有一个TFLite_Detection_PostProcess自定义算子,tflite2onnx 转不了,直接报错。解决办法是把后处理逻辑从模型里剥离出来,在板端用 C++ 或 Python 手动实现。具体来说,就是让 ONNX 模型只输出原始的张量,后处理(解码框、NMS)在 CPU 上做。
2.3 RKNN-Toolkit2 的量化配置
量化是整個流程里最关键的环节。RKNN 支持两种量化方式:一是用校准数据集做 PTQ(训练后量化),二是加载量化感知训练后的模型。我用的 PTQ,校准集选了 200 张包含各种手势的图片,覆盖不同光照、不同手部姿态。
量化脚本的核心配置:
rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8', quantized_algorithm='normal', optimization_level=3 )这里mean_values和std_values必须和训练时的预处理一致。MediaPipe 的预处理是把像素归一化到 [-1, 1],所以 mean 和 std 都是 127.5。如果这里填错,量化后的模型精度会掉得很厉害。
实操心得:校准集不要只用一种场景的图片。我一开始只用了室内白墙背景的手势图,结果在户外复杂背景下误检率很高。后来补了 50 张户外图,问题就解决了。
3. 模型转换与量化实操全流程
3.1 手掌检测模型的转换与优化
手掌检测模型输入是 192×192×3,输出是 896 个锚框的置信度和回归值。原始 TFLite 模型里带了后处理,转 ONNX 时我把后处理砍掉了,只保留主干网络。
转换脚本:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8' ) ret = rknn.load_onnx(model='palm_detection.onnx') ret = rknn.build(do_quantization=True, dataset='calibration_palm.txt') ret = rknn.export_rknn('palm_detection.rknn')calibration_palm.txt里每行是一张校准图片的路径。图片要先 resize 到 192×192,格式是 RGB。
转换过程中 RKNN 会打印每一层的量化信息。重点看Quantize和Dequantize节点的分布,如果某一层量化后 scale 特别大或特别小,说明这一层的数值分布有问题,可能需要调整校准集或者把这一层设为 FP16。
3.2 关键点模型的混合量化策略
关键点模型输入 224×224×3,输出 21 个关键点的 xyz 坐标和可见性。这个模型对坐标精度要求高,全 INT8 量化后关键点抖动很明显,尤其是手指尖的坐标。
我的做法是:卷积层用 INT8,全连接层用 FP16。RKNN 支持通过hybrid_quantization配置来实现:
rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3588', quantized_dtype='asymmetric_quantized-8', hybrid_quantization=True, hybrid_quantization_method='layer_wise' )然后在build的时候指定哪些层用 FP16:
ret = rknn.build( do_quantization=True, dataset='calibration_landmark.txt', rknn_batch_size=1 )实测下来,混合量化后关键点精度比纯 INT8 提升了约 40%,推理时间只增加了 2ms 左右,很划算。
3.3 板端推理代码实现
RK3588 板子上跑的是 Ubuntu 20.04,Python 环境用 RKNN-Toolkit-Lite2。先装运行时库:
pip install rknn_toolkit_lite2-1.6.0-cp38-cp38-linux_aarch64.whl推理代码的核心逻辑:
from rknnlite.api import RKNNLite import cv2 import numpy as np rknn_palm = RKNNLite() rknn_palm.load_rknn('palm_detection.rknn') rknn_palm.init_runtime(core_mask=RKNNLite.NPU_CORE_0) rknn_landmark = RKNNLite() rknn_landmark.load_rknn('hand_landmark.rknn') rknn_landmark.init_runtime(core_mask=RKNNLite.NPU_CORE_1) def preprocess(img, size): img = cv2.resize(img, (size, size)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = (img.astype(np.float32) - 127.5) / 127.5 return np.expand_dims(img, axis=0) def detect_hand(frame): input_palm = preprocess(frame, 192) outputs = rknn_palm.inference(inputs=[input_palm]) boxes, scores = postprocess_palm(outputs) if len(boxes) == 0: return None best_box = boxes[np.argmax(scores)] crop = crop_and_pad(frame, best_box) input_landmark = preprocess(crop, 224) landmarks = rknn_landmark.inference(inputs=[input_landmark]) return decode_landmarks(landmarks)这里有个细节:RK3588 有 3 个 NPU 核心,可以把两个模型分配到不同核心上并行推理。我实测下来,双核并行比单核串行快了将近一倍。
注意:
core_mask的设置要在init_runtime时指定,运行中不能改。如果板子上的 RKNN 运行时版本低于 1.5.0,可能不支持多核配置,需要先升级。
4. 性能调优与常见问题排查
4.1 推理速度优化实战
刚部署完的时候,单帧总耗时(手掌检测+关键点回归)在 28ms 左右,离 15ms 的目标还有距离。我做了几轮优化:
第一轮,把两个模型分配到不同 NPU 核心,耗时降到 22ms。第二轮,把输入图片的 resize 操作从 CPU 换成 RGA 硬件加速,又省了 3ms。第三轮,把后处理的 NMS 用 C++ 重写,再省 2ms。最后稳定在 16-17ms,基本达标。
RGA 的使用需要调用 librga 库,代码稍微复杂一点,但效果很明显。核心接口是imresize和cvtcolor,可以直接把摄像头采集的 NV12 数据转成 RGB 并 resize,省掉 CPU 上的转换开销。
4.2 精度问题的排查思路
精度问题是最头疼的。我遇到过的典型症状和解决方法:
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| 关键点整体偏移 | 预处理 mean/std 不匹配 | 检查量化配置和训练配置是否一致 |
| 手指尖抖动严重 | 全连接层量化误差大 | 改用混合量化,FC 层保留 FP16 |
| 手掌检测漏检 | 校准集场景覆盖不足 | 补充多场景校准图片 |
| 输出全为 NaN | 量化 scale 溢出 | 检查校准集是否有异常值 |
| 推理结果随机 | 输入数据格式错误 | 确认 NHWC 和 NCHW 的转换 |
其中“输入数据格式错误”这个坑我踩得最深。RKNN 默认按 NHWC 处理输入,但 ONNX 模型可能是 NCHW。如果转换时没注意,推理结果会完全乱掉。解决办法是在load_onnx时指定inputs_format='nchw',或者在预处理时手动转置。
4.3 板端部署的稳定性问题
RK3588 长时间跑推理,偶尔会出现 NPU 挂死的情况。我排查下来,主要是两个原因:一是内存泄漏,二是温度过高。
内存泄漏的排查用valgrind或者直接看/proc/meminfo。RKNN 的inference接口每次调用会分配临时内存,如果 Python 层没有及时释放,跑几个小时就会 OOM。解决办法是复用输入输出 buffer,不要每次新建 numpy 数组。
温度问题在夏天比较明显。RK3588 的 NPU 满载时功耗不低,如果散热片太小,温度上到 80 度以上就会降频。我加了一个小风扇,温度控制在 60 度以下,稳定性就好了很多。
实操心得:板端部署一定要加看门狗。我写了一个简单的监控脚本,每 10 秒检查一次推理进程是否存活,如果挂死就自动重启。这个在无人值守的场景里特别重要。
5. 实际应用场景与扩展方向
5.1 手势交互在智能设备上的落地
这套方案我实际用在了两个场景里。一个是智能电视的隔空手势控制,用户挥手切换频道、握拳暂停播放。另一个是工业巡检场景,工人戴手套做特定手势来确认巡检项,避免触屏操作。
智能电视场景对延迟要求高,我把推理帧率限制在 30fps,配合摄像头的 60fps 采集,整体交互延迟在 80ms 以内,体验很流畅。工业场景对精度要求高,我把关键点模型的输入从 224 提到了 256,精度提升了但耗时增加了 4ms,整体还能接受。
5.2 模型更新与迭代策略
MediaPipe 的模型不是一成不变的,谷歌偶尔会更新。我的做法是把模型转换和量化脚本做成自动化流水线,每次有新模型,跑一遍脚本就能生成新的 RKNN 文件。脚本里加了精度校验环节,用固定的测试集跑一遍,如果关键点误差超过阈值就报警。
另外,如果业务场景有特殊手势需求,可以用 MediaPipe Model Maker 做微调。微调后的模型还是 TFLite 格式,走同样的转换链路就行。Model Maker 的好处是只需要少量标注数据,几百张图就能出一个可用的模型。
5.3 从手势识别到更复杂的视觉任务
这套 TFLite 到 RKNN 的转换链路,其实不限于手势识别。我后来用同样的方法把 YOLOv8 和 DINOv3 也转到了 RK3588 上。YOLOv8 的转换更简单,因为它的 ONNX 导出很成熟,RKNN 支持也好。DINOv3 稍微麻烦一点,因为它的注意力机制里有动态 shape,需要固定输入尺寸才能转。
RKNN 的 batch 推理也值得提一下。如果业务场景需要同时处理多路视频,可以把 batch size 设成 2 或 4,NPU 的利用率会更高。但 batch 增大会增加内存占用,RK3588 的 8GB 内存跑 batch=4 的 YOLOv8 没问题,再大就要看具体模型了。
最后分享一个小技巧:RKNN 模型转换时,optimization_level设成 3 会做更激进的图优化,通常能提升 10%-15% 的速度,但偶尔会导致精度下降。如果发现精度异常,可以降到 2 试试。这个参数没有万能值,得根据具体模型调。