ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HALCON机器视觉尺寸检测:从鱼条测量看亚像素与标定实战

HALCON机器视觉尺寸检测:从鱼条测量看亚像素与标定实战

1. 例程整体思路拆解:鱼条尺寸检测到底在测什么

食品加工行业里,生鱼条(fish stick)的尺寸检测是个很典型的机器视觉应用场景。这类产品在进入冷冻、包装环节之前,需要按照规格标准筛选出长度、宽度、厚度合格的批次。人工用游标卡尺一个个去量显然不现实,产线速度摆在那里,检测节拍往往按每秒几根来计算。所以业界普遍的做法,就是用工业相机配合图像处理算法,对输送线上的鱼条做在线测量。

HALCON 里自带的这个check_fish_stick_dimension例程,就是干这件事的。它解决的问题很聚焦:通过单目相机拍摄鱼条图像,经过图像分割、形态学处理、边缘提取,再到尺寸计算和规格判定,最终输出被测鱼条的长度、宽度,并判断是否在设定公差范围内。

这个例程适合三类人参考:

  • 刚接触机器视觉、想理解“从图像到测量数值”全流程的初学者;
  • 做食品、农产品分拣产线的视觉工程师,需要类似方案打个底;
  • 对 HALCON 的标定、形态学、亚像素边缘等算子的配合关系不清楚,想找个完整示例学一下的开发者。

我第一次跑这个例程的时候,最大的感受是:它把机器视觉测量类项目最常见的链路——标定、分割、处理、测量、判定——完整串起来了。虽然业务场景是鱼条,但把鱼条换成别的长条形被测物(比如螺钉、条状零件、包装袋),流程基本可以平移复用。

先聊整体设计思路。HALCON 这类视觉库做测量的核心逻辑,不是单纯地“看图像”,而是“把像素坐标变成物理尺寸再判断是否合格”。所以例程里有一个绕不开的环节:相机标定。通过标定,把图像坐标系和世界坐标系建立映射关系。否则你只能得到“这条鱼占了1180个像素”这样的结果,没法告诉产线工人“这条鱼长12.3厘米,超公差了”。

有了标定关系之后,接下来就是经典的图像处理四板斧:

  1. 预处理——让目标从背景里清晰地分离出来;
  2. 分割——用阈值把鱼条区域和背景区域分开;
  3. 形态学处理——把分割出来的区域修整干净,去掉毛刺、填补空洞;
  4. 测量——基于区域的几何特征(或者亚像素边缘)计算尺寸。

然后加一个判定环节,把测量值和公差上下限比较,输出 OK/NG。

这套流程对我的启发在于:它展示了一个务实的选择逻辑——分割用灰度阈值,测量用亚像素精度。为什么这么说?因为鱼条和输送带背景之间通常有足够的灰度差,阈值分割简单高效;但单纯靠阈值分割得到的区域边界是像素级锯齿状的,直接量尺寸误差太大,所以例程在拿到区域之后,又提取亚像素边缘来精算尺寸。这种“先粗分割、再精测量”的两段式思路,是工业测量项目里非常实用的做法。

2. 采集与预处理:图像质量决定了后面所有环节的上限

做机器视觉的人都知道一句话:图像采集没做好,算法再牛也白搭。check_fish_stick_dimension例程的第一步,就是读取一张已经拍好的鱼条图像,然后立即进入预处理环节。

HALCON 里读取图像的算子很简单,一条read_image就行。但这里有一个容易被初学者忽略的点:例程里的图像是从本地文件读的,而真实产线上,图像来自相机。这两者有本质区别——文件是静态的,相机是动态的。如果你直接把例程搬到产线,必须替换成grab_image或grab_image_async这类采集算子。我第一次移植例程时就踩过这个坑,忽略了异步采集和多帧缓存的问题,结果产线一跑起来,图像严重的运动模糊,测量结果忽大忽小。

