拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Halcon与C#工业视觉框架架构设计:产线级稳定性与实装案例解析

Halcon与C#工业视觉框架架构设计:产线级稳定性与实装案例解析

做工业视觉上位机开发的朋友,应该都有类似的经历:Halcon负责"看得准",C#负责"管得住",两者凑在一起就是一套完整的视觉检测系统。我手头这套框架是在2.0版本的基础上改出来的,2.0当年在公司内部传得挺广,Demo演示效果不错,但真正扔到产线上连续跑,问题一个接一个冒出来。这篇文章就聊聊我基于2.0改成现在这版实用框架的思路,以及这版框架在轴承圆度测量、表面缺陷检测、机器人引导定位三个实际项目里的落地情况。

文章不是讲Halcon算法本身——找圆、Blob这些基础操作网上一抓一大把——而是讲怎么把这些算法组织进一套能扛住产线连续运行、换型号不用重新编译、相机断线能自己缓过来的C#框架里。目标读者是正在用Halcon和C#搭上位机、或者手上也有一套"能跑但不好用"的老框架的人。读完你就知道改版重点在哪、整体架构怎么搭、源码拿到手之后从哪里切入。

1. 改这版框架前,2.0版本让我最难受的三件事

动手改之前,我先把2.0版本在产线上暴露的问题列了个清单。不夸张地说,绝大部分问题不是算法精度不够,而是工程结构撑不住连续生产的环境。

1.1 图像处理把UI线程堵了个严严实实

2.0版本里典型的写法是:界面点"开始检测",直接在按钮的Click事件里调Halcon算子,加载图片、跑模板匹配、出结果、再刷新界面控件,一条龙全在UI线程里做完。Demo阶段问题不大,图片小、模板少,几百毫秒能完成。上了产线就完全不一样了:图一换大,模板匹配加上两三个测量步骤,单次检测要1.5秒左右。这期间窗口拖不动、按钮点不了,操作员第一反应就是"死机了",不停问怎么回事。

工业上位机的底线是:算法再慢,界面必须能动。哪怕一条检测流程要3秒,界面也应该能响应用户操作、能取消检测、能切换页面。所以改版第一刀,就是把所有Halcon调用全部挪出UI线程。UI只做两件事:把任务丢给后台队列,然后接收后台抛出来的结果事件去更新界面。

1.2 参数散落在二十多个窗体里,换个产品就要重新编译

2.0版本另一个坑是参数管理混乱。曝光值、ROI坐标、阈值下限、模板文件路径、超时时间……这些参数散落在各个窗体和类里,不少还是直接写死的常量。客户打招呼说"下周换新机型,孔径从12.5改成9.8",我就得全局搜索、逐个改、重新编译、重新打包。改十处漏一处,线上就出废品。

改版之后,我把所有跟工艺相关的参数收拢进一个"产品模型配置",每个产品型号一套JSON文件,里面包含相机参数、ROI定义、算法参数、公差范围、输出格式。换型号就是加载另一套配置,代码一行不用动。这个改动看着简单,实际省下来的工作量非常可观。

1.3 相机一断线,整个系统跟着躺平

产线环境不比实验室。USB相机线被撞松一下、交换机重启、PLC通信偶发超时,都是日常。2.0版本里相机一断,程序要么直接崩溃,要么一直卡在GrabImage里不返回,最后只能杀进程重启。重启之后模型加载、相机初始化又得一两分钟,产线那边已经堆了一堆料。

这一版我下决心处理通信的健壮性:相机采集加状态检测和自动重连,PLC通信加超时和重试,TCP服务加心跳保活。这些在后面都有具体实现说明。

2. 改版后的整体结构:五个模块,各管一摊

改版之前我先把2.0的代码翻了底朝天,画了一张模块关系图,发现最要命的是互相引用:窗体直接new相机对象、视觉类直接new串口对象,牵一发动全身。所以第一件事就是按职责把代码拆成五个模块,模块之间只通过接口和事件通信。

2.1 模块划分与职责边界

