
1. 先搞清楚RK3588 的 NPU 到底怎么用1.1 为什么非要走 RKNN 这条链路很多第一次接触 RK3588 的朋友拿到板子装好系统以后第一反应是把 YOLOv5 的 Python 代码直接跑起来。代码能跑但跑的是 CPU图像一进来就能感觉到风扇转速上去了FPS 却惨不忍睹。原因很简单PyTorch 模型文件.pt里面存的是张量运算图它再聪明也只是一堆计算指令而 NPU 是一个专用的硬件加速器它只认自己的一套指令格式。想让 NPU 干活必须先把模型从“PyTorch 能读的格式”翻译成“NPU 能读的格式”这个格式在瑞芯微的平台上就叫 RKNN。所以整条链路是固定的PyTorch 权重 → ONNX 中间格式 → RKNN 模型 → 交给 NPU 推理。ONNX 在这里扮演的是通用翻译官的角色它和具体硬件无关哪个平台都能读。瑞芯微提供的 RKNN-Toolkit2 工具链负责把 ONNX 再翻译成 RKNN翻译过程中还会做一件很重要的事——量化。原本模型里的浮点参数可以压成 int8体积变小、速度变快代价是精度会有一点点损失。这个后面聊先说路线。1.2 两条落地路线PC 端转换 板端推理还是全在板子上做在麒麟V10系统上跑通 YOLOv5可以走两条路差别在于“在哪台机器上完成 RKNN 转换”。路径 A 是常规做法在一台 x86 的 PC 上安装 RKNN-Toolkit2把.pt转成.onnx再转成.rknn然后把.rknn文件拷到 RK3588 板子上。板子这边只需要安装很轻量的运行时库比如 rknn-toolkit-lite2或者干脆直接调用 C API。转换过程不消耗板子的资源调试也方便。路径 B 是顺着标题来的做法在麒麟V10系统上直接安装完整的 RKNN-Toolkit2转换和推理全在板子上一站式完成。好处是省了一台 PC坏处是麒麟V10 的 Python 环境和官方工具链之间偶尔会出现依赖打架的情况需要手动补一些包。我个人的建议是如果你手头有 x86 电脑老老实实走路径 A这是生产环境里最通用的结构。如果只有板子或者就是想体验一把全流程路径 B 完全可行只是要有心理准备在环境上多花点时间。这篇教程会以“转换在 PC、推理在麒麟V10板子”为主线写同时把板端直装的注意点也标出来两条路都覆盖。1.3 别把 RK3588 当成昇腾那块 NPU有一个非常常见的误区我在论坛里见过好几个人问为什么在代码里设置设备为 NPU或者看到类似 “npu is selected as device, but torch_npu is not available” 的报错就开始到处找 torch_npu 的安装包。这是典型的走错片场。torch_npu 是昇腾系列芯片的适配层和 RK3588 没有关系。RK3588 的 NPU 驱动和推理接口都是瑞芯微自己的一套体系Python 侧的入口叫rknn.api.RKNN底层是 librknnrt。把 torch_npu 那套思路搬到 RK3588 上只会浪费一整天时间。记住一句话RK3588 上跑 AI 模型工具链就是 RKNN-Toolkit2推理接口就是 init_runtime inference没有第二个官方主流选项。想清楚这一点后面每一步都不会跑偏。2. 环境准备麒麟V10 上把工具链装到能用的状态2.1 先确认系统、Python 和 NPU 驱动都在场不管走哪条路线动手前先花两分钟把底细摸清楚。在板子上打开终端执行这几条命令cat /etc/os-release uname -a python3 --version ls /dev/rknpu dmesg | grep -i rknpu前面三条是确认系统版本、内核和 Python 版本后面两条是关键。如果/dev/rknpu设备节点存在说明固件里的 NPU 驱动已经加载这是后面一切推理的前提。如果看不到这个节点再多代码也白搭大概率是固件问题去换一个带 NPU 驱动的新版固件。麒麟V10 系统的底座是 Linux目前常见的桌面版走的是 Debian/Ubuntu 血统的包管理所以 apt 工具是能用的。这也是后续补依赖的基础。我个人在装各种工具链前习惯先把系统软件源更新一遍sudo apt update sudo apt upgrade -y这一步不是必须但能减少很多“装到一半发现某个底层库版本太老”的破事。2.2 搭建 Python 环境与处理系统依赖Python 环境我强烈建议用 conda 或者 venv 隔离不要直接往系统 Python 里塞东西。麒麟V10 的系统 Python 是系统组件依赖的万一被 pip 搞乱版本后果很麻烦。你只需要一个独立的虚拟环境来跑 RKNN 和推理代码干干净净。创建虚拟环境的方式看个人习惯conda 的话conda create -n rknn python3.8 -y conda activate rknn如果板子是 aarch64 架构装 RKNN-Toolkit2 的包版本要对上 Python 版本。多数 RKNN-Toolkit2 的 wheel 是为 x86 准备的aarch64 要单独找对应版本这一点在 2.3 节展开。然后是系统依赖的坑。很多人装完工具链一执行import cv2就报错最常见的一句话是libGL.so.1: cannot open shared object file。这个和 OpenCV 本身没关系是图像显示模块依赖的图形库没装。Debian/Ubuntu 体系下一行搞定sudo apt install -y libgl1 libglib2.0-0另外还有 gcc、g、make、cmake 这类的编译工具虽然 Python 接口不一定用得上但保不齐哪个组件要现场编译提前装上省心sudo apt install -y build-essential cmake2.3 安装 RKNN-Toolkit2 并核对版本关系RKNN-Toolkit2 的安装是整套流程里最容易踩坑的一步因为它对版本非常敏感。工具链版本、板子固件里的 NPU 驱动版本、运行库 librknnrt 版本这三个必须配套。版本对不上代码再对也会在 init_runtime 阶段给你一个莫名其妙的错误码。一个典型的安装例子是在 x86 PC 上装 1.6.0 版本pip install rknn-toolkit2-1.6.0-cp38-cp38-linux_x86_64.whl注意包名里的cp38这代表需要 Python 3.8。不同版本的 wheel 对应的 Python 版本不一样装之前先确认一下自己在虚拟环境里的 Python 版本。在麒麟V10板子上直接装完整版需要找 aarch64 对应的 wheel。如果找不到两条路一是干脆走路径 A用 PC 转换、板子只装 lite 版推理二是直接在板子上编译源码但会很折腾没有强烈需求不建议。还有一个小细节RKNN-Toolkit2 对 numpy 的版本有要求。用太新的 numpy比如 2.x经常会报一些浮点精度或者接口兼容的警告有的版本甚至会直接报错。稳妥的办法是安装工具链时让 pip 自动解析依赖或者手动把 numpy 锁在 1.xpip install numpy2 opencv-python装完以后验证一下python -c from rknn.api import RKNN; print(ok)能打印ok环境就算通了一大半。接下来开始折腾模型本身。3. 把 YOLOv5 导出成 ONNX这一步决定了后面一半的坑3.1 准备 YOLOv5 工程与权重既然是要“跑通 YOLOv5”那就直接拿官方仓库来。在 PC 上找个目录执行git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt这里提醒一句官方仓库的 requirements.txt 会装 torch、torchvision 一堆东西如果只是在 PC 上做转换后面的推理其实用不到 torch但导出 ONNX 这一步确实需要它。所以该装还得装。如果网络环境一般可以设置国内 PyPI 镜像加速pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple权重文件先用官方的yolov5s.pt跑通全流程是最稳妥的。这个模型小、转换快后续如果要做自己的数据集再把自己训的权重放在同一路径下替换就行。第一次做绝对别一上来就用自己训的大模型否则一旦某个后处理逻辑出问题你根本分不清是模型的问题还是流程的问题。3.2 export.py 关键参数与理由导出 ONNX 用的是官方自带的 export.py命令长这样python export.py --weights yolov5s.pt --include onnx --opset 12 --simplify这里两个参数非常关键。第一个是--opset 12。ONNX 的算子集版本越高能表达的运算越丰富但 RKNN 工具链对高版本算子支持得更晚。我见过太多人直接用了默认的 opset 17结果转换时报一屏 Unsupported operator。把 opset 压到 12是 RKNN 转换最稳的区间。如果你的模型里有些特殊算子必须高版本支持再一个个往上加但起步一定是 12。第二个是--simplify。onnx-simplifier 会遍历计算图把很多复合算子合并成更简单的算子顺便剔除一些对结果没有影响的冗余节点。这样做的好处是 RKNN 转换时遇到的节点种类更少、更主流成功率明显更高。不夸张地说光加了这个参数就能少踩一半的坑。导出完成后你会得到一个yolov5s.onnx文件。建议花一分钟用 Netron 打开看看图重点确认最后一层输出节点的名称和维度后面写后处理和转换脚本都用得上。3.3 理解导出后的输出结构YOLOv5 的检测头有三个尺度输入 640x640 的图时三个输出特征图的分辨率分别是 80x80、40x40、20x20。每个特征图位置的每个 anchor 会预测 85 个数4 个框坐标xywh 1 个置信度 80 个类别概率。一组 anchor 是 3 个所以一个尺度的输出通道是 3 × 85 255。原始导出模型输出的形状类似[1, 255, 80, 80] [1, 255, 40, 40] [1, 255, 20, 20]注意通道在中间这是 PyTorch 常用的 NCHW 布局。RKNN 推理拿到的输出就是这个样子后面后处理必须先把通道维换到末尾再变形才能解析。也有一种做法是改模型结构在导出前手动加一个 permute 和 reshape让输出直接变成[1, 25200, 85]。这种方法后端写起来省事但会牺牲一点转换时的灵活性而且一旦后面要移植到 C/C 部署反而要多处理一层预定义的张量形状。我的建议是保持原版输出在后处理里自己解码这样逻辑通用后续换模型、换部署语言都不用改架构。4. 编写 RKNN 转换脚本量化不是玄学4.1 最小可用的转换脚本接下来是整条链路的核心环节把.onnx转成.rknn。新建一个文件convert.py内容如下from rknn.api import RKNN ONNX_MODEL yolov5s.onnx RKNN_MODEL yolov5s.rknn DATASET dataset.txt rknn RKNN(verboseTrue) # 配置输入输出与量化方式 rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3588 ) # 加载 ONNX 模型 if rknn.load_onnx(modelONNX_MODEL) ! 0: raise RuntimeError(load onnx failed) # 构建 RKNN 模型do_quantizationTrue 表示做 int8 量化 if rknn.build(do_quantizationTrue, datasetDATASET) ! 0: raise RuntimeError(build rknn failed) rknn.export_rknn(RKNN_MODEL) rknn.release()这个脚本本身就是完整可用的。“mean_values 和 std_values”这两个参数要特别留意它定义的是模型输入侧的数据归一化方式。YOLOv5 训练时用的是把图像像素从 0-255 缩放到 0-1等价于 mean0、std255。转换时这样配置推理前就不需要自己在代码里再除以 255 了这个细节到推理章节还会再强调一次。target_platformrk3588是告诉工具链为哪颗芯片做编译优化。如果你的板子是 rk3576 或者别的型号这里要对应改掉。4.2 校准数据集怎么准备才靠谱上面脚本里的dataset.txt是量化校准文件里面每行写一张图片的路径。它的作用是让工具链在量化时统计模型各层激活值的真实分布然后决定每个 float32 数值映射到 int8 时怎么取缩放系数。很多人图省事随手往里写三五张图甚至从网上下个一张图凑数结果转换出来的模型在测试集上目标框一个都检不到或者置信度低得离谱。这不是 RKNN 的问题是校准集样本太少、太偏。我的建议是准备 50 到 200 张图来源要贴近真实使用场景。比如你要检测的是停车场里的车就多放停车场各个角度、各个光线条件下的图你要检测的是工厂传送带上的零件就多放传送带上的真实照片。图片本身不需要标注模型只是拿它们做激活值统计不是训练。图片默认是会被工具统一缩放到模型输入尺寸的但如果你发现量化后精度特别差可以把图片先自己用 OpenCV 缩放到 640x640、再按 RGB 顺序保存成二进制或处理后的图片再把处理后的路径写进 dataset.txt。这个动作的本质是消除“工具缩放算法”和“真实预处理算法”之间的差异很多精度问题其实都出在这个不一致上。4.3 int8 与 fp16精度和速度怎么权衡rknn.build()里的do_quantization参数决定模型是否做 int8 量化。int8 是 RK3588 NPU 发挥 6 TOPS 算力的前提模型体积小、推理快代价是精度有损。如果你对精度要求高比如检测小目标、密集场景量化后掉点明显可以暂时把do_quantization设为 False工具会按 fp16 甚至 fp32 保存权重。这个模式下精度几乎无损速度比 int8 要慢一些但通常仍比纯 CPU 推理快得多。我的习惯是先用 int8 跑通全流程看效果如果检测框总是偏、置信度普遍偏低再切成 fp16 对比一次。别一上来就追求最优先把链路走通比什么都重要。另外转换脚本跑完会打印各种 layer 的量化统计信息有 verbose 日志在能清楚地看到每一层被压缩成 int8 后的数据范围。如果看到某些层的数据范围特别异常比如最大值比别的层大几个数量级就要警惕该层量化损失严重必要时可以在 config 里给这些层单独设置不量化的策略。不过这是进阶玩法新手遇到最多的情况还是校准集问题先从那着手。5. 板端推理预处理、解码到 NMS 的完整代码5.1 推理运行时选哪种rknn-toolkit、lite 还是 CAPI.rknn文件生成以后就到了板子这边。麒麟V10上推理有两种姿势。第一种是板子上装了完整 RKNN-Toolkit2直接from rknn.api import RKNN调用load_rknn init_runtime来做推理。这个姿势胜在方便而且可以在同一环境里调试转换但缺点是重量级不适合最终交付。第二种是只装轻量的推理运行时比如rknn-toolkit-lite2它同样提供 Python 接口但只支持加载已经转换好的.rknn文件不能做转换。这两种接口的调用方式几乎一样代码可以无缝切换。再往下就是 C API直接动态链接librknnrt.so性能更好、依赖更少但那属于正式产品阶段的事情这篇先把 Python 流程捋通。小白记住一句话用 lite2 就够了。安装方式就是在板子的虚拟环境里pip install rknn-toolkit-lite2-1.6.0-cp38-cp38-linux_aarch64.whl同样的版本对应原则依然适用。5.2 输入预处理最容易翻车的地方推理代码我会一段段拆开讲。先看预处理部分import cv2 import numpy as np # 读取图像 img cv2.imread(bus.jpg) # 颜色通道转换OpenCV 默认 BGR模型训练用的是 RGB img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 缩放到模型输入尺寸 img cv2.resize(img, (640, 640)) # 转成 float 类型保留 0-255 范围 img img.astype(np.float32) # 调整维度为 NCHW img np.transpose(img, (2, 0, 1)) img np.expand_dims(img, axis0)这里有个最重要的细节不要在外部再除以 255。转换脚本的 config 里已经写了mean_values[[0,0,0]]、std_values[[255,255,255]]这意味着 RKNN 工具会在推理时自动对输入做(pixel - 0) / 255的归一化。如果你在代码里又手动除一遍 255相当于输入变成了原来的 1/255检测结果必然全部异常。这个错我见过太多人踩包括我自己第一次也栽在这。判断标准很简单如果 config 里设了 std255外部就保留 0-255 的浮点输入如果 config 里设了 std1外部才需要手动归一化到 0-1。一定要保持两端一致。5.3 后处理代码解码、还原坐标和 NMS推理本身只有一行outputs rknn.inference(inputs[img])outputs是一个列表里面是三个特征图数组。麻烦的是解码。新建一个postprocess.py把下面这套代码贴进去import numpy as np OBJ_THRESH 0.25 NMS_THRESH 0.45 ANCHORS { 80: [[10, 13], [16, 30], [33, 23]], 40: [[30, 61], [62, 45], [59, 119]], 20: [[116, 90], [156, 198], [373, 326]] } def sigmoid(x): return 1.0 / (1.0 np.exp(-x)) def decode_output(output, stride, grid_size): # output shape: [1, 255, grid, grid] output output[0].reshape(3, 85, grid_size, grid_size) output output.transpose(0, 2, 3, 1) # [3, grid, grid, 85] boxes [] scores [] for i, anchor in enumerate(ANCHORS[grid_size]): layer output[i] # [grid, grid, 85] conf sigmoid(layer[..., 4]) class_conf sigmoid(layer[..., 5:]) * conf[..., None] cls np.argmax(class_conf, axis-1) cls_conf np.max(class_conf, axis-1) # 过滤低置信度 mask cls_conf OBJ_THRESH if mask.sum() 0: continue xs, ys np.meshgrid(np.arange(grid_size), np.arange(grid_size)) cx (xs sigmoid(layer[..., 0])) * stride cy (ys sigmoid(layer[..., 1])) * stride w np.exp(layer[..., 2]) * anchor[0] * stride h np.exp(layer[..., 3]) * anchor[1] * stride x1 cx - w / 2 y1 cy - h / 2 x2 cx w / 2 y2 cy h / 2 box np.stack([x1, y1, x2, y2, cls_conf, cls.astype(np.float32)], axis-1) boxes.append(box[mask]) return boxes这段代码做的事是把 YOLOv5 特征图里的相对预测值还原成原图坐标系下的真实框。关键点是特征图上每个 cell 预测的 cx、cy 要先加格点索引再乘 stride才是原图坐标宽度和高度用的是 exp 指数映射再乘以对应的 anchor 尺寸和 stride类别概率首先要过 sigmoid再和置信度相乘得到每个类别的最终得分。把三个尺度的结果合并后再做 NMS非极大值抑制把同一目标上的重复框去掉def nms(boxes, score_threshOBJ_THRESH, iou_threshNMS_THRESH): if len(boxes) 0: return [] boxes np.concatenate(boxes, axis0) if len(boxes) 1 else boxes[0] order np.argsort(boxes[:, 4])[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(boxes[i, 0], boxes[order[1:], 0]) yy1 np.maximum(boxes[i, 1], boxes[order[1:], 1]) xx2 np.minimum(boxes[i, 2], boxes[order[1:], 2]) yy2 np.minimum(boxes[i, 3], boxes[order[1:], 3]) inter np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1) iou inter / ( (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) (boxes[order[1:], 2] - boxes[order[1:], 0]) * (boxes[order[1:], 3] - boxes[order[1:], 1]) - inter ) order order[1:][iou iou_thresh] return boxes[keep]最后在主程序里把推理和后处理串起来outputs rknn.inference(inputs[img]) boxes decode_output(outputs[0], 8, 80) boxes decode_output(outputs[1], 16, 40) boxes decode_output(outputs[2], 32, 20) results nms(boxes)坐标是相对于 640x640 输入尺寸的如果要画回原图记得按缩放比例还原scale_x orig_width / 640.0 scale_y orig_height / 640.0 # 对 results 里每个框的 x1, x2 乘 scale_xy1, y2 乘 scale_y这一步极其容易被忽略检测结果画上去总是偏多半是坐标还原没做对。5.4 实测数值与瓶颈分析我在麒麟V10板子上跑通后的实际数字YOLOv5s 输入 640x640int8 量化NPU 单帧推理大约 50 到 70 毫秒加上图像读取、预处理、后处理整链路一帧大概 90 到 120 毫秒也就是 8 到 11 FPS 的样子。这个速度对实时摄像头检测来说刚好够用对追求流畅视频流来说还有优化空间办法在下一章说。瓶颈通常不在 NPU 本身。NPU 推理完成后三个特征图要回传到 CPU 内存解码和 NMS 全是 Python 循环这一块 Python 的解释执行开销很大经常比 NPU 推理还慢。所以如果后续想提速方向很清楚要么缩小输入分辨率要么把解码和 NMS 搬进 C/C 或者用多线程优化。6. 常见问题速查与性能优化实录6.1 高频报错对照表整理一份我实际遇到过、以及被身边朋友问得最多的报错对照表按症状查就行现象大概率原因处理办法import cv2报libGL.so.1找不到系统缺 OpenCV 依赖库sudo apt install -y libgl1from rknn.api import RKNN失败装的是 lite 版却试图用完整接口或 wheel 架构不对确认包名和 Python 版本、架构匹配init_runtime返回负数错误码工具链版本和固件 NPU 驱动不匹配检查 librknnrt 版本统一工具链与固件Unsupported operator报错ONNX opset 过高或未做 simplify导出时加--opset 12 --simplify转换通过但推理什么都检不到输入预处理和 config 的 mean/std 不一致外部不要重复归一化确认 0-255 或 0-1 只保留一套检测框位置错乱后处理坐标没有还原到原图分辨率按原图和输入尺寸比例换算坐标量化后精度明显崩掉校准集图片太少或场景不真实准备 50 张以上贴近真实场景的图优先验证 dataset 文件速度没有预期的快解码和 NMS 在 Python 层耗时过大降低输入尺寸或者把后处理换 C/C 实现第一列是现象第二列是原因第三列是解法。这张表建议截图存一份后面做项目大概率用得上。6.2 性能优化三板斧第一板斧是降分辨率。这是收益最大、改动最小的一招。640x640 改成 416x416特征图面积几乎缩水一半以上NPU 计算量大幅下降整链路 FPS 基本能翻倍。代价是小目标检测能力变弱具体取舍按业务来。第二板斧是并行化。RKNN 的 Python 接口提供了异步推理模式的入口但更简单有效的做法是把图像读取、预处理、NPU 推理、后处理拆成流水线用多线程或者生产者-消费者模型跑。NPU 工作的时候 CPU 同时处理下一帧的预处理FPS 能提升 20% 到 40%。第三板斧是把后处理下沉到 C/C。YOLOv5 的解码和 NMS 本质是大量矩阵运算和循环Python 写起来容易跑起来慢。把这段逻辑用 C/C 实现、编译成 Python 扩展或者干脆整个流程都用 C API 部署性能差距非常明显。很多工业项目最后都是这么干的。不过这条属于进阶优化先把 Python 流程跑通、指标达标再考虑。6.3 最后几句掏心窝的提醒这篇稿子写到最后想凭经验说几句平时文档里不太会提的事。第一版本对应关系是最大的坑没有之一。RKNN-Toolkit2 的版本、板子固件的版本、librknnrt 的版本这三者任何一边对不上都会出现“我代码照着教程敲的怎么就是跑不起来”的局面。出问题先查版本而不是改代码。第二中间产物留好。转换出的yolov5s.rknn文件很小拷贝到板子上就能直接推理不需要在板子上重新转换。经常有人板子一换、环境一装就要重头来其实把.rknn文件备份好能省掉大量在 PC 端转换的步骤。第三在麒麟V10这类国产 Linux 系统上做 AI 部署最大的困难往往不是模型本身而是环境依赖。用 conda 隔离环境、缺什么库就手动装什么库、尽量别动系统自带的 Python这些都是我用时间和头发换来的教训。第四RK3588 的 NPU 部署 YOLOv5 这件事真正跑通以后会发现它没有想象中神秘。PyTorch 到 ONNX 是通用规则ONNX 到 RKNN 是厂商工具链的活后处理是 YOLO 系列的标准操作。一旦理解了这条链路你就能举一反三把 YOLOv8、YOLOX 甚至其他检测模型都塞进这块 NPU 里跑。我能在这篇教程里做的就是帮你把这条路走平一点让你少摔几次。