ARTICLE DETAIL

资讯详情

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

C# 部署 YOLOv8 实战:ONNX 推理与 NMS 后处理全解析

C# 部署 YOLOv8 实战:ONNX 推理与 NMS 后处理全解析 简介这份资源面向希望将Yolov8系列目标检测模型落地到C#工程中的开发者尤其适合具备一定深度学习基础、需要在Windows桌面或服务端集成推理能力的工程师。它解决了从Python训练到C#部署之间的衔接问题提供可直接运行的完整工程覆盖检测、分类等常见任务场景。压缩包共56个文件约3.02MB以cs源码、csproj工程文件、sln解决方案为主辅以cpp与h头文件用于底层推理封装另有jpg示例图片、txt标签说明及py脚本整体结构围绕TensorRTSharp、OpenVinoSharp、CommonSharp、ResultSharp等模块组织便于按需查阅与二次开发。资源已有329人学习下载说明其在C#部署Yolov8方向具备一定参考价值。读者可获得一套可编译运行的部署方案理解模型加载、推理封装与结果解析的完整链路并借助示例图片与标签文件快速验证效果减少自行摸索环境配置与接口对接的时间成本。1. C# 接住 YOLOv8为什么 .NET 团队开始认真对待本地推理这两年做工业视觉和桌面端上位机的团队越来越多被同一个问题卡住算法同事丢过来一个 YOLOv8 的.pt权重说「效果很好你们集成一下」而你的技术栈是 C# WPF/WinForm团队里没人想为了一个检测功能再养一套 Python 服务。于是「基于 C# 部署 YOLOv8 系列模型」这件事从可选项变成了刚需。它要解决的核心问题很具体把训练好的 YOLOv8 模型在纯 .NET 环境里跑起来不依赖 Python 运行时能拿到检测框、类别和置信度并且能稳定地嵌进现有业务系统。适合谁做上位机、MES、质检软件、桌面工具的 C# 工程师以及需要把模型部署到客户现场、又不想让客户装一堆 Python 依赖的交付团队。这篇就按「模型怎么来、环境怎么搭、代码怎么写、坑在哪」的顺序把一条能复现的路讲清楚。2. 从 .pt 到 ONNXC# 侧真正能吃的模型格式C# 本身没有原生的 PyTorch 运行时所以第一步不是写 C#而是把 YOLOv8 的权重转成 C# 能加载的格式。目前最稳、生态最成熟的路线是 ONNX配合 Microsoft.ML.OnnxRuntime 这个 NuGet 包做推理。这一章先把「模型从哪来、怎么转、转的时候注意什么」讲透因为后面 80% 的翻车都出在这一步。2.1 为什么选 ONNX 而不是直接调 Python 或 TorchSharp先说选型理由避免你走弯路。常见做法有三种一是起一个 Python 进程用 HTTP/gRPC 通信二是用 TorchSharp 直接加载.pt三是转 ONNX 用 OnnxRuntime 推理。第一种方案在开发机上很爽但交付到客户现场就是灾难客户机器要装 Python、要装 torch、要处理 CUDA 版本冲突一个环境问题能让交付拖一周。第二种 TorchSharp 理论上能加载模型但 YOLOv8 的 Ultralytics 实现里有大量自定义算子和后处理逻辑TorchSharp 对.pt的兼容性一直不完整尤其是导出后的模型结构踩坑成本很高。ONNX 的优势在于它是中间表示格式Ultralytics 官方直接支持导出OnnxRuntime 在 Windows/Linux 上都有成熟的 C# 绑定CPU 和 GPUCUDA/TensorRT都能跑而且推理时不需要任何 Python。代价是后处理要自己写——YOLOv8 的输出是原始张量NMS非极大值抑制得在 C# 里实现。这个代价是值得的因为后处理逻辑是确定的写一次就能复用。提示如果你的场景对延迟极其敏感且确定部署在 NVIDIA 显卡上可以考虑导出 TensorRT engine但 C# 侧加载 TensorRT 的绑定不如 OnnxRuntime 成熟建议先用 ONNX 跑通再优化。2.2 导出 ONNX 的具体命令与参数假设你已经在 Python 环境里训练好了best.pt用 Ultralytics 的导出命令# 安装 ultralytics训练环境里一般已有 pip install ultralytics onnx onnxruntime # 导出为 ONNXimgsz 必须和推理时一致 yolo export modelbest.pt formatonnx imgsz640 opset12 simplifyTrue dynamicFalse参数逐个说明imgsz640导出时的输入尺寸必须和 C# 推理时喂进去的尺寸完全一致否则检测框坐标会整体偏移。这是最常见的坑之一。opset12ONNX 算子集版本。opset 太低可能不支持某些算子太高部分 OnnxRuntime 版本不认。12 是兼容性最好的选择OnnxRuntime 1.10 以上都支持。simplifyTrue调用 onnx-simplifier 做图优化能去掉冗余节点推理速度通常有 10%~20% 提升。dynamicFalse固定输入尺寸。设成 True 会导出动态 batch/尺寸C# 侧处理起来更麻烦除非你确实需要变长输入否则保持 False。导出成功后你会得到一个best.onnx文件通常几 MB 到几十 MB取决于模型大小n/s/m/l/x。这个文件就是 C# 项目要引用的资源。2.3 验证 ONNX 模型是否正常先用 Python 跑一遍在写 C# 之前强烈建议先用 Python 验证 ONNX 模型本身没问题这样能把「模型问题」和「C# 代码问题」分开import onnxruntime as ort import numpy as np # 加载模型确认能创建会话 sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name print(输入名:, input_name, 形状:, sess.get_inputs()[0].shape) print(输出名:, [o.name for o in sess.get_outputs()]) # 造一个假输入跑一遍 dummy np.random.rand(1, 3, 640, 640).astype(np.float32) outputs sess.run(None, {input_name: dummy}) for o in outputs: print(输出形状:, o.shape)正常情况下YOLOv8 的 ONNX 输出形状是[1, 84, 8400]84 4 个框坐标 80 个类别分数8400 是候选框数量。如果你看到的是[1, 8400, 84]说明导出时的转置逻辑不同后处理代码要相应调整。这一步确认清楚能省掉后面大量调试时间。3. C# 工程搭建NuGet 包、项目结构与推理封装模型准备好了接下来是 C# 侧。这一章把工程结构、依赖、以及一个可复用的推理类写出来。目标是你照着敲完能拿到一个输入Bitmap、输出检测框列表的类。3.1 创建项目与安装正确的 NuGet 包新建一个 .NET 6/7/8 的类库或控制台项目WPF 项目同理然后装两个核心包dotnet new console -n YoloSharpDemo cd YoloSharpDemo # ONNX 推理运行时CPU 版 dotnet add package Microsoft.ML.OnnxRuntime --version 1.16.3 # 图像处理用于读取图片和缩放 dotnet add package SixLabors.ImageSharp --version 3.1.3版本说明OnnxRuntime 的版本要和导出时的 opset 兼容1.16.x 支持 opset 12~19足够用。ImageSharp 用来替代System.Drawing因为System.Drawing.Common在 .NET 6 之后跨平台支持受限Linux 上跑会报错ImageSharp 是纯托管实现跨平台无坑。如果你要用 GPU 推理把Microsoft.ML.OnnxRuntime换成Microsoft.ML.OnnxRuntime.Gpu并在创建会话时指定SessionOptions使用 CUDA provider。但注意 GPU 版对 CUDA 和 cuDNN 版本有要求交付到客户机器上要确认驱动环境否则会静默回退到 CPU。3.2 图像预处理letterbox 缩放与归一化YOLOv8 推理前的预处理不能简单拉伸必须用 letterbox保持宽高比短边补灰边否则检测框会变形。这是新手最容易忽略的一步。using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; public static class Preprocess { // 返回张量数据和缩放/填充信息后处理要用 public static (float[] data, float ratio, int padW, int padH) Letterbox( ImageRgb24 img, int targetSize 640) { int w img.Width, h img.Height; // 计算缩放比例取较小值保证整图放得下 float ratio Math.Min((float)targetSize / w, (float)targetSize / h); int newW (int)(w * ratio); int newH (int)(h * ratio); int padW (targetSize - newW) / 2; int padH (targetSize - newH) / 2; // 缩放到 newW x newH再补边到 640x640 using var resized img.Clone(ctx ctx.Resize(newW, newH)); using var canvas new ImageRgb24(targetSize, targetSize); canvas.Mutate(ctx ctx .Fill(Color.Gray) // YOLOv8 默认填充 114这里用灰近似 .DrawImage(resized, new Point(padW, padH), 1f)); // 转成 CHW 排列的 float 数组并归一化到 0~1 float[] data new float[3 * targetSize * targetSize]; canvas.ProcessPixelRows(accessor { for (int y 0; y targetSize; y) { var row accessor.GetRowSpan(y); for (int x 0; x targetSize; x) { int idx y * targetSize x; data[idx] row[x].R / 255f; // R 通道 data[targetSize * targetSize idx] row[x].G / 255f; // G 通道 data[2 * targetSize * targetSize idx] row[x].B / 255f; // B 通道 } } }); return (data, ratio, padW, padH); } }逻辑说明letterbox 的核心是「等比缩放 居中补边」ratio和padW/padH必须保存下来因为模型输出的框坐标是在 640x640 图上的要映射回原图必须用这三个值反算。归一化用/255fYOLOv8 训练时就是这么做的别用别的均值方差否则置信度会整体偏低。参数说明targetSize默认 640必须和导出 ONNX 时的imgsz一致。填充色官方是 114灰色这里用Color.Gray近似对精度影响极小但如果你追求极致一致可以手动填Rgb24(114,114,114)。3.3 推理会话与后处理解析输出张量并做 NMS这是整个方案的核心。YOLOv8 的输出是[1, 84, 8400]需要转置、过滤低置信度、做 NMS。using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class YoloDetector : IDisposable { private readonly InferenceSession _session; private readonly string _inputName; private const int NumClasses 80; // COCO 类别数自定义模型要改 private const float ConfThreshold 0.25f; private const float IouThreshold 0.45f; public YoloDetector(string modelPath) { var options new SessionOptions(); options.GraphOptimizationLevel GraphOptimizationLevel.ORT_ENABLE_ALL; _session new InferenceSession(modelPath, options); _inputName _session.InputMetadata.Keys.First(); } public ListDetection Detect(ImageRgb24 img) { var (data, ratio, padW, padH) Preprocess.Letterbox(img); // 构造输入张量 [1,3,640,640] var tensor new DenseTensorfloat(data, new[] { 1, 3, 640, 640 }); var inputs new ListNamedOnnxValue { NamedOnnxValue.CreateFromTensor(_inputName, tensor) }; using var results _session.Run(inputs); var output results.First().AsTensorfloat(); // [1,84,8400] var candidates new ListDetection(); int numAnchors output.Dimensions[2]; // 8400 for (int i 0; i numAnchors; i) { // 前 4 个是 cx,cy,w,h后面是类别分数 float cx output[0, 0, i], cy output[0, 1, i]; float bw output[0, 2, i], bh output[0, 3, i]; float maxScore 0; int maxClass 0; for (int c 0; c NumClasses; c) { float s output[0, 4 c, i]; if (s maxScore) { maxScore s; maxClass c; } } if (maxScore ConfThreshold) continue; // 反算回原图坐标 float x1 (cx - bw / 2 - padW) / ratio; float y1 (cy - bh / 2 - padH) / ratio; float x2 (cx bw / 2 - padW) / ratio; float y2 (cy bh / 2 - padH) / ratio; candidates.Add(new Detection { X1 x1, Y1 y1, X2 x2, Y2 y2, Score maxScore, ClassId maxClass }); } return Nms(candidates); } private ListDetection Nms(ListDetection dets) { var result new ListDetection(); // 按类别分组后各自做 NMS避免不同类互相抑制 foreach (var group in dets.GroupBy(d d.ClassId)) { var sorted group.OrderByDescending(d d.Score).ToList(); while (sorted.Count 0) { var best sorted[0]; result.Add(best); sorted.RemoveAt(0); sorted.RemoveAll(d Iou(best, d) IouThreshold); } } return result; } private float Iou(Detection a, Detection b) { float ix1 Math.Max(a.X1, b.X1), iy1 Math.Max(a.Y1, b.Y1); float ix2 Math.Min(a.X2, b.X2), iy2 Math.Min(a.Y2, b.Y2); float iw Math.Max(0, ix2 - ix1), ih Math.Max(0, iy2 - iy1); float inter iw * ih; float areaA (a.X2 - a.X1) * (a.Y2 - a.Y1); float areaB (b.X2 - b.X1) * (b.Y2 - b.Y1); return inter / (areaA areaB - inter 1e-6f); } public void Dispose() _session.Dispose(); } public class Detection { public float X1, Y1, X2, Y2, Score; public int ClassId; }逻辑说明输出张量的索引顺序是[batch, channel, anchor]channel 0~3 是框4~83 是类别分数。注意 YOLOv8 的输出没有单独的 objectness 分数类别分数本身就是置信度这点和 YOLOv5 不同很多人照搬 v5 的后处理会出错。NMS 按类别分组做是因为不同类别的框即使重叠也不应该互相抑制。参数说明ConfThreshold0.25和IouThreshold0.45是 Ultralytics 的默认值实际项目里按场景调——漏检多就降到 0.15误检多就升到 0.4。NumClasses必须改成你自己模型的类别数COCO 是 80自定义数据集是多少就写多少写错了会数组越界或漏检。4. 避坑与排查C# 部署 YOLOv8 最常见的 5 个翻车点这一章全是血泪经验每条都按「现象 → 原因 → 解决」写遇到问题直接对号入座。4.1 检测框整体偏移或缩放错位现象框能出来但位置整体偏了或者框比实际目标大一圈小一圈。原因预处理和后处理的尺寸没对齐。要么 letterbox 的ratio/padW/padH没参与反算要么导出 ONNX 的imgsz和推理时喂的尺寸不一致。解决确认三处尺寸完全一致——导出命令的imgsz、Letterbox的targetSize、构造张量时的new[] {1,3,640,640}。反算坐标时严格用(cx - padW) / ratio这个公式别自己简化。4.2 置信度普遍偏低明明有目标却检测不到现象模型在 Python 里跑得好好的C# 里置信度只有 0.1 左右大量漏检。原因归一化方式不对。常见错误是用了 ImageNet 的均值方差(x-mean)/std而 YOLOv8 训练时只做了/255。解决预处理只做/255f不要减均值除方差。另外确认通道顺序是 RGB 不是 BGRImageSharp 默认是 RGB如果你从 OpenCV 那边迁移过来容易搞反。4.3 Linux 上跑报 System.Drawing 不支持现象Windows 上正常部署到 Linux 服务器或 Docker 里报System.Drawing.Common is not supported。原因.NET 6 之后System.Drawing.Common被限制为仅 Windows 可用。解决全程用 ImageSharp 或 SkiaSharp 做图像处理不要碰System.Drawing.Bitmap。如果现有代码大量依赖 Bitmap写一个转换层把 Bitmap 转成 ImageSharp 的ImageRgb24再进推理。4.4 GPU 推理没生效速度还是 CPU 水平现象装了 GPU 版包但推理速度和 CPU 一样任务管理器里 GPU 占用为 0。原因创建InferenceSession时没指定 CUDA provider或者 CUDA/cuDNN 版本不匹配导致静默回退。解决显式配置 provider并打印实际使用的 provider 确认var options new SessionOptions(); options.AppendExecutionProvider_CUDA(0); // 0 是 GPU 编号 // 创建会话后确认 Console.WriteLine(string.Join(,, _session.InputMetadata.Keys));如果 CUDA 版本不对OnnxRuntime 会回退到 CPU 且不报错所以一定要在日志里确认 provider 列表。4.5 内存持续增长跑几小时就 OOM现象长时间运行后内存一直涨最终崩溃。原因InferenceSession或Image对象没释放或者每次推理都新建 session。解决InferenceSession创建一次复用用using或单例管理ImageRgb24用完即DisposeNamedOnnxValue和IDisposableReadOnlyCollection也要释放。把推理类实现IDisposable在应用退出时统一释放。5. 进阶批处理、动态尺寸与自定义类别映射的实战技巧跑通单张图之后真正上生产还要解决三件事吞吐、灵活性和可维护性。先说批处理。如果你的场景是离线批量检测比如一次处理几千张质检图逐张推理浪费严重。ONNX 支持 batch 输入导出时把dynamic设为 True 或者导出固定 batch然后构造[N,3,640,640]的张量一次喂进去。实测在 CPU 上 batch4 比逐张快 30% 左右GPU 上提升更明显。但要注意batch 推理时 letterbox 的ratio/padW/padH每张图都不同得用数组存起来后处理时按索引取。再说动态尺寸。固定 640 对小目标不友好如果你检测的是 PCB 上的元件可以导出imgsz1280的模型精度会明显提升代价是推理时间翻倍。我的习惯是准备两个模型文件小图走 640大图走 1280运行时按图片尺寸路由。最后是类别映射。ONNX 模型里只有类别索引没有类别名。别把类别名硬编码在 C# 里用一个 JSON 配置文件管理{ numClasses: 3, classes: [defect_a, defect_b, defect_c], confThreshold: 0.3, iouThreshold: 0.45 }启动时读进来NumClasses和阈值都从配置取换模型只改 JSON 不改代码。这个习惯让我在客户现场改阈值时省了重新编译的麻烦。验证方面我一般会做一个「对照测试」同一批图Python 脚本跑一遍存结果C# 跑一遍存结果逐张比对框坐标和置信度误差在 1e-3 以内才算对齐。这个测试能一次性暴露预处理、后处理、坐标反算的所有问题比肉眼看图靠谱得多。踩了这么多坑我最大的教训是别急着写 C#先把 ONNX 模型在 Python 里验证透把输入输出形状、归一化方式、类别数全部确认清楚再动手写推理代码。模型层面的问题在 C# 里排查成本是 Python 里的五倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表