YOLOv5s 部署 RK3588:ONNX 转 RKNN 与 INT8 量化实战

发布时间:2026/10/17 11:05:17

YOLOv5s 部署 RK3588:ONNX 转 RKNN 与 INT8 量化实战 YOLOv5s 在 PC 上训练完拿到 RK3588 上跑中间隔着的不是一条命令而是一整条工具链。我见过太多人卡在同一个地方PyTorch 权重转 ONNX 成功ONNX 转 RKNN 报错或者转出来了但推理结果一塌糊涂分类全乱、框全飘。这一篇就专门讲清楚从 ONNX 到 RKNN 再到 INT8 量化这条链路把每一步的坑和判断依据都摊开说。这篇是系列第三篇假设你已经有了训练好的 YOLOv5s 权重也搞定了 RK3588 的 Ubuntu 环境和 NPU 驱动。如果你还没到这一步建议先回去把前两篇的环境搭建和模型导出看完。本篇聚焦的核心就三件事ONNX 导出的正确姿势、RKNN 转换的参数逻辑、INT8 量化数据集怎么准备才不掉精度。1. 为什么 ONNX 是这条链路绕不开的中间站1.1 从 PyTorch 到 RKNN 的必然中转RKNN-Toolkit2 不直接吃 PyTorch 的 .pt 文件它支持的输入格式是 ONNX、TensorFlow、Caffe、Darknet 这几类。对 YOLOv5s 来说ONNX 是最顺的一条路因为 Ultralytics 官方就提供了 export 脚本而且 ONNX 的算子表达和 RKNN 的算子支持列表重合度最高。你可以把 ONNX 理解成一个通用翻译件。PyTorch 说的是方言RKNN 只认普通话ONNX 就是那个普通话版本。中间多一道翻译好处是解耦——PyTorch 版本升级、训练框架换掉只要 ONNX 不变后面 RKNN 的流程就不用动。但这里有个反直觉的点ONNX 导出不是越新越好也不是 opset 越高越好。RKNN-Toolkit2 对 opset 版本有明确的支持范围我实测下来 opset 12 是最稳的opset 11 也可以但到了 opset 16、17 就容易在转换阶段报算子不支持。原因在于高版本 opset 引入了一些新算子或者改变了某些算子的属性定义RKNN 的解析器还没跟上。1.2 导出 ONNX 时那几个必须改的参数Ultralytics 的 export.py 默认导出是给 ONNX Runtime 用的直接拿去转 RKNN 会出问题。核心要改三个地方opset 版本显式指定--opset 12别用默认值。输入尺寸固定成--img-size 640 640RKNN 对动态 shape 支持有限固定尺寸能省掉后面一堆麻烦。简化模型加--simplify这一步会调用 onnx-simplifier 把冗余算子合并掉对后续量化精度有正面影响。我用的导出命令是这样的python export.py --weights yolov5s.pt --include onnx --opset 12 --img-size 640 640 --simplify --dynamic注意这里我加了--dynamic但后面转 RKNN 时又会把动态维度固定死。为什么多此一举因为 YOLOv5 的导出脚本在某些版本下不加 dynamic 会导致输出节点的维度信息不完整加上之后 ONNX 的 graph 更规范转 RKNN 时解析更顺。这是一个经验性的操作不是必须但能减少报错概率。导出完成后强烈建议用 Netron 打开看一眼。重点确认三件事输入节点名字是不是images输出是不是三个检测头对应 stride 8、16、32以及有没有残留的aten或prim开头的算子。如果有说明导出没干净得回去检查 simplify 是否生效。1.3 ONNX 自检转 RKNN 前的最后一道关在把 ONNX 喂给 RKNN-Toolkit2 之前我习惯先用 ONNX Runtime 跑一遍确认模型本身没问题。这一步能帮你排除掉到底是 ONNX 的问题还是 RKNN 的问题。import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov5s.onnx) input_name sess.get_inputs()[0].name dummy np.random.randn(1, 3, 640, 640).astype(np.float32) outputs sess.run(None, {input_name: dummy}) for i, o in enumerate(outputs): print(foutput {i}: shape{o.shape}, dtype{o.dtype})如果这一步能正常输出三个张量shape 分别是(1, 25200, 85)或者分头的(1, 255, 80, 80)之类说明 ONNX 是健康的。如果这里就报错那问题在导出环节别往下走。提示ONNX Runtime 和 ONNX 是两个东西。ONNX 是格式标准ONNX Runtime 是执行引擎。前者定义长什么样后者负责跑起来。很多人把这两个概念混在一起排查问题时容易找错方向。2. RKNN 转换参数背后的取舍逻辑2.1 环境准备里最容易翻车的地方RKNN-Toolkit2 的安装本身不复杂但它对 Python 版本和依赖库版本很挑。我踩过的坑集中在两点第一Python 版本必须是 3.6 到 3.10 之间。3.11 及以上会直接装不上因为部分依赖 wheel 还没适配。我建议用 3.8 或 3.9这两个版本兼容性最好。第二numpy 版本要锁。RKNN-Toolkit2 对 numpy 的某些 API 有依赖numpy 1.24 以上会报np.float已移除之类的错误。锁到 1.23.x 最稳。pip install numpy1.23.5 pip install rknn-toolkit21.6.0版本号这块1.6.0 是我在 RK3588 上验证过比较稳的一版。太老的版本对 YOLOv5 的某些算子支持不好太新的版本又可能和板端 runtime 不匹配。工具链版本和板端 runtime 版本必须对齐这是硬性要求不对齐会出现PC 上转换成功板子上加载失败的情况。2.2 config 里每个参数到底在干什么RKNN 转换的核心是一个 config 字典参数不多但每个都有讲究。我把它拆开讲from rknn.api import RKNN rknn RKNN(verboseTrue) rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588, quantized_dtypeasymmetric_quantized-8, quantized_algorithmnormal, optimization_level3 )mean 和 std这两个参数最容易搞错。YOLOv5 训练时的预处理是img / 255也就是把像素从 0-255 归一化到 0-1。所以在 RKNN 里mean 设 0std 设 255等价于做了一次除以 255 的操作。如果你训练时用了别的归一化方式这里必须对应改否则量化后的输入分布和训练时对不上精度直接崩。target_platform必须写rk3588写错了转换出来的模型板子加载不了。quantized_dtype选asymmetric_quantized-8这是非对称 INT8 量化。RK3588 的 NPU 对非对称量化的支持更好实测精度也比对称量化高一点。optimization_level我一般开到 3这是最高级别的图优化会做一些算子融合和常量折叠。级别越高转换越慢但推理越快。如果转换时报错可以降到 2 试试有时候是某个优化 pass 和特定算子冲突。2.3 加载 ONNX 与 build 的关键节点config 之后是 load_onnx 和 buildret rknn.load_onnx(modelyolov5s.onnx) if ret ! 0: print(load_onnx failed) exit(ret) ret rknn.build(do_quantizationTrue, dataset./dataset.txt) if ret ! 0: print(build failed) exit(ret)do_quantizationTrue就是开启 INT8 量化。这里有个关键点量化不是可选项是 RK3588 上跑 YOLOv5s 的必选项。FP16 虽然精度高但 RK3588 的 NPU 对 FP16 的算力支持远不如 INT8实测同一模型 INT8 比 FP16 快 2 到 3 倍。FP32 就更不用说了基本是 CPU 在跑没有实用价值。dataset参数指向量化校准数据集这是整个流程里最影响精度的一环下一节专门讲。build 过程中如果报算子不支持通常有两种处理方式一是回退 opset 版本重新导出 ONNX二是用 RKNN 的自定义算子接口。前者更省事优先试。2.4 导出 RKNN 模型与精度分析build 成功后导出ret rknn.export_rknn(./yolov5s_int8.rknn)导出后别急着上板子先在 PC 上做一次精度分析。RKNN-Toolkit2 提供了accuracy_analysis接口可以对比量化前后每一层的输出差异rknn.accuracy_analysis( inputs[./test_image.jpg], output_dir./snapshot, targetrk3588 )这个分析会生成每一层的余弦相似度报告。重点看余弦相似度低于 0.9 的层这些层就是量化损失的重灾区。如果集中在检测头附近说明量化对输出分布影响大需要调整校准集如果集中在 backbone 的某些卷积层可能是这些层的权重分布太分散可以考虑混合量化。混合量化是 RKNN 提供的一个进阶功能允许把某些层保持 FP16其余层 INT8。用法是在 config 里指定hybrid_quantization或者在 build 时传入自定义的量化配置。代价是这部分层会走 FP16 通路速度略降但精度能救回来。3. INT8 量化数据集决定精度生死的一环3.1 校准集不是随便找几张图就行INT8 量化的本质是用一批代表性数据跑一遍模型统计每一层激活值的分布范围然后据此确定量化的 scale 和 zero_point。所以这批数据的分布必须和实际推理时的数据分布一致。我见过最常见的错误是拿训练集里随便抽 100 张图当校准集。如果训练集和实际场景差异大量化后的模型在实际场景里精度会掉得很厉害。正确的做法是校准集要从实际部署场景的数据里抽。比如你要做的是园区安防检测那就从实际摄像头抓的图里抽如果是工业质检就从产线相机里抽。数量上官方建议 100 到 500 张我实测 200 张左右是个平衡点再多收益递减。3.2 dataset.txt 的格式与常见错误dataset.txt 就是一个纯文本文件每行一个图片路径./calib/001.jpg ./calib/002.jpg ./calib/003.jpg ...看起来简单但坑不少路径必须是绝对路径或相对于执行目录的正确相对路径。我遇到过因为路径写错RKNN 静默跳过所有图片量化用了默认分布结果精度惨不忍睹。图片格式要统一。混着 jpg 和 png 一般没事但如果混了灰度图和彩色图量化统计会出错。图片尺寸不要求统一RKNN 会自动 resize 到模型输入尺寸。但 resize 的方式是直接缩放如果你的实际推理流程里有 letterbox校准集最好也预先做 letterbox保持分布一致。注意dataset.txt 里的图片数量如果太少比如少于 20 张RKNN 会警告量化统计不充分。这个警告不能忽略它直接对应精度下降。3.3 量化精度掉了怎么救量化后精度下降是常态关键是怎么定位和补救。我总结了一个排查顺序现象可能原因处理方式分类全乱框位置还行检测头量化损失大对检测头做混合量化保持 FP16框位置偏移明显backbone 特征提取受损增加校准集数量检查 mean/std小目标漏检严重浅层特征量化粗糙提高校准集里小目标样本比例整体置信度偏低量化 scale 偏大换 asymmetric 量化调 optimization_level我遇到过一次典型情况量化后 mAP 从 0.89 掉到 0.72。排查发现校准集用的是训练集图片而实际场景是夜间红外图分布差异巨大。换成 200 张实际红外图重新量化后mAP 回到 0.86。这个案例说明校准集的代表性比数量重要得多。另一个技巧是校准集里要有一定比例的困难样本。全是清晰正样本量化统计会偏向简单分布遇到复杂场景就崩。我一般会刻意混入 20% 左右的遮挡、逆光、小目标样本。3.4 量化前后的对比验证方法量化完必须做对比验证不能只看 RKNN 的转换日志。我的做法是在 PC 上用 ONNX Runtime 跑一遍测试集记录 mAP。用 RKNN-Toolkit2 的仿真模式不连板子跑同一测试集记录 mAP。上板子用 RKNN Runtime 跑再记录一次。三次结果的差异能定位问题在哪一环。如果 1 和 2 差很多是量化的问题如果 2 和 3 差很多是板端 runtime 或输入预处理的问题。仿真模式的代码大概是这样rknn.init_runtime(targetrk3588, perf_debugTrue) outputs rknn.inference(inputs[img])perf_debugTrue会打印每一层的耗时方便定位性能瓶颈。如果某一层耗时异常高可能是这层被回退到 CPU 执行了需要检查算子支持情况。4. 上板实测从加载到推理的完整链路4.1 板端 runtime 版本对齐板子上的 RKNN Runtime 版本必须和 PC 上 Toolkit 版本匹配。查看板端版本cat /usr/lib/librknnrt.so | grep -a version或者用 rknn_server 的日志确认。版本不匹配的典型症状是加载模型时报RKNN_ERR_MODEL_INVALID或者直接段错误。如果版本不对需要更新板端的 librknnrt.so。这个库在 RKNN-Toolkit2 的安装包里就有路径一般在rknpu2/runtime/RK3588/Linux/librknn_api/aarch64/下直接替换板子上的对应文件即可。4.2 输入预处理的坑letterbox 必须一致YOLOv5 的标准预处理是 letterbox保持长宽比缩放然后填充灰边到 640x640。如果你在 RKNN 转换时没有把 letterbox 做进模型那板端推理前必须手动做同样的 letterbox。我见过有人板端直接 resize 到 640x640结果检测框全部偏移。原因是 resize 改变了长宽比而模型是在 letterbox 数据上训练的坐标映射对不上。板端 letterbox 的 Python 实现def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, (left, top)后处理时要用返回的 ratio 和 padding 把框映射回原图坐标这一步漏了框就会偏。4.3 后处理三个检测头的解码RKNN 输出的三个张量对应 stride 8、16、32 三个尺度。如果导出时没做 decode输出是原始预测值需要自己解码。解码逻辑和 YOLOv5 的 detect 层一致sigmoid 激活、加上 grid 偏移、乘上 anchor、再乘 stride。这部分代码量不小我建议直接用现成的实现比如 rknn_model_zoo 里的 yolov5 示例。但要注意不同版本的输出布局可能不同有的是(1, 25200, 85)有的是分头的(1, 255, 80, 80)。用 Netron 看清楚输出节点的 shape再对应写解码。4.4 性能实测与调优RK3588 上 YOLOv5s INT8 的实测数据我这边跑下来大概是配置单帧耗时FPS单核 NPU约 25ms40三核 NPU约 12ms83三核 零拷贝约 9ms110三核 NPU 是通过 RKNN 的多线程推理实现的把模型复制三份三个线程并行跑。RK3588 有 3 个 NPU 核心理论上能到 3 倍吞吐。实测没到 3 倍是因为内存带宽和调度开销。零拷贝是指输入数据直接从 DMA 缓冲区读取省掉一次内存拷贝。这个需要在初始化 runtime 时设置rknn.init_runtime(core_maskRKNN_NPU_CORE_0_1_2)并用rknn_inputs_set的零拷贝接口。如果 FPS 上不去先看 perf_debug 的输出确认没有层回退到 CPU。有回退的话要么改模型结构要么用混合量化把不支持的层单独处理。5. 几个反复踩到的坑和对应解法5.1 转换成功但推理输出全零这个现象我遇到过两次。第一次是 mean/std 设反了把 mean 设成 255、std 设成 0输入直接变成 inf。第二次是 ONNX 导出时用了 dynamic shapeRKNN 转换时没固定导致输入维度是 -1推理时数据没喂进去。排查方法先用一张全 0 的图推理看输出是不是全 0再用一张全 255 的图看输出有没有变化。如果两张图输出一样基本是输入没进去或者预处理错了。5.2 量化后 mAP 掉超过 10 个点这种情况八成是校准集的问题。按这个顺序排查校准集路径是否正确、图片数量是否够、分布是否匹配实际场景、mean/std 是否和训练一致。我遇到过一次是校准集图片全是 1920x1080而实际推理是 640x640resize 后的分布差异导致量化 scale 偏大。把校准集预先 resize 到 640x640 后问题解决。5.3 板端加载模型报内存不足RK3588 的 NPU 有独立内存但默认分配可能不够。YOLOv5s INT8 模型大概 7MB 左右正常不会内存不足。如果报这个错检查是不是同时加载了多个模型或者模型没释放。RKNN 的 release 接口要记得调用否则重复加载会累积占用。5.4 多线程推理时的线程安全问题三核并行时每个线程要有独立的 RKNN context不能共享。共享会导致输出错乱或者崩溃。正确的做法是每个线程RKNN()初始化一次加载同一份模型文件各自持有独立的 context。import threading def worker(model_path, input_data): rknn RKNN() rknn.load_rknn(model_path) rknn.init_runtime(core_maskRKNN_NPU_CORE_AUTO) # 推理... rknn.release() threads [threading.Thread(targetworker, args(path, data)) for _ in range(3)]core_mask设成 AUTO 让 runtime 自动分配核心比手动指定更省心。6. 关于精度和速度的取舍我的一点经验整条链路走下来最耗时的不是转换本身而是量化精度的调优。我的经验是先把 FP16 跑通确认整条链路没问题再切 INT8 调精度。FP16 转换几乎不会掉精度用它验证 ONNX 导出、RKNN 转换、板端加载、后处理这一整套流程是否正确。流程通了再开 INT8 量化这时候如果精度掉了问题范围就缩小到量化这一环排查效率高很多。另一个体会是校准集的准备工作值得花时间。我现在的习惯是专门维护一个校准集目录从实际场景里持续补充样本每次量化前从中随机抽 200 张。这个目录的样本分布会随着实际场景变化而更新量化精度也就一直保持稳定。至于 INT8 和 FP16 的选择如果你的场景对精度极其敏感比如医疗影像FP16 多花的那点时间可能值得。但绝大多数检测场景INT8 的精度损失在 2 到 3 个点以内速度翻倍这笔账怎么算都划算。
延伸阅读