我最终定下来的模块如下,每个模块在代码里对应一个独立的命名空间和目录:

模块职责对外暴露
CameraService相机初始化、采集、断线重连ICamera接口,采图事件
VisionService加载Halcon算法流程、执行检测、返回结果IVisionStep接口,检测完成事件
ComServiceTCP服务、OPC UA/DA、PLC通信SendAsync/RegisterHandler
ConfigService读取JSON配置、切换产品型号ModelProfile模型对象
AppService状态机、任务调度、日志、UI绑定应用级事件

这里有个容易犯的毛病:模块拆得太细,接口满天飞,反而比不拆还难维护。五个模块是我在实际维护中觉得比较舒服的粒度——每个模块内可以再分小类,但模块之间一定不能互相new对方的实现类。比如UI需要显示相机状态,它不直接访问CameraService内部,而是订阅一个CameraStatusChanged事件。这样以后换相机品牌、换Halcon版本、加新的通信协议,都只动对应模块内部。

2.2 算法层:HDevEngine和导出代码的取舍

Halcon和C#集成的常规方式有三种:HDevelop直接导出C#文件、用HDevEngine在运行时加载.hdev工程、封装成C++ DLL再P/Invoke调用。2.0版本用的是第一种,全量导出,一个算法流程生成一大坨C#代码,看着能跑,但维护起来非常痛苦——HDevelop里改一句阈值,就得重新导出、重新编译上位机。

这版我改成了混合模式:算法流程放在HDevelop里写成独立的procedure或script,上位机用HDevEngine在运行时加载。这样算法工程师在HDevelop里调参,保存文件,上位机下次加载就是新逻辑,不用重新编译。个别对性能要求极高、或者需要跟C#对象深度交互的步骤,才单独用HDevelop导出C#类再包一层接口。

这一改的收益是巨大的。产线调试阶段,算法参数一天改十几回,如果每次都重新编译上位机,一天时间就搭进去了。用HDevEngine之后,算法文件一替换,点一下"重新加载流程"就完事。

2.3 数据流水线:从采图到结果回传

流水线的核心是生产者-消费者模型。相机采集线程是生产者,把抓到的图像丢进一个有界队列;视觉处理线程是消费者,从队列里取图、执行检测、抛结果。C#里直接用BlockingCollection就能实现,用法很简单:

var taskQueue = new BlockingCollection<VisionTask>(boundedCapacity: 2); // 生产者:采集线程 while (!ct.IsCancellationRequested) { var image = camera.GrabImageAsync(ct, timeout); taskQueue.Add(new VisionTask(image), ct); } // 消费者:视觉处理线程 foreach (var task in taskQueue.GetConsumingEnumerable(ct)) { var result = visionSession.Run(task.Image); ResultRaised?.Invoke(this, result); task.Image.Dispose(); }

有界队列容量设2,是为了防止处理速度跟不上采集速度时内存无限涨。满了之后采集线程自然阻塞,相当于背压保护。界面上显示检测结果用事件+Invoke的方式,绝不直接在后台线程里操作控件,这是避免跨线程UI异常的底线。

3. 从2.0到这一版,改动最大的四个地方

这四块是跟2.0比改动最彻底的,也是这套框架能在产线上活下来的关键。每个我都踩过不少坑,写出来大家可以少走弯路。

3.1 相机采集:抽象接口和自动重连

相机这块我先定义了一个ICamera接口,把打开、关闭、取图、参数设置这四件事抽象出来。Hikrobot、Basler、海康、大恒,每家一个实现类。不要直接用Halcon的OpenFramegrabber一把梭——Halcon虽然能通过不同驱动名来兼容各家相机,但厂家SDK有时候能拿到更详细的设备状态和错误信息,遇到故障排查起来方便很多。

自动重连逻辑是重头戏。2.0的痛点是GrabImage抛异常后整个采集线程死掉。我现在的做法是:采集主循环里catch掉HalconException,记录错误类型;凡是跟"设备丢失""连接断开"相关的错误码,就进入重连流程。重连用指数退避:第1次等1秒、第2次等2秒、第4次等4秒,最多等10秒,直到重新Open成功。重连期间界面状态栏显示"相机重连中",但程序不下线、队列不销毁,恢复之后继续正常生产。

