简介:面向RK3588、RK3399Pro等嵌入式平台的目标检测与跟踪需求,这份C++完整源码将YOLOv5与DeepSORT算法落地到实际开发板,适合具有一定嵌入式Linux和深度学习部署经验的开发者。压缩包共68个文件,除头文件、源文件外,还提供10个RKNN模型、3个动态库和详细操作说明,整体约92MB,代码与模型目录划分清晰。资源针对部署中常见问题做了专门优化:修复边界框漂移、无检测目标时的异常,并处理了代价矩阵出现的NaN;同时加入隔帧检测开关与Re-ID多线程功能,可根据算力需求灵活调整,显著提升推理速度。配套说明文档覆盖环境配置、编译步骤和运行方式,配合演示视频和GIF动图,可帮助开发者快速在RK3588/RK3399Pro上完成算法迁移。目前已有5591人学习/下载,对正在做车辆、行人检测跟踪项目落地的工程师有直接参考价值。 最近在嵌入式板子上跑目标跟踪的需求不少,刚好手上有一套在RK3588和RK3399Pro上部署YOLOv5+DeepSORT的C++完整工程(车辆与行人跟踪),本文把这套方案的落地思路、模型转换细节、C++工程结构和避坑经验完整梳理一遍,给正在做边缘端视觉项目的人一个可直接上手的参考。这套代码不是简单的demo,而是可以真正接入业务的完整链路:模型推理、跟踪匹配、目标生命周期管理、可视化输出都做了,编译脚本和部署文档也齐全,适合有C++基础、熟悉Linux开发的算法工程师或嵌入式开发人员。
1. 项目整体设计与硬件平台特点
1.1 为什么选YOLOv5+DeepSORT这个组合
目标跟踪任务在安防、交通流量统计、无人零售这些场景里非常常见,核心诉求是把视频帧序列中的同一辆车或同一个人持续标记出来,而不是每一帧都重新识别。YOLOv5负责检测当前帧里的目标位置,DeepSORT负责把这些检测结果和已有的轨迹做关联,从而给出稳定的ID。这个组合在工程界的成熟度极高,社区资料多、模型好调、部署方案完善,不是最优的算法,但绝对是落地最顺的套路。
单帧检测任务只需要一个检测器就够了,但凡是涉及"统计某条路上30分钟内过了多少辆车""某个区域里人员徘徊多久",就必须上跟踪。DeepSORT的核心逻辑是通过卡尔曼滤波预测目标在下一帧的位置,然后用匈牙利算法把当前帧的检测框和已有轨迹做最优匹配。它比简单的IOU匹配强在两点:一是加了运动模型,不会因为目标短暂被遮挡就丢失ID;二是用了级联匹配策略,优先处理那些连续被跟踪的可靠轨迹。
1.2 RK3588和RK3399Pro的算力差异与适配思路
这两个芯片都是瑞芯微的旗舰级SoC,但跨度很大。RK3399Pro是老一代产品,板载NPU算力约3.0 TOPS,放在今天只能说够用;RK3588则是新一代平台,NPU算力达到6 TOPS(INT8),CPU也从A72升级到了A76的大核架构,整体性能翻倍。
这个差异直接决定了部署策略不同:
| 项目 | RK3399Pro | RK3588 |
|---|---|---|
| NPU算力 | 3.0 TOPS | 6 TOPS |
| CPU核心 | 双A72+四A53 | 四A76+四A55 |
| 内存 | 通常2-4GB | 最高可达32GB |
| 推理建议输入 | 640x640,FP16或INT8 | 640x640或更高,INT8 |
在RK3399Pro上,我建议YOLOv5模型使用s或n版本,输入尺寸保持在640x640,INT8量化,这样才能压着实时性走;而RK3588可以比较轻松地跑m版本,甚至可以适当提高batch或增加输入分辨率来提升小目标的检测能力。这套源码在工程结构上对两板做了统一的封装,核心推理接口用条件编译区分平台,底层NPU调用统一走RKNN Toolkit生成的runtime API,上层业务代码完全一致。
2. 模型训练、转换与量化实操
2.1 训练自己的车辆行人数据集
这套代码自带了针对车辆和行人类的COCO子集权重,但真要落地到具体场景,比如某个特定停车场或某个路口的视角,我强烈建议用自己的数据微调。训练部分用的是标准的YOLOv5流程,建议用yolov5s.pt作为预训练权重,因为s模型的体积小、推理速度快,在嵌入式端的性价比最高。
数据集格式用YOLO格式最省事:每张图片对应一个同名txt文件,每行是class_id cx cy w h,坐标全部归一化。要是没有现成的标注工具,可以用LabelImg或者Label Studio转成这个格式。训练命令三件套:
python train.py --data your_dataset.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 python val.py --data your_dataset.yaml --weights runs/train/exp/weights/best.pt python export.py --weights runs/train/exp/weights/best.pt --include torchscript onnx关键点在于训练前要仔细检查类别数量和名称是否和部署端的标签文件一致。很多人会在这里翻车,部署时发现检测出来的框对应的标签全乱了,其实是类名顺序没对齐。
2.2 RKNN模型转换:从ONNX到RKNN的完整链路
这一部分是最容易卡住新手的地方。RK3588和RK3399Pro不能直接消化ONNX或者PyTorch的权重,必须经过RKNN Toolkit转成rknn格式。工具链选择上要注意,RK3588需要rknn-toolkit2,而RK3399Pro用的是rknn-toolkit1.x,两套工具链的输出格式不通用。
转换过程最核心的代码是基于Python的,主要逻辑如下:
from rknn.api import RKNN rknn = RKNN() # 1. 配置量化参数,target_platform按芯片选择 rknn.config( mean_values=[[0, 0, 0]], std_values=[[255, 255, 255]], target_platform='rk3588' ) # 2. 加载ONNX模型 ret = rknn.load_onnx(model='yolov5s.onnx') if ret != 0: print('模型加载失败') # 3. 构建RKNN模型,do_quantization开启INT8量化 ret = rknn.build(do_quantization=True, dataset='dataset.txt') if ret != 0: print('模型构建失败') # 4. 导出rknn文件 ret = rknn.export_rknn('yolov5s_rk3588.rknn')注意RK3399Pro上target_platform要改成rk3399pro,而且rknn-toolkit的版本不要乱动,最好用官方文档配好的版本组合。我踩过一次很深坑,用新版本的toolkit去转老芯片的模型,转换时报错一堆莫名其妙的算子不支持。后来老老实实看了瑞芯微官方wiki,把toolkit版本、runtime版本、NPU驱动版本对齐了才顺畅起来。
2.3 量化过程中如何保住精度
INT8量化就是用一张校准集去统计各层的数值范围,然后把FP32的权重和激活值映射到8bit整数。校准集不用太多,100-300张能代表真实场景的图就够了,但要注意:这300张图必须覆盖各种光照、距离、目标规模和遮挡情况。如果只用干净的白天图做校准,到了傍晚或者逆光场景,检测率会明显下降。
代码里做量化时,dataset.txt指定的是校准图片路径列表,每行一张图片,按实际路径修改即可。量化完建议先在PC上用模拟器跑一遍精度评估,对比FP32和INT8的mAP差异,通常掉点在2%以内算正常,超过5%就必须检查校准集或者某个特殊层是否被异常量化。
3. C++工程结构核心模块解析
3.1 源码目录与模块划分
这套C++源码的工程组织非常清晰,大致结构如下:
project/ ├── CMakeLists.txt ├── src/ │ ├── detect/ │ │ ├── rknn_detector.h # 封装RKNN推理 │ │ └── rknn_detector.cpp │ ├── track/ │ │ ├── tracker.h # DeepSORT核心 │ │ ├── tracker.cpp │ │ ├── kalman_filter.h │ │ ├── hungarian.h # 匈牙利匹配 │ │ └── feature_extractor.cpp │ ├── utils/ │ │ ├── postprocess.h # YOLOv5后处理 │ │ └── config_parser.cpp │ ├── main.cpp # 主流程 │ └── camera/ │ ├── stream_reader.cpp # 支持视频文件/RTSP │ └── stream_reader.h └── models/ ├── yolov5s_vehicle_person.rknn └── labels.txtCMakeLists.txt里需要链接的依赖包括:RKNN runtime的librknnmrt.so、OpenCV、(用于图像的前处理解码和目标框绘制)以及Eigen3(卡尔曼滤波的计算)。如果是从SDK包里拉出来的rknpu runtime,要多注意头文件和库的路径是否指向了板卡对应的架构目录。
3.2 RKNN推理接口封装
RKNN的C++调用链路是:读取模型文件、初始化上下文、设置输入、运行推理、获取输出。我在工程里把这些细节全部封装成了RKNNDetector类,外部调用只需要传入一帧图像,就能拿到vector 格式的检测结果。
// 初始化 int rknn_init(const char* model_path); // 推理接口,输入cv::Mat图像,输出检测列表 std::vector<Detection> rknn_detect(const cv::Mat& img, float conf_thres, float iou_thres);实现里有一个很重要的细节:YOLOv5的输出需要经过完整的后处理才能变成目标框。后处理包括去除低置信度框、按类别阈值过滤、NMS去重。这部分我建议用C++手写实现,不要直接用Python转换时那套numpy操作,否则数据搬运和解析效率会差很多。NMS我用的OpenCV的dnn模块里的NMSBoxes,实测比手写快且稳定,自适应于不同尺寸的输入。
3.3 DeepSORT跟踪器的集成细节
在DeepSORT模块里,最核心的类就是Tracker和KalmanFilter。每个跟踪目标对应一个KalmanFilter状态向量,格式为[x, y, a, h, vx, vy, va, vh],其中a是宽高比,h是高度。卡尔曼滤波做的事情本质是:用一个恒定速度模型预测下一时刻目标状态,然后用新的检测来更新修正。
匈牙利算法的输入是一个代价矩阵,矩阵的每个元素表示某个Detection和某个Track之间的"距离"。这里距离融合了两个信号:一是运动距离(马氏距离,通过卡尔曼滤波预测的协方差来衡量检测框和预测框位置的接近程度),二是外观距离(ReID特征向量的余弦相似度)。只有两个距离都小于一定阈值时,才认为该检测属于该轨迹。
刚开场的几帧会有大量新的跟踪目标出现,因此代码里专门为临时轨迹保留了一个unconfirmed队列。轨迹只有在连续命中若干帧后才会转正,避免误检测造成ID飘移;如果连续丢帧达到上限,比如30帧,轨迹会被删除释放。
4. 部署实测与性能调优
4.1 RK3399Pro板卡上的实测数据
先说配置:RK3399Pro板卡,双核A72+四核A53,板载NPU 3.0 TOPS,运行时CPU Governor设为performance模式,2路视频流以1080p分辨率输入。
测试结果:YOLOv5s,INT8量化,640x640输入,单路视频流上的NPU推理耗时约在85ms到110ms之间,结合DeepSORT跟踪的整个链路,平均帧率大概能跑到9-12 FPS。这个结果实时性不够,但对于很多安防巡检场景其实够用了,因为并不是每一帧都需要模型跑,可以设定隔帧推理策略,比如每3帧推理一次,中间帧用上一次检测结果补上,帧率可以拉到20FPS以上。
4.2 RK3588上的性能表现与优化空间
RK3588上换上同样的YOLOv5s INT8模型,NPU推理耗时能压到25ms左右,如果开启多进程或多线程并发处理,2-4路视频流同时跑,单路还能维持25-30 FPS。换YOLOv5m模型,推理耗时大约45ms,仍然有实时性。
如果还想压性能,有几个实测有效的维度:
- 输入分辨率:从640x640降到544x320,精度有轻微损失,推理耗时下降非常明显。
- CPU绑核:把NPU相关线程绑定到A76大核,把图像解码线程绑到A55小核,能有效减少调度噪声。
- RKNN的zero-copy模式:让NPU直接读取摄像头buff里对齐过的内存,避免copy到NPU再copy出来这一大段搬运。
4.3 大核小核调度的坑
RK3588是大小核架构,如果所有线程都默认调度,系统可能会把计算密集型的推理线程丢到A55小核上,导致性能骤降。我建议在初始化时用pthread_setaffinity_np将推理线程绑定到4个A76核心,实测性能提升可达20%-30%。
void bind_to_big_core(pthread_t thread) { cpu_set_t mask; CPU_ZERO(&mask); // RK3588: CPU 4-7 是A76大核 CPU_SET(4, &mask); CPU_SET(5, &mask); CPU_SET(6, &mask); CPU_SET(7, &mask); pthread_setaffinity_np(thread, sizeof(mask), &mask); }同理,OpenCV的图像缩放和颜色转换操作比较吃CPU,但也属于可以并行化的操作,用OpenMP或者自己开线程池分摊就好。
5. 常见问题与排查技巧实录
5.1 部署后检测结果错乱
这是最让人头疼的问题:明明PC上跑ONNX一切正常,部署到板卡后检测框位置不对、类别错乱、甚至全黑。我遇到过的原因主要集中在三类:一是输入图像的尺寸和通道没有按模型要求预处理,比如模型要RGB,OpenCV默认读进来是BGR,忘了色彩空间转换就会导致检测结果完全不对;二是mean_values和std_values设置错误,量化后输出层结果的scale崩了;三是标签文件顺序和模型训练时的类别顺序不一致,检测框位置对但标签名字全错。
排查方法:先在板端跑一张固定的图片,把所有中间数据打印出来,和PC端的输出做对比,一行一行地排查差异点。一旦上了板卡,不要相信任何"应该没问题"的直觉。
5.2 RKNN模型初始化失败
初始化失败十有八九是runtime版本不匹配。NPU驱动版本、rknn-toolkit版本和runtime库版本必须严格匹配,这个在瑞芯微的官方release note里会明确说明。我用的组合是rknn-toolkit2 1.5.0 + runtime 1.5.0 + 板卡固件对应版本,稳定运行。版本一旦不对,初始化直接报错,或者模型莫名跑出NaN。
5.3 多线程下视频流延迟追不上
DeepSORT的关联阶段在目标多的时候会瞬间计算量激增,比如画面里同时出现50辆车,匈牙利算法要处理一个50x50的代价矩阵,耗时明显上升。如果严格按照串行逻辑去跑:解码-推理-跟踪-绘制,帧率会非常不稳。我的处理方式是把这条流水线改成四个线程,通过BlockingQueue连接各环节,推理线程和跟踪线程并发执行,用结果显示最新的帧即可。实测延迟依然可控,但吞吐量提升明显。
6. 扩展方向与最终心得
6.1 从车辆行人跟踪到业务落地的演进
整个工程里,DeepSORT部分负责稳定输出每个目标的ID和轨迹,但业务逻辑比如逆行检测、区域计数、出入口流量统计这些,并没有写死。如果我们想统计一个区域内的在岗人数,只需要订阅Tracker输出的轨迹,对轨迹做区域判断即可。这样设计的好处是:检测器和跟踪器是纯算法组件,不跟业务耦合,将来换场景换模型完全不影响跟踪逻辑。
6.2 多路视频流的并发处理建议
RK3588的NPU支持多个上下文(context)并发执行,我在工程里提供了一个多路复用示例,做法是每一路视频流创建独立的RKNN上下文,通过线程池管理,实测4路1080p视频流同时推理,每路帧率依旧能保持25FPS以上。注意一点,多路并发时NPU的内存占用是线性增长的,如果板卡内存只有8GB,建议单路输入分辨率控制在640x640以下。
6.3 一些实际操作后的体会
最后说一点个人感受。这类部署项目最耗时的往往不是算法本身,而是工具链和开发板的磨合。第一次做RK系列部署的人,建议拿到板子的前两周不要急着跑模型,先把官方给的rknn_benchmark跑通,理解推理耗时怎么评测、内存占用怎么看、模型文件各段的内存分配规律是什么,后面再上手业务工程会顺畅很多。对于RK3588和RK3399Pro这两个平台,代码层面已经通过宏隔离好了,只需在CMake里传入目标平台标识,就能同一套源码编译出两份二进制挂到不同板卡上,后续维护非常方便。
本文还有配套的精品资源,点击获取