C#调用ONNX Runtime部署YOLOv5全流程实战

发布时间:2026/9/11 23:36:02
C#调用ONNX Runtime部署YOLOv5全流程实战 简介本资源是一个基于C#与ONNX Runtime实现YOLOv5目标检测的完整工程示例面向具备基础C#开发能力及计算机视觉入门经验的开发者用于快速上手在Windows平台部署轻量级推理模型。压缩包共310个文件包含67个运行时依赖DLL、4个ONNX模型文件、12个核心C#源码.cs、8张示例图片.jpg/.png及配套XML配置、TXT说明文档和NuGet包.nupkg整体达367.35MB结构清晰便于理解模型加载、预处理、推理调用与结果可视化全流程。已有287人学习下载资源附带可直接编译运行的Visual Studio解决方案.sln .csproj含调试符号.pdb与构建配置.props/.targets显著降低ONNX Runtime在C#环境中的集成门槛并提供跨平台运行所需的动态库.so/.dylib与资源文件.resx/.resources适合希望将YOLOv5落地至桌面端应用的实践者参考与二次开发。1. 用 C# 调用 ONNX Runtime 运行 YOLOv5 模型不是“封装个 DLL 就完事”的 Demo很多刚接触模型部署的 C# 开发者看到 “C# OnnxRuntime YoloV5 Demo.rar” 这类压缩包第一反应是解压、双击 exe、看个窗口弹出检测框——然后就停在这儿了。但真实产线场景里这根本不够摄像头持续推流时 CPU 占用飙到 95%、小目标漏检率超 30%、切换不同分辨率摄像头就报Invalid input shape、甚至加载.onnx文件时直接抛Microsoft.ML.OnnxRuntime.OnnxRuntimeException: Invalid model file。这个标题指向的不是一个“能跑就行”的演示程序而是 C# 工程师在 Windows 或 .NET 6 Linux 环境下稳定接入 YOLOv5 推理链路的最小可行闭环从 ONNX 模型加载、预处理含 BGR→RGB、归一化、letterbox 缩放、推理执行、后处理NMS、坐标还原到结果可视化。它面向的是需要把视觉检测嵌入上位机、工业 HMI、MES 数据采集终端的开发者尤其关注 .NET 生态下内存管理、线程安全与实时性之间的平衡点。2. 为什么必须用 ONNX Runtime 而非直接调 PyTorch选型依据与 C# 绑定逻辑2.1 ONNX Runtime 是 C# 部署 YOLOv5 的事实标准而非可选项YOLOv5 官方导出的.onnx模型如yolov5s.onnx本质是计算图中间表示它剥离了 PyTorch/TensorFlow 运行时依赖只保留算子定义与权重。C# 无法原生加载.pt或.h5而 ONNX Runtime 提供了跨平台、高性能、低内存开销的 C API并通过Microsoft.ML.OnnxRuntimeNuGet 包暴露为强类型 .NET 接口。对比其他方案TensorRT C# wrapper需 NVIDIA GPU、CUDA 版本严格匹配Windows 上驱动兼容性极差OpenVINO C# bindingIntel 硬件绑定强ARM/x86 通用性弱直接调 Python 子进程启动延迟高300ms、GC 不可控、异常堆栈难追踪。提示ONNX Runtime 的InferenceSession在 .NET 中是线程安全的但OrtSessionOptions和OrtEnv必须全局复用——这是避免反复初始化 CUDA context 导致显存泄漏的关键。2.2 NuGet 包版本与 ONNX 模型兼容性必须对齐YOLOv5 不同版本v5.0/v6.0/v6.2/v6.3导出的 ONNX 模型算子集差异显著。例如 v6.2 后引入NonMaxSuppression算子而 ONNX Runtime 1.14 不支持该算子。实际项目中必须按表匹配YOLOv5 模型来源推荐 ONNX Runtime 版本关键 NuGet 包名是否需启用 CUDA官方 GitHubexport.py(v6.2)≥1.15.1Microsoft.ML.OnnxRuntime.Gpu是若用 NVIDIA GPUUltralytics 8.x 导出含--opset 12≥1.14.0Microsoft.ML.OnnxRuntime否CPU 推理自训练模型含自定义 NMS 层≥1.16.0Microsoft.ML.OnnxRuntime.DirectML是Windows DirectML安装命令以 CPU 版本为例dotnet add package Microsoft.ML.OnnxRuntime --version 1.16.3注意Microsoft.ML.OnnxRuntime.Gpu包体积超 200MB需确保目标机器已安装对应 CUDA/cuDNN 版本如 11.8/8.6且nvidia-smi可见设备。若仅用 CPU务必卸载 Gpu 包否则运行时会因找不到cudart64_118.dll崩溃。2.3 C# 中 ONNX Runtime 初始化的三要素Session、Input、Output一个健壮的推理会话必须显式管理以下三部分SessionInferenceSession实例应作为单例或静态字段缓存避免频繁创建销毁Input NameYOLOv5 ONNX 模型输入名通常为images非input可通过 Netron 工具打开.onnx文件确认Output NamesYOLOv5 输出为(1, 25200, 85)张量名称常为output若导出时启用了--dynamic则可能有多个输出如boxes,scores。验证输入输出名的 C# 代码using var session new InferenceSession(yolov5s.onnx); Console.WriteLine($Input count: {session.InputMetadata.Count}); foreach (var input in session.InputMetadata) { Console.WriteLine($Input: {input.Key}, Shape: [{string.Join(,, input.Value.Shape)}]); } // 输出示例Input: images, Shape: [1,3,640,640]3. 从图像到检测框C# 实现 YOLOv5 全流程推理的 5 个关键步骤3.1 步骤 1图像预处理——Letterbox 缩放与归一化非简单 ResizeYOLOv5 要求输入尺寸严格匹配模型输入 shape如[1,3,640,640]且必须保持宽高比。直接Bitmap.Resize()会导致目标形变必须实现 letterbox 填充public static (float[] data, int padTop, int padLeft) Preprocess(Bitmap src, int targetWidth, int targetHeight) { // 计算缩放比例 float scale Math.Min((float)targetWidth / src.Width, (float)targetHeight / src.Height); int newWidth (int)(src.Width * scale); int newHeight (int)(src.Height * scale); // 创建缩放后 BitmapBGR 格式YOLOv5 训练时使用 OpenCV 读取 using var resized new Bitmap(newWidth, newHeight); using (var g Graphics.FromImage(resized)) g.DrawImage(src, 0, 0, newWidth, newHeight); // 创建 letterbox 目标数组CHW, float32 float[] data new float[targetWidth * targetHeight * 3]; int padTop (targetHeight - newHeight) / 2; int padLeft (targetWidth - newWidth) / 2; // 填充 BGR 通道注意YOLOv5 训练时未做 RGB/BGR 转换此处保持 BGR for (int y 0; y newHeight; y) { for (int x 0; x newWidth; x) { var pixel resized.GetPixel(x, y); int dstIdx ((padTop y) * targetWidth padLeft x) * 3; data[dstIdx 0] pixel.B / 255.0f; // B data[dstIdx 1] pixel.G / 255.0f; // G data[dstIdx 2] pixel.R / 255.0f; // R } } return (data, padTop, padLeft); }说明padTop/padLeft用于后续将检测框坐标还原到原始图像坐标系。此处data是按 CHWChannel-Height-Width排列的 float32 数组符合 ONNX Runtime 输入要求。3.2 步骤 2构建输入 Tensor 并执行推理ONNX Runtime 要求输入为NamedOnnxValue且数据类型必须为float32var (preprocessed, padTop, padLeft) Preprocess(bitmap, 640, 640); var inputTensor OrtExtensions.CreateTensorfloat(preprocessed, new long[] { 1, 3, 640, 640 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(images, inputTensor) }; // 执行推理同步适用于单次调用 using IDisposableReadOnlyCollectionDisposableNamedOnnxValue outputs session.Run(inputs); var outputTensor outputs.First().AsTensorfloat().ToArray();参数说明OrtExtensions.CreateTensor是社区常用扩展方法需引用Microsoft.ML.OnnxRuntime.Extensions它自动处理内存 pinningsession.Run()返回IDisposableReadOnlyCollection必须用using释放非托管资源否则连续调用 1000 次后内存泄漏超 500MB。3.3 步骤 3解析 YOLOv5 输出张量——解码(1,25200,85)结构YOLOv5 输出是(1, num_boxes, 85)其中85 4(box) 1(confidence) 80(class_probs)。需提取置信度 0.4 的框var detections new ListDetection(); for (int i 0; i outputTensor.Length; i 85) { float confidence outputTensor[i 4]; if (confidence 0.4f) continue; float x outputTensor[i 0]; float y outputTensor[i 1]; float w outputTensor[i 2]; float h outputTensor[i 3]; // 还原到 letterbox 坐标系 x (x - padLeft) / 640f * bitmap.Width; y (y - padTop) / 640f * bitmap.Height; w w / 640f * bitmap.Width; h h / 640f * bitmap.Height; // 计算左上角坐标 float x1 Math.Max(0, x - w / 2); float y1 Math.Max(0, y - h / 2); float x2 Math.Min(bitmap.Width - 1, x w / 2); float y2 Math.Min(bitmap.Height - 1, y h / 2); // 获取最高概率类别 int clsId 0; float maxProb 0; for (int c 5; c 85; c) { if (outputTensor[i c] maxProb) { maxProb outputTensor[i c]; clsId c - 5; } } detections.Add(new Detection { X1 (int)x1, Y1 (int)y1, X2 (int)x2, Y2 (int)y2, Confidence confidence * maxProb, ClassId clsId }); }3.4 步骤 4C# 实现非极大值抑制NMS——避免重复框YOLOv5 ONNX 模型默认不包含 NMS 层除非导出时加--include-nms必须在 C# 中实现public static ListDetection ApplyNms(ListDetection detections, float iouThreshold 0.45f) { detections.Sort((a, b) b.Confidence.CompareTo(a.Confidence)); var keep new Listint(); var isSuppressed new bool[detections.Count]; for (int i 0; i detections.Count; i) { if (isSuppressed[i]) continue; keep.Add(i); var a detections[i]; for (int j i 1; j detections.Count; j) { if (isSuppressed[j]) continue; var b detections[j]; float iou CalculateIou(a, b); if (iou iouThreshold) isSuppressed[j] true; } } return keep.Select(i detections[i]).ToList(); } private static float CalculateIou(Detection a, Detection b) { float interX1 Math.Max(a.X1, b.X1); float interY1 Math.Max(a.Y1, b.Y1); float interX2 Math.Min(a.X2, b.X2); float interY2 Math.Min(a.Y2, b.Y2); if (interX1 interX2 || interY1 interY2) return 0; float interArea (interX2 - interX1) * (interY2 - interY1); float areaA (a.X2 - a.X1) * (a.Y2 - a.Y1); float areaB (b.X2 - b.X1) * (b.Y2 - b.Y1); return interArea / (areaA areaB - interArea); }3.5 步骤 5绘制检测结果到 WinForms/WPF 控件在Paint事件中绘制矩形与标签private void pictureBox_Paint(object sender, PaintEventArgs e) { foreach (var det in _detections) { using var pen new Pen(Color.Red, 2); e.Graphics.DrawRectangle(pen, det.X1, det.Y1, det.X2 - det.X1, det.Y2 - det.Y1); string label ${CocoClasses[det.ClassId]} {det.Confidence:F2}; using var font new Font(Segoe UI, 10); using var brush Brushes.White; e.Graphics.DrawString(label, font, brush, det.X1, det.Y1 - 20); } }注意WinForms 中pictureBox.Image不能直接修改必须在Paint事件中绘制若需保存带框图像用Graphics.FromImage(bitmap)绘制后bitmap.Save()。4. 解决 C# YOLOv5 推理卡顿、崩溃、结果不准的 4 类高频问题4.1 内存泄漏InferenceSession未正确释放导致 GC 压力飙升现象连续推理 500 帧后Private Bytes内存占用达 2GBUI 线程卡死。根因InferenceSession内部持有非托管 CUDA/DirectML contextDispose()未被调用。修复方案绝对禁止在using块外持有InferenceSession实例若需多线程共享用static readonly InferenceSessionLazyT初始化private static readonly LazyInferenceSession _session new(() new InferenceSession(yolov5s.onnx, SessionOptions.MakeSessionOptionWithCudaProvider(0))); public static InferenceSession Session _session.Value;检查GC.GetTotalMemory(false)在每次推理前后变化若增长 1MB/帧则存在未释放 Tensor。4.2 输入尺寸不匹配System.ArgumentException: Input tensor shape mismatch现象加载yolov5s.onnx后传入[1,3,416,416]数据报错。根因ONNX 模型输入 shape 固定为[1,3,640,640]Netron 查看Input shape: [1,3,640,640]即可确认。修复方案预处理函数中targetWidth/targetHeight必须与模型输入一致若需动态尺寸导出 ONNX 时加--dynamic参数并在 C# 中用SessionOptions启用动态维度var options new SessionOptions(); options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_EXTENDED; var session new InferenceSession(yolov5s_dynamic.onnx, options);4.3 检测框偏移坐标还原错误导致框位置漂移现象检测框整体右下偏移 20 像素。根因YOLOv5 输出的x,y,w,h是归一化到640x640网格的中心坐标letterbox 填充后未减去padTop/padLeft。修复关键行// 错误写法未减去 padding x x / 640f * bitmap.Width; // 正确写法先还原到 640x640 坐标系再映射到原始图 x (x - padLeft) / 640f * bitmap.Width;4.4 类别标签错乱COCO 类别索引与模型输出不一致现象检测出“apple”却显示“person”。根因Ultralytics YOLOv5 默认使用 COCO 80 类但导出 ONNX 时若用了自定义数据集class_id映射关系改变。验证方法用 Python 加载同一模型打印model.names在 C# 中硬编码映射表public static readonly string[] CocoClasses { person, bicycle, car, /* ... 80 个 */ toothbrush }; // 若为自定义模型替换为此模型训练时的 names.yaml 中顺序5. 提升吞吐量用 C# 多线程流水线处理视频流的实战配置5.1 构建三阶段流水线Capture → Preprocess → Inference单线程串行处理 640p 视频30fps时CPU 利用率仅 35%GPU 利用率不足 20%。改为生产者-消费者模式// 阶段 1摄像头采集独立线程 var captureQueue new ConcurrentQueueBitmap(); Task.Run(() CaptureLoop(captureQueue)); // 阶段 2预处理2 个线程 Parallel.ForEach(Enumerable.Range(0, 2), _ PreprocessLoop(captureQueue, preprocessQueue)); // 阶段 3推理GPU 线程池1 个线程即可GPU 本身并行 Task.Run(() InferenceLoop(preprocessQueue, resultQueue));关键参数preprocessQueue使用ConcurrentQueue(float[], int, int)存储预处理数据及 padding 偏移resultQueue用BlockingCollectionDetection[]实现背压控制。5.2 ONNX Runtime 性能调优的 3 个必设参数在SessionOptions中启用以下选项实测提升 15~25% 吞吐var options new SessionOptions(); options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; // 启用所有图优化 options.ExecutionMode ExecutionMode.ORT_PARALLEL; // 启用多线程执行CPU options.AppendExecutionProvider_CUDA(0); // 指定 GPU 设备 IDCUDA // 或 options.AppendExecutionProvider_DML(); // Windows DirectML注意ORT_PARALLEL仅对 CPU 有效CUDA 下应关闭此选项由 GPU 自行调度。5.3 实时性监控每秒统计 FPS 与延迟在推理循环中加入毫秒级计时var sw Stopwatch.StartNew(); var results session.Run(inputs); sw.Stop(); Console.WriteLine($Inference time: {sw.ElapsedMilliseconds} ms, FPS: {1000.0 / sw.ElapsedMilliseconds:F1});稳定运行时yolov5s在 RTX 3060 上应达12~15ms/帧≈83 FPS若超过30ms/帧检查是否启用了ORT_ENABLE_ALL且模型未被量化INT8 量化可再降 40% 延迟。用dotnet-counters监控 GC 压力dotnet-counters monitor -p pid --counters System.Runtime # 关注 Gen 0/1/2 Collections 每秒次数5 次/秒即需优化内存分配本文还有配套的精品资源点击获取