还有一个细节:有些相机的网卡驱动会触发"设备丢失"事件,在SDK回调里就已经能感知。这时候不能直接在回调线程里去重连,要投递到一个后台监控线程统一处理,避免跟采集线程抢资源。

3.2 产品模型配置:把工艺参数从代码里赶出去

产品模型配置是这套框架里改动最大、也是后来觉得最值的一块。每个产品对应一个JSON文件,比如Circle23mm.json:

{ "name": "Circle23mm", "camera": { "exposure": 2500, "gain": 12, "roi": { "x": 100, "y": 120, "width": 800, "height": 600 } }, "steps": [ { "name": "CircleMeasure", "minRadius": 11.4, "maxRadius": 11.6 }, { "name": "BlobDefect", "areaThreshold": 50 } ], "output": { "plcDB": 22, "saveImageOnNG": true } }

上位机启动时用JsonSerializer读进来,转成强类型的ModelProfile对象。运行时切型号就是重新读配置、重建视觉流程。ROI坐标我建议存相对坐标,也就是相对基准位置的偏移量,而不是绝对像素值。因为换一台相机或者微调安装位置后,整体偏移可以通过一个标定偏移量修正,不用把每个ROI都改一遍。

3.3 通信模块:TCP多客户端、OPC订阅和PLC超时重试

产线检测系统基本都躲不开跟PLC、机器人或者MES通信。2.0版本用的是最简单粗暴的串口收发,没有超时控制,没有重试机制。这版我重写了ComService。

TCP服务端我实现了多客户端管理,用TcpListener监听,每个客户端一条接收线程,收到的报文按协议头解析后分发到对应处理器。心跳机制很关键:客户端每3秒发一次心跳包,服务端连续6秒没收到就判定掉线,清理连接并触发断线事件。反向的保活逻辑也要有,否则单向心跳等于没有。

跟西门子PLC通信,我用的是S7协议的标准库(S7.Net风格),OPC这边走OPC UA订阅模式。PLC通信最容易出问题的是"写指令后苦等回复"——所以全部改成SendAsync方式,设置2秒超时,超时后进重试队列,并且把错误抛到UI层。重试次数可配置,超过上限就报警停机,绝不无限重发。

3.4 异常与留档:把"偶发问题"变成"可复现问题"

产线上最怕的是"偶发问题"——明明刚才还正常,突然来一张NG图,换下来再看又是好的。这种问题如果程序不主动留证据,排查难度极高。所以这版框架里,所有视觉检测结果但凡判定NG,或者算法执行过程中抛了异常,我都会自动保存原始图像和关键中间结果。

保存时机放在处理线程里,用时间戳加批次号命名,同时用Halcon的disp_message把检测到的圆半径、缺陷面积、OK/NG标志直接写到图像上再存盘,方便现场人员直接看图。异常侧也一样:HDevEngine调用抛出的HalconException会带上算子和错误码,我把这些信息连同当时的配置、相机参数一起写进日志。这个习惯救了我好几次——客户反馈"偶尔检测不对",我翻一下当天NG图,两分钟就定位到是光源衰减导致的对比度不足。

4. Halcon和C#协作,绕不开的两个底层问题

框架层面的问题解决之后,还有两个底层问题几乎每个用Halcon+C#的人都会撞上,一个是跨线程崩溃,一个是版本环境匹配。这俩不解决,框架搭得再漂亮都会被线上事故打回原形。

4.1 跨线程访问和Access Violation的根源

很多朋友在做C#调用Halcon时遇到"System.AccessViolationException: Attempted to read or write protected memory",也就是0xC0000005。这个错误的根源,本质上是HObject和HTuple这些包装类型的生命周期管理出了问题。

