YOLOv5+DeepSORT落地方案:RK3588 NPU上的C++工程实战

发布时间:2026/10/27 8:41:35
YOLOv5+DeepSORT落地方案:RK3588 NPU上的C++工程实战

简介:基于YOLOv5与DeepSort算法在瑞芯微RK3588和RK3399Pro开发板上的C++实现,主要面向计算机专业正在完成大作业的学生,以及需要项目实战练习的开发者,适用于车辆、行人等目标的检测与多目标跟踪场景。代码已完成NPU平台适配,并配套详细文档,属于个人经导师指导、评审99分的高分设计项目。资源包共68个文件,包含23个C++头文件、15个C++源文件、10个RKNN模型文件,以及动态库、动图、视频和说明文档,整体压缩后约92MB,目录结构清晰,便于按模块查看与二次开发。已有409人学习或下载。内容涵盖可直接编译运行的完整工程、多种RKNN模型(如YOLOv5检测模型、ReID重识别模型)、CMake构建脚本和运行示例录屏,可帮助读者快速掌握在瑞芯微开发板上部署检测与跟踪算法的流程,同时为毕业设计、课程设计或期末大作业提供可复用参考,并降低环境搭建与排错成本。

1. 一份C++源码把YOLOv5+DeepSORT压进RK3588:先说这是什么

做目标检测与多目标跟踪部署的人,十有八九卡在同一个地方:模型在电脑上跑得飞起,一上开发板就掉帧、报错、内存越界,最后只能对着终端发愣。这份资源解决的就是这个痛点,它不是一份只讲原理的论文式项目,而是把YOLOv5做检测、DeepSORT做轨迹跟踪的完整C++工程,直接面向RK3588和RK3399Pro这两块瑞芯微的NPU开发板落地。源码里包含模型、文档说明和编译脚本,拿到手能按步骤走通,从ONNX转成RKNN,再编译进板子跑起来,前后大概半天时间。如果你是做毕业设计、期末大作业,或者手头有RK3588开发板想跑个像样的视觉Demo,它的价值在于给你一条已经踩过坑的完整路径,而不是让你从零去啃官方文档。

2. 为什么是YOLOv5+DeepSORT组合:从算力匹配到NPU部署路径

2.1 YOLOv5负责“看见”,DeepSORT负责“记住”

YOLOv5在检测任务里的地位不用多说,它把anchor-based检测做得很成熟,s/m/l/x几个尺度覆盖了从嵌入式到服务器的需求。在RK3588上,大家用得最多的是YOLOv5s和YOLOv5m,因为这两个版本的模型体量和精度平衡点最舒服。YOLOv5s的参数量大约7.2M,计算量约16.5 GFLOPs(640x640输入),在RK3588的NPU上跑INT8量化之后,单帧推理能做到30ms左右。

DeepSORT则负责把检测框“串”成轨迹。它核心做了两件事:一是用卡尔曼滤波预测每个目标下一帧的位置,二是用匈牙利算法做检测框和轨迹的级联匹配。它的输入是YOLOv5输出的检测框(xywh格式)和置信度,输出是带稳定ID的轨迹序列。这里有个关键点:DeepSORT本身不做外观特征提取,那个ReID特征网络是独立挂上去的。你这份资源里的C++实现,通常在DeepSORT里用的是一个轻量级的特征提取网络,或者干脆用检测框的宽高比加位置信息做IOU匹配,前者精度高但NPU占用大,后者省算力但容易在目标交叉时ID跳变。

部署姿态上,YOLOv5检测部分是必须吃NPU的,因为卷积计算量大,CPU跑不动。DeepSORT部分则分为特征提取和匹配计算:特征提取如果有ReID网络,建议也放进NPU;卡尔曼滤波和匈牙利匹配纯属矩阵运算和逻辑操作,放CPU即可。RK3588是4核A76+4核A55,RK3399Pro是2核A72+4核A53,这两个平台的CPU算力差距明显,但NPU算力才是决定你跑什么模型档位的关键。

2.2 RK3588与RK3399Pro各自的算力边界

