ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于VS2019 C#与VisionPro9.0的机器视觉通用检测框架解析

基于VS2019 C#与VisionPro9.0的机器视觉通用检测框架解析

简介:一套基于VS2019与VisionPro9.0开发的机器视觉通用检测框架软件源码,面向工业视觉项目开发与VisionPro二次学习人群,帮助快速搭建稳定可用的检测系统。工业视觉难点常在工程化细节,而非单一算法;该框架已在实际项目中验证,涵盖多相机采集时序同步、TCP通讯断线重连、标定数据误差补偿、权限管理等环节,开发者可直接复用并聚焦业务逻辑调整。压缩包共208个文件,约91.54MB,主要包含36个C#源文件、27个dll动态库、8个vpp视觉工具和17个xml配置,以及日志、资源文件与全套VS工程配置,结构清晰,便于在VS2019中直接打开修改。已有410人学习浏览,适合需要快速落地视觉检测项目、或希望从成熟源码中理解工业级框架设计的读者。拿到后可按模块替换检测模板、修改通讯参数与标定配置,省去环境配置和版本兼容的折腾;同时可参考源码中的模块划分、异常处理与通讯重连逻辑,提升自身工程能力。

1. 机器视觉通用检测框架:基于VS2019 C#+VisionPro9.0,本质是视觉检测项目的可复用骨架

在机器视觉项目实施里,真正让工期失控的往往不是检测算法本身,而是采图、界面、通信、参数保存这些流程粘合工作。这套以VS2019 C#搭建、以VisionPro9.0作为图像处理内核的通用检测框架,目的就是把每个视觉项目都会重复出现的功能模块固化成骨架。新项目上只换相机型号、检测工具组和通信协议,剩下的界面、日志、图像保存、运行控制直接复用,从零到能跑联调的时间能缩短一个量级。

“通用”不是说把定位、测量、读码、缺陷检测都写进同一份代码里,而是给“采图→运行视觉工具组→输出结果→记录日志”这条主链提供一套统一驱动机制。拿到全套源码后,第一步不是翻开源码读,而是先在概念上划清边界:哪些是框架已经做好的,哪些是按新项目需要替换的。下面就从源码组织、项目搭建、部署调试三个角度把这套框架讲透,其中会贴着VS2019的工程配置和VisionPro9.0的ToolBlock细节展开,因为它们才是框架能不能改动、能不能扩展的关键。

2. 看源码先看主流程:VS2019 C#框架里,VisionPro9.0从哪里接入

拿到全套源码,我一般不会逐行读,而是先找“入口”和“心脏”。入口就是Program.cs,它创建主窗体FrmMain;心脏是窗体Load事件里实例化的VisionEngine对象,它负责相机初始化、ToolBlock运行、结果回调三件事。这套框架能不能改成适应新项目的形态,基本取决于这个对象和UI层划分得够不够干净。

2.1 从主窗体和流程引擎入手:启动顺序里藏着的依赖关系

// Program.cs 入口,WinForms标准入口 [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new FrmMain()); }
// FrmMain_Load 里按固定顺序初始化检测引擎 private void FrmMain_Load(object sender, EventArgs e) { _engine = new VisionEngine(); if (!_engine.LoadConfig(@"config\vision.json")) { MessageBox.Show("配置文件加载失败,请检查config目录"); return; } if (!_engine.LoadVisionJob(@"jobs\measure.vpp")) { MessageBox.Show("视觉作业加载失败,请确认VPP文件完整"); return; } _engine.InitCamera(); _engine.Start(); }

第一个值得记的细节是启动顺序:先加载配置,再加载视觉作业,最后才初始化相机。VPP文件里保存了相机型号、采集模式这些信息,如果C#代码先初始化相机再加载作业,作业里的相机节点会尝试再次创建采集通道,造成端口冲突。把相机初始化放在最后,就只用一份相机配置来驱动,作业里的相机节点只在VisionPro编辑器里作为记录,不实际打开设备。

LoadConfig读的是一个JSON文件,里面至少有相机IP、触发源、图像保存路径、PLC IP和端口。VS2019的工程配置里,建议把这个JSON和jobs文件夹用“复制到输出目录”的方式放进bin\Release,免得部署时手动拷贝漏文件。我在项目中实际看到过太多因为漏拷VPP导致现场启动报错的情况,等跑到第5章你会看到,这类问题都可以用自检清单规避。

2.2 VisionPro9.0 ToolBlock封装成C#类:关键代码与端口校验

