ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#调用TensorRT实现YOLOv8检测与ByteTrack追踪的完整方案

C#调用TensorRT实现YOLOv8检测与ByteTrack追踪的完整方案 简介本资源是一套基于C#实现YOLOv8目标检测与ByteTrack多目标追踪的完整工程方案面向具备.NET开发基础及计算机视觉入门经验的开发者解决在Windows平台高效部署TensorRT加速模型并实现稳定实时追踪的技术难点。压缩包共78个文件包含25个核心C#源码文件、18个关键DLL动态库涵盖TensorRT、OpenCvSharp及自定义封装模块、8个XML配置与文档文件以及C底层封装项目.vcxproj、.h/.cpp和完整VS解决方案.sln整体体积325.26MB结构清晰支持开箱即用。已有814人学习下载配套提供详细测试环境说明Win10VS2019CUDA11.7TensorRT-8.6.1.6OpenCvSharp 4.9.0、可直接运行的演示程序、全部依赖DLL及PDB调试符号文件并附有B站实操视频与CSDN技术博文详解显著降低TensorRT在C#生态中的集成门槛与调试成本。1. 项目背景与方案选型1.1 为什么要在C#里做目标检测与追踪这段时间一直在折腾一个工业视觉项目上位机软件是C#写的整套UI、通信、数据库、流程控制都已经跑得很顺了。但客户那边突然提了一个新需求需要在实时视频流里同时锁定多个目标并且持续跟踪它们的运动轨迹不能只做单帧检测。也就是说每一帧不光要知道“画面里有几个目标、在什么位置”还要知道“下一秒这些目标分别跑到哪里去了”。我第一时间想到的就是接YOLOv8。这个模型的检测精度和速度在工业场景里已经很能打了而且Ultralytics官方生态成熟导出、量化、部署的资料都相当全。但问题也随之而来C#这边要调YOLOv8最直接的方案是用ONNX Runtime但实测下来在客户那台老旧的GTX 1660 Ti上ONNX Runtime的推理延迟大概在26到35毫秒之间视觉上勉强能接受可一旦加上ByteTrack追踪、目标抠图、UI绘制和数据库落盘整条流水线的帧率就开始往下掉。换到TensorRT之后同样的模型在同样的显卡上推理延迟直接降到7到10毫秒性能差距摆在那里没有不换的理由。1.2 为什么不直接用OpenCV DNN或纯ONNX Runtime很多人会问既然都要做目标检测为什么非要绕一圈去搞TensorRT直接用OpenCV的DNN模块加载ONNX模型不就行了我说下我自己的结论OpenCV DNN在CPU上跑YOLOv8确实能用但在GPU上一直存在各种兼容性和性能问题尤其是对TensorRT导出的engine文件OpenCV压根读不了。ONNX Runtime虽然支持GPU加速但底层的TensorRT执行提供程序Execution Provider在Windows上配置起来很折腾而且如果CUDA版本、TensorRT版本有一项对不上这个EP就直接加载失败属于那种“能用的时候很香不能用的时候能折腾你三天”的东西。所以我最终选择了一条更为稳妥的路用C封装TensorRT推理引擎编译成原生DLLC#通过P/Invoke调用。推理引擎的内部是TensorRT检测结果输出之后C#侧自行完成ByteTrack目标追踪。这样一个清晰的分层架构C#和C各干各的活调试起来非常方便后续要替换模型或升级推理引擎也只需要改C那一个DLLC#主程序一行代码都不用动。2. 整体架构与关键依赖2.1 C#与C的跨语言调用链路整个项目的架构其实可以用一条链路来描述摄像头采集画面 - C#侧将Bitmap转为RGB字节数组 - 传入C DLL推理接口 - TensorRT执行推理 - 返回检测框和置信度 - C#侧执行ByteTrack追踪 - 绘制结果并显示。这条链路里最关键的一点是C#和C之间的数据传递要做到零拷贝或尽量少的拷贝否则性能会浪费在不必要的转换上。我这边采用的是C#主动分配非托管内存的方式。在程序启动时通过Marshal.AllocHGlobal申请一块足够大的连续内存之后每一帧的RGB数据直接拷贝到这块内存里C DLL侧通过接收指针的方式读取图像数据。检测结果则是定义了一个结构体数组C侧把检测框坐标、类别ID、置信度写入一个预先分配的数组再通过输出参数返回检测到目标的个数。这样函数调用次数少数据结构也简单整个接口看起来非常清爽。2.2 整个方案涉及的DLL清单这里我想把整个项目涉及的依赖文件整理一份出来方便读者对照自己的环境做检查。因为标题里写着“所有dll文件”我就在这直接摊开讲这个演示项目实际依赖这些文件。依赖文件作用说明nvinfer.dllTensorRT核心库负责engine文件加载和推理nvinfer_plugin.dllTensorRT插件库YOLOv8使用到的插件算子cudart64_12.dllCUDA运行时库显存分配与GPU上下文管理cublas64_12.dll矩阵运算库TensorRT内部调用cudnn64_8.dll深度神经网络库某些层的加速需要my_yolo_infer.dll自封装的C推理DLL对外暴露C风格接口的封装库OpenCvSharp或原生OpenCV的DLL图像预处理辅助库非必需但推荐用于解码和Resize实测下来最小的稳定配置是CUDA 12.2 TensorRT 8.6 cuDNN 8.9。有朋友问过我用CUDA 11.8行不行答案是可以但TensorRT版本要同步降到8.5以下否则会报版本不匹配的运行时错误。另外提醒一句如果你用的是GTX 16系这种图灵架构的显卡TensorRT 8.6是没有任何问题的但如果是更老的Maxwell架构建议降低TensorRT大版本否则某些算子会直接编译失败。2.3 为什么ByteTrack是C#侧实现而不是塞进C DLL这是我在设计时特意做的一个决定。ByteTrack算法本身并不复杂它的核心思想是基于检测框的IoU关联来给目标分配ID按分数高低分成高分框和低分框两批低分框也有机会参与关联。用C实现或者在C#里实现性能差异很小因为追踪这部分大部分是纯CPU逻辑不涉及GPU算子。把ByteTrack放在C#侧有几个实际的好处第一是调试方便C#的Visual Studio调试体验比C好太多追踪出问题时可以直接打断点看数据结构第二是代码可读性好ByteTrack源码大量使用了Java风格的面向对象结构C#版本可以直接参考Java版的实现思路迁移难度极低第三是方便定制追踪逻辑工业项目里经常要加ROI限制、目标消失判定、滞留时间统计之类的规则这些逻辑放在C#侧开发效率高很多。3. 核心技术拆解从推理到追踪3.1 YOLOv8模型导出与TensorRT engine生成我把这个过程的细节完整走一遍。首先你需要有一个训练好的YOLOv8模型官方权重是.pt格式第一步是导出成ONNX。YOLOv8官方仓库已经提供了export脚本但直接用官方默认参数导出的ONNX模型在TensorRT里可能存在推理结果不对的问题原因在于NMS算子被包含进了计算图。我这里推荐两个方案第一种是导出时不要包含NMS在C推理代码里自己实现NMS第二种是导出时带NMS但必须注意TensorRT的EfficientNMS插件兼容性。我实际采用的是第一种因为自行实现NMS逻辑清晰调试可控性能损耗可以忽略不计。用ultralytics的API导出命令如下from ultralytics import YOLO model YOLO(yolov8s.pt) model.export(formatonnx, imgsz640, halfTrue, simplifyTrue, opset12)导出ONNX之后TensorRT需要把ONNX解析成engine文件。在Windows命令行下可以直接用trtexec来转换这是TensorRT自带的工具用法为trtexec --onnxyolov8s.onnx --saveEngineyolov8s.engine --fp16这里有个很关键的点fp16半精度模式。GTX 1660 Ti虽然对FP16的支持不如RTX系列那么夸张但依然有可观的加速效果实测推理延迟从FP32的14毫秒降到了FP16的8毫秒左右。如果你的显卡是RTX 20系以上FP16加速会更明显。另外--fp16模式下如果出现个别算子精度异常表现为检测框偏移或者漏检可以把那一层单独保留FP32精度但实际我这边跑了YOLOv8s模型没有遇到这种情况。3.2 TensorRT推理DLL的封装要点这一节我详细说说自封装推理DLL的过程。整个C DLL的职责有三个接收图像数据、运行TensorRT推理、返回检测结果。类的内部会保存engine文件路径、推理上下文context、输入输出buffer的显存指针、以及每一帧的CUDA流。初始化阶段做的事情比较多包括读取engine文件、创建runtime和context、根据输入输出的维度分配显存、创建CUDA流。这里要注意TensorRT 8.6及以上版本里IExecutionContext的推理接口推荐使用enqueueV3相比旧版enqueueV2在显存管理和异步执行上有明显改进。代码结构大致是这样的extern C __declspec(dllexport) void* YoloInit(const char* engine_path) { YoloTensorRT* yolo new YoloTensorRT(); if (!yolo-init(engine_path)) { delete yolo; return nullptr; } return yolo; } extern C __declspec(dllexport) int YoloInfer(void* handler, unsigned char* rgb_data, int width, int height, DetectResult* results, int max_results) { YoloTensorRT* yolo (YoloTensorRT*)handler; return yolo-infer(rgb_data, width, height, results, max_results); }推理函数内部的流程是将CPU上的RGB数据拷贝到锁页内存pinned memory然后通过cudaMemcpyAsync异步拷贝到显存执行推理再把显存结果异步拷贝回CPU。因为YT系列显卡的PCIe带宽有限如果不使用异步拷贝每次推理会有约2到3毫秒的等待开销这个优化不要省。预处理方面YOLOv8需要将输入图像缩放到640x640然后除以255归一化并进行RGB通道格式转换。这些操作在GPU上完成最高效我使用的是PyTorch的TensorRT教程里常见的预处理kernel在缩放的同时完成归一化。但第一版实现我图省事在CPU做了预处理结果推理延迟没变但整帧处理时间增加了4毫秒后来改成GPU预处理才把完整流水线的帧率打上去。3.3 ByteTrack算法的C#移植思路ByteTrack算法的核心是数据关联。每一帧检测之后首先用高置信度的检测框根据IoU去匹配已有的轨迹集合匹配成功的轨迹更新位置信息匹配不上的高置信度检测框被认为是新目标或新进入画面的目标低置信度的检测框再匹配一轮主要为了找回被遮挡或短暂模糊的目标剩余的未匹配轨迹如果连续多帧没被更新就判定为目标离开释放对应ID。为了让代码结构清晰我定义了三个核心类Tracklet代表一条运动轨迹包含目标ID、历史框位置、速度和存活帧数TrackState定义了轨迹的状态有New、Tracked、Lost三种ByteTracker则是总控类负责每一帧的关联匹配。实际C#代码量差不多600行左右不长但逻辑必须理清楚。在匹配阶段我使用了匈牙利算法求最大IoU匹配。C#里我没有引入额外的NuGet库而是自己实现了一个静态的匈牙利算法类矩阵大小取决于当前帧检测目标数和活跃轨迹数。实测一个画面10个追踪目标、20条活跃轨迹的情况下整个关联过程耗时不到1毫秒性能完全不用担心。这里的核心代码可以这样写public void Update(ListDetectBox detections) { var highScore detections.Where(d d.Confidence 0.5).ToList(); var lowScore detections.Where(d d.Confidence 0.5 d.Confidence 0.1).ToList(); // 第一步高置信度框匹配已有轨迹 var matched MatchTracks(highScore, _activeTracks); // 第二步更新匹配上的轨迹未被匹配的高置信度框创建新轨迹 // 第三步低置信度框与剩余轨迹二次匹配 // 第四步清理连续丢失超过阈值的轨迹 }4. 演示项目的搭建与运行4.1 环境准备与版本对照在开始搭建之前先把自己的运行环境理清楚。我这边开发机是Windows 10 22H2Visual Studio 2022显卡GTX 1660 Ti。客户那边有两台机器是Windows 7这里要注意一下TensorRT 8.6官方已经不再支持Windows 7如果你想在Windows 7上跑完整方案需要回退到TensorRT 8.2 CUDA 11.4的组合并且DLL里的VC运行时也要用旧版。如果说得直接一点环境版本对照表是决策的重要依据组件推荐版本兼容性说明Windows10/11 或 Server 2019Win7仅支持老版本TensorRTVisual Studio2022兼容.NET Framework 4.8.NET4.8或.NET 6/8推荐Framework部署更省事CUDA12.2向下不兼容需要做版本矩阵确认cuDNN8.9必须与CUDA版本严格匹配TensorRT8.6当前最稳定的Windows版本YOLOv8权重yolov8s.pt也支持n/m/l/x系列4.2 项目结构说明整个演示项目的目录结构我放在这里方便读者对照理解YoloByteTrackDemo/ ├── CppInference/ │ ├── include/ │ │ ├── yolo_infer.h │ │ ├── common.h │ │ └── cuda_utils.h │ ├── src/ │ │ ├── yolo_infer.cpp │ │ ├── preprocess.cu │ │ └── postprocess.cpp │ └── CMakeLists.txt ├── CSharpDemo/ │ ├── Models/ │ │ ├── DetectBox.cs │ │ ├── Tracklet.cs │ │ └── ByteTracker.cs │ ├── Inference/ │ │ ├── YoloNative.cs │ │ └── YoloWrapper.cs │ ├── Utils/ │ │ ├── CameraHelper.cs │ │ └── DrawingHelper.cs │ ├── MainForm.cs │ └── Program.cs └── models/ ├── yolov8s.engine └── labels.txt这个结构里CSharpDemo是主程序CppInference是C侧的原生DLL工程。C#侧的YoloNative.cs负责声明DLL里的导出函数YoloWrapper是一个封装类把底层的IntPtr句柄、结构体转换、异常处理都封装成友好的C#调用方式避免MainForm里充斥着Marshal相关代码。4.3 从摄像头到追踪显示的完整线程流水线很多人拿到类似的演示源码后容易犯一个错把推理和绘制全部放在UI线程里跑。UI线程一旦被阻塞画面就会卡顿拖动窗口时明显掉帧。正确的做法是用三个线程采集线程负责读摄像头帧推理线程负责处理TensorRT推理加追踪UI线程仅负责把结果绘制到PictureBox上。采集线程和推理线程之间用生产者-消费者模式组织这里我用了C#的BlockingCollection 它天然支持阻塞和线程安全的队列操作。推理线程处理完一帧后把结果对象检测框列表追踪ID列表经过缩放处理的Bitmap放进另一个队列UI线程通过定时器取队列里的结果来绘制。队列的长度要控制比如固定为3帧超过就丢弃最早未处理的帧这样视频显示永远是实时的不用等待积压的清空。_blockQueue new BlockingCollectionFrameData(3); // 采集线程 Task.Run(() { while (_capturing) { using (var frame _camera.QueryFrame()) { var data new FrameData { Bitmap (Bitmap)frame.Clone(), Timestamp DateTime.Now }; _blockQueue.TryAdd(data, 10); } } }); // 推理线程 Task.Run(() { foreach (var frame in _blockQueue.GetConsumingEnumerable()) { var result _yoloWrapper.Detect(frame.Bitmap); var tracks _byteTracker.Update(result.Boxes); _resultQueue.Add(new FrameResult(frame.Bitmap, tracks)); } });整条流水线在这个模型下运行非常稳定。170万像素分辨率的USB摄像头采集到640x360的检测分辨率加上TensorRT推理8毫秒和ByteTrack追踪1毫秒整体帧率可以稳定在25帧以上。如果摄像头直接输出1920x1080则需要额外对图像做一次缩放缩放操作放在采集线程里提前做掉不要拖到推理线程里。5. 常见问题与排查实录5.1 DLL加载失败和版本冲突问题标题里写了“所有dll文件”但实际拿到的项目往往还是会遇到DLL加载失败的情况。最常见的异常是“无法加载DLL…找不到指定的模块”或者“未能加载文件或程序集”。前者通常意味着C运行库缺失后者往往是依赖的Native DLL没有复制到程序输出目录。这里有一个排查思路非常管用先用Dependencies工具打开你的C推理DLL看它依赖的每一层DLL是否都能找到很多时候问题并不出在TensorRT本身而是CUDA的小版本不匹配。举例来说客户的一台机器上装了CUDA 12.0但我们的程序是CUDA 12.2编译的运行时直接报cudart64_12.dll找不到因为CUDA运行时DLL的版本没有向上兼容。解决方法有三种打包时带上对应版本的cudart或者让客户安装匹配的CUDA运行时或者在一开始就使用运行时加载的方式从指定目录动态加载依赖DLL。实际项目中我倾向于第一种直接把用到的运行时DLL全部放在exe同目录下简单省心。5.2 TensorRT推理结果偏移或检测框错位这种情况我遇过两次。一次是由于预处理时图像通道顺序颠倒了TensorRT推理出来的物体明明在那里但坐标全部镜像错位。另一次是图像缩放时没有保持宽高比用直接拉伸的方式把1920x1080的图拉到640x640检测框位置在回映射时超出了实际目标范围。YOLOv8的onnx导出默认输入是640x640正方形但工业现场的视频流通常是16:9或4:3不做等比缩放会导致变形。解决方案是使用letterbox缩放。具体做法把原始图像等比缩放到640x640的黑色填充画布上缩放比例取原始宽高和640比例中较小的一个然后在另一侧填充灰色通常填充114这是YOLO系列代码里的默认值并记录原始图像在填充后画布上的偏移量。推理结束后把检测框坐标减去偏移量再除以缩放比例换算回原始图像坐标。这块代码务必在C DLL里完成不要把偏移量的计算交给C#否则每个环节都要记住自己有没有处理偏移非常容易出错。5.3 追踪ID频繁跳变ByteTrack本身在遮挡场景下已经比SORT要好很多但偶尔还是会出现ID切换的情况。我排查后发现问题不在ByteTrack而在于检测器对同一个目标输出不稳定。比如一辆车在画面边缘前后两帧的置信度一会儿0.52一会儿0.46导致高置信度框和低置信度框的分类结果在两帧之间发生变化目标ID就跟着丢了。解决思路有两个方向。第一放宽置信度阈值把高置信度的判定从0.5降到0.35低置信度降为0.1让更多质量略低的检测框参与关联第二在ByteTrack的匹配阶段增加卡尔曼滤波预测的辅助。我在C#的Tracklet类里加了简单的速度估计用上一帧和这一帧的位置差预测下一帧位置再计算预测框和检测框的IoU匹配准确率提升明显ID跳变的频率降低了很多。5.4 GTX 1660 Ti上帧率达不到预期有很多朋友拿着GTX 1660 Ti跑YOLOv8模型发现TensorRT推理延迟没有网上说的那么低。我分析下来原因一般出在几个地方一是没有开启FP16模式FP32推理速度天然慢一半二是图像缩放预处理跑在CPU上每次4到5毫秒比GPU推理本身还耗时三是没有使用batch size1下的多流优化虽然单帧模式下多流影响不大但错误配置cudaStream会导致同步等待。实测数据在我的GTX 1660 Ti上YOLOv8s模型640x640输入FP32约14msFP16约8msFP16配合GPU预处理约6ms如果开启TensorRT的多流模式可以进一步压缩到5.5ms左右。如果你测得数值差距比这个大可以直接怀疑有没有正确加载engine文件有时候由于DLL路径写错TensorRT没读到engine而是走了一遍在线build流程这就会让耗时暴增到几十毫秒甚至上百毫秒。6. 使用心得与几个提高性能的细节6.1 为什么我建议在Windows下用TensorRT 8.6而不是9.x如果你的需求不依赖新模型里的新算子TensorRT 8.6目前是Windows平台最稳的版本。TensorRT 9.x之后调整了plugin API不少第三方模型和工具链还没有跟上而且Windows下的官方预编译包版本选择也变少了。当然如果你用到的模型包含一些需要新特性的算子那就用新版本但不能盲目追新。我的习惯是每个模型先做一次benchmark对比再决定选哪个TensorRT版本。这个方案的主题就是“演示源码全部源码所有dll文件”本质上是要让读者少走弯路。所以我把运行时版本也整理进了一个配置文件后续更换机器时只需核对环境版本就能快速定位加载问题。这种做法在接手不熟悉的项目时特别实用。6.2 让C#调用层更稳的几个细节C#和C互操作的场景里最容易出问题的点往往非常细微。我平时写这类代码时会特别注意几个点第一C#侧声明的结构体布局要和C侧严格对应包括字段顺序、字节对齐必要时使用[StructLayout(LayoutKind.Sequential, Pack 1)]第二回调或批量数据返回时不要用C#去分配一个托管数组传给C而是用Marshal.AllocHGlobal分配非托管内存用完之后再释放这样可以避免GC把内存移动导致指针失效第三DLL入口处要加extern C和__declspec(dllexport)并尽量避免使用C的异常抛出而是在接口边界上返回错误码。举一个真实踩过的坑第一版YoloInfer函数里我直接传了一个C#的byte[]数组给C侧当时跑起来没问题但运行到第10分钟左右就会随机崩溃。排查后发现是GC把大对象堆里的字节数组压缩移动了C侧拿到的指针指向了已经失效的内存。改成AllocHGlobal固定内存后问题彻底消失。这就是为什么数据跨语言边界时一定要用非托管内存这也是我不会在代码注释里跳过的关键点。6.3 这个项目的后续扩展方向这套底座已经打通了C# TensorRT ByteTrack的完整链路后续可以做的事情相当多。比如在ByteTrack的关联结果上增加业务规则统计每个ID在指定区域内停留的时长用于商场客流分析或工业安防也可以把检测类别扩展到YOLOv8分割模型从目标框升级到像素级掩码还可以把追踪结果输出为结构化JSON推到消息队列让其他端侧或上层平台消费。如果你需要把模型换成自己的特定类别只需要重新训练YOLOv8并导出为engineC#侧的代码完全不用动。因为检测结果的类别ID和置信度都是通过结构体传回来的类别名称只是通过labels.txt映射显示。这也是为什么我把labels.txt单独放在models目录下的原因替换模型时只要把txt文件内容同步修改即可。6.4 最后再分享一个实际运行的小技巧如果你在部署到客户机器的时候不想带一堆CUDA和cuDNN安装包可以用瘦身方案。TensorRT在运行时其实只依赖几个核心DLL没有必要装完整版CUDA Toolkit。你只需要把cudart、cublas和cudnn对应的DLL复制到exe目录配合TensorRT的DLL就能跑起来。但这里有一个隐藏条件显卡驱动版本必须足够新因为CUDA Runtime是向前兼容驱动版本的驱动太老会报错。用GPU-Z或nvidia-smi确认一下驱动支持的最高CUDA版本如果低于12.0就还是安装对应版本的CUDA运行时吧。我自己在实际操作中大改过几次这套流水线从一开始的纯ONNX方案到后来切到TensorRT再到现在加上ByteTrack每一步都能感觉到性能和处理效果在提升。C#做上位机有天然的优势加上C深度学习推理引擎双方取长补短整套系统既能快速迭代业务逻辑又能在推理性能上不掉链子。如果你们也想在C#项目里落地目标追踪这套模式可以直接参考从源码编译到效果调优大概一天时间就能跑通。本文还有配套的精品资源点击获取
返回列表