RK3588是瑞芯微的旗舰级SoC,NPU算力标称6TOPS INT8,实际使用中如果只跑单一模型,YOLOv5s量化后能稳在30fps以上。它还有8K视频编解码能力,适合做视频流输入。RK3399Pro则老一代,NPU算力标称只有约2.8TOPS(实际可用约1.2TOPS左右,这个数字官方宣传和实际体验差距较大),跑YOLOv5s INT8量化后大概10-15fps,能跑但别指望流畅。

这决定了你在两个板子上的部署策略完全不一样:

平台NPU算力(标称)CPU配置建议模型目标帧率
RK35886TOPS INT84xA76+4xA55YOLOv5s/m INT825-30fps
RK3399Pro2.8TOPS INT8(实际约1.2)2xA72+4xA53YOLOv5s INT810-15fps

RK3588上可以用YOLOv5m换精度,代价是帧率掉到20fps上下。RK3399Pro老老实实用YOLOv5s,而且输入分辨率建议降到416或320,不要迷信640。另一个坑是内存带宽,RK3399Pro的LPDDR3/LPDDR4带宽有限,NPU搬运数据时CPU容易等着发愣,这在后文的避坑章节会展开。

2.3 RKNN工具链:为什么偏要转一次模型

瑞芯微的NPU不接受PyTorch的.pt格式,也不直接吃ONNX,所有模型必须过一遍RKNN-Toolkit2转成.rknn格式。这个工具链在你的PC上跑,它会解析ONNX模型结构,做算子映射和量化,最后生成一个板子能加载的.rknn文件。整个转换流程里最核心的是量化:把FP32的权重和激活值映射到INT8,用的是逐通道(per-channel)量化,权重直接用KL散度或最大绝对值法校准,激活值需要准备一组校准图片。

为什么特意提校准图片?因为量化后的精度损失很大程度上取决于你选的校准样本。常见做法是从测试集里抽200-500张覆盖不同光照和姿态的图片,放到一个文件夹里,转换脚本会自动读入并统计每层激活值的分布。如果你偷懒只放十张,检测精度可能暴跌,小目标直接消失。

3. 模型转换与C++工程实战:从ONNX到板载运行

3.1 环境准备:交叉编译工具链与RKNN Toolkit

动手之前先把环境理清楚。你需要在PC上准备两套东西:一套是RKNN-Toolkit2,负责把ONNX转成RKNN;另一套是交叉编译工具链,因为板子是ARM架构,而C++工程是在PC上编译出ARM可执行文件再推送到板子。RKNN-Toolkit2有pip安装包,直接按官方文档装就行,注意Python版本建议3.8或3.10,别用太新的版本,依赖容易出幺蛾子。

交叉编译工具链方面,RK3588和RK3399Pro用的都是aarch64架构,推荐用arm-linux-gnueabihf(32位)或aarch64-linux-gnu(64位),但瑞芯微官方更推荐直接用buildroot或yocto SDK自带的交叉编译器,因为它带了板载系统的库匹配版本。如果你用Ubuntu宿主机,装一个gcc-aarch64-linux-gnu也能编译,但胶水库(比如gstreamer、opencv)的版本容易对不上,运行时会报GLIBC版本错误。我的习惯是用板子的出厂sdk里带的交叉编译器,一劳永逸。

3.2 模型转换三步走:ONNX导出、量化校准、生成RKNN

第一步是导出ONNX。假设你已经有一个训练好的YOLOv5s.pt,在yolov5源码目录里执行:

python export.py --weights yolov5s.pt --include onnx --opset 12

这里--opset 12是算子集版本,RKNN-Toolkit2对opset 12的兼容性最稳。opset太高会出现某些新算子不兼容,流片版本报ERR_UNSUPPORTED_OP。

导出后用onnxsim精简一下图结构:

python -m onnxsim yolov5s.onnx yolov5s_sim.onnx

第二步写RKNN转换脚本。这份资源里的convert.py做的事情可以拆解开来看:

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

mean_values和std_values表示输入图像的归一化方式,YOLOv5官方训练用的是RGB、归一化到0-1,所以mean=0、std=255。如果你训练时改了归一化参数,这里必须同步,否则检测精度直接崩掉。dataset.txt每行写一张校准图片的路径:

calib/0001.jpg calib/0002.jpg ... calib/0500.jpg

