news 2026/9/25 2:01:29

YOLOv8模型部署到RK3568:从ONNX到RKNN的完整量化与推理指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv8模型部署到RK3568:从ONNX到RKNN的完整量化与推理指南

YOLOv8大部分时候是在显卡上跑着玩的,真正让人头皮发麻的是把它塞进RK3568这种边缘盒子。我最近刚把YOLOv8s完整走了一遍从ONNX到RKNN的量化部署,中间踩了不少坑,也整理出了一套基本可以照抄的流程。这篇文章就把整条链路从头拆到尾:环境准备、模型导出、量化转换、板端推理、后处理,最后是性能和精度问题排查。文里的操作我都实际跑过,如果你用的是RK3566或者RK3588,流程差别也不大,主要是target_platform参数改一下。适合的读者很明确:打算把YOLOv8落到瑞芯微平台的嵌入式工程师、做边缘视觉方案的开发者,以及正在搞毕业设计需要完成模型部署的同学。

1. 整体方案:为什么偏偏是ONNX到RKNN这条路

1.1 先看清RK3568这颗芯片的底子

RK3568是一颗性价比非常高的SoC,四核Cortex-A55,最高主频2.0GHz左右,关键是带了一个NPU,算力在1TOPS @INT8这个级别。说实话,这个算力放在今天并不夸张,但做单路或者双路视觉检测完全够用。跑YOLOv8s这种模型,FP16或者FP32基本别想流畅,必须走INT8量化,量化之后NPU的利用率才能起来,单帧推理时间也能压到几十毫秒级别。

所以项目立项的时候你要先有个预期:RK3568不是拿来跑大模型的,它适合的是固定模型、低功耗、批量出货的视觉场景,比如门禁、闸机、IPC摄像头、AGV小车识别、工位安全检测这类任务。

还有个经常被问的问题:RK3568和RK3566有什么区别?从NPU角度看,两者是同一个NPU架构,算力水平也在一个档次,差异主要在外设、显示和成本上。部署的流程基本通用,转换时把target_platform设置成对应的型号就行。如果后续往RK3588上移植,流程也一样,RK3588的NPU算力高了几个档次,甚至可以跑更大分辨率的输入或更复杂的模型。

1.2 这条转换链路好在哪,以及有哪些替代方案

在瑞芯微平台上,模型部署的标准路径就是:训练好的权重 -> ONNX -> RKNN -> 板端推理。

为什么非要中间插一个ONNX?最直接的原因是RKNN-Toolkit2对ONNX的兼容性和调试体验最好。虽然它也能直接加载PyTorch模型,但PyTorch版本一换、自定义算子一多,转换失败的概率就上来了。ONNX相当于一个稳定的中间表达,导出之后你可以用Netron打开看结构,可以用onnxsim做简化,还可以在转换失败的时候很清楚地定位到是哪个算子出了问题。

有朋友可能会问能不能直接用TensorRT?不能,那是NVIDIA平台的东西。能不能用OpenVINO?同样不适合,瑞芯微的NPU只认RKNN格式。所以只要想用上这颗NPU的加速能力,找ONNX转RKNN这条路就是最主流的方案。

值得一提的还有这个工具链的兼容范围。现在不止YOLOv8能转,YOLOv5、YOLOX、YOLO26、DINOv3这些模型都有人成功跑通过。网上能搜到各种“某某模型转RKNN”的帖子,本质上都是同一套思路:先把模型导出为ONNX,再喂给RKNN-Toolkit2做解析和量化。

2. 环境准备:工具链真的不难装,坑在版本匹配

2.1 PC端rknn-toolkit2怎么装舒服

转换RKNN模型需要在PC上装rknn-toolkit2,注意是带完整转换功能的PC版本,不是板端那个精简版。这个工具依赖一堆Python库,包括numpy、onnx、opencv等,版本约束比较死,动不动就报protobuf版本不对、依赖冲突。

我的建议是不要在自己主力环境里硬装,直接用Docker。官方有现成镜像,拉下来把工作目录挂载进去就能用,省去所有依赖地狱。大致命令是这样:

