☰
C#调用HALCON引擎执行HDevelop脚本:从参数绑定到部署避坑
2026/10/14 22:42:39 网站建设 项目流程

简介:C#联合HALCON编程的完整示例工程,面向需要在微软.NET框架中集成机器视觉能力的开发人员,重点解决如何引入官方接口、创建引擎对象、加载匹配算子并完成图像检测等关键问题。压缩包共收录36个文件,以C#源码工程为主,包含窗体代码、项目配置文件、资源定义文件,同时附带编译后的可执行程序、动态链接库、调试符号文件以及一个HALCON过程脚本,构成一套可直接运行的WinForms解决方案,便于对照学习接口初始化、参数设置与资源回收等环节。整体大小仅为418KB,下载与使用都很轻量。目前已有1886人浏览学习,适合正在从事自动化或机器视觉项目、希望较快上手C#与HALCON联合编程的中级开发者。借助该工程,可以直观看到图像读取、相似度参数封装、模板匹配执行和对象释放的完整调用脉络,并在此基础上扩展自定义封装类,从而提升开发效率、缩短交付周期。

1. 一个视觉检测项目为什么要用 HALCON 引擎

我认识一位做视觉检测的开发者,算法在 HDevelop 里调得很快,一到 C# 整合阶段就头疼。他不愿意把 threshold、connection 这些算子一个个翻译成 C# 调用,也不想每次改一个阈值就让整个软件重新编译发布。HALCON 引擎解决的就是这个:把 .hdev 脚本原样带进 C# 进程,脚本里的算子由引擎在运行时解释执行,C# 业务代码只负责传图、传参数、接结果。这篇文章用一套最小可运行的示例把这条链路走通,再讲清楚图像数据怎么交换、脚本参数怎么绑定,以及部署阶段绕不开的坑。

这套方案适合两类人:一类是算法已经在 HDevelop 里验证过、现在要把它嵌进 WinForm/WPF 或服务程序里的视觉工程师;另一类是团队里有多条视觉线,想让算法同学和上位机同学各自维护自己那部分,不互相踩代码的人。看完你大概能判断:什么场景真的该用引擎方式,什么场景其实直接导 C# 代码更划算。

2. 引擎调用与代码导出的路线选择:先搞清楚 HALCON 运行时

2.1 引擎里的解释执行与导出 C# 代码编译执行的差别

HALCON 给 C# 开发者提供了两条路。第一条是引用 halcondotnet.dll,在 C# 里逐行调用 HOperatorSet 下的算子,相当于把 HDevelop 脚本手工翻译成 C#。第二条就是本文要讲的:引用 HDevEngine 相关程序集,直接用 C# 加载并执行 .hdev 脚本。第二条路里,脚本不是被编译成 IL,而是由 HALCON 的引擎组件在运行时解释执行,C# 进程内相当于内置了一个 HDevelop 执行环境。

理解这个差别很重要,因为它直接决定了你后面的调试方式和性能预期。用导出的 C# 代码,变量类型在编译期就定死了,哪一行出错 IDE 能告诉你;而引擎方式下,脚本里写错了只能在执行到该行时抛异常,C# 这侧收到的是一个包含错误信息的 HDevEngineException。调试时要先在 HDevelop 里把脚本跑通,C# 这边只是“驱动脚本”,不要指望 C# 侧的断点能帮你查 HDevelop 算子的逻辑。

性能上,引擎方式做到“加载一次脚本,反复调用”之后,单次执行的开销比导出代码略高一点,但这个差距在绝大多数视觉检测场景里可接受,帧率瓶颈通常不在算子解释这一层,而在图像采集和后续业务处理。反过来,引擎方式有几个导出代码没有的好处:算法脚本可以独立替换,即使客户现场改了检测逻辑,你也只需要替换 .hdev 文件,不需要重新编译整个上位机程序;算法工程师在 HDevelop 里调好的参数、区域筛选逻辑,原样搬到 C# 端,不存在翻译过程中的理解偏差。