回到预处理环节。鱼条是生的,表面有水分、有光泽,不同批次的鱼条颜色也会有差异。这些因素都会导致灰度值分布不稳定。所以例程里大概率会用到gray_range_image或mean_image之类的算子来处理光照不均的问题。虽然 HALCON 例程对单张测试图不一定需要特别强力的滤波,但在实际项目中,这一步我会建议做得更充分一些:

  • 如果是背光照明(鱼条经过光源和相机之间),背景亮度均匀,鱼条呈现暗色剪影,这时预处理可以很简单,灰度拉伸一下就行;
  • 如果是前景照明(光源在相机同侧),鱼条表面反光严重,需要加偏振片,同时用中值滤波去除椒盐噪声。

例程里更核心的预处理分支点,其实是 ROI(感兴趣区域)的设定。鱼条在图像中并不会占满整个视野,如果直接对全图做阈值分割,会把输送带边缘、水渍、反光点全部卷进来,增加后续处理难度。通过reduce_domain或crop_domain限定处理范围,只针对鱼条可能出现的区域做分析,这一招在工业现场特别管用。计算量降低是其次,关键是减少了误检来源。

我通常的做法是:先取一帧空背景图像,做个差影,确定鱼条出现的大致区域范围,再把这个范围适当外扩20~30个像素作为 ROI。这样即使输送带轻微抖动,鱼条也不会跑出 ROI。例程本身没有把 ROI 精修到这种程度,因为它针对的是示例图,但思路是完全一致的。

还有一个细节:灰度图像的单通道问题。彩色相机采集到的是三通道 RGB 图像,而 HALCON 里做阈值分割一般只需要一个灰度通道。例程中会用rgb1_to_gray之类的算子转成灰度图。这里送大家一个经验:如果鱼条是白色肉质且背景是深色输送带,直接取蓝色通道往往灰度对比度更高,因为大多数工业相机和光源搭配下,蓝色通道受环境杂散光干扰最小。当然,这个规律不绝对,建议实际项目里把三通道拆开分别测试一下,选对比度最高且最稳定的通道。

预处理做完,评价标准就一句话:鱼条区域和背景区域的灰度直方图是否呈现出明显的双峰。如果是,下一步阈值分割基本不会出问题;如果双峰不明显,别急着调阈值,先回头优化光源和曝光参数,这比在代码里拼命调阈值要高效得多。

3. 分割与形态学处理:从灰度图到干净的鱼条区域

check_fish_stick_dimension例程中间的重头戏,是把鱼条从背景里“抠”出来。玩 HALCON 的人对threshold算子一定不陌生,它做的事情就是遍历图像每个像素,把灰度值落在指定范围内的像素归为目标区域。

鱼条在图像里通常比背景暗(背光照明)或比背景亮(前景照明),所以阈值选择有方向性。例程里多半是取一个下限值,把所有低于该灰度值的像素划为鱼条候选。这个下限值的选取有讲究:调得太大,鱼条边缘的弱信号被切掉,测量结果偏小;调得太小,背景噪声被圈进来,测量结果偏大。一个务实的做法是灰度直方图取谷底值作为初始阈值,再根据实测结果微调。

分割完成后,得到的区域往往不够干净,这时候就要上形态学处理了。例程里会用opening和closing这类算子来做区域的开运算和闭运算。

  • 开运算(先腐蚀后膨胀):主要用来去除区域外围的细小突起和孤立噪点。比如鱼条边缘的碎肉、输送带上的小水珠,被圈进来后形成一些很小的离散区域,开运算能把这些“毛刺”清理掉。
  • 闭运算(先膨胀后腐蚀):主要用来填补区域内部的细小孔洞。比如鱼条表面的暗斑导致灰度值低于阈值下限,在内部形成小黑点,闭运算能把这些空洞填平,保持区域完整性。

选择哪个操作、用多大尺寸的结构元素,取决于实际图像里噪声的特征。例程中针对鱼条这类比较规则的长条形目标,一般用小尺寸圆形结构元素(比如半径3.5像素)做一到两轮开闭运算就够。结构元素太大会导致鱼条形状失真,尤其是两端会变得圆钝,直接影响长度测量精度。

分割和形态学做完之后,还有一个非常关键的步骤:connection(连通域拆分)。图像里可能同时出现多条鱼条,或者一些不属于鱼条但灰度值相近的干扰物。connection算子会把目标区域拆分成一个个独立的连通域,然后再结合select_shape或select_shape_std做筛选。

