VisionPro多相机高速检测性能优化实战:从图像堆积到稳定运行

最近几年做过一些多相机机器视觉项目以后,我发现一个很有意思的现象:

单相机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生命周期相关内容。

相关推荐
昇腾知识体系1 小时前
CANN 安装升级避坑:version.cfg 查版本、银河麒麟找不到驱动、nnrt --version 无输出排查
人工智能·华为·知识图谱
java_logo1 小时前
Claude 遭大规模「蒸馏」?过去 8 个月,AI 行业另一场战争被摊开了
人工智能·claude·qwen·ai 安全·kimi·模型蒸馏·anthropic
yychen_java1 小时前
第二篇:从世界模型到 Physical AI——一套可落地的工业智能体架构
人工智能·架构
matlab代码2 小时前
基于matlab多尺度形态学提取眼前节组织【源码73期】
图像处理·人工智能·计算机视觉
IT_陈寒2 小时前
Redis的Set操作居然能把我的服务整挂了?
前端·人工智能·后端
Mininglamp_27182 小时前
MCP和A2A之后,机器人网络里还缺一层开放通信协议
人工智能·agent·多智能体·具身智能
angered2 小时前
「AI 应用 / AI Agent」行业日报 · 2026-09-08
人工智能·ai编程
wxl7812272 小时前
深度解析:无本体VLA/WLA模型选型、数据采集与家用机器人产业终局
人工智能