简介:本资源是一个基于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# binding:Intel 硬件绑定强,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.1 | Microsoft.ML.OnnxRuntime.Gpu | 是(若用 NVIDIA GPU) |
Ultralytics 8.x 导出(含--opset 12) | ≥1.14.0 | Microsoft.ML.OnnxRuntime | 否(CPU 推理) |
| 自训练模型(含自定义 NMS 层) | ≥1.16.0 | Microsoft.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
一个健壮的推理会话必须显式管理以下三部分:
- Session:
InferenceSession实例,应作为单例或静态字段缓存,避免频繁创建销毁; - Input Name:YOLOv5 ONNX 模型输入名通常为
"images"(非"input"),可通过 Netron 工具打开.onnx文件确认; - Output Names:YOLOv5 输出为
(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 缩放与归一化(非简单 Resize)
YOLOv5 要求输入尺寸严格匹配模型输入 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); // 创建缩放后 Bitmap(BGR 格式,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是按 CHW(Channel-Height-Width)排列的 float32 数组,符合 ONNX Runtime 输入要求。
3.2 步骤 2:构建输入 Tensor 并执行推理
ONNX Runtime 要求输入为NamedOnnxValue,且数据类型必须为float32:
var (preprocessed, padTop, padLeft) = Preprocess(bitmap, 640, 640); var inputTensor = OrtExtensions.CreateTensor<float>(preprocessed, new long[] { 1, 3, 640, 640 }); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("images", inputTensor) }; // 执行推理(同步,适用于单次调用) using IDisposableReadOnlyCollection<DisposableNamedOnnxValue> outputs = session.Run(inputs); var outputTensor = outputs.First().AsTensor<float>().ToArray();参数说明:
OrtExtensions.CreateTensor是社区常用扩展方法(需引用Microsoft.ML.OnnxRuntime.Extensions),它自动处理内存 pinning;session.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 List<Detection>(); 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 步骤 4:C# 实现非极大值抑制(NMS)——避免重复框
YOLOv5 ONNX 模型默认不包含 NMS 层(除非导出时加--include-nms),必须在 C# 中实现:
public static List<Detection> ApplyNms(List<Detection> detections, float iouThreshold = 0.45f) { detections.Sort((a, b) => b.Confidence.CompareTo(a.Confidence)); var keep = new List<int>(); 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内存占用达 2GB,UI 线程卡死。
根因:InferenceSession内部持有非托管 CUDA/DirectML context,Dispose()未被调用。
修复方案:
- 绝对禁止在
using块外持有InferenceSession实例; - 若需多线程共享,用
static readonly InferenceSession+Lazy<T>初始化:
private static readonly Lazy<InferenceSession> _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 ConcurrentQueue<Bitmap>(); 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用BlockingCollection<Detection[]>实现背压控制。
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 设备 ID(CUDA) // 或 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 次/秒即需优化内存分配本文还有配套的精品资源,点击获取