更多相关文章

2026/10/17 11:00:16

Tiptap无渲染架构:解耦UI与状态的富文本编辑器设计哲学

1. 为什么“无渲染”是Tiptap最被低估的硬核优势?很多人第一次看到“Tiptap”这个名字,下意识会把它和Quill、CKEditor、TinyMCE这些老牌富文本编辑器划进同一类——不就是个带工具栏、能加粗斜体、插图片表格的所见即所得组件吗?直到某天&am…

2026/10/17 11:00:16

FinBERT分词器完全指南:30522词表与5个特殊标记的工作原理

【免费下载链接】finbert 项目地址: https://ai.gitcode.com/hf_mirrors/ProsusAI/finbert 点击查看 免费下载 FinBERT 是专为金融文本训练的情感分析模型,可输出 positive / negative / neutral 三类判断。它的"第一站"是 BERT 分词器&#…

2026/10/17 12:25:22

AI情报系统重构:向量引擎+大模型实现语义检索与RAG闭环

信息爆炸这个词说了十几年,真正做情报系统的人才懂它有多疼。每天几十个RSS源、上百条行业快讯、一堆PDF报告和内部文档,全堆在面前,你要的是"某竞品最近一个月在华南区的投放变化",结果翻了几十页搜索结果,…

2026/10/17 12:25:22

