最近后台总有粉丝私信我,说想做一个“看得见、能操作”的实时目标检测工具,既要用C# WinForm做可视化界面(毕竟工业场景里上位机还是C#的天下),又要结合当下最火的YOLOv8保证检测精度和速度。其实这个需求看似复杂,本质上就是把YOLOv8的推理能力和C#的界面开发能力结合起来,再加上摄像头的视频流处理就行。

我前阵子刚好给工厂做过一个类似的小项目,用来检测生产线上的零件是否到位,今天就把整个过程手把手拆解出来,从环境配置到代码实现,再到踩坑优化,全给大家讲透。哪怕你是刚接触C#和YOLO的新手,跟着步骤走也能做出成品。

一、先搞清楚:我们要用到哪些工具和技术?

在开始之前,先梳理一下核心依赖,避免大家走弯路。其实整个流程的核心就是:

“YOLOv11模型转ONNX格式 + C# WinForm调用ONNX模型 + AForge/OpenCVSharp处理视频流”

推荐技术栈(2026年新手最友好组合)

用途 推荐工具/库 为什么选它(新手视角) 替代方案(次选)
开发环境 Visual Studio 2022(社区版免费) 界面拖拽 + 调试最友好 VS Code(需要自己配环境)
.NET版本 .NET 9.0(或 .NET 8.0) 性能最好、跨平台支持最完善 .NET Framework 4.8(老项目)
YOLO模型推理 YoloSharp 或 YoloDotNet(NuGet) 最简单,几行代码就能跑 Microsoft.ML.OnnxRuntime(更底层)
模型格式 ONNX(yolov11n.onnx) C#最友好,不依赖Python环境 TensorFlow .NET(已过时)
视频采集 AForge.Video.DirectShow 支持大多数USB工业相机/普通摄像头,开箱即用 OpenCVSharp4(功能更强但稍复杂)
图像绘制 SkiaSharp.Views.WinForms 画框速度快、效果好、跨平台 System.Drawing(老项目常用)
PLC/报警联动 NModbus4(可选) Modbus TCP/RTU最常用,几行代码就能写线圈 Snap7(西门子S7专用)

二、环境准备(30分钟内必须搞定)

1. 安装 Visual Studio 2022 社区版
  • 官网下载:https://visualstudio.microsoft.com/zh-hans/downloads/
  • 安装时至少勾选:
    • .NET 桌面开发
    • 使用 C# 的 .NET 跨平台开发(可选,但推荐)
2. 创建项目
  • 打开VS → 创建新项目 → 搜索 “Windows 窗体应用” → 选 .NET 9.0(或 .NET 8.0)
  • 项目名称随意,例如:YoloWinFormDemo
3. 安装必须的 NuGet 包

右键项目 → 管理NuGet程序包 → 浏览 → 搜索并安装:

Install-Package YoloSharp                     # YOLO推理(最简单)
Install-Package SkiaSharp.Views.WinForms      # 高效画框
Install-Package AForge.Video.DirectShow       # 摄像头采集
Install-Package System.Drawing.Common         # .NET 8+ 必须
# 可选(需要PLC联动时安装)
Install-Package NModbus4
4. 获取 YOLOv11 模型(最关键一步)

