做视觉的同行应该都有同感:产线上只要涉及尺寸检测,就绕不开“先定位,再测量”这个套路。Halcon里最经典、也最实用的一套组合,就是标题里写的“模板匹配+对齐模板+测量边”。简单说就是先用模板匹配找到工件在当前图像里的位置和旋转角度,再把模板坐标系里的测量区域通过仿射变换搬到工件实际所在的位置,最后用测量工具在指定窗口内把边缘精确提出来、算出距离或位置。三步接力,解决的就是工件摆放有偏移、有旋转时仍然能稳定测出尺寸的问题。这篇文章把这个例子的完整思路、算子参数、实操过程和踩坑记录都写清楚,适合正在学Halcon的初学者,也适合在项目里做视觉方案验证的工程师参考。
1. 整体思路拆解:为什么模板匹配、对齐、测量要组合在一起
1.1 三个模块各自扮演的角色
先说定位。工业场景里,工件放到相机底下很少是纹丝不动的,传送带停位有误差、夹具装夹有公差、来料方向也可能有旋转。如果不对图像做任何处理,直接用固定位置的测量窗口去测边缘,结果一定是乱的——工件稍微偏一点,测量窗口可能就落在背景上,测出来的尺寸就废了。
这时候模板匹配的作用就出来了。Halcon的shape-based matching基于图像的边缘梯度方向建立模板描述子,鲁棒性比灰度相关匹配好得多。它对光照变化不敏感,对轻微的遮挡也能扛,而且匹配精度能做到亚像素级。匹配完成后,我们会拿到三个量:当前工件在图像中的行坐标Row、列坐标Column、旋转角度Angle。这三个量就是“工件现在到底在哪里、歪了多少”的完整描述。
有了位置和角度,下一步就是“对齐模板”。这里的对齐不是把图像本身旋转平移过去,而是把模板坐标系下的各种参考信息——测量窗口的中心坐标、测量角度——通过刚体变换矩阵映射到当前图像坐标系下。为什么要这么做?因为我们在建模板的时候,是在一幅标准图像上手动画出的测量区域,那套坐标只属于模板图。真实工件一旋转一平移,测量窗口如果还按照模板图的坐标去放,角度和位置全对不上,测出来的边缘就是斜着截过去的,结果自然不准。
测量边是这个流程最后一步。对齐之后的测量窗口已经有了正确的中心和角度,用它去提取边缘,才能得到真实可靠的像素坐标、灰度梯度以及边缘之间的距离。测量窗口本质上就是一把虚拟卡尺:它在一个长条形的ROI内沿着指定方向采样灰度变化,通过导数找极值点,再用亚像素拟合出边缘的具体位置。输出的是边缘的行列坐标和幅度,距离自然就能算出来。
1.2 “对齐”是整条链路的关键衔接点
很多人初次接触模板匹配,会有个误解:find_shape_model找到了位置,尺寸测量直接用一个fit_line或threshold就能做了,干吗非要搞“对齐模板”?我早期也这么干过,后面吃了亏。原因是,工业测量要求的是可重复、可追溯、鲁棒性高。你直接在整幅图上找边,周围有毛刺、脏污、反光、其他工件边缘靠近时,边缘点会混入大量干扰。而用一个“知道该去哪里测”的测量窗口,相当于把搜索范围限制在真正有用的局部区域内,干扰大幅减少。
再具体说“对齐”这个环节的精度价值。Halcon里做刚体变换,核心算子是vector_angle_to_rigid。它接收匹配得到的Row、Column、Angle,输出一个HomMat2D变换矩阵。之后用affine_trans_point_2d把模板坐标系下的测量窗口中心点变换到当前图像坐标,测量角度直接在当前匹配角度的基础上偏移。这个变换只用了一次矩阵运算,坐标转换是严格可逆的,只要匹配位置是准的,测量位置就一定是准的。反过来,如果为了省事把整幅图像做affine_trans_image重采样到模板坐标系,再在重采样图上测量,看起来是一次性解决,但图像插值会损失边缘信息,亚像素精度会打折扣,性能也差。所以我的习惯是:永远变换“测量区域”,不要变换“图像本体”。
1.3 这套方案适合什么场景
最典型的是3C小零件检测:测端子引脚间距、测连接器宽度、测电池极耳位置。这类场景的共同特点是:工件轮廓清晰、边缘对比度稳定、每次摆放有平移和旋转但量级可控。模板匹配+对齐+测量边这套组合,几乎可以无脑复用。即使工件有轻微缩放(比如注塑件收缩率),也可以把create_shape_model换成create_scaled_shape_model,多加一个ScaleMin和ScaleMax的范围,其他流程完全不变。如果工件形状差异非常大,边缘不再是直线而是一段圆弧,测量边可以换成measure_arc或直接提取XLD轮廓做拟合,但前两步——匹配和对齐——依然成立。
2. 核心原理与关键参数:先把底层逻辑吃透
2.1 基于形状的模板匹配原理与参数含义
Halcon的create_shape_model建立的是一个“形状模板”,它不是保存整幅灰度图,而是提取目标区域内的边缘点,记录每个边缘点的梯度方向和位置关系。匹配时,Halcon在搜索图像的不同位置、不同角度上,用模板的梯度方向场与图像局部梯度方向场做相似度评估。只要边缘梯度方向基本一致,就算灰度绝对值有偏移(比如打光变亮或变暗),评分依然很高。这是它比NCC相关匹配强的地方。
创建模板时有一组参数必须认真调:
- NumLevels:金字塔层数,一般填‘auto’。层数越高,匹配越快,但代价是丢失细小的边缘细节。如果目标很小或者边缘细节很关键,auto不行,就手动设小一点,比如4或者6。如果目标边缘又粗又大,auto很聪明地会选择多一些层数。
- AngleStart / AngleExtent:允许的旋转范围。我在示例里常用rad(-15)、rad(15),也就是正负15度。这个范围不要任性地给到正负180度,因为旋转范围越大,搜索空间越大,匹配时间越长,也越容易在类似物上误匹配。先观察产线上工件实际可能的偏移量,留出1.2倍余量就够了。
- Optimization:模板优化方式,用‘auto’或‘none’都行。保留全部特征点则匹配更稳健,速度会慢一点;优化掉一部分则更快,但可能在极端光照下丢特征。
- Contrast:边缘对比度阈值,单位是灰度差。比如设30,表示只有梯度幅度超过30的边缘才会被纳入模板。如果目标表面纹理乱,或者有多余的印刷图案,把Contrast调高一些,能过滤掉一些噪点,模板更干净。
- MinContrast:在匹配图像中,目标边缘至少要达到多少灰度对比度才认为有可能是目标。如果图像有噪声或者光照偏暗,设一个较小的MinContrast(比如10)能提高找到目标的概率,但误匹配也会增加。这个参数属于防守型参数,实际项目里我一般设成Contrast的一半或三分之一。
匹配时的参数也有讲究:
- MinScore:最低匹配得分,0到1之间。设太高容易丢目标,设太低又容易出假匹配。通常从0.5开始调。在调试阶段我会先把MinScore设成0.3,多跑几张图看正常工件的Score普遍是多少,然后设定在正常值以下的0.8倍。
- NumMatches:期望找到几个目标。单个工件设为1。如果是多个工件同时定位,可以设多一些,但注意要配合MaxOverlap处理重叠候选。
- SubPixel:亚像素细化方式。‘least_squares’是常用选择,它会在匹配位置附近做最小二乘拟合,位置精度能到0.1像素甚至更好。
- Greediness:贪婪度。0是最保守,匹配最稳但慢;1是最激进,快但可能跳过正确位置。一般场景用0.7到0.9之间,对速度敏感就用0.9,对稳定性敏感就用0.7。
2.2 仿射对齐:从匹配结果到测量区域
对齐模板的核心是刚体变换。Halcon里vector_angle_to_rigid的完整语义是:将模板坐标系里的一个参考点(我习惯在创建模板后用set_shape_model_origin把原点设到工件特征点上)映射到当前图像坐标系中匹配到的位置。调用方式:
vector_angle_to_rigid (0, 0, 0, Row, Column, Angle, HomMat2D)
前三个0表示模板坐标系的原点,后三项是匹配结果。得到的HomMat2D描述的就是“模板坐标到图像坐标”的平移加旋转关系。之后,模板坐标系里任何一个测量ROI的中心坐标,都可以用affine_trans_point_2d一次变换得到当前图像中的实际坐标。
有一点需要特别注意:测量窗口的角度。在模板图像里定义测量矩形时,它本身会有一个角度(比如0度表示测量方向沿水平方向)。当前图像里测量窗口的实际角度应该是模板测量窗口角度加上匹配得到的旋转角度Angle,而不是直接用Angle。很多人图省事,只用Angle作为gen_measure_rectangle2的角度,如果模板里的测量窗口不是水平或竖直的,测出来的位置就会偏。
2.3 边缘测量的工作原理:虚拟卡尺与亚像素
Halcon的测量工具核心是gen_measure_rectangle2加measure_pos或measure_pairs。测量矩形本质上是一个长条形ROI,它有中心坐标(Row, Column)、角度Phi、半长度Length1和半宽度Length2。Length1是测量方向上的半长,Length2是垂直于测量方向的半宽。比如要测一条竖直边缘的水平位置,测量矩形应该水平放长,Length1远大于Length2,边缘在测量矩形中间穿过。测量时,Halcon在矩形长轴方向上等距采样若干条扫描线,每条扫描线计算灰度的导数,找到极值点,再在垂直于边缘方向上做亚像素插值,最终输出边缘点的行坐标、列坐标和幅度。
measure_pos适合测单条边缘,它输出每个扫描线上的边缘点;measure_pairs适合测成对的边缘,它把一对“亮到暗”和“暗到亮”的边缘配对,直接输出边缘对之间的距离(IntraDistance)和相邻对之间的距离(InterDistance)。测宽度、间距这类任务,用measure_pairs往往一步就能出结果。
这里有个很基础但很关键的参数逻辑:Sigma决定平滑程度。Sigma太小时噪声会给出很多假边缘,Sigma太大时弱小边缘会被模糊掉。对于清晰的工业边缘,我常用的Sigma是2到3。Threshold是边缘幅度阈值,低于阈值的候选边缘会被过滤,典型值是20到30。Transition参数指定仅检测‘positive’(亮到暗)、‘negative’(暗到亮)还是‘all’。能明确就尽量明确,减少干扰。
3. 实例实操:Halcon模板匹配加测量宽度的完整代码
3.1 示例任务与图像准备
这里用一个非常典型的需求演示:检测某个矩形工件的宽度。工件左右各有两条竖直边,相机从上往下拍照,工件每次摆放有大约正负10度的旋转。我们需要在Halcon里建一个模板,然后在新的图像里匹配工件位置,对齐测量窗口,最终输出两条竖边之间的宽度。
实操前先准备两幅图:一幅“模板图”,要求工件位置端正、光照正常、没有遮挡;一幅或多幅“测试图”,模拟实际产线上的偏移和旋转。模板图用来框选ROI和创建模板,测试图用来验证匹配和测量效果。
读取图像并查看尺寸:
* 读取模板图像和测试图像 read_image (ImageModel, 'template.png') read_image (ImageTest, 'test_01.png') get_image_size (ImageModel, Width, Height) dev_open_window (0, 0, Width / 2, Height / 2, 'black', WindowHandle) dev_display (ImageModel)3.2 创建模板:框选ROI与参数设定
在模板图上,用draw_rectangle1框选出工件上特征最明显的区域作为模板ROI。这个区域不用框整个工件,但最好包含独特的转角、切边或孔特征,这样匹配鲁棒性更高。
* 交互式画一个矩形ROI draw_rectangle1 (WindowHandle, Row1, Column1, Row2, Column2) gen_rectangle1 (ModelROI, Row1, Column1, Row2, Column2) reduce_domain (ImageModel, ModelROI, ImageModelReduced) * 创建形状模板:允许正负15度旋转,对比度阈值30 create_shape_model (ImageModelReduced, 'auto', rad(-15), rad(15), 'auto', 'none', 'use_polarity', 30, 10, ModelID) get_shape_model_contours (ModelContours, ModelID, 1)创建完成后,get_shape_model_contours可以让我们看到模板的轮廓长什么样,直观判断模板是否提取到了太多无关碎边。如果轮廓很杂乱,回过去调大Contrast,或者把ROI缩小,只包含主要结构。
同时,建议在模板图上用set_shape_model_origin把模板原点设置到一个明确的特征点上,比如工件左下角。这样后面find_shape_model返回的行列坐标含义清晰,测量坐标转换也好理解。
3.3 匹配测试图:确认工件位置和角度
一张测试图进来后,默认状态是不知道工件在哪的,先用一定范围内的角度搜索:
* 在测试图像中搜索模板,要求最高得分,最小得分阈值0.6 find_shape_model (ImageTest, ModelID, rad(-15), rad(15), 0.6, 1, 0.5, 'least_squares', 0, 0.8, RowMatch, ColMatch, AngleMatch, ScoreMatch)如果ScoreMatch低于0.6,说明当前图像里可能没有目标,或者光照、遮挡变化太大。实际项目中这一句通常是根因排查的起点。为了调试方便,可以把ScoreMatch显示到窗口上,确认匹配质量。
这一步的花费时间一般只有几毫秒到十几毫秒,具体取决于图像分辨率、金字塔层数和搜索角度范围。
3.4 对齐模板与测量宽度
现在的核心动作:把模板坐标系里的测量窗口转换到当前图像里。早在模板图上,已经通过人工观察确定了两个竖直边缘的大概位置,比如左边边缘在Column=150左右,右边边缘在Column=450左右。测量窗口取两者中间,中心点设为(MeasureRow=200, MeasureCol=300),测量方向水平(角度0),Length1设为200(覆盖两条边),Length2设为30(竖直方向半宽)。这样在一个测量矩形内就能同时截住左右两条竖边。
在代码中完整实现:
* 模板坐标下的测量窗口参数 MeasureRow := 200 MeasureCol := 300 MeasureAngle := 0.0 * 将测量窗口中心变换到当前图像坐标系 vector_angle_to_rigid (0, 0, 0, RowMatch, ColMatch, AngleMatch, HomMat2D) affine_trans_point_2d (HomMat2D, MeasureRow, MeasureCol, CurRow, CurCol) * 当前测量角度 = 模板测量角度 + 匹配角 CurAngle := MeasureAngle + AngleMatch * 生成测量矩形并测量边缘对 gen_measure_rectangle2 (MeasureHandle, CurRow, CurCol, CurAngle, 200, 30, Width, Height, 'nearest_neighbor', MeasureHandle) measure_pairs (ImageTest, MeasureHandle, 2, 20, 'all', 'all', RowEdgeFirst, ColEdgeFirst, AmplitudeFirst, RowEdgeSecond, ColEdgeSecond, AmplitudeSecond, IntraDistance, InterDistance) close_measure (MeasureHandle)measure_pairs会返回一组或多组边缘对,每组包含第一条边缘和第二条边缘的坐标。本例中如果只有宽度两条边,IntraDistance就是像素宽度。因为工件可能旋转了10度,两条边缘在图像里不再是严格竖直,但测量矩形已经跟着旋转了同样的角度,所以扫描线依然垂直于边缘,测出来的IntraDistance就是真实宽度在像素上的投影。
如果要留出调试的余地,可以再加一段把找到的边缘显示出来:
disp_cross (WindowHandle, RowEdgeFirst, ColEdgeFirst, 12, 0) disp_cross (WindowHandle, RowEdgeSecond, ColEdgeSecond, 12, 0) set_tposition (WindowHandle, 20, 20) write_string (WindowHandle, 'Width(pixel): ' + IntraDistance)3.5 从像素到物理尺寸:标定或无标定的处理
测量边输出的是像素距离。要换算成毫米,最可靠的办法是提前用Halcon标定板做相机标定,得到像素当量ScaleX、ScaleY。如果没有标定条件,也可以用已知长度的物体粗算比例,比如知道这个工件标准宽度是10毫米,测出像素宽度是300像素,那比例就是每像素0.0333毫米。这种粗算方法在验证方案可行性时够用,但要是做正式量检具,一定不能省标定。畸变和透视的影响在视场边缘尤其明显,只用一个平均比例是撑不住的。
换算代码很简单:
ScaleX := 0.0333 * 单位:mm/pixel WidthMm := IntraDistance * ScaleX标定板的处理路径通常是:采集标定板图像 -> calibrate_cameras -> 得到相机内参和外参 -> 在测量时把像素坐标反投影到世界平面或测量平面上。Halcon里也常用vector_to_plane把图像坐标变换到物理平面,配合measure_pairs的输出坐标使用。
4. 常见问题与排查技巧实录
4.1 模板匹配分数低或直接找不到目标
得分低的常见原因有几个。一是光照外观变了,目标边缘从亮到暗的极性翻转了,模板建的时候是‘use_polarity’,如果反光比较严重,可以试‘ignore_global_polarity’模式,它对局部对比度反转更宽容,但误匹配的风险也会上升。二是模板ROI选得不好,特征太少,比如只框了一个光滑平面,边缘信息稀薄。作者的经验是ROI一定要框在包含转角、孔、异形边缘的区域,否则后续再调参也白搭。三是旋转范围参数和实际不匹配,比如实际工件歪了30度,匹配只设置了正负15度,那当然找不到。这时候先把AngleExtent扩到正负45度,确认能匹配上再逐步收紧。四是MinScore设得太高,可以先降到0.3看真实得分分布。
排查的时候不要靠猜,直接在调试窗口打印ScoreMatch,并对同一工件在不同位置下采集多张图,观察Score的波动范围。正常情况一个稳定的视觉方案,Score应该能稳定在0.8以上。
4.2 测量结果抖动明显、边缘点不稳定
测量抖动经常不是“测量算子”的错,而是测量矩形没有对齐。最典型的就是忘了在CurAngle里加模板测量角度,或者用了resize_gray、affine_trans_image这类重采样后图像导致边缘被插值抹平。另一个常见原因是Threshold设置太低,把边缘附近的反光、毛刺也当成了边缘。我调试时常把measure_pairs的输出边缘点和幅度显示出来,看一眼边缘是否都落在两条竖直边附近。如果边缘点七零八落,优先调大Sigma和Threshold。如果边缘点都在但振幅很小,说明对比度不够,要考虑打光或者换‘positive’/‘negative’限定方向。
测量区域较长时,还会出现边缘在测量窗口内“弯曲”的问题。矩形工件虽然整体是方形,但冲压件边缘可能有微小的弧度,measure_pairs沿竖直方向找边,每一条扫描线上的边缘位置会有一点点差异,最终IntraDistance等于这些差异的平均效果。这个差异如果稳定,其实不影响最终尺寸一致性判断;但如果忽大忽小,就要检查相机安装是否松动、图像是否有运动模糊。
4.3 多目标、多个测量区域时的扩展
实际产线很少只测一个位置。比如同一张图里要同时测左边宽度、右边间距、下边到基准边的距离。处理方式很简单:把所有测量窗口的中心坐标和角度都记为模板坐标系下的数组,for循环里逐个做affine_trans_point_2d转换,然后依次gen_measure_rectangle2、measure_pairs。这样写出来的代码扩展性很强,后面加检测项只是多添几个坐标数组的问题。需要注意的是每个测量窗口的Length1、Length2、Sigma、Threshold可能不同,不要写死在循环外面,用对应的参数数组或字典结构管理起来。
4.4 性能优化建议
模板匹配的计算量主要取决于搜索角度范围和金字塔层数。产线上如果角度偏移确实很小,就把AngleExtent收窄到正负5度,匹配速度会有肉眼可见的提升。还有一个被忽略的优化点是图片输入尺寸:很多项目从相机拿到的原图动辄3000x4000,直接全图搜索很慢。可以用reduce_domain配合一个预先设定的搜索区域,比如只在图像中间三分之一范围搜索,这样在不损失精度的情况下大幅减少计算量。测量阶段本身开销很小,真正吃时间的仍然在匹配环节。Greediness从0.7调高到0.9也能进一步加速,但要在样本测试里确认没有漏匹配的风险。
5. 经验总结与方案扩展方向
整套流程跑通之后,你会发现它不只是“一个例子”,而是一个可以反复复用的套路。碰到新材料、新工件,只要这个工件有稳定的边缘轮廓,先在模板图里框好ROI、建模板,再在模板坐标里摆好测量窗口,匹配、对齐、测量三步走,几乎都能顺下来。我在实际项目中经常是在HDevelop里调试完这套流程,然后导出成C#或C++代码,嵌入到上位机软件里。算子在库里的调用方式与HDevelop一致,只是语法换成对应编程语言,因此调试结论可以无损迁移到正式系统。
后续如果要继续扩展,可以考虑的方向有:一是从单一模板升级成多模板切换,适应一个产线上多种型号的工件,匹配前根据产品标识选择对应ModelID;二是把测量宽度升级为测量两个孔中心距,这时加一步用模板匹配定位孔,再用二维卡尺或XLD拟合求圆心,流程本质还是匹配加测量;三是加上通讯逻辑,把测量结果按照格式发送到PLC或MES系统,完成从“看得准”到“产线能用”的最后一步。
我自己在调试这类视觉方案时最深的体会是:模板匹配的调参不要贪多,不要同时把五个参数一起改。每改一个参数,固定其他参数,跑一组测试图,对比得分和测量偏差。这样一轮轮收敛,问题很快就能定位。测量值偶尔抖动时先怀疑对齐,再怀疑参数,最后才怀疑算法本身。
最后分享一个实战小技巧:正式测量前,先用模板匹配结果算一遍“工件中心与图像中心”的偏差值。如果偏差在几个像素以内,而且角度基本为0,说明机构定位本身很好,这时候可以适当放低模板匹配的MinScore;如果偏差很大且角度也大,再强的视觉算法也救不了机械装夹的先天不足。先看定位稳定性,再谈算法精度,这是视觉项目里少走弯路的关键。