基于YOLOv8的疲劳驾驶检测系统:从数据标注到部署的完整实践

前阵子做了个基于YOLOv8的疲劳驾驶检测系统,从数据标注、模型训练到UI界面集成,整个链路自己完整跑了一遍。这篇文章把技术选型的考虑、训练参数怎么调、推理端的疲劳判断策略,还有实操中踩过的坑一次性整理出来,给后面要在这条路…

2026/10/17 12:25:22

高速自由流收费稽核AI一体机:从疑点流水到证据包的落地实践

上个月我参加了一场面向路段运营方的方案演示,屏幕打开后我看到一个此前从没在宣传册里讲清楚的工作流:一套AI一体机接入了门架的图片流和OBU流水流,自动把一条“货车在门架A到门架B之间OBU轨迹丢失,但相邻门架照片显示车辆全程在…

2026/10/17 12:25:22

Hive导入导出CSV全攻略:从建表到踩坑实战

1. 项目背景与核心诉求解析做数据开发的同学,十有八九都跟CSV打过交道。CSV这玩意儿说简单也简单,就是个逗号分隔的纯文本文件,说麻烦也麻烦——编码问题、引号转义、换行符不一致,每一样都能折腾人半天。我自己在实际项目中接过不…

2026/10/17 12:25:22

QN8035 FM单芯片驱动开发:I2C通信、RDS解析与自动搜台实战

简介:这是一份面向嵌入式开发者的QN8035 FM单芯片驱动代码包,针对STM32平台下通过模拟I2C控制FM广播接收芯片的场景,解决调频接收、音量调节与RDS数据解码等功能的驱动实现问题,适合具备一定单片机与I2C通信基础的中级开发者参考移…

2026/10/17 12:20:22

Shell echo命令详解:引号、转义与环境变量实战

如果你刚开始写 shell 脚本,echo 大概率是你敲得最多的命令之一。它做的事情很简单——把字符串打印到终端,或者把环境变量的值显示出来。但就是这个看似不起眼的命令,引号用错、转义漏掉、变量没展开,脚本就能跑出各种莫名其妙的…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