Halcon的C#接口(HalconDotNet)本质上是对native层halcon.dll的P/Invoke封装。HObject内部持有一个指针,指向原生内存。当这个对象在某个线程创建、然后在另一个线程使用,或者被GC回收的顺序不对时,native内存已经被释放,C#这边还拿着指针用,就会触发Access Violation。2.0版本里最常见的触发场景是:图像采集线程把HImage丢给UI线程显示,UI线程用完没释放,然后采集线程又对同一张图做处理。

解决办法有几个原则,我在框架里是强制执行的:

  • 谁创建HObject,谁负责释放,尽量不要跨线程传递HObject本身。跨线程要传,就传图像缩小后的副本或者直接转成字节数组。
  • 后台线程处理完的图像,Dispose放在finally块里,或干脆用using。千万别依赖GC,GC管不了native内存。
  • 事件里传递结果时只传数值和小的HTuple,不传大的HImage引用。UI要预览,自己另存一张副本。

另外还有一个容易忽略的点:HDevEngine在运行时加载脚本,脚本里的算子执行完会创建临时对象,如果脚本写得不好,大量临时HObject没有被释放,内存也会悄悄涨上去。在脚本里跑完ReduceDomain、Threshold这些步骤后,及时调用clear_obj清理中间对象。

4.2 授权版本和32/64位匹配

Halcon的授权是个经典坑,但它不是什么见不得人的事。开发机上装HDevelop开发授权,产线部署机上装Halcon Runtime授权,这两者本来就是分开的;深度学习推理还需要对应模块的授权。方案上没有任何灰色空间,老老实实按官方规则来,反而最省心。

更常见的坑是版本不匹配。Halcon版本换代快,从17.12到20.11,再到新版23.05,halcon.dll、halcondotnet.dll、hdevengine.dll这组文件必须来自同一个版本,混搭就会出现各种莫名其妙的调用异常。部署机上的系统如果是32位,C#工程就编译成x86;如果装的是64位Halcon,工程必须Any CPU或x64。原生DLL位数跟托管DLL不一致,一调用就崩,这个问题在2.0部署时坑过我好几次。

我现在的做法是把运行时需要的这些DLL统一放在程序目录下的runtime文件夹里,部署时整个文件夹拷贝,用脚本检查三件套版本一致才能启动。开发机跟产线机版本锁死,不随意升级。

5. 三个实际项目里的落地场景

框架改完不是拿来当摆设的,这几年我在三个方向的产线项目里实打实跑过这套东西,每个项目的技术路线都多少有些代表性。

5.1 轴承外圈圆度测量:找圆和半径拟合

第一个项目是轴承外圈圆度在线测量。相机固定朝下拍,每个工件过检的时候触发相机采图,框架里的视觉流程分三步:ROI裁剪、亚像素边缘提取、圆拟合。HDevelop里写的核心脚本类似于这样:

read_image (Image, 'current_frame') reduce_domain (Image, RoiRegion, ImageReduced) sub_pix (ImageReduced, Edges, 1.5, 1, 5) fit_circle_contour_xld (Edges, 'algebraic', -1, 0, 0, 3, 2, Row, Column, Radius, StartPhi, EndPhi, PointOrder)

这里注意,找圆不一定要用复杂的圆查找算子,工程上最稳的是亚像素轮廓加最小二乘拟合。如果边缘受毛刺干扰,可以先加一步轮廓筛选,只保留长度在合理范围内的边缘段。拟合出来的半径跟标准值做差,落在公差带内判定OK,结果通过OPC写到PLC的DB块,下料机构据此分拣。

实测下来这套流程单次耗时80毫秒左右,产线节拍完全能跟上。关键是ROI一定要先裁出来,整幅图直接sub_pix的话,背景干扰会让拟合结果漂移。

5.2 表面缺陷检测:Blob分析和深度学习的配合

第二个项目是金属件表面缺陷检测。有段时间客户要求能识别划痕和凹坑,这类缺陷形状不规则,纯灰度阈值很难稳定。我的做法是经典Blob分析和深度学习双轨并行:先跑Blob把面积大、对比度高的缺陷直接捞出来,漏掉的细小、低对比度缺陷交给深度学习模型兜底。