推荐新手直接下载这个(416×416,轻量、速度快、精度够用):

  • 下载链接:https://github.com/ultralytics/assets/releases/download/v8.3.0/yolov11n.onnx
    (大小约6MB,下载后重命名为 yolov11n.onnx

操作步骤

  1. 在项目根目录新建文件夹 models
  2. 把下载的模型拖进去
  3. 右键模型文件 → 属性 → 复制到输出目录 = 始终复制

三、完整代码实现(单文件可运行版)

新建项目后,直接把下面全部代码粘贴到 Form1.cs,覆盖原有内容即可。

using System;
using System.Collections.Concurrent;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using AForge.Video;
using AForge.Video.DirectShow;
using SkiaSharp;
using YoloSharp;

namespace YoloWinFormDemo
{
    public partial class Form1 : Form
    {
        // ==================== 控件 ====================
        private PictureBox picPreview;
        private Label lblStatus;
        private Button btnStart, btnStop;

        // ==================== YOLO & 采集 ====================
        private YoloPredictor yolo;
        private VideoCaptureDevice videoSource;
        private readonly ConcurrentQueue<Bitmap> frameQueue = new();
        private volatile bool isProcessing = false;
        private CancellationTokenSource cts = new();

        // FPS 计算用
        private int frameCount = 0;
        private DateTime lastFpsTime = DateTime.Now;

        // Letterbox 反算参数
        private float scale = 1f;
        private int padX, padY;

        public Form1()
        {
            InitializeComponent();
            SetupUI();
            this.Load += Form1_Load;
            this.FormClosing += Form1_FormClosing;
        }

        private void SetupUI()
        {
            this.Text = "C# YOLO 实时目标检测(新手友好版)";
            this.Size = new Size(1000, 700);
            this.StartPosition = FormStartPosition.CenterScreen;

            picPreview = new PictureBox { Dock = DockStyle.Fill, SizeMode = PictureBoxSizeMode.Zoom };
            lblStatus = new Label { Text = "状态:未启动 | FPS: --", Dock = DockStyle.Bottom, Height = 40, Font = new Font("微软雅黑", 12), BackColor = Color.Black, ForeColor = Color.Lime };
            btnStart = new Button { Text = "启动检测", Width = 140, Height = 45 };
            btnStop = new Button { Text = "停止", Width = 140, Height = 45 };

            btnStart.Click += BtnStart_Click;
            btnStop.Click += BtnStop_Click;

            var panel = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 60, BackColor = Color.FromArgb(30, 30, 30) };
            panel.Controls.Add(btnStart);
            panel.Controls.Add(btnStop);
            panel.Controls.Add(lblStatus);

            this.Controls.Add(picPreview);
            this.Controls.Add(panel);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // 加载模型(只需一次)
            try
            {
                yolo = new YoloPredictor("models/yolov11n.onnx", YoloTask.Detect);
                lblStatus.Text = "模型加载成功 | 等待启动摄像头...";
            }
            catch (Exception ex)
            {
                lblStatus.Text = "模型加载失败!请检查 models/yolov11n.onnx 是否存在";
                MessageBox.Show(ex.Message);
            }

            // 启动后台处理线程
            _ = Task.Run(ProcessFramesAsync, cts.Token);
        }

        private void BtnStart_Click(object sender, EventArgs e)
        {
            var devices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
            if (devices.Count == 0)
            {
                MessageBox.Show("未找到摄像头!请检查设备管理器");
                return;
            }

            videoSource = new VideoCaptureDevice(devices[0].MonikerString);
            videoSource.NewFrame += Video_NewFrame;
            videoSource.Start();

            btnStart.Enabled = false;
            btnStop.Enabled = true;
            lblStatus.Text = "状态:运行中 | FPS: 计算中...";
        }

        private void BtnStop_Click(object sender, EventArgs e)
        {
            videoSource?.SignalToStop();
            btnStart.Enabled = true;
            btnStop.Enabled = false;
            lblStatus.Text = "状态:已停止";
        }

        // ==================== 采集最新帧(只保留最新一帧,防堆积) ====================
        private void Video_NewFrame(object sender, NewFrameEventArgs e)
        {
            // 优化:丢弃旧帧,只保留最新一帧
            while (frameQueue.Count > 1)
            {
                frameQueue.TryDequeue(out var old);
                old?.Dispose();
            }
            frameQueue.Enqueue((Bitmap)e.Frame.Clone());
        }

        // ==================== 后台推理线程(防UI卡顿) ====================
        private async Task ProcessFramesAsync()
        {
            while (!cts.IsCancellationRequested)
            {
                if (frameQueue.TryDequeue(out var frame) && !isProcessing)
                {
                    isProcessing = true;
                    try
                    {
                        await Task.Run(() => DetectAndDraw(frame));
                    }
                    finally
                    {
                        frame?.Dispose();
                        isProcessing = false;
                    }
                }
                else
                {
                    await Task.Delay(5);
                }
            }
        }

        // ==================== 核心:预处理 + 推理 + 坐标反算 + 绘图 ====================
        private void DetectAndDraw(Bitmap original)
        {
            int originalWidth = original.Width;
            int originalHeight = original.Height;

            // 步骤1:Letterbox 预处理(带黑边缩放,保持比例)
            var processed = Letterbox(original, 416, 416);

            // 步骤2:YOLO推理(后台执行)
            var results = yolo.Detect(processed);

            // 步骤3:坐标反算(从416x416映射回原始图像)
            var finalResults = new System.Collections.Generic.List<YoloResult>();
            foreach (var r in results)
            {
                if (r.Confidence < 0.5f) continue;

                float x = (r.BoundingBox.X - padX) / scale;
                float y = (r.BoundingBox.Y - padY) / scale;
                float w = r.BoundingBox.Width / scale;
                float h = r.BoundingBox.Height / scale;

                // 限制坐标不超出原始图像边界
                x = Math.Max(0, Math.Min(x, originalWidth));
                y = Math.Max(0, Math.Min(y, originalHeight));
                w = Math.Min(w, originalWidth - x);
                h = Math.Min(h, originalHeight - y);

                finalResults.Add(new YoloResult
                {
                    Label = r.Label,
                    Confidence = r.Confidence,
                    BoundingBox = new SKRect(x, y, x + w, y + h)
                });
            }

            // 步骤4:绘制结果到原始图像(而不是416x416的processed)
            using var skOriginal = SKBitmap.FromImage(SKImage.FromBitmap(original));
            using var canvas = new SKCanvas(skOriginal);
            foreach (var r in finalResults)
            {
                var rect = r.BoundingBox;
                using var paint = new SKPaint { Style = SKPaintStyle.Stroke, Color = SKColors.Red, StrokeWidth = 4 };
                canvas.DrawRect(rect, paint);

                canvas.DrawText($"{r.Label.Name} {r.Confidence:P0}", rect.X, rect.Y - 5,
                    new SKPaint { Color = SKColors.Yellow, TextSize = 20 });
            }

            // 步骤5:更新UI(主线程安全)
            this.Invoke((MethodInvoker)delegate
            {
                picPreview.Image?.Dispose();
                picPreview.Image = skOriginal.ToBitmap();

                // FPS 计算
                frameCount++;
                if ((DateTime.Now - lastFpsTime).TotalSeconds >= 1)
                {
                    lblStatus.Text = $"状态:运行中 | FPS: {frameCount}";
                    frameCount = 0;
                    lastFpsTime = DateTime.Now;
                }
            });
        }

        // ==================== Letterbox + 记录缩放参数(用于坐标反算) ====================
        private float scale = 1f;
        private int padX, padY;

        private SKBitmap Letterbox(Bitmap src, int targetW, int targetH)
        {
            float ratio = Math.Min((float)targetW / src.Width, (float)targetH / src.Height);
            int newW = (int)(src.Width * ratio);
            int newH = (int)(src.Height * ratio);

            scale = ratio;
            padX = (targetW - newW) / 2;
            padY = (targetH - newH) / 2;

            var skSrc = SKBitmap.FromImage(SKImage.FromBitmap(src));
            var dst = new SKBitmap(targetW, targetH);

            using var canvas = new SKCanvas(dst);
            canvas.Clear(SKColors.Black); // 黑边填充(YOLO官方推荐)

            canvas.DrawBitmap(skSrc, new SKRect(padX, padY, padX + newW, padY + newH));

            skSrc.Dispose();
            return dst;
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            cts.Cancel();
            videoSource?.SignalToStop();
            yolo?.Dispose();
            base.OnFormClosing(e);
        }
    }

    // 简单结果包装类
    public class YoloResult
    {
        public YoloLabel Label { get; set; }
        public float Confidence { get; set; }
        public SKRect BoundingBox { get; set; }
    }
}

