ARTICLE DETAIL

资讯详情

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

C#二次开发Halcon:静态调用从入门到工程实战

C#二次开发Halcon:静态调用从入门到工程实战 干了这么多年机器视觉上位机C#和Halcon这套组合几乎贯穿了我的所有项目。今天专门把C#二次开发Halcon里的静态调用方式掰开揉碎讲清楚。所谓静态调用就是直接在HDevelop里把调试好的图像算法导出成原生C#代码然后编译进你的上位机工程让Halcon的处理能力和C#的业务逻辑在同一个进程里无缝协作。这篇内容适合正在做C#上位机集成、刚接触Halcon二次开发的朋友或者被动态加载脚本搞到头大的开发者看完你能直接上手写出第一版可运行的静态调用代码。1. 先把概念捋清楚静态调用和动态调用到底差在哪1.1 两种调用方式的底层逻辑Halcon的二次开发思路无非两条线静态调用和动态调用。动态调用的核心是HDevEngine。你的上位机程序在运行过程中通过HDevEngine去加载和解释执行HDevelop的脚本文件.hdev或者打包后的.hdvp。程序跑到find_shape_model这行时引擎才把脚本里的算子真正解释执行调用结束后再把结果返回给C#层。开发、调试、修改算法脚本完全不用重新编译C#工程现场改个阈值、调个ROI直接替换脚本文件就行。听起来很灵活但代价也不小脚本以明文文件形式存在算法逻辑等于裸奔每个算子都有引擎解释的开销而且不同版本Halcon对应的HDevEngine行为有差异部署环境稍微变一点就容易出幺蛾子。静态调用则完全是另一回事。你在HDevelop里写好的程序通过菜单里的导出功能直接生成一份完整的C#代码文件。这份代码用的是Halcon官方提供的C#接口——halcondotnet.dll里面成千上万个算子在C#里都被声明成了静态方法。你把这文件拉进Visual Studio工程把dll一引用编译整个图像处理流程就固化在你的exe里了。运行效率高、算法逻辑跟着程序走、断点调试直接看中间变量这才是工业现场最喜欢的形态。1.2 为什么我推荐静态调用先声明动态调用有它的价值特别适合做算法快速验证、给非程序员同事改参数的场景。但如果你做的是交付型项目我的建议很直接能用静态调用就别用动态调用。理由一是性能。静态调用省掉了引擎解释执行这一层跑同一个模板匹配任务静态调用的耗时通常比动态调用低10%到20%。产线上节拍紧的时候这个差距直接决定要不要多买一套工控机。理由二是调试体验。静态调用导出的代码每个算子的输入输出都是实实在在的C#变量。在Visual Studio里打断点你能直接看到HImage的像素信息、Region的坐标数组、Tuple的值。算法异常了用VS的Watch窗口就能扒出问题在哪一步。动态调用出问题时你只能在C#层拿到一个异常码要定位脚本里哪一行出事只能回HDevelop一遍遍试。理由三是保护算法。静态编译后算法逻辑就在二进制里不能让甲方轻松改掉你的视觉核心。而且部署时不需要把脚本文件一起发出去避免路径问题、编码问题这些乱七八糟的现场故障。2. 环境准备从Halcon安装到VS工程配置2.1 Halcon版本选择与LicenseHalcon的版本号很多朋友分不清。我这里只谈现实你写C#调静态导出代码需要的是开发版的授权安装完整版Halcon带HDevelop那种。Runtime授权是不能用HDevelop的只能跑别人编译好的程序。XLD、深度学习推理这些功能模块还需要额外把对应的license文件放到安装目录的license文件夹里。简单说做二次开发的机器上必须得是完整的开发授权否则导出功能根本用不了。版本方面18.11到24.x我都用过。说实话算子接口层面的变化不算大但halcondotnet.dll的托管接口从20.11之后稳定了很多。如果是新项目建议直接上21.05或者更新版本老版本在.NET框架兼容性上多少有些别扭。Halcon 21.05之后对于64位环境支持得更好模型训练、深度学习的接口也更完整。2.2 Visual Studio工程的关键配置项创建C#工程WinForms或WPF都行有四个配置项是必须动手改的漏一个都会在运行时炸雷。第一添加DLL引用。在解决方案资源管理器里右键引用添加Halcon安装目录下的halcondotnet.dll。默认路径类似C:\Program Files\MVTec\Halcon\bin\dotnet35\halcondotnet.dll。注意dotnet35这个文件夹里的程序集兼容性最好老框架新框架都能用。同一个目录下还有个halconx.dll是老版COM接口用的现在基本用不到。第二把目标平台改成x64。Halcon从17版本开始对64位支持得彻底工业相机SDK、图像处理大内存场景全都离不开64位。在VS的解决方案平台里新建x64配置或者直接在项目属性里把首选32位勾选去掉、平台目标设为x64。这一步不做DLL加载会直接报试图加载格式不正确的程序这个错误我见过无数次。第三环境变量。装了完整版Halcon后系统会自动配好HALCONROOT和PATH。但到了部署客户电脑时如果只装了运行时就需要手动把HALCONROOT指向运行时安装目录并把bin\x64-win64加进PATH。建议代码里不要硬编码路径部署时写个批处理设置环境变量再启动exe最省心。第四如果是.NET 6及以上的项目还要注意halcondotnet.dll是.NET Framework程序集跨版本引用时可能需要开启UseWindowsForms等兼容开关。实测下来.NET Framework 4.7.2配合Halcon 21.05是最稳的组合WPF或WinForms都能跑得很流畅。3. HDevelop端导出C#代码的正确姿势3.1 算法脚本的整理习惯在HDevelop里写程序时就要想着后面导出的事情。首先要养成分步注释的习惯每个算子的作用写清楚。导出的C#代码会保留这些注释后续你在VS里维护代码时这些注释就是最好的导航。其次输入输出变量要起规范名字。HDevelop里默认的变量名如Image1、Region2这种导出来以后仍然是这些名字。我习惯在HDevelop里就改成InputImage、TextRegion、MeasureResults这种语义化命名导出后C#代码的阅读性会好很多。再一个关键习惯凡是相机采集、文件读取这类和外部打交道的操作尽量不要写进算法脚本。HDevelop里的read_image算子在导出后就是ReadImage它读的是磁盘上的图片文件而你的上位机程序大概率是从相机SDK里拿图。这种情况我强烈建议在算法脚本里用变量名占位把读图那一步删掉导出后在C#代码里手动把图像塞进去。这样算法脚本更纯粹C#集成时也不用注释大段无用代码。3.2 导出操作与参数设置脚本写完、验证跑通后依次点菜单文件 → 导出程序。弹窗里选语言时注意选C#而不是C或VB.NET。导出界面下面有几个选项需要说清楚第一个是导出类型通常选函数或者主程序。如果你的HDevelop脚本是单段流程就直接导主程序。如果脚本里写了多个main函数或者过程Procedure就选导出全部过程。我一般把算法封装成几个过程然后在主程序里调用导出后就能看到每个过程对应一个C#的静态方法后续在VS里拼接逻辑很顺手。第二个是名字修饰。这一段决定你的C#文件里类名和方法名。默认情况它会把main作为主入口方法名如果有过程方法名和过程名对齐。类名可以在导出前在脚本里用dev_set_window旁边的那个类名设置或者导出后手动改影响不大。第三个是代码格式。建议选格式化输出这样导出的代码带缩进、带括号排版VS里看着舒服。不要选紧凑模式那种代码没法维护。点确定后Halcon会生成一个.cs文件。用记事本或VS打开你能看到密密麻麻的算子调用每一个都挂在HOperatorSet这个静态类上。比如HOperatorSet.ReadImage、HOperatorSet.CreateShapeModel全部是静态方法调用。这就是静态调用这个名字的来历。4. C#工程集成与核心代码拆解4.1 导出代码的命名空间与主流程把导出的.cs文件拖进你的工程后先看文件头的一堆using。里面必然有这几个using HalconDotNet;、using System;、using System.Collections.Generic;。放心这些命名空间你的工程里已经有了重复引用不报错。主流程代码往往长这样private void RunAlgorithm(HImage inputImage) { // Local iconic variables HObject ho_Image null; HObject ho_TextRegion null; // Local control variables HTuple hv_ModelID new HTuple(); HTuple hv_Row new HTuple(), hv_Column new HTuple(); HTuple hv_Angle new HTuple(), hv_Score new HTuple(); // 读取图像这句在集成时通常要改成外部传入 // HOperatorSet.ReadImage(out ho_Image, printer_chip.png); // 创建形状匹配模板 HOperatorSet.CreateShapeModel(inputImage, auto, 0, 6.28318, auto, auto, use_polarity, auto, auto, out hv_ModelID); // 执行查找 HOperatorSet.FindShapeModel(inputImage, hv_ModelID, 0, 6.28318, 0.5, 1, 0.5, least_squares, 0, 0.9, out hv_Row, out hv_Column, out hv_Angle, out hv_Score); inputImage.Dispose(); }导出的主体是这样一个方法参数和变量都已经声明好了。你需要做的是把原本从文件读图的那一行注释掉改成接收外部传入的HImage对象。这段代码里所有变量都是HTuple类型它本质上是Halcon的通用值容器可以装整数、浮点、字符串还能装数组。out hv_Row这种语法对应的是C# 7.0之后的out变量声明方式老版本框架也可以用传统的先声明再传参效果一样。4.2 从相机采集到HImage图像转换上位机接手相机SDK的图像数据后转成HImage是静态调用绕不开的一步。大多数工业相机SDK给的是灰度图数据可能还有Bayer格式的彩色原始数据。我在项目里最常用的转换方案是直接用指针构HImage零拷贝速度最快// 假设cameraBuffer是相机SDK返回的byte数组width和height是图像尺寸 // 8位灰度图 HImage img new HImage(); img.GenImage1(byte, width, height, cameraBuffer); // 如果是彩色RGB图多平面数据用GenImageInterleaved // HImage img new HImage(); // img.GenImageInterleaved(colorBuffer, rgb, width, height, 0, byte, 0, 0, 0, 0, 0);注意GenImage1的最后一个参数是像素数据的起始地址传入byte数组即可Halcon内部会做一次数据拷贝到它的内存空间。如果你追求极致性能可以用GenImage1Extern把外部内存直接挂给HImage但这时你一定要保证托管数组在算法跑完前不被GC回收否则内存被回收后Halcon访问了非法地址程序直接崩。新手别碰这个默认用GenImage1就够了。转换完得到HImage下一步就是把它传给算法方法。整个调用流程在C#里就是普通方法调用不需要任何特殊机制。这也是静态调用最大的快感所在——视觉算法和你读写数据库、控制IO卡没有任何区别。4.3 显示与交互绑定显示控件显示这块Halcon对WinForms和WPF都提供了封装好的控件。WinForms里叫HWindowControlWPF里叫HSmartWindowControlWPF。从工具箱拖到窗体上就行。代码里把HWindowControl的HalconWindow属性传给显示算子// 把算法处理的结果区域显示出来 HOperatorSet.DispObj(outputRegion, hWindowControl1.HalconWindow); // 清空窗口 HOperatorSet.ClearWindow(hWindowControl1.HalconWindow);这里有一个细节坑Halcon的窗口句柄绑定的是窗体句柄如果你在非UI线程里做图像处理跨线程访问HalconWindow的HWindow对象WinForms下大概率会触发线程间操作无效异常。我的做法是把图像处理放到后台线程但只把HWindow对象当作一个不跨线程的私有变量传进去每次显示前先用Control.Invoke或Dispatcher.Invoke切回UI线程或者干脆用控件自身的BeginInvoke。这样既不卡界面又不炸线程。5. 常见问题与排查速查表5.1 许可证相关报错静态调用跑起来最常见的报错就是非法许可证错误码一般是#13012或者#13003。13012通常是license文件缺失或过期13003是授权不包含当前使用的算子模块。前者的解决办法是把正确的开发版license放到C:\Program Files\MVTec\Halcon\license目录重启程序。后者更麻烦比如你用了深度学习的边缘提取算子但license没买深度模块程序会在那一步直接抛异常。好消息是这类异常在C#里可以被捕获而且Halcon会告诉你缺少的模块名。我在代码里统一包了一层try { RunAlgorithm(img); } catch (HalconException ex) { // ex.Message里有完整的错误码和描述 // 记录日志然后走人工兜底流程 }注意别用空的catch(Exception)吞掉Halcon异常因为你根本不知道算法失败是图像质量原因还是算子授权原因。日志里把HALCON error #xxxx完整记录下来现场出问题看错误码就能快速判断是环境问题还是算法问题。5.2 环境与部署问题部署到客户现场最容易翻车的几个坎依次是没装Halcon运行环境、环境变量没配、dll版本不匹配。运行时环境的良心建议是直接用安装包装一次Halcon运行时Runtime然后把整个安装目录一起发给现场或者用安装包程序自动安装。只拷dll是不行的Halcon还有一堆资源文件、初始化数据需要位置匹配。Runtime授权文件也需要和软件版本对应。还有一个我踩过特别深的坑客户现场同时装了几个版本的HalconPATH里的HALCONROOT指向了旧版本程序启动时加载了旧版的halcondotnet.dll结果一堆算子在旧版本里没有直接MethodNotFound。真的防不胜防。现在的做法是在程序启动时强制指定环境变量Environment.SetEnvironmentVariable(HALCONROOT, D:\Program Files\MVTec\Halcon);这句代码必须在任何Halcon对象创建之前执行这样至少能把搜索路径锁死在你期望的版本上。问题现象常见原因处理办法加载DLL报格式不正确目标平台不是x64项目属性改x64关闭首选32位#13012许可证错误license缺失/过期/不匹配替换license文件检查版本合法HalconWindow跨线程异常UI线程与后台线程混用窗口句柄用Invoke切线程或控件自带回调类型初始化异常halcondotnet.dll程序集不匹配统一用dotnet35下的dll图像显示空白未调用ClearWindow或DispObj顺序错先清窗再显示检查显示算子对应的窗口句柄5.3 性能优化与内存管理小灶静态调用模式下性能优化其实比动态调用更顺手。当一段算法代码被反复调用比如每帧都跑模板匹配有个关键点模板、标定数据这类一次创建多次使用的对象一定要在类成员里缓存而不是每帧都重建。像CreateShapeModel、ReadShapeModel这类操作耗时可能占整个算法流程的30%以上反复执行纯属浪费。再就是HObject和HTuple的释放问题。Halcon的C#接口里HC对象有Dispose()方法。虽然GC最终会回收但回收时机不可控量大时会导致Halcon内部内存暴涨。我的习惯是每个循环局部变量用完了及时调用Dispose()。但注意如果这个HObject被作为返回结果传给上层调用方负责释放生产方不要提前释放否则上层拿到的就是一个空对象。6. 实操心得与扩展建议6.1 静态调用在完整项目中的角色静态调用不是你整个视觉程序的全部。它解决的是算法执行这一层的问题但你依然要面对相机取流、多线程调度、日志系统、看门狗这些上位机标配建设。我在项目里的分层思路是独立的视觉算法类库封装HDevelop导出的代码对外只暴露相机帧进、结果出的接口。上层UI和服务层完全不知道Halcon的存在。这种解耦带来的好处是立竿见影的。后续如果要把视觉算法从Halcon换到OpenCV或者用深度学习推理框架替换掉传统模板匹配主程序几乎不需要改动。算法内部怎么折腾都是类库的事。6.2 静态调用的局限与未来演进静态调用也不是银弹。算法参数调整需要重新编译整个程序这对项目验收阶段频繁调参非常不友好。我的折中方案是核心算法流程用静态调用把阈值、搜索角度范围、金字塔层数这些参数放到配置文件里程序启动时读配置注入到HTuple变量里。这样既享受了静态调用的性能和稳定性又保留了调参灵活性。还有些特殊场景比如甲方要求能在现场自己改检测逻辑那静态调用就满足不了得考虑用HDevEngine动态加载脚本。但那种项目要提前评估好脚本安全性和版本一致性面向交付的项目能不做就不做。6.3 最后分享一个省事的小技巧我在所有C#上位机项目里都会封装一个HalconHelper静态类里面放着图像转换、结果可视化、异常日志这几个高频方法。不管当前算法是静态调用还是动态调用Helper类的接口不变改算法只是换底层实现。这算是多年踩坑换来的经验省下的时间绝对值得。还有个小细节HDevelop导出的代码文件名默认是User.cs取决于你脚本里设的类名拖进VS前先重命名成有意义的名称比如VisionAlgorithm.cs。名字直观一点后来接手的人不会在几十个文件里翻白眼。
返回列表