docker run -it --rm \ -v $(pwd):/workspace \ --name rknn \ ruyisdk/rknn-toolkit2:1.6.0 \ /bin/bash

镜像是rhysd?记混了,具体镜像名称以你拿到的最新release为准,rknn-toolkit2的GitHub Release页面会写清楚。进去之后,把模型文件和脚本放到当前目录,在容器里直接跑转换脚本就行。

如果不用Docker,建议单独建一个Python3.8或3.10的虚拟环境来装,Ubuntu 18.04、20.04、22.04都有人跑通过。安装命令一般是:

pip install rknn-toolkit2-x.x.x-cp38-cp38-linux_x86_64.whl

装完可以跑一下from rknn.api import RKNN验证是否能导入,导入不报错基本就是环境OK了。

这里有个版本匹配的问题必须提前说:PC端的rknn-toolkit2版本、导出的.rknn模型格式,以及板端运行的rknpu2 runtime版本,三者是要对齐的。现实中很多人栽在这个地方:电脑上用新版本转换模型,板子上烧的还是老固件,结果运行时直接报“model version mismatch”或者“invalid model”。建议一次性把rknn-toolkit2和rknpu2的release配套版本都下载好,固定组合用。

2.2 板端运行时与Python推理库

板端要跑起来,需要两部分东西:一个是内核里的NPU驱动,一个是用户态的librknnmrt.so运行库。一般你烧写的系统固件里已经带好了驱动,不需要单独折腾。

Python推理方案推荐装rknn-toolkit-lite2,这是专门为板端推理设计的精简包,安装包很小,安装后使用RKNNLite接口。如果你的产品是C++架构,那就用rknpu2的C API,对应头文件是rknn_api.h。两者底层走的是同一个runtime,性能上没有本质差别。

板端装rknn-toolkit-lite2也比较简单:

pip install rknn_toolkit_lite2-x.x.x-cp38-cp38-linux_aarch64.whl

注意板端是aarch64,别下成x86的包。

如果你希望在PC上直接连板子调试,也可以不把模型文件拷到板端,而是通过USB连接,在PC版的RKNN中调用init_runtime(target='rk3568'),让工具自动把模型推到板载NPU上跑。这种方式调试起来非常方便,模型文件一改就能马上测,不用反复scp到板子上。

3. 模型转换实操:从YOLOv8的pt文件到int8的rknn

3.1 导出ONNX的两种姿势,以及为什么有时候要改造检测头

第一步是把YOLOv8的权重导出成ONNX。最省事的方式是用ultralytics自带的导出命令:

yolo export model=yolov8s.pt format=onnx opset=12 simplify=True imgsz=640

这里几个注意点:opset建议固定为12,选12是因为RKNN-Toolkit2对它的支持比较成熟;simplify=True会做一遍计算图简化,去掉一些冗余节点;imgsz固定为640,不要用dynamic动态输入,RKNN转换对动态shape的支持极差,能避开就避开。

导出之后,用Netron打开ONNX文件看一下输出节点。这里有个关键点:YOLOv8的模型输出到底是什么形状。

如果你只是用上面这条命令导出,Detect头会被完整导出,最终输出shape是(1, 84, 8400)。其中84 = 4个box坐标 + 80个COCO类别。这4个坐标其实是已经经过DFL解码和sigmoid处理后的结果,前4个通道是x1, y1, x2, y2的像素坐标。我强烈建议你导出后用Python打印一下输出的shape,因为不同ultralytics版本之间输出顺序可能会变。

第二种方式是导出“不带检测头解析逻辑”的版本,也就是让模型只输出三个特征图,把DFL解码、sigmoid这些操作挪到板端的后处理代码里。为什么要这么干?因为在RKNN量化转换时,检测头里的一些自定义算子(比如DFL中的卷积和exp、sigmoid组合)兼容性不够好,有时会导致转换失败,有时转换能过但量化精度明显下降。把检测头砍掉,只保留backbone和head的卷积输出,ONNX里剩下的就都是标准算子,转换非常顺。

