ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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(targetrk3568)让工具自动把模型推到板载NPU上跑。这种方式调试起来非常方便模型文件一改就能马上测不用反复scp到板子上。3. 模型转换实操从YOLOv8的pt文件到int8的rknn3.1 导出ONNX的两种姿势以及为什么有时候要改造检测头第一步是把YOLOv8的权重导出成ONNX。最省事的方式是用ultralytics自带的导出命令yolo export modelyolov8s.pt formatonnx opset12 simplifyTrue imgsz640这里几个注意点opset建议固定为12选12是因为RKNN-Toolkit2对它的支持比较成熟simplifyTrue会做一遍计算图简化去掉一些冗余节点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_platformrk3568 ) rknn.load_onnx(modelyolov8s.onnx) rknn.build(do_quantizationTrue, datasetdataset.txt) rknn.export_rknn(yolov8s.rknn) rknn.release()脚本很短但每一个参数都有讲究。mean_values和std_values必须和训练时的预处理一致。YOLOv8训练时就是简单地除以255所以mean填0std填255。如果你填反了比如std[[0,0,0]]推理结果直接全乱一个框都出不来。这个属于最常见低级错误但出错率真的很高。target_platform要和你板子的SoC对上rk3568就写rk3568rk3566就写rk3566。do_quantizationTrue表示执行INT8量化。量化不是凭空把模型缩小一圈这么简单它需要一组图片来估计每一层激活值的分布这组图片就是校准数据集。dataset.txt的内容如下每行一个图片路径/workspace/calib/0001.jpg /workspace/calib/0002.jpg /workspace/calib/0003.jpg ...校准图不要随便凑数我强烈建议从训练集里随机抽200到500张要求覆盖不同光照、不同场景、不同目标尺度。特别是真实业务场景中常见的视角、模糊程度、遮挡情况校准集里都要有。很多人量化后精度从0.6直接掉到0.3大概率就是校准图太少了或者图都集中在某一种场景上。如果量化后精度还是不理想还有几个方向可以试。一是把量化方式换成混合量化在config里加quantized_dtypew8a16保留部分层为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, 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, 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, axis0)转成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_thres0.25, iou_thres0.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, axis1) 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、dhx1_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就好。
返回列表