最近几年做过一些多相机机器视觉项目以后,我发现一个很有意思的现象:
单相机Demo运行正常,并不代表把相机数量增加到6台、8台甚至更多以后,软件还能正常运行。
很多问题只有在线连续运行几个小时以后才会出现:
检测越来越慢;
图像开始堆积;
内存持续上涨;
UI越来越卡;
偶尔出现产品超时;
模型运行时间突然增加;
图片保存拖慢整个检测流程。
本文不讨论具体视觉算法,而是整理一下多相机 VisionPro 项目中比较容易遇到的性能问题,以及我目前比较常用的一套处理思路。
为了方便说明,下面以一个多相机高速检测系统作为示例,具体项目业务信息做了抽象处理。
1. 一个典型的多相机视觉流程
实际项目通常类似:
Sensor / PLC
↓
Camera Trigger
↓
Image Acquisition
↓
Image Matching
↓
Image Stitching
↓
VisionPro / DL
↓
Graphic Rendering
↓
Result
↓
PLC
↓
Image Save
如果是单相机,这条链路比较简单。
但多相机情况下,实际上是很多条链路同时执行:
Camera1 ─┐
Camera2 ─┤
Camera3 ─┤
Camera4 ─┤
Camera5 ─┤→ Match → Inspection
Camera6 ─┤
Camera7 ─┤
Camera8 ─┤
Camera9 ─┘
任何一个环节阻塞,都可能影响整个CT。
2. 第一类问题:图像匹配错误
多相机系统首先要解决的并不是算法,而是:
哪几张图片属于同一个产品?
尤其当多个相机由同一个传感器触发时,不能简单认为:
Camera1第100张
=
Camera2第100张
因为实际运行过程中可能出现:
触发延迟;
网络延迟;
相机曝光不同;
偶发掉帧;
SDK回调时间不同。
更可靠的方法是给每张图像增加采集时间:
public class CameraFrame
{
public int CameraId { get; set; }
public long FrameId { get; set; }
public DateTime Timestamp { get; set; }
public ICogImage Image { get; set; }
}
然后根据时间戳进行配对。
例如:
Cam4 10:20:01.120
Cam5 10:20:01.126
Cam6 10:20:01.123
如果允许时间误差:
±20~30ms
就可以认为它们属于同一次触发。
实际项目中的容差需要根据硬件触发方式和相机响应时间测试确定。
3. 不要在相机回调里面直接运行ToolBlock
非常常见的代码:
private void OnImageReceived(ICogImage image)
{
_toolBlock.Inputs["InputImage"].Value = image;
_toolBlock.Run();
ShowResult();
SaveImage();
}
这种方式在低速项目可能正常。
高速多相机项目里风险很大。
相机回调应该尽可能短:
private void OnImageReceived(CameraFrame frame)
{
if (!_imageQueue.TryAdd(frame))
{
WriteLog("Image queue full.");
}
}
然后Worker处理:
private void ProcessLoop()
{
foreach (CameraFrame frame in
_imageQueue.GetConsumingEnumerable())
{
try
{
ProcessFrame(frame);
}
catch (Exception ex)
{
WriteLog(ex.ToString());
}
}
}
4. VisionPro ToolBlock不要被多个线程同时Run
如果多个线程共享一个:
CogToolBlock
然后同时执行:
_toolBlock.Run();
很容易出现不可预期的问题。
我的处理方式通常是:
Camera/Station
↓
独立Vision Instance
↓
独立ToolBlock
例如:
Station1 → ToolBlock1
Station2 → ToolBlock2
Station3 → ToolBlock3
如果必须共享资源,就需要明确做同步。
但对于性能敏感的视觉程序,我更倾向于让运行实例之间尽量独立。
5. 图像拼接不要重复创建大量Bitmap
多相机拼接另外一个非常容易被忽略的问题就是:
ICogImage
↓
Bitmap
↓
Clone
↓
Draw
↓
Bitmap
↓
ICogImage
如果每一帧进行大量图像格式转换,很容易产生:
大量内存分配;
GC压力;
GDI对象;
额外Copy;
CPU消耗。
所以在VisionPro项目中,我通常建议尽可能保持:
ICogImage → VisionPro
只有最终需要保存或者特殊处理时才转换。
6. 为什么程序刚启动很快,运行两小时以后越来越慢
如果出现这种现象,我一般首先检查四类问题。
第一类是内存。
重点检查:
Bitmap有没有Dispose
Graphics有没有Dispose
CogRecord有没有长期持有
历史结果有没有无限保存
第二类是队列。
记录:
ImageQueue.Count
SaveQueue.Count
ResultQueue.Count
如果Queue不断增长,说明生产速度大于消费速度。
第三类是磁盘IO。
大量NG/OK图像同时保存时,SSD可能成为瓶颈。
第四类是模型资源竞争。
如果多个工位同时调用GPU模型,单次模型运行耗时可能会明显波动。
7. 图片保存为什么一定要单独做队列
不要:
RunVision();
SaveImage();
SendPLC();
而应该:
RunVision();
SendPLC();
_saveQueue.TryAdd(saveTask);
保存Worker:
private void SaveLoop()
{
foreach (ImageSaveTask task in
_saveQueue.GetConsumingEnumerable())
{
try
{
SaveImage(task);
}
catch (Exception ex)
{
WriteLog(ex.ToString());
}
}
}
这样磁盘IO不会直接进入视觉CT。
8. OK和NG图片可以采用不同保存策略
很多项目实际上没必要永久保存全部OK图。
例如:
NG:全部保存
OK:按比例保存
异常:全部保存
调试模式:全部保存
这样既能保留追溯能力,也可以显著降低硬盘压力。
具体保存策略当然要根据客户追溯要求决定。
9. UI很可能是隐藏的性能杀手
多相机项目很容易出现:
算法其实已经运行完成;
但CogRecordDisplay还在绘制。
特别是包含:
大量Region;
大量Line;
大量文字;
缺陷Graphic;
高分辨率图像。
因此我通常会把:
VisionTime
和:
DisplayTime
分开统计。
不要只统计:
Stopwatch sw = Stopwatch.StartNew();
RunAll();
sw.Stop();
因为这样不知道慢在哪里。
10. 推荐给每个步骤单独统计时间
例如:
[Station3]
Grab 18 ms
Pair 3 ms
Stitch 26 ms
Vision 72 ms
DL 95 ms
Graphic 11 ms
PLC 2 ms
SaveQueue 1 ms
Total 228 ms
代码非常简单:
Stopwatch sw = Stopwatch.StartNew();
RunVision();
long visionTime = sw.ElapsedMilliseconds;
RunDL();
long dlTime =
sw.ElapsedMilliseconds - visionTime;
真正重要的不是Stopwatch怎么写。
而是:
一旦项目发生性能异常,你是否能够通过日志定位具体步骤。
11. 建议增加性能异常日志
例如正常模型耗时:
80~130ms
那么可以设置:
if (modelTime > 200)
{
WriteLog(
"Model running over, time-consuming "
+ modelTime
+ " ms");
}
后面统计一天或者一周日志,就能看到:
平均值;
最大值;
异常次数;
异常时间段。
相比现场盯着软件看,这种方式效率高很多。
12. 队列必须监控
实际运行建议周期性记录:
FrameQueue = 1
InspectionQueue = 0
SaveQueue = 7
假设突然变成:
FrameQueue = 18
InspectionQueue = 30
SaveQueue = 420
就说明系统已经出现明显积压。
甚至可以设置报警:
if (_saveQueue.Count > 500)
{
RaiseAlarm("Image save queue backlog.");
}
13. 多相机系统真正需要优化的是整条链路
做这种项目以后我最大的感受是:
不要只盯着:
ToolBlock.Run()
因为整个CT可能是:
Camera
+
Network
+
Image Copy
+
Stitch
+
VisionPro
+
DeepLearning
+
Graphic
+
UI
+
Disk
+
PLC
VisionPro本身运行80ms,并不意味着系统CT就是80ms。
所以性能优化一定要先:
测量。
再:
定位。
最后:
优化。
没有数据直接改代码,很多时候只是把问题从一个地方移动到另一个地方。
总结
一个稳定的多相机机器视觉程序,本质上是一个实时数据处理系统。
视觉算法只是其中一个消费者。
真正决定系统稳定性的,是:
相机采集;
任务调度;
图像生命周期;
队列;
线程;
GPU;
磁盘;
UI;
通讯;
异常恢复。
尤其是在高速项目中,最危险的问题往往不是某一次检测慢,而是:
每个产品慢10ms,最终慢慢积累成几百张图像的队列。
因此我现在做多相机项目时,会优先把整个处理链路和性能监控建立起来,再去优化具体算法。
后续准备继续整理 VisionPro 多线程、CogRecord显示、图像拼接、异步存图以及ToolBlock生命周期相关内容。