第三步验证转换结果。rknn.init_runtime()可以在PC上模拟NPU推理,也可以用rknn.eval_perf()查看每层的耗时分布,这一步骤能看出哪些算子没有被NPU加速而是在CPU上兜底跑。

3.3 C++源码结构:主循环与两路推理

这份资源的核心C++代码结构,我拆开来看,它大概是这么组织的:

void MainLoop() { cv::VideoCapture cap; cap.open(VIDEO_PATH); // RKNN 加载 YOLOv5 检测模型 rknn_app_ctx_t det_ctx; rknn_init(&det_ctx, "yolov5s_rk3588.rknn"); // DeepSORT 状态初始化 DeepSORT tracker; while (cap.read(frame)) { // 1. 检测:NPU 推理 + 后处理(NMS) std::vector<Detection> dets = Detect(&det_ctx, frame); // 2. 跟踪:卡尔曼预测 + 级联匹配 std::vector<Track> tracks = tracker.Update(dets, frame); // 3. 绘制并输出 DrawTracks(frame, tracks); cap.write(frame); } }

Detect()里有两个关键函数:一个是rknn_run(),把输入图像送入NPU并取出三个输出特征图(对应YOLOv5的80x80、40x40、20x20三个尺度);另一个是后处理中的Decode(),把输出张量解码成检测框坐标和类别概率,然后做NMS(非极大值抑制)。这两个地方是性能瓶颈所在。

后处理有一件反常但常见的优化:把YOLOv5的detect头里的解码过程从C++代码里拆出来,利用RKNN的score_threshold参数在NPU端先把低置信度过滤掉。这个阈值的设置我在避坑章节会写,它和NMS的阈值配合不当会导致目标框抖动。

3.4 编译与部署:CMakeLists到底放什么

这份资源的CMakeLists大概长这样:

cmake_minimum_required(VERSION 3.10) project(rknn_yolov5_deepsort) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # RKNN API 路径 set(RKNN_API_PATH ${CMAKE_SOURCE_DIR}/rknn-toolkit2/rknpu2) include_directories(${RKNN_API_PATH}/include) link_directories(${RKNN_API_PATH}/lib/${TARGET_ARCH}) # OpenCV 路径(板端预编译版本) set(OpenCV_DIR /usr/lib/aarch64-linux-gnu/cmake/opencv4) find_package(OpenCV REQUIRED) add_executable(yolov5_deepsort_demo src/main.cpp src/yolov5_postprocess.cpp src/deepsort_tracker.cpp ) target_link_libraries(yolov5_deepsort_demo rknn_api ${OpenCV_LIBS} pthread )

TARGET_ARCH必须是你板子的架构名,rk3588对应librknnrt.so在aarch64目录下,rk3399pro的库路径不同。直接链错库会在板子上报cannot open shared object file。

编译时我用交叉编译器的完整路径调用cmake:

cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DTARGET_ARCH=aarch64 .. make -j4

toolchain.cmake里的核心是:

set(CMAKE_C_COMPILER /opt/arm-rockchip830-linux-gnueabihf/bin/arm-rockchip830-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER /opt/arm-rockchip830-linux-gnueabihf/bin/arm-rockchip830-linux-gnueabihf-g++)

编译完把可执行文件和.rknn模型、.so库一起推送到板子:

adb push yolov5_deepsort_demo /userdata/ adb push yolov5s_rk3588.rknn /userdata/ adb push librknnrt.so /usr/lib/ chmod +x /userdata/yolov5_deepsort_demo ./yolov5_deepsort_demo

4. 避坑排查:五个部署现场的典型事故

4.1 现象一:NPU推理时间正常,但整体帧率上不去

你以为瓶颈在NPU,结果NPU单帧只跑30ms,但实际帧率只有10fps。一看CPU占用率发现其中一个核跑到了100%,其他核在睡觉。

原因:RKNN API默认的同步推理模式,rknn_run()在NPU计算期间CPU线程被阻塞等待,等NPU算完才返回,期间CPU只等不干活。如果你在主循环里串行执行“取帧→检测→跟踪→画框→显示”,每一步CPU都有等待时间。

解决:把rknn_run和rknn_outputs_get改为异步模式,用双缓冲队列把采集和推理重叠起来。我在这个项目里的做法是开两个线程:线程A负责VideoCapture读取并推入缓冲区,线程B负责从缓冲区取帧做检测和跟踪。缓冲区用环形数组实现:

// 双缓冲:当前帧和下一帧交替使用 cv::Mat frame_buffer[2]; int buf_index = 0; // 线程A:读取 while (cap.read(frame_buffer[buf_index ^ 1])) { buf_index ^= 1; // 交换缓冲 } // 线程B:推理 Detect(frame_buffer[buf_index]);

这样能把采集的等待时间和NPU的计算时间重叠起来,总帧率提升明显。

4.2 现象二:转换后的RKNN模型检测框全部偏左上角

模型跑起来了,画框位置明显偏移,坐标像是被裁剪过,目标在画面中间,框却框在左上角。

原因:输入图像预处理不一致。YOLOv5训练时它做letterbox处理,把图像等比缩放到640x640,两边补灰边。你如果直接把原始帧不做letterbox就送进NPU,模型看到的就是一张变形拉伸的图,推理自然错乱。还有一种情况是你做了letterbox,但发送给NPU前没有把补边的颜色设置成114(YOLOv5默认的填充值)。

解决:在预处理里严格按YOLOv5原版逻辑做letterbox,并且把填充值写死114。同时记录缩放比例和padding偏移,在后处理解码出框后,要反算出在原图中的真实坐标。伪代码是这样:

float scale = min(640.0 / img.cols, 640.0 / img.rows); int new_w = img.cols * scale; int new_h = img.rows * scale; int pad_w = (640 - new_w) / 2; int pad_h = (640 - new_h) / 2; // 推理得到 x, y, w, h float x_orig = (x - pad_w) / scale; float y_orig = (y - pad_h) / scale;

这个坑几乎每个做RKNN + YOLO的人都会踩,血泪经验,务必在预处理函数里写成死模板。

4.3 现象三:DeepSORT的跟踪ID在目标交叉后交换了

两个行人擦肩而过之后,他们的ID互换了,跟踪轨迹跟着错乱,这影响毕业设计的演示效果。

原因:卡尔曼滤波的状态矩阵初始化不对,或者级联匹配时IOU阈值设得太大,导致匹配到了错误的框。如果DeepSORT用的ReID模型没放进NPU,CPU上跑不动就不跑了,退化成纯IOU匹配最要命。

解决:级联匹配里,IOU的匹配阈值建议设在0.3到0.45之间。太大会让远处的框错误匹配,太小会让目标跟丢。另外把ReID网络加上来,不用太大,一个MobileNetV3 Small或者更轻的EfficientNet-Lite就够,放到NPU的第2个上下文里和YOLOv5交替跑。注意RK3588支持同时加载多个模型,RK3399Pro就要权衡了,建议只在遮挡频繁的场景启用ReID。

4.4 现象四:板子上跑起来内存直接爆掉

运行几秒后进程被杀,或者报std::bad_alloc,某些时候还导致系统卡死。

原因:RKNN初始化时指定的内存rknn_init里want_pre_alloc参数设为了1,它会预先申请一大块连续内存。如果板子的可用内存不够(RK3399Pro常见),初始化失败后系统也不会挂,但在推理阶段申请不到内部缓冲,直接抛异常。更隐蔽的是视频流解码后你每次循环cv::Mat一直在重新分配,没有用reuse。

解决:初始化时把want_pre_alloc调成0,让NPU按需申请。视频帧处理用固定尺寸的Mat容器,用cv::Mat::create避免反复malloc。我在RK3399Pro上还把输入分辨率降到416,内存占用顺手降下来一大截。

4.5 现象五:librknnrt.so版本不一致,运行报GLIBC错误

本地编译好好的,放到板子上就报version GLIBC_2.34 not found,或者librknnrt.so: cannot open shared object file。

原因:PC的交叉编译器用的glibc版本比板子的高,或者你板子系统里装的librknnrt.so和编译时链接的库版本对不上。

解决:不要从网上下一个librknnrt.so就扔上去,用你板子出厂镜像里自带的版本。编译时也只链接那个板子对应的库。检查一下最简单的方法:

strings /usr/lib/librknnrt.so | grep GLIBC_

看它依赖的最高版本,如果超过板子系统的glibc版本,说明库文件给错了。另外RK3588和RK3399Pro的NPU驱动版本不一样,它们的librknnrt.so不能互换,这是众多翻车事故的来源之一。

5. 进阶实战:多线程流水线、性能验证与参数调优

5.1 输出维度与性能验证方法

部署任务验收时,我一般会做一个“四件套”验证:帧率、单帧检测耗时、跟踪ID切换率、内存峰值。前两个用rknn.eval_perf()和time()封装的统计函数就能测;ID切换率需要一段尽量长的视频,人工数目标物的ID变化次数,或者通过轨迹点是否越界来判断;内存峰值用/usr/bin/time -v来观测。

具体的验证脚本大概这样:

/usr/bin/time -v ./yolov5_deepsort_demo --video test.mp4 --model yolov5s_rk3588.rknn

里面time -v会输出Maximum resident set size,这个数就是内存峰值。帧率这边我习惯在程序里统计300帧,用实际耗时除以帧数,而不是看OpenCV的API报告,因为后者在异步缓冲下容易虚高。我的参照标准:RK3588跑YOLOv5s INT8 + DeepSORT(带轻量ReID),实测单帧检测35ms,整个跟踪流程45ms,综合帧率22fps左右,属于正常水平。

5.2 多线程流水线:把CPU和NPU都榨干

前文提过双缓冲,更进一步的做法是三段流水线:采集、检测、跟踪渲染各占一个线程,线程间用带互斥锁的队列衔接。这里有个需要注意的点:DeepSORT的卡尔曼滤波更新一次只依赖上一帧的状态,天然支持生产者消费者模型。我在这个工程里把检测结果和对应时间戳打包成struct FrameResult,丢给跟踪线程消费,跟踪线程就不再阻塞采集和检测。

struct FrameResult { cv::Mat frame; std::vector<Detection> dets; int64_t timestamp; }; std::queue<FrameResult> det_queue; std::mutex mtx;

采集线程往队列塞帧,跟踪线程从队列取结果更新轨迹。队列长度限制在3帧,防止处理不及时导致内存增长或延迟上升。当队列满时,丢弃最老的那帧,保证实时性。

还有一个技巧:YOLOv5的NMS后处理其实很靠CPU,在RK3588上这部分可能占掉整个后处理时间的60%。换一种做法是把三个尺度的输出合并后,用单次排序加稀疏化的方式代替对每个类别分别NMS。具体操作是用置信度阈值过滤后,按score降序排一次,然后逐框和已保留的框做IOU判断,每个框只比较一次。这个方法在目标数量少于100个时,比OpenCV自带的NMS快很多。

5.3 最后调参:置信度阈值与NMS阈值

这份资源最终的运行效果,很大程度上取决于两组阈值:检测置信度和NMS IOU阈值。按照YOLOv5官方推荐的默认值,置信度在0.25,NMS的IOU在0.45。但在RKNN量化之后,我建议把置信度稍微调到0.3,因为INT8量化会让一些原本低于阈值的小目标分数更不稳定,调高一点点能过滤掉一半多的误检。NMS阈值在0.5左右也可以接受,但目标密集时建议降到0.4。这套参数用在DeepSORT的输入里,会影响跟踪效果,我一般用三个场景测试来定:密集人群、稀疏道路、单目标快速移动。

这组调参经验是从数次板端调试里总结的:先用默认参数跑一遍离线视频,记录漏检和误检的帧号,再回放,把误检帧的阈值往上拉,把漏检帧的阈值往下放,来回两三次就能定下来。

从那以后我每次部署到新板子,都强制走一遍流程:先跑通转换,再搞定预处理,然后才碰性能调优。像这次做RK3588版本,我先把YOLOv5的letterbox补边参数检查了三遍,才敢动DeepSORT的级联匹配。原因很简单,NPU部署的坑大多在数据进模型之前,而不是模型本身。这套流程走顺了,你拿到手后先跑通默认配置,再按自己的GPU/CPU去调参数,希望帮到你。

本文还有配套的精品资源,点击获取