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/RegisterHandlerConfigService读取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 BlockingCollectionVisionTask(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#协作绕不开的两个底层问题框架层面的问题解决之后还有两个底层问题几乎每个用HalconC#的人都会撞上一个是跨线程崩溃一个是版本环境匹配。这俩不解决框架搭得再漂亮都会被线上事故打回原形。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。千万别依赖GCGC管不了native内存。事件里传递结果时只传数值和小的HTuple不传大的HImage引用。UI要预览自己另存一张副本。另外还有一个容易忽略的点HDevEngine在运行时加载脚本脚本里的算子执行完会创建临时对象如果脚本写得不好大量临时HObject没有被释放内存也会悄悄涨上去。在脚本里跑完ReduceDomain、Threshold这些步骤后及时调用clear_obj清理中间对象。4.2 授权版本和32/64位匹配Halcon的授权是个经典坑但它不是什么见不得人的事。开发机上装HDevelop开发授权产线部署机上装Halcon Runtime授权这两者本来就是分开的深度学习推理还需要对应模块的授权。方案上没有任何灰色空间老老实实按官方规则来反而最省心。更常见的坑是版本不匹配。Halcon版本换代快从17.12到20.11再到新版23.05halcon.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也不算差它的问题是只考虑了算法层没考虑运行环境。我这版改完最大的收获不是代码多优雅而是现场出问题的时候我能在十分钟内找到原因而不是对着黑屏日志发愁。希望你拿到这份源码之后也能先把通信健壮性和参数配置这两块理顺它们比任何花哨的算法都更能决定一套视觉系统能不能真正在产线上活下去。
返回列表