前言
按照HDevelop用户指南/手册的章节顺序,代码导出是第十章的内容。
但6789这四章,是偏查阅的内容,不是那种流程介绍的。拿第六章GUI来说,里面子章节对HDevelop中的各种窗口做详细说明,显然前期学习阶段没有必要把里面的内容全看一遍,在用到时查阅更好,剩下的789章同理。
从个人使用的角度来讲,在学完1-5章后,已经对HALCON基本概念和HDevelop基本使用有了一定了解,之后就想把HDevelop的功能导出来,试着集成到自己的程序中。所以接下来,我决定直接跳到第十章——代码导出,进行学习。
十、代码导出
代码导出(或者说代码生成)的思想如下:根据需求开发出HDevelop程序后,要将其转换到最终的环境中(毕竟HDevelop不是专门做应用程序开发的)。为此,需要将HDevelop程序转换为另一种编程语言。
HDevelop允许将开发好的HDevelop程序导出成C++、VB .NET、C#和C,然后你只需要将导出的相应代码添加到文件即可。接下来的几节将介绍C#语言中,使用该特性进行程序开发的一般步骤(因为我的目标程序是C#的,所以只介绍C#部分)。
除了一般步骤,还会介绍代码生成和优化方面的内容。
因为HDevelop不仅执行HALCON程序,所以导出程序的行为在某些方面与相应的HDevelop程序有所不同。一个明显的点是,在HDevelop中所有结果都自动显示,而在导出的程序中,必须显式地插入算子来呈现结果。
对C++和C#开发人员来说,还有一种方法可以将HDevelop代码集成到自己的项目中。你可以导出一个库项目/工程(library project),而不必将整个HDevelop程序导出到C++或C#,该库项目可以直接集成到你自己的项目中。
接下来做介绍。
10.1. C#代码生成(HALCON/.NET)
本节描述如何从HDevelop开发的程序开始,在C#中用HALCON应用程序。HALCON能与基于.NET接口(HALCON)一起使用。(直译很拗口,反正就是如何在C#中用HALCON)
10.1.1. 基本步骤
10.1.1.1. 导出
第一步是使用菜单中的 文件>导出… 来导出程序,导出格式选中 C#-HALCON/.NET ,导出的结果是一个具有给定名称(默认名称是打开的.hdev名)且扩展名为".cs"的新文件。(导出范围选 程序 , 否则会把过程全导出,动辄几万行代码)
10.1.1.2. C#模板导出
如果用 使用导出模板(Export Template) 选项来导出文件,则它将与以下目录中预定义的C#项目一起使用(有WPF的和WinForm的)
%HALCONEXAMPLES%\c#\HDevelopTemplate
该项目包含一个窗体,窗体带有一个显示窗口(HWindowControl)和一个运行按钮。将HDevelop生成的文件添加到 解决方案资源管理器(Solution Explorer) 中(使用 添加现有项(Add Existing Item))。接着,运行项目,然后按下窗体上的运行按钮以调用导出的代码。
几点注意项:
- 默认安装在 C:\Users\Public\Documents\MVTec\HALCON-18.11-Progress\examples
- .NET框架的版本最好与模板项目匹配,相差太大可能用不了,我一开始框架是.NET6.0的,退回到.NET Framework4.7才行。
- 以 使用导出模板 的方式导出的代码,想集成到最终的程序,你可以参考HALCON的模板工程去写。
10.1.2. 程序结构
如果已经使用 使用导出模板 选项导出了程序,则HDevelop创建的文件包含一个子例程(subroutine,这里你可以看做一个函数),该子例程带有与每个HDevelop过程名对应的名称(除了主过程),主过程包含在子例程 action() 中。此外, 文件要导出为独立应用程序 。过程的图像输入输出参数分别以 HObject 和 out HObject 传递,控制输入输出参数分别以 HTuple 和 out HTuple 传递。子例程 RunHalcon() 包含对子例程 action() 的调用,并且有一个参数 Window ,该参数属于 HTuple 类型。这是窗体上所有输出操作传递到的窗口的链接。此外,还创建了另一个名为 InitHalcon() 的子例程。这个子例程应用HDevelop执行的相同初始化。
大多数变量(图像和控制)都局部声明在相应的子例程中。图像变量属于类 HObject ,控制变量属于 HTuple。
根据程序的不同,还会声明额外的子例程和变量。
下面看一下导出程序的大致结构,
10.1.2.1. 停止
HDevelop的 stop 算子在C#中被转换为创建消息框的子例程。该消息框会导致程序暂停直到按下按钮。
10.1.2.2. 用到的类
仅有四个类/类型被用到:HTuple 用于控制参数,HObject 用于图像数据。此外,还有类 HWindowControl 。它在项目中用于输出窗口,且一个类型为 HTuple 的变量将输出指向该窗口。最后,类 HOperatorSet 用作所有HALCON算子的容器。只要程序有与HDevelop中相同的功能,就不需要其他类了。当编辑生成的程序时,你可以自由使用任何HALCON/.NET类来扩展功能。
10.1.3. 限制与故障排除
除了本节和 “代码生成的一般方面” 小节中提到的限制外,也请查看 开发 章节中对HDevelop算子的描述。
10.1.3.1. 变量名
导出为所有本地图像变量增加了前缀 ho_ ,为控制变量增加了前缀 hv_ ,以避免与保留词发生冲突。
10.1.3.2. 异常处理
在HDevelop中,每个异常通常都会导致程序停止,并在对话框窗口中报告错误消息。这点在C#中可能没啥用。在C#中处理这个问题的标准方法是使用 try/catch 机制。这允许访问异常的原因并继续相应的操作。因此,对于包含错误处理 ((dev_)set_check(“~give_error”)) 的HDevelop程序,相应的代码会自动被包含。假设HALCON错误机制被关闭,则每个算子调用都包含在 try块(后面接着catch块)中。后者(指catch块)会处理异常,并将相应的HALCON错误号分配给由 dev_error_var 激活的错误变量 或 本地错误变量。
请注意,(dev_)set_check(“~give_error”) 的调用对算子调用没有影响。异常始终会被触发。对于像 H_MSG_FAIL 这样的消息也是如此,在C++中不会被当作异常处理。
10.1.3.3. 内存管理
.NET Framework运行时环境CLR(Common Language Runtime,公共语言运行时)有一种称为垃圾收集器(简称GC)的机制,CLR用它来移除内存中不再需要的.NET对象。正如前面所提,在导出的C#代码中,每个图像对象都由.NET HObject 对象表示。从GC的角度来看,.NET HObject 对象相当小。因此,它可能不会从内存中被回收,尽管底层的图像对象(例如,image)实际可能占有很大的内存。为了避免这种情况引起的内存泄漏(memory leak),在导出的代码中,每个图像对象在被分配一个新值之前都被显式删除。
10.2. 代码生成的一般性质⭐
接下来,将介绍HDevelop程序和它导出程序之间行为的一般差异。
10.2.1. 任意程序代码
可以将任意代码嵌入到HDevelop程序中。该代码在HDevelop中会被忽略。当你将程序导出为编程语言时,嵌入的代码将会一字不差的导出。
以 # 为首字符的程序行会标记任意代码行。在导出程序时,标记及其后面的第一个空格字符会被丢弃。例如,下面代码行
# Call MsgBox("Press button to continue",vbYes,"Program stop","",1000)
在HDevelop中会导出以下VB代码行:
Call MsgBox("Press button to continue",vbYes,"Program stop","",1000)
#后面可以跟其他特殊字符,以进一步制定导出时放置代码块的位置。例如,下面代码:
#^^ #define NO_EXPORT_APP_MAIN
它会在导出程序最开始位置生成下面代码:
#define NO_EXPORT_APP_MAIN
这种格式的代码行首先会从主过程中收集,然后从其它过程中的 #^^ 中收集。
这种宏定义一般都是放程序开头处的,所以不难理解会先从主过程中寻找,然后再从其他过程中找并放在程序开头处。
公用的特殊标记在下表中展示。
若你使用算子窗口来输入任意代码行,则必须选用特殊算子 export_def 。它的第一个参数指定导出代码行的目标位置(见下表最后一列);第二个参数是代码行本身;
当你将算子提交到程序窗口时,算子调用将被转换为特殊的前缀字符以增强可读性。
前缀 | 目标位置 | export_def中的表示 |
---|---|---|
# | IC的位置 | ‘in_place’ |
#^^ | 程序的最开始处 | ‘at_file_begin’ |
#$$ | 程序的结尾处 | ‘at_file_end’ |
#^ | 当前过程前 | ‘before_procedure’ |
#$ | 当前过程后 | ‘after_procedure’ |
10.2.2. 分配
在HDevelop中,每当一个新值被赋给一个变量时,它的旧内容就会自动删除,无论变量是什么类型。在导出的代码中,图像变量也是如此(HALCON/.NET中的 HObject)。关于HALCON/.NET中的图像对象的内存问题,后面内存管理(C#)章节还会详细介绍。
10.2.3. 变量名
HDevelop中的变量名是区分大小写的,即 x 和 X 在HDevelop程序中是不同的变量名。如果你将这样的程序导出到不区分大小写的目标语言(如VB .NET),开发环境会抱怨有个多个声明。要么一开始就计划好不使用这些变量名,要么在导出程序之前替换掉冲突的变量名。
10.2.4. for循环
与其他编程语言相比,旧的HDevelop程序(HALCON11之前)具有不同的for循环语义。如果打开了旧的HDevelop程序,for循环会被标记为以兼容模式运行,以模拟旧的行为 (windows系统上也有兼容模式运行,或许是类似原理?)。为了避免导出代码的混淆,建议通过删除特殊的 use_internal_index 标记来禁用兼容模式。
下面是一些兼容模式的细节问题,我这边略过了,因为比较罕见,而且现在也不会去用老版本的HALCON。
总之,建议按照以下规则来编程:
- 不要修改循环变量或循环中的步长值。若你确实需要该行为,使用 while 循环。
- 不要在循环后使用循环变量。
10.2.5. 受保护的过程🔺
正如对不同编程语言所描述的,HDevelop过程会自动导出到所选编程语言的过程或子例程/程序(subroutine)。但这并不适用于受保护的过程。由于这些过程由密码保护,因此未经授权的用户无法查看和修改它们。也因此,只要它们被密码锁定,就不能导出到任何编程语言。
10.2.6. 系统参数🔺
你应该知道HDevelop通过调用 set_system 算子来执行HALCON的一些系统参数的更改(详情见参考手册)。这可能导致导出的程序不能产生相同的输出。如果出现了这样的问题,你可以在运行程序的原始HDevelop版本后或运行时,通过HDevelop中的 get_system 方法查询系统参数。根据问题不同,可以通过显式调用导出程序中的 set_system 算子来修改相关参数。
10.2.7. 图形窗口⭐
HALCON为HALCON窗口(这个窗口可能是抽象的)提供了一种功能——模拟HDevelop图形窗口行为。
这个HALCON窗口栈可以通过HALCON接口中的类方法和函数访问,并且从HDevelop导出的代码在打开、关闭、设置或访问活动窗口时会使用该功能(有点操作窗口对象的感觉)。HALCON窗口栈机制是线程安全的,且能在线程之间共享。不过,在多线程应用程序中,用户在不同线程中切换活动窗口时必须小心,因为在一个线程中做的设置在其他线程也会生效。(就是操作时是安全的,但是线程间共用的话,可能会产生你意想不到的效果(不好的方面))
对于.NET代码导出,是使用HDevelop导出示例模板来将程序导出为代码,还是在图形窗口输出时使用前面提到的HALCON窗口栈将程序导出为代码,这是可选的。在后一种情况下,导出的代码包含一个主函数,因此可以作为一个独立的应用程序使用。HDevelop 导出 对话框允许选择相应的选项。
HDevelop的图形窗口和HALCON库的基本窗口
- HALCON/C++:class HWindow
- HALCON/.NET:class HWindowControl
- HALCON/C:addressed via handles
有着不同的功能。
-
多窗口(Multiple windows)
- 如果在HDevelop中使用 dev_open_window 算子打开多个图形窗口,只有选项 使用HALCON窗口 被选中时,这些打开窗口的调用才会转换为对应的 open_window 调用。在导出 VB .NET 和 使用选项 使用导出模板 的C#程序时,所有窗口操作都将被抑制,因为导出的代码将会和模板一起工作。如果要在此模式导出的程序中使用多个窗口,则必须手动修改代码和项目。注意,如果在程序执行期间用鼠标更改了活动的图形窗口,则使用 使用HALCON窗口 选项导出的包含多窗口的程序可能是不正确的。建议显式地使用 dev_set_window 算子来实现相同的功能。 窗口尺寸
- 在导出的VB .NET 和 C# 程序中,窗体中的窗口尺寸预定义为512×512;因此,通常并不适配你的图片大小。也因此,必须以交互方式或通过窗口的属性来调整大小。 显示结果
-
通常,每个算子的结果都会显示在HDevelop的图形窗口中。但在导出程序中,情况并非如此。导出程序的行为类似于HDevelop程序运行时的选项:“update window = off” 。建议在HDevelop程序中的每个想要显示数据的点插入算子
dev_display 。这不会改变HDevelop程序的行为,但会使导出代码中能显示图像。
当使用 使用HALCON窗口 选项生成代码时,在第一次调用 dev_display 之前关闭默认的图形窗口(使用 dev_close_window) 并 打开一个新窗口(使用 dev_open_window),以确保正确的导出。
显示图像
-
在HDevelop中,图像会自动缩放以适应当前窗口的大小,但在导出程序中不是这样的。例如,如果你加载并显示两个不同大小的图像,若第二个图像比第一个图像大,则第二个图像将会被剪辑;如果第二个图像较小,则会被黑色区域填充。为了正确显示,在使用
dev_display 显示图像前,必须使用
dev_set_part 算子,如下:
本例中, Image 是图像变量, ImageHeight 和 ImageWidth 表示它的大小。你可以使用 get_image_size 算子查询图像的大小。dev_set_part(0, 0, ImageHeight - 1, ImageWidth - 1) dev_display(Image)
注意, dev_set_part 算子(HALCON库中与之等效的 set_part )更常用于显示(从而缩放)图像的部分。通过使用上面所示的图像的完整大小来调用它,可以确保图像完全适配窗口。
更改显示参数
- 如果你在HDevelop中通过菜单项 可视化 来交互式地更改了结果的显示方式(颜色、线宽等),这些更改不会被合并到导出程序中。建议显式地在HDevelop程序中插入对应的 开发 算子(如 dev_set_color 或 dev_set_line_width ),这会使导出的代码中进行( set_color 、 set_line_width 等)适当调用。
10.2.8. 过程中未设的输出参数
如果在过程中没有为输出参数分配值,HDevelop和导出的代码对于过程的输出参数的值可能表现不同。如果过程在设置输出参数前返回异常,或者故意不设置参数,这种情况下,输出参数的值未定义,因此HDevelop和不同的代码导出可能表现会不同。代码导出不适配HDevelop行为的主要原因是,这会导致导出代码的运行时效率下降。
10.2.9. 访问未初始化的元组或向量元素
当用HDevelop访问未初始化的元组或向量元素时,会引发异常。由于性能原因,HALCON语言接口中没有相应的检查。代替引发异常的是,返回一个默认类型的元素。
a := [0, 1]
b:= a[2]
例如,这段代码在HDevelop中引发异常,但可能在某种语言中,b返回任意值。因此,必须避免这种编码风格。