RTMPose这个词,这两年做边缘计算的人肯定不陌生。作为OpenMMLab在姿态估计方向放出的一个重要模型,它的卖点就是轻量化和高精度能同时做,尤其适合RK3588这种端侧NPU平台。RK3588这颗芯片,瑞芯微旗舰级的边缘SoC,8核ARM + 6TOPS算力NPU,做视觉AI落地工程的应该人手一块开发板了。把RTMPose部署上去,等于在端侧拿到了实时人体关键点检测能力,健身姿态纠正、安防行为分析、人机交互这类项目都能直接接上。
这篇内容就围绕“RK3588部署rtmpose模型”这件事展开,从模型选型、ONNX导出、RKNN量化转换,到板端推理和前后处理,把完整流程和过程中的坑都过一遍。适合手里有RK3588开发板、想把姿态估计模型真正跑起来的人看,也适合准备做端侧AI落地但还没摸清工具链的朋友。我不讲虚的,全是实际跑过之后总结出来的东西。
1. 先想清楚:为什么是RTMPose + RK3588
1.1 模型选型:边缘端姿态估计的几种选项
姿态估计模型不算少,但能塞进端侧NPU、跑得动实时推理的,数来数去就那几个。我在选型的时候对比过OpenPose、LiteHRNet、MoveNet和RTMPose,最后选了RTMPose,核心原因是它在精度和推理速度之间的平衡点非常合适端侧场景。
OpenPose是老牌模型,精度高,但结构复杂,计算量太大,即便裁剪版本在RK3588上也很难实时,更适合服务器。LiteHRNet在移动端口碑不错,但它在ONNX导出后算子偏杂,转换到RKNN时经常遇到算子不支持的问题,调试成本高。MoveNet是TensorFlow生态的,如果项目纯走PyTorch流程,还得ffmpeg导入导出绕一圈,麻烦。
RTMPose是2023年OpenMMLab团队放出的,基于Transformer的轻量级姿态估计模型。它最核心的设计思路是用simCC(simulated coordinate classification)替代传统heatmap做关键点定位。传统heatmap方案需要输出高分辨率的特征图来保证定位精度,计算量和内存开销都大。simCC把坐标预测拆成两个一维分类问题,x方向和y方向各做一次分类,输出维度大幅压缩,定位精度还能保持甚至更好。这就是RTMPose能在极低计算量下拿到高精度的根本原因。
具体到型号选择,RTMPose-t参数量只有3.3M左右,计算量在1 GFLOPs上下,对RK3588的NPU来说属于轻松负载;RTMPose-m大概是18M参数、8 GFLOPs左右,精度更高但算力消耗大不少。实测下来,如果只是单人姿态估计或者低并发场景,RTMPose-t完全够用;如果要做多人、高精度场景,可以看RTMPose-m,但要做好帧率下降的心理准备。
1.2 平台与工具链:RK3588 NPU的玩法
RK3588的NPU标称6 TOPS INT8算力,这在端侧芯片里属于中上水平。但芯片规格只是纸面数据,真正决定落地速度的其实是工具链的成熟度。RKNN-Toolkit2是瑞芯微官方推出的模型转换与推理工具链,支持从PyTorch、ONNX、TensorFlow等框架导入模型,转换成RKNN格式后在板端运行。
它的整体流程是这样的:PyTorch训练/导出的模型先转成ONNX,再用RKNN-Toolkit2在PC上完成量化转换,生成.rknn文件,最后拷贝到RK3588板子上用RKNN Runtime加载推理。这个链路中ONNX是承上启下的中间格式,RKNN-Toolkit2负责格式转换和量化压缩。
选择RK3588还有一个优势是它同时配了4个A76大核和4个A55小核,GPU是Mali G610。这意味着即使某些算子没有落到NPU上执行,CPU也能扛一部分,不会出现整个推理动弹不得的局面。实际项目中,前处理图像缩放、归一化这些操作如果在CPU上做,A76大核的性能完全可以支撑,瓶颈反而不大。
1.3 整体部署路线的技术架构拆解
把整个部署过程拆开看,大致是四段:模型准备、模型转换、板端推理、业务集成。模型准备阶段的目标是拿到干净、标准、可复现的ONNX文件;转换阶段的目标是得到精度损失可控的INT8量化模型;推理阶段要处理好输入输出数据结构,把模型能力真正用起来;集成阶段才是把姿态数据变成业务价值的部分。
我最初犯过的错误是急于进入转换阶段,拿到一个ONNX就直接扔给RKNN工具链,结果后面所有问题都堆在推理阶段爆发。后来学乖了,每个阶段固定节点做验证,ONNX先用onnxruntime跑一遍,确认输出坐标合理再做RKNN转换。这个习惯帮我筛掉了很多无用功。
2. 正式开工前的模型导出与预处理
2.1 拿到RTMPose的ONNX模型
RTMPose在OpenMMLab生态里分布在MMPose仓库,MMPose 1.0以上版本对它支持完整。如果从零开始,最稳妥的方法是先安装mmcv、mmdet、mmpose这几个包,然后从MMPose官方模型库下载rtmpose-t或rtmpose-m的预训练权重。
如果是纯部署项目,不想引入整套MMPose依赖,直接从官方仓库release页面下载onnx文件也是可以的。但我个人更建议自己走一遍pytorch2onnx的导出流程,因为这一步能让你看清楚模型输入的尺寸要求、归一化参数、输出结构,后面写代码时心里有底。
模型导出脚本大概长这样(这是MMPose 1.0时代的常见做法):
from mmpose.apis import init_model import torch model = init_model('rtmpose-t.py', 'rtmpose-t.pth', device='cpu') model.eval() dummy_input = torch.randn(1, 3, 192, 192) torch.onnx.export( model, dummy_input, 'rtmpose-t.onnx', opset_version=11, input_names=['input'], output_names=['simcc_x', 'simcc_y'], dynamic_axes=None )注意这里输入尺寸设成了192×192,dummy_input随机生成的形状必须固定。RKNN工具链对动态shape支持不友好,转换时建议固定输入尺寸。如果你要换256×192或者224×224,后面的归一化和坐标映射都要跟着调整,建议一开始就定好,不要中途频繁换。
2.2 输入尺寸、归一化与RGB顺序:三个最容易被反噬的细节
RK3588部署rtmpose模型时,这三个细节是最容易被忽略又最容易出问题的。先说输入尺寸。RTMPose训练时用的是192×192或256×192两种常见分辨率,直接决定了推理速度和精度。192×192在NPU上的计算量不到256×192的一半,如果需要高并发或多路视频分析,192是更合理的选择;如果精度要求高、单路处理,256×192更好。
然后是归一化参数。RTMPose在训练时的图像归一化统计量,mean是[123.675, 116.28, 103.53],std是[58.395, 57.12, 57.375],这个参数不是随便写的,它来自ImageNet数据集的统计,模型训练时也就地固化了这个配置。板端推理时必须用完全相同的mean/std做归一化,否则输入数据分布和训练时不一致,关键点位置会整个偏移。我记得第一次部署时偷懒没做归一化,输出热力图全部塌掉,排查了半天才发现是这个原因。
最后是RGB顺序。OpenMMLab生态默认图像通道顺序是BGR,但很多ONNX模型转换后期望的是RGB。这个坑特别隐蔽,因为如果顺序反了,不会直接报错,只是精度明显下降。我的做法是在转换前后各跑一遍同一张图像的推理,对比关键点输出,如果位置偏差超过5个像素,大概率就是通道顺序问题。用Netron打开ONNX模型查看输入节点的定义,如果写了BGR,那板端预处理就必须按BGR顺序喂数据。
3. RKNN转换与量化:从ONNX到rknn
3.1 RKNN-Toolkit2环境准备
RKNN-Toolkit2提供PC端的Python工具包,以及板端的推理运行时RKNN Runtime。转换这个动作必须在PC端完成,因为量化过程需要跑浮点模型来收集激活值分布,板端算力不适合干这个活。
安装RKNN-Toolkit2最简单的方式是pip install,但要注意它依赖的numpy、onnx版本不能太高,装完后再用官方提供的rknpu2交叉编译包做板端运行时。实操中我建议把Python环境单独用conda或venv隔离出来,因为RKNN-Toolkit2对部分依赖包版本敏感,和其他项目混在一起容易出现libstdc++或者onnx版本冲突。
这一点是我的亲身教训:一开始我在一个既有PyTorch训练环境又有TensorRT环境的机器上装RKNN-Toolkit2,结果两个依赖链互相打架,光是处理依赖冲突就花了两天。后来单独建了一个环境,五分钟装完。做嵌入式AI工具链部署,环境隔离是底线。
3.2 转换脚本与量化参数
核心转换代码不长,如下:
import os import numpy as np from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], target_platform='rk3588' ) ret = rknn.load_onnx(model='rtmpose-t.onnx') if ret != 0: print('load onnx failed') exit(-1) ret = rknn.build( do_quantization=True, dataset='dataset.txt' ) if ret != 0: print('build rknn failed') exit(-1) rknn.export_rknn('rtmpose-t.rknn')这里有几个关键点。mean_values和std_values如果已经在config里写了,那板端推理时就不需要再做一遍归一化,RKNN Runtime会自动处理输入,这个设计很贴心,但也意味着如果两边都做了归一化,数据就双重归一化错了。我建议把归一化交给RKNN Runtime处理,板端代码只做resize和通道顺序转换,逻辑简单很多。
dataset.txt里每行写一张用于量化校准的图像路径,建议至少写30张以上真实场景图像。量化过程实际上就是用这些图像在模型里跑一遍浮点推理,统计激活值分布,然后按照INT8映射关系做参数压缩。如果你的应用场景是室内健身,校准图就应该用室内健身的人体图像;如果是户外安防,就准备户外视频帧。校准数据分布和实际推理数据分布越接近,量化带来的精度损失越小。
build阶段do_quantization=True开启INT8量化。RTMPose模型量化为INT8后体积会从几十MB压缩到几MB,NPU推理速度提升明显。实测RTMPose-t在192×192输入下,FP32推理一帧需要20-30ms,INT8量化后能压到8ms以内,这个收益非常大。代价是精度可能掉1%-3%的AP,在姿态估计任务里表现为关键点抖动稍明显一点,大多数人可接受。
3.3 量化精度不够时怎么办
有时候INT8量化掉点会比较厉害,尤其是小模型,RTMPose-t本身就轻量,敏感层被量化后误差可能被放大。遇到这种情况,不要急着换大模型,先尝试混合量化方案。
RKNN-Toolkit2支持设置部分层不参与INT8量化,保留为FP16或FP32精度。具体做法是在rknn.build之前先做一次不量化的build,用rknn的模型分析工具找出对精度影响较大的层,再针对这些层单独设置混合精度。更简单粗暴但有效的方法是:如果精度实在保不住,就退一步,将输出层附近的若干层设为FP16,其余保持INT8。这个方法实测可以把AP掉点降到1%以内,推理速度损失并不大。
这里还要强调一个测试指标问题。部署完模型后不要只看单张图像的主观效果就下结论,要用一个带标注的测试集跑一遍mAP或者PCK,量化前后对比。用我之前的项目数据做参考,RTMPose-t原模型在COCO val上AP约68%,INT8量化后大概65%左右,混合精度能回到66%以上,这个下降幅度对大多数业务场景都是够用的。
4. 板端推理落地与性能调优
4.1 RKNN Runtime的两种用法
RKNN Runtime在板端有两个主流用法:Python API和C API。Python API适合快速验证原型,代码写起来快,适合把流程跑通;生产环境建议用C API,内存占用更低,线程控制更灵活,性能也更好。
我个人的开发路径是先Python原型验证,再转C API做正式交付。Python侧核心逻辑比较简单:
from rknnlite.api import RKNNLite rknn_lite = RKNNLite() ret = rknn_lite.load_rknn('rtmpose-t.rknn') rknn_lite.init_runtime() outputs = rknn_lite.inference(inputs=[img_192x192_bgr])注意这里用的是rknnlite,不是PC端那个rknn库,板端SDK里带的是RKNNLite简化版。init_runtime执行后模型就加载到NPU里了,inference输入是一个四维numpy数组,形状是(1, 3, 192, 192),数据类型是uint8或者float32,具体取决于config里normalize的方式。
C API那边,代码量会大不少,但核心就三步:rknn_init加载模型、rknn_inputs_set输入数据、rknn_run + rknn_outputs_get拿输出。如果你没有C++开发经验,先用Python做完整项目也没问题,RK3588跑Python部署速度仍然可接受。
4.2 前后处理的正确姿势
RTMPose的输出结构需要特别说明。由于模型用的是simCC分类方式,输出通常是两个分支:x方向的分类得分和y方向的分类得分,每个分支的形状是[1, K, D],K是关键点数量(COCO是17),D是分类长度,一般等于输入尺寸除以4或者固定值。
后处理时,需要对每个关键点的x分支和y分支分别算softmax,然后用期望值(weighted average)算出浮点坐标,再乘上缩放因子映射回原图。这个处理逻辑比heatmap要简单,但也是容易出错的地方。有些版本的ONNX导出会在模型内部包含softmax,输出已经是归一化的概率分布,有些则没有,需要你在外部做。建议先用Netron查看输出节点定义,再写对应的后处理代码。
把坐标映射回原图是整个后处理中最关键的环节。RTMPose训练和推理都使用一个“框内姿态估计”的范式——先检测人体框,然后把框内的图像resize到192×192输入模型。模型输出的关键点坐标是相对于192×192图像坐标系的,要映射回原始帧,需要记录裁剪和缩放参数,然后逆变换回去。如果跳过这一步直接把输出坐标当成原图坐标使用,关键点位置会完全错乱。
如果不接检测器,直接把整帧图像resize成192×192输入模型,那映射公式就简单一些,只需要乘上原图尺寸和输入尺寸的比值,但这种方式在多人场景下效果会很差,因为模型训练时是以单人体为中心的。
4.3 多线程流水线与实测性能
姿态估计的完整推理链路包含解码、resize、推理、后处理四步。如果串行执行,每一步之间都在等,系统的吞吐量会很低。我用的是经典的四段流水线:线程1读视频帧,线程2做预处理,线程3做NPU推理,线程4做后处理和可视化。四组线程之间用队列衔接,队列满了就丢帧,保证整个系统不会因为某一帧处理慢而整体卡死。
线程同步这块要注意,RKNNLite推理接口本身是阻塞的,同一个实例不能同时被多个线程调用。如果要多线程并发推理,需要创建多个RKNNLite实例分别加载同一个模型,每个线程用各自的实例。实测NPU对多实例并发的支持还不错,但过多实例也有上限,一般两个到四个并发就足够覆盖大部分场景。
实测性能数据供参考。在RK3588上,RTMPose-t,192×192输入,INT8量化,NPU单次推理大约7-9ms,加上前后处理整个pipeline能跑到45-60fps(单路1080p输入,抠出人体框后做推理);RTMPose-m在同样的输入尺寸下,NPU单次推理约15-20ms,全流程约25-30fps。这个性能对实时互动类应用完全够用。
另一个影响性能的因素是输入图像缩放。Python下直接调cv2.resize,192×192的resize一次只需零点几毫秒,但如果先裁剪出多个人体框再逐个resize,会累积不少开销。建议用缓存池复用图像缓存,避免频繁申请内存。
5. 我踩过的坑:常见问题与排查速查表
5.1 转换阶段的坑
转换阶段最容易遇到的一个问题是ONNX格式不兼容。RTMPose如果从MMPose导出的ONNX版本过新,RKNN-Toolkit2可能报算子不支持的错误。解决办法是设置导出时的opset_version,我推荐先用opset_version=11试,不行再降到10,再不行就检查猫腻算子。Transformer结构里常见的GELU激活、LayerNorm在某些rknn版本上支持不够好,可以先用onnxsimplifier把图优化一遍再转换。
量化校准阶段的灾难现场是dataset.txt路径写错,或者图像打不开,工具直接报错退出。用绝对路径是最稳妥的,而且一定要在日志里确认每张校准图都被正常加载。我遇到过一种诡异情况,校准图集用了一组全是深色背景的图像,导致量化后模型在亮色环境下明显掉点,所以校准集覆盖范围要广,光照、服装颜色、姿态分布都要覆盖。
5.2 运行阶段的坑
板端运行最常见的错误是报libstdc++.so.6版本问题,原因是板子系统库版本和RKNN Runtime要求的版本不匹配。解决办法是升级系统库或者编译一个静态链接的推理程序,我更推荐后者。另一个常见问题是模型加载后init_runtime报错,提示找不到目标设备,多半是NPU驱动没加载,在板子上挂载并启用rknpu驱动就行。
推理结果异常这块,我总结过一张问题速查表:
| 现象 | 可能原因 | 解决参考 |
|---|---|---|
| 关键点整体偏到图像角落 | 归一化参数配错或通道顺序反了 | 检查mean/std和BGR/RGB设置 |
| 关键点位置对但抖动厉害 | 量化精度不够或输入分辨率偏低 | 混合量化保留FP16敏感层,提高分辨率 |
| 输出全为0或NaN | ONNX输出节点没找对 | 用Netron确认输出分支名称 |
| 首帧推理特别慢 | 模型在板端做了设备侧编译 | 预热一次推理,或转换时缓存设备模型 |
| 多线程调用报段错误 | 同一个RKNNLite实例被并发调用 | 每个线程独立加载模型实例 |
| 掉点严重且只在特定场景 | 量化校准集分布与实际场景不匹配 | 换用真实场景图像重新校准 |
后处理用softmax时如果输入数值过大,softmax容易出现溢出NaN。稳妥的处理方式是先减最大值再softmax,这在数值上等价但稳定性高很多。这是一行代码的事,但能省一晚上的排查时间。
5.3 精度与性能调优的经验
如果量化后关键点精度不够,先确认是不是校准集问题,校准图像最好在20到100张之间,太少分布覆盖不够,太多转换时间太长收益反而有限。然后是混合量化,把输出层和分类头相关的层设置为FP16,这是性价比最高的一招。再不行就提高输入分辨率,但性能会下降,属于最后手段。
性能调优方面,我建议用RKNN工具包自带的perf工具做profile,它能显示每一层NPU算子的耗时。如果发现某一层耗时特别高,优先考虑是不是该算子没有完全走NPU,而是回退到了CPU。另一个简单但有效的优化是调整NPU工作频率,RK3588的NPU支持不同频率档位,可以手动设置到最高档来换更低的单帧延迟,代价是功耗上升。如果是电池供电的设备要谨慎,如果是插电边缘盒子,直接拉满。
内存分配也值得注意。RKNN Runtime在运行时会为输入输出分配连续内存,如果在Python侧反复创建新的numpy数组,内存碎片会越来越多。建议在循环外预先分配好输入缓冲区和输出缓冲区,推理时直接复用,既能减少gc压力,也能让整体运行时间更稳定。
结尾
最后分享一个我自己在实际项目里的做法。我一般不会把RTMPose单模型当全线用,而是配一个轻量检测器,先做人脸或人体框检测,再把框送给RTMPose做姿态分析。RK3588 NPU跑一个RTMDet-s再加RTMPose-t,整体还能稳住实时,效果比只输入整帧要好很多。另外,如果业务对关键点稳定性要求高,建议在后处理端加一个轻量的坐标平滑算法,比如卡尔曼滤波或者滑动窗口平滑,能让姿态看起来更自然。在RK3588上部署RTMPose这件事,真正决定项目成败的不是模型本身,而是你在输入预处理、量化校准、坐标映射这些容易被忽略的细节上花了多少功夫。把每一步都抠干净,部署基本就成功一大半了。