
产线上一块 30mm 见方的冲压件表面带着冲压油和氧化斑纹要求 200ms 内测出长宽。我最早用的是阈值分割加最小外接矩形的方案结果上午和下午测出来的尺寸能差出 0.1mm根源就是边缘毛刺和反光让二值化区域每天都在变。后来换成 HALCON 的 2D 测量模型用add_metrology_object_rectangle2_measure()在矩形四条边上铺卡尺每个卡尺只负责自己眼前那一小段边缘再由所有卡尺找到的边缘点统一拟合成矩形稳定性立刻上了一个台阶。这篇就把这个算子的参数含义、内部原理、实操代码和我在现场踩过的坑完整梳理一遍适合已经跑通过 HALCON 基础例程、正准备做精密尺寸测量的工程师参考。1. 从一片五金件说起为什么测量矩形要交给 Metrology 模型1.1 传统阈值分割的边界困境冲压件的边缘并不是一条干净的直线冲压油会形成反光带氧化斑纹会让局部灰度接近背景模具磨损还会带出细小的毛刺。用阈值分割提取工件区域时这些干扰会直接变成区域边界上的锯齿。对区域做最小外接矩形本质上是在用一堆锯齿边界去拟合一个理想矩形毛刺每变化一点拟合结果就跟着抖一次。我测过一块表面有轻微拉丝纹的零件同一张图换一个阈值矩形宽度能差出 0.05mm。这在微米级测量需求面前完全不可接受。后来我试过形态学开闭运算去磨平毛刺毛刺确实少了但边缘位置也被打磨掉了十几微米尺寸失真同样致命。阈值分割适合找区域、做有无判断不适合做高精度边界定位。1.2 卡尺思路把找边变成沿线扫边Metrology 模型的做法和人工测量非常像一个人拿游标卡尺去量矩形不会贴着整条边滑过去而是在边上选几个点分别测量每个点到对边的距离。add_metrology_object_rectangle2_measure()就是这个思路的程序化版本。它在矩形每条边上自动生成一串测量卡尺每个卡尺只覆盖边上的一小段区域。卡尺内部沿着垂直边缘的方向做一维灰度扫描找到灰度发生跳跃的位置记录为亚像素边缘点。最后模型把所有卡尺找到的边缘点放在一起用最小二乘拟合出一个矩形。因为每个卡尺只看局部氧化斑、反光点、毛刺只会影响个别卡尺不会像全局阈值分割那样把整个边界带偏。1.3 带 measure 和不带 measure 的版本怎么选HALCON 里还有一个add_metrology_object_rectangle2()算子它只登记矩形几何对象不自动生成卡尺你需要用其他通用算子手动往对象上挂测量工具。而带_measure后缀的版本会在添加矩形的同时把卡尺也铺好省掉大量手工编码。实际项目里除非有非常特殊的卡尺排布需求否则直接选用带_measure的版本。一个 Metrology 模型里可以同时添加多个矩形、圆、椭圆、直线对象每个对象有独立的 Index。一次apply_metrology_model()会遍历模型里所有对象完成测量统一读取结果。这对一个视野里要测多个工件的场景特别方便模型内部会自己管理卡尺和拟合过程。2. 矩形对象与卡尺add_metrology_object_rectangle2_measure 的参数地图2.1 先说清两个 Length 组很多初学者第一次看到这个算子会被参数吓到Row、Column、Phi、Length1、Length2、MeasureLength1、MeasureLength2三个一组一共两组。其实这两组参数的含义是一一对应的。第一组是矩形本身的几何参数Row 和 Column矩形中心的像素坐标。Phi矩形方向角单位是弧度决定矩形绕中心旋转的角度。Length1矩形沿 Phi 方向从中心到边缘的半长也就是长边的一半。Length2矩形垂直 Phi 方向从中心到边缘的半宽也就是短边的一半。第二组是测量卡尺的参数MeasureLength1每个卡尺沿矩形边方向的半长度也就是卡尺的长轴。MeasureLength2每个卡尺垂直矩形边方向的半宽度也就是卡尺沿边缘法线的扫描范围。这里最经典的错误是把 Length1 和 Length2 传反或者把 MeasureLength1 和 MeasureLength2 传反。Length1 和 Length2 反了矩形会旋转 90 度拟合最后得到的长度宽度完全对调MeasureLength1 和 MeasureLength2 反了卡尺就会横在边上而不是站在边上扫描方向变成沿着边缘走根本找不到跨越边缘的灰度变化。2.2 几何参数、测量参数和通用参数一览下面是这个算子完整的参数清单按实际调用顺序排列方便照着抄参数名含义类型说明MetrologyHandle计量模型句柄HTuple由create_metrology_model创建Row矩形中心行坐标HTuple像素坐标Column矩形中心列坐标HTuple像素坐标Phi矩形方向角HTuple弧度通常范围在 -pi/2 到 pi/2Length1矩形半长HTuple沿 Phi 方向Length2矩形半宽HTuple垂直 Phi 方向MeasureLength1卡尺沿边方向半长HTuple越大单个卡尺覆盖的边越长MeasureLength2卡尺沿法线方向半宽HTuple决定找边时的扫描范围MeasureSigma高斯平滑系数HTuple用于抑制噪声通常 0.4 到 5.0MeasureThreshold边缘幅度阈值HTuple灰度跳跃幅度低于此值的边缘不采纳GenParamName通用参数名HTuple如measure_transition、measure_selectGenParamValue通用参数值HTuple与GenParamName一一对应Index返回的对象索引HTuple后续用set_metrology_object_param改参数时用通用参数不一定要在添加对象时全部填完可以先用空数组[]、[]添加后面用set_metrology_object_param()按需修改。我习惯先在添加时只给基础参数然后单独设置极性和边缘选择策略每一行代码对应一个参数出问题时排查更直接。2.3 Phi 的角度细节不能靠猜Phi 的方向在 HALCON 里是矩形长边方向与图像列方向之间的夹角逆时针为正单位是弧度。文字描述很容易被坐标系绕晕我的建议是不要凭空推算添加完对象后立刻用get_metrology_object_model_contour()把模型轮廓取出来叠在原图上显示一次亲眼确认矩形方向和位置是否符合预期。HDevelop 里角度常用rad(30)这种写法注意是弧度不是度。如果从其他系统读到的角度是度要乘上 pi 除以 180 换算。还有一个容易忽略的问题同一个矩形用 30 度和用 -150 度描述是同一个方向但 HALCON 在输出拟合结果时会把角度归一化到某个范围。如果你后续要拿测量值和 PLC 对数据角度域不一致会造成莫名其妙的的跳变最好在读取结果后统一做一次归一化处理。3. 卡尺内部到底在算什么一维投影与亚像素边缘定位3.1 从二维图像到一维灰度轮廓要理解这个算子为什么准得看卡尺内部的工作原理。每个卡尺本质上是一个小矩形 ROIROI 的长轴沿着目标边缘方向短轴沿着边缘法线方向。卡尺工作时会沿着短轴方向逐行采样把二维区域内的灰度值在长轴方向上做平均或加权平均压缩成一条一维灰度轮廓。这条轮廓上边缘就表现为灰度值从低到高或从高到低的跳变。跳变发生的位置就是卡尺认为的边缘点。因为做了局部平均随机噪声会被抑制单个像素的异常灰度不会对边缘定位造成太大影响这是卡尺比直接逐像素找边缘更稳的原因。3.2 sigma 和 threshold 在边缘提取中的角色MeasureSigma控制轮廓计算前的平滑程度。sigma 越大轮廓越平滑对噪声越不敏感但代价是真实边缘的位置也可能被平滑掉一点尤其对锐利边缘过大的 sigma 会把边缘抹圆。sigma 越小轮廓保留细节越多但对毛刺和噪声也越敏感。MeasureThreshold决定什么样的灰度跳变才算边缘。轮廓的一阶导数会形成一个峰峰值高于 threshold 才会被记录为边缘。threshold 设得太低反光、阴影造成的伪边缘会被当成真边缘设得太高真实边缘可能因为对比度不足被漏掉。生产环境里光照变化大threshold 要留出足够余量不能卡在边缘幅度的临界值上。3.3 为什么能做到亚像素精度找到灰度跳变的大致位置后算法会在边缘点附近对灰度轮廓做插值或拟合计算出更精确的边缘位置。最常用的是在导数极值附近用抛物线拟合把极值点定位到亚像素级别。配合标定整套测量可以达到 1/10 到 1/50 像素的重复精度。这也是 Metrology 模型比普通阈值分割高一个维度的原因阈值分割的边缘定位精度受像素网格限制而卡尺的亚像素定位突破了像素分辨率。一块 500 万像素相机拍的视野一个像素对应 0.05mm像素级测量极限只有 0.05mm亚像素测量却能稳定到 0.005mm 量级。4. 从模型到结果最小可运行代码与调试可视化4.1 创建模型与添加矩形对象下面是一段可以直接在 HDevelop 里跑通的示例代码以一张包含矩形的图像为例* 读取图像并获取尺寸 read_image (Image, rectangle_part.png) get_image_size (Image, Width, Height) * 创建计量模型并告知图像尺寸 create_metrology_model (MetrologyHandle) set_metrology_model_image_size (MetrologyHandle, Width, Height) * 设定矩形几何参数 Row : 300 Column : 400 Phi : rad(30) Length1 : 100 Length2 : 50 * 设定卡尺参数 MeasureLength1 : 20 MeasureLength2 : 10 MeasureSigma : 1.0 MeasureThreshold : 30 * 添加矩形测量对象 add_metrology_object_rectangle2_measure (MetrologyHandle, Row, Column, Phi, Length1, Length2, \ MeasureLength1, MeasureLength2, MeasureSigma, MeasureThreshold, [], [], RectIndex)set_metrology_model_image_size这行经常被人忽略。如果模型不知道图像尺寸某些依赖图像边界的内部判断会失效建议每次创建模型后都设置一次成本极低收益却很实在。添加对象时如果对矩形初始位置没有把握可以先直接把工件二值化后求一个粗略的最小外接矩形把它的 Row、Column、Phi、Length1、Length2 作为初始值传给算子然后再用测量结果去修正。这样能减少卡尺偏离真实边缘导致拟合失败的次数。4.2 应用模型并读取拟合结果对象添加完成后真正执行测量只需要一个算子* 应用模型在当前图像上执行测量 apply_metrology_model (MetrologyHandle, Image) * 读取拟合后的矩形参数 get_metrology_object_result (MetrologyHandle, RectIndex, all, result_type, all_param, Parameter)Parameter返回的是包含五个元素的数组依次是拟合矩形的中心 Row、中心 Column、方向角 Phi、半长 Length1、半宽 Length2。比如* 假设测量结果 * Parameter : [301.234, 398.765, 0.5241, 99.862, 49.914]注意这里返回的半长半宽已经经过卡尺边缘点拟合和添加对象时给的初始值大概率有微小差异。如果初始值偏差过大说明矩形初始位置离真实边缘太远卡尺没有在正确位置找到边缘。矩形实际长度 2 * 半长宽度 2 * 半宽乘上像素标定系数就能换算成物理尺寸。4.3 把卡尺画出来永远先看可视化我调试任何测量模型第一件事永远是把卡尺和边缘点叠加到原图上肉眼确认卡尺方向、位置、覆盖范围是否合理。用下面两个算子就能拿到可视化数据* 获取测量卡尺轮廓和实际找到的边缘点 get_metrology_object_measures (Contours, MetrologyHandle, RectIndex, all, RowEdge, ColumnEdge, Amplitude) * 显示卡尺轮廓 dev_display (Image) dev_display (Contours) * 显示边缘点十字 disp_cross (WindowHandle, RowEdge, ColumnEdge, 6, 0)这一步能看出很多问题卡尺是否垂直于边缘边缘点是否全部贴合矩形边有没有卡尺找不到边缘导致点缺失有没有某些卡尺把边上的一条划痕当成了边缘点。边缘点的Amplitude数组里存的是每个点的边缘幅度如果某些点的幅度明显偏低说明那个位置的对比度不足后续可以针对性地调 sigma 或 threshold。5. 踩坑清单方向、极性、干扰与参数联动5.1 极性设置错误会让边缘点全部跑到反方向measure_transition控制卡尺寻找边缘时的极性可选值有positive、negative、uniform、ignore。positive表示找由暗到亮的边缘negative表示找由亮到暗的边缘uniform表示两种极性都接受。很多初学者第一次遇到的问题是明明矩形位置全对拟合出来却比实际物体大了一圈或小了一圈往往就是极性反了导致每个卡尺找到的其实是边缘外侧或内侧的干扰点。我的经验是如果工件比背景亮边缘是亮到暗用negative工件比背景暗边缘是暗到亮用positive。如果一开始不确定可以用uniform先跑一遍观察边缘点到底落在哪一侧确认后再改成精确极性。这个参数可以在添加对象时通过通用参数传入add_metrology_object_rectangle2_measure (MetrologyHandle, Row, Column, Phi, Length1, Length2, \ MeasureLength1, MeasureLength2, MeasureSigma, MeasureThreshold, \ [measure_transition], [negative], RectIndex)也可以后面单独设置set_metrology_object_param (MetrologyHandle, RectIndex, measure_transition, negative)5.2 干扰边缘比目标边缘更强时用 measure_select 控制目标边缘附近常常有其他灰度跳变工件的倒角、边上的字符、背景里的另一条线。卡尺默认会按照某种策略从这些边缘里选一个如果选错了拟合出的矩形就被带偏。measure_select提供几个选择策略first表示取距离卡尺起点最近的边缘last表示取最远的边缘all表示把所有边缘都返回first_last等变体会组合选择。具体用哪个取决于目标边缘在卡尺扫描范围内的位置。我的做法是先设成all把所有检测到的边缘点连同 Amplitude 一起打出来看看目标边缘在卡尺扫描方向上是第几个、幅度排第几再决定用first还是last或者配合min_score按幅度阈值过滤。all不是最终方案但它是诊断问题的最高效手段。5.3 卡尺数量不足或不均拟合结果会飘Metrology 模型在矩形每条边上生成卡尺的数量不是固定的它根据矩形边长和 MeasureLength1 的比值自动计算。如果矩形长边有 200 像素MeasureLength1 设为 10那么这条边上可能会生成十几个卡尺如果把 MeasureLength1 设成 50卡尺数量会明显减少。卡尺太少边缘点不足最小二乘拟合的矩形容易被单个噪声点牵动卡尺太多计算量上升实时性受影响但拟合结果会更稳健。生产环境里我一般保证长边上至少有 10 个以上有效边缘点少于 8 个就要考虑缩短 MeasureLength1 或者改用更大分辨率的相机。HALCON 也允许强制指定卡尺数量通过set_metrology_object_param设置num_measures。这个参数可以传一个数组分别控制四条边上的卡尺数量但数组顺序对应四条边的具体关系在不同版本里略有差异强烈建议设置后立刻用可视化确认数量是否落在目标边上。5.4 只测其中几条边时用 measure_edges 控制实际测量中有一种需求是只关心矩形的相对两边比如只测宽度不测长度。这时可以让卡尺只出现在目标边上既提高速度也避免短边边缘干扰长边的拟合。measure_edges参数允许指定要测量的边比如[left, right]或者[top, bottom]。这里要注意left、right、top、bottom 是相对于矩形自身坐标系定义的不是图像坐标系的上下左右。矩形旋转 90 度后原本的 left 边可能出现在图像右侧。设置后同样要可视化确认是哪两条边被选中。5.5 一组完整的排查链路现场遇到测量结果异常我一般按下面这个顺序排查先看可视化确认卡尺轮廓是否贴合目标矩形方向是否垂直于边卡尺数量是否足够。再看边缘点确认每个卡尺找到的边缘点是否落在真实边界上有没有明显偏内或偏外。查看 Amplitude确认所有参与拟合的边缘点幅度是否处于同一量级如果有个别点幅度特别低说明该处对比度不足。检查 measure_transition 是否与工件背景关系匹配。检查 measure_select 策略确认选中的是正确的边缘。最后才考虑调 sigma 和 threshold这两个参数属于微调不是用来纠正方向性错误的。这套顺序能避免很多无效调试。我见过有人一上来就把 sigma 从 1 调到 5结果边缘位置偏移更大后来发现只是极性设置反了跟平滑参数毫无关系。6. 参数调优经验不同场景下怎么给初始值6.1 一套可以起手的基准参数调参不是从零开始瞎试而是先有一个可靠的初始点再根据实际效果微调。下面是我在不同场景下常用的初始值可以作为起点场景MeasureSigmaMeasureThresholdMeasureLength2备注金属冲压件、CNC 加工件0.8 - 1.225 - 408 - 15边缘清晰、可能有少量毛刺PCB 板线路、焊盘1.0 - 1.520 - 305 - 10对比度较高、边缘密度大玻璃、透明材质边缘1.5 - 3.010 - 2010 - 20边缘模糊、需要大范围扫描塑料件、粗糙表面1.0 - 2.015 - 2510 - 15表面纹理多、噪声大MeasureLength1 的取值原则是覆盖边缘长度后卡尺数量不少于目标数量。如果矩形长边 100 像素想把长边分成大约 10 段每段约 10 像素那么 MeasureLength1 可以设在 5 到 8 之间配合卡尺之间的重叠或间隔达到所需数量。MeasureLength2 则覆盖边缘可能的偏移范围包括工件位置波动和尺寸公差通常取 5 到 20 像素取大了会把相邻其他边缘也扫进来。6.2 调参顺序先几何、再平滑、最后阈值参数之间不是独立的调参顺序会影响效率。我建议按这个顺序先确定卡尺几何参数 MeasureLength1 和 MeasureLength2确保卡尺覆盖了足够多目标边缘且没有把旁边的干扰边框进来。这一步看可视化就够了。再调 MeasureSigma让边缘轮廓既平滑又不失真。判断标准是边缘点的 Amplitude 值分布稳定不会因为轻微噪声出现大起大落。最后调 MeasureThreshold目标是所有目标边缘点的幅度都高于阈值而干扰边缘点的幅度低于阈值。确认极性 measure_transition 和选择策略 measure_select这两项放在阈值之前会更合理因为它们决定边缘点来源。如果发现无论怎么调边缘点都会周期性缺失优先检查光源是否均匀而不是继续堆参数。测量模型的参数补偿不了硬件层面的大范围亮度波动这一点在现场比什么都重要。6.3 一个实测数据参考用一块 500 万像素相机、视野约 60mm 的测宽工位做过验证像素当量约 0.018mm/pixel工件标称宽度 30mm。初始参数设 MeasureLength1 10MeasureLength2 12MeasureSigma 1.0MeasureThreshold 30measure_transition 设为 negative。连续测量 100 次测量结果的极差稳定在 0.006mm 以内3σ 重复性约 0.002mm。这个精度对大部分五金件尺寸测量是够用的。如果还想再压重复性只能换更高分辨率的相机并通过标定修正畸变靠继续加大 sigma 已经没有意义反而会把真实边缘匀掉导致准确度下降。我在实际使用中最大的体会是Metrology 模型的参数没有一套万能配置但所有参数都能在图像上找到对应物。卡尺长宽对应覆盖范围sigma 对应平滑程度threshold 对应边缘对比度测量策略对应边缘选择逻辑。把这个对应关系记在脑子里任何疑难现场都不会慌。写代码之外我每次调试完都会把卡尺和边缘点快照保存出来留作后期追溯真的很省事。