筛选条件通常是三个:

  • 面积:鱼条的面积在合理范围内,明显过大的可能是多条鱼粘连,过小的基本是噪点;
  • 长宽比:鱼条是长条形,长宽比明显偏大的区域更可能是输送带边缘;
  • 轮廓平滑度:用来排除随机水渍之类的伪目标。

在例程里,筛选完的区域就是一个“确认过的鱼条区域”。“确认”这两个字很重要。因为视觉测量的准确率取决于“是否是目标物体”判断得准不准。如果这一步把杂质的区域当成鱼条量尺寸,后面的测量精度再高也没有意义。

这里我必须多说一句实际心得:形态学处理的参数(结构元素尺寸、迭代次数)不应该是拍脑袋定的,最好直接从图像上量出来。比如鱼条边缘的碎肉突起通常只有3~5个像素,那么结构元素半径设为5就能把它们清掉,同时又不至于把鱼条本身的主体面积吃掉太多。如果你发现开运算之后鱼条区域缩水了,说明结构元素选大了,需要调小。这类参数的调试思路,就是平时说的“量出来,而不是感觉出来”。

4. 亚像素边缘提取与尺寸计算:从像素精度到微米级测量

鱼条区域拿到之后,接下来的问题就是:这个区域的尺寸是多少?

如果直接基于区域计算,HALCON 里有smallest_rectangle2(求区域的最小外接矩形)、area_center(求面积和中心点坐标)这些算子,很快能得到长度和宽度。但像素精度的区域边缘是有鋸齿的,直接量出来的尺寸波动会比较大。所以工业上做精密测量,更倾向用亚像素级别的边缘提取。

例程中比较核心的操作应该是edges_sub_pix。这个算子会在图像灰度梯度变化剧烈的地方提取出一条平滑的、位置精度达到子像素级别的 XLD(eXtended Line Description)轮廓。说白了,就是沿着鱼条边界的灰度过渡带找出一条最贴合真实物理边缘的曲线,精度比像素边缘高一个数量级。

有了亚像素轮廓之后,怎么算长度和宽度?这里有一个关键的几何操作:拟合。鱼条在图像里未必是严格水平或垂直放置的,可能带一点倾角。需要用fit_rectangle2_contour_xld直接把轮廓拟合成一个带倾斜角度的矩形。这个矩形有中心坐标、方向角、半长边长(Rect2Len1)和半短边长(Rect2Len2)。理论上鱼的宽度乘以2就是鱼条的宽度,长度乘以2再乘个系数就是鱼条长度(因为矩形拟合的是轮廓的最小外接效果,实际鱼条两端可能不完全顶满)。

测量算子的选择上有两条路线:

  • 最小外接矩形:用smallest_rectangle2拟合区域,计算长宽。优点是计算快、抗噪好;缺点是两端如果有缺肉,矩形会向内收紧,导致长度偏小。
  • 卡尺测量:或者叫边缘对测量,在鱼条的横向和纵向分别取若干个卡尺区域,用measure_pos求每条线上的边缘位置,再平均出宽度和长度。优点是精准且能反映局部尺寸波动;缺点是实现复杂,需要设计卡尺的采样位置和方向。

HALCON 例程的测量环节一般做得比较经典,它并不追求最复杂的测量算法,而是用最稳妥的方式:对亚像素边缘拟合矩形,再结合distance_pp或distance_pc计算关键点之间的距离。

把像素尺寸转换成物理尺寸,是测量环节里必不可少的一步。这个转换关系,来自相机标定结果。HALCON 里标定物一般是标定板,用find_calib_object找到标定板,再用get_calib_data取出相机内外参和畸变系数。在测量鱼条之前,要把这些系数输入到测量算子里,例如通过image_to_world_plane做畸变校正,把图像映射到世界平面,这样量出来的数据才是毫米单位的。

