尧图精选

rv1126部署yolov6实时目标检测:RKNN模型转换与板端推理实践

🕒 发布时间:2026/9/16 13:16:57 📁 来源:尧图网络
简介面向嵌入式视觉开发者的RV1126平台YOLOv6实时目标检测源码包适用于安防IPC、智能摄像头等需要低算力边缘检测的场景可在瑞芯微环境下开箱即用。代码工程按src、bin、include、lib分区组织src目录实现入口主函数、模型加载与推理、后处理、RKMedia媒体管道以及模型读取等模块逻辑分层清晰bin目录中提供已编译的可执行文件和RKNN模型可减少手动转换步骤include存放接口头文件lib提供依赖库便于二次开发与交叉编译。压缩包共109个文件以C/C源码、CMake构建脚本、头文件、JSON配置、二进制可执行文件等类型为主整体大小约3.87MB。目前已有124人学习下载。开发者拿到后可直接参考工程结构通过修改model.h指定模型路径、在rkmedia.h中调整采集分辨率快速替换或调试自己的模型并结合目录中的构建脚本完成部署能有效减少RKNN适配与环境搭建的时间成本帮助快速完成视频流目标检测应用落地。1. rv1126配yolov6实时目标检测的“开箱即用”卡在哪RV1126这颗SoC经常出现在IPC和边缘盒子的方案选型里四核Cortex-A7配上一块2T算力的NPU跑轻量目标检测模型绰绰有余。但“板子上能跑yolov6”和“基于rv1126的yolov6实时目标检测源码开箱即用”之间隔着一整条工程链路PyTorch权重导出ONNX、转换成RKNN格式并量化校准、板端用NPU接口做推理、yolov6输出解码与NMS后处理最后把检测框叠加进RTSP视频流。每一步都有参数陷阱合在一起就变成了新人一两周踩不完的坑。下面按“转换—推理—管线—落地”的顺序拆开命令和代码都按可直接改动使用的方式给出适合手里有一块RV1126核心板、想在两周内交付实时检测demo的开发者。2. 从yolov6的pt权重到.rknn环境、ONNX导出与INT8量化2.1 先把工具链版本对齐rknn-toolkit2是PC端负责把ONNX转成RKNN的转换工具板端运行时则是librknnrt.so。两者版本不匹配时最典型的症状是模型在板子上加载失败报错只是rknn_init的负返回码几乎看不出原因。所以第一步不是跑转换脚本而是确认板端runtime版本再回到PC端安装对应的工具包。conda create -n rknn python3.8 -y conda activate rknn pip install rknn-toolkit22.6.0 python -c from rknn.api import RKNN; print(rknn toolkit ok)这段命令做了三件事创建独立的Python环境、安装rknn工具箱、验证导入是否成功。用conda而不是把包直接塞进系统环境是想避开numpy、onnx、tensorflow等依赖的互相污染如果装的numpy版本过新rknn-toolkit2会在from rknn.api import RKNN这一步直接报ABI错误排查成本远高于建环境的成本。版本选择上RV1126属于偏早期的RKNN平台PC端工具版本不是越新越好新版本如果移除了对rv1126的target_platform支持反而无法完成转换。验证通过后在转换脚本里把target_platform写成rv1126。这个字段会决定工具链按哪套指令集做算子匹配和内存布局规划字符串必须小写写成RV1126会在build阶段直接失败。2.2 yolov6导出onnx时的输出头选择yolov6官方仓库自带导出脚本常见用法是cd yolov6 python tools/export.py --weights weights/yolov6s.pt --include onnx --simplify不同版本仓库里可能是export_onnx.py参数名略有差异以自己clone下来的分支为准。导出动作本身不难难在选择保留哪种输出头。裸输出指的是网络只给出三个尺度的特征图每个尺度包含框坐标、目标分数和类别分数具体解码放到板端C代码里完成end2end输出则把解码和NMS都放进onnx图里推理端少写代码但NMS这类带循环和排序的算子很难完整映射到NPU上通常会被rknn工具链切成CPU执行推理时间翻倍。我一般选裸输出让NPU只计算卷积、激活、归一化这条主干路径后处理放到四核A7上并行RV1126处理640分辨率的yolov6后处理压力并不大。导出之后顺手确认一下图结构用一段短脚本打印输入输出张量形状import onnx m onnx.load(yolov6s.onnx) for i in m.graph.input: print(in :, [d.dim_value for d in i.type.tensor_type.shape.dim]) for o in m.graph.output: print(out:, [d.dim_value for d in o.type.tensor_type.shape.dim])正常情况会看到1个输入、2到3个输出取决于yolov6发布版本输入形状类似[1, 3, 640, 640]。只要输出形状不是类似[1, 8400, 85]这种已经带解码结果的风格后续处理逻辑就清晰。这个形状信息会成为第3章后处理代码的调试依据建议保存下来。2.3 转换脚本和量化数据集接下来是转换本身代码如下from rknn.api import RKNN rknn RKNN() rknn.config( mean_values[[0, 0, 0]], std_values[[1, 1, 1]], target_platformrv1126, quantized_dtypeasymmetric_quantized-8, ) ret rknn.load_onnx(modelyolov6s.onnx) assert ret 0, load onnx failed: %d % ret ret rknn.build(do_quantizationTrue, datasetdataset.txt) assert ret 0, build failed: %d % ret ret rknn.export_rknn(yolov6s.rknn) assert ret 0, export failed: %d % ret rknn.release()脚本逻辑是配置、加载、构建、导出四步。配置里的mean_values和std_values保持[0,0,0]和[1,1,1]表示后续输入的是0到255的原始像素归一化由NPU侧处理。这个配置必须和板端代码的输入保持一致如果板端先手动转成float并做了归一化这里就要写成对应的mean和std。quantized_dtype默认是asymmetric_quantized-8RV1126的NPU对int8支持最好误差在线性不对称量化下也最稳定。dataset.txt是量化校准图片清单每行一个绝对路径建议从真实场景采集数量80到120张之间。太少会导致量化后的检测精度明显下降太多则增加build耗时但对精度提升有限。参数常用取值作用target_platformrv1126按rv1126的算子库做图优化mean_values[[0,0,0]]板端输入前减去的均值std_values[[1,1,1]]板端输入前除的标准差quantized_dtypeasymmetric_quantized-8int8非对称量化NPU提速关键do_quantizationTrueFalse用于先验证float精度是否正常2.4 转换报错先看这几类load onnx failed多半是opset版本或导出时带了训练辅助节点可以试着降低opset再导出。build failed要先看日志里报出的算子名如果出现Transpose、Gather这类重排算子优先检查导出脚本有没有开simplifyonnx-simplifier能折叠大量连续Transpose。量化后精度崩先检查dataset.txt图片是否和检测场景差距太大其次检查mean/std是否和训练预处理一致最后再考虑是不是某个层对量化敏感。转换这一步的产出是yolov6s.rknn这个文件连同第3章的C代码一起构成可分发的最小工程。3. rv1126板端推理代码rknn上下文、yolov6后处理与RGA对齐3.1 完成rknn初始化并核对输入输出属性板端C工程以librknnrt为基础初始化上下文#include rknn_api.h rknn_context ctx; int ret rknn_init(ctx, /userdata/model/yolov6s.rknn, 0, NULL); if (ret 0) { fprintf(stderr, rknn_init fail: %d\n, ret); return -1; } rknn_input_output_num io_num; ret rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(io_num)); if (ret 0) { fprintf(stderr, query num fail: %d\n, ret); return -1; } printf(in%d, out%d\n, io_num.n_input, io_num.n_output);rknn_init负责把模型加载进NPU驱动并分配对应内存资源第二个参数是模型文件的绝对路径必须在run之前确认文件存在。flag传0表示默认模式后续做零拷贝时要改成对应标志位。rknn_query返回的io_num用于确认模型输入输出个数yolov6裸输出通常是3个。之后用RKNN_QUERY_INPUT_ATTR拿到每个输入张量的dims、fmt和typerknn_tensor_attr in_attr; memset(in_attr, 0, sizeof(in_attr)); in_attr.index 0; rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, in_attr, sizeof(in_attr)); printf(w%d h%d fmt%d type%d\n, in_attr.dims[2], in_attr.dims[1], in_attr.fmt, in_attr.type);这里dims顺序和实际布局有关fmt如果是RKNN_TENSOR_NHWC就给NPU一份h*w*3的RGB连续内存type是UINT8时输入直接承接摄像头过来的图像缓冲不需要转float。这段查询在调板阶段非常重要打印出来的宽高决定后面的VPSS和RGA按什么分辨率配置。3.2 推理一次的标准写法一张640×640的RGB图到手后通过rknn_inputs_set送入NPUrknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf rgb_img; inputs[0].size 640 * 640 * 3; inputs[0].pass_through 0; ret rknn_inputs_set(ctx, 1, inputs); if (ret 0) { fprintf(stderr, inputs_set fail: %d\n, ret); return -1; } rknn_output outputs[3]; memset(outputs, 0, sizeof(outputs)); for (int i 0; i 3; i) { outputs[i].index i; outputs[i].want_float 1; } ret rknn_outputs_get(ctx, 3, outputs, NULL);pass_through0的意思是让runtime对输入应用第2章配置的mean/std归一化传入的原始RGB按uint8方式读取如果设为1输入数据必须已经是浮点归一化结果否则精度必然出错。want_float1表示让runtime把NPU输出的int8数据反量化为float虽然多一次内存搬运但对yolov6这种需要浮点置信度计算的后处理而言是性价比最高的选择。outputs里的buf和size就是后续解码的直接输入。这段初始化逻辑在程序启动时执行一次即可不要在检测循环里反复rknn_init每次init都会重新加载模型并申请NPU内存循环调用会把启动耗时拖进每帧延迟里。3.3 yolov6输出解码与NMS拿到三组float输出后按尺度逐一解码。下面是一个最小可运行的解码函数骨架void decode_one(float* feat, int stride, int h, int w, int n_cls, float thresh, Box* boxes, int* nb) { int step n_cls 5; for (int i 0; i h; i) { for (int j 0; j w; j) { float* p feat (i * w j) * step; float cls p[4]; // 该anchor的类别置信度汇总 if (cls thresh) continue; int best 0; float score 0; for (int c 0; c n_cls; c) { if (p[5 c] score) { score p[5 c]; best c; } } float conf cls * score; if (conf thresh) continue; Box* b boxes[*nb]; b-x1 (p[0] j) * stride - p[2] * stride * 0.5f; b-y1 (p[1] i) * stride - p[3] * stride * 0.5f; b-x2 (p[0] j) * stride p[2] * stride * 0.5f; b-y2 (p[1] i) * stride p[3] * stride * 0.5f; b-score conf; b-cls best; (*nb); } } }这个函数把anchor偏移换算成原图坐标P3/P4/P5三个尺度分别对应stride 8/16/32。不同yolov6导出版本在通道排列上可能有差异有的把obj分数放在第0个通道有的放在中间所以代码里的p[4]要对照2.2节打印的输出形状调整。解码完成后做贪心NMSvoid nms(Box* boxes, int count, float iou_thr) { for (int i 0; i count; i) { if (boxes[i].score 0) continue; for (int j i 1; j count; j) { if (boxes[j].score 0) continue; if (boxes[i].cls ! boxes[j].cls) continue; if (iou(boxes[i], boxes[j]) iou_thr) boxes[j].score -1.0f; } } }iou函数就是两个矩形交并比计算矩形坐标已经在原图尺寸上直接计算即可。源码包里通常会先按score降序排一次序再只和排在前面的框比较上面这个版本是保证正确性的最小实现。NMS的阈值一般取0.45到0.5之间类别之间的框不会互相抑制所以计算公共的iou前要先判断cls是否相同否则行人框会把车框压掉。3.4 输入图像从哪里来RGA尺寸与格式对齐摄像头采集的一帧通常是YUV420SP的1920×1080NPU要吃640×640的RGB888。最直接的做法是让VPSS通道直接把目标分辨率设成640×640硬件缩放一步到位这是最省CPU的路径。如果VPSS那边因为编码需求必须保持1080p输出就得在检测前用RGA处理。RGA在RV1126上负责缩放和格式转换核心步骤是配置源图宽高和格式、目标宽高和格式然后同步调用一次把输出buffer直接交给rknn作为输入整条路径不经过CPU拷贝。RGA的API在不同SDK版本里差异比较大有的用rga_info_t有的用im2d系列接口但配置思路一致源格式、目标格式、宽高、内存地址四个字段填对就不会出问题。4. 实时目标检测的视频管线VI到VENC的帧流转4.1 用MPP搭起VI到VPSS的采集骨架实时目标检测必须接上摄像头而不是反复测试单张图片。RV1126的SDK里通常带sample_vio例程直接从例程改比从零写接口可靠。骨架如下int ret RK_MPI_SYS_Init(); VI_DEV_ATTR_S vi_attr; memset(vi_attr, 0, sizeof(vi_attr)); vi_attr.enViMode VI_MODE_0_1280X720; ret RK_MPI_VI_SetDevAttr(0, vi_attr); ret RK_MPI_VI_EnableChn(0, 0); VPSS_GRP_ATTR_S grp; memset(grp, 0, sizeof(grp)); grp.u32MaxW 1280; grp.u32MaxH 720; grp.enPixelFormat RK_FMT_YUV420SP; ret RK_MPI_VPSS_CreateGrp(0, grp); ret RK_MPI_VPSS_EnableChn(0, 0);这套调用先把系统媒体层初始化然后把sensor绑定到VI设备0再创建一个VPSS分组对VI传入的帧做缩放和格式转换。enViMode里的1280×720要和sensor实际输出一致分辨率不匹配会看到颜色错乱或根本没有出帧。VPSS通道可以配两个输出一路输出720p给编码推流另一路输出640×640给检测编码和解码互不干扰。4.2 检测线程与编码线程的双缓冲架构检测和推流不能在同一个线程里顺序执行否则NPU推理耗时直接决定视频帧率。常见做法是三线程加两个队列void* vi_thread(void* arg) { while (g_running) { VIDEO_FRAME_INFO_S* frame malloc(sizeof(*frame)); if (RK_MPI_VPSS_GetChnFrame(0, 0, frame, 1000) 0) { queue_push(det_q, frame); } } } void* infer_thread(void* arg) { while (g_running) { VIDEO_FRAME_INFO_S* f queue_pop(det_q, 1000); if (!f || !g_det_engine) continue; void* rgb rga_convert(f, 640, 640); rknn_inputs_set(g_det_engine, 1, inputs); rknn_outputs_get(g_det_engine, 3, outputs); post_process_and_draw(rgb, f); queue_push(venc_q, f); } }线程之间通过有界队列解耦vi_thread只负责取帧infer_thread负责NPU推理和画框venc_thread负责编码推流。队列长度一般设3到5太短会在一路帧率波动时丢帧太长会引入画面延迟。VPSS取到的帧用完必须调用ReleaseChnFrame归还缓冲否则两三个小时后画面会突然卡死这是MPP调试里最常见的资源泄漏。4.3 性能基线分辨率、量化与NPU耗时的关系一个能交付的源码包通常会附带一张实测性能基线常见数据如下模型输入分辨率量化NPU单帧耗时yolov6n320×320INT8约20msyolov6n416×416INT8约30msyolov6s640×640INT8约40到50msyolov6s640×640FP16约80到100ms这张表的价值在于方向判断如果实时性差先降分辨率而不是换模型从yolov6s换成yolov6n或者把输入从640缩到416FPS提升通常不止一倍。want_float1的开销也不可忽略把output设为is_prealloc1并在初始化时申请复用buffer能省去每次分配的抖动。NPU和CPU的并行性可以利用起来让检测线程在拿到上一帧结果的同时把这一帧的rknn_inputs_set提前提交后处理执行时NPU已经在算下一帧。4.4 把检测框叠到视频流上检测到目标之后坐标是相对输入尺寸的画框前要按VPSS输出分辨率做比例换算。嵌入式上画框不走OpenCV那条慢路径而是用MPP的RGN模块做OSD叠加硬件把带透明度的位图合成到视频帧上VENC编码器直接看到带框的画面。常见做法是准备一张与视频帧等大的alpha图检测线程把框和类别写到对应区域周期性把OSD提交给RGN硬件。RTSP推流通常用SDK自带的rtsp server或轻量方案做H264/H265裸流封装VENC编码后的码流不需要回到CPU做任何再处理。程序启动后对外表现就是网络摄像头地址可拉流画面上有稳定跟手的检测框。5. 开箱即用的源码包目录结构、启动顺序与调优技巧5.1 目录结构就是最小交付物一个能做到开箱即用的工程目录结构要让接手的人十分钟内知道哪里改模型、哪里改参数、哪里交叉编译路径对应内容model/yolov6s.rknn、转换脚本和dataset.txtapp/板端C工程含CMakeLists和源码build.sh一键交叉编译脚本start.sh板端启动脚本完成模型路径检查和进程拉起5.2 启动脚本与验收命令把模型拷到板子的/userdata目录进入应用目录后执行start.sh脚本内容通常是#!/bin/sh cd /userdata/yolov6_rv1126 export LD_LIBRARY_PATH/usr/lib:/oem/usr/lib:$LD_LIBRARY_PATH ./app/bin/yolov6_demo -c app/opt.conf启动后看两个东西。第一是终端是否按时打印FPS和每帧NPU耗时第二是拉流验证在PC上执行ffplay rtsp://192.168.1.100/live能看到画面且检测框位置正确整条链路就通了。如果画面黑屏先检查VI和VPSS的分辨率对齐如果检测框位置漂移检查画框时使用的缩放映射关系如果能拉流但画面没有框最可能是OSD图层没有开启或叠加位置没有同步更新。5.3 把后处理参数改成运行时可配置最后说一个对源码包使用者最友好的改动把conf_thresh、iou_thresh、输入分辨率放进一个opt.conf文件main函数启动时用fgets逐行解析并赋值给全局变量。这样一来现场调参从“改代码重新编译”变成“改一行文本重启进程”效率差别很大。对拿到源码包的开发者来说“开箱即用”并不是打开就能跑而是改最少的地方就能用自己的模型和数据跑起来。做到这一点的关键全在三个文件转换脚本里的dataset、主程序里的队列深度、opt.conf里的阈值参数。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →