尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#中部署YOLOv8:OpenVINO与TensorRT完整实战

C#中部署YOLOv8:OpenVINO与TensorRT完整实战 简介面向需要在C#环境中部署YOLOv8的开发者资源围绕OpenVINO与TensorRT两大推理平台展开解决目标检测模型从训练到实际业务系统集成时的跨语言调用与性能优化问题。压缩包共63个文件约3.01MB以C#工程文件为主包含19个.cs源码、5个.csproj项目文件以及cpp/h原生扩展另有md文档、txt说明、jpg测试图片和sln解决方案分别对应核心推理逻辑、平台封装、使用指南与调试素材。资源内含针对两个平台的详细Markdown文档从模型下载、格式转换到推理结果处理均有逐步说明配合源码可快速跑通端到端流程。目前已有420人学习下载。通过该包可拿到完整的C#部署工程架构包括OpenVINO与TensorRT的Sharp封装、结果后处理流程、模型下载与转换说明以及10张测试图片便于快速验证与二次开发。适合具备一定C#基础、希望将YOLOv8落地到实际项目中的中高级开发者。1. 用C#部署YOLOv8不是调用Python先看清两个推理引擎的边界我接手过一个产线项目上位机是C#写的PLC通讯、MES上报和摄像头管理都在同一个进程里但目标检测却单独用Python脚本起进程。数据来回拷贝不说模型一更新还得两个人协同上线。后来我把YOLOv8的推理直接收进C#进程用OpenVINO在CPU上跑又把同一套模型转到TensorRT在NVIDIA GPU上跑才算把这件事做顺。这就是标题要解决的问题基于C#在OpenVINO和TensorRT两个平台部署YOLOv8让检测能力变成C#程序里的一个函数而不是一个黑盒Python进程。OpenVINO适合Intel CPU、核显和无NVIDIA显卡的工控机TensorRT适合有NVIDIA GPU且在意延迟的场景。读者应该是做C#上位机、视觉集成或边缘设备的开发手上已经有YOLOv8权重想把模型真正用起来。2. ONNX 是两者的共同底座先把 pt 导出成中间格式2.1 为什么 OpenVINO 和 TensorRT 都从 ONNX 开始OpenVINO 的原生模型格式是 IRXML binTensorRT 的原生格式是 engine两者的编译工具链不同但它们都接受 ONNX 作为输入。所以第一步不是去研究两套转换工具而是先拿到一个正确的 ONNX 文件。这个文件既是 OpenVINO 的输入也是 TensorRT 的输入后续所有环节都围绕它展开。常见做法是用 ultralytics 官方提供的导出命令而不是自己从 PyTorch 权重里手写 torch.onnx.export。因为 YOLOv8 的网络结构里包含了 C2f、DFL 这些自定义算子手写导出很容易在节点融合时出错。官方导出脚本已经把输入输出名、动态轴、以及最后的解码输出都处理好了踩坑最少。2.2 导出命令与三个关键参数yolo export modelyolov8s.pt formatonnx dynamicTrue opset12这条命令会把 yolov8s.pt 导出成 yolov8s.onnx。dynamicTrue让模型的宽高轴变成动态维度这样同一个 ONNX 文件既可以被 OpenVINO 用 640x640 推理也可以被 TensorRT 配置成多个分辨率。opset12是兼容性很稳的选择如果你用的 TensorRT 版本较新可以换成opset17但要确认 TensorRT 的解析器支持到这个版本。导出完成后确认目录下多出了 yolov8s.onnx。如果之前训练过自己的数据集命令里的model换成你的训练权重路径即可数据集标注和训练环节不影响后面转换。2.3 看懂输出张量的形状84 和 8400 从哪来用 Python 快速看一眼这个 ONNX 的输入输出import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov8s.onnx, providers[CPUExecutionProvider]) for inp in sess.get_inputs(): print(input:, inp.name, inp.shape) for out in sess.get_outputs(): print(output:, out.name, out.shape)input: images [1, 3, 640, 640] output: output0 [1, 84, 8400]84来自4 80前 4 个是边界框的 cx、cy、w、h注意是归一化坐标后 80 个是 COCO 类别得分。8400是 YOLOv8 三个尺度特征图拼接后的候选框总数等于 80x80 40x40 20x20。这个信息后续写 C# 后处理时直接决定数组怎么切。2.4 导完先验证在 Python 里跑一次再进 C#直接跳到 C# 里调试如果出问题很难分辨是模型的问题还是绑定层的问题。先在 Python 里用 ONNX Runtime 跑一张图做基准import onnxruntime as ort import numpy as np import cv2 sess ort.InferenceSession(yolov8s.onnx, providers[CPUExecutionProvider]) img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].astype(np.float32) / 255.0 # BGR 转 RGB归一化 img img.transpose(2, 0, 1)[None] out sess.run(None, {images: img})[0] print(out.shape)img[:, :, ::-1]是把 OpenCV 读出的 BGR 转成 RGBYOLOv8 训练时用的是 RGB 顺序这一步错了后面检测结果会很怪。transpose(2, 0, 1)把 HWC 转成 CHW。如果输出形状是(1, 84, 8400)说明模型管道已经通了接下来继续写 C# 侧。3. 在 OpenVINO 上落地用 ONNX Runtime 的 OpenVINO 执行提供程序3.1 为什么不开箱即用 OpenVINO 原生 C# APIOpenVINO 官方提供的是 C/C APIC# 没有一等公民支持。网上能找到一些封装好的 C# 包但大多是把 C API 再做一层 P/Invoke接口设计参差不齐更新也未必跟上 OpenVINO Runtime 的版本。常见做法是换一条路用 ONNX Runtime 的 OpenVINO 执行提供程序Execution Provider。模型文件仍是同一个 ONNX计算后端换成了 OpenVINOC# 侧只需要调用 ONNX Runtime 的 API。这样推理算子由 OpenVINO 接管C# 代码量最少也不存在手写 P/Invoke 导致的内存崩溃问题。如果你确实需要更底层的 OpenVINO 网络结构操作比如直接在 IR 上改输入输出再用 C 封装一层也不迟。3.2 NuGet 引包与会话创建dotnet add package Microsoft.ML.OnnxRuntime.OpenVINO打开你的 C# 项目在 NuGet 里搜Microsoft.ML.OnnxRuntime.OpenVINO安装后项目会自动带上 OpenVINO 的本地依赖。注意不要同时安装Microsoft.ML.OnnxRuntime和Microsoft.ML.OnnxRuntime.OpenVINO否则执行提供程序列表会互相覆盖报奇怪的初始化错误。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; // CPU 推理适合普通工控机和不带独显的环境 var opts new SessionOptions(); opts.AppendExecutionProvider_OpenVINO(CPU); // 如果目标机器有 Intel 核显可改成 GPU.0输出张量会自动拷贝回内存 // opts.AppendExecutionProvider_OpenVINO(GPU.0); using var session new InferenceSession(yolov8s.onnx, opts); // 打印输入名和输入维度方便后面构造输入张量 var inputMeta session.InputMetadata.First(); Console.WriteLine($input: {inputMeta.Key} {string.Join(,, inputMeta.Value.Dimensions)});AppendExecutionProvider_OpenVINO(CPU)的关键是传入的 device 字符串CPU最省事GPU.0对应 Intel 核显设备编号。我一般先在 CPU 上跑通再切 GPU因为 GPU 插件对驱动和 OpenCL 运行时的要求更苛刻。session.Run之前收到的输入名必须和导出时一致多数情况下是images。3.3 预处理letterbox 归一化的正确顺序深度学习模型的输入是固定 640x640但摄像头或图片几乎不会是正方形。直接 Resize 拉伸会改变物体的宽高比导致检测框偏移。要用 letterbox按比例缩放图片到一边贴合 640另一边补灰边。private static float[] Preprocess(byte[] bgr, int srcW, int srcH, int dst 640, out float scale, out int padX, out int padY) { scale Math.Min((float)dst / srcW, (float)dst / srcH); int nw (int)(srcW * scale); int nh (int)(srcH * scale); padX (dst - nw) / 2; padY (dst - nh) / 2; // 用双线性插值把 BGR 图缩放到 (nh, nw) var resized new byte[nh * nw * 3]; // ResizeBilinear(bgr, srcW, srcH, resized, nw, nh); // 生成 1x3x640x640 的 float 张量BGR - RGB var tensor new float[1 * 3 * dst * dst]; for (int y 0; y nh; y) for (int x 0; x nw; x) { int srcIdx y * nw * 3 x * 3; int dstIdx (y padY) * dst * 3 (x padX) * 3; tensor[dstIdx 0] resized[srcIdx 2] / 255f; // R tensor[dstIdx 1] resized[srcIdx 1] / 255f; // G tensor[dstIdx 2] resized[srcIdx 0] / 255f; // B } return tensor; }注意注释里我把 ResizeBilinear 留成了占位实际工程里可以用 OpenCVSharp 的Cv2.Resize也可以手写一个双线性插值。手写时别忘了这一步做的是BGR - RGBOpenCV 读出来是 BGRYOLOv8 训练时用的是 RGB。很多第一次部署的人会在这里漏掉结果是模型输出置信度一直很低。scale、padX、padY三个值后面反过来解析坐标时要用。3.4 推理与后处理从 8400 个候选框里筛出目标var inputName session.InputMetadata.First().Key; var inputTensor new DenseTensorfloat(tensor, new[] { 1, 3, 640, 640 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensorfloat(inputName, inputTensor) }; using var results session.Run(inputs); var output results.First().AsEnumerablefloat().ToArray(); // output 长度为 1 * 84 * 8400按输出语义切分output是个一维数组长度 80640084 x 8400。它的内存布局是 [1, 84, 8400]也就是先把 84 个通道的某一列排完再排下一列。遍历 8400 个候选框时访问下标是row * 8400 colconst int numClasses 80; const int numBoxes 8400; var boxes new ListDetectResult(); for (int col 0; col numBoxes; col) { float score 0; int classId -1; for (int c 0; c numClasses; c) { float s output[(4 c) * numBoxes col]; if (s score) { score s; classId c; } } if (score 0.25f) continue; float cx output[0 * numBoxes col]; float cy output[1 * numBoxes col]; float w output[2 * numBoxes col]; float h output[3 * numBoxes col]; // 还原到原图坐标 float x1 (cx - w / 2f - padX) / scale; float y1 (cy - h / 2f - padY) / scale; float x2 (cx w / 2f - padX) / scale; float y2 (cy h / 2f - padY) / scale; boxes.Add(new DetectResult(x1, y1, x2, y2, score, classId)); }置信度阈值 0.25 是检测任务里比较通用的值如果漏检多可以降到 0.2误检多就抬到 0.4。最后再做一次 NMS把重叠的框去掉C# 里可以用System.Numerics加速 Intersection over Union 的计算。OpenVINO 执行提供程序跑 YOLOv8s 在 i5 处理器的 CPU 上640x640 单帧推理一般在 40 到 80 毫秒之间如果超过 150 毫秒先检查是不是 openmp 线程数没有设对。4. 在 TensorRT 上落地C 封装推理 DLLC# 通过 P/Invoke 调用4.1 TensorRT 全家桶里没有官方 C# 绑定TensorRT 的 API 是 CNVIDIA 没有提供官方 C# 包装。C# 程序里直接调 TensorRT 不现实常见做法是写一个 C 动态库把模型的加载、推理、输出解析封装好导出几个extern C函数再用 C# 的 DllImport 去调用。整个链路是 C# - P/Invoke - C DLL - TensorRT Runtime。C DLL 是这一层唯一能直接接触 TensorRT 的代码。这个过程中最容易翻车的是内存边界C 申请的内存由谁释放、float 数组按什么布局传递都必须在一开始定死。我在下面的方案里统一约定C# 负责申请输入 float 数组和输出 float 数组C 只读写不持有这样避免跨语言释放内存。4.2 从 pt 到 engine 的转换动作# step 1: 导出 ONNX和 OpenVINO 那一份是同一个文件 yolo export modelyolov8s.pt formatonnx dynamicTrue opset17 # step 2: 用 trtexec 构建 engine并序列化到磁盘 trtexec --onnxyolov8s.onnx \ --saveEngineyolov8s.engine \ --fp16 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:1x3x640x640trtexec是 TensorRT 自带的命令行工具。--fp16是半精度推理YOLOv8s 量化成 FP16 精度损失很小但推理速度能提升一倍左右。--minShapes、--optShapes、--maxShapes三件套是给动态 batch 和动态分辨率用的我通常把分辨率固定 640只留 batch 维动态这样既缩小了引擎体积也避免了 C 侧每次换 shape 都要重新查 profile 的麻烦。如果你的显卡不支持 FP16去掉--fp16即可。如果trtexec直接报错说该 GPU 不支持你用的 TensorRT 版本参看第五章的避坑说明。engine 文件是二进制缓存构建一次之后下次加载不用重新编译部署时把yolov8s.engine和 DLL 一起拷到目标机器即可。4.3 C 侧最小推理 DLLextern C 导出三个函数#include NvInfer.h #include cuda_runtime_api.h using namespace nvinfer1; struct YoloRuntime { IRuntime* runtime nullptr; ICudaEngine* engine nullptr; IExecutionContext* context nullptr; void* buffers[2] { nullptr, nullptr }; int inputSize 0, outputSize 0; }; extern C { __declspec(dllexport) void* YoloInit(const char* enginePath) { auto* rt new YoloRuntime(); rt-runtime createInferRuntime(gLogger); rt-engine rt-runtime-deserializeCudaEngineFromFile(enginePath); rt-context rt-engine-createExecutionContext(); // 用 fixed shape 简化输入 1x3x640x640输出 1x84x8400 rt-inputSize 1 * 3 * 640 * 640; rt-outputSize 1 * 84 * 8400; cudaMalloc(rt-buffers[0], rt-inputSize * sizeof(float)); cudaMalloc(rt-buffers[1], rt-outputSize * sizeof(float)); return rt; } __declspec(dllexport) void YoloInfer(void* handle, const float* input, float* output) { auto* rt static_castYoloRuntime*(handle); cudaMemcpy(rt-buffers[0], input, rt-inputSize * sizeof(float), cudaMemcpyHostToDevice); rt-context-enqueueV2(rt-buffers, 0, nullptr); cudaMemcpy(output, rt-buffers[1], rt-outputSize * sizeof(float), cudaMemcpyDeviceToHost); } __declspec(dllexport) void YoloRelease(void* handle) { auto* rt static_castYoloRuntime*(handle); cudaFree(rt-buffers[0]); cudaFree(rt-buffers[1]); delete rt-context; delete rt-engine; delete rt-runtime; delete rt; } }YoloInit负责反序列化 engine、创建执行上下文、申请显存缓冲。YoloInfer做三轮拷贝输入从内存到显存执行推理输出从显存拷回内存。这个版本省略了 CUDA Stream实际生产里应该用多 stream 做异步至少把输入拷贝和计算重叠起来。enqueueV2在 TensorRT 10.x 里改叫enqueueV3接口变化不大但如果你用 10.x上下文里要额外绑定 CUDA event。C DLL 里有个很容易忽略的细节YoloInfer的 input 和 output 都是裸指针C# 侧必须保证数组内存固定住GC 移动托管堆内存会让指针瞬间失效表现就是随机野指针崩溃这正好是第五章第一个坑的根源。4.4 C# 侧 DllImport调用约定与传参布局using System.Runtime.InteropServices; internal static class YoloNative { private const string DllName YoloNative.dll; [DllImport(DllName, CallingConvention CallingConvention.Cdecl)] internal static extern IntPtr YoloInit(string enginePath); [DllImport(DllName, CallingConvention CallingConvention.Cdecl)] internal static extern void YoloInfer(IntPtr handle, [In] float[] input, [Out] float[] output); [DllImport(DllName, CallingConvention CallingConvention.Cdecl)] internal static extern void YoloRelease(IntPtr handle); }调用侧直接用 float[] 传参P/Invoke 默认会把数组 pin 住编组为指针。但[In]和[Out]必须显式写C# 侧默认按 input 处理而 YOLOv8 的输出是 84x8400 的 float 数组数组元素个数要在 C# 里提前声明准确IntPtr h YoloNative.YoloInit(yolov8s.engine); float[] input GetPreprocessedTensor(); // 1*3*640*640 长度的 float 数组 float[] output new float[1 * 84 * 8400]; // 长度必须和 C 约定一致 YoloNative.YoloInfer(h, input, output); // 到这里 output 里的布局和 OpenVINO 章节的后处理完全一致 YoloNative.YoloRelease(h);CallingConvention.Cdecl必须和 C 导出的函数保持一致默认是 Winapi 的 stdcall不一致时第一个参数会被当成返回值的高位轻则参数错乱重则直接 AccessViolation。YoloRelease必须在程序退出前调用否则显存不释放进程一结束 GPU 显存才回收。如果引擎文件在别的机器构建先确认两台机器的 GPU 架构尽量一致TensorRT 没有干过跨架构兼容的事。5. 部署避坑从崩溃到精度漂移的五个真实案例5.1 C# 调用 C 出现 Access Violation 0000005现象第一次调用 DLL 里的函数就直接崩异常报access violation c0000005堆栈里看不到有效信息Visual Studio 只提示“对 PInvoke 函数调用导致堆栈不对称”。原因最常见的有三个。一是调用约定不一致C 里用的__cdeclC# 侧没加CallingConvention.Cdecl二是结构体或数组长度不匹配C 读到了越界内存三是回调函数持有 C# 委托但没做 GCHandle 固定GC 回收后委托地址变成悬空指针。解决先统一调用约定DllImport 显式声明CallingConvention.Cdecl。再把所有跨语言的数据交换收窄成纯 float 数组加长度参数避免传 struct。最后给 C 的每个函数入口加上尺寸校验比如输出数组长度不等于1*84*8400就直接返回错误码不要天真地相信调用方。5.2 TensorRT 10.x 不支持 GTX 1070 这类老架构现象trtexec 构建 engine 时报错说 GPU 架构 sm_61 不支持或者运行时 deserialize engine 失败。原因TensorRT 9 之后官方逐步移除了对 Maxwell 和 Pascal 架构的支持GTX 1070 是 Pascal而 10.x 的官方架构列表里只保留了 Turing、Ampere、Ada、Hopper、Blackwell 这些较新架构。解决要么降级到 TensorRT 8.6老版本对 Pascal 的算子覆盖还算完整要么说服业务方换一块 20 系列以上的卡。如果你只是做 CPU 推理那根本不用碰 TensorRTOpenVINO 在老机器上是更省心的方案。部署前先查目标链路的架构不要拿到新引擎就往旧卡上扔。5.3 预处理的两处不对称BGR/RGB 和 letterbox现象OpenVINO 的 CPU 推理结果很怪有的类别检不出有的类别重复识别置信度普遍低于 0.3。原因OpenVINO 章节代码里写了RGB顺序但你在验证时如果直接用 OpenCV 读的 BGR 图喂进去模型的输入分布和训练时差了一整个通道顺序让特征提取从一开始就错位。另一个问题是 letterbox 的 pad 计算没对齐你训练或验证时用的是默认灰色填充 (114,114,114)但预处理里用了纯黑 0也会让检测精度下降。解决预处理函数里固定写死BGR - RGB的转换不要依赖外部参数。letterbox 的填充值用常量 114不要用new byte[3]默认的 0。写一个自检脚本把一张纯色图片分别用 Python 跑和 C# 跑对输出做全部对比两个输出数组差超过 1e-5 就说明预处理还有分叉。5.4 首次推理卡死三十秒初始化没和推理分开现象程序一启动点击按钮触发第一次推理界面卡住十几秒之后又恢复正常。测出来的平均延迟虚高领导在演示现场看到转圈。原因OpenVINO 首次加载模型要创建子图并做算子融合TensorRT 首次反序列化 engine 要准备 CUDA context这些初始化工作往往耗掉几十秒。你把它放在了点击事件的处理函数里UI 线程被占住整个窗口自然假死。解决把初始化放到后台线程启动时就开始加载模型加载完再置一个_ready标志。推理请求来了之后如果_ready false就先弹“正在加载模型”不要阻塞 UI。用Task.Run(() { _session CreateSession(); _ready true; })是最短路径的解法。5.5 动态分辨率的相反面TensorRT 反复查 profile现象C# 里用 640x640 跑完第一帧第二帧换成了 1280x720报告直接报错提示 binding 的形状不兼容。原因TensorRT 的动态 shape 不是随意传引擎构建时--minShapes/--maxShapes定义的优化档位只覆盖你声明的范围。超出范围的 shape 之间切换要重新调用context-setBindingDimensions并同步预分配显存否则中间态的缓冲区尺寸不够。解决业务上能固定分辨率就固定省掉动态 shape 带来的整套麻烦。实在需要切换分辨率就把两种 shape 都写进 profileC 侧根据输入尺寸切换 buffer 并重新分配显存而不是等它运行时再报。6. 把部署收尾成工程习惯线程模型、性能基线与硬件选型6.1 把推理挪到后台线程UI 与推理最终解耦做上位机视觉最怕的是摄像头画面和检测结果都挤在 UI 线程里。推荐的模型是生产者消费者摄像头回调拿帧放进一个容量有限的 Channel后台推理线程取帧做预处理、推理、后处理把结果丢回 UI 线程绘制。这样 UI 永远只画最新一帧即使推理偶发抖动也不会让窗口卡顿。C# 里用System.Threading.Channels写这个队列很顺手多路摄像头就开多个 Channel给每个推理线程设独立优先级。6.2 分三段计时建立部署后的性能基线我每次交付前都会在代码里埋三个 Stopwatch分别测预处理、推理、后处理耗时。YOLOv8s 在 i5 上 OpenVINO CPU 推理大约 40-80 毫秒预处理 10 到 20 毫秒后处理如果不做优化能到 30 毫秒以上。看到任何一份性能报告先问它是分几段统计的只说总耗时的报告多半漏了预处理瓶颈。后处理优化的重点是循环向量化和 NMS 提前裁剪80 类 x 8400 框的全遍历是可以压缩的。6.3 硬件决定引擎OpenVINO 与 TensorRT 的最终选择我的最后一条建议是先定硬件再选引擎。工控机没有独显OpenVINO 的 CPU 推理是最省心的Intel 核显也能跑但要注意驱动兼容。有 NVIDIA GPU 且对延迟敏感再上 TensorRT收益主要在 FP16 和显存带宽上。至于 RK3588、Jetson 这类板子导出的同一个 ONNX 可以继续走 RKNN 或 JetPack但那就脱离本文两个平台的范围了。我现在的习惯是先用 OpenVINO 跑通整套链路再切换 TensorRT 对比延迟翻车概率低很多希望帮到你。本文还有配套的精品资源点击获取
返回列表