我自己在复现这个例程时,在这里踩过一个不小的坑:直接用未做畸变校正的图像算尺寸,鱼条处于图像边缘时,测量结果和真实值差了将近5%。为什么?因为镜头在边缘位置的畸变会让鱼条看起来变长或变窄。如果生产线上对公差要求比较高(比如允许±2mm偏差),不校正畸变直接影响到判定结果。所以当你看到例程里有一大堆标定相关的代码时,不要觉得它多余——这正是工业测量和实验室图像处理之间最重要的分水岭。

再补充一个很多人问过的问题:鱼条表面有水分反光,会不会影响亚像素边缘提取?会。水分反光会造成边缘附近的灰度梯度出现双峰或多峰,edges_sub_pix可能把两个梯度响应都认为是边缘,从而提取出两条靠得很近的轮廓线。应对办法是加偏振照明减少镜面反射,或者对图像先做一次高斯平滑,让梯度峰合并成一个。HALCON 例程里用到的binocular相关算子主要是为双目视觉准备的,单目方案的这套流程就是靠照明和滤波解决的。

测量做完之后,要把结果存下来,用于后续的判定。这里建议不要直接拿原始像素值去判,而是先做单位换算。HALCON 里这些换算通常在标定模块里统一处理。也可以自己写一个PixelToMm := calibration_factor的比例因子,用起来更直观。不过要注意:比例因子只在物体位于同一高度平面时成立。如果鱼条大小不一,堆叠高度变化明显,单目加固定比例因子就会引入误差。碰到这种情况,要么用激光轮廓仪做高度补偿,要么改用双目立体视觉方案。这不是例程能替你解决的,但如果你明白了原理,就能判断自己的项目到底该用哪种方案。

5. 判定逻辑与公差不合格处理:结果不是“量出来”那么简单

测量出尺寸只是第一步。产线要的不是一组数字,而是一个明确的结论:这根鱼条合格还是不合格。所以例程的最后部分一定有判定逻辑。

HALCON 里做判定通常有两种思路:

第一,区域几何判定法。直接对分割出的区域做几何判断,比如面积是否落在上下限范围内、长宽比是否合理、最小外接矩形的长和宽是否在公差范围内。这种方式胜在速度极快,适合高速在线检测,但只能做整体判断,无法定位局部异常。

第二,边缘尺寸判定法。对亚像素轮廓的多个截面同时测量,逐段计算长度和宽度,不仅能判断整体是否合格,还能定位鱼条哪个地方偏瘦、哪个地方偏短。这个方式的精度和细粒度更好,但计算量稍大,节拍受限。

例程中使用的是第二种思路,因为它的目标就是获取尺寸数值,而且类似“鱼条中部太细则说明肉量不足”这种局部特征,只有逐段测量才能发现。这在食品安全和质量分级场景里尤其重要。

判定条件一般包含:

判定项合格范围(示例)判定结果
长度90mm ~ 110mm过长 / 过短 / 合格
宽度18mm ~ 22mm过宽 / 过窄 / 合格
面积均匀性最大最小宽度差 ≤ 3mm形状不规则

这组数据我是拿某个水产加工项目里用过的规格举例,实际参数每个厂都有自己的企标。例程里的具体数值只是一个示意,但逻辑框架是一致的:先做比较,再给结果。

这里有一个在例程里体现得不太明显、但在实际系统中很重要的点:误检和漏检的权衡。如果你的判定阈值卡得太紧,比如宽度21.9mm就算超差,那你可能时不时误杀一些实际上能用的大块鱼肉,造成浪费;但如果卡得太松,不良品流出,又可能被客户投诉。做视觉尺寸检测项目,这个阈值到底取多少、要不要加迟滞区间,需要和现场质量负责人反复确认,不要自己拍脑袋定。我一般在给客户交付前,会把判定逻辑做成参数列表,由客户工艺人员提供数值,然后我方写进配置文件里。

判定结果不只是显示在屏幕上。产线需要执行动作,比如吹气阀把不合格鱼条吹出产线,或者在屏幕上亮红灯提示人工取出。HALCON 例程通常通过输出变量把结果传给上位机(PLC 或者工控机软件)。这里就涉及一个通信协议问题。比较常见的做法是走 TCP/IP 或者 Modbus,把 OK/NG 信号和尺寸数值发给下位机。例程中没有展开这部分内容,但实际部署时这是必须自己实现的模块。