Halcon的深度学习推理在HDevEngine里也能跑,模型文件加载后调用推理算子即可。但要注意几个工程前提:推理需要独立线程,避免阻塞采集;GPU和CPU混用要明确隔离,不然卡顿会非常明显;还有法务层面,深度学习模块的授权是单独计价的,部署前要跟Halcon供应商确认清楚。

这个双轨方案上线后,误检率从初版的每千件二三十件降到了一两件,主要还是靠Blob先筛掉绝大多数正常样本,深度学习只处理少数边界情况,推理压力小,产线节拍不受影响。

5.3 机器人引导定位:手眼标定和坐标下发

第三个项目是视觉引导机器人抓取。相机装在设备上方固定位置,工件在料盘里位置有偏差,视觉系统计算出偏移后通过TCP把坐标发给机器人。这里不需要复杂的三维手眼标定——所有工件都平放在同一个平面上,用二维仿射变换就够。

标定过程:在视野内取9个点,记录像素坐标和机器人实际坐标,用vector_to_hom_mat2d求出变换矩阵。运行时,视觉流程找到工件的中心像素坐标,用这个矩阵换成机器人坐标系下的X、Y和角度,再按约定协议组包下发,例如:

P,100.52,-20.35,45.2

机器人在收到格式不正确的数据时会返回错误码,框架里照样走超时重试。这个场景的调试重点其实不在算法,而在通信的时序——机器人没到位时视觉已经把结果发过去了,就会对不上位。所以我在框架里加了握手机制:机器人到位后先发一个Trigger指令,视觉收到才采图检测,检测完成再回传坐标。一应一答,绝不抢占。

6. 拿到框架源码后,建议你先做这几件事

框架源码给到你手里,不代表直接编译就能上产线。按我自己的经验,拿到之后建议按下面这个顺序过一遍再动手改。

6.1 搞清楚目录约定和依赖版本

第一件事是看runtime文件夹里的DLL集合,确认跟你本机安装的Halcon版本一致。还有NuGet里引用的Newtonsoft.Json、NLog这些库的版本,尽量别用太新的,跟框架测试过的一致最稳妥。项目文件如果用的还是旧版csproj格式,直接升级到SDK风格会省很多麻烦,但升级前确认HalconDotNet的引用方式没被破坏。

6.2 加一个新的视觉步骤该怎么改

框架的算法层设计成了一个步骤流水线(Step Pipeline),每一步实现IVisionStep接口,各自入参出参独立。要加一个新的检测算法,流程是:先在HDevelop里写好并验证脚本,导出成procedure文件放到Scripts目录;然后在UI里配置步骤参数;框架运行时会自动加载并执行。整个过程中完全不需要改上位机主程序,这也是HDevEngine方案最大的好处。

6.3 性能调优的先后顺序

如果上线后发现检测节拍不够,按这个顺序排查:

  • 先看ROI是否够小,处理是只针对ROI区域还是一整幅图。很多情况下单纯缩小ROI就能把耗时砍掉一半。
  • 确认采集和处理是否并行。采集线程在上一张图还没处理完时就开始抓下一张,管道利用率才上得去。
  • 看中间结果有没有写日志或保存图像拖慢速度。生产模式下把中间图像保存关掉,只留NG留档。
  • 最后才考虑换更快的硬件或者开GPU推理。上来就堆硬件往往解决不了结构性问题。

按这个顺序,我把之前一个项目的单件检测时间从1.2秒压到了差不多0.6秒,没有动任何算法精度。

最后分享一点个人的体会。框架这东西,没有完美的版本,只有适不适合当前团队和产线的那一版。2.0也不算差,它的问题是"只考虑了算法层,没考虑运行环境"。我这版改完,最大的收获不是代码多优雅,而是现场出问题的时候,我能在十分钟内找到原因,而不是对着黑屏日志发愁。希望你拿到这份源码之后,也能先把通信健壮性和参数配置这两块理顺,它们比任何花哨的算法都更能决定一套视觉系统能不能真正在产线上活下去。

返回列表