在工业自动化、安防监控、缺陷检测等场景中,实时、准确地识别图像中的目标物体是核心需求。对于广大使用 C# 进行上位机、MES 系统或工业软件开发的工程师而言,虽然 Python 生态的 AI 模型资源丰富,但将其无缝集成到 C# 项目中却常常面临环境复杂、依赖众多、部署困难等门槛。本文将彻底解决这个问题,手把手带你将当前流行的 YOLOv8 目标检测模型集成到 C# 项目中,从环境搭建到运行推理,全程代码可复制,确保新手也能在 30 分钟内成功跑通第一个检测程序。
本文适合有一定 C# 基础,希望将 AI 视觉能力融入现有 .NET 项目的开发者。无论你是想为 WinForm/WPF 应用添加实时检测功能,还是希望对接工业相机 SDK 进行在线质检,本文提供的方案都能为你提供一个坚实、可扩展的起点。
1. 背景与核心概念:为什么是 C# + YOLOv8 + ONNX?
在深入代码之前,理解我们选择的技术栈及其背后的原因至关重要。这能帮助你在未来遇到问题时,拥有清晰的排查思路。
1.1 YOLOv8:平衡速度与精度的目标检测利器
YOLO(You Only Look Once)系列模型因其“单次前向传播即可完成检测”的特性,在实时目标检测领域占据主导地位。YOLOv8 是 Ultralytics 公司发布的最新版本,它在精度、速度和易用性上达到了新的平衡。相较于前代,YOLOv8 提供了更简洁的 API、更丰富的预训练模型(从轻量级的n、s到高精度的l、x),并且原生支持导出为 ONNX 格式,这为跨平台、跨语言部署铺平了道路。
1.2 ONNX Runtime:跨平台推理引擎的桥梁
ONNX(Open Neural Network Exchange)是一个开放的模型格式标准。将 PyTorch 或 TensorFlow 训练的模型转换为 ONNX 格式后,就可以通过 ONNX Runtime 这个高性能推理引擎在各种硬件和编程语言(包括 C#)中运行。这意味着,我们可以利用 Python 强大的训练生态来生产模型,然后在 C# 的生产环境中进行高效部署,完美结合了两者的优势。
1.3 C# 端的价值:无缝集成与工业级应用
C# 凭借其强大的桌面应用开发能力(WinForms, WPF)、稳定的运行时和丰富的工业通信库(如 OPC UA、各种相机 SDK),在工业软件领域应用广泛。通过 ONNX Runtime,C# 开发者可以直接调用训练好的 YOLOv8 模型,无需引入复杂的 Python 环境或进行耗时的模型重写。这种方案保留了 YOLO 的实时性(在普通 CPU 上也能达到可观的帧率),又能让检测逻辑深度嵌入到现有的 C# 业务流程、用户界面和数据系统中。
核心流程总结:在 Python 环境训练或获取 YOLOv8 模型 -> 导出为 ONNX 格式 -> 在 C# 项目中通过 NuGet 引入 ONNX Runtime -> 编写预处理、推理、后处理代码 -> 集成到应用程序中。
2. 环境准备与版本说明
工欲善其事,必先利其器。以下是完成本教程所需的环境和工具清单。请注意,版本号是成功的关键,建议尽量与本文保持一致以避免兼容性问题。
2.1 开发环境与工具
- 操作系统:Windows 10 或 Windows 11(本教程基于 Windows 环境)。
- 集成开发环境 (IDE):Visual Studio 2022(社区版即可)。确保安装时勾选了“.NET 桌面开发”工作负载。
- .NET 版本:.NET 6.0 或 .NET 8.0(长期支持版本)。本教程使用 .NET 8.0 Console 项目。
- Python 环境(仅用于模型导出):Python 3.8 或 3.9。需要安装
ultralytics和onnx包。如果你已有训练好的 ONNX 模型,可跳过此部分。
2.2 关键 NuGet 包
我们将通过 Visual Studio 的 NuGet 包管理器为 C# 项目安装以下库:
Microsoft.ML.OnnxRuntime:ONNX Runtime 的 .NET 绑定,用于加载和运行 ONNX 模型。Microsoft.ML.OnnxRuntime.Managed:同上,通常安装这个或上一个即可,本教程使用Microsoft.ML.OnnxRuntime。OpenCvSharp4和OpenCvSharp4.runtime.win:用于图像的读取、预处理(缩放、颜色空间转换)和结果绘制。这是 C# 中处理计算机视觉任务的常用库。System.Drawing.Common:用于图像的基本操作(可选,部分场景备用)。
2.3 模型文件准备
你需要一个 YOLOv8 的 ONNX 格式模型文件(.onnx)。有两种方式获取:
- 自行导出(推荐):如果你有 Python 环境,使用以下命令可以快速导出一个预训练的 YOLOv8n 模型(检测 80 类 COCO 物体)。
执行后会在当前目录生成pip install ultralytics onnx python -c "from ultralytics import YOLO; model = YOLO('yolov8n.pt'); model.export(format='onnx')"yolov8n.onnx文件。 - 直接下载:从 Ultralytics 的官方 GitHub Release 页面或可靠的模型仓库下载预导出的 ONNX 模型。
将得到的.onnx文件(例如yolov8n.onnx)复制到你的 C# 项目目录下,例如Assets/Models/,并在 Visual Studio 中将其属性设置为“如果较新则复制”。
3. 核心原理与流程拆解
在编写代码前,我们必须理解将一张图片送入 YOLOv8 ONNX 模型并得到检测结果的完整数据流。这个过程可以分为三个核心阶段。
3.1 图像预处理:从原始像素到模型输入
YOLOv8 模型有固定的输入尺寸(如 640x640)。我们的输入图片可能是任意大小和通道顺序(OpenCV 默认 BGR)。预处理步骤包括:
- 读取图像:使用
OpenCvSharp的Cv2.ImRead。 - 保持宽高比缩放:将图像等比例缩放至长边等于 640,短边按比例缩放,并在短边两侧填充灰色,使最终图像大小为 640x640。这一步至关重要,直接影响检测精度。
- 颜色空间转换:将 BGR 格式转换为 RGB 格式。
- 归一化:将像素值从 0-255 缩放到 0-1 范围。
- 调整维度:将图像数据从
[高度, 宽度, 通道]的格式转换为[批次大小, 通道, 高度, 宽度](即 NCHW 格式)。批次大小通常为 1。 - 转换为张量:将处理后的数据转换为
float[]数组,并包装成 ONNX Runtime 所需的Tensor<float>。
3.2 模型推理:调用 ONNX Runtime
预处理后的张量被送入 ONNX Runtime 的推理会话(InferenceSession)中。我们需要:
- 指定输入节点的名称(如
images)和输入数据。 - 指定输出节点的名称(如
output0)。 - 调用
Run方法执行推理,得到一个IDisposableReadOnlyCollection<DisposableNamedOnnxValue>的集合,其中包含了模型的原始输出。
3.3 结果后处理:从原始输出到可视化框
YOLOv8 的原始输出是一个形状为[1, 84, 8400]的张量(对于 640x640 输入,8400是锚框数量)。我们需要解析它:
- 解析输出:
84 = 4 (bbox坐标) + 80 (COCO类别分数)。遍历所有 8400 个预测,根据置信度阈值(如 0.5)进行初步筛选。 - 非极大值抑制 (NMS):对于同一类别的多个重叠框,使用 NMS 算法(如
OpenCvSharp.Cv2.Dnn.NMSBoxes)去除冗余框,保留最有可能的一个。 - 坐标反算:将模型输出的归一化坐标(相对于 640x640 输入)反算回原始图像上的像素坐标。这里需要用到之前预处理时记录的缩放比例和填充偏移量。
- 绘制与输出:使用
OpenCvSharp在原始图像上绘制矩形框、类别标签和置信度分数。
4. 完整实战:创建 C# 控制台检测程序
现在,我们将把上述理论转化为可运行的代码。请跟随步骤,创建一个全新的项目。
4.1 创建项目与安装 NuGet 包
- 打开 Visual Studio 2022,选择“创建新项目”。
- 选择“控制台应用”(.NET 8.0),命名为
Yolov8OnnxRuntimeDemo,选择合适的位置创建。 - 在解决方案资源管理器中,右键单击项目 -> “管理 NuGet 程序包”。
- 在“浏览”选项卡中,搜索并安装以下包:
Microsoft.ML.OnnxRuntime(最新稳定版,如 1.16.3)OpenCvSharp4(最新稳定版,如 4.8.0.20230708)OpenCvSharp4.runtime.win(版本需与OpenCvSharp4匹配)System.Drawing.Common(可选,如 6.0.0)
4.2 组织项目结构与添加模型
- 在项目根目录下创建两个文件夹:
Assets/Models和Assets/Images。 - 将你准备好的
yolov8n.onnx模型文件复制到Assets/Models文件夹。 - 准备一张包含常见物体(如人、车、狗)的测试图片(如
test.jpg),复制到Assets/Images文件夹。 - 在 Visual Studio 中,右键单击模型和图片文件,选择“属性”,将“复制到输出目录”设置为“如果较新则复制”。这确保程序运行时能找到这些文件。
4.3 编写核心推理类Yolov8OnnxRuntime
在项目中创建一个新的类文件Yolov8OnnxRuntime.cs。这个类将封装所有检测逻辑。
// Yolov8OnnxRuntime.cs using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; namespace Yolov8OnnxRuntimeDemo { public class Yolov8OnnxRuntime : IDisposable { // 模型相关 private readonly InferenceSession _session; private readonly string _inputName; private readonly int _inputWidth; private readonly int _inputHeight; // COCO 数据集类别名称(YOLOv8n 默认) private readonly string[] _classNames = { "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" }; // 颜色池,用于不同类别的绘制 private readonly Scalar[] _colors; public Yolov8OnnxRuntime(string modelPath, int inputWidth = 640, int inputHeight = 640) { _inputWidth = inputWidth; _inputHeight = inputHeight; // 创建 ONNX Runtime 推理会话 var options = new SessionOptions(); // 可以根据需要设置执行提供程序,例如 CUDA 或 TensorRT 以加速 GPU 推理 // options.AppendExecutionProvider_CUDA(0); // options.AppendExecutionProvider_Tensorrt(0); _session = new InferenceSession(modelPath, options); // 获取输入节点信息(YOLOv8 ONNX 模型通常只有一个输入,名为“images”) _inputName = _session.InputMetadata.Keys.First(); // 初始化随机颜色 var rnd = new Random(); _colors = new Scalar[_classNames.Length]; for (int i = 0; i < _colors.Length; i++) { _colors[i] = new Scalar(rnd.Next(0, 256), rnd.Next(0, 256), rnd.Next(0, 256)); } } /// <summary> /// 执行目标检测 /// </summary> /// <param name="imagePath">输入图像路径</param> /// <param name="confidenceThreshold">置信度阈值</param> /// <param name="nmsThreshold">非极大值抑制阈值</param> /// <returns>绘制了检测框的图像(Mat对象)</returns> public Mat Detect(string imagePath, float confidenceThreshold = 0.5f, float nmsThreshold = 0.5f) { // 1. 使用 OpenCV 读取图像 using var srcImage = Cv2.ImRead(imagePath, ImreadModes.Color); if (srcImage.Empty()) { throw new ArgumentException($"无法读取图像: {imagePath}"); } // 2. 图像预处理 var (inputTensor, scaleFactor, pad) = Preprocess(srcImage); // 3. 准备输入数据 var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(_inputName, inputTensor) }; // 4. 运行推理 using var results = _session.Run(inputs); var outputTensor = results.First().AsTensor<float>(); // 5. 后处理:解析输出并应用 NMS var detections = Postprocess(outputTensor, confidenceThreshold, nmsThreshold, scaleFactor, pad); // 6. 在原始图像上绘制检测结果 var resultImage = srcImage.Clone(); DrawDetections(resultImage, detections); return resultImage; } /// <summary> /// 图像预处理:缩放、填充、归一化、转换格式 /// </summary> private (Tensor<float> tensor, float scale, (int top, int bottom, int left, int right) pad) Preprocess(Mat image) { int srcHeight = image.Height; int srcWidth = image.Width; // 计算缩放比例,保持长宽比 float scale = Math.Min((float)_inputWidth / srcWidth, (float)_inputHeight / srcHeight); int newWidth = (int)(srcWidth * scale); int newHeight = (int)(srcHeight * scale); // 计算填充量,使图像居中 int padX = (_inputWidth - newWidth) / 2; int padY = (_inputHeight - newHeight) / 2; // 缩放图像 using var resized = new Mat(); Cv2.Resize(image, resized, new Size(newWidth, newHeight)); // 创建目标图像并填充灰色 using var padded = new Mat(_inputHeight, _inputWidth, MatType.CV_8UC3, new Scalar(114, 114, 114)); // 将缩放后的图像复制到填充图像的中央 resized.CopyTo(padded[new Rect(padX, padY, newWidth, newHeight)]); // 转换为 RGB 并归一化到 [0, 1] using var rgb = new Mat(); Cv2.CvtColor(padded, rgb, ColorConversionCodes.BGR2RGB); rgb.ConvertTo(rgb, MatType.CV_32FC3, 1.0 / 255.0); // 将数据从 HWC [Height, Width, Channel] 转换为 CHW [Channel, Height, Width] var inputData = new float[3 * _inputHeight * _inputWidth]; int channels = rgb.Channels(); int height = rgb.Height; int width = rgb.Width; unsafe { float* ptr = (float*)rgb.Data; for (int c = 0; c < channels; c++) { for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { // 计算在 HWC 布局中的索引 int hwcIndex = h * width * channels + w * channels + c; // 计算在 CHW 布局中的目标索引 int chwIndex = c * height * width + h * width + w; inputData[chwIndex] = ptr[hwcIndex]; } } } } // 创建张量,形状为 [1, 3, Height, Width] var tensor = new DenseTensor<float>(inputData, new[] { 1, 3, _inputHeight, _inputWidth }); return (tensor, scale, (padY, padY, padX, padX)); } /// <summary> /// 后处理:解析模型输出,应用置信度过滤和 NMS /// </summary> private List<Detection> Postprocess(Tensor<float> output, float confThreshold, float nmsThreshold, float scale, (int top, int bottom, int left, int right) pad) { var detections = new List<Detection>(); // YOLOv8 输出形状: [1, 84, 8400] int dimensions = output.Dimensions[1]; // 84 = 4 (box) + 80 (classes) int numPredictions = output.Dimensions[2]; // 8400 for (int i = 0; i < numPredictions; i++) { // 找到最大类别分数及其索引 float maxScore = float.MinValue; int classId = -1; for (int j = 4; j < dimensions; j++) { float score = output[0, j, i]; if (score > maxScore) { maxScore = score; classId = j - 4; } } // 应用置信度阈值 if (maxScore >= confThreshold) { // 解析边界框坐标 (cx, cy, w, h),相对于 640x640 输入 float cx = output[0, 0, i]; float cy = output[0, 1, i]; float w = output[0, 2, i]; float h = output[0, 3, i]; // 转换为左上角坐标 (x1, y1) 和右下角坐标 (x2, y2) float x1 = (cx - w / 2); float y1 = (cy - h / 2); float x2 = (cx + w / 2); float y2 = (cy + h / 2); // 将坐标映射回原始图像尺寸 // 1. 去除填充 x1 = Math.Max(0, x1 - pad.left); y1 = Math.Max(0, y1 - pad.top); x2 = Math.Min(_inputWidth - pad.left - pad.right, x2 - pad.left); y2 = Math.Min(_inputHeight - pad.top - pad.bottom, y2 - pad.top); // 2. 根据缩放比例反算 x1 /= scale; y1 /= scale; x2 /= scale; y2 /= scale; // 确保坐标在图像范围内 x1 = Math.Max(0, x1); y1 = Math.Max(0, y1); x2 = Math.Min(x2, _inputWidth / scale); // 原始图像宽度 y2 = Math.Min(y2, _inputHeight / scale); // 原始图像高度 detections.Add(new Detection { BoundingBox = new Rect((int)x1, (int)y1, (int)(x2 - x1), (int)(y2 - y1)), Confidence = maxScore, ClassId = classId, ClassName = _classNames[classId] }); } } // 应用非极大值抑制 (NMS) 去除重叠框 var boxes = detections.Select(d => d.BoundingBox).ToList(); var scores = detections.Select(d => d.Confidence).ToList(); var classIds = detections.Select(d => d.ClassId).ToList(); Cv2.Dnn.NMSBoxes(boxes, scores, confThreshold, nmsThreshold, out int[] indices); var nmsDetections = new List<Detection>(); foreach (var index in indices) { nmsDetections.Add(detections[index]); } return nmsDetections; } /// <summary> /// 在图像上绘制检测框和标签 /// </summary> private void DrawDetections(Mat image, List<Detection> detections) { foreach (var det in detections) { var color = _colors[det.ClassId % _colors.Length]; // 绘制矩形框 Cv2.Rectangle(image, det.BoundingBox, color, 2); // 构建标签文本 string label = $"{det.ClassName}: {det.Confidence:F2}"; // 计算文本大小,用于绘制背景 var textSize = Cv2.GetTextSize(label, HersheyFonts.HersheySimplex, 0.5, 1, out int baseline); var textTopLeft = new Point(det.BoundingBox.Left, det.BoundingBox.Top - textSize.Height - baseline); // 绘制文本背景 Cv2.Rectangle(image, new Rect(textTopLeft.X, textTopLeft.Y, textSize.Width, textSize.Height + baseline), color, Cv2.Filled); // 绘制文本 Cv2.PutText(image, label, new Point(det.BoundingBox.Left, det.BoundingBox.Top - baseline), HersheyFonts.HersheySimplex, 0.5, Scalar.White, 1); } } public void Dispose() { _session?.Dispose(); } } /// <summary> /// 检测结果数据结构 /// </summary> public class Detection { public Rect BoundingBox { get; set; } public float Confidence { get; set; } public int ClassId { get; set; } public string ClassName { get; set; } } }4.4 修改主程序Program.cs
现在,在Program.cs中编写主函数,调用我们刚创建的类。
// Program.cs using OpenCvSharp; using System; using System.IO; namespace Yolov8OnnxRuntimeDemo { internal class Program { static void Main(string[] args) { Console.WriteLine("=== C# YOLOv8 ONNX Runtime 目标检测演示 ==="); // 1. 定义路径 (假设模型和图片在 Assets 文件夹下) string modelPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Assets\Models\yolov8n.onnx"); string imagePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Assets\Images\test.jpg"); string outputPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Assets\Images\result.jpg"); // 2. 检查文件是否存在 if (!File.Exists(modelPath)) { Console.WriteLine($"错误: 未找到模型文件 '{modelPath}'。请确保 yolov8n.onnx 已放置在正确位置。"); return; } if (!File.Exists(imagePath)) { Console.WriteLine($"错误: 未找到测试图片 '{imagePath}'。请放置一张名为 test.jpg 的图片。"); return; } Console.WriteLine($"模型路径: {modelPath}"); Console.WriteLine($"输入图片: {imagePath}"); // 3. 创建检测器并执行推理 try { using var detector = new Yolov8OnnxRuntime(modelPath); Console.WriteLine("模型加载成功,开始推理..."); var resultImage = detector.Detect(imagePath, confidenceThreshold: 0.25f, nmsThreshold: 0.45f); // 4. 保存并显示结果 Cv2.ImWrite(outputPath, resultImage); Console.WriteLine($"检测完成!结果已保存至: {outputPath}"); // 可选:使用 OpenCV 窗口显示结果 Cv2.ImShow("Detection Result", resultImage); Cv2.WaitKey(0); // 等待任意按键 Cv2.DestroyAllWindows(); } catch (Exception ex) { Console.WriteLine($"推理过程中发生错误: {ex.Message}"); Console.WriteLine(ex.StackTrace); } Console.WriteLine("程序结束。按任意键退出..."); Console.ReadKey(); } } }4.5 运行与验证
- 确保
Assets/Models/yolov8n.onnx和Assets/Images/test.jpg文件已存在且属性设置正确。 - 在 Visual Studio 中,按
F5或点击“开始调试”运行程序。 - 控制台将输出加载和推理信息。如果一切顺利,将弹出一个窗口显示带有检测框的图片,同时结果图片
result.jpg会保存到输出目录。 - 观察检测框是否准确,标签和置信度是否显示正确。
5. 常见问题与排查思路
即使按照步骤操作,你也可能遇到一些问题。以下是常见问题的排查指南。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
运行时错误:System.DllNotFoundException: 无法加载 DLL 'onnxruntime' | ONNX Runtime 本地库未正确加载。 | 1. 确认安装了正确的Microsoft.ML.OnnxRuntimeNuGet 包。2. 对于 GPU 版本,可能需要额外安装 Microsoft.ML.OnnxRuntime.Gpu并确保 CUDA/cuDNN 环境正确。3. 尝试清理解决方案并重新生成。 |
错误:OpenCvSharp.OpenCVException: (-215:Assertion failed) !_src.empty() in function 'cv::cvtColor' | 预处理时图像为空或读取失败。 | 1. 检查imagePath是否正确,图片文件是否存在。2. 检查 OpenCvSharp 的 native 库是否正确部署( OpenCvSharp4.runtime.win包负责此工作)。3. 在 Preprocess方法开始处,检查padded或rgb矩阵是否成功创建。 |
模型推理输出形状不符合预期(如不是[1, 84, 8400]) | 使用的 ONNX 模型不是标准的 YOLOv8 检测模型,或者是不同版本(如分割、姿态估计模型)。 | 1. 使用 Netron (https://netron.app) 工具打开你的.onnx模型文件,查看输入输出节点的名称和形状。2. 根据 Netron 显示的信息,修改代码中的 _inputName和Postprocess方法中的维度解析逻辑。 |
| 检测框位置严重偏移或大小错误 | 预处理(缩放/填充)或后处理(坐标反算)逻辑有误。 | 1. 在Preprocess方法中,打印或记录scale,padX,padY,newWidth,newHeight的值,检查计算是否正确。2. 在 Postprocess方法中,逐步调试坐标反算公式,确保先减填充再除缩放比例。3. 使用一张简单图片(如中心有一个大矩形)进行测试,便于观察坐标变换。 |
| 程序运行非常慢 | 1. 在 CPU 上运行较大的模型(如yolov8x.onnx)。2. 没有使用 Release 模式编译。 | 1. 尝试使用更小的模型(如yolov8n.onnx或yolov8s.onnx)。2. 在 Visual Studio 顶部将编译模式从 Debug切换到Release,性能会有显著提升。3. 如果拥有 NVIDIA GPU,可以安装 Microsoft.ML.OnnxRuntime.Gpu包,并在创建SessionOptions时启用 CUDA 或 TensorRT 提供程序。 |
Cv2.ImShow窗口一闪而过或无法显示 | 控制台程序在显示后立即退出。 | 确保在Cv2.ImShow后调用了Cv2.WaitKey(0),它会阻塞程序直到有按键输入。我们的示例代码已包含。 |
6. 最佳实践与工程建议
将演示代码转化为生产可用的组件,还需要考虑以下方面:
6.1 性能优化
- 会话复用:
InferenceSession的创建开销较大。在你的应用程序中,应该将其作为单例或长时间存活的对象,在整个生命周期内重复使用,而不是每次检测都新建。 - 批量推理:如果需要对多张图片进行检测,可以尝试将预处理后的张量在批次维度上进行拼接,进行一次批量推理,这通常比循环单张推理更高效。需要修改预处理逻辑以支持批量。
- 异步处理:对于 GUI 应用(如 WPF/WinForms),务必在后台线程(如
Task.Run)中执行耗时的模型推理和图像预处理,避免阻塞 UI 线程导致界面卡顿。 - 模型选择:根据实际场景在速度与精度间权衡。
yolov8n最快,yolov8s是较好的平衡点,yolov8m/l/x精度更高但更慢。
6.2 代码健壮性与可维护性
- 配置化:将模型路径、输入尺寸、置信度阈值、NMS 阈值等参数提取到配置文件(如
appsettings.json)中,便于不同环境部署和调参。 - 异常处理:如示例所示,对文件 IO、模型加载、推理过程进行完整的
try-catch,并记录详细的日志,便于线上问题追踪。 - 资源释放:确保实现了
IDisposable接口,并在使用完毕后释放InferenceSession、Mat等非托管资源,防止内存泄漏。 - 单元测试:为核心类
Yolov8OnnxRuntime编写单元测试,特别是针对Preprocess和Postprocess方法,使用固定的输入和模拟输出来验证坐标转换的正确性。
6.3 集成到工业应用
- 对接工业相机:替换从文件读取图像的逻辑。大多数工业相机 SDK(如海康、大华、Basler)都提供 C# API,可以获取相机帧(通常是
byte[]或IntPtr),将其转换为OpenCvSharp的Mat对象,即可送入检测流程。 - WPF/WinForms 显示:将检测结果的
Mat转换为Bitmap或BitmapSource,然后赋值给 WPF 的Image控件或 WinForms 的PictureBox控件,实现实时视频流的检测显示。 - 结果处理与通信:检测到的目标信息(类别、坐标、置信度)可以封装成结构体或类,通过事件、回调函数或消息队列发送给业务逻辑模块,触发后续的报警、计数、数据存储或与 PLC 通信等操作。
6.4 模型管理与更新
- 模型版本管理:在生产环境中,模型文件应与应用程序二进制文件分离管理。可以通过网络下载、配置文件指定路径等方式动态加载模型,便于模型热更新。
- 监控与告警:监控推理的耗时、帧率以及检测结果的置信度分布。如果性能下降或检测质量异常,应及时发出告警。
通过以上步骤,你不仅成功在 C# 中运行了 YOLOv8 目标检测,还获得了一个结构清晰、易于扩展的代码框架。这个框架是连接 AI 模型与具体工业应用的桥梁,你可以在此基础上,轻松地添加新的预处理逻辑、对接不同的数据源、优化性能,并将其部署到实际的产线检测、安全监控或智能仓储系统中。