我的建议是:如果你的视觉项目只有一两个固定工位、算法已经冻结、近半年不会改,直接用导出 C# 代码更省事;如果算法还在迭代、现场要能快速调参、或者同一个上位机要兼容多种产品切换,那引擎方式更合适。下面所有步骤都按引擎方式展开。

2.2 搭建 C# 工程环境:两条 DLL、环境变量与最小初始化骨架

先准备工程引用。HALCON 安装目录的 bin\dotnet 文件夹下至少有两个程序集:halcondotnet.dll 和 hdevenginedotnet.dll。前者提供 HObject、HOperatorSet 这类基础类型,后者提供 HDevEngine、HDevProgram、HDevProgramCall 这些引擎类。C# 工程里把两个引用都加上,注意安装目录可能同时存在 x86 和 x64 两套运行时,按你的系统位数和工程目标选一套,不要在 64 位系统里拿 32 位 DLL 硬凑。

License 这块,开发机上通常插着加密狗或配置了本地 License 文件,引擎创建时会自动找。如果你的 License 文件放在自定义目录,设置环境变量 HALCONROOT_LICENSE 指向该目录,再启动程序。部署到客户机的 License 问题放到最后一章讲,先不展开。

初始化引擎的代码框架如下:

using HalconDotNet; public class HalconEngineWrapper { private HDevEngine engine; // 引擎实例 private HDevProgram program; // 加载的脚本对象 // 初始化引擎,hdevFile为脚本完整路径,procPath为外部过程目录 public void Init(string hdevFile, string procPath) { engine = new HDevEngine(); // 脚本如果依赖自定义.hdvp外部过程,必须设置搜索路径 engine.SetProcedurePath(procPath); // 把.hdev整个加载进内存,只需一次 program = new HDevProgram(hdevFile); } // 如果需要把脚本里的disp结果显示到C#窗口控件 public void AttachWindow(HWindowControl window) { HDevOpWindowImpl windowOp = new HDevOpWindowImpl(window.HalconWindow); engine.SetHDevOperatorImpl(windowOp); } }

这里的关键点是 engine 和 program 应该作为成员变量长期存活,而不是每次执行时重新 new。HDevEngine 的构造过程会做运行时校验和初始化,HDevProgram 构造时会解析整个脚本,这两步放在程序启动时做一次,后面每次检测只调用 CreateCall 创建一次执行会话。SetProcedurePath 的作用对象是脚本里可能出现的自定义算子调用,如果你脚本里只有 threshold、connection 这类内置算子,路径留空或指向空目录不影响运行。

还有一个容易被忽略的地方:HDevOpWindowImpl 绑定的是 HWindowControl 内部的原生窗口句柄。WinForm 里直接用 HWindowControl 控件没问题,但如果你的图像显示在 WPF 的 WindowsFormsHost 里,要确保宿主窗口在绑定前已经创建句柄,否则窗口回调绑定不上,脚本里的 dev_display 会不执行或报窗口无效。

3. 从 HDevelop 到 C#:第一个能被引擎执行的 hdev 脚本

3.1 在 HDevelop 里写一个带输入输出参数的分割脚本

引擎能执行脚本,但脚本本身必须按“可调用”的方式组织。常见的错误是直接在 HDevelop 里写一段从头到尾的流程,用 read_image 把图像读进来,调试没问题,但放进 C# 后根本不知道该从哪里传图。正确做法是:把脚本当成一个带输入输出的函数,图像、阈值是输入,筛选后的区域和面积是输出。

下面是一段可以做“提取暗色小区域”的脚本,假设待检测目标是深色背景上较暗的缺陷,灰度值低于 120 的像素被阈值切出来,再用面积筛掉噪点。这里 MinGray 设计成可调的输入参数,方便 C# 端每次传入不同的值。

* detect_region.hdev * 输入:Image(图像参数)、MinGray(控制参数,阈值下限) * 输出:SelectedRegions(筛选后的区域)、Area(总面积)、Row/Column(区域中心) threshold (Image, Region, MinGray, 255) connection (Region, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 100, 9999999) area_center (SelectedRegions, Area, Row, Column)

写完这段脚本后,关键一步是打开 HDevelop 菜单里的“参数”面板,把 Image、MinGray 设置为输入参数,把 SelectedRegions、Area、Row、Column 设置为输出参数。这一步很多人会漏:如果变量只作为脚本内的局部变量存在,C# 端不管怎么按名字传参,引擎都会提示找不到这个参数。参数列表里的名字就是 C# 端要用的键,名字大小写必须一致。

作为一段示例脚本,它在逻辑上完成了三件事:用固定阈值提取候选区域,用连通域分析把相邻像素合成一个整体区域,再用面积筛选去掉太小的噪声点。area_center 输出的 Row、Column 是每个区域的中心坐标,直接可以作为后续机械定位的输入。如果你实际项目里需要找的是亮色区域,把 threshold 的上下限颠倒一下即可。

3.2 C# 端加载、传参、执行、取结果:最小完整代码

现在写 C# 端调用的完整流程。假设你已经通过 OpenFileDialog 让用户选择了一张图片,并把它读成了 HObject。下面代码展示一次完整的“传图、传阈值、执行、取结果”过程:

using HalconDotNet; public class DetectionService { private HDevEngine engine; private HDevProgram program; public DetectionService(string hdevFile) { engine = new HDevEngine(); program = new HDevProgram(hdevFile); } // 执行分割,返回筛选后的区域和总面积 public (HObject region, HTuple area) RunSegment(HObject image, int minGray) { // 创建一次执行会话 HDevProgramCall call = program.CreateCall(); try { // 输入图像参数:脚本里名为Image call.SetInputIconicParam("Image", image); // 输入控制参数:脚本里名为MinGray call.SetInputCtrlParam("MinGray", new HTuple(minGray)); // 执行脚本 call.Execute(); // 从执行会话中取出输出参数 HObject selectedRegion = call.GetOutputIconicParam("SelectedRegions"); HTuple totalArea = call.GetOutputCtrlParam("Area"); return (selectedRegion, totalArea); } finally { // 会话对象不再使用后尽快释放 // 如果当前版本没暴露Dispose,交给GC回收即可 if (call is IDisposable disposable) { disposable.Dispose(); } } } }

这段代码里,SetInputIconicParam 负责传图像、区域这类图标参数,SetInputCtrlParam 负责传 int、double、string 这类控制参数。两者在引擎内部走的是不同的绑定通道,不能混用。new HTuple(minGray) 把 int 显式包装成 HALCON 的控制参数元组,是为了避免重载解析产生歧义。取结果时 GetOutputIconicParam 返回 HObject,GetOutputCtrlParam 返回 HTuple,HTuple 你可以直接取整数值,也可以把它当成数组遍历,因为 area_center 对多区域会输出多个面积值。

这套代码如果跑通,说明你的 HDevelop 脚本、参数定义、C# 引用三层都没有问题。接下来所有复杂项目都可以沿这个骨架扩展:换更复杂的脚本、加更多输入输出参数、把图像从文件换成相机帧。调试时如果遇到异常,先用 HDevelop 自己跑一遍同名参数,确认脚本本身没问题,再去排查 C# 端绑定,这样可以省掉大量排查时间。

4. 图像与变量在 C# 和引擎之间的数据交换

4.1 控制参数与图标参数:两种绑定方式及其规则

引擎里的参数分成两类,理解清楚后你在脚本和 C# 两边的变量设计会少走很多弯路。第一类是图标参数,指的是图像、区域、XLD 轮廓这类自带坐标系和内存管理的数据类型,在 C# 里对应 HObject;第二类是控制参数,指的是整数、浮点数、字符串以及它们的数组,在 C# 里对应 HTuple。两类参数在脚本参数列表里可以同时存在,C# 端也可以任意组合传入。

我把最常用的绑定接口整理成一张表:

方向参数类型C# 调用方式数据类型常见错误
传入图标参数SetInputIconicParam("名字", HObject)HObject传成 Bitmap 或 byte[]
传入控制参数SetInputCtrlParam("名字", HTuple)HTuple / int / double / string名字拼写不一致
取出图标参数GetOutputIconicParam("名字")返回 HObject脚本没声明为输出参数
取出控制参数GetOutputCtrlParam("名字")返回 HTuple当成普通 C# 类型强转

绑定规则里有三个容易踩的细节。第一是参数名的大写:HALCON 变量名是大小写敏感的,脚本里定义的是 minGray,C# 端写 MinGray 就会找不到。第二是脚本必须把变量声明为输入或输出参数,否则引擎不会把它放进可绑定的参数表里。第三是 HTuple 取出后是一个容器,多区域场景下它的 Length 大于 1,你要循环取下标,不能直接拿 .I 属性去读,否则遇到多元素元组会抛异常。

举一个实际例子:脚本输出 Area 在像素面积相同的情况下可能只有一个元素,但两台相机拍出的图像分辨率不同后,同一个区域集合的面积个数是变量。C# 端稳妥做法是先把 HTuple 转成 int[] 或者遍历赋值给列表。这样无论脚本内部处理出多少个区域,C# 侧不会因为元组长度问题翻车。

字符串参数也是控制参数,比如脚本里有个输入参数 ProductName 用于切换逻辑,C# 端 new HTuple("A类产品") 直接传入即可。注意 HALCON 的 HTuple 构造可以接受 string、int、double,但不要传 null,引擎侧处理空字符串参数容易报无效参数,建议传空串而不是 null。

4.2 Bitmap 图像进引擎:像素格式转换的惯用做法

相机 SDK 拿到的通常是 Bitmap 或 byte[],而引擎只认 HObject。把 Bitmap 转 HObject 是 C# 调 HALCON 最常见的麻烦点,尤其是像素格式不对齐时,出来的图像要么全黑要么错色。

最快、最稳妥的验证做法是先把 Bitmap 存成临时 BMP 文件,再用 HOperatorSet.ReadImage 读成 HObject。这个方式在调试阶段非常好用,因为格式转换由文件系统帮你做,不存在字节对齐问题。代价是多一次磁盘写读,实时处理时不推荐。

using System.Drawing; using System.Drawing.Imaging; using HalconDotNet; // 调试阶段:临时文件中转,格式不会翻车 public HObject BitmapToHObjectByFile(Bitmap bmp) { string tmpFile = Path.GetTempFileName() + ".bmp"; bmp.Save(tmpFile, ImageFormat.Bmp); HObject image; HOperatorSet.ReadImage(out image, tmpFile); File.Delete(tmpFile); return image; }

性能敏感场景下,更通用的是把 Bitmap 的像素内存直接构造为 HImage。这个方法要求 Bitmap 已经是 8 位灰度图,并且调色板与灰度值一致。如果相机出来的图是 24 位彩色,先用灰度化方法转成 8 位灰度再走下面的代码:

using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using HalconDotNet; // 性能优先:8位灰度Bitmap用像素指针直接构造HObject public unsafe HObject Bitmap8ToHObject(Bitmap grayBmp) { Rectangle rect = new Rectangle(0, 0, grayBmp.Width, grayBmp.Height); BitmapData data = grayBmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); try { HObject image; // GenImage1用外部像素内存构造图像,不等同拷贝 HOperatorSet.GenImage1(out image, "byte", grayBmp.Width, grayBmp.Height, new IntPtr(data.Scan0)); return image; } finally { grayBmp.UnlockBits(data); } }

这段代码里,GenImage1 本质上是让 HALCON 引用你提供的内存区域,不是深拷贝。所以在图像传给引擎并执行完脚本之前,Bitmap 不能被 Dispose,否则引擎读到的是被释放的内存,结果不可预期。如果你希望引擎内部立刻拷贝一份数据,在脚本里第一行加 copy_image 把传入图像复制到新变量,后续算子操作新变量,这样 C# 端用完 Bitmap 随时释放,代价是额外一次内存拷贝,换来内存安全。

除了 Bitmap,还有一类常见来源是相机 SDK 直接给 byte[] 原始灰度数据。这种情况可以跳过 Bitmap,直接用固定指针 + GenImage1 构造图像,省掉一次格式转换。如果相机输出的是 Bayer 格式或 YUV,优先让采集库转成 RGB24 或灰度再进 HALCON,HALCON 本身也能处理 Bayer,但需要你明确设置彩色格式类型,初学者容易设错,先用采集卡的格式转换更省心。

5. 调用 HALCON 引擎的常见坑与排查:五个血泪记录

5.1 启动阶段的高频故障:DLL、位数与 License 环境

现象一:程序启动时抛 DllNotFoundException 或 BadImageFormatException,错误信息指向 halcon.dll 或 hdevenginedotnet.dll。

原因基本出在两个地方:一是 C# 工程目标平台和 HALCON DLL 位数不一致,比如你的 HALCON 是 64 位,而工程编译成 x86;二是运行时找不到 DLL,也就是 DLL 不在 exe 所在目录,也没有被系统路径覆盖。这个错误在开发机上最常发生在新建项目时顺手选了 AnyCPU,项目在 64 位系统上会优先按 64 位跑,但引用的某个 32 位原生依赖又把它拽回 32 位模式,导致加载错乱。

解决方式:排查 C# 工程“平台目标”,明确改成 x64(如果装的 64 位 HALCON),再把 HALCON 安装目录下对应位数 bin 文件夹里的 HALCON DLL 复制到 exe 输出目录,或者把该目录加入系统 PATH。建议是不要依赖系统 PATH,直接把需要的 DLL 放到输出目录随程序走,部署到客户机时少一个环境变量配置项。

现象二:引擎对象 new 成功,但执行脚本时抛异常,提示 License 相关错误或被拒绝的运行时行为。

原因:开发机的加密狗 License 没有覆盖正在使用的 HALCON 版本,或者环境变量 HALCONROOT_LICENSE 指向的 License 目录无效。引擎方式对 License 的校验比直接调算子更严格,因为它要启动完整的解释执行组件。

解决:开发机上确认 HALCON 自带的助手能看到有效 License 并支持引擎功能;如果 License 文件是手动放置的,用环境变量指向正确目录后重启程序。客户机上部署的问题在第 6 章展开,这里先提一句:不要试图把开发机 License 文件直接拷到客户机,HALCON 运行时授权和开发授权是两套机制。

5.2 运行阶段翻车:变量绑定、外部过程与图像格式

现象三:Execute() 调用时抛 HDevEngineException,提示类似“变量 XXX 不存在”或“参数 XXX 未被定义”。

原因:脚本里的 XXX 确实存在,但它是脚本内的局部变量,没有在“参数”面板里声明为输入或输出。引擎只认参数列表里的变量,其他变量虽然参与脚本内部计算,但 C# 端没有访问权限。还有一种情况是参数名大小写不一致,HALCON 变量名对大小写敏感,脚本里写 minGray,C# 端传 MinGray,引擎找不到。

解决:在 HDevelop 中打开脚本的参数面板,核对输入输出参数的名字是否和 C# 端完全一致。建议把参数名定为有业务含义且大小写固定的英文单词,比如 MinGray、MaxArea、ProductName,C# 端写一个常量类统一管理这些名字,避免字符串散落各处的拼写隐患。

现象四:脚本调用自定义外部过程(.hdvp)时报错,找不到算子或加载过程失败。

原因:你的脚本在开发调试时依赖 HDevelop 全局的过程搜索路径,到了 C# 端这个路径不存在了。引擎虽然能加载脚本,但脚本里引用的外部过程需要单独通过 SetProcedurePath 告知搜索目录。漏配这个路径是最常见的“开发机上跑得好,部署到 C# 就报缺算子”的原因。

解决:把所有自定义外部过程文件放到同一个目录,在引擎初始化后立即调用 SetProcedurePath 设置该目录。如果你用的是外部过程包,同样把包含 .hdpl 和 .hdvp 的目录配置进去。配完之后可以在脚本里加一句 dev_get_preferences 之类的调试输出确认搜索路径,但大多数时候只要目录放对就能解决。

现象五:从 Bitmap 转出来的图像在脚本里处理结果异常,全黑、颜色错乱、区域完全对不上。

原因:像素格式和 HALCON 期望的通道数不一致。最典型的是把 24 位彩色 Bitmap 直接当单通道灰度传给 GenImage1,HALCON 按单字节像素读取,得到的是混乱的通道交错数据。另一个常见做法是用 Format32bppArgb 的 Bitmap 直接构造图像,HALCON 侧没有对应感知到 Alpha 通道,结果自然不对。

解决:在转换前明确图像通道。处理灰度检测任务,先把 Bitmap 统一转成 8 位灰度图;处理彩色识别任务,用 RGB 三通道构造 HObject,然后把彩色格式类型通过脚本里的算子声明清楚。如果只是为了快速验证,临时文件中转的方式最省心。

6. 跑得快还要发得出去:Procedure 调用与发布要点

6.1 用 HDevProcedure 替代 HDevProgram 提升重复调用效率

如果你的程序要在一段连续图像流上逐帧调用同一个脚本,每次都通过 HDevProgram.CreateCall 创建会话,会让引擎在每次执行前做一次脚本状态的完整解析。更轻量的做法是把脚本作为 Procedure 加载,启动时解析一次,后续调用只绑定新参数并执行。核心代码差异不大:

using HalconDotNet; HDevProcedure procedure = new HDevProcedure(@"detect_region.hdev"); HDevProcedureCall procCall = procedure.CreateCall(); procCall.SetInputIconicParam("Image", image); procCall.SetInputCtrlParam("MinGray", new HTuple(100)); procCall.Execute(); HObject region = procCall.GetOutputIconicParam("SelectedRegions"); HTuple area = procCall.GetOutputCtrlParam("Area"); // 如果接口暴露了Reset/Dispose,在下一次调用前按版本签名调用

把 procedure 对象缓存为成员变量,每次检测只创建新的 Call 执行,是我在某个实时定位项目里做过的优化:单帧处理耗时明显下降,而且脚本修改后只要替换文件不需要重编译。注意引擎实例不要在多线程里共享同一个 Call,稳妥做法是每个采集线程维护自己的引擎和程序对象。

6.2 脱机发布时 License 与运行时目录的落地方式

发布到客户机前,把 HALCON 相关 DLL 随程序输出目录一起打包,并在安装程序里配置 HALCONROOT_LICENSE 环境变量指向运行时 License 文件所在目录。License 走开发商提供的运行时授权,不要把开发狗和开发 License 的配置方式带到生产环境。装完后用一台无 HALCON 开发环境的机器验证:程序启动、引擎初始化、脚本执行、图像显示四步都正常才算发布完成。我在交付时习惯额外打印引擎版本和 License 状态到启动日志,客户现场出问题时能第一时间区分是环境问题还是算法问题。

一开始我总觉得引擎方式会比导出代码慢,后来一个实时项目里遇到隔几分钟卡顿,排查半天发现是每帧都重新 new HDevProgram,还跨线程共用一个 Call,改回“单例引擎 + 每线程独立 Procedure”之后再没出现过。类似的坑踩多了,经验就是:引擎本体尽量长寿,会话对象尽量轻量。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询