最近带教一位刚入行的同学跑Halcon基础练习,他指着我屏幕上那个用线段和色块拼出来的楼梯图形问:“这个打印是怎么实现的?”一听“打印”这两个字我就笑了——Halcon里其实没有打印机驱动,日常我们说的“打印”,就是把Region、XLD这类几何对象渲染到图形窗口里。本文就用这个斜式楼梯和正方形的小案例,把Halcon最常打交道的两类对象、四个常用生成算子、坐标与角度的换算讲透。内容适合刚接触Halcon的开发者,也适合准备给团队做入门分享的朋友直接拿去当讲课素材。
1. 动手前先弄清:Halcon里的“打印”到底在做什么
1.1 Region、XLD与图形窗口的关系
Halcon里有两类最常碰到的可显示对象:Region(区域)和XLD(亚像素轮廓)。Region本质是一个像素集合,分辨率对齐到图像栅格,适合做掩膜、ROI、形态学处理;XLD则是由点序列构成的连续轮廓,通常由边缘拟合、亚像素测量得到,坐标精度远高于像素级。
我们说的“打印”,实际是调用dev_display或disp_obj,把几何对象画在dev_open_window打开的图形窗口里。窗口就是画布,不是打印机。画出来的结果可以截图保存,但大多数调试场景下,我们只关心窗口里的即时反馈。
这里有一个初学者最常忽略的点:Region只能“像素级”存在,XLD是“亚像素级”存在。二者不能直接混用,但可以互相转换。后面你会看到,同一个正方形,既可以用矩形算子直接生成Region,也可以先画XLD轮廓再转Region,两条路结果一样,过程完全不同。
1.2 Row/Column坐标系的坑先埋在这
Halcon的图像坐标系原点在左上角。Row代表行号,向下增大;Column代表列号,向右增大。也就是说,Row对应视觉上的Y坐标,Column对应视觉上的X坐标,但Y轴方向是翻转的。
这个点特别容易出问题。我见过不少同学把Row当横坐标、Column当纵坐标,画出来的图形横竖颠倒。你只需要记住一句话:凡是涉及到位置和尺寸的算子,参数顺序基本都是Row在前、Column在后。
后面所有代码都遵循这个顺序,包括顶点序列里的Rows和Cols。
1.3 跑案例前的最小环境与代码模板
案例需要一个能正常运行的HDevelop环境,任何近几年发布的版本都行。不依赖额外库,也不需要真实图像,窗口本身就可以当画布。
先写一个通用初始化片段:
* 关闭可能残留的窗口,打开一个400x400的黑色画布 dev_close_window() dev_open_window(0, 0, 400, 400, 'black', WindowHandle) dev_set_color('green') dev_set_draw('margin') dev_set_line_width(2)这里重点说下dev_set_draw('margin'),它表示只描边不填充。这在调试组合图形时非常关键。如果默认是'fill'填充模式,多个矩形叠加后重叠区域会被后画的覆盖,你根本看不清边界。等图形逻辑全部确认后,再把'fill'打开看最终效果也不迟。
2. 斜式楼梯第一种做法:循环堆矩形,简单直观
2.1 核心算子与建模思路
gen_rectangle2是Halcon里最常用来画旋转矩形的算子,签名是gen_rectangle2(Rectangle, Row, Column, Phi, HalfWidth, HalfHeight)。它围绕中心点生成一个矩形,HalfWidth控制水平半宽,HalfHeight控制垂直半高,Phi控制整体旋转弧度。
想画出斜式楼梯,最直接的思路是拆解:把楼梯拆成若干个矩形台阶,每个台阶相对上一个在Row和Column方向同时偏移固定步长,循环生成,再合并成一个对象集合。
我特意选gen_rectangle2而不是gen_rectangle1,是因为gen_rectangle2支持旋转参数Phi。等会发现,楼梯的“斜”其实有两种理解:一种是台阶排列方向斜,另一种是台阶本身带倾角。用gen_rectangle2能同时控制这两种倾斜,而gen_rectangle1只能画轴对齐矩形,能力上就少了一截。
2.2 完整代码与逐行拆解
* 初始化 dev_close_window() dev_open_window(0, 0, 500, 500, 'black', WindowHandle) dev_set_color('green') dev_set_draw('margin') dev_set_line_width(2) * 方法一:循环堆矩形 Stairs := [] for i := 1 to 8 by 1 Row := 20 + (i - 1) * 15 Col := 20 + (i - 1) * 15 Phi := -0.15 gen_rectangle2(Rect, Row, Col, Phi, 18, 8) if (i > 1) concat_obj(Stairs, Rect, Stairs) else copy_obj(Rect, Stairs) endif endfor * 显示合并后的对象集合 disp_obj(Stairs, WindowHandle)运行后你会看到:8个半宽18、半高8的小矩形,每个矩形中心在行、列方向都增加15,并整体旋转-0.15弧度。矩形之间有重叠,但margin模式下每一级台阶的边界都看得清清楚楚,整体形成一条斜着往右下延伸的阶梯带。
几个关键参数解释一下:
Row和Col同步增加15,让台阶沿45度方向排列。如果你想改变坡度,把Row增量调大或把Col增量调小即可。Phi统一设成-0.15,让每个台阶沿自身轴线轻微旋转,楼梯轮廓看起来更“斜”。如果设成0,效果就是正交矩形沿斜线排列,变成俯视状态的楼梯。实际项目中两种效果都有用途。concat_obj把新对象追加到对象元组。首次循环用copy_obj把第一个矩形存入Stairs,避免空元组拼接报错。HDevelop里也有等价写法Stairs := [Stairs, Rect],更简洁,但concat_obj在跨语言调用时语义更清晰,我习惯用它。
2.3 调参公式与预览效果
台阶总数n、起始点(R0, C0)、Row步长StepR、Col步长StepC之间的关系可以写成一个通用公式:
第i级台阶的中心坐标:
Row_i = R0 + (i - 1) * StepR Col_i = C0 + (i - 1) * StepC我整理了一个调参表格,方便你按需微调:
| 参数 | 本例取值 | 调整效果 |
|---|---|---|
| 台阶数n | 8 | 楼梯总长度成正比 |
| Row步长 | 15 | 越大楼梯越陡 |
| Col步长 | 15 | 越大楼梯越宽 |
| 半宽HalfWidth | 18 | 每级踏面长度 |
| 半高HalfHeight | 8 | 每级立面高度 |
| Phi | -0.15 | 台阶自身倾角,弧度制 |
特别提醒:当HalfWidth大于ColStep时,相邻台阶会重叠。重叠后如果用union1做区域合并,会形成连通实体,这在做掩膜时是优点;但如果你的目标是逐级统计台阶数量,重叠反而会干扰后续的连通域分析。取舍取决于你到底要“一个整体楼梯”还是“一组独立台阶”。
这是我实际调试中反复纠结过的地方,写在这里算是给后来人指个方向。
2.4 这种做法的真实适用面
循环堆矩形特别适合“每个结构需要独立操作”的场景。
比如做芯片引脚检测时,同一行引脚完全可以用循环生成多个小矩形ROI,每个ROI独立调参数,而不是画成一个整体多边形。这种写法还有一个隐性好处:对象元组里每个对象自带序号,后续用select_obj就能单独取出来做差异性处理,比如给不同的引脚设置不同的灰度阈值。
缺点是参数分散。台阶数量、间距、角度全部散落在循环代码里,后期想调整数值,得翻代码逐行找。如果要生成几十个台阶,这种写法也会显得冗长。这个缺点恰恰引出第二种方法。
3. 斜式楼梯第二种做法:多边形顶点一把梭
3.1 为什么换个思路:外轮廓建模
如果你想要的是一个“整体的楼梯区域”,而不是一组独立矩形,循环堆矩形就不太合适了。虽然可以最后用union1把所有矩形粘成一个连通区域,但绕了一步。
更直接的做法是提取楼梯外轮廓的顶点序列,用gen_region_polygon一次生成。签名很简单:gen_region_polygon(Region, Rows, Columns),按顶点顺序围出一个填充多边形。
顶点顺序只要沿外轮廓走一圈即可,不需要自己计算每一块矩形的位置。这个方法生成的区域天然是一个整体对象,后续做最小外接矩形、凸包、形态学处理时,逻辑非常干净。
坏处也明显:顶点顺序一旦写错,很容易得到自交多边形或完全变形的图形,而且不像循环堆矩形那样容易看出是哪一步的问题。
3.2 顶点序列怎么算:循环生成可延伸
为了避免硬编码一堆数组,我习惯用循环生成顶点元组。下面设计一个6级台阶的侧视楼梯,平台宽20、升高20,起始点(50, 50)。
思路是每一级台阶贡献两个顶点:水平踏面末端和竖直立面底端。看代码:
Rows := [] Cols := [] for i := 0 to 5 by 1 Rows := [Rows, 50 + i * 20, 50 + i * 20] Cols := [Cols, 50 + i * 20, 50 + (i + 1) * 20] endfor * 底边和左边闭合 Rows := [Rows, 50 + 6 * 20, 50] Cols := [Cols, 50, 50] gen_region_polygon(StairsPoly, Rows, Cols) disp_obj(StairsPoly, WindowHandle)手动模拟一下顶点序列前几段:
(50, 50) -> (50, 70) -> (70, 70) -> (70, 90) -> (90, 90) -> (90, 110) ...第一个点到第二个点是水平踏面,第二个点到第三个点是竖直向下走一级,第三个点到第四个点又是一格踏面,依此类推。走到最右下角后,最后一组顶点拉一条水平底边回起始行,再从底边直接竖着回到起点,闭合多边形。生成的就是一个完整的锯齿形楼梯区域,不是一堆矩形拼接,视觉上就是一个斜楼梯的整体。
这里有两个常用变种:
- 如果想画朝左上方的斜式楼梯,把循环里的增量改成负值即可。
- 如果
StepRow不等于StepCol,台阶会沿更陡或更缓的方向倾斜。
但要注意:如果希望台阶本身带倾角,也就是边界不是横平竖直而是斜的,这种多边形方案做不到,因为锯齿边必须是水平与垂直交替的。这时候只能回到gen_rectangle2旋转法。这是两种方法真正的分水岭。
3.3 两种楼梯方法的对比,别选错
我把两种方法做成了一张对比表,方便你按场景选型:
| 对比维度 | 循环堆矩形 | 多边形顶点法 |
|---|---|---|
| 生成对象数 | 多个独立矩形 | 单个整体区域 |
| 是否支持台阶自身倾角 | 支持,通过Phi调整 | 不支持,锯齿边只能横平竖直 |
| 区域是否连通 | 不一定,取决于重叠与否 | 必然连通 |
| 后续单独处理某级台阶 | 容易,select_obj即可 | 很麻烦,要先拆分 |
| 代码可读性 | 循环较直观 | 顶点生成段稍短 |
| 典型场景 | 引脚ROI、逐级测量 | 掩膜、形态学、整体分析 |
我见过不少人在这个方法选择上纠结很久,其实没有绝对的优劣。如果你是做模板匹配前的ROI截取,或者做形态学预处理,多边形顶点法明显更快;如果你要做多目标独立分析,循环堆矩形更好使。先想清楚后续算子要什么,再选生成方式,别本末倒置。
4. 正方形的两种实现:矩形算子和XLD路线
4.1 方法一:gen_rectangle1/gen_rectangle2一把生成
画正方形最简单的方式就是矩形算子。
gen_rectangle1(Rect, Row1, Col1, Row2, Col2)接收两个对角顶点,生成轴对齐矩形;gen_rectangle2(Rect, Row, Col, Phi, HalfWidth, HalfHeight)围绕中心生成,支持旋转。
要得到一个200×200的正方形,两种写法:
* 轴对齐正方形,左上角(20,20),右下角(220,220) gen_rectangle1(Square1, 20, 20, 220, 220) * 中心点(120,120),半宽半高均为100,角度0为轴对齐 gen_rectangle2(Square2, 120, 120, rad(0), 100, 100)这里有个细节必须强调:gen_rectangle2的Phi单位是弧度,不是角度。rad(0)就是0度,rad(45)是45度。
当Phi等于rad(45)时,正方形看起来会变成菱形,这在画旋转ROI时非常常用。比如标记倾斜的晶圆边缘或倾斜的码盘标记,都需要这种“带角度的正方形”。如果某些资料里写的是角度值,跑出来却完全不对,大概率就是忘记转弧度。
4.2 方法二:多边形XLD转区域,自由度更高
有时候我们希望先拿到一个精确的轮廓对象,再从轮廓转成区域。因为轮廓在亚像素测量中更有价值,比如后续要做亚像素边缘拟合、轮廓匹配。
画正方形的第二条路线就是用gen_contour_polygon_xld生成闭合多边形轮廓,再用gen_region_contour_xld填充成区域:
* 第一个点与最后一个点相同,形成闭合轮廓 gen_contour_polygon_xld(SquareContour, [50,50,250,250,50], [50,250,250,50,50]) gen_region_contour_xld(SquareContour, SquareRegion, 'filled') disp_obj(SquareContour, WindowHandle) * 换个颜色显示区域 dev_set_color('red') disp_obj(SquareRegion, WindowHandle)执行后你会看到绿色轮廓和红色填充同时存在,非常直观。
这里有两个关键点:
第一,gen_region_contour_xld的第二个参数是填充方式,'filled'生成实心区域,'margin'只保留轮廓边。只要你需要的是区域掩膜,就用'filled'。
第二,闭合轮廓必须首尾重合。上面代码里第一个点和最后一个点都是(50,50)。如果忘了闭合,填充区域会出现缺角,甚至完全无法生成有效区域。
为什么明明有更简单的gen_rectangle1,还要走XLD这条路?因为实际项目里很多轮廓不是我们凭空画的,而是从图像边缘拟合出来的。我们要把已有的XLD轮廓转成ROI区域,这套流程就绕不开。先把空手画正方形的流程跑通,后面处理拟合轮廓时才不会慌。
4.3 区域、轮廓、多边形三者如何来回倒
实际工程里,Region、XLD、点坐标三种形态经常需要互相转换。我把常用转换路径整理成一张表:
| 起点 | 终点 | 常用算子 |
|---|---|---|
| 矩形/多边形 | 区域 | gen_rectangle1 / gen_rectangle2 或 gen_region_polygon |
| 区域 | XLD轮廓 | region_to_xld |
| XLD轮廓 | 区域 | gen_region_contour_xld |
| XLD轮廓 | 点坐标 | get_contour_xld |
| 点坐标 | XLD轮廓 | gen_contour_polygon_xld / gen_contour_xld |
| 点坐标 | 区域 | gen_region_polygon |
这张表是我刚开始用Halcon时最常翻的参考。你记住了这套转换路径,后面做模板匹配兴趣区、亚像素边缘测量、形态学预裁剪,基本都能快速套用,不用每次翻算子文档查来查去。
5. 四个图形画完后,我踩过的坑和积累的习惯
5.1 窗口显示状态三件套:颜色、填充、线宽
很多人跑完代码发现“我怎么没看到图形”,大概率不是生成失败,而是显示状态问题。
dev_set_color控制颜色,dev_set_draw控制填充还是描边,dev_set_line_width控制线宽。特别是多个对象一起显示时,如果不设置颜色,所有对象全用默认色叠在一起,根本分不清谁是谁。此时可以调用dev_set_colored(12),让Halcon自动为每个对象分配不同颜色。
还有一个我常犯的错:循环里disp_obj之前忘了dev_clear_window,导致上一帧图形残留在窗口里,看起来像重影。HDevelop的dev_display自带清窗逻辑,但纯disp_obj不会。
我的调试固定动作是三行连招:
dev_clear_window() dev_set_draw('margin') disp_obj(Obj, WindowHandle)每次都是干净的一帧,绝不让迭代结果叠成一锅粥。
5.2 角度写错等于白画:弧度与正方向
有一次我让一个学员画旋转正方形,他把gen_rectangle2的Phi直接写成45。结果图形完全乱掉,显示成一团线。原因很简单:Halcon所有角度参数统一用弧度,他传的是角度,等于让矩形旋转了约2578度,画面自然没法看。
如果你习惯用角度,记得用rad(45)或tuple_rad(45)做转换。
方向问题同样容易踩坑。由于Row轴向下为正,当Phi为正值时,矩形在视觉上是顺时针旋转;想要逆时针效果,需要给负角度。我最早调斜式楼梯时,以为画歪了是因为代码写错,调试了很久才发现是方向符号理解反了。
稳妥做法是写一个只有一行的小脚本,分别测试Phi=0.1和Phi=-0.1的效果,几秒钟就能确认方向对应的符号。别凭直觉写正负号,在这个坐标系里直觉经常不靠谱。
5.3 从“画图形”到“画掩膜”的一点现实经验
斜式楼梯和正方形这两个案例看着简单,但它们延伸出来的技能非常实用。
我在一个图像分割项目里,为了把倾斜的表单区域裁出来,就是用gen_region_polygon按表格外边框顶点生成掩膜,再通过reduce_domain把图像裁剪到区域内。区域叠加在图上显示时,视觉上就像给表格画了一个斜框,裁剪后表格里的所有内容自动对齐到后续处理管线。
正方形ROI则在模板匹配里反复出现。先用gen_rectangle1确定搜索窗口,再通过reduce_domain缩小搜索范围,匹配速度能提升好几倍。如果搜索目标有旋转角度,改用gen_rectangle2带上Phi参数,把旋转范围先限制住,匹配效率和精度都上一个台阶。
这也是我强烈建议初学者认真练一遍这类“画图形”小案例的原因。它训练的不仅是几个算子的用法,更是Region和XLD之间的选择意识——什么时候用区域逻辑,什么时候用轮廓几何,这种判断能力才是最值钱的东西。
最后补一个我自己常年保留的调试习惯:凡是画组合图形的Demo,开场一律把dev_set_draw('margin')写在初始化里,等图形逻辑全部确认之后,再改成'fill'看最终填充效果。这个习惯帮我排掉了不知道多少“图形叠在一起看不清边界”的无效调试时间。
斜式楼梯和正方形只是开始,等你把这条“画图形”的手感练熟,再去做ROI裁剪、掩膜生成、亚像素轮廓重建,会发现一切都顺很多。动手把这几个例子敲一遍,比看十遍文档都管用。