MediaPipe 的手势识别模型在 PC 上跑起来很轻松,Python 一行mp.solutions.hands就能出结果,但要把这套东西搬到 RK3566 这种边缘计算板子上,中间隔着的不是一步两步。我最近刚把一个基于 MediaPipe 手势关键点检测的模型完整部署到了 RK3566 的 Android 环境里,从模型转换、量化、推理验证到性能调优,踩了不少坑,也积累了一些在官方文档里找不到的经验。这篇文章就把整个流程拆开讲清楚,包括 RKNN Toolkit2 的模型转换细节、量化校准集的制作、RK3566 NPU 的推理配置,以及实际跑起来之后帧率和精度的平衡策略。
如果你手上正好有一块 RK3566 的开发板或者安卓一体机,想跑手势识别、关键点检测这类轻量级视觉模型,又不想从头摸索 RKNN 这套工具链,那这篇内容应该能帮你省下不少时间。我会尽量把每个步骤的"为什么"讲明白,而不是只丢一堆命令让你复制粘贴。
1. 先搞清楚 RK3566 的 NPU 到底能吃什么样的模型
1.1 RK3566 NPU 的基本规格与限制
RK3566 搭载的 NPU 算力是 0.8 TOPS,支持 INT8 和 INT16 推理,不支持 FP16 和 FP32 的直接硬件加速。这一点非常关键,因为 MediaPipe 原始输出的模型通常是 FP32 或者 FP16 的 TFLite 格式,你不可能直接把它丢给 RKNN 去跑。必须经过转换和量化,把模型权重压缩到 INT8,才能让 NPU 真正发挥作用。
很多人第一次接触 RKNN 的时候会有一个误解,觉得 RKNN Toolkit2 就是一个"格式转换器",把 TFLite 转成 RKNN 就完事了。实际上转换只是第一步,量化策略的选择、校准集的质量、算子兼容性才是决定最终推理效果的核心因素。我见过不少人转出来的模型在 PC 上精度看着还行,一上板子就发现关键点漂移严重,根本原因就是量化过程中校准集选得不对。
RK3566 的 NPU 对算子的支持是有限制的。MediaPipe 手势识别模型里面用到了大量的深度可分离卷积、残差连接、以及一些自定义的激活函数。这些算子在 RKNN 的算子支持列表里大部分是有的,但某些特定版本的算子可能需要拆解或者替换。所以在动手转换之前,建议先去查一下 RKNN Toolkit2 对应版本的算子支持列表,确认你的模型结构里没有"黑名单"算子。
1.2 MediaPipe 手势模型的结构拆解
MediaPipe 的手势识别方案通常包含两个阶段:首先是手掌检测(Palm Detection),然后是基于检测结果的手部关键点回归(Hand Landmark)。手掌检测用的是类似 SSD 的单阶段检测器,输入分辨率通常是 192x192 或 128x128;关键点回归网络输入是 224x224 或者更小的尺寸,输出 21 个关键点的坐标。
这两个模型是分开的,部署的时候也需要分别转换和推理。有些人想偷懒,把两个模型合并成一个大模型,在 RK3566 上这样做反而会拖慢整体速度,因为两个模型的输入分辨率不同,合并之后要么牺牲检测精度,要么增加不必要的计算量。分开跑的好处是可以做流水线优化:手掌检测不需要每帧都跑,可以隔几帧跑一次,中间帧直接用上一帧的检测框做关键点回归。
从 RKNN 转换的角度来看,手掌检测模型的转换难度相对低一些,因为它的结构比较规整,主要是卷积和池化。关键点回归模型就麻烦一些,里面有一些 reshape 和 transpose 操作,这些在 RKNN 转换时容易出问题。我的建议是先把两个模型分别转出来,单独验证推理结果,确认没问题之后再考虑集成到 Android 应用里。
1.3 模型输入输出的对齐问题
MediaPipe 原始模型的输入输出格式和 RKNN 转换后的格式往往不一致。举个例子,原始 TFLite 模型的输入可能是[1, 224, 224, 3]的 float32 张量,像素值范围是[-1, 1]或者[0, 1]。但 RKNN 量化之后,输入通常变成[1, 224, 224, 3]的 uint8 张量,像素值范围是[0, 255]。如果你在 Android 端做前处理的时候没有对应调整归一化参数,推理结果会完全不对。
输出端也有类似的问题。关键点回归模型的输出通常是归一化到[0, 1]的坐标值,但 RKNN 量化后输出的可能是 INT8 的定点数,需要根据量化参数反算回浮点坐标。这个反算过程如果搞错了 scale 和 zero_point,关键点就会整体偏移。我在第一次部署的时候就因为这个问题调试了整整一个下午,最后发现是输出层的量化参数没有正确读取。
注意:每次转换模型之后,务必用同一张测试图片分别在 PC 端和 RK3566 端跑一遍推理,对比输出的数值差异。如果差异超过阈值,优先检查输入前处理和输出后处理的参数是否对齐。
2. 用 RKNN Toolkit2 转换模型时最容易翻车的几个环节
2.1 环境搭建与版本匹配
RKNN Toolkit2 的版本和 RK3566 的 NPU 驱动版本是有对应关系的,不是随便装一个最新版就能用。我建议先去确认板子上 NPU 驱动的版本号,然后选择对应的 Toolkit2 版本。一般来说,RK3566 常用的组合是 Toolkit2 1.5.x 或 1.6.x 配合 NPU 驱动 1.5.x 以上。
安装 Toolkit2 的时候,Python 版本也有讲究。官方推荐的是 Python 3.8 到 3.10,我用 3.11 试过,有些依赖包会编译失败。另外,如果你是在 Windows 上做转换,某些算子会报不支持,换到 Ubuntu 20.04 或者 22.04 上就正常了。这个不是玄学,是因为 Toolkit2 底层依赖的一些推理库在 Linux 上支持得更完整。
# 创建虚拟环境 python3.8 -m venv rknn_env source rknn_env/bin/activate # 安装 RKNN Toolkit2 pip install rknn-toolkit2 -i https://mirrors.aliyun.com/pypi/simple/ # 验证安装 python -c "from rknn.api import RKNN; print('RKNN Toolkit2 installed successfully')"安装完成之后,建议先跑一下官方提供的示例脚本,确认基础环境没问题。官方仓库里有 YOLOv5、MobileNet 等模型的转换示例,拿这些示例练手可以快速熟悉整个流程。
2.2 从 TFLite 到 RKNN 的转换脚本编写
转换脚本的核心逻辑其实不复杂,但细节很多。下面是我实际使用的转换脚本框架,以手掌检测模型为例:
from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置模型参数 rknn.config( mean_values=[[127.5, 127.5, 127.5]], std_values=[[127.5, 127.5, 127.5]], target_platform='rk3566', quantized_dtype='asymmetric_quantized-8', optimization_level=3 ) # 加载 TFLite 模型 ret = rknn.load_tflite(model='palm_detection.tflite') if ret != 0: print('Load model failed') exit(ret) # 构建模型,指定量化校准集 ret = rknn.build(do_quantization=True, dataset='calibration_dataset.txt') if ret != 0: print('Build model failed') exit(ret) # 导出 RKNN 模型 ret = rknn.export_rknn('palm_detection.rknn') if ret != 0: print('Export model failed') exit(ret) rknn.release()这里有几个关键参数需要解释。mean_values和std_values决定了输入图像的归一化方式,必须和原始模型训练时的预处理保持一致。quantized_dtype选择asymmetric_quantized-8是 RK3566 上比较通用的选择,非对称量化对激活值的分布适应性更好。optimization_level设为 3 会启用更激进的图优化,可能会合并一些算子,通常能提升推理速度,但如果遇到精度下降可以降到 2 试试。
2.3 量化校准集的制作:决定精度生死的一步
量化校准集是 RKNN 转换过程中最容易被忽视、但对最终精度影响最大的环节。校准集的作用是让 Toolkit2 统计每一层激活值的分布范围,从而确定量化的 scale 和 zero_point。如果校准集里的图片和你实际使用场景的图片分布差异很大,量化后的模型在实际场景中就会表现很差。
我的做法是从实际使用场景中采集至少 200 到 500 张图片,覆盖不同的光照条件、手势姿态、背景复杂度。这些图片不需要标注,只需要是模型实际会看到的输入即可。校准集的图片列表写成一个 txt 文件,每行一个图片路径。
# calibration_dataset.txt ./calib_images/hand_001.jpg ./calib_images/hand_002.jpg ./calib_images/hand_003.jpg ...有一个经验值得分享:校准集图片的数量不是越多越好,关键是分布要合理。我曾经用 2000 张几乎一样的图片做校准,结果还不如 300 张多样化的图片效果好。另外,校准集的图片分辨率应该和模型输入分辨率一致,或者在脚本里做好 resize,否则 Toolkit2 可能会报错或者自动裁剪,导致校准结果偏差。
提示:如果你手头没有足够的实际场景图片,可以用数据增强的方式从少量图片生成校准集,比如随机调整亮度、对比度、旋转角度。但要注意增强后的图片不能太离谱,否则反而会拉大量化范围。
3. 在 RK3566 上跑推理:从加载模型到拿到关键点
3.1 Android 端 RKNN Runtime 的集成
RK3566 跑 Android 系统的话,推理部分需要用 RKNN 的 Android Runtime 库。这个库是一个.so文件,需要放到 Android 工程的jniLibs目录下。同时,Java 层通过 JNI 调用 C++ 接口来加载模型和执行推理。
集成过程中最容易出问题的是库的版本匹配。RKNN Runtime 的版本必须和转换模型时使用的 Toolkit2 版本兼容,否则加载模型的时候会报版本不匹配的错误。我建议在转换模型之前就先确认好板子上要用的 Runtime 版本,然后选择对应的 Toolkit2 版本,避免来回折腾。
// Java 层加载 RKNN 库 static { System.loadLibrary("rknn_api"); System.loadLibrary("hand_gesture"); } // Native 方法声明 public native int initModel(String modelPath); public native float[] runInference(byte[] imageData, int width, int height); public native void releaseModel();C++ 层的实现逻辑大致是:初始化 RKNN 上下文,加载.rknn模型文件,设置输入输出张量属性,然后循环执行推理。每次推理之前需要把 Android Camera 采集到的图像数据转换成 RKNN 输入要求的格式,推理之后再解析输出张量。
3.2 图像前处理的性能陷阱
前处理看起来简单,实际上在嵌入式设备上很容易成为性能瓶颈。Android Camera 输出的通常是 NV21 或者 YUV_420_888 格式,而 RKNN 模型需要的是 RGB 格式的 uint8 数据。这个转换过程如果放在 CPU 上做,一帧 224x224 的图像转换大概需要 2 到 5 毫秒,看起来不多,但如果每帧都要做,加上 resize 操作,累积起来就很可观了。
我的优化方案是尽量利用 RK3566 的 RGA(Raster Graphic Acceleration)硬件来做图像格式转换和缩放。RGA 可以在几乎不占用 CPU 的情况下完成 YUV 到 RGB 的转换和 resize,速度比纯 CPU 实现快好几倍。如果 Android 系统层面不方便直接调用 RGA,也可以考虑用 OpenGL ES 做 GPU 加速的转换。
另一个容易忽略的点是图像的 stride 对齐。Camera 输出的图像每行字节数可能不是 4 的倍数,而 RKNN 输入要求对齐。如果直接按宽度拷贝数据,会出现图像错位或者花屏。处理方法是按照 stride 逐行拷贝,或者用 RGA 自动处理对齐。
3.3 输出解析与关键点后处理
关键点回归模型的输出是一个[1, 21, 3]或者[1, 63]的张量,包含 21 个关键点的 x、y 坐标和置信度。RKNN 量化后的输出是 INT8 类型,需要根据量化参数反算回浮点值。这个反算公式是:
float_value = (int8_value - zero_point) * scale其中scale和zero_point可以从 RKNN 模型的输出张量属性中获取。如果你在转换时没有手动指定,Toolkit2 会自动计算并保存在模型里。在 Android 端加载模型后,可以通过rknn_query接口查询输出的量化参数。
拿到浮点坐标之后,还需要做一步反归一化,把[0, 1]的坐标映射回原始图像的像素坐标。这一步需要用到前处理时的缩放比例和填充偏移量。如果前处理时做了 letterbox 填充,后处理时就要对应去掉填充的影响,否则关键点位置会偏移。
注意:手掌检测和关键点回归是两个独立的模型,关键点回归的输入是从原始图像中裁剪出来的手部区域。裁剪时的坐标变换需要仔细处理,确保关键点坐标最终能正确映射回原始图像。
4. 实测性能数据与调优策略
4.1 基准测试:RK3566 上的实际帧率
我在 RK3566 上做了几组基准测试,硬件配置是 4GB RAM、Android 12、CPU 四核 A55 1.8GHz。测试条件是用 640x480 的摄像头输入,手掌检测模型输入 192x192,关键点模型输入 224x224。
| 测试项 | 纯 CPU 推理 | NPU 推理(INT8) | NPU 推理(INT16) |
|---|---|---|---|
| 手掌检测单帧耗时 | 45ms | 8ms | 15ms |
| 关键点回归单帧耗时 | 32ms | 6ms | 11ms |
| 前处理耗时(CPU) | 12ms | 12ms | 12ms |
| 后处理耗时 | 3ms | 3ms | 3ms |
| 合计(每帧都跑检测) | 92ms | 29ms | 41ms |
| 合计(隔帧跑检测) | 79ms | 23ms | 35ms |
从数据可以看出,NPU 加速的效果非常明显,手掌检测从 45ms 降到 8ms,关键点回归从 32ms 降到 6ms。但前处理占了 12ms,成了新的瓶颈。如果把前处理也优化到 RGA 硬件上,整体耗时还能再降 8 到 10ms。
隔帧跑检测的策略也很有效,因为手掌的位置在连续帧之间变化不大,没必要每帧都做全图检测。实际测试下来,隔一帧跑一次检测,关键点仍然能稳定跟踪,整体帧率从 34fps 提升到 43fps 左右。
4.2 量化精度损失的补偿方法
INT8 量化不可避免地会带来精度损失。在我的测试中,手掌检测的 mAP 从 FP32 的 0.92 降到了 INT8 的 0.89,关键点的平均像素误差从 2.1 像素增加到了 3.5 像素。这个损失在大多数应用场景下是可以接受的,但如果你对精度要求很高,可以考虑以下几种补偿方法。
第一种方法是混合量化。RKNN Toolkit2 支持对特定层使用 INT16 量化,其他层保持 INT8。通常把模型的最后几层(输出层附近的层)设为 INT16,可以显著减少精度损失,而对整体速度的影响很小。因为最后几层的数据量小,计算量也小,用 INT16 不会明显拖慢推理。
第二种方法是调整校准集的分布。如果你发现模型在某些特定场景下精度下降明显,可以在校准集中增加这些场景的图片比例。比如你的应用主要在室内使用,但校准集里有很多室外图片,那量化参数就会偏向室外场景,导致室内精度下降。
第三种方法是后处理补偿。对于关键点回归,可以在后处理阶段加入一些平滑滤波,比如卡尔曼滤波或者简单的移动平均,来减少量化噪声带来的抖动。这个方法不改变模型本身,实现起来也简单,适合快速上线。
4.3 内存占用与功耗的平衡
RK3566 的内存带宽有限,两个模型同时加载会占用不少内存。手掌检测模型量化后大约 1.2MB,关键点回归模型大约 2.8MB,加上 RKNN Runtime 本身的开销,总共大概占用 15 到 20MB 的内存。这个量级在 4GB RAM 的设备上不算什么,但如果你还要跑其他视觉任务,就需要考虑内存的复用。
功耗方面,NPU 推理的功耗远低于 CPU 推理。我用功率计测过,纯 CPU 跑推理时整板功耗大约 2.8W,切换到 NPU 后降到 2.1W 左右。对于电池供电的便携设备来说,这个差异还是很明显的。如果设备有散热限制,NPU 方案也更友好,因为发热量更小。
提示:如果应用场景允许,可以把两个模型合并到一个 RKNN 上下文里管理,减少上下文切换的开销。但要注意两个模型的输入分辨率不同,合并后需要分别设置输入。
5. 从 Demo 到产品:那些文档里不会写的事
5.1 摄像头采集与推理的线程模型
在实际产品中,摄像头采集和模型推理通常需要在不同的线程里跑,否则采集线程会被推理阻塞,导致预览画面卡顿。我的做法是开一个采集线程和一个推理线程,中间用一个有界队列做缓冲。采集线程负责从 Camera 拿数据并做初步的格式转换,推理线程从队列里取数据跑模型。
队列的大小需要根据实际帧率和推理耗时来调整。如果推理耗时大于采集间隔,队列会逐渐堆积,这时候要么丢帧,要么降低采集帧率。我一般设置队列长度为 2 到 3,超过就丢弃最旧的数据,保证推理的永远是最新的帧。
线程优先级也需要注意。推理线程的优先级可以设得比采集线程高一些,确保推理不会被采集打断。但也不能太高,否则可能影响系统其他任务的调度。在 Android 上可以用Process.setThreadPriority()来调整。
5.2 模型热更新与版本管理
产品上线之后,模型可能需要迭代更新。如果每次更新都要重新刷固件,那维护成本就太高了。我的方案是把.rknn模型文件放在外部存储或者应用私有目录里,应用启动时从指定路径加载。更新模型只需要替换文件,不需要重新编译 APK。
版本管理方面,我建议在模型文件名里带上版本号和日期,比如hand_landmark_v2.1_20250101.rknn。同时在应用里维护一个模型版本映射表,记录每个版本对应的精度指标和兼容的 Runtime 版本。这样出问题的时候可以快速回滚到上一个稳定版本。
5.3 异常处理与降级策略
嵌入式设备上跑模型,异常情况比 PC 上多得多。NPU 可能因为温度过高而降频,模型文件可能损坏,摄像头可能被其他应用占用。这些情况都需要有对应的处理策略。
我的做法是加一个看门狗机制,监控每帧推理的耗时。如果连续多帧耗时超过阈值,就自动降级到 CPU 推理或者降低模型输入分辨率。同时记录异常日志,方便后续分析。模型加载失败的时候,要有 fallback 方案,比如加载一个更小的备用模型,或者提示用户重启应用。
另外,RK3566 在长时间高负载运行后,NPU 可能会因为温度触发降频。如果应用对帧率稳定性要求高,可以考虑在检测到降频时主动降低推理频率,而不是硬扛着跑,这样反而能获得更稳定的用户体验。
5.4 实际部署中的几个小技巧
第一个技巧是关于模型输入的。如果你的应用场景中手部区域在画面中的占比比较固定,可以适当缩小模型输入分辨率,比如从 224x224 降到 192x192,推理速度能提升 20% 左右,精度损失通常在可接受范围内。
第二个技巧是关于 RKNN 的optimization_level。我实测下来,level 3 和 level 2 在 RK3566 上的速度差异大概在 5% 到 8%,但 level 3 在某些模型上会导致精度下降。如果你的模型对精度敏感,建议先用 level 2 验证,确认没问题再尝试 level 3。
第三个技巧是关于校准集的。如果你发现量化后的模型在某个特定手势上识别率特别低,可以专门针对这个手势采集一批图片加入校准集,重新转换模型。这个方法比调后处理参数有效得多。
第四个技巧是关于多模型并行的。RK3566 的 NPU 在同一时间只能执行一个推理任务,如果你有多个模型需要跑,需要串行执行。这时候合理安排模型的执行顺序就很重要,把对延迟敏感的模型放在前面执行,对实时性要求不高的模型可以降低执行频率。
我在实际项目中还遇到过一个比较隐蔽的问题:Android 系统的省电模式会限制后台应用的 CPU 和 NPU 使用频率。如果你的应用需要在后台持续运行手势识别,一定要在应用里申请忽略电池优化的权限,否则推理速度会莫名其妙地变慢。这个问题排查了很久才定位到,希望后来的人不要在这上面浪费时间。
最后说一个关于调试的经验。在 RK3566 上调试模型推理,最有效的方法是把中间结果 dump 出来。比如把前处理后的图像保存成文件,把模型输出的原始张量打印出来,然后拿到 PC 上用同样的输入跑一遍,逐步对比。这样能快速定位问题出在前处理、模型推理还是后处理环节。我一开始总想直接在板子上调试,效率很低,后来改成"板子 dump 数据,PC 分析对比"的模式,排查速度快了很多。