VisionPro9.0的作业文件以.vpp持久化,本质上是一个工具组的序列化结果。C#框架不需要逐个调用VisionPro里的工具,而是通过CogToolBlock这个容器把整个工具链一次运行掉。ToolBlock的好处是,工具之间的连线、参数、坐标系关系都在VisionPro编辑器里维护,C#侧只留输入输出两个接口。

using Cognex.VisionPro; using Cognex.VisionPro.ToolBlock; /// <summary>加载视觉作业(.vpp),并校验关键端口</summary> public bool LoadVisionJob(string path) { try { // VPP反序列化:从文件加载ToolBlock对象 _toolBlock = CogSerializer.LoadObjectFromFile(path) as CogToolBlock; } catch (Exception ex) { LogService.Error("加载VPP异常", ex); return false; } // 这里做端口校验,比运行时再报错友好得多 if (_toolBlock == null || !_toolBlock.Inputs.ContainsKey("InputImage") || !_toolBlock.Outputs.ContainsKey("Pass")) { LogService.Warn("VPP缺少InputImage或Pass端口"); return false; } return true; }

逻辑说明:CogSerializer.LoadObjectFromFile是VisionPro序列化的标准入口,返回Object,需要按实际情况转成CogToolBlock。把端口校验放在加载阶段而不是运行阶段,是因为视觉工具链一旦缺输入输出参数,运行时会直接弹异常把采集线程打断;在加载阶段把问题暴露出来,现场还能快速回退到上一个VPP版本。

端口命名建议写成常量,不要散落在各方法里用字符串裸标:

public static class VppPorts { public const string InputImage = "InputImage"; public const string OutputPass = "Pass"; public const string OutputValue = "MeasureValue"; }

这样在场景侧改动ToolBlock端口名时,编译期就能暴露不一致,而不是运行到检测那一帧才报错。我做过的项目里,因为这个字符串不一致导致的排查少说占了三成,统一收口之后这类问题基本清零。

2.3 框架中的数据流:采集回调、后台线程和结果事件

框架的运行时数据流可以概括成三条线:采集回调线程、后台检测线程、UI显示线程。采集回调由VisionPro的采集FIFO触发,它只负责拿到图像并扔到队列里,不执行检测;后台线程从队列里取图像运行工具块;UI通过事件接收结果并刷新。

// 相机采集完成回调:拿到图像后立刻交给后台线程 private void OnFrameComplete(object sender, EventArgs e) { CogImage greyImage = _acqFifo.Operator.Fifo.Complete as CogImage; if (greyImage == null) return; // 用线程池调度,避免阻塞采集回调线程 ThreadPool.QueueUserWorkItem(ProcessCore, greyImage); } private void ProcessCore(object state) { var image = (CogImage)state; try { _toolBlock.Inputs[VppPorts.InputImage].Value = image; _toolBlock.Run(); bool pass = (bool)_toolBlock.Outputs[VppPorts.OutputPass].Value; // 向UI层发布结果事件 ImageReady?.Invoke(this, new DetectResultEventArgs(pass, _toolBlock.Outputs)); } catch (Exception ex) { LogService.Error("检测异常", ex); } finally { // 关键点:图像用完释放,否则内存只涨不降 image.Dispose(); } }

这一段是整个框架里最容易踩坑的地方。VisionPro的采集回调运行在它自己的采集线程上,如果在这个线程里做ToolBlock运行或UI刷新,轻则界面按钮没反应,重则采集超时。所以框架里强制用ThreadPool.QueueUserWorkItem把检测逻辑丢到后台线程,UI层再通过事件同步更新,这是最省事的线程隔离方式。image.Dispose()不能省,CogImage保存的是非托管内存图像,不释放会导致8小时运行后内存涨到几个GB,GC也帮不上忙。

3. 用这套框架搭一个可运行检测程序:从相机采图到结果输出

这一章是拿到“开箱即用”源码后最想直接看到的部分:怎么把它变成真正测一个产品的程序。核心还是VisionEngine,但需要把相机参数、触发方式和结果通道逐项落到实际配置上。

3.1 相机采图模块:触发模式和三组必调参数

这套C#框架和VisionPro接相机,最常见的是GigE网络相机。VisionPro通过Cognex的GigE采集接口访问相机,C#侧不用直接处理相机SDK,眼睛盯住CogAcqFifo的TriggerMode、Timeout、像素格式三项就够用。

