
简介面向需要在Windows环境下快速落地YOLOv10目标追踪的C开发者资源以VS2019工程形式将YOLOv10的ONNX模型通过ONNX Runtime推理并接入ByteTrack多目标跟踪算法给出完整的演示源码、模型文件与运行配置测试环境为OpenCV 4.7.0、ONNX Runtime 1.12.0、CMake 3.24.3。压缩包共1749个文件约22.36MB以cpp源码和h头文件为主同时包含cmake构建脚本、onnx权重、dll运行库及mp4演示视频等还内置Eigen等第三方依赖库省去手动收集依赖的麻烦目录结构清晰便于按模块查阅。借助工程内的VS工程文件、测试图片与B站演示视频可快速复现从目标检测到轨迹关联的完整流程尤其适合正在做目标计数、车辆跟踪等项目或希望从Python转向C部署的读者直接参考。目前已有615人学习下载。 做目标检测也有一段时间了之前一直用Python写推理、调参、出图流程跑通倒是很快但真要放到实际项目里做常驻服务还是得用C。原因很直接C在内存管理、延迟控制、依赖打包上比Python好控太多。我这次把YOLOv10的ONNX模型和ByteTrack串起来在C里完整实现了一个目标追踪Demo把检测和跟踪的逻辑彻底沉淀进原生代码里跑下来的效果和性能都让我比较满意。这篇文章就把整个实现过程、踩过的坑、关键细节都梳理清楚。如果你想在C环境里做实时目标追踪或者想搞清楚ONNX Runtime怎么和ByteTrack搭配使用这篇文章应该能帮你省下不少试错时间。1. 整体方案设计与追踪思路1.1 为什么选YOLOv10加ONNX RuntimeYOLOv10相比之前的YOLO系列最大的改动是去掉了NMS后处理模型端到端直接输出目标框和类别置信度。这一步砍掉了传统检测里最麻烦的一个环节部署的时候少写一段NMS逻辑推理延迟也降了一截。至于推理框架我用的是ONNX Runtime。它在CPU、CUDA、TensorRT后端都有支持跨平台能力也很强Windows、Linux都能跑而且只需要链接一个动态库就能用。实操中我对比过几种方案直接用LibTorch部署模型要带整个Torch运行时体积大OpenVINO对硬件有偏向性ONNX Runtime是最折中的选择——模型从PyTorch转一次ONNX之后整个C工程只需要ONNX Runtime和OpenCV两个依赖干净利落。1.2 ByteTrack的追踪逻辑拆解ByteTrack的核心思路是“让每个检测框都参与追踪”。它有两个关键点第一用高置信度的检测框和已有轨迹做匹配第二对低置信度的检测框再做一次关联。这种做法能在目标短暂遮挡、部分检测失败时维持轨迹比单纯只靠高置信框的DeepSORT类算法在密集场景里的表现好很多。具体实现上ByteTrack用卡尔曼滤波预测轨迹的状态用匈牙利算法做检测框和轨迹框的匹配。匹配的代价矩阵用的是IoU轨迹与检测框的交并比步骤分为两步先用高分数检测框匹配再用低分数检测框给未匹配的轨迹补一次。每个Track在连续丢帧后会进入待删除状态达到阈值就清理掉这样能有效避免ID重复分配。这里有一个细节值得注意ByteTrack本身的追踪是基于检测结果驱动不依赖Re-ID特征所以它对检测器质量的要求很高。如果检测框抖动、漏检严重追踪效果会直线下降。这也是为什么我在工程里把检测器的置信度阈值放在追踪器外面统一调而不是包在模型内部。1.3 整体流程设计与模块划分整个工程的流程可以按一个固定循环来处理读取一帧图像做预处理ONNX Runtime执行模型推理后处理解析检测框将检测结果喂给ByteTrackByteTrack输出带ID的轨迹最终在图像上画框画ID并显示或写视频。我把代码拆成三个模块Detector负责模型加载和推理Tracker负责跟踪状态Visualizer负责画图输出。这样做的最大好处是以后换检测模型或者换跟踪策略只需要改对应模块不用动主循环。实际写代码的时候你会发现模块划分清晰了调试定位问题能快三分之一的时间。2. 环境准备与模型导出2.1 依赖清单在开始写代码之前需要先把依赖准备好。我的环境是Ubuntu 20.04编译工具是CMake加GCC核心依赖就三样OpenCV 4.x负责图像读取、预处理、画框、显示版本只要在4.0以上基本没有接口差异ONNX Runtime直接用动态库Linux下是libonnxruntime.soWindows下是onnxruntime.dll我用的版本是1.15.0以上ByteTrack源码官方C版本里有BYTETracker.h和BYTETracker.cpp直接拿过来用代码本身依赖OpenCV不需要额外装库有人说还要装Eigen之类的数学库其实不需要。ByteTrack自带的卡尔曼滤波实现用的是矩阵运算源码里已经把所有依赖写死了直接源码编译进去就行省事。2.2 导出ONNX模型的关键参数YOLOv10导出ONNX的步骤很简单官方仓库已经写好了导出脚本。命令行里执行以下命令就能完成转换yolo export modelyolov10s.pt formatonnx dynamicTrue opset13有几个参数我特别提醒一下。第一个是dynamicTrue它表示让模型的输入尺寸变成动态的允许在推理时传入不同的分辨率。如果不加这个参数模型会固定成训练时的尺寸比如640×640后续如果画面长宽比不是1:1就得先拉伸再推理检测框位置会偏。第二个是opsetONNX Runtime对opset 13以上的支持最稳如果导出的模型在加载时报算子不兼容的错误试着调整这个参数。导出之后可以先用Netron看一眼模型结构。YOLOv10的输出一般是(batch, 300, 6)这类形状其中300是模型预测的候选框数量6是(x1, y1, x2, y2, score, class_id)的拼接。注意这个输出已经是模型内部解码之后的结果不需要再像YOLOv5那样做anchor解码。我实际在调试的时候遇过一个典型的坑导出的ONNX如果带了对CPU不友好的算子比如某些版本的动态尺寸导出会生成一个Resize导致CPU推理速度慢很多。这时候的解决办法是固定输入尺寸或者改用更稳定的opset版本。2.3 工程目录结构我这里给出一个可以直接套用的工程目录你按这个结构建就行yolov10_bytetrack_demo/ ├── CMakeLists.txt ├── src/ │ ├── detector.h │ ├── detector.cpp │ ├── tracker_wrapper.h │ ├── tracker_wrapper.cpp │ └── main.cpp ├── models/ │ └── yolov10s.onnx └── third_party/ ├── ByteTrack/ │ ├── BYTETracker.h │ ├── BYTETracker.cpp │ ├── STrack.h │ ├── STrack.cpp │ ├── KalmanFilter.h │ ├── KalmanFilter.cpp │ ├── lapjv.h │ └── lapjv.cppOpenCV和ONNX Runtime的安装路径在CMakeLists.txt里配置。我建议把它们都安装在系统的/usr/local下这样CMake的find_package能直接找到不用写一堆绝对路径换机器也方便。cmake_minimum_required(VERSION 3.16) project(yolov10_bytetrack_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(OpenCV REQUIRED) find_package(onnxruntime REQUIRED) include_directories( ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/third_party/ByteTrack ${ONNXRUNTIME_INCLUDE_DIRS} ) add_executable(yolov10_bytetrack_demo src/main.cpp src/detector.cpp src/tracker_wrapper.cpp ) target_link_libraries(yolov10_bytetrack_demo ${OpenCV_LIBS} ${ONNXRUNTIME_LIBRARIES} )3. 核心代码实现与细节解析3.1 检测器封装模型加载与预处理Detector模块是整个工程的基础我把它封装成一个类构造函数传入模型地址内部完成推理Session的创建。class Detector { public: Detector(const std::string model_path, int input_size 640, float conf_thres 0.25) : input_size_(input_size), conf_thres_(conf_thres) { session_ Ort::Session(env_, model_path.c_str(), Ort::SessionOptions{nullptr}); } std::vectorDetection infer(const cv::Mat frame); private: Ort::Env env_{ORT_LOGGING_LEVEL_WARNING, yolov10}; Ort::Session session_{nullptr}; int input_size_; float conf_thres_; };预处理的三步操作是统一的缩放、归一化、转换格式。缩放用的是letterbox方式不直接拉伸图像而是在短边补灰边。这里最关键的一点是记录缩放比例和填充偏移后处理时要把坐标映射回原图否则画出来的框全错位。我习惯写一个结构体保存这些信息struct ScaleInfo { float ratio; int dw; int dh; };letterbox的具体实现可以参考YOLOv5的源码逻辑按目标尺寸和原图尺寸算出缩放比例取最小值再算出填充边距。代码不复杂但非常容易出现索引顺序错误。这个“等比缩放加灰边补齐”的方案最终效果是目标不会被拉伸变形对检测精度影响最小。归一化方面我直接把图像数据从0到255除以255转换成0到1区间的浮点数并设置成NCHW排列。注意ONNX Runtime的输入格式要求就是NCHW颜色通道是RGB。OpenCV读出来的是BGR所以要把通道顺序改一下用cv::cvtColor一行搞定。推理时构造Ort::Run输入是预处理后的std::vectorfloat输出是一个二维数组形状是(batch, num_boxes, 6)。代码上按维度解析后再做一次置信度过滤把得分低于阈值的框先扔掉最后转成ByteTrack需要的Object结构体。后处理里坐标映射是重中之重。ONNX输出的坐标是相对于模型输入图像640×640的所以要按(x - dw) / ratio这样的公式还原到原图坐标。这一行写错了检测框就会整体偏移我调试时在这个问题上花了不少时间。3.2 Tracker封装与ByteTrack的对接ByteTrack官方的C版本提供BYTETracker类核心接口就一个update()方法接收检测结果向量返回轨迹向量。它要求的检测结果结构体是里面的Object类型字段包括tlwh目标框格式是左上角x、y宽度w高度hscore置信度class_id类别IDByteTrack的NMS会在内部按类别信息处理Detector输出的坐标中心点加宽高的格式和ByteTrack需要的tlwh格式需要转换一下。另外ByteTrack内部有NMS逻辑会把重复的框去重这个对模型无明显NMS的YOLOv10来说是好事不会丢掉信息也不会造成计算浪费。Tracker的封装代码如下class TrackerWrapper { public: TrackerWrapper(int fps, int track_buffer 30) : tracker_(fps, track_buffer) {} std::vectorResult update(const std::vectorDetection detections) { std::vectorObject objects; for (const auto det : detections) { Object obj; obj.tlwh Eigen::Vector4f(det.bbox.x, det.bbox.y, det.bbox.width, det.bbox.height); obj.score det.score; obj.class_id det.class_id; objects.push_back(obj); } auto tracks tracker_.update(objects); std::vectorResult results; for (const auto track : tracks) { Result r; r.track_id track-track_id; r.bbox track-tlwh; r.score track-score; r.class_id track-class_id; results.push_back(r); } return results; } private: BYTETracker tracker_; };这里有一个从实际项目里总结的经验一个视频流对应一个Tracker实例多路视频流要创建多个Tracker不要复用同一个实例。追踪器的内部状态卡尔曼滤波协方差、轨迹ID等会随着每帧更新一旦多个视频帧混着喂给同一个Tracker追踪数据就会互相污染ID匹配直接乱掉。另外ByteTrack的update()是可以被直接随处调用的但锁还是要加一层。如果推理是多线程的比如多路视频流同时跑检测Tracker的调用就得有互斥保护。这个在真实项目中很常见提前做好设计会省很多事。3.3 主循环与可视化输出主循环的逻辑比较固定打开视频、逐帧读取、检测、追踪、画框、显示。画框时我根据track_id生成颜色这样每个目标在视觉上的区分度会很高。cv::VideoCapture cap(video_path); cv::Mat frame; while (cap.read(frame)) { auto detections detector.infer(frame); auto results tracker.update(detections); for (const auto r : results) { cv::Scalar color get_color(r.track_id); cv::rectangle(frame, r.bbox, color, 2); cv::putText(frame, ID: std::to_string(r.track_id), cv::Point(r.bbox.x, r.bbox.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.6, color, 2); } cv::imshow(Track, frame); if (cv::waitKey(1) 27) break; }我在实际跑的过程中发现ByteTrack返回的tlwh字段是Eigen向量画框时要转换成cv::Rect或用cv::Rect_float直接接受。如果不想引入太多类型转换你也可以在封装层直接转成cv::Rect这样画框代码会更简洁。显示时的帧率问题也需要留意。如果用OpenCV自带的imshow在低分辨率视频上基本流畅但对1080P视频显示本身也会占一点CPU。如果不需要实时观看想把结果存成视频文件用cv::VideoWriter把每一帧的结果写进去就行不用做额外处理。4. 编译运行与问题排查实录4.1 编译运行的全流程细节工程代码写完后编译过程也踩过一些坑。我用的命令大致是mkdir build cd build cmake .. make -j$(nproc)如果CMake找不到ONNX Runtime多半是安装位置不在系统默认查找路径下。解决办法是在CMakeLists里手动指定ONNXRUNTIME_ROOTset(ONNXRUNTIME_ROOT /your/path/to/onnxruntime) find_path(ONNXRUNTIME_INCLUDE_DIRS onnxruntime_cxx_api.h PATHS ${ONNXRUNTIME_ROOT}/include) find_library(ONNXRUNTIME_LIBRARIES onnxruntime PATHS ${ONNXRUNTIME_ROOT}/lib)运行时要确保libonnxruntime.so在LD_LIBRARY_PATH里。Linux下最稳妥的方式是直接写进.bashrc或者在运行命令前加export LD_LIBRARY_PATH/your/path/lib:$LD_LIBRARY_PATH。忘了这步是最常见的运行时启动报错。Windows下运行稍微麻烦一点onnxruntime.dll要放到可执行文件同目录或者配置到系统PATH环境变量里。另外Windows下调试建议打开ONNX Runtime的日志级别方便排查模型加载阶段的问题。4.2 常见问题与解决方案调试过程中我整理了下面这张问题对照表基本覆盖了这类Demo最常见的出错点问题原因解决办法启动时报Failed to load libraryONNX Runtime动态库路径不对设置LD_LIBRARY_PATH或将dll放入exe同目录推理输出全为0或结果偏移letterbox参数没有同步保存scale和padding值后处理做逆变换追踪ID频繁跳变置信度阈值太低杂框参与匹配适当调高conf_thres比如0.3或0.4检测框重叠但ID混乱ByteTrack的NMS检测不充分检查是否传入class_id是否打开了NMS开关编译报undefined reference未链接ByteTrack源码中的.cpp文件把third_party/ByteTrack下所有.cpp加入工程推理速度很慢模型没有用FP16或INT8优化导出时尝试半精度或使用TensorRT EP输出维度不对导出时opset或动态轴参数有误按官方脚本导出用Netron核对输出形状我再单独说说信度阈值和跟踪的匹配关系。ByteTrack内部用高、低两个阈值分开匹配。conf_thres设得太低会把背景噪声也当作目标低分框参与匹配就容易误关联设得太高低分框匹配阶段直接失去意义漏检会很严重。一个比较实用的调法是先不管追踪单独看检测框是否稳定找一个检测框清晰、不闪不抖的阈值再在这个基础上调跟踪参数。另一个容易被忽视的点是ByteTrack的track_buffer参数。它表示一个轨迹在丢失后最多保留多少帧。在行人场景下30是一个比较稳妥的值如果目标运动快、遮挡时间长可以把这个值调大比如50甚至80。但要提醒的是track_buffer越大ID被占用的时间越长重新出现时容易变成新ID所以并不是越大越好。4.3 性能优化方向代码跑通之后如果想追求更低的延迟可以从两个方向优化。第一是模型精度与速度的取舍。YOLOv10官方提供了N/S/M/L/X等不同规模在CPU上跑的话N模型用ONNX Runtime的CPU线程优化可以跑到30毫秒一帧左右S模型可能要45毫秒。如果你对延迟要求高可以尝试把模型导出成FP16并在ONNX Runtime的session options里开启ORT_ENABLE_ALL优化级别或者用CUDA EP切到GPU推理。第二是预处理和后处理的并行化。如果有多路视频同时推理可以让预处理在一个线程池里并行做把推理放到GPU队列上跟踪结果在主线程里同步画框。这种方法能显著提高多路视频的总吞吐量但代码复杂度会增加适合后续工程化再做。写在最后的一点实操体会整套代码跑下来我自己最大的感受是C部署工程的难点其实不在模型推理本身而在类型转换和状态管理。ONNX Runtime把前向过程包装得很清晰Detector到Tracker再到画框每个环节的数据结构一旦定义清楚后续开发会很顺畅。建议你动手时先定好Detection和Result两个结构体再往里面填数据而不是边写边想。最后再分享一个小技巧调试追踪效果时不要把检测结果和追踪结果画在同一张图上可以开两个窗口分别展示。只画检测框能清楚看到丢检和误检只画追踪框能观察到ID切换的规律这样能准确定位是检测的问题还是跟踪的问题。这个做法在我调试密集行人场景时帮了大忙希望对你有用。本文还有配套的精品资源点击获取