还有一点关于重复性验证的建议。当你把例程跑通之后,别急着改进程,先在稳定的光照条件下连续测量同一根鱼条50~100次,统计测量值的标准差。如果标准差超过了你允许的公差的十分之一,说明测量系统本身的稳定性不够,这时候盲目调判定阈值是无效的。这是衡量测量系统能力的一个基本门槛(我们常说的 GR&R 概念简化版),控制好了,后面调整判定逻辑才有立足点。

6. 常见问题与排查技巧实录

我在参考和移植这个例程的过程中,遇到和收集过不少典型的坑,整理成一张速查表,希望帮大家少走几条弯路。

现象可能原因排查方向
分割出来的鱼条区域明显比实际鱼条短阈值下限偏高,边缘弱信号被裁掉降低阈值下限,观察直方图谷底位置
宽度测量值等于一个像素到三个像素的随机跳变边缘提取时噪声干扰检查光照反光,增加平滑滤波强度
图像边缘处的鱼条尺寸偏大镜头畸变未校正确认标定流程是否执行,检查畸变系数是否启用
形态学处理后鱼条两端明显变圆滑闭运算结构元素偏大减小结构元素半径,只对目标区域中的孔洞做填充
同一条鱼条连续测量10次,结果差异超过0.3mm系统稳定性不够,可能是震动或光源频闪固定相机、加遮光罩、改用无频闪光源
鱼条表面水渍被当成独立目标区域灰度值与鱼条重叠加偏振镜,或者在 select_shape 中增加面积下限筛选
不同批次的鱼条灰度差异大,固定阈值失效原料色泽差异改用自适应阈值(如local_threshold),或用对比直方图做动态阈值
多条鱼条首尾相连,被合并成一个区域粘连处灰度差异不明显用分水岭或基于距离变换的切割,或使用形态学腐蚀分离后再还原

其中有两个问题我特别想展开说。

第一个是“边缘处尺寸偏大”的问题。如果你在图像中心量一块标准件,尺寸是对的,但把它移到图像边缘,数值就飘了。几乎可以肯定是畸变没校正。但要注意,HALCON 的标定并不只是“基于标定板跑一次函数”那么简单,标定板的平面必须和被测物体的平面在同一个高度,否则标定结果只能保证在那个高度上准确。鱼条有一定厚度,如果厚度方向有变化,还是会产生误差。务实的做法是:针对鱼条平均高度重新做一次标定,而不是用出厂标定数据将就。

第二个是“固定阈值失效”的问题。水产原料的品质波动很大,一批鱼偏白,一批鱼偏黄,用同一个灰度阈值去分割,很难保证都分得干净。我在类似项目里常用local_threshold代替全局threshold,它的原理是计算每个像素周围的局部灰度统计量来决定阈值,自适应能力更强。代价是计算量稍高,但在现代工控机上完全可接受。如果你不想换算子,也可以做个简易校准流程:每天开机时拍一张空输送带图像,自动算一次动态偏移,把这天的偏移补进阈值里。

最后一个我踩过的坑,是关于例程的“直接搬运”。初学者最容易犯的错误是把例程里的调试参数直接搬到自己的项目里。例程里的阈值、形态学参数、公差值都是针对示例图片的,换一条产线、换一个相机、换一种灯光,所有参数都需要重新标定和测试。HALCON 例程的真正价值在于展示处理流程和算子组合方式,而不是给你一套万能参数。理解了这一点,你才算真正会用它。

个人经验来说,我每拿到一个新的视觉检测用例,都会先跑通流程,再用一批典型的合格品和废品图去调参,最后才进入现场实测。这个顺序错不得,因为室内调参和现场环境的差异非常大,你永远不知道现场的振动、反光、灰尘会带给你什么样的惊喜。

这个例程本身只是一个起点,但它把测量类视觉项目的骨架搭得清清楚楚:采集、预处理、分割、形态学、亚像素测量、标定映射、判定输出。如果你能顺着这个骨架自己重写一遍,再结合实际产线的约束条件去优化,那么你对机器视觉的理解会往上走一个台阶。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进