// 初始化相机:使用VisionPro的GigE帧缓冲器 public bool InitCamera() { _acqFifo = new CogAcqFifo(); // 相机IP和厂商信息从配置读取后传给采集算子 _acqFifo.Operator.Create("GigE", _config.CameraIp, null); // 触发模式:FrameContinuous=连续采集,FrameSingle=单帧,FrameSoftware=软触发 _acqFifo.Operator.OwnedFifo.Fifo.TriggerMode = (CogTriggerModeConstants)Enum.Parse( typeof(CogTriggerModeConstants), _config.TriggerMode); // 超时时间:超过该时间无新帧,触发Timeout事件而不是死等 _acqFifo.Operator.Timeout = 3000; // 注册图像完成回调,进入检测流程 _acqFifo.Operator.Fifo.Complete += OnFrameComplete; return true; }

参数说明:TriggerMode有三种常用取值。产线传感器给硬触发信号时用FrameSoftware,相机在硬触发信号的边沿抓一帧;相机调试阶段用FrameContinuous连续采图,方便调焦和调光源;需求单张图像判定时用FrameSingle。三种模式在VisionPro中的枚举分别是FrameContinuous、FrameSoftware、FrameSingle,注意字符串必须和枚举名一致,否则解析时会当场抛异常。

如果你用的是一体化工控机带USB3或CameraLink采集卡,做法会不一样,但框架里取的仍是同一套抽象:外部拿到一张图像即可送入ProcessCore,采集细节全部封装在实现里。所以这个框架要接新相机,只需要替换InitCamera这个模块,后面的检测流程完全不受影响。

3.2 加载ToolBlock并执行视觉工具链:参数从哪里来,结果到哪里去

新项目里改动最多的是VPP文件内的工具组:在VisionPro9.0的ToolBlockEditV2控件里拖入定位、卡尺、Blob工具,再把输入图像和输出结果连好。C#侧只负责把图像塞进Inputs、读走Outputs。

