C#工业机器视觉软件架构设计:从能运行的Demo到能够稳定跑产线的软件

做机器视觉项目几年以后,我越来越觉得,视觉项目中真正困难的部分往往并不是某一个算法。

用 HALCON 找一条边、用 VisionPro 做一个 PatMax、用 OpenCV 做一次二值化,这些问题通常都有比较明确的解决方法。

真正让项目变得复杂的是:

相机怎么管理?

PLC触发的时候程序正在处理上一片产品怎么办?

两台甚至十台相机同时回来图像怎么办?

算法异常会不会把整个程序卡死?

图片保存会不会影响CT?

软件运行几天以后内存为什么越来越大?

程序崩溃以后没有保存的数据怎么办?

换产品型号以后参数怎么管理?

这些问题决定的,才是一套机器视觉软件到底只是一个Demo,还是一套能够真正运行在产线上的工业软件。

本文结合我这些年使用 C#、VisionPro、HALCON、OpenCV 等视觉平台开发非标视觉软件的一些经验,整理一下我目前比较推荐的工业机器视觉软件架构。


1. 最常见的问题:所有代码都写在Form里面

很多视觉项目最开始都会写成这样:

复制代码
MainForm
 ├─ 相机连接
 ├─ 相机采图
 ├─ PLC通讯
 ├─ VisionPro运行
 ├─ HALCON运行
 ├─ 保存图片
 ├─ 保存CSV
 ├─ 更新界面
 └─ 报警处理

项目小时没有任何问题。

但随着功能增加,MainForm很快就会变成几千甚至上万行。

最后会出现一种非常典型的情况:

修改PLC通讯逻辑,影响相机采集;

修改界面显示,影响算法线程;

增加一个相机,需要改很多地方;

程序出现偶发异常,很难判断到底是通讯、算法还是UI造成的。

所以我现在做视觉软件时,一个非常重要的原则就是:

复制代码
Form只负责显示,不负责业务。

2. 我比较推荐的视觉软件分层方式

对于一个中等规模的工业视觉软件,可以按下面的方式拆分:

复制代码
VisionApplication
│
├─ UI
│   ├─ MainForm
│   ├─ CameraForm
│   ├─ VisionForm
│   ├─ LogForm
│   └─ SettingForm
│
├─ Application
│   ├─ InspectionService
│   ├─ ProductionService
│   └─ RecipeService
│
├─ Vision
│   ├─ IVisionTool
│   ├─ VisionProEngine
│   ├─ HalconEngine
│   └─ OpenCvEngine
│
├─ Camera
│   ├─ ICamera
│   ├─ HikCamera
│   ├─ BaslerCamera
│   └─ CognexCamera
│
├─ Communication
│   ├─ PlcService
│   ├─ TcpService
│   └─ MesService
│
├─ Data
│   ├─ Repository
│   ├─ SQLiteRepository
│   └─ CsvRepository
│
└─ Infrastructure
    ├─ Logger
    ├─ Config
    ├─ ImageSaveQueue
    └─ PerformanceMonitor

这种结构最大的意义并不是"看起来专业",而是让每个模块只负责自己的事情。

例如PLC出现问题时,我只需要检查 Communication。

图片保存慢,只检查 ImageSaveQueue。

算法运行异常,只检查 Vision。

这样维护成本会低很多。


3. 相机一定要抽象接口

假设项目一开始使用海康相机:

cs 复制代码
public interface ICamera
{
    bool IsConnected { get; }

    bool Connect();

    void Disconnect();

    void StartGrab();

    void StopGrab();

    Bitmap GrabImage();
}

然后实现:

cs 复制代码
public class HikCamera : ICamera
{
    public bool IsConnected { get; private set; }

    public bool Connect()
    {
        // 海康SDK连接
        return true;
    }

    public void Disconnect()
    {
    }

    public void StartGrab()
    {
    }

    public void StopGrab()
    {
    }

    public Bitmap GrabImage()
    {
        return null;
    }
}

以后项目改成 Basler,相机上层逻辑基本不需要变化。

cs 复制代码
InspectionService
        ↓
     ICamera
     ↓     ↓
HikCamera BaslerCamera

这对于非标项目非常重要。

因为视觉软件经常会因为客户指定品牌、成本或者交期更换硬件。


4. 不要让相机回调直接执行算法

这是多相机项目里一个非常容易出现的问题。

