【C#】以 BlockingCollection 为核心的多相机 YOLO 检测任务处理框架

以 BlockingCollection 为核心的多相机 YOLO 检测任务处理框架

在工业视觉应用中,我们经常会遇到 一台电脑同时接入多台相机 的场景。比如 10 台相机同时拍摄生产线上的产品,通过硬件 IO 触发采图,然后进行 YOLO 缺陷检测。

如果不合理设计,容易出现以下问题:

  • 回调线程阻塞导致相机触发丢帧
  • 多线程抢占 YOLO 对象导致检测结果错乱
  • 图像内存泄露或 UI 更新异常

本文将以 BlockingCollection 为核心,介绍一个线程安全、支持多相机任务队列的 YOLO 检测框架。


1. 为什么选择 BlockingCollection

BlockingCollection<T> 是 .NET 提供的一个 线程安全的生产者-消费者队列,非常适合处理相机采图任务。

优点:

  • 线程安全:多个线程同时 Add/Take 没问题
  • 阻塞消费:队列为空时,Take 或 GetConsumingEnumerable 会自动等待
  • 容量限制:防止队列无限增长
  • 优雅结束 :调用 CompleteAdding() 后,消费者循环可以自动退出

Tip:BlockingCollection 内部默认基于 ConcurrentQueue<T>,也可以替换为 ConcurrentStack<T>ConcurrentBag<T>


2. 相比 ConcurrentQueue / ConcurrentBag 的优势

特性 ConcurrentQueue ConcurrentBag BlockingCollection
顺序 FIFO 无序 FIFO / 自定义
阻塞
容量限制
支持生产者-消费者模式 ✅ 内置
可结束队列/循环 ✅ CompleteAdding

总结:BlockingCollection 本质上是在 ConcurrentQueue/Bag 基础上加了阻塞、容量、结束控制,更适合多线程任务队列场景。


3. 多相机任务处理思路

针对 10 台相机 IO 触发的场景,设计思路如下:

  1. Update 回调

    • 回调线程只做一件事:将 CameraInfo 入队
    • 避免在回调中执行耗时的检测任务,防止阻塞触发
  2. 后台 WorkerLoop

    • BlockingCollection 队列中取任务
    • 调用 Detection() 进行 YOLO 处理
  3. Detection 处理逻辑

    • 图像预处理(Blob、裁剪、摆正等)
    • YOLO 推理(检测或分割)
    • UI 显示(通过 Dispatcher 确保线程安全)
    • 任务完成后释放图像内存
  4. 停止 / 完整退出

    • 调用 CompleteAdding() 告诉消费者:以后不再有新任务
    • 消费者循环自动退出

4. 核心代码示例

csharp 复制代码
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;

public class CameraProcessor
{
    private BlockingCollection<CameraInfo> taskQueue;
    private CancellationTokenSource cts;
    private Task workerTask;
    private readonly Dispatcher uiDispatcher;

    public CameraProcessor(Dispatcher dispatcher)
    {
        uiDispatcher = dispatcher;
        StartWorker();
    }

    private void StartWorker()
    {
        taskQueue = new BlockingCollection<CameraInfo>();
        cts = new CancellationTokenSource();
        workerTask = Task.Run(() => WorkerLoop(), cts.Token);
    }

    public void Update(CameraInfo info) => taskQueue.Add(info);

    private void WorkerLoop()
    {
        foreach (var info in taskQueue.GetConsumingEnumerable(cts.Token))
        {
            try
            {
                var graphic = GetGraphicBySn(info.SerialNumber);
                if (graphic != null)
                {
                    var results = Detection(graphic, info.Image);

                    // UI 更新
                    uiDispatcher.Invoke(() =>
                    {
                        graphic.StatusText = "检测完成";
                        graphic.LastResults = results;
                    });
                }

                info.Image?.Dispose(); // 避免内存泄漏
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"检测异常: {ex}");
            }
        }
    }

    public void Stop()
    {
        taskQueue.CompleteAdding();
        cts.Cancel();
        workerTask.Wait();
    }

    public void Restart()
    {
        Stop();
        StartWorker();
    }

    private List<YOLOData> Detection(GraphicInfo graphic, HObject image)
    {
        Stopwatch sw = new Stopwatch();
        List<YOLOData> data = new List<YOLOData>();

        // 1. 预处理
        sw.Restart();
        HObject imgProduct = PreprocessImage(graphic, image);
        sw.Stop();
        long preprocessTime = sw.ElapsedMilliseconds;

        // 2. YOLO 推理
        sw.Restart();
        //推理~~~~~
        sw.Stop();
        long detectTime = sw.ElapsedMilliseconds;

        return data;
    }

    private HObject PreprocessImage(GraphicInfo graphic, HObject image)
    {
        HObject imgProduct = null;
        if (graphic.BlobConfig.BlobEnable)
            imageScriptTool.BlobProduct(image, graphic.BlobConfig, out imgProduct);

        if (graphic.ScriptConfig.Preprocessing)
            imageScriptTool.Preprocess(imgProduct ?? image, graphic.ScriptConfig.PreprocessFuncName, out imgProduct);

        if (imgProduct == null)
            imgProduct = image.Clone();

        if (graphic.ScriptConfig.定位摆正)
            imageScriptTool.PosJust(image, imgProduct, out imgProduct, out HTuple FixDeg, false);

        return imgProduct;
    }

    private GraphicInfo GetGraphicBySn(string sn)
    {
        // TODO: 根据相机序列号找到对应的 GraphicInfo
        return null;
    }
}

5. 使用建议

  1. 回调线程只入队

    避免直接在 IO 回调里做检测,防止阻塞相机触发。

  2. 后台线程消费
    WorkerLoop 永远循环消费任务,保证线程安全。

  3. UI 更新

    通过 Dispatcher.Invoke 确保 WPF 控件安全访问。

  4. 停止 / 重启

    • 程序退出时调用 Stop() → 完成队列 → 优雅退出
    • 如果需要"暂停/恢复",可用标志位控制,而不是 CompleteAdding() 再恢复。
  5. 多相机独立 YOLO 实例

    每台相机维护独立的 YOLO 和预处理对象,避免线程竞争。


6. 总结

  • BlockingCollection<T> 是多线程生产者-消费者模式的核心利器,适合高并发图像处理场景
  • 将相机回调与耗时检测解耦,保证系统稳定
  • 完整框架支持 多相机并发检测、UI 安全更新、暂停/停止控制
相关推荐
hez20101 天前
在 .NET 上构建超大托管数组
c#·.net·.net core·gc·clr
雨落倾城夏未凉6 天前
第四章c#方法-参数数组和可选参数(16)
后端·c#
唐青枫7 天前
线程不是越多越快:C#.NET Thread 生命周期、同步与后台工作线程实战
c#·.net
唐青枫8 天前
别只会反射:C#.NET Emit 动态生成代码实战详解
c#·.net
咕白m6258 天前
.NET 环境下 Word 超链接批量提取方案
c#·.net
用户91721561902118 天前
C# 通信协议增量解析:用状态机处理半包和粘包
c#
小码编匠9 天前
C# 工控上位机必备:数据转换工具类与十个核心模块
后端·c#·.net
唐青枫11 天前
别再乱用 StartNew:C#.NET TaskFactory 任务调度实战详解
c#·.net
Artech12 天前
[MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力
ai·c#·agent·memory·maf
Scout-leaf13 天前
C#摸鱼实录——IoC与DI案例详解
c#