private void ProcessCore(object state) { var image = (CogImage)state; try { _toolBlock.Inputs[VppPorts.InputImage].Value = image; _toolBlock.Run(); // 从Outputs里读Pass和MeasureValue,框架再统一定义结果结构 bool pass = (bool)_toolBlock.Outputs[VppPorts.OutputPass].Value; var result = new DetResult { IsPass = pass, Value = (double)_toolBlock.Outputs[VppPorts.OutputValue].Value, Timestamp = DateTime.Now }; _events.RaiseDetected(result, image); } catch (Exception ex) { LogService.Error("检测异常: " + ex.Message); } finally { image.Dispose(); } }

参数说明:Inputs["InputImage"]必须在VPP里提前建好对应端口,类型是CogImage8Grey;Outputs["Pass"]类型是bool,Outputs["MeasureValue"]是double。工具组内部的中间结果代码不关心,只需要把最终结果统一收口到DetResult类。这样做的直接好处是:现场需要换检测算法时,C#框架不动,只需在VPP里按相同端口名重新组织工具链;换完后再用第5章的回归测试把历史图片过一遍,确认没有把检测带偏。

3.3 结果输出:搭建现场,结果只能走这三条通道

框架里结果输出通常有三条通道,在DetResult产生之后分别走不同接收方:

  • 界面显示:刷新主窗体的检测数据表格和图像显示控件;
  • PLC输出:通过TCP/IP或串口写入判定结果,让产线知道放行还是剔除;
  • 本地存储:写CSV或SQLite,用于质量追溯和回放。
private void SendResultToPlc(DetResult r) { // 这里以Modbus TCP写单个线圈为例:0=NG,1=OK byte coilVal = r.IsPass ? (byte)1 : (byte)0; // _modbusClient是框架里封装的客户连接,注意重连逻辑 var ok = _modbusClient.WriteSingleCoil(0x0000, coilVal); if (!ok) { LogService.Warn("PLC写线圈失败,检查网络通信"); _alarm.Fire(AlarmLevel.Warning); } }

这里最容易翻车的不是通信协议本身,而是断线重连。生产现场的PLC重启或网线松动会断开TCP连接,框架里必须做成“写失败后自动重连+断开报警”,否则后半夜产线PLC重启,上位机还一直显示OK,废品就流到下一道工序去了。我见过不止一次因为这个漏了,第二天被产线经理拉着复盘到中午。

4. 避坑:基于VS2019 C#和VisionPro9.0的视觉框架,部署中五个亲历的坑

这一章从部署和现场调试角度写,每一条都在类似项目里真实烧过时间。新手照着走能少走弯路,老手看一遍也能对号入座。

4.1 开发机能跑,部署机找不到VisionPro许可证

现象:源码在开发调试机上编译运行一切正常,把bin\Release整个文件夹拷到工控机上,跑到引擎初始化时报“VisionPro runtime license not found”或类似提示,程序直接退出。

原因:VisionPro的授权分开发授权和运行授权。开发机上装的是带工具集的开发版,部署机的运行授权必须单独申请并绑定机器硬件信息,不是复制几个dll就能解决的。

解决:把工控机的操作系统、主机序列号等信息提交给VisionPro供货商或授权管理岗,申请对应部署授权;安装后在控制面板确认VisionPro相关服务处于Running状态;装完授权还找不到,先重启工控机再看服务是否随系统启动。不要把时间浪费在复制dll上,这条路走不通。

4.2 相机采图超时、花屏:GigE网络的功耗设置

现象:GigE相机在同一工控机上时好时坏,连续采集几小时以后采图超时,或者图像出现花屏、撕裂。

原因:Windows默认电源策略会在无操作时让网卡进入节能状态,相机和工控机的链路出现丢包,GigE协议虽然会请求重发,但VisionPro的FIFO超时设置太短,等不到重发就报错了。

解决:在工控机的网络适配器高级设置里,把“电源节能”相关项全部关闭;设备允许时开启网卡的Jumbo Frame,把相机Packet Size调到网络允许的最大值(一般是9000;注意交换机也要支持,否则反而丢包);同时把_acqFifo.Operator.Timeout放宽到3000~5000毫秒。注意超时别改太长,否则网线真的断开时,错误恢复会显得非常迟钝。

4.3 按钮点击后界面假死:线程同步写错了

现象:点击“开始运行”按钮后程序无响应,鼠标一直转圈,有时10秒才恢复。打断点看调用栈,发现停在按钮的Click事件处理器里,进一步发现它直接调用了_toolBlock.Run()。

原因:WinForms的UI线程在Click事件里直接调用了VisionPro的阻塞型方法。工具链执行时,UI线程被占住,界面不能重绘,自然假死。

解决:给框架加一条硬性约定——UI事件里只发起操作,不执行检测。UI事件的处理器里用ThreadPool.QueueUserWorkItem把实际动作丢到后台线程;如果运行中需要修改工具参数,用一把锁包住_toolBlock的入口,UI改参数和后台跑检测互斥。这个锁不能省,否则两个线程同时操作同一个CogToolBlock,会出现不可预期的工具结果。

4.4 VPP文件加载不了,或加载后端口名对不上

现象:运行时CogSerializer.LoadObjectFromFile返回null或抛异常;再或者VPP加载OK,但_toolBlock.Inputs["InputImage"].Value = image这行抛KeyNotFoundException。

原因:VPP文件是用旧版本VisionPro保存的,9.0环境里某些工具组兼容性出问题;或者VPP在ToolBlockEditV2里重命名过输入端口,但C#代码里的字符串没同步。

解决:凡是遇到反序列化异常,先在开发机的ToolBlockEditV2里打开VPP,确认工具组能正常运行,不要一上来就在C#侧排查。遇到端口名对不上,按2.2节的方案把端口名收敛到VppPorts常量类,避免“VPP里偷偷改名、C#代码同步遗漏”这种低级事故。视觉项目里这类字符串不一致问题占排查量的三成以上。

4.5 图像内存只涨不降:CogImage没有释放,ToolBlock内部缓存残留

现象:框架跑了两小时后,进程内存从300MB慢慢爬到2.5GB,后台再继续运行时采图速度下降,最后在ToolBlock.Run()时出现内存不足。

原因:两个方面叠加。一是ProcessCore里image没有Dispose;二是ToolBlock每次运行完会把输入输出都留在对象里,上一帧图像被下一帧覆盖,但老的CogImage引用还挂在内部缓存里没有释放。

解决:ProcessCore的finally块里image.Dispose(),运行完成后把_toolBlock.Inputs[VppPorts.InputImage].Value = null。有必要时给CogImage加using语句,让托管和非托管资源同步释放。框架日志里可以每100帧输出一次GC.GetTotalMemory(false),一旦监测到内存增长趋势,立刻定位是哪个模块泄漏,别等现场崩了才来找。

5. 从“能运行”到“敢上线”:回归测试、耗时统计和日志系统

很多源码能运行,但能不能上产线跑一整夜是另一回事。这一章写框架里必须补上的“验收三件套”,缺哪个都别急着下单。

5.1 批量回放图片的回归测试:不触碰相机也能验证算法

现场相机一直在跑,改一次VPP参数就动一次相机,效率低还容易误伤在制品。更好的做法是:用已采集的离线图片回放做回归测试。框架里的LoadVisionJob和ProcessCore与真实采图是同一套代码,只需把图像来源换成文件夹。

private void RunRegressionTest(string folderPath) { var results = new List<(string File, DetResult Result)>(); foreach (string file in Directory.GetFiles(folderPath, "*.bmp")) { using (var img = new CogImage8Grey(file)) { var result = DetectOnce(img); results.Add((file, result)); } } // 再对results做统计:OK类误判数、NG类漏判数 ReportRegression(results); }

参数说明:回放图片至少包含OK和NG两类、每类不少于100张,覆盖不同角度和光照。回放测试的目的不是调算法,而是验证“改一个参数不会把以前的检测结果带偏”。每次修改VPP后强制跑一遍,能救下很多现场翻车。这套框架接新项目时,我一般会把现场采集的第一批图留档,回放测试也就变成了一个持续性质量闸口。

5.2 检测耗时预算和节拍优化

视觉框架的节拍以毫秒计,判断是否能在下一颗料到位前完成判定,直接决定产线吞吐。框架里统计的不只是ToolBlock.Run()的耗时,还要把采图、检测、通讯、UI刷新的整链路耗时分开统计。

var sw = Stopwatch.StartNew(); // 在框架各关键阶段记录耗时,输出到trace日志 sw.Stop(); double totalMs = sw.Elapsed.TotalMilliseconds; LogService.Trace($"采图{acqMs}ms 检测{detectMs}ms UI{uiMs}ms 总耗时{totalMs:F1}ms");

如果检测耗时超过节拍预算的一半,先别急着改算法参数,看VPP里是否把不必要的中间结果显示打开了。CogToolBlockEditV2工具组界面里有大量可视化输出,部署时这些输出会把运行负荷拉高一截。建议在部署配置里关闭中间结果可视化,只保留最终结果输出,通常能让单帧耗时降低10%~15%。这个优化我每次做都会顺手检查一遍,性价比极高。

5.3 日志系统:每次检测都可追溯,出问题时不用靠猜

上线调试阶段,日志的价值是决定性的。框架里的运行日志至少要有三个层级:Info记录正常流程,Warn记录重试和降级操作,Error记录异常。每次检测要记录时间戳、工号、检测值、判定结果;每100帧或异常时记录一次内存占用。

public static void LogDetectResult(DetResult r) { var line = string.Format("{0:yyyy-MM-dd HH:mm:ss.fff},{1},{2}", r.Timestamp, r.IsPass ? "OK" : "NG", r.Value); File.AppendAllText( Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "detect.csv"), line + Environment.NewLine); }

这里不用数据库,是因为单条产线一天产生的检测记录通常不到几十万行,CSV足够支撑几个月的保留时长,换班时打包归档即可。如果后续要上多线集控或质量数据分析,再把CSV导入SQLite或专用服务器。日志切分和保留策略也要在框架里写死:按天切文件,超过90天自动清理,避免工控机硬盘被日志占满。

6. 让“开箱即用”真正成立:把配置文件和部署自检一次做全

“开箱即用”在我的理解里,不是把源码给你就能编译出来,而是新项目换一台电脑、换一个相机,能在半小时内把流程调整到可用状态。这里的关键,是把配置和部署动作标准化。

{ "Camera": { "Vendor": "GigE", "Ip": "192.168.100.10", "Trigger": "FrameSoftware", "TimeoutMs": 3000 }, "Vpp": { "Path": "jobs\\measure.vpp", "InputImage": "InputImage", "Pass": "Pass", "MeasureValue": "MeasureValue" }, "Output": { "PlcIp": "192.168.0.50", "PlcPort": 502, "SavePath": "D:\\Images" }, "Log": { "Level": "Info", "RetainDays": 90 } }

为了这一份配置JSON,C#代码里所有反复改的字符串都从vision.json读取,绝不写死在代码里。部署时,把这个JSON和jobs文件夹、运行授权一并复制到工控机固定目录,现场调试就变成一个可复制的自查流程。我习惯用一张五行的自检表:

检查项判定标准
VisionPro授权服务服务状态为Running
相机网络能Ping通相机IP,网卡节能已关闭
VPP可编辑用ToolBlockEditV2能正常打开并保存一次
空跑10帧内存增长不超过峰值5%,耗时在节拍内
硬触发信号触发一次,检测结果和PLC输出一致

这五项核完,产线上再出问题,就能快速定位是硬件、网络还是软件问题,不会把时间耗在“重新配置再试”的重复循环里。

去年我交付一个类似框架,现场误以为相机IP冲突,折腾了三个小时,最后发现是工控机更新网卡驱动后,节能开关被重置了。从那以后,我每换一台工控机,先执行自检表,再补一条“更新网卡驱动后回写节能设置”的备注。视觉项目的落地,体力活并不会减少,但成体系的配置和自检,能让你放心大胆地把这套基于VS2019 C#+VisionPro9.0的框架往新产线上搬。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进