以 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 触发的场景,设计思路如下:
-
Update 回调
- 回调线程只做一件事:将
CameraInfo
入队 - 避免在回调中执行耗时的检测任务,防止阻塞触发
- 回调线程只做一件事:将
-
后台 WorkerLoop
- 从
BlockingCollection
队列中取任务 - 调用
Detection()
进行 YOLO 处理
- 从
-
Detection 处理逻辑
- 图像预处理(Blob、裁剪、摆正等)
- YOLO 推理(检测或分割)
- UI 显示(通过 Dispatcher 确保线程安全)
- 任务完成后释放图像内存
-
停止 / 完整退出
- 调用
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. 使用建议
-
回调线程只入队
避免直接在 IO 回调里做检测,防止阻塞相机触发。
-
后台线程消费
WorkerLoop
永远循环消费任务,保证线程安全。 -
UI 更新
通过
Dispatcher.Invoke
确保 WPF 控件安全访问。 -
停止 / 重启
- 程序退出时调用
Stop()
→ 完成队列 → 优雅退出 - 如果需要"暂停/恢复",可用标志位控制,而不是
CompleteAdding()
再恢复。
- 程序退出时调用
-
多相机独立 YOLO 实例
每台相机维护独立的 YOLO 和预处理对象,避免线程竞争。
6. 总结
BlockingCollection<T>
是多线程生产者-消费者模式的核心利器,适合高并发图像处理场景- 将相机回调与耗时检测解耦,保证系统稳定
- 完整框架支持 多相机并发检测、UI 安全更新、暂停/停止控制