做刹车盘视觉检测的同行应该都清楚,孔位检测在每个机加工产线上几乎都是标配。一批刹车盘毛坯或半成品上来,要看的孔包括中心孔、安装孔、散热孔,形状不全是圆孔,还有腰形孔和沉头异形孔。Halcon 的 shape matching 在这类场景里一直是最稳的主力方案。这篇文章把“刹车盘孔洞形状匹配”从模板制作、匹配调用到参数调优、故障排查整个链路都拆开讲,适合刚接触 Halcon 的视觉工程师,也适合已经在产线上跑模板但效果不稳定的朋友对照排查。
文章会以一个常见刹车盘型号的6个安装孔定位项目为例,模板孔取其中一个边缘特征最明显的腰形安装孔,检测目标是每张图中找到全部6个孔的位置和角度。实际项目里,这几个孔的位置误差通常要求控制在0.1毫米以内,角度误差在1度以内,节拍按秒计算。走近以后你会发现,形状匹配本身不是难点,难点在参数权衡和现场干扰的应对上。
1. 这个示例到底在解决什么问题
1.1 刹车盘孔洞检测的现场痛点
刹车盘零件不像手机屏那么“干净”,它从铸造、机加工到成品检测,每一个环节的来料状态都不一样。同一个孔洞在不同光线下呈现的对比度差异非常大;盘面上会有油污、铁屑、防锈油残留;光源打上去还会出现强反光。这些干扰对阈值分割类方法极其不友好,但形状匹配因为依赖的是边缘梯度方向信息,对灰度整体偏移并不敏感,所以能抗住不少现场光照波动。
另一个痛点是孔型复杂。圆孔用 Blob 分析能解决,但腰形孔、沉头孔、带倒角的孔洞,不仅要找到位置,还要拿到旋转角度。很多刹车盘的散热孔是异形孔,形状不规则,方向特征明显,这时就需要一个带方向信息的定位算法。形状匹配天然输出行列坐标和角度,正好匹配这种需求。
还有一个容易被忽略的点:模板要覆盖实际产线状态。现场工程师经常犯一个错——用实验室里最完美的零件做模板,一上线全灭。刹车盘孔洞的倒角大小、刀具磨损带来的边缘毛刺,都会让模板和实际图之间出现偏差。好的做法是模板图和线上图从同一批次、同一工位采集,先把边缘提取结果摆在一起看,确认边缘走势一致再谈后面的匹配。
1.2 为什么用形状匹配而不是普通Blob
如果任务只是统计几个圆形孔的个数和圆心,单纯用阈值分割加 Blob 分析就够了。但刹车盘孔洞检测往往有两个问题卡住 Blob:一是孔内往往有铁屑不完全遮住,圆度被破坏;二是腰形孔、异形孔需要角度,Blob 算出的相当于最小外接矩形方向,误差很大,根本没法用于导向或装配。形状匹配直接对模板轮廓和图像中的梯度特征做匹配,返回的 Angle 就是孔洞当前的旋转角,这个优势是 Blob 无法替代的。
从速度上说,shape matching 在 Halcon 里做了金字塔分层和边缘点稀疏化,几十毫秒内可以完成全图搜索。多孔同时查找时,只需要把 NumMatches 设成目标数量,再配合 MaxOverlap 解决相邻孔之间的重叠抑制。相比之下,如果要逐个孔做 Blob 分析,还得先完成区域分割和连通域编号,代码量更大,鲁棒性却更差。
当然形状匹配也不是没有前提。它要求被检测物体有足够的纹理或边缘对比度。刹车盘孔洞是加工面,边缘对比度充足,恰好满足条件。如果孔洞经过黑色涂层,对比度被压得很低,又要另当别论,那种情况通常要先做增强或者直接改变打光方案,而不是在匹配参数里硬调。
2. 形状匹配的核心原理与参数设计
2.1 匹配到底在“匹配”什么
Halcon 的形状匹配,匹配的不是整幅图像的灰度,而是模板区域里那些梯度变化剧烈的位置点,也就是边缘点的集合。每个边缘点都有方向和幅度信息,模板图和搜索图在同一尺度下比对时,算法计算这些边缘点与搜索图中边缘位置的吻合程度,输出一个0到1之间的 Score。这个分数可以简单理解为“模板轮廓在多大比例上与当前目标对齐”。
这个过程和人在一堆零件里找一个特定形状的孔洞很像:你不会去对比整块面的颜色,而是先扫一眼轮廓,认出几个关键拐点。形状匹配就是把这件事交给机器。它对灰度变化、整体亮度偏移不敏感,但对边缘形状和方向敏感,所以非常适合金属零件上的孔位定位。
金字塔是这个算法速度的关键。创建模板时,Halcon 会把模板的精细度不断降低,生成一个多层图像金字塔。搜索时先在最粗糙的层上找到候选位置,再逐层向精细层精化。层数越多,初始搜索越快,但对极小的孔洞或特征太少的模板可能不友好。参数 NumLevels 就是这个层数,工程上直接写 'auto' 让软件按模板尺寸自动决定,大多数情况下比手填要稳。
2.2 模板创建时几个参数背后的道理
先看 AngleStart 和 AngleExtent。对刹车盘安装孔来说,孔在圆周上随着整个盘安装时的姿态会旋转,但旋转范围通常不会超过正负几十度;如果设成0到360度全角度搜索,速度和可靠性都吃亏。经验上先按机械公差扩大一点余量,比如正负25度,对应到代码里就是 AngleStart=-0.4365,AngleExtent=0.873,单位是弧度,别随手就设个 rad(360) 进去。
AngleStep 是旋转搜索的步进,默认 'auto' 会根据金字塔层数自动算。手动设置时要注意:步进太大容易漏掉真实角度,太小则搜索角度数剧增、耗时成倍上升。在 Halcon 里,角度范围拆成多少个离散步进来近似,最后匹配输出还能通过亚像素优化得到连续角度,所以自动计算通常够用,不建议手动压得太细。
Contrast 与 MinContrast 决定了哪些边缘点进入模板以及匹配时边缘响应的阈值。Contrast 太大会把孔边缘上的一些弱梯度丢掉,模板看起来“少了一块”;太小又会把反光毛刺等噪声也当成特征。用边缘XLD建模板时,可以直接用 select_shape_xld 先按轮廓长度筛一遍,把太短的噪声轮廓去掉,再交给 create_shape_model_xld。
2.3 模板形态:灰度图还是边缘轮廓
Halcon 创建形状模型主要有两个入口:create_shape_model 用于从灰度图直接建模板,create_shape_model_region 用区域建模板,create_shape_model_xld 用XLD轮廓建模板。实际项目里怎么选,我个人的经验是这样的:如果孔洞边缘和周围背景对比度极好,且边缘完整闭合,用灰度图或区域建模板都行;如果孔洞被倒角影响,边缘不是一个闭合圆,而是一些断断续续的弧段,用 XLD 轮廓建模板更直接,因为可以在建模板前先做轮廓筛选,把没有意义的短线段过滤掉。
本文示例里的腰形安装孔就属于边缘完整但内部有阶梯面的情况。直接用整片灰度区域建模板时,孔内的高光台阶会被当成额外特征,模板变得复杂,匹配速度变慢还容易误检。XLD 方式先把边长轮廓提取出来,只保留孔洞真正的边界线段,模板特征干净,匹配更稳定。这个差别在孔内部有高光的刹车盘上非常明显。
3. 完整代码实现与逐步讲解
3.1 离线模板制作阶段
模板制作只做一次,可以独立写成一个 Script,保存成 .shm 文件放到产线程序的固定目录。以下代码围绕一个腰形安装孔做模板,注意这里没有引入任何标定,先以像素坐标为讨论基础。
* 模板图读取,图像要选择光照正常、孔洞完整的一张 read_image (Image, 'brake_disc_template.png') get_image_size (Image, Width, Height) dev_open_window (0, 0, Width / 2, Height / 2, 'black', WindowHandle) dev_display (Image) * 在图像中框选包含目标孔的最小矩形 draw_rectangle1 (WindowHandle, Row1, Column1, Row2, Column2) gen_rectangle1 (ROI, Row1, Column1, Row2, Column2) reduce_domain (Image, ROI, ImageReduced) * 提取孔洞边缘,注意卡好Canny两阈值 edges_sub_pix (ImageReduced, Edges, 'canny', 1.5, 20, 40) segment_contours_xld (Edges, ContoursSplit, 'lines_circles', 5, 4, 2) select_shape_xld (ContoursSplit, SelectedContours, 'contlength', 'and', 30, 100000) * 基于筛选后的轮廓创建形状模型 create_shape_model_xld (SelectedContours, 'auto', -0.4365, 0.873, 'auto', 'no_pregeneration', 'no_pregeneration', 1, ModelID) get_shape_model_contours (ModelContours, ModelID, 1) * 保存模板,供在线程序调用 write_shape_model (ModelID, 'brake_hole.shm')这一步有四个容易出问题的地方。第一,ROI 不要贪大,孔洞边界要完整,但四周不要包太多相邻孔的边,否则模板里其他孔特征混乱。第二,Canny 的阈值20, 40只是针对这个示例的初始值,实际要根据模板图的边缘响应来回试,原则是“主要孔边连续、噪声线段短而少”。第三,select_shape_xld 里30这个最小长度要大于刹车盘高光噪声线段长度,我通常拿掉这个限制跑一遍,统计所有线段长度再设一个分位值。第四,保存模板后要再用get_shape_model_contours检查一次轮廓,确认没有把孔内的台阶边缘和刀纹当特征。
3.2 在线匹配与多孔位筛选
在线阶段读入搜索图,直接调用匹配。搜索图里一张盘有6个安装孔,因此 NumMatches 设为6。这里 MinScore 取0.6是一个平衡点,后面章节会详细说为什么不是0.9。
read_image (SearchImage, 'brake_disc_scene.png') find_shape_model (SearchImage, ModelID, -0.4365, 0.873, 0.6, 6, 0.5, 'least_squares', 0.8, Row, Column, Angle, Score)执行完以后,Row、Column、Angle、Score 是四个元组,长度等于实际找到的匹配个数。正常情况和预期一致是6个,但千万不要假设它一定是6个。工业程序里要先判断|Row|是否等于6,不等于就走 NG 分支,这种硬校验比事后看分数更可靠。
匹配出来的孔位顺序是随机的,如果后道工序需要按固定编号写入孔位坐标,一定要做排序。常用的办法是用tuple_sort_index按 Row 或 Column 排序,再结合孔在图像中的物理分布规律,给每个孔编上固定序号。顺序一旦乱,后面机器人抓取的对应关系就全错,这个坑在产线上比匹配分数不够还隐蔽。
3.3 结果投影显示与坐标输出
匹配返回的只是位置参数,要用模板轮廓把结果画出来,需要先把 ModelContours 通过刚体变换投影到当前图像坐标系。Halcon 的做法是先构造一个平移加旋转的矩阵,再对轮廓做仿射变换。注意旋转算子hom_mat2d_rotate的方向和匹配输出 Angle 的符号关系,这里必须传-Angle,否则画出来的轮廓会以错误角度叠加到孔上,一眼就能看出对不上。
dev_display (SearchImage) get_shape_model_contours (ModelContours, ModelID, 1) for I := 0 to |Row| - 1 by 1 hom_mat2d_identity (HomMat2D) hom_mat2d_translate (HomMat2D, Column[I], Row[I], HomMat2DTranslate) hom_mat2d_rotate (HomMat2DTranslate, -Angle[I], Column[I], Row[I], HomMat2DTrans) affine_trans_contour_xld (ModelContours, ContoursAffine, HomMat2DTrans) dev_set_color ('green') dev_set_line_width (2) dev_display (ContoursAffine) endfor如果后续要接机器人或测量程序,需要把像素坐标转成世界坐标。这一步需要预先完成相机标定,用image_points_to_world_plane或者标定好的映射关系把 Row、Column 转换过去。角度值的转换也别忘了考虑坐标系方向,我在现场看到过像素坐标对、机械手角度方向反的案例,调了半天最后发现是影像坐标系和机械手坐标系差了180度。
4. 参数调试方法与现场经验
4.1 六个关键参数的速查表
把匹配相关的核心参数集中列成一张表,方便调试时对照。表里的经验值基于一个前提:刹车盘孔洞对比度正常、现场光源色温稳定。如果打光条件差,数字要整体往保守方向调。
| 参数 | 作用 | 推荐区间 | 本文初始值 | 调整方向 |
|---|---|---|---|---|
| NumLevels | 金字塔层数 | 'auto' 或 4-8 | 'auto' | 模板太小/漏检时降低 |
| AngleStart | 起始搜索角 | 依据机械公差 | -0.4365 | 不要盲目用0 |
| AngleExtent | 角度范围 | 依据机械公差 | 0.873 | 范围越大耗时越高 |
| MinScore | 最低评分 | 0.5-0.8 | 0.6 | 误检多时抬高,漏检多时压低 |
| MaxOverlap | 最大重叠率 | 0.0-0.6 | 0.5 | 相邻孔太密时调低 |
| Greediness | 搜索贪婪度 | 0.5-0.9 | 0.8 | 速度慢时提高,误检明显时降低 |
| SubPixel | 亚像素精度 | least_squares 系 | least_squares | 追求极致精度换 least_squares_high |
调试顺序建议从 AngleStart 和 AngleExtent 开始,先把搜索角度范围锁死;再调 MinScore 和 Greediness 这一对速度和可靠性组合;最后看 MaxOverlap 处理多孔场景。不要一上来就动 Greediness,那个数字对最终精度影响很小,对耗时影响却很大。
4.2 我调参踩过的坑
先说我踩得最狠的一个坑:某个型号模板创建时没限定角度,直接用 rad(-180) 到 rad(180),线上 MinScore 设到0.55。结果一天之内出现几十次误匹配,故障盘孔位画在了完全错误的边缘上。后来排查发现,全角度搜索让模板在热量孔和结构筋的边缘上也找到了高分候选位置。把 AngleExtent 从360度压到45度后,误检直接归零。角度范围不是越宽越保险,它反而会给匹配算法更大的犯错空间。
第二个坑是 NumMatches 设成0(匹配所有候选)配合 MaxOverlap 设成0。在孔洞密集的散热区,同一个孔的边缘被算到两个不同角度位置,产生一对相邻候选,Score 都挺高。程序按前6个结果输出坐标,其中一个孔重复出现两次,真正位置缺损,后道引导直接偏掉。后来强制 MaxOverlap=0.3,并且在对结果排序前先做位置去重,问题才消失。
第三个坑是边缘长度阈值设得太死。实验室模板图边缘质量好,select_shape_xld 里的最小轮廓长度设得很大也没问题,可线上图因为刀具磨损,孔边缘出现连续细碎毛刺,真实边缘被分段成多条小线段,单段长度都低于阈值,结果建出来的模板几乎只有半圈边缘,匹配分数永远上不去。现在我的做法是阈值留30%余量,宁多勿缺,反正后面还有轮廓筛选可以把多余的噪声在开匹配前去掉。
4.3 一个实用的多孔位筛选技巧
当一张盘上有多个同类孔且分布呈圆周排列时,单纯调 MaxOverlap 还不够。我会在匹配结果里加大尺度几何校验:先算出6个孔的质心(平均 Row 和 Column),然后逐个计算每个孔中心到质心的距离,以及每个孔相对于质心的角度。把计算出来的角度按顺序排序,要求相邻角度差近似60度,距离差控制在合理范围内。任何一项异常直接 NG。
这个办法听着土,但非常管用。形状匹配偶尔会因为毛刺或反光产生一个离群候选,分数接近正常孔;如果只看 Score 和个数,很难抓出来。加一个几何分布校验,单个孔的位置错误会在角度序列里暴露得很明显。类似的校验逻辑也可以扩展到矩形排布、圆弧排布等场景,只要旧工艺有规律,都能用几何约束增强稳定性。
5. 常见问题排查与落地建议
5.1 误匹配:分数高但位置明显不对
误匹配的典型表现是 Score 值很高,但画出来的轮廓根本没套在孔上。常见原因有三个:模板选择了孔内高光边缘导致特征雷同;AngleExtent 过大让模板在多个相似位置都能命中;模板 ROI 里包含相邻孔的一部分。排查时先把模板轮廓单独显示,看看特征是不是干净;然后把匹配角度范围压缩到真实需要;最后检查 ROI。不要一上来就提高 MinScore,那样会把真正的孔也漏掉。
还有一种误匹配来自对称形状。如果模板孔本身是中心对称的,匹配时180度翻转也能得到高分,输出 Angle 会跳变180度。处理办法是在匹配后对 Angle 做范围校验,超过机械允许范围就判定为错误候选。对称性问题在腰形孔上不严重,但圆形沉头孔经常遇到,加了角度范围校验就好很多。
5.2 漏检:该找到的孔没有出来
漏检最常见的原因是模板特征弱化:线上图的边缘对比度比模板图低,MinScore 又设高了,真实孔的计算分数低于阈值。这时先看 Score 输出,如果真实孔分数偏低,调低 MinScore 到0.4左右看能不能找回。另一个原因是搜索角度范围没盖住实际偏转角度,孔已经转出了 AngleStart 和 AngleExtent 的范围,分数骤降。
光照波动导致的漏检还有一个隐蔽因素:MinContrast。如果模板创建时设置了较高的 MinContrast,而线上图边缘较弱,匹配阶段会认为边缘点不足。可以把模板创建里的 MinContrast 调低,或者在前处理里加一个边缘增强,比如用 illuminate 或 shock_filter 把孔边缘打出来。注意前处理要同时用在模板图和在线图上,模板处理的思路要和在线一致。
5.3 定位精度不够:重复性差
如果匹配每次位置都有零点几个像素的抖动,先确认相机没有震动,光源也算稳定。如果都正常,把 SubPixel 从 'none' 或 'least_squares' 换成 'least_squares_high',一般压力不大时精度还能再提升一截。模板金字塔层数也对精度有影响,如果最终精化层太粗,亚像素优化没有足够的细节依据。
另一个经常被忽略的点是 ROI 内不要包含过多背景边缘。模板特征越“杂”,匹配时的主峰就越不尖锐,评分平坦区变宽,亚像素定位就会在平坦区里漂移。把 ROI 收紧到只包围孔洞一周的边,匹配主峰通常会尖锐很多。现场自检精度时,可以用同一个孔重复匹配50次,统计 Row 和 Column 的标准差,目标是不超过0.05像素量级,换算成物理尺寸满足公差。
5.4 速度优化:节拍不够时的三个方向
节拍不够时先不要急着换算法。第一,裁剪搜索区域,把 find_shape_model 的输入图先用之前定位到的圆心区域裁出来,搜索面积小了,耗时直接按比例下降。第二,调节 Greediness 从0.7到0.9,搜索时更早剪枝,多数场景肉眼看不出稳定性变化。第三,适当降低金字塔层数有时反而快,因为模板层数过多时,最粗层只留下很少的边点,粗层候选无法区分真假,返回来做很多次确认。匹配时间要用真实图像统计,不要只看单幅图。
如果这三种手段都压不住耗时,再考虑把模板拆细。孔洞检测场景多半不是全场无规则搜索,先定位盘面圆心和大致半径,然后只在环形区域里跑匹配,能省掉大量无效区域。这个思路在很多视觉项目里通用,不限于刹车盘。
5.5 给准备落地这个示例的同行几句经验
最后分享一点个人体会:模板图和生产图最好采自同一工位、同一光源,不要用做好的标称图。我在工厂里见过很多项目死在“模板太完美”上。真实工件有加工公差,孔边缘的倒角大小每一次都可能不一样,模板要包容这种正常波动,而不是追求绝对精确。
调试匹配参数前,先把打光做稳。反光、油污、切削液这些现场问题,靠调 MinScore 和 Greediness 是压不住的,只会把参数越调越激进,最后十分脆弱。每次改动参数后,至少拿50张不同状态的现场图回归测试,不要用5张图就下结论。把实测数据和参数版本记录下来,后面出问题才能快速定位。形状匹配用好了,就是一个很省心的工具,值得花时间把参数细节吃透。