ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

YOLOv8+ByteTrack+TensorRT:C++实现实时目标检测与跟踪部署

YOLOv8+ByteTrack+TensorRT:C++实现实时目标检测与跟踪部署 简介面向需要在Jetson或Linux x86_64平台落地实时目标跟踪的开发者这套项目基于TensorRT C API部署YOLOv8检测器与ByteTrack跟踪器覆盖从PyTorch权重转换engine、CUDA预处理/后处理到两类动态链接库封装的全流程并支持在main.cpp中按需过滤跟踪类别。压缩包共64个文件、约25.16MB包含27个头文件与14个cpp实现核心接口另有5个cu文件负责GPU加速、4个md文档说明编译部署、5个txt配置文件及2个py脚本辅助权重转换目录按yolo和bytetrack分模块组织便于解耦复用。目前已有163人学习下载适合具备一定C基础、希望跳过重复造轮子直接集成目标检测与多目标跟踪的开发者。资源附带效果动图、演示视频和标签文件可快速验证跟踪效果对Jetson嵌入式设备性能调优也有直接参考价值。1. 这包资源解决什么问题YOLOv8 检测、ByteTrack 跟踪、C 推理一张链条打通做目标检测部署的人都知道一个尴尬Python 里调 ultralytics 跑 YOLOv8FPS 很好看但一旦要求「把检测结果跟上 ID、跨帧跟踪、还要在边缘设备上实时跑」原来那套代码就碎了。你需要一个不止会画框还会数得清「这是第几个进入画面的人」的系统。这份 C TensorRT 加速的 YOLOv8 ByteTrack 对象跟踪工程包解决的就是这条链路YOLOv8 负责每帧检测ByteTrack 把检测框关联成稳定轨迹TensorRT 负责让这一切在 GPU 上以可用速度跑起来而不是停留在演示阶段。适合正在做安防、交通、工业视觉或者想把检测模型落地成跟踪应用的算法工程师和部署工程师。2. 为什么是 YOLOv8、ByteTrack、TensorRT 这三样选型理由与衔接逻辑2.1 YOLOv8 在跟踪链路里承担的职责一个 anchor-free 检测底座的输出形态跟踪的前提是检测。ByteTrack 本身不产生目标框它消费的是检测器输出的每一帧结果。YOLOv8 之所以适合做这个底座核心在于它的输出设计很规整网络结构采用 anchor-free 分支分类头和回归头解耦不需要像 YOLOv5 那样维护一堆 anchor 先验。对部署来说这意味着后处理逻辑简单、张量形状固定容易在 TensorRT 里做层融合。以 COCO 80 类模型为例输入 640×640 的图YOLOv8 输出的特征张量形状是 1×84×8400。其中 84 是 4 个边框坐标加 80 个类别分数8400 是三个尺度特征图展平后的候选框总数6400 1600 400。这里要注意YOLOv8 在导出 ONNX 时默认不带 NMS也就是说框架只负责吐出原始预测非极大值抑制要拿到 C 里自己做。很多新手在这里翻车拿 TensorRT 直接推理得到 1×84×8400不知道如何解析成框也不知道为什么跟 Python 里看到的结果对不上。我一般习惯在 C 侧把后处理拆成三步先按置信度阈值过滤低分框再做坐标解码最后按类别做 NMS。这个顺序不要乱否则漏检率和重复框会同时失控。2.2 ByteTrack 的 BYTE 关联为什么低分框反而是跟踪稳定的关键ByteTrack 跟 SORT、DeepSORT 的核心差别在于它的名字本身BYTE。传统做法只保留置信度高于阈值的检测框去做轨迹关联低于阈值的框直接丢弃。ByteTrack 反其道而行把检测框分成高分数和低分数两拨先拿高分数框跟已有轨迹做第一轮匈牙利匹配未匹配上的轨迹再拿低分数框去补匹配。这套思路解决了一个非常实际的痛点目标被遮挡或者运动模糊时检测器的置信度会骤降框还在但分数低了。如果直接把低分框扔掉跟踪 ID 就会断。ByteTrack 把这些低分框当作「补位候选」可以显著减少 ID Switch。代价也很明显它比纯 SORT 多了一次匹配计算量略涨而且低分框本身质量参差如果匹配阈值设得太松轨迹会被垃圾框带偏。后面第 4 章我会给一组能直接抄的参数。2.3 TensorRT 在这一层扮演的角色Python 里不会遇到的显存和 CUDA 问题TensorRT 是把 PyTorch 模型变成部署用引擎的标准路径。它做的事情包括把 ONNX 解析成计算图做层融合比如 ConvBiasReLU 合并成一条按 GPU 架构选择 kernel以及把精度降到 FP16 甚至 INT8。这里的取舍要清楚FP16 一般可以把推理速度拉到 FP32 的 1.5 到 2 倍显存占用也减半但个别算子在低精度下会出现可感知的精度损失。INT8 更快但需要校准集去算 scale工程复杂度高一截。这个工程包默认走 FP16我建议不要在最初阶段碰 INT8除非你已经有稳定的标定数据管线。还有一个部署特有的问题Python 里的 PyTorch 推理自带动态 shape而 TensorRT 引擎需要显式声明 optimize profile。你输入的是 640×640但如果想支持 batch size 可变就必须在构建引擎时配置好最小、最优、最大三个维度。C 侧改 batch 比 Python 侧麻烦得多C 里多一个维度意味着所有绑定 buffer 都要重新分配显存。2.4 三个组件拼成一条流水线从 camera 到画框的完整数据流把整条链路拆开看实际运行时是这样一个循环读帧 → letterbox 预处理 → 拷贝到 GPU → TensorRT 推理 → 从 GPU 拷回结果 → 后处理解析框 → 交给 ByteTrack 更新轨迹 → 按 track_id 画框并显示或推流。这个流程里最容易忽视的是 letterbox 的缩放信息。TensorRT 吃进去的是 640×640原始画面通常不是正方形预处理时会在边缘填充灰边。后处理阶段要把框坐标还原回原图必须知道缩放比例和填充偏移量。如果只存了缩放比例忘了偏移检测框在画面下方会整体偏。C 里我习惯把这两个值存在一个结构体里跟推理结果一起传递而不是用全局变量避免多线程调用时串数据。另一个要注意的阶段是 ByteTrack 的输入输出。它吃的是一个 vector 的检测框结构体包含 x1、y1、x2、y2、score、class_id输出的是带 track_id 的同样结构体。所以后处理和跟踪之间的接口应当定义成一个统一的数据结构不要在两处各写一套框的表示方法否则改到后面你自己都分不清坐标是原图坐标还是 letterbox 坐标。3. 环境与编译TensorRT 版本、CUDA 与依赖让工程先跑起来3.1 先对齐版本CUDA、cuDNN、TensorRT 三者必须匹配这套工程是用 C 写的意味着你绕不开本机环境的 CUDA 和 TensorRT。第一个要命的点是三者版本必须匹配。TensorRT 发布时是针对特定 CUDA 和 cuDNN 版本编译的你用 CUDA 12.4 配 TensorRT 8.5 大概率能跑但如果你用老显卡比如 GTX 1070就得看看自己卡的算力够不够。GTX 10 系列是 Pascal 架构FP16 支持没问题但性能和 Turing 之后的卡有明显差距。网络热词里有个高频问题「tensorrt 版本如果是 10.x 是否支持 gtx1070」这里我要说清楚TensorRT 10.x 本身支持 Pascal 架构但 10.x 把大量 C API 标记为 deprecated比如 enqueueV2 被 enqueueV3 取代destroy 不再显式调用。工程的 CMakeLists 里如果写的是老 API在 10.x 上编译会直接报错。我建议先确定你手头的 TensorRT 版本再选 API 风格不要盲目追新。3.2 编译前的清单你需要准备哪些东西打开终端前先按下面这份清单核对环境。缺一项都会卡在编译期或者运行期而这两类的报错信息完全不一样。依赖项推荐版本区间作用常见坑CUDA Toolkit11.x / 12.xGPU 计算库驱动版本要配套cuDNN与 CUDA 配套深度网络加速忘记设置 LD_LIBRARY_PATHTensorRT8.5 ~ 10.xONNX 解析、推理引擎版本间 API 差异大OpenCV4.x图像读取、预处理、画框4.1 和 4.5 的 API 有小变化CMake3.16构建系统需要能找到 TensorRT 的安装路径在 Ubuntu 20.04 上搭这套环境最省心配合 vscode 的 C/C 插件调试也比较顺。装完 TensorRT 之后马上检查环境变量export LD_LIBRARY_PATH/path/to/TensorRT/lib:$LD_LIBRARY_PATH export CUDA_HOME/usr/local/cuda ldconfig环境变量这块最容易被忽略。TensorRT 的 .so 库如果找不到编译能过但运行直接报 libnvinfer.so.10: cannot open shared object file。我一般会在跑程序前先执 ldd 确认依赖链完整而不是到运行失败才开始排查。3.3 CMakeLists.txt 写法和编译命令工程的构建文件通常会把 OpenCV、CUDA、TensorRT 串起来。一个能跑的 CMakeLists 核心长这样cmake_minimum_required(VERSION 3.16) project(yolo_tensorrt_bytetrack) set(CMAKE_CUDA_STANDARD 14) find_package(CUDA REQUIRED) find_package(OpenCV REQUIRED) set(TENSORRT_ROOT /path/to/TensorRT) include_directories(${TENSORRT_ROOT}/include) link_directories(${TENSORRT_ROOT}/lib) add_executable(run_tracker src/main.cpp src/preprocess.cpp src/postprocess.cpp src/bytetrack.cpp) target_link_libraries(run_tracker nvinfer nvparsers nvinfer_plugin cudart ${OpenCV_LIBS})注意 find_package 的位置。TensorRT 官方不提供 CMake module所以要用 include_directories 和 link_directories 手动指路径。如果你用的是 TensorRT 9 以上还要额外链接 nvonnxparser 库否则 ONNX 解析器会报符号找不到。编译命令很常规mkdir build cd build cmake .. -DTENSORRT_ROOT/path/to/TensorRT make -j$(nproc)编译报错大多集中在头文件路径不对、-l 库名拼写不一致这两个地方。我踩过一次坑是 OpenCV 4.5 之后 imgcodecs 库被拆分画框要用到 imgproc链接时漏了它原图显示功能就起不来。3.4 工程包的文件布局长什么样拿到手先看哪几个文件这类 C 工程包的目录结构高度相似拿到手先别急着编译按下面顺序核一遍project_root/ ├── CMakeLists.txt # 构建入口先看 TensorRT 路径是否指向你的安装位置 ├── README.md # 一般会写清版本号和构建顺序 ├── src/ │ ├── main.cpp # 主循环读帧推理画框 │ ├── preprocess.cpp # letterbox 和归一化 │ ├── postprocess.cpp # decode NMS │ └── bytetrack.cpp # 跟踪器封装 ├── include/ │ ├── trt_model.h # TensorRT 引擎加载与推理类 │ └── bytetrack.h # 跟踪器接口定义 ├── models/ │ └── yolov8n.engine # 转换好的引擎文件没有就自己转 └── configs/ └── track_params.json # 跟踪参数最关键的是 CMakeLists.txt 里的 TensorRT 路径和 models 目录下的 engine 文件。如果包没带 engine你得按第 4 章的流程从 pt 自己转。配置文件里的 track_thresh 和 match_thresh 决定了跟踪表现先别动用默认值跑通再调。4. 从 pt 到 engine 再到 ByteTrack核心流程与可抄的参数配置4.1 模型导出用 YOLOv8 官方方式把 pt 写成 ONNXYOLOv8 的模型导出比 YOLOv5 省事直接用 ultralytics 库的 export 接口。我实际在命令行里用的方式是这样yolo export modelyolov8n.pt formatonnx dynamicTrue opset12 simplifyTrue如果你的环境装的是 ultralytics 的 Python 包也可以写成脚本from ultralytics import YOLO model YOLO(yolov8n.pt) success model.export(formatonnx, dynamicTrue, opset12, simplifyTrue) print(export onnx:, success)这里几个参数含义要说清楚dynamicTrue生成的 ONNX 允许 batch size 或输入尺寸可变后面用 TensorRT 构建引擎时可以指定动态 shape。opset12ONNX 算子集版本。opset 太低TensorRT 解析某些算子会失败opset 太高老版本 TensorRT 认不出。12 是在兼容性和算子支持上的平衡点。simplifyTrue让 onnx-simplifier 把计算图做一轮精简去掉冗余 reshape 和 transposeTensorRT 解析会更容易。导出后建议先用 onnxruntime 验证一遍输出再进入 TensorRT 环节。很多人上来直接转引擎结果发现跟踪框乱飘回头排查到是导出环节的问题。4.2 ONNX 到 engine用 trtexec 构建 FP16 引擎TensorRT 提供了 trtexec 命令行工具可以直接把 ONNX 转成序列化的 engine 文件我在构建阶段一直用它比写 C builder 代码快得多trtexec --onnxyolov8n.onnx \ --saveEngineyolov8n_fp16.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:4x3x640x640 \ --workspace4096这个命令的对应参数含义--fp16开启半精度。默认是 FP32不开启加速效果就出不来。--minShapes / --optShapes / --maxShapes这三组必须同时给TensorRT 用它们构建优化 profile。如果你之后要在 C 端用 batch4 推理maxShapes 至少要写到 4只有 1 的话运行时一旦改成 2 就会报 invalid binding shape。--workspace4096构建时允许使用的显存上限单位 MB。解析大模型时给太小白报 workSpace exhausted。转完看到类似 Engine built in x seconds 的日志就说明成功。我习惯把转好的 .engine 文件记录一个校验信息比如拿 md5sum 记一下因为换一次 TensorRT 版本同样的命令生成的文件内容会不同。程序运行异常时可以快速排除是不是引擎文件跟运行库版本不匹配。4.3 C 侧推理封装加载引擎、分配显存、执行推理engine 文件准备好后C 端要做的事就是把引擎加载进 runtime创建 execution context分配输入输出 buffer然后跑推理。核心代码长这样// 加载序列化引擎 std::ifstream file(yolov8n_fp16.engine, std::ios::binary); std::vectorchar data((std::istreambuf_iteratorchar(file)), std::istreambuf_iteratorchar()); nvinfer1::IRuntime* runtime nvinfer1::createInferRuntime(logger); nvinfer1::ICudaEngine* engine runtime-deserializeCudaEngine(data.data(), data.size()); nvinfer1::IExecutionContext* context engine-createExecutionContext(); // 分配显存 —— 输入输出各一块 void* buffers[2]; cudaMalloc(buffers[0], batch * 3 * 640 * 640 * sizeof(float)); // 输入 cudaMalloc(buffers[1], batch * 84 * 8400 * sizeof(float)); // 输出 // 推理 context-setTensorAddress(images, buffers[0]); context-setTensorAddress(output0, buffers[1]); context-enqueueV2(buffers, 0, nullptr, nullptr); cudaDeviceSynchronize();几个容易出问题的点buffer 大小要和引擎的 binding 维度一致。TensorRT 在 deserialize 时有自己的对齐规则如果显存分配小了推理时直接越界轻则打出乱码重则显存报错。enqueueV2 是旧版 API 风格TensorRT 10.x 推荐用 enqueueV3 加 stream 的方式但旧代码里如果绑定名称跟 ONNX 输出名不一致setTensorAddress 会报错你需要在第一次运行前打印 context-getEngine()-getBindingName(i) 确认名字。推理输出是一维 1×84×8400 的浮点数组。每 8400 个元素对应一个位置的坐标或者一个类别的分数内存布局是 [x1, y1, x2, y2, class0_score, class1_score, ...]理解这个之后才好在代码里做解析。4.4 后处理置信度过滤、坐标解码与 NMS后处理是检测环节的最后一步。代码里我会写成三个独立函数方便单测struct Detection { float x1, y1, x2, y2, score; int class_id; }; std::vectorDetection decode_and_nms(float* raw_output, float conf_thres, float iou_thres) { std::vectorDetection detections; const int num_candidates 8400; const int num_classes 80; const int stride 84; for (int i 0; i num_candidates; i) { float max_score 0.0f; int max_class -1; for (int j 0; j num_classes; j) { float score raw_output[4 j i * stride]; if (score max_score) { max_score score; max_class j; } } if (max_score conf_thres) continue; float x1 raw_output[i * stride]; float y1 raw_output[i * stride 1]; float x2 raw_output[i * stride 2]; float y2 raw_output[i * stride 3]; detections.push_back({x1, y1, x2, y2, max_score, max_class}); } // 类别内 NMS按 score 降序 std::sort(detections.begin(), detections.end(), [](const Detection a, const Detection b) { return a.score b.score; }); std::vectorbool removed(detections.size(), false); // IoU 计算省略标准实现 return detections; }逻辑说明第一步先找每个候选框分数最高的类别并以这个分数做阈值过滤相当于把 80 类分数压缩成一个最大值。第二步做 NMS 时我只在同一 class_id 内做不同类别的框允许重叠这是 YOLO 系列默认处理逻辑。参数上conf_thres 我建议设 0.25 起步这个值对应 ByteTrack 输入的高分框定义。如果设成 0.5跟踪时的低分框匹配阶段就基本失效ByteTrack 退化成普通 SORT。iou_thres 设 0.7这是检测标准值改大会出现多个框重叠同一目标改小会漏框。4.5 对接 ByteTrack参数配置表和第一个跟踪结果后处理得到的 Detection 列表直接喂给 ByteTrack 的 update 方法得到带 track_id 的结果。ByteTrack 的 C 实现参数一般集中在头文件里配置含义如下表参数建议值含义调高 / 调低的影响track_thresh0.5高分框阈值调高则跟踪更保守目标反应慢match_thresh0.8关联匹配阈值调高则容易切换 ID调低则容易保持旧 IDframe_rate30输入视频帧率影响卡尔曼滤波运动预测的补偿min_box_area20忽略过小框过滤远处噪声框调大会漏小目标我第一次跑通时用的就是这组值track_thresh0.5、match_thresh0.8、frame_rate30、min_box_area20。跟踪结果会在控制台打印 track_id 和坐标同时在窗口里用不同颜色标出每个 ID 的框和编号。看到 ID 稳定不跳这一步就算成了。5. 避坑与排查四个最容易翻车的现场和对应处理5.1 现象、原因、解决我把最常见的四类问题拆开讲这一章写的是我自己在部署过程中反复踩过的坑每一条都是「现象 → 原因 → 解决」三段式建议你遇到异常时按这个顺序排查比在网上搜报错快得多。坑一程序编译通过但运行报 libnvinfer.so.10 找不到现象cmake 构建成功make 也成功一运行就报 cannot open shared object file。原因TensorRT 的 lib 目录没有加进 LD_LIBRARY_PATH或者路径写的是老版本号。解决在编译前先 export LD_LIBRARY_PATH并检查 TensorRT/lib 下实际的 .so 文件名。血泪经验TensorRT 从 8.x 升到 10.x.so 后缀名变了所有硬编码路径都白搭最后必须用通配符或环境变量。坑二跟踪框比目标框大一圈而且越靠画面边缘偏差越大现象检测框套在目标外围偏移量随目标在画面中的位置变化。原因letterbox 填充偏移量 x_offset 和 y_offset 没有传到还原函数里后处理按原图尺寸直接缩放坐标。解决确认预处理阶段记录了填充宽高还原坐标时用公式 x (x1 - x_offset) / scaley (y1 - y_offset) / scale。我自己在这上面栽过一次原因是把 letterbox 参数放进了全局变量多线程下一帧的偏移覆盖了上一帧。坑三跟踪 ID 频繁跳变同一目标来回变号现象目标直线行走track_id 从 3 跳到 8 又跳回 3。原因有两种可能一是 track_thresh 设太高导致低分框全部被丢弃二帧率参数与视频实际帧率不一致卡尔曼滤波的运动预测节奏乱了。解决先把 track_thresh 降到 0.4 试一轮再看 match_thresh 是否低于 0.7。ByteTrack 对帧率很敏感frame_rate 设置成 25 但实际视频是 30运动补偿就会偏。坑四TensorRT 10.x 下编译报错函数名对不上现象enqueueV2 未定义或者 destroy 方法不存在。原因TensorRT 10.x 把旧版 API 移除了。解决要么锁定 TensorRT 8.x 版本编译要么改写成新 API。我一般推荐先按包内带的 CMakeLists 锁定版本不要用系统里最新的 TensorRT 去编老工程。这里有个玄学现象同一份 ONNX不同 TensorRT 版本生成的 engine 文件的数值结果会有极小差异但足以影响跟踪的稳定性判断。5.2 排查顺序从哪个环节入手定位问题当你发现跟踪结果不对先不要急着调参数。按下面顺序排查效率最高确认检测没问题把跟踪器停用单独输出检测框看单帧结果是否正常。确认推理没问题用 trtexec 跑同一张图对比输出和 onnxruntime 的一致性。确认后处理没问题把解析出的框画到图上检查坐标是否贴合目标。最后才是跟踪器问题对比不同 track_thresh 下的 ID 稳定性。这个顺序背后的逻辑是链路越靠前的问题影响越大。检测框本身偏了跟踪器再准也没用。我见过有人花两天调 match_thresh最后发现是 TensorRT 输出布局解析错了坐标顺序反了跟踪器拿到的框全是乱的。不要跳过前几步直接调参。6. 进阶调优验证跟踪效果的三个习惯和一个让结果更稳的关键动作6.1 用可视化验证替代盯着控制台数字盯到怀疑人生跟踪器的输出不能只看 FPS也不能只看不报错。我建议用 OpenCV 把每一帧画上 track_id 和检测置信度保存成视频然后慢速播放。重点看三个地方目标交叉时 ID 是否会互换、目标短暂出画再回来能否找回原 ID、远处小目标是否有轨迹抖动。C 里画框很简单cv::rectangle(frame, cv::Rect(det.x1, det.y1, det.x2 - det.x1, det.y2 - det.y1), cv::Scalar((det.track_id * 50) % 255, 128, 255), 2); cv::putText(frame, std::to_string(det.track_id), cv::Point(det.x1, det.y1 - 5), cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 255, 0), 2);ID 号按取模方式映射成颜色同一个目标前后帧颜色应该一致。如果颜色闪来闪去说明 ID 不稳定这时候要看的是目标交互场景而不是画面整体。6.2 每次换模型都强制走一遍的验证流程换检测器、换数据集、换 TensorRT 版本之后我每次都强制按同一套流程走先跑单帧静态图再跑一段 30 秒真实视频最后统计 ID Switch 次数。单帧保证检测正确视频保证跟踪稳定统计保证改进可量化。这算是从多次翻车里换来的习惯现在每拿到一个新模型我都先忍住不调参数把基线跑出来再说。希望这套习惯能帮你在部署 YOLOv8 ByteTrack 时少走几趟弯路。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表