三、最常见报错 & 一键解决口诀

报错关键词 原因 解决口诀(记住就行)
Could not load model / file not found onnx文件没复制到输出目录 右键模型文件 → 属性 → 始终复制
Out of memory / 内存不足 Bitmap没释放 所有Bitmap/SKBitmap必须用using块
UI无响应 / 卡死 推理放在主线程 必须用 Task.Run + Invoke 更新UI
检测框位置完全错 没做坐标反算 必须记录 Letterbox 的 scale/padX/padY
FPS很低(<10) 模型太大 / 分辨率太高 用 yolov11n + 416×416 输入
摄像头打不开 被其他软件占用 关闭QQ/微信视频、OBS等软件

四、快速上手 & 下一步建议

  1. 新建WinForms项目(.NET 9)
  2. 安装上面4个NuGet包
  3. 下载 yolov11n.onnx(416×416)放 models 文件夹 → 属性 → 始终复制
  4. 替换 Form1.cs 为上面全部代码
  5. F5运行 → 插摄像头 → 点击“启动检测”

下一步进阶(建议顺序)

  1. 加PLC报警:检测到异常 → 写Modbus线圈
  2. 加语音播报:用NAudio或Windows Speech
  3. 加异常截图保存 + Excel报表
  4. 加GPU加速:安装 Microsoft.ML.OnnxRuntime.DirectML

点赞+收藏,这可能是你今年最适合新手的C# YOLO上位机教程!

更多推荐