C# + WPF + ONNX Runtime 实现一个 YOLO 视觉算法工作台

📅 2026/7/3 5:55:40
C# + WPF + ONNX Runtime 实现一个 YOLO 视觉算法工作台
前言最近做了一个基于 C# / WPF 的本地视觉算法工作台主要目标是把常见的 ONNX 视觉模型跑起来并提供一个相对完整的桌面端操作界面。项目目前支持目标检测、图像分割、图像分类、批量处理、结果导出、性能测试以及视频/摄像头实时检测。这个项目比较适合用来学习- WPF 桌面软件界面开发- C# 调用 ONNX Runtime 进行模型推理- YOLOv5 / YOLOv8 ONNX 输出解析- 图像预处理、后处理、NMS 非极大值抑制- OpenCvSharp 读取视频流和摄像头- 检测结果可视化与 CSV 导出项目技术栈- .NET 9- WPF- Microsoft.ML.OnnxRuntime- OpenCvSharp4.Windows- YOLO ONNX 模型## 项目效果软件启动后会加载默认示例资源包括- 模型assets\models\yolov5s.onnx- 标签assets\labels\coco.txt- 示例图片assets\images\bus.jpg界面主要分为三块- 左侧任务模式、模型预设、资源选择、推理参数、运行按钮- 中间图片/视频预览区域并在图像上绘制检测框或分割结果- 底部结果表格和运行日志支持的任务模式包括- 目标检测输出类别、置信度、坐标框并绘制检测框- 图像分割输出分割覆盖图和类别占比信息- 图像分类输出 TopK 分类结果一、项目结构项目的核心目录如下textYOLO├─ assets│ ├─ labels│ ├─ models│ └─ images├─ YoloWpfApp│ ├─ Models│ ├─ Services│ ├─ MainWindow.xaml│ └─ MainWindow.xaml.cs└─ YoloWpfApp.sln其中 Services目录承担主要算法逻辑textServices├─ OnnxRuntimeYoloDetector.cs // YOLO 目标检测├─ OnnxRuntimeImageClassifier.cs // 图像分类├─ OnnxRuntimeImageSegmenter.cs // 图像分割├─ OnnxRuntimeTensorHelper.cs // ONNX 输入输出张量处理├─ OnnxRuntimeSessionCache.cs // 模型 Session 缓存├─ YoloPostProcessor.cs // NMS 后处理├─ AnnotatedImageRenderer.cs // 结果图保存├─ VisionResultCsvWriter.cs // CSV 导出└─ VisionBenchmarkReportBuilder.cs // 性能报告这种拆分方式的好处是WPF 界面层只负责交互和展示模型推理、后处理、导出等逻辑都放到服务层后续维护和扩展会更清晰。二、项目依赖项目文件 YoloWpfApp.csproj中主要依赖如下xmlProject SdkMicrosoft.NET.SdkPropertyGroupOutputTypeWinExe/OutputTypeTargetFrameworknet9.0-windows/TargetFrameworkNullableenable/NullableImplicitUsingsenable/ImplicitUsingsUseWPFtrue/UseWPFRuntimeIdentifierwin-x64/RuntimeIdentifierPlatformTargetx64/PlatformTargetSelfContainedfalse/SelfContained/PropertyGroupItemGroupPackageReference IncludeMicrosoft.ML.OnnxRuntime Version1.26.0 /PackageReference IncludeOpenCvSharp4.Windows Version4.13.0.20260602 //ItemGroup/Project这里有几个点需要注意- UseWPF 必须设置为 true- 由于 ONNX Runtime 和 OpenCvSharp 依赖 native dll建议明确指定 win-x64- 如果模型较大首次加载会有一定耗时因此项目中做了 Session 缓存三、ONNX Runtime 推理流程目标检测的核心类是 OnnxRuntimeYoloDetector。整体流程可以分为五步1. 检查 ONNX 模型路径2. 创建或复用 InferenceSession3. 将 WPF 的 BitmapSource 转为 NCHW 格式的输入张量4. 调用 ONNX Runtime 执行推理5. 解析 YOLO 输出并执行 NMS核心代码逻辑如下csharpvar session _sessionCache.GetOrCreate(options.ModelPath);var inputMetadata session.InputMetadata.First();var inputName inputMetadata.Key;var inputSize ResolveInputSize(inputMetadata.Value.Dimensions, options.InputSize);var tensorInput ImageToTensor(image, inputSize);var inputs OnnxRuntimeTensorHelper.CreateNchwFloatInput(inputName,inputMetadata.Value.ElementType,tensorInput.Data,inputSize,inputSize);using var results session.Run(inputs);var output OnnxRuntimeTensorHelper.ReadOutputAsSingleArray(results.First(), out var dimensions);var detections DecodeYoloOutput(output,dimensions,image.PixelWidth,image.PixelHeight,options,tensorInput);这里比较关键的是输入格式。YOLO 模型常见输入是text[1, 3, 640, 640]也就是- batch 1- channel 3- height 640- width 640所以图片需要转成 NCHW 格式并把 RGB 三个通道分别写入 float 数组。四、图片预处理等比缩放 Padding直接把原图强行拉伸到 640x640 会造成图像比例变形检测框映射回原图时也容易出错。因此项目中采用了类似 letterbox 的做法- 先按比例缩放图片- 不足的位置用灰色填充- 记录缩放比例和 padding 偏移- 后处理时再把检测框映射回原图坐标关键逻辑csharpvar scale Math.Min((double)inputSize / converted.PixelWidth,(double)inputSize / converted.PixelHeight);var resizedWidth Math.Max(1, (int)Math.Round(converted.PixelWidth * scale));var resizedHeight Math.Max(1, (int)Math.Round(converted.PixelHeight * scale));var paddingX (inputSize - resizedWidth) / 2;var paddingY (inputSize - resizedHeight) / 2;Array.Fill(data, 114f / 255f);其中 114是 YOLO 系列中比较常见的 padding 灰色值。检测框映射回原图时需要把 padding 和缩放比例扣掉csharpvar x (centerX - width / 2 - tensorInput.PaddingX) / tensorInput.Scale;var y (centerY - height / 2 - tensorInput.PaddingY) / tensorInput.Scale;var w width / tensorInput.Scale;var h height / tensorInput.Scale;这一点很重要否则界面上画出来的检测框会出现明显偏移。五、YOLO 输出解析不同版本的 YOLO 导出的 ONNX 输出形状可能不一样常见格式有text[1, 25200, 85] // YOLOv5 常见格式[1, 84, 8400] // YOLOv8 常见格式[1, 8400, 84] // 另一种转置形式项目中通过判断维度自动兼容这两类输出csharpvar rows dimensions[1];var columns dimensions[2];var transposed rows columns;var predictions transposed ? columns : rows;var attributes transposed ? rows : columns;同时根据类别数量判断输出中是否包含 objectnesscsharpif (attributes labelCount 5){return (5, true);}if (attributes labelCount 4){return (4, false);}YOLOv5 通常是textx, y, w, h, objectness, class1, class2, ...YOLOv8 通常是textx, y, w, h, class1, class2, ...所以兼容这两种布局对提高模型适配能力很有帮助。六、NMS 非极大值抑制模型原始输出会有大量候选框如果不做 NMS同一个目标周围可能出现多个重叠框。项目中的 NMS 逻辑是1. 按置信度从高到低排序2. 选中当前最高分框3. 计算同类别候选框和它的 IoU4. 如果 IoU 大于阈值就抑制该候选框核心代码csharpvar ordered detections.OrderByDescending(d d.Confidence).ToList();var selected new ListDetectionResult();var suppressed new bool[ordered.Count];for (var i 0; i ordered.Count; i){if (suppressed[i]){continue;}var best ordered[i];selected.Add(best);for (var j i 1; j ordered.Count; j){if (suppressed[j] || ordered[j].ClassId ! best.ClassId){continue;}if (IoU(best.Box, ordered[j].Box) threshold){suppressed[j] true;}}}IoU 计算公式为textIoU 交集面积 / 并集面积在界面上可以通过滑块调节 NMS 阈值方便观察不同参数下的检测效果。七、WPF 界面设计界面使用 MainWindow.xaml 实现整体采用左侧参数面板 中间预览 底部结果表格的布局。主要控件包括- ComboBox选择任务模式、模型预设、输入尺寸- Slider调整置信度阈值和 NMS 阈值- Image显示原图或视频帧- Canvas绘制检测框和标签- DataGrid展示检测/分类/分割结果- TextBox输出运行日志检测框不是直接画到图片上而是通过覆盖在图片上的 Canvas 来绘制csharpvar rectangle new Rectangle{Width width,Height height,Stroke brush,StrokeThickness 2.5};Canvas.SetLeft(rectangle, left);Canvas.SetTop(rectangle, top);OverlayCanvas.Children.Add(rectangle);这样做的好处是实时刷新方便用户切换结果过滤条件时只需要重绘 overlay不需要重新生成图片。八、批量处理与结果导出除了单张图片推理项目还支持选择一个图片文件夹进行批量处理。批量处理时会输出- 带检测框或分割结果的图片- 明细 CSV- 汇总 CSV处理流程大致如下csharpvar images Directory.EnumerateFiles(inputFolder).Where(IsImageFile).OrderBy(path path, StringComparer.OrdinalIgnoreCase).ToArray();foreach (var imagePath in images){var image LoadBitmap(imagePath);var taskResult await RunTaskForImageAsync(image, options, mode, busy.Token);VisionResultCsvWriter.WriteRows(writer, imagePath, taskResult.Rows);var outputImage Path.Combine(outputFolder,${Path.GetFileNameWithoutExtension(imagePath)}_{mode.ToString().ToLowerInvariant()}.png);SaveAnnotatedImage(outputImage, image, taskResult.Detections, taskResult.SegmentationOverlay);}这个功能很适合做数据集快速验证或者批量检查模型效果。九、性能测试项目中还加入了 Benchmark 功能可以选择多张图片统计每张图片的耗时并生成 CSV 报告。报告指标包括- 图片数量- 总耗时- 平均耗时- 平均 FPS- P50 / P95 耗时- 平均结果数量其中 P50、P95 比单纯平均值更能反映稳定性csharpvar averageTotal rows.Average(row row.TotalMs);var averageFps 1000d / Math.Max(1, averageTotal);var p50 Percentile(totalTimes, 0.50);var p95 Percentile(totalTimes, 0.95);如果后续要优化性能可以重点看- 预处理耗时- 推理耗时- 后处理耗时- Session 是否重复创建- 是否需要 GPU 推理十、视频和摄像头检测视频和摄像头部分使用 OpenCvSharp 的 VideoCapturecsharpusing var capture useCamera? new VideoCapture(cameraIndex): new VideoCapture(sourceName);if (!capture.IsOpened()){throw new InvalidOperationException(无法打开视频源。);}每读取一帧就转换成 WPF 可显示的 BitmapSource然后调用检测器csharpvar bitmap MatToBitmapSource(frame);var result _detector.DetectWithMetrics(bitmap, options, token);由于视频是连续帧处理界面上会实时更新- 当前帧画面- 检测框- 检测结果表格- FPS- 运行状态目前视频/摄像头模式只支持目标检测因为分类和分割对实时帧的展示逻辑还可以继续单独优化。十一、一些踩坑点1. ONNX 输出形状不统一YOLOv5 和 YOLOv8 的输出格式不完全一样有些模型导出后还会转置。因此不能只按一种固定格式解析最好根据输出维度动态判断。2. 检测框坐标容易偏移如果预处理时做了等比缩放和 padding后处理时一定要扣除 padding再除以缩放比例。否则检测框在原图上会明显错位。3. WPF UI 线程不能直接被后台线程更新模型推理建议放到后台任务中执行但界面更新必须通过 Dispatcher回到 UI 线程。4. Session 不要每次都重新创建ONNX Runtime 的 InferenceSession创建成本较高频繁创建会拖慢性能。项目中通过 OnnxRuntimeSessionCache 缓存模型 Session重复推理时可以直接复用。5. Native dll 要匹配平台ONNX Runtime 和 OpenCvSharp 都依赖 native 组件建议统一使用 x64并在项目中明确xmlRuntimeIdentifierwin-x64/RuntimeIdentifierPlatformTargetx64/PlatformTarget总结这个项目从一个简单的 YOLO 图片检测 Demo扩展成了一个相对完整的 WPF 视觉算法工作台。它不仅可以跑单张图片还支持批量处理、视频检测、摄像头检测、性能测试和结果导出。从工程角度看比较值得复用的点有- 使用 ONNX Runtime 在 C# 中部署深度学习模型- 对 YOLOv5 / YOLOv8 输出格式做兼容处理- 使用 letterbox 预处理保证检测框坐标准确- 使用 WPF Canvas 叠加绘制检测结果- 使用服务层拆分推理、后处理、导出、报告等逻辑后续还可以继续扩展- 增加 CUDA / DirectML 推理支持- 增加模型热切换和更多预设- 增加检测结果统计图表- 增加视频结果保存- 支持更多模型结构和自定义任务对于想用 C# 做 AI 桌面应用的同学这个项目可以作为一个比较完整的入门实战案例。