C# 上位机 + YOLO 实时检测
以下是针对 C# 上位机 + YOLO 实时检测(通常基于 ONNX Runtime、OpenCvSharp + YOLOv8/v9/v10/v11 等导出ONNX模型)最实用、入门级就能落地的优化技巧。这些方法基本能解决80%的新手遇到的卡顿(低FPS、延迟累积)和闪退/内存飙升问题。
我把内容按优先级和实施难度排序,先做高收益、低成本的改动。
核心优化清单(按优先级)
| 优先级 | 问题类型 | 优化手段 | 预期收益 | 难度 |
|---|---|---|---|---|
| ★★★★★ | 内存泄漏+闪退 | using 块 / Dispose 所有 Mat / Bitmap / Tensor | 内存稳定,不再线性增长 | 低 |
| ★★★★★ | 帧堆积+延迟 | 引入单帧缓冲(只保留最新一帧) | 延迟从几秒降到<100ms | 低 |
| ★★★★★ | UI卡死 | 推理放到BackgroundWorker / Task | 界面流畅,不假死 | 中 |
| ★★★★☆ | 推理太慢 | 开启GPU(CUDA / DirectML) | FPS 提升3–10倍 | 中 |
| ★★★★☆ | 推理太慢 | 换小模型(nano/tiny/s)+ 降输入尺寸(320/416) | FPS 提升2–5倍 | 低 |
| ★★★☆☆ | 内存/速度 | 开启半精度FP16(如果支持) | 速度+20–50%,内存减半 | 中 |
| ★★★☆☆ | 帧率不稳定 | 固定推理频率 + 跳帧策略 | 更平滑体验 | 中 |
1. 最致命问题:不释放资源 → 内存泄漏 → 闪退
最常见写法(有问题):
while (true)
{
Mat frame = camera.QueryFrame(); // 不释放
var bitmap = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame); // 不释放
var results = session.Run(...); // Tensor 不释放
pictureBox1.Image = bitmap; // 旧的也没释放
}
正确写法(using + Dispose):
using Mat frame = camera.QueryFrame();
if (frame.Empty()) continue;
using var bitmap = frame.ToBitmap(); // 或用 Mat 直接画框后 ToBitmap
// 推理(假设用 OnnxRuntime + YOLO ONNX)
using var inputs = new DisposableDictionary<string, IDisposable>();
inputs["images"] = CreateInputTensor(frame); // 自己封装成一次性 Tensor
using var results = session.Run(inputs);
// 处理 results ...
pictureBox1.InvokeIfRequired(() =>
{
if (pictureBox1.Image != null) pictureBox1.Image.Dispose();
pictureBox1.Image = bitmap.Clone(); // Clone 防止 UI 线程和推理线程冲突
});
关键点:
- 所有 Mat、Bitmap、OrtValue / OrtTensor 都要用
using或手动Dispose() - PictureBox 的 Image 每次赋值前先 Dispose 旧的
- 不要在循环里反复
new Bitmap()或new Mat()而不释放
2. 解决画面延迟越来越大(帧堆积)
经典错误:用 Queue 或 ConcurrentQueue 存所有帧,结果越堆越多。
推荐方案:只保留最新一帧(覆盖式缓冲)
// 类成员
private Mat latestFrame = null;
private readonly object lockObj = new();
// 采集线程(Task.Run 或 BackgroundWorker)
while (!cts.IsCancellationRequested)
{
using var temp = camera.QueryFrame();
if (temp.Empty()) continue;
lock (lockObj)
{
latestFrame?.Dispose();
latestFrame = temp.Clone(); // 只保留最新
}
await Task.Delay(5); // 轻微限速,避免 CPU 100%
}
// 推理线程(另一个 Task)
while (!cts.IsCancellationRequested)
{
Mat frameToInfer = null;
lock (lockObj)
{
if (latestFrame == null) continue;
frameToInfer = latestFrame.Clone();
}
if (frameToInfer == null) continue;
// 推理、画框...
using var resultBitmap = ProcessFrame(frameToInfer); // 包含推理+画框
pictureBox1.Invoke(() =>
{
pictureBox1.Image?.Dispose();
pictureBox1.Image = resultBitmap.Clone();
});
frameToInfer.Dispose();
}
这样延迟永远只≈单帧推理时间,不会累积。
3. 别在 UI 线程做推理(最常见卡顿原因)
// 推荐:用 Task + ContinueWith 或 BackgroundWorker
private async void btnStart_Click(...)
{
cts = new CancellationTokenSource();
await Task.Run(() => InferenceLoop(cts.Token), cts.Token);
}
或使用 System.ComponentModel.BackgroundWorker(新手最友好)。
4. 快速提速组合拳(不改模型)
- 模型选 YOLOv8n / v8s / YOLOv11n(nano 版),不要直接用 m/l/x
- 导出 ONNX 时指定 imgsz=320 或 416(别用默认640)
- ONNX Runtime 开启 GPU(最重要!)
// SessionOptions 示例(CUDA)
var sessionOptions = new SessionOptions();
sessionOptions.AppendExecutionProvider_CUDA(0); // 或 DirectML:AppendExecutionProvider_DML(0)
// 如果用 DirectML(支持 AMD/Intel/NVIDIA)
sessionOptions.AppendExecutionProvider_DML(0);
- 尝试 FP16(部分显卡支持)
sessionOptions.AddSessionConfigEntry("session.use_device_allocator_for_initializers", "1");
sessionOptions.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
5. 额外小技巧(锦上添花)
- 推理频率控制:每隔 40–50ms 才推理一次(≈20–25FPS),其余帧直接显示原图或上一次结果
- 画框尽量用 OpenCvSharp 的
Cv2.Rectangle而不是 GDI+(更快) - PictureBox 设为 DoubleBuffered = true
- 程序启动时预热模型:先跑 5–10 张无关图
按上面顺序改一遍,绝大多数 i5 + 入门独显(GTX 1650 / RTX 3050)都能跑到 20–40 FPS 稳定运行,内存控制在 1–2GB 内不飙升。
以下是针对 C# 上位机 + YOLO 实时检测 的更多实用代码示例,全部基于常见组合:OpenCvSharp + Microsoft.ML.OnnxRuntime(支持 CPU/GPU),聚焦优化卡顿和闪退问题。
这些示例假设你已经:
- 安装 NuGet:
OpenCvSharp4、OpenCvSharp4.runtime.win、Microsoft.ML.OnnxRuntime(或Microsoft.ML.OnnxRuntime.Gpu/.DirectML) - 导出 YOLOv8/v11 的 ONNX 模型(推荐 yolov8n.onnx 或 yolov11n.onnx,imgsz=416 或 320)
示例 1:完整单帧覆盖 + 双线程(采集 & 推理分离) + 资源释放(最推荐入门方案)
using OpenCvSharp;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using System;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
public partial class MainForm : Form
{
private CancellationTokenSource cts;
private Mat latestFrame;
private readonly object frameLock = new();
private Bitmap displayBitmap; // 用于 UI 显示
private InferenceSession session;
private VideoCapture capture;
private string[] labels; // coco.names 或你的类别列表
public MainForm()
{
InitializeComponent();
pictureBox1.DoubleBuffered = true;
pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
// 加载模型(GPU 示例用 CUDA)
var options = new SessionOptions();
options.AppendExecutionProvider_CUDA(0); // 或 .AppendExecutionProvider_DML(0) for AMD/Intel
// options.AppendExecutionProvider_CPU(0); // fallback
session = new InferenceSession("yolov8n.onnx", options);
// 读取类别标签(自己准备 txt 文件,每行一个类别)
labels = System.IO.File.ReadAllLines("coco.names");
}
private async void btnStart_Click(object sender, EventArgs e)
{
if (cts != null) return;
cts = new CancellationTokenSource();
capture = new VideoCapture(0); // 0=默认摄像头
if (!capture.IsOpened())
{
MessageBox.Show("摄像头打开失败");
return;
}
// 采集线程
_ = Task.Run(() => CaptureLoop(cts.Token));
// 推理 & 显示线程
await Task.Run(() => InferenceAndDisplayLoop(cts.Token));
}
private void CaptureLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
using var frame = new Mat();
if (!capture.Read(frame) || frame.Empty()) continue;
lock (frameLock)
{
latestFrame?.Dispose();
latestFrame = frame.Clone(); // 只保留最新帧
}
Task.Delay(10, token).Wait(); // 控制采集速率,避免 CPU 爆满
}
}
private void InferenceAndDisplayLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Mat inferFrame = null;
lock (frameLock)
{
if (latestFrame == null || latestFrame.IsDisposed) continue;
inferFrame = latestFrame.Clone();
}
if (inferFrame == null) continue;
try
{
// 预处理(YOLOv8 通常需要 640x640 或你导出的尺寸,这里假设 640)
using var resized = new Mat();
Cv2.Resize(inferFrame, resized, new Size(640, 640));
using var blob = Cv2.Dnn.BlobFromImage(resized, 1.0 / 255.0, resized.Size(), new Scalar(), swapRB: true, crop: false);
// 输入 Tensor (1,3,640,640)
var inputTensor = new DenseTensor<float>(blob.GetData<float>(), new[] { 1, 3, 640, 640 });
var inputs = new[] { NamedOnnxValue.CreateFromTensor("images", inputTensor) };
// 推理
using var results = session.Run(inputs);
// 后处理(简单 NMS 示例,实际建议用更完整的 post-process)
var output = results.First().AsTensor<float>(); // [1,84,8400] 或 [1,56,8400] 等,根据模型调整
using var resultImg = DrawDetections(inferFrame, output);
// 更新 UI
this.Invoke((MethodInvoker)(() =>
{
displayBitmap?.Dispose();
displayBitmap = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(resultImg);
pictureBox1.Image = displayBitmap?.Clone(); // Clone 避免跨线程问题
}));
}
catch (Exception ex)
{
Console.WriteLine("推理异常: " + ex.Message);
}
finally
{
inferFrame?.Dispose();
}
Task.Delay(40, token).Wait(); // 约 25 FPS 推理频率
}
}
private Mat DrawDetections(Mat img, Tensor<float> output)
{
// 简化版后处理:假设 output 是 [1, 84, n] (xywh + conf + classes)
// 实际项目建议封装一个 YOLO PostProcess 类
var mat = img.Clone();
// ... 解析 boxes, scores, classIds,进行 NMS ...
// 示例:画第一个检测框(伪代码)
// Cv2.Rectangle(mat, new Rect(x, y, w, h), Scalar.Red, 2);
// Cv2.PutText(mat, $"{labels[classId]} {conf:F2}", new Point(x, y-10), HersheyFonts.HersheySimplex, 0.9, Scalar.Red, 2);
return mat;
}
private void btnStop_Click(object sender, EventArgs e)
{
cts?.Cancel();
cts?.Dispose();
cts = null;
capture?.Release();
latestFrame?.Dispose();
displayBitmap?.Dispose();
pictureBox1.Image = null;
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
btnStop_Click(null, null);
session?.Dispose();
base.OnFormClosing(e);
}
}
示例 2:使用 YoloSharp / YoloDotNet 等封装库(更简单,推荐新手)
一些社区库已经帮你处理了预处理/后处理/NMS/GPU切换:
- YoloSharp(支持 YOLO11,NuGet: YoloSharp 或 YoloSharp.Gpu)
using Compunet.YoloSharp;
using var predictor = new YoloPredictor("yolov11n.onnx", new YoloPredictorOptions
{
ExecutionProvider = ExecutionProvider.Cuda // 或 DirectML / Cpu
});
while (!token.IsCancellationRequested)
{
using var frame = capture.QueryFrame();
if (frame.Empty()) continue;
var results = predictor.Detect(frame); // 自动返回 YoloResult 列表
using var drawn = frame.Clone();
foreach (var box in results.Boxes)
{
Cv2.Rectangle(drawn, box.Bounds, Scalar.Red, 2);
Cv2.PutText(drawn, $"{box.Class} {box.Confidence:F2}", ...);
}
pictureBox1.Image?.Dispose();
pictureBox1.Image = drawn.ToBitmap();
}
- YoloDotNet(NuGet: YoloDotNet,支持多种 YOLO 变体)
类似用法,参考其 GitHub demo。
额外小示例:预热模型(启动后先跑几帧避免首次卡顿)
private void WarmUpModel()
{
using var dummy = new Mat(640, 640, MatType.CV_8UC3, Scalar.Black);
var blob = Cv2.Dnn.BlobFromImage(dummy, ...);
var tensor = new DenseTensor<float>(...);
session.Run(new[] { NamedOnnxValue.CreateFromTensor("images", tensor) });
// 跑 3~5 次
}
这些代码片段组合使用,能让大多数入门项目达到 20~35 FPS(视模型和显卡),内存稳定不泄漏。
如果你的模型输出形状不同(例如 yolov8 seg / pose / obb),或想贴出你当前的推理后处理代码,我可以再帮你细化 NMS / 画框部分。加油!
如果还有具体代码片段或报错,可以贴出来,我再帮你针对性定位。祝检测顺畅!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)