简介面向NVIDIA Jetson平台的目标跟踪部署源码包整合YOLOv5与DeepSORT算法借助TensorRT推理优化以C实现高性能实时多目标跟踪适合作毕业设计、监控系统、自动驾驶及人群分析等场景的技术参考要求读者具备一定的深度学习部署基础。压缩包共46个文件容量24.56MB源码以头文件、C与CUDA源文件为主并配有Python转换脚本、TensorRT部署教程、设计报告及训练评估图表能覆盖从模型转换到工程落地的完整链路。已有220人学习下载。内容包含YOLOv5检测与DeepSORT跟踪的完整C实现、TensorRT引擎构建与推理脚本、COCO类别映射文件、mAP与检测结果可视化图等既可作为Jetson平台算法移植的参考基线也能帮助理解PyTorch模型量化、TensorRT加速和嵌入式部署的关键步骤。资源内目录结构清晰便于按模块二次开发与调试验证。1. 把 YOLOv5 检测和 DeepSort 跟踪串起来TensorRT 在 Jetson 上的那份现成源码在 Jetson Nano、Xavier NX 或者 AGX Orin 上跑目标跟踪真正的拦路虎往往不是算法理解而是 TensorRT 版本匹配、engine 文件序列化、C 内存回收这些工程琐事。这份源码把 YOLOv5 检测、DeepSort 跟踪和 TensorRT 加速串成了完整的 C 链路检测完直接送进 DeepSort 维持跟踪 ID推理延迟可以压到单帧几十毫秒。适合两种人准备做目标跟踪方向毕业设计的学生以及要在边缘设备上验证跟踪方案的产品工程师。下面我会按架构原理、环境编译、模型转换、部署排错和调优验证的顺序把它拆完源码包里的目录结构和脚本我会按实际操作路径来讲。2. 先拆架构YOLOv5、DeepSort 和 TensorRT 在 Jetson 上怎么分工2.1 检测器管看到什么YOLOv5 的 C 推理链路YOLOv5 在整条链路里承担的是每一帧里有哪些目标、框在哪这个任务。以 Jetson 上最常用的 YOLOv5s 为例输入是 640×640 的三通道图输出是三个检测头的特征图分别对应原图的 1/8、1/16、1/32 下采样。以 640×640 输入计算三个头各产出 80×80、40×40、20×20 的特征图每个位置带 3 个 anchor总共 25200 个候选框每个候选框是一个 85 维向量cx、cy、w、h 四个框坐标一个目标置信度80 个 COCO 类别得分。C 侧拿到 TensorRT 推理结果之后第一步不是直接画框而是做 decode。因为 YOLOv5 的输出是经过 sigmoid 和 stride 归一化的需要反算回 640×640 坐标空间。我一般会把 decode 单独拆成一个函数方便换成 YOLOv7 或者 YOLOv8 时只动这一块。下面这段是核心的候选框解析逻辑源码里大概率有类似的实现// 从模型输出解析候选框注意数据排布是 [batch][3][grid][grid][85] void decodeYoloOutput(const float* data, int gridSize, int stride, const float* anchorW, const float* anchorH, float confThresh, std::vectorDetection dets) { for (int a 0; a 3; a) { // 每个尺度固定 3 个 anchor for (int i 0; i gridSize; i) { for (int j 0; j gridSize; j) { int idx a * gridSize * gridSize * 85 i * gridSize * 85 j * 85; float obj sigmoid(data[idx 4]); if (obj confThresh) continue; // 先按置信度粗筛 // YOLOv5 的框中心是 2*sigmoid()-0.5再映射到 stride 网格 float cx (sigmoid(data[idx 0]) * 2.0f - 0.5f j) * stride; float cy (sigmoid(data[idx 1]) * 2.0f - 0.5f i) * stride; float w pow(sigmoid(data[idx 2]) * 2.0f, 2.0f) * anchorW[a]; float h pow(sigmoid(data[idx 3]) * 2.0f, 2.0f) * anchorH[a]; float score obj; int cls -1; for (int c 0; c 80; c) { // 找最高类别得分 float clsScore sigmoid(data[idx 5 c]); if (clsScore score) { score clsScore; cls c; } } if (cls 0 score confThresh) { dets.push_back({cx, cy, w, h, score, cls}); } } } } }这段代码有几点要注意。stride 参数对应的是当前特征图的缩放倍数80×80 的特征图 stride 是 840×40 对应 1620×20 对应 32。anchorW 和 anchorH 从模型的 YAML 配置里读取YOLOv5s 在 COCO 上的 anchor 是固定的三组换数据集必须重新聚类。decode 完还要做一次 NMS按类别分别抑制重叠框IOU 阈值一般取 0.45这个值在行人密集场景下建议降到 0.3否则两个紧贴的人会只保留一个框直接影响后面 DeepSort 的跟踪质量。后处理是全链路里最容易出现 CPU 瓶颈的地方源码里如果对 25200 个框做全量排序会很慢合理的做法是先按 confThresh 筛掉大部分框再做 NMS。2.2 跟踪器管是谁DeepSort 的卡尔曼预测与级联匹配DeepSort 解决的是上一帧的那个人这一帧还是不是同一个人。它内部是两个模块的配合卡尔曼滤波负责预测目标在下一帧的位置匈牙利匹配负责把当前检测框和已有轨迹一一对应。卡尔曼滤波的状态向量是 8 维的cx、cy、宽高比、高度以及对应的四个速度分量。它假设目标是匀速运动在 Jetson 上这个假设对行人和车辆都够用但目标突然急转弯或者被遮挡后再出现预测框和真实框往往对不上这时候就要靠外观特征救场。DeepSort 里每个轨迹都会维护一个 ReID 特征向量通常是 128 维或者 512 维浮点数组用余弦距离计算当前检测框和目标轨迹的相似度再结合马氏距离做级联匹配。C 实现里轨迹不是简单一个结构体就能搞定的。源码里大概率会有一个类似下面这样的 Track 定义struct Track { int trackId; // 跟踪 ID分配后不再改变 cv::Rect2f bbox; // 当前帧的预测框 cv::Mat feature; // ReID 外观特征1x128 或 1x512 int hits; // 连续命中帧数达到阈值才确认轨迹 int timeSinceUpdate; // 离上次成功匹配隔了多少帧 float confidence; // 轨迹置信度 Eigen::VectorXf kfState; // 卡尔曼 8 维状态 Eigen::MatrixXf kfCov; // 8x8 协方差矩阵 };trackId 是这个结构体里最重要的字段它一旦分配就不能变否则跟踪就失去了意义。hits 和 timeSinceUpdate 控制轨迹的出生和死亡DeepSort 默认 max_age 是 70 帧意味着一条轨迹如果连续 70 帧没有被任何检测框匹配上就会被删除min_hits 默认是 3新检测框要连续命中 3 帧才会被确认成正式轨迹防止单帧误检产生幽灵轨迹。在 Jetson 上跑的时候这两个参数是需要调的画面里目标频繁进出遮挡区域max_age 可以加到 100如果是快速移动的车辆min_hits 保持 3 就好设太高会导致短轨迹来不及确认就断了。2.3 TensorRT 层融合和精度选择为什么边缘端必须用它同样一份 YOLOv5s 权重在 Jetson Nano 上用 PyTorch 直接推理单帧耗时在 200 毫秒以上基本是幻灯片换成 TensorRT FP16 精度之后可以压到 20 到 35 毫秒。这个差距来自三个方面。第一是层融合。TensorRT 会把 Conv、Bias、ReLU 这种连续的算子融合成一个 kernel把 BN 层的参数折叠进卷积权重里减少 kernel 启动和显存读写的次数。第二是 kernel 自动调优。TensorRT 在构建 engine 时会针对当前 GPU 架构跑一批 benchmark选出最优的卷积算法Jetson 的 GPU 和桌面显卡架构不同TensorRT 的这一层优化是普通 PyTorch 推理引擎做不到的。第三是显存复用。engine 内部的中间 tensor 会复用同一块显存峰值显存占用明显下降对 Jetson Nano 这种 2GB 到 4GB 共享内存的设备来说很关键。精度选择上我按实际部署习惯整理了下面的对照以 YOLOv5s 在 Jetson Nano 上的表现作为参照精度engine 大小约单帧推理延迟约精度损失适用场景FP3295 MB35 ms基准调试阶段确认链路正确FP1647 MB20 ms可忽略默认选择跟踪场景首选INT824 MB12 msmAP 下降 1%2%帧率吃紧时的进阶选项FP16 是绝大多数 Jetson 项目的甜蜜点精度损失肉眼基本看不出来部署时优先确认源码里有没有开启 FP16 的宏开关。INT8 需要额外做校准源码包如果带了校准脚本可以在确认 FP16 链路稳定之后再尝试。TensorRT 10.x 以后 workspace 参数改成了 memPoolSize老命令不能照抄这一点在后面的模型转换部分会详细说。3. 源码落地环境搭建和编译步骤3.1 版本检查刷完 JetPack 先确认四件套Jetson 平台和其他 Linux 设备最大的区别是CUDA、cuDNN、TensorRT 是跟着 JetPack 整体发布的单独升级其中某一个模块很容易把系统搞崩。所以拿到源码后第一步不是急着编译而是先确认板子上的环境是否在源码预期范围内。JetPack 4.6 对应的是 CUDA 10.2 和 TensorRT 8.2.xJetPack 5.x 对应 CUDA 11.4 及更高版本、TensorRT 8.5 或 8.6到 JetPack 6 已经上了 TensorRT 10.x。源码如果是在 TensorRT 8 时代写的直接拿到 TensorRT 10 的板子上编译大概率会有 API 报错。先跑下面几条命令确认环境# 确认 TensorRT 版本很多 API 依赖 8.x 以上 dpkg -l | grep nvinfer # 确认 CUDA 版本 nvcc --version # 查看内存和 swapJetson Nano 是共享内存2GB/4GB 差异很大 free -hdpkg 输出的 nvinfer 版本号比如 8.5.2就代表当前 TensorRT 的版本。如果源码的 CMakeLists 里写了find_package(TensorRT)CMake 大概率是从/usr/local/aarch64-linux-gnu这个路径下找头文件的这个路径是 JetPack 安装 TensorRT 的默认位置和 PC 上从官网下的 tar 包路径不一样。版本不匹配的情况不用慌优先考虑升级 JetPack 而不是单独重装 TensorRT这也是我在这个平台上吃过亏之后形成的习惯。3.2 编译源码CMake 参数和依赖这份源码的依赖是标准三件套OpenCV 4.5 以上、Eigen 3.x、TensorRT 8.x 之后。OpenCV 负责读视频、letterbox 预处理和画框Eigen 负责卡尔曼滤波的矩阵运算。源码目录里一般会有src、include、models、weights四个核心目录weights下面放的是 pt 文件和转换好的 engine 文件models下面放的是检测器和 ReID 特征网络的配置。编译时我用的是下面的参数组合mkdir -p build cd build cmake .. \ -DCMAKE_BUILD_TYPERelease \ -DOPENCV_DIR/usr/local/opencv4 \ -DENABLE_FP16ON \ -DUSE_INT8OFF make -j4ENABLE_FP16直接决定了 engine 构建时是否启用半精度建议第一次编译就打开因为后续全部基于 FP16 调优。USE_INT8先关掉INT8 需要额外的校准数据集不是开个开关就能跑的贸然打开会直接构建失败。make -j4在 Jetson Nano 上大概要编译 5 到 10 分钟如果编译过程中 CPU 温度过高导致卡死改成make -j2会慢一些但更稳定。3.3 主循环读视频、推理、跟踪、画框源码的主流程比纯检测工程多了一层跟踪状态管理。一个标准的主循环骨架是这样的int main(int argc, char** argv) { // 1. 读取或现场构建检测器 engine std::unique_ptrYoloDetector detector std::make_uniqueYoloDetector(config.enginePath, config.confThresh); // 2. 初始化 DeepSort传入 ReID 特征网络的 engine 路径 DeepSort tracker(config.reidEnginePath); cv::VideoCapture cap(config.videoPath); if (!cap.isOpened()) { std::cerr Failed to open video: config.videoPath std::endl; return -1; } cv::Mat frame; while (cap.read(frame)) { auto dets detector-run(frame); // YOLOv5 推理 后处理 auto tracks tracker.update(dets); // DeepSort 更新轨迹 drawTracks(frame, tracks); // 画框和 ID 标注 } return 0; }这个循环里最需要注意的一点是detector-run(frame)内部不要每次创建新的 cudaStream 和 buffer应该在一个init()阶段就分配好。Jetson 的内存带宽本来就紧张频繁复制 Mat 和显存 buffer 会产生明显卡顿。tracker.update(dets)这一行看起来简单内部实际上是卡尔曼预测、级联匹配、匈牙利匹配、轨迹删减四个步骤的串行执行每一步的状态都保存在 tracker 内部的 Track 列表里。源码如果支持读取摄像头调用方式不用变只需要把VideoCapture的参数从视频路径换成 0 或者 RTSP 地址。4. 模型转换从 pt 文件到 TensorRT engine4.1 pt 转 onnx导出节点和动态轴Jetson 项目里最折腾的环节就是模型转换。PyTorch 的 pt 权重不能直接喂给 TensorRT必须先过一遍 onnx。这一步我在 PC 上做也可以在 Jetson 上做但 Jetson 的显存小导出时显存占用波动大推荐在 PC 上导出 onnx把 onnx 拷贝到板子上再构建 engine。导出 onnx 时有一个关键点只导出三个检测头不要导出后处理。YOLOv5 的官方导出脚本如果直接导出会把 NMS 也包进去TensorRT 对 NMS 的支持比较捉急反而增加构建失败的几率。常见的做法是手工指定输出节点import torch # 加载模型并转入推理模式 model torch.load(yolov5s.pt, map_locationcpu)[model].float() model.eval() # 虚拟输入固定 640x640 x torch.zeros(1, 3, 640, 640) torch.onnx.export( model, x, yolov5s.onnx, opset_version11, # 11 最稳12/13 也可以 do_constant_foldingTrue, input_names[images], output_names[output_80, output_40, output_20], dynamic_axes{images: {0: batch}, output_80: {0: batch}, output_40: {0: batch}, output_20: {0: batch}} )opset 版本建议用 11TensorRT 对这个版本的算子兼容性最好太高或太低都会出现莫名其妙的算子不支持报错。dynamic_axes 这里只给 batch 维度设了动态不要给宽高设动态因为宽高变化会让 TensorRT 的显存分配策略变得很保守推理性能反而下降。如果你的场景只需要单路视频甚至可以把 batch 也固定成 1构建出来的 engine 更小、延迟更稳定。4.2 onnx 转 enginetrtexec 参数与显存控制onnx 转 engine 有两种方式调用 TensorRT C API 写一个 builder 脚本或者直接用 trtexec 命令行工具。trtexec 在 Jetson 上的路径是/usr/src/tensorrt/bin/trtexec它是排查模型兼容性问题最趁手的工具报错信息比自写脚本详细得多。/usr/src/tensorrt/bin/trtexec \ --onnxyolov5s.onnx \ --saveEngineyolov5s_fp16.engine \ --fp16 \ --workspace1024 \ --buildOnly--workspace1024的意思是给构建过程分配 1024 MB 显存作为工作空间这个值不是越大越好在 Jetson Nano 2GB 上超过 1200 会直接导致构建失败4GB 版可以给到 1500 左右。--buildOnly告诉 trtexec 只构建 engine 不做推理测试省掉构建完还跑一遍 profile 的时间。TensorRT 10.x 之后--workspace改名成了--memPoolSize单位也变成了字节用老命令会报参数不识别这一点容易让人误以为是模型问题实际上只是版本差异。4.3 engine 复用序列化、反序列化与设备绑定engine 构建是一个非常耗时的过程在 Jetson Nano 上构建一个 YOLOv5s 的 FP16 engine动辄需要 5 到 10 分钟。所以工程上必须把构建过程做成一次性的第一次运行构建并保存到本地之后直接加载文件。这也解释了为什么源码包里会同时出现 .onnx 和 .engine 两种格式的文件。加载端代码的典型写法是懒加载检查文件存在性不存在才构建std::ifstream file(enginePath, std::ios::binary); if (!file.good()) { // engine 文件不存在或已损坏从 onnx 现场构建 buildAndSave(onnxPath, enginePath); } else { file.seekg(0, std::ios::end); size_t size file.tellg(); file.seekg(0, std::ios::beg); std::vectorchar blob(size); file.read(blob.data(), size); // 反序列化得到可用的 ICudaEngine runtime-deserializeCudaEngine(blob.data(), size); }这里有一个必须注意的机制TensorRT 的 engine 文件是和 GPU 架构绑定的。在 Jetson Nano 上构建的 engine拷贝到 Xavier NX 或者 AGX Orin 上大概率加载失败因为它们的 GPU 算力不同。就算同样是 NanoJetPack 升级导致 TensorRT 小版本变化旧 engine 也可能失效。所以我一般会保留 onnx 作为后悔药每次换设备或者升级系统后让程序自动走一遍engine 不存在就从 onnx 构建的流程省去手工排查的麻烦。5. 部署避坑记录Jetson 上最常见的几个坑5.1 编译和构建阶段的三次翻车坑一CMake 找不到 TensorRT 头文件。现象是 configure 阶段报Could NOT find TensorRT或者nvinfer.h: No such file or directory。原因在 Jetson 上很典型TensorRT 的头文件安装在/usr/local/aarch64-linux-gnu/tensorrt/include而 CMake 默认搜索路径是/usr/include。解决方法是手动指定路径在 CMakeLists 里加一行include_directories(/usr/local/aarch64-linux-gnu/tensorrt/include)同时把库路径指向对应的lib目录。坑二engine 构建过程中内存爆掉。现象是 trtexec 跑到一半进程被 kill日志最后一行是Killed。原因基本上是内存不足Jetson Nano 2GB 版本在构建 YOLOv5s FP16 engine 时峰值内存能到 1.6GB 以上系统 OOM 直接杀进程。解决方法是加 swap用fallocate创建一个 4GB 的 swapfile 并挂载实测构建过程立即稳定下来。如果还不行就把输入分辨率从 640 降到 416构建时间和内存占用都会明显下降。坑三engine 文件拷到另一台 Jetson 上报错。现象是deserializeCudaEngine返回空指针或者抛异常。原因是 engine 与 GPU compute capability 和 TensorRT 版本绑定跨设备不通用。解决方法是像第 4 章那样保留 onnx在目标设备上现场重新构建不要试图传 engine 文件。这个坑我在展会前一周踩过一次从那以后 onnx 和 engine 永远同时带上。5.2 运行时的 ID 跳变与性能瓶颈坑四跟踪 ID 频繁跳变。现象是一个人走到画面中间ID 从 1 跳到 3 又跳到 5轨迹完全连不起来。排查顺序是先看检测框稳不稳如果检测框本身在人体上左右飘说明 confThresh 设得太低把过滤阈值从 0.25 提到 0.4 试试再看 ReID 特征维度是否和 engine 一致源码里如果写死分配 128 维但 ReID engine 实际输出 512 维读取的是内存垃圾数据特征相似度完全失真ID 必然乱跳。打印一次特征向量的尺寸就能确认。坑五显存和 CPU 双双吃满帧率掉到几个 FPS。现象是程序跑起来之后系统负载拉满帧率个位数。原因通常是两处一是在主循环里频繁clone()Mat每次克隆都在共享内存上拷贝一整帧二是解码出来的检测框没有复用对象反复 new/delete 造成内存碎片。解决办法是预分配好 buffer只在画框时才拷贝其余环节全部用引用传递。在 Jetson 上优化和整改的最大空间往往不在算法侧而在这些内存使用习惯上。6. 验证与进阶从跑通 demo 到交付结果跑通之后不能只盯着画面说看起来跟上了要有一个可度量的验证方法。我的习惯是跑一段 30 秒的固定视频统计三个指标平均帧率、ID 切换次数、轨迹断点数量。帧率用程序里的计时日志直接读ID 切换次数可以通过在画框时把 trackId 写入 CSV再用脚本统计同一个物理目标是否出现了多个 ID。如果 30 秒内一个行人从画面左侧走到右侧ID 切换超过两次就需要回去调 DeepSort 的 max_age 或者检测器的 confThresh。这个验证脚本不复杂但它是判断跟踪质量是否达标的客观依据比肉眼判断可信得多。进阶方向上第一个值得动的是 DeepSort 的 ReID 特征网络。源码里 ReID 是以独立 engine 形式加载的这意味着想改进跟踪效果只需要把 ReID 网络替换成更轻或者更强的模型跟踪主逻辑完全不用动。我在实际项目里做过一次实验把原始的浅层 CNN 换成一个经过知识蒸馏的小型网络在 Jetson Xavier NX 上特征提取耗时从 12ms 降到了 6msID 切换次数下降了大约 30%。改动范围集中在模型转换和一处特征加载代码风险和收益比很高。第二个方向是 INT8 量化。FP16 在 YOLOv5s 上已经能跑到 20ms 左右如果还想要更高帧率就需要走 INT8。步骤比 FP16 多一个校准环节准备 100 张有代表性的图片图片内容要和实际场景接近用 TensorRT 的 calibration 工具跑一遍生成校准表后再构建 engine。以我的实测经验INT8 在 Jetson Nano 上能把 YOLOv5s 压到 12ms 左右代价是 mAP 下降 1 到 2 个百分点在夜间或者低对比度场景下误检会变多。对应付密集行人跟踪INT8 的精度损失往往可以在 DeepSort 的级联匹配环节被消化一部分整体跟踪效果下降没有检测指标看起来那么夸张。第三个进阶是多路视频的支持。如果你最终要做的是四路摄像头同时跟踪不要开四个串行推理线程正确做法是共享同一个 engine 的 context把多路输入拼成 batch 推理或者用 CUDA stream 并行。这个改造在纯 C 工程里涉及把单帧处理函数改成批量接口工作量集中在内存管理和结果切分。我从一次现场 demo 的翻车里得到的教训是每次部署前强制跑一遍峰值内存脚本确认设备余量在 30% 以上再考虑多路扩展。希望这些拆解对你有用祝你在 Jetson 上把跟踪跑稳。本文还有配套的精品资源点击获取