很多程序会这样写:

cs 复制代码
private void Camera_ImageReceived(Bitmap image)
{
    RunVision(image);

    SaveImage(image);

    UpdateUI(image);
}

问题在于,相机SDK回调线程被算法占用了。

如果算法耗时增加,后续图像继续进入,就很容易造成阻塞、掉帧甚至SDK内部缓存堆积。

更合理的方式是:

cs 复制代码
Camera
   ↓
ImageReceived
   ↓
ImageQueue
   ↓
WorkerThread
   ↓
Vision
   ↓
Result

例如:

cs 复制代码
private readonly BlockingCollection<Bitmap> _queue =
    new BlockingCollection<Bitmap>(20);

回调只负责入队:

cs 复制代码
private void Camera_ImageReceived(Bitmap image)
{
    if (!_queue.TryAdd(image))
    {
        image.Dispose();

        Logger.Error("图像队列已满");
    }
}

检测线程单独执行:

cs 复制代码
private void ProcessLoop()
{
    foreach (Bitmap image in _queue.GetConsumingEnumerable())
    {
        try
        {
            RunVision(image);
        }
        catch (Exception ex)
        {
            Logger.Error(ex.ToString());
        }
        finally
        {
            image.Dispose();
        }
    }
}

这样采集线程和算法线程就解耦了。


5. 为什么一定要限制队列长度

很多人第一次写异步队列时会使用:

cs 复制代码
ConcurrentQueue<Bitmap>

然后不断往里面放。

这在工业视觉里面其实存在风险。

假设:

相机每100ms产生一张图片;

算法因为某种原因变成200ms;

那么生产速度永远高于消费速度。

结果就是:

cs 复制代码
10张
20张
100张
1000张
...

内存最终一定会出问题。

所以视觉系统中的队列通常应该是:

cs 复制代码
| 有界队列,而不是无限队列。

队列满以后应该明确决定:

丢弃?

报警?

停止设备?

这属于生产策略,而不应该让程序无限堆积。


6. 图片保存必须异步

工业视觉软件另外一个常见性能问题就是存图。

尤其是高分辨率相机。

一次 JPEG/PNG/BMP 编码加磁盘写入可能耗费几十毫秒甚至更多。

如果代码是:

cs 复制代码
RunVision();

SaveImage();

SendResult();

那么保存图片直接进入产品CT。

更合理的是:

cs 复制代码
检测线程
   │
   ├── 输出PLC结果
   │
   └── ImageSaveQueue
              ↓
          SaveWorker
              ↓
             SSD

检测完成以后,只需要将保存任务放入队列。

真正的编码和磁盘IO交给后台线程。


7. UI显示也不应该每一帧都刷新

WinForms更新图像其实也有成本。

特别是:

cs 复制代码
9台相机
+
高分辨率图像
+
检测Region
+
文字
+
大量Graphic

如果每一帧都刷新UI,很容易造成主线程压力。

工业视觉软件中的UI本质上只是:

cs 复制代码
| 给人看的监控界面。

它不是生产逻辑。

例如相机100FPS运行,并不意味着UI也必须显示100FPS。

完全可以算法处理100FPS,而UI只显示10FPS。


8. 每一个产品都应该有唯一的生命周期

一个比较成熟的检测流程不应该只是:

cs 复制代码
PLC触发
↓
拍照
↓
检测
↓
写结果

而应该生成一个 InspectionContext:

cs 复制代码
public class InspectionContext
{
    public long Id { get; set; }

    public DateTime TriggerTime { get; set; }

    public DateTime GrabTime { get; set; }

    public DateTime FinishTime { get; set; }

    public string ProductCode { get; set; }

    public bool Result { get; set; }

    public double VisionTime { get; set; }
}

以后所有日志都带这个ID:

cs 复制代码
[120382] PLC Trigger

[120382] Camera Grab 23ms

[120382] Vision Start

[120382] Vision Finished 81ms

[120382] Result NG

[120382] Send PLC OK

当产线出现偶发异常时,这种日志价值非常高。


9. 日志不要只记录"程序运行了"

我现在认为视觉软件最重要的功能之一其实是日志。

至少应该能够看到:

cs 复制代码
GrabTime
VisionTime
ModelTime
DisplayTime
SaveTime
CommunicationTime
TotalCT

否则客户告诉你:

cs 复制代码
| 昨天晚上3点设备偶尔慢了一次。