具体做法是改一下ultralytics源码里的Detect.forward,让它只返回三个尺度的原始特征图,不执行dfl和sigmoid解码。导出的ONNX输出就是三个张量,每个张量shape为(bs, 84, 80*网格数)之类的,然后你在板端自己写解码。

两种方式怎么选?如果你的ONNX转换顺利,量化后效果能接受,建议优先用第一种,省事。如果碰到算子不支持或者量化后掉点严重,就果断用第二种,把后处理放回CPU执行。

3.2 RKNN转换脚本与量化校准数据

下面这是一个最基础的转换脚本,我直接放出来:

from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3568' ) rknn.load_onnx(model='yolov8s.onnx') rknn.build(do_quantization=True, dataset='dataset.txt') rknn.export_rknn('yolov8s.rknn') rknn.release()

脚本很短,但每一个参数都有讲究。

mean_values和std_values必须和训练时的预处理一致。YOLOv8训练时就是简单地除以255,所以mean填0,std填255。如果你填反了,比如std=[[0,0,0]],推理结果直接全乱,一个框都出不来。这个属于最常见低级错误,但出错率真的很高。

target_platform要和你板子的SoC对上,rk3568就写rk3568,rk3566就写rk3566。

do_quantization=True表示执行INT8量化。量化不是凭空把模型缩小一圈这么简单,它需要一组图片来估计每一层激活值的分布,这组图片就是校准数据集。

dataset.txt的内容如下,每行一个图片路径:

/workspace/calib/0001.jpg /workspace/calib/0002.jpg /workspace/calib/0003.jpg ...

校准图不要随便凑数,我强烈建议从训练集里随机抽200到500张,要求覆盖不同光照、不同场景、不同目标尺度。特别是真实业务场景中常见的视角、模糊程度、遮挡情况,校准集里都要有。很多人量化后精度从0.6直接掉到0.3,大概率就是校准图太少了,或者图都集中在某一种场景上。

如果量化后精度还是不理想,还有几个方向可以试。一是把量化方式换成混合量化,在config里加quantized_dtype='w8a16',保留部分层为FP16;二是用rknn.accuracy_analysis()做逐层精度分析,定位掉点严重的层,再决定是否把这些层排除量化。这个工具很好用,会输出每层的余弦相似度,一眼就能看到谁在拖后腿。

3.3 离线推理验证

转换完成的.rknn模型不要急着拷到板子上,先在PC端做一次离线推理验证,这一步能帮你节省大量时间。

方法是再用RKNN工具加载生成的rknn模型,喂一张测试图进去,拿到输出,同时用ONNX Runtime跑同一个输入,拿ONNX的输出,两者做对比:

import numpy as np outputs = rknn.inference(inputs=[img]) onnx_outputs = ort_session.run(None, {'images': img}) print('rknn output shape:', outputs[0].shape) print('onnx output shape:', onnx_outputs[0].shape) print('rknn output mean:', np.mean(outputs[0])) print('onnx output mean:', np.mean(onnx_outputs[0]))

对比重点是两个:shape是否一致,数值范围是否在一个量级。INT8量化后的输出本来就和FP32有误差,所以不需要数值完全一样,但如果shape对不上,那说明转换过程中图结构出了问题,必须回到上一步检查。

我个人的习惯是,转换完之后第一件事不是往后走,而是先在PC机上打印两边输出,形状不对的排查效率比到板子上高十倍。

4. 板端推理:从加载RKNN模型到画框显示

4.1 初始化RKNNLite

板端使用rknn-toolkit-lite2时,代码很短:

from rknnlite.api import RKNNLite rknn_lite = RKNNLite() ret = rknn_lite.load_rknn('yolov8s.rknn') if ret != 0: print('load rknn model failed') exit(1) rknn_lite.init_runtime()

一个特别需要注意的点:RKNNLite实例在同一个时间只能执行一次inference调用。如果你写了多线程程序,每个线程都去调用同一个实例的inference,会互相干扰甚至Crash。正确做法是每个线程维护一个单独的RKNNLite实例。跑多路视频的时候,可以做一个简单的实例池,提前创建N个实例,按需分配。

