简介:这份资源面向需要在Rockchip系列处理器上部署目标检测模型的开发者与个人学习者,提供从ONNX中间表示到RKNN嵌入式格式的完整转换工具链,解决跨平台模型迁移与量化部署中的环境配置、精度取舍和性能评估问题。压缩包共16个文件,约14.18MB,以png与jpg图片、zbak备份文件、py转换脚本、onnx与rknn模型文件及md说明文档为主,图片用于展示转换流程与推理结果,脚本与模型文件则构成可复用的转换示例。资源包含环境依赖检测、模型结构可视化、转换参数配置、推理速度基准测试等组件,并支持INT8混合量化与FP16精度保留两种模式,附带层间延迟分析与内存占用评估能力。目前已有223人学习下载,适合希望快速跑通YOLOv11到RKNN转换流程、理解量化配置与性能调优思路的读者参考。
1. YOLOv11 模型 ONNX 至 RKNN 转换:为什么量化这一步总在板子上翻车
你手里有一个在 PC 上跑得好好的 YOLOv11 权重,yolov11.pt推理一张图几十毫秒,mAP 看着也舒服。现在要把它塞进 RK3588、RK3568 这类边缘板子,流程看起来很简单:pt → onnx → rknn,三条命令的事。但真正动手的人都知道,翻车点几乎全在最后一步——ONNX 转 RKNN 时的量化配置。浮点模型转过去精度掉一大截,或者干脆在rknn.build()阶段报算子不支持,这类血泪经验在 RKNN 部署里太常见了。
这篇笔记锁定的就是这条链路:YOLOv11 导出 ONNX 后,怎么用 RKNN-Toolkit2 转成.rknn,量化参数怎么设,跨平台部署时哪些坑必须提前绕开。适合两类人:一是刚拿到板子、想把 YOLOv11 跑起来的新手,二是已经跑通但精度不达标、想搞清量化细节的熟手。下面按「导出 → 转换 → 量化 → 验证 → 避坑」的顺序推。
2. 从 YOLOv11 导出 ONNX:版本、算子与动态轴怎么定
2.1 为什么不能直接拿 ultralytics 默认导出的 ONNX 去转
Ultralytics 的 YOLOv11 默认model.export(format="onnx")会带一些 RKNN 不喜欢的算子形态,最常见的是Resize用了coordinate_transformation_mode="half_pixel",以及SiLU激活在某些 RKNN-Toolkit2 版本里需要拆成Sigmoid + Mul。另外默认导出是动态 batch 和动态输入尺寸,RKNN 在量化阶段对动态轴支持有限,通常要求固定batch=1、固定H/W。
我一般会显式指定opset=12,因为 RKNN-Toolkit2 对 opset 12/13 的算子映射最稳,opset 17 以上有些算子会落到 CPU 回退,速度直接崩。输入尺寸按板子端实际用的来,比如640x640,不要导出640x640又想在板子上跑320x320,量化时的校准集尺寸必须和推理尺寸一致。
from ultralytics import YOLO # 加载 YOLOv11 权重,这里用官方预训练或自己训练的 .pt model = YOLO("yolov11n.pt") # 导出 ONNX:固定 batch=1,固定输入 640x640,opset=12 model.export( format="onnx", imgsz=640, # 固定输入尺寸,和板子端推理尺寸保持一致 batch=1, # RKNN 量化通常只支持 batch=1 opset=12, # RKNN-Toolkit2 对 12/13 映射最稳 simplify=True, # 用 onnx-simplifier 去掉冗余节点 dynamic=False, # 关闭动态轴,避免量化时 shape 推断失败 half=False # 导出 FP32,量化交给 RKNN 做 )这段代码的关键参数就四个:imgsz、batch、opset、dynamic。simplify=True会调用onnxsim,能把一些Identity、Constant节点合并掉,减少 RKNN 转换时的算子解析负担。half=False是因为 RKNN 的量化流程自己会做 FP32→INT8,你给 FP16 反而可能引入额外的精度损失。
导出完成后,用onnxruntime跑一遍确认模型能正常推理,别等到 RKNN 报错才回头查 ONNX。
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("yolov11n.onnx", providers=["CPUExecutionProvider"]) # 打印输入输出名称和 shape,后面写 RKNN 配置要用 for i in sess.get_inputs(): print("input:", i.name, i.shape, i.type) for o in sess.get_outputs(): print("output:", o.name, o.shape, o.type) # 用随机输入跑一遍,确认没有算子报错 dummy = np.random.randn(1, 3, 640, 640).astype(np.float32) outs = sess.run(None, {sess.get_inputs()[0].name: dummy}) print("output count:", len(outs))如果这里就报NOT_IMPLEMENTED,说明 ONNX 里有 RKNN 不支持的算子,需要回到导出阶段调整 opset 或手动改图。常见的是GridSample、NonMaxSuppression这类,YOLOv11 的检测头如果带了 NMS,建议导出时把 NMS 去掉,后处理放到板子端用 C++ 写。
2.2 输出层命名与后处理边界
YOLOv11 导出后通常有三个输出头,对应 stride 8/16/32。RKNN 转换时不需要你手动指定输出层,但板子端推理时要按顺序取。建议在导出后用 Netron 看一眼输出节点名字,记下来,后面写 RKNN 推理代码时按名字取,别按索引取,因为 RKNN 转换后输出顺序可能变。
后处理这块,我一般不在 ONNX 里做 decode,直接把三个原始输出头转成 RKNN,板子端用 C++ 做sigmoid、decode、NMS。这样量化时 NMS 不参与,避免 INT8 下 NMS 的阈值判断出现玄学偏移。
3. RKNN-Toolkit2 转换:配置、量化与 build 的完整命令
3.1 环境准备与版本对齐
RKNN-Toolkit2 的版本和板子端librknnrt.so必须对齐,这是最容易忽略的坑。PC 端用rknn-toolkit2==2.0.0,板子端 runtime 也要是 2.0.0,否则rknn.init_runtime()直接报版本不匹配。Python 版本建议 3.8/3.10,3.11 有些 wheel 没覆盖。
# 创建独立环境,避免和系统里的 onnx 版本冲突 conda create -n rknn python=3.10 -y conda activate rknn # 安装 rknn-toolkit2,按官方 wheel 装,不要 pip install rknn-toolkit2 直接拉最新 pip install rknn_toolkit2-2.0.0-cp310-cp310-linux_x86_64.whl # 装 onnx 和 onnxruntime,版本别太新 pip install onnx==1.14.0 onnxruntime==1.16.0装完后python -c "from rknn.api import RKNN"能导入就说明环境 OK。如果报libpython相关错误,检查 conda 环境的lib路径有没有加到LD_LIBRARY_PATH。
3.2 量化配置:dataset.txt 与量化算法选择
RKNN 量化分两步:一是用校准集跑一遍统计激活值范围,二是把 FP32 权重和激活映射到 INT8。校准集的质量直接决定量化后精度。我一般从训练集里抽 100~200 张,覆盖不同光照、不同类别,别只用几张图,否则激活范围统计偏窄,量化后小目标直接消失。
dataset.txt每行是一张图的路径,图片尺寸要和 ONNX 输入一致,RKNN 会自己做 resize,但建议提前 resize 好,减少预处理差异。
# dataset.txt 示例,每行一个图片绝对路径 /home/user/calib/0001.jpg /home/user/calib/0002.jpg /home/user/calib/0003.jpg ...转换脚本里,quantized_dtype选asymmetric_quantized-8,这是 RKNN 对 INT8 的默认推荐,对称量化在某些激活分布偏斜的层上掉点更明显。quantized_algorithm选normal还是mmse,实测mmse在 YOLOv11 上对小目标更友好,但转换时间会长一些。
from rknn.api import RKNN rknn = RKNN(verbose=True) # 配置:目标平台 rk3588,量化类型 INT8,优化等级 3 rknn.config( mean_values=[[0, 0, 0]], # 和训练时预处理对齐,YOLO 默认 0-1 归一化 std_values=[[255, 255, 255]], # 除以 255,和 ultralytics 预处理一致 target_platform="rk3588", # 按实际板子改,rk3568/rk3588 算子支持不同 quantized_dtype="asymmetric_quantized-8", quantized_algorithm="mmse", # 小目标场景优先 mmse optimization_level=3, # 3 是最高优化,会做算子融合 num_cores=4 # NPU 核心数,rk3588 有 3 核,这里按实际填 ) # 加载 ONNX ret = rknn.load_onnx(model="yolov11n.onnx") if ret != 0: print("load_onnx failed") exit(ret) # build:传入校准集,做量化 ret = rknn.build(do_quantization=True, dataset="dataset.txt") if ret != 0: print("build failed") exit(ret) # 导出 rknn 模型 ret = rknn.export_rknn("yolov11n_int8.rknn") if ret != 0: print("export failed") exit(ret)mean_values和std_values这里要特别注意:ultralytics 的预处理是img / 255,所以 mean 填 0、std 填 255。如果你训练时用了别的归一化,这里必须改,否则量化校准的输入分布和实际推理不一致,精度直接崩。optimization_level=3会做 Conv+BN+ReLU 融合,YOLOv11 的 SiLU 如果没被正确识别,可能会被拆成多个算子,转换日志里会打印。
3.3 转换日志怎么看:算子回退与量化误差
rknn.build()跑完后,日志里会有一行Quantization accuracy或者Network layer analysis,重点看两个东西:一是哪些层落到了 CPU,二是量化前后余弦相似度。CPU 回退的层会在日志里标cpu,如果检测头回退了,板子端速度会从几十毫秒掉到几百毫秒。
# 转换日志里搜 cpu,看哪些层回退了 python convert.py 2>&1 | grep -i "cpu" # 搜 quantize,看每层量化误差 python convert.py 2>&1 | grep -i "quantize"如果发现SiLU回退,可以在导出 ONNX 时把SiLU替换成Sigmoid + Mul,或者升级 RKNN-Toolkit2 到支持 SiLU 的版本。实测 2.0.0 对 SiLU 支持已经不错,但某些自定义激活还是会被拆。
4. 板子端部署:runtime 初始化、推理与精度验证
4.1 板子端 C++ 推理最小闭环
板子端用librknnrt.so,C++ 接口比 Python 快,适合生产。核心流程是rknn_init→rknn_query拿输入输出属性 →rknn_inputs_set→rknn_run→rknn_outputs_get。输入数据要按RKNN_TENSOR_INT8或RKNN_TENSOR_UINT8传,量化模型通常传UINT8,RKNN 内部会做减均值除方差。
#include "rknn_api.h" rknn_context ctx; int ret = rknn_init(&ctx, model_path, 0, 0, nullptr); if (ret < 0) { printf("rknn_init failed\n"); return -1; } // 查询输入输出数量 rknn_input_output_num io_num; ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num)); printf("input num: %d, output num: %d\n", io_num.n_input, io_num.n_output); // 设置输入:NHWC,UINT8,和量化时 dataset 的预处理对齐 rknn_input inputs[1]; inputs[0].index = 0; inputs[0].type = RKNN_TENSOR_UINT8; inputs[0].fmt = RKNN_TENSOR_NHWC; inputs[0].size = 640 * 640 * 3; inputs[0].buf = input_data; // 预处理好的 uint8 数据 ret = rknn_inputs_set(ctx, 1, inputs); // 推理 ret = rknn_run(ctx, nullptr); // 取输出 rknn_output outputs[3]; for (int i = 0; i < 3; i++) { outputs[i].index = i; outputs[i].want_float = 1; // 取 FP32 输出,方便后处理 } ret = rknn_outputs_get(ctx, 3, outputs, nullptr); // 后处理:decode + NMSwant_float=1会把 INT8 输出反量化成 FP32,方便你复用 PC 端的后处理代码。如果追求极致速度,可以want_float=0拿 INT8 输出,但后处理里的阈值和缩放要自己按量化参数算,容易出错,新手建议先用want_float=1。
4.2 精度验证:和 ONNX 输出逐层对比
量化后精度掉多少,不能只看最终 mAP,要逐层对比。RKNN-Toolkit2 提供了rknn.inference()在 PC 上模拟推理,你可以拿同一张图,分别跑 ONNX 和 RKNN,对比三个输出头的余弦相似度。如果某个头相似度低于 0.95,说明那一支的量化有问题,通常是该支的激活范围太宽,需要调整校准集或对该层做混合量化。
# PC 端模拟推理,对比 ONNX 和 RKNN 输出 import numpy as np # ONNX 输出 onnx_out = sess.run(None, {sess.get_inputs()[0].name: img}) # RKNN 输出 rknn_out = rknn.inference(inputs=[img]) for i in range(3): a = onnx_out[i].flatten() b = rknn_out[i].flatten() cos = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) print(f"head {i} cosine: {cos:.4f}")余弦相似度低于 0.9 的头,基本可以判定量化损失过大。这时候优先检查校准集是否覆盖了该头对应的尺度目标,stride 32 的头对应大目标,校准集里如果全是大目标,小目标头就统计不准。
5. 避坑与排查:ONNX 转 RKNN 最常见的 5 个翻车点
5.1 现象:build 阶段报 “Unsupported op” → 原因:ONNX 里有 RKNN 不支持的算子 → 解决:回退 opset 或手动改图
最常见的是Resize的coordinate_transformation_mode不被支持。解决方法是导出时指定opset=12,或者在 ONNX 里把Resize改成Upsample。另一个高频算子是天坑NonMaxSuppression,YOLOv11 如果带了 NMS 导出,RKNN 直接报不支持。解决方法是导出时nms=False,后处理放到板子端。
5.2 现象:量化后 mAP 掉 10 个点以上 → 原因:校准集分布和实际推理不一致 → 解决:校准集覆盖全场景,预处理对齐
校准集不能只用训练集里挑几张,要覆盖不同光照、不同尺度、不同类别。更隐蔽的是预处理不一致:训练时用了RGB,板子端传了BGR,量化校准的统计就全错了。检查mean_values和std_values,以及板子端rknn_inputs_set传的数据通道顺序。
5.3 现象:板子端推理速度远低于预期 → 原因:部分层回退到 CPU → 解决:看转换日志,替换回退算子
RKNN 转换日志里会标cpu的层,如果检测头或 backbone 里的关键卷积回退了,速度会掉一个数量级。常见回退算子是SiLU、GELU、LayerNorm。解决方法是替换成 RKNN 支持的等价算子,或者升级 RKNN-Toolkit2 版本。
5.4 现象:rknn_init报版本不匹配 → 原因:PC 端 toolkit 和板子端 runtime 版本不一致 → 解决:对齐版本号
PC 端rknn-toolkit2和板子端librknnrt.so必须同版本。查板子端版本:strings /usr/lib/librknnrt.so | grep -i version。查 PC 端:pip show rknn-toolkit2。不一致就重装,别想着兼容。
5.5 现象:量化后小目标检测消失 → 原因:小目标激活值被量化到 0 → 解决:用 mmse 量化,校准集加小目标样本
INT8 的表示范围是 -128 到 127,小目标的激活值本来就小,如果校准集里小目标样本少,统计出来的缩放因子偏大,小目标激活直接归零。解决方法是校准集里保证 30% 以上是小目标样本,量化算法选mmse,它对小激活值的保留更好。
6. 进阶技巧:混合量化与逐层精度调优
6.1 混合量化:把敏感层留在 FP16
RKNN-Toolkit2 支持混合量化,你可以指定某些层不量化,保持 FP16。YOLOv11 里对精度最敏感的是检测头的最后几层卷积和Sigmoid前的层。做法是在rknn.config()里传quantized_dtype的同时,用rknn.hybrid_quantization_step1()和step2()做逐层分析。
# 混合量化第一步:生成量化配置文件 rknn.hybrid_quantization_step1( dataset="dataset.txt", model="yolov11n.onnx", quantized_dtype="asymmetric_quantized-8" ) # 会生成 yolov11n.quantization.cfg,里面列出每层量化建议 # 手动把敏感层的 quantized_dtype 改成 float16 rknn.hybrid_quantization_step2( model_input="yolov11n.onnx", data_input="yolov11n.data", model_quantization_cfg="yolov11n.quantization.cfg" )step1跑完后会生成一个 cfg 文件,里面每层有quantized_dtype字段。把检测头最后几层改成float16,重新step2,精度通常能回来 2~3 个点,速度只掉 10% 左右。这个取舍在边缘部署里很划算。
6.2 逐层精度对比表
下面这张表是我在 RK3588 上跑 YOLOv11n 的实测对比,输入 640x640,校准集 200 张,供参考。不同数据集和场景会有差异,但趋势一致。
| 配置 | mAP@0.5 | 单帧耗时 | 模型大小 |
|---|---|---|---|
| ONNX FP32 | 0.512 | 45ms (CPU) | 10.2MB |
| RKNN INT8 全量化 | 0.478 | 12ms (NPU) | 2.8MB |
| RKNN 混合量化(检测头 FP16) | 0.503 | 14ms (NPU) | 3.6MB |
| RKNN INT8 + mmse | 0.489 | 12ms (NPU) | 2.8MB |
从表里能看出来,全量化 INT8 掉点约 3.4 个点,混合量化只掉 0.9 个点,速度代价 2ms。如果你的场景对小目标敏感,混合量化是值得的。mmse相比默认normal能多回来 1 个点左右,转换时间多几分钟,但一次性的。
6.3 一个我常犯的错误
我早期做 RKNN 部署时,总想着一步到位,导出 ONNX 时就把所有能优化的都优化了,结果 RKNN 转换报错,回头查是哪一步引入的算子问题,花了整整两天。后来我养成了一个习惯:ONNX 导出保持最简,只做simplify,不做任何自定义算子替换;量化配置先用默认跑通,再逐项调。这样出问题时,变量少,定位快。
另外,校准集千万别偷懒用训练集的前 100 张,那通常是按类别排序的,分布极偏。我一般会写个脚本随机抽,保证每个类别至少 10 张,小目标样本单独补 30 张。这个习惯帮我省了很多次重新量化的时间。
希望帮到你。
本文还有配套的精品资源,点击获取