如果没有性能日志,基本只能猜。

而有日志以后,很容易判断:

到底是相机慢了;

模型慢了;

磁盘IO慢了;

PLC响应慢了;

还是UI卡住了。


10. 算法和业务逻辑一定要分离

例如检测螺丝。

算法层只应该返回:

cs 复制代码
public class VisionResult
{
    public bool IsOK { get; set; }

    public double Score { get; set; }

    public double X { get; set; }

    public double Y { get; set; }

    public double Angle { get; set; }

    public string ErrorMessage { get; set; }
}

至于:

cs 复制代码
OK写PLC 1
NG写PLC 2
保存NG图片
MES上传
统计产量

这些都不应该写进算法里面。

因为这些属于业务逻辑。


11. 配方Recipe也是视觉软件非常重要的一部分

工业项目几乎不可能永远只有一个产品。

因此项目一开始就应该考虑:

cs 复制代码
Recipe
├─ CameraParameters
├─ VisionParameters
├─ PLCParameters
├─ SaveParameters
└─ ProductParameters

切换型号,本质上应该是:

cs 复制代码
LoadRecipe
↓
ApplyCameraParameters
↓
LoadVisionProgram
↓
UpdateCommunicationParameters
↓
Ready

而不是在代码里面写:

cs 复制代码
if (Product == "A")
{
}

if (Product == "B")
{
}

后者型号一多以后会非常难维护。


12. 一个真正稳定的视觉软件,算法可能只占30%

项目早期,我们经常会把注意力集中在:

模板匹配准不准;

Blob能不能找到;

边缘稳不稳定。

但软件真正上线以后,经常遇到的问题反而来自:

线程;

内存;

磁盘;

网络;

PLC;

相机SDK;

异常恢复;

状态同步。

所以我现在理解的机器视觉软件开发已经不只是:

cs 复制代码
| 图像处理。

而是:

cs 复制代码
| 图像处理 + 工业自动化 + 软件工程。

算法决定系统"能不能检测"。

软件工程决定系统"能不能稳定运行"。

这两件事情缺一不可。


结语

如果只是做一个Demo,把所有代码写到MainForm里并没有什么问题。

但如果目标是一套真正需要运行几年、面对各种异常、支持多型号、多相机、PLC和MES的工业视觉软件,那么架构设计越早做,后面的维护成本就越低。

后续我准备继续整理一些自己在实际机器视觉项目中遇到的问题,包括:

C#多相机并发;

VisionPro二次开发;

HALCON工程化;

PLC视觉握手;

异步图像保存;

视觉软件性能监控;

3D视觉数据处理;

通用视觉软件插件化架构。

希望能把以前单纯记录代码的博客,逐渐整理成一套完整的工业机器视觉软件开发笔记。

相关推荐
TomEval1 小时前
【App 自动化】15 - App 自动化框架搭建
运维·自动化
天远API2 小时前
零信任架构实战:基于天远公安三要素即时版构建自动化理赔合规网关
人工智能·python·架构·自动化
XiaoZhenHua982 小时前
C# WinForms + 西门子S7 + SQLite + EPPlus生产数据采集系统架构设计
系统架构·sqlite·c#
运维全栈笔记2 小时前
Ansible 自动化 Nginx 集群部署实战:动态 Inventory、滚动发布与 Role 工程化
nginx·自动化·ansible
这张生成的图像能检测吗2 小时前
(论文速读)基于两阶段多模式深度学习的轮胎表面缺陷自动检测及严重程度分类系统
人工智能·深度学习·计算机视觉·数据采集·检测系统·缺陷检测分类·轮胎缺陷
Thomas.Sir2 小时前
第42课:TensorFlow|计算机视觉实战二【图像分割基础流程与数据标注规范】
人工智能·计算机视觉·tensorflow
粉色大象2 小时前
handdrawn‑architecture‑video:开源SVG架构图转手绘4K动画视频|本地AI Agent Skill
人工智能·ai·ai作画·系统架构·开源·aigc·音视频
跨境小彭2 小时前
Temu 广告投放实操:ROAS 底层逻辑与批量广告作业方案
大数据·人工智能·自动化·temu
吴佳浩 Alben3 小时前
走向 Memory OS:企业私有化 Agent 设计与实现
人工智能·深度学习·神经网络·语言模型·架构·自动化·ai编程