另外,load_rknn和init_runtime都很耗时,不要在循环里反复调用,模型加载一次就够了,后面每帧直接inference。

4.2 输入预处理:letterbox和颜色顺序

YOLOv8训练时用的预处理是letterbox到640x640,不是简单resize拉伸。如果推理时用普通resize,框的位置会出现系统性偏移,特别是当原图长宽比和640差别较大的时候。

我的预处理函数是这样写的:

import cv2 import numpy as np def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): shape = img.shape[:2] # (h, w) 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 if shape[::-1] != new_unpad: img = cv2.resize(img, new_unpad, interpolation=cv2.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, value=color) return img, r, dw, dh

调用时注意颜色顺序。用cv2.imread读出来的图是BGR,但YOLOv8训练时用的是RGB顺序,RKNN默认也期望RGB输入,所以读图之后要转一下:

img = cv2.imread('test.jpg') img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_letterbox, ratio, dw, dh = letterbox(img_rgb) img_input = img_letterbox.astype(np.float32) img_input = np.expand_dims(img_input, axis=0)

转成float32还是uint8?我的经验是float32更稳,因为ONNX输入类型本来就是float32。RKNN会根据你在config里设置的mean/std在模型内部做归一化,你喂给它的数据保持0-255范围即可,不用自己手动除255。

4.3 后处理:解码、NMS、坐标还原

模型推理代码:

outputs = rknn_lite.inference(inputs=[img_input])

拿到outputs之后,第一件事是打印它的shape,因为你的YOLOv8导出方式不同,输出可能是(1, 84, 8400),也可能是(1, 8400, 84),还可能是三个特征图。这里我以输出(1, 84, 8400)为例,写一个完整的后处理:

def postprocess(output, conf_thres=0.25, iou_thres=0.45): preds = output[0] if preds.shape[1] != 84: preds = preds.transpose(0, 2, 1) preds = preds[0] # (84, 8400) boxes = preds[:4].T # (8400, 4) 格式 x1 y1 x2 y2 cls_scores = preds[4:].T # (8400, 80) class_ids = np.argmax(cls_scores, axis=1) scores = cls_scores[np.arange(cls_scores.shape[0]), class_ids] mask = scores > conf_thres boxes = boxes[mask] class_ids = class_ids[mask] scores = scores[mask] if len(boxes) == 0: return [] keep = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), conf_thres, iou_thres) if len(keep) > 0: keep = np.array(keep).flatten() result = [] for i in keep: x1, y1, x2, y2 = boxes[i] result.append((x1, y1, x2, y2, scores[i], class_ids[i])) return result

这里有个很关键的点:如果RKNN的输出本身就是量化后的int8类型,而rknnlite通常已经帮你反量化成float32了,所以你不需要手动做反量化。如果你发现输出值是0到255之间的整数,再去查是不是拿到了原始量化数据。

拿到坐标之后,要还原回原图坐标。前面letterbox函数返回了ratio、dw、dh:

x1_orig = (x1 - dw) / ratio y1_orig = (y1 - dh) / ratio x2_orig = (x2 - dw) / ratio y2_orig = (y2 - dh) / ratio

不做这一步,框的位置就会整体偏上或偏下,目标越大偏得越明显。

如果你的模型导出的是三个特征图,那后处理要自己实现DFL解码。过程其实也不复杂,就是把每个特征图对应的grid坐标乘上stride,再对DFL分支做softmax加权求和,得到四边距离,最后得到x1y1x2y2。具体代码我就不贴了,网上有很多现成实现,关键是理解四个输出值分别代表目标框四条边距离网格中心点的偏移量,而不是直接代表坐标。

5. 调优与排坑:实测数据、常见问题和心得

5.1 性能实测与优化方向

在我的RK3568板子上,开性能模式的情况下,YOLOv8n INT8、640输入,单帧推理时间大概在35到60毫秒之间;YOLOv8s INT8、640输入,大概在90到130毫秒之间。这个数据会受固件版本、DDR频率、CPU频率策略的影响,仅供参考。如果是YOLOv8m或更大的模型,单帧时间就会逼近200毫秒,就不太适合RK3568了。

性能优化我建议按这个顺序做:

第一,降低输入分辨率。从640降到416,推理时间能省40%左右,AP的损失通常在可接受范围。很多室内固定机位场景,320也能凑合用。

第二,换更小的模型。YOLOv8n比YOLOv8s在NPU上的推理时间能快一倍,对于轻量任务完全够用。

第三,把前后处理和NPU推理放到不同线程,形成流水线。NPU在计算第N帧的时候,CPU同时在处理后处理的第N-1帧,整体吞吐量会上去。

第四,检查板子的CPU和NPU频率调节策略。有些固件默认的调频策略比较保守,手动切到performance模式后,推理时间会有明显改善。查看和设置NPU频率一般通过sysfs节点操作,不同BSP略有差异。

5.2 常见问题速查表

现象可能原因解决办法
一个框都检测不到mean/std填反或填错检查config中mean_values/std_values与训练预处理是否一致
能出框但位置偏移letterbox坐标没还原后处理坐标按(dw, dh)和ratio反算回原图
检测结果错乱BGR/RGB顺序不对cv2读图后记得cvtColor转成RGB
目标能识别但置信度普遍偏低量化校准数据不足或太单一补充200到500张覆盖各种场景的校准图
RKNN转换报某个算子不支持检测头里有自定义算子导出ONNX时去掉检测头,或改用更低opset版本
板端初始化报model mismatchPC工具链与板端runtime版本不匹配统一rknn-toolkit2与rknpu2的版本组合
多线程推理卡死或异常多个线程共用同一个RKNNLite实例每个线程独立创建RKNNLite实例
速度突然变慢CPU/NPU降频检查频率策略,必要时切performance模式

5.3 三个印象最深的坑

第一个坑是颜色通道顺序。我刚开始部署的时候,直接用cv2读图喂给RKNN,不转RGB,结果检测结果东倒西歪,我还以为是量化精度问题,排查了很久才发现就是缺了一行cvtColor。这个坑之所以隐蔽,是因为颜色错乱在可视化框上并不会让你觉得“框跑偏”,而是类似模型失效的奇怪表现。

第二个坑是letterbox的坐标还原。有次检测车流,原图是1920x1080的宽屏,letterbox之后上下加了一百多像素的灰边,我忘了在坐标准换的时候去掉这部分偏移,结果所有车的框都明显偏上。后来我干脆把预处理函数和后处理映射写在一个模块里,保证ratio和pad信息必定被保存下来,避免再次遗漏。

第三个坑是量化校准图没有覆盖真实场景。第一次量产测试时,模型在实验室数据上表现很好,到了现场强光环境直接漏检一半。原因就是校准集全是干净的白天图像,现场有逆光、有模糊、有遮挡,激活值分布差异太大。后来我把真实环境的抓拍图混进校准集,问题立刻缓解。量化校准绝不是走过场,它的数据质量直接决定了模型在你目标场景里的表现。

我个人现在的体会是,跑通YOLOv8s到RK3568这件事本身难度不大,真正的工程工作量都在细节里:版本匹配、预处理对齐、量化校准、坐标还原,每一环都不能想当然。最后再分享一个小技巧:到板子上验证的时候,一定把后处理结果画出来保存成图片,肉眼看到的错误信息比任何打印日志都直观。等这一条链路稳定之后,车牌识别、安全帽检测这些新需求都能快速套用同一套流程,换个模型权重重新出一版RKNN就好。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:00:45

Turtlebot2导航全解析:SLAM建图、路径规划与参数调优实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 2:00:30

Jetson Orin NX WiFi断连排查:从驱动超时到持久化修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:59:43

HTML5多图片上传预览核心实现与内存优化:从FileReader到ObjectURL

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:59:39

2026芯片IP选型实战手册:避坑指南与决策树

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:59:39

ESP32-C5双频Wi-Fi 6模块实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华