☰
机器视觉尺寸测量怎么做?拆解HALCON生鱼条检测例程全流程
2026/10/19 5:15:46 网站建设 项目流程

生鱼条尺寸检测这件事,熟悉食品加工自动化的人应该都不陌生。产线上一条条处理好的生鱼条要按长度和宽度分选等级,过短、过长、太薄的都要单独挑出来。人工拿尺子量又慢又容易疲劳,视觉方案就成了标配。HALCON 里有个专门针对这个场景的例程check_fish_stick_dimension,不少朋友拿它入门,却又被里面一连串算子绕晕,今天我就按自己的理解把它拆开,从标定到尺寸输出一步一步讲清楚,也把实际调试中容易踩的坑一并说掉。

这个例程本质上是一条典型的机器视觉尺寸测量流水线:采图、预处理、分割、形态学修复、计算区域几何参数、像素当量换算、最后给判定结果。整个过程看懂了,不只是生鱼条,换到鸡块、饼干、螺丝这类外形相对规则的小尺寸物体,思路也基本都能套用。下面直接进入正题。

1. 为什么鱼条测量不能用卡尺硬量:先看这个例程要解决的真实问题

1.1 产线测量的痛点和视觉方案的切入角度

水产加工工厂里,生鱼条从切割机出来后是连续不断地在传送带上往前走的,速度通常在每分钟几十米。如果靠人工挑拣,一条鱼条从眼前过,眼睛要判断长度是不是在规格范围内,还要看宽度是否均匀,连续工作两个小时之后精度和速度都会明显下降。更重要的是人工测量的结果没法自动记录,更没法跟批次追溯。尺寸数据的数字化在这个行业里越来越被看重,机器视觉刚好补上这个环节。

HALCON 这个例程解决的就是“单条鱼条在传送带上经过相机下方时,自动测出长和宽,并判断是否合格”这个场景。注意它默认的是单条鱼条出现的情况,鱼条之间如果挨得太近甚至叠在一起,处理逻辑会复杂得多,这个后面讲坑的时候专门说。

1.2 例程的完整处理链路

我习惯先把一条视觉测量程序的骨架理清楚,再往里面填算子。这个例程的整体链路大致如下:

  • 采集图像:从相机拿到一帧图像,或者从文件读取测试图。
  • 预处理:把图像灰度范围拉伸,做降噪,让鱼条和背景的分界更清楚。
  • 阈值分割:把目标区域从背景里分出来,得到鱼条的候选区域。
  • 形态学修复:填补鱼条内部的小孔洞,去掉边缘毛刺,让区域更接近真实轮廓。
  • 连通域分析和选择:把鱼条区域从其他杂质区域中挑出来,挡掉碎屑和噪点。
  • 几何参数计算:计算最小外接矩形、面积、中心坐标、方向角。
  • 尺寸换算:把像素值乘以标定得到的像素当量,转成毫米。
  • 判定输出:跟预设上下限比较,输出 OK 或 NG。

每一步都不复杂,但每一步都有讲究。下面按照这个链路逐个展开。

1.3 谁适合拿这个例程当教材

如果你是刚接触 HALCON 的机器视觉工程师,这个例程比那些只演示图像增强的样例有价值得多,因为它覆盖了从图像处理到工程落地的完整闭环。如果你已经在做食品、医药、电子这些小尺寸产品的视觉检测,这个例程同样值得拆一遍,你会在里面看到很多可复用的处理手法。

2. 尺寸测量的地基:标定、采图和预处理逻辑

2.1 不标定的像素测量都是耍流氓

很多初学者拿到图像后,直接数鱼条占了多少个像素就当成“尺寸”,这只有在视场尺寸和分辨率都固定的理想情况下才勉强成立。实际产线中,相机安装高度稍微变化、镜头焦距不同、视场大小不同,同一个物体对应的像素个数差别很大。所以必须做一件事:确定“一个像素等于多少毫米”,这个值在视觉行业里叫像素当量。

标定的方式很简单:放一个已知尺寸的物体在测量平面上,拍一张图,计算它在图像里占了多少像素,用真实尺寸除以像素数即可。比如一个宽度为 50mm 的标准块,在图像中占 1000 像素,那像素当量就是 50 / 1000 = 0.05mm/像素。更正规的做法是使用 HALCON 的标定板配合标定助手,可以得到镜头畸变参数和相机内参,精度更高。如果只是测量平面尺寸、对精度要求不那么苛刻,用前面说的简单比例法也够用。

这里有一条实际现场非常容易忽视的规则:标定用的平面高度必须跟测量时鱼条所在的平面高度一致。如果相机是倾斜安装或者高度不同,像素当量要重新算。注意,标定完成后相机高度、镜头焦距就不能再动了,动了就要重新标定。

2.2 预处理不是简单“变清晰”,而是为分割铺路

生鱼条从水里出来,表面湿度大,跟背景的对比度可能并不理想,传送带本身如果有纹路,还会给后续阈值分割带来干扰。所以预处理的目标不是好看,而是让“鱼条和背景”在灰度直方图上分得开。

常用的做法是先用scale_image或者scale_image_max做灰度拉伸,把原本集中在某个窄区间的灰度拉开。再看图像有没有明显噪点,如果有,可以加一个median_image,对去除椒盐噪点和表面纹理很有效。

但在处理这类湿润食材时有个度要把握住:灰度拉伸得太猛,鱼条内部的纹理也会变成明显的高低起伏,后面提取边缘时会出现很多假的边缘点。我见过有人把对比度拉到极端,结果原本光滑的鱼条表面被分割成了几十个小碎块。预处理永远是为目标服务的,不要为了“看起来清晰”而过度处理。

2.3 ROI 设定:减少计算量,也减少误判

产线图像里不是所有区域都需要处理。传送带边缘、固定支架、甚至飞溅的水滴,都可能出现在画面里。例程通常会先划定一个感兴趣区域,把测量对象限制在中央区域。

这一步在我实际调试中受益很明显:减少 ROI 后,面积过滤的上下限好定得多,因为不需要考虑画面角落里的杂光。而且图像小了一圈,处理时间也能省下不少,对后面要跑高速产线的场景意义很大。

3. 找鱼条:阈值分割与形态学修复的完整操作

3.1 固定阈值还是动态阈值

HALCON 里最基础的threshold算子就是拿一个固定灰度区间去切图像,例如threshold(Image, Regions, 100, 255),意思是灰度 100 到 255 之间的像素都算作前景。如果产线光照很稳定、鱼条和背景对比度很高,这个是够用的。

但实际产线的光照没那么理想。传送带速度变化、环境光干扰、光源衰减都会让图像的灰度整体漂移。今天灰度 100 能分割得很好,明天可能就不行了。这种情况下我建议优先考虑动态阈值,用dyn_threshold配合均值滤波或者高斯滤波,把图像跟模糊后的背景做对比,大于局部对比度的区域才保留。相当于每一小块都用自己的背景做参照,灯光变化带来的影响会被抵消掉。

对应到这个例程,若鱼条比较白、背底比较暗,一个好的组合是先做一次mean_image生成背景近似,再用dyn_threshold得出区域。若原文例程里给的是固定阈值,你换到产线环境时就知道该怎么改。

3.2 形态学修复:闭运算补洞,开运算去毛刺

阈值分割之后得到的区域往往不干净。鱼条表面有水分形成的暗斑、鱼肉的纹理裂隙,会造成区域内部出现一个个小孔洞;边缘又常有毛刺和凸起。这时需要形态学处理:

  • closing_circle或closing_rectangle:先膨胀再腐蚀,通常用来补上内部小孔洞,也能把边缘细小的缺口接起来。
  • opening_circle:先腐蚀再膨胀,主要用来去掉边缘伸出来的细毛刺。

我用一个比喻来解释这两者的关系:闭运算像是用腻子把墙面上的小坑抹平,开运算像是用砂纸把墙面的凸刺磨掉。两个操作配合使用的时候,顺序很关键。一般先闭运算补洞,再做开运算去毛刺。如果反过来,补完洞又被削掉一层,等于白做。

结构元素的大小需要按像素当量折算。比如像素当量是 0.05mm/像素,想要修复 1mm 宽的小洞,结构元素半径给 10~20 像素是合理的。太大也不行,会把鱼的边缘磨圆,导致测量出来的长度偏小,这种误差是系统性的,非常难排查。

3.3 连通域分析与杂质过滤

形态学做完之后,区域里可能还包含背景上的水滴、传送带污渍、鱼皮碎片等小目标。connection算子把连在一起的目标拆成一个个独立的连通域,然后用select_shape按照面积、宽度、高度等特征做筛选。

这里有个需要长期调试积累的经验:面积上下限不要拍脑袋定。我习惯先跑一遍不加筛选的程序,把图像里出现的所有连通域的面积值都打出来,看鱼条区域和其他杂质的分布区间,再确定过滤范围。做了几十个样本之后,这个区间会非常稳定。如果一上来就定一个看起来很合理的范围,一旦有稍大一点的杂质进来,后面的测量就会连锁出错。

整个分割链路走完后,我已经能拿到一个只包含鱼条的二值区域了,下一步才是算尺寸。

4. 算尺寸:从区域几何参数到毫米级输出的三种做法

4.1 最小外接矩形:最快但要注意方向角

拿到鱼条区域后,最省事的方法是调用smallest_rectangle2。这个算子会计算区域的最小外接矩形,返回四个关键信息:

  • Row、Column:矩形的中心点坐标。
  • Phi:矩形长轴与水平方向的角度。
  • Length1:矩形半长轴长度,也就是长边的一半。
  • Length2:矩形半短轴长度,也就是短边的一半。

鱼条的实际长度就是2 * Length1,宽度就是2 * Length2。再分别乘以像素当量,就得到毫米值。

这里我强调一下“半长轴”这个细节。不少初学者直接拿Length1当作长度,出来的结果比实际小一倍,然后满世界找原因。真正的长度一定要乘 2。同样的道理,HALCON 里很多几何算子返回的都是半值,比如半长轴、半短轴、半径,换算前先搞清楚定义。

smallest_rectangle2最大的优点是快、逻辑简单,对长条形的鱼条来说,方向角也给得很准。但它在鱼条略有弯曲时就不够好了,外接矩形会把弯曲区域多出来的空间也框进去,导致测量值整体偏大。

4.2 亚像素轮廓拟合:精度更高但阈值要调好

如果鱼条的相对笔直,标准做法是在分割区域的基础上,进一步提取亚像素边缘,再拟合成直线或矩形。流程大致是:

  • 先用reduce_domain把处理范围限制在鱼条区域。
  • 使用edges_sub_pix提取亚像素精度的边缘轮廓。
  • 再用fit_rectangle2_contour_xld或者fit_line_contour_xld对轮廓两侧的长边做拟合。

edges_sub_pix用的是边缘梯度做插值,理论上能达到亚像素精度,受阈值分割的量化误差影响更小。这套做法的精度上限比区域法高不少,但算法敏感度也高,边缘断断续续、反光干扰严重时,拟合结果会很不稳定,需要反复调edges_sub_pix里的滞后阈值。

实际项目中我的习惯是:批量检测粗定位用区域法,单点高精度复检用轮廓拟合法。两条路并行,互相验证。如果两个结果对不上,要么是标定有问题,要么是预处理把边缘处理坏了,排查方向会很明确。

4.3 卡尺阵列测量宽度:应对鱼条宽度不均匀

鱼条不是完美的矩形,头部、中部、尾部宽度可能差不少。如果只拿最小外接矩形的短轴当宽度,相当于只看一个平均值,很多“局部偏细”的缺陷会被掩盖。

这种时候用卡尺阵列更合适。沿鱼条长轴方向等间隔地生成多个测量卡尺,每个卡尺沿垂直方向扫描边缘,得到该位置的实际宽度。把所有卡尺测出来的宽度做一个统计,比如最大值、最小值、平均值,就能同时判断“这条鱼是否有局部过细”的问题。

实现方法是用gen_measure_rectangle2创建测量对象,再循环调用measure_pos获取边缘点。卡尺的数量根据鱼条长度来定,一般每 10~15mm 放一个卡尺就很密了。需要注意卡尺宽度要覆盖鱼条左右边缘,不能只覆盖到一半,否则边缘查找会漏。

这三套方法并不冲突,真正做到工程级,往往先外接矩形得到方向角和粗尺寸,再用方向角信息去布卡尺,最后用卡尺测量结果给出更精确的长度和宽度分布。一步比一步精细,计算量递增,但最终结果更接近真实。

4.4 像素长度到毫米的换算公式

把整个计算过程总结成一个通用公式:

实际长度(mm) = 像素长度(px) × 像素当量(mm/px)

如果相机画面存在畸变,严格做法是先把像素坐标反畸变到世界坐标,再用坐标差算物理距离。不过对低畸变镜头、450mm 以下的小视场测量,畸变影响通常可以忽略,这也是这个例程能简化处理的前提。

5. 实际调试中绕不开的四个坑

5.1 光照波动:直接导致阈值漂移

这是我在某水产加工厂的测试线上遇到最多的一个问题。车间里的日光灯会频闪,传送带上方如果有设备震动,光源支架也会轻微摆动,图像灰度每帧都有波动。固定阈值在这种情况下就是灾难,今天分割正常,明天同一段代码分割出来的区域面积能差 10% 以上。

应对策略有两个方向。硬件层面把光源供电改成直流或高频驱动,减少频闪;同时把相机曝光时间固定下来,不要用自动曝光,自动曝光会把灰度变化掩盖掉,但对视觉测量是致命的,因为灰度虽然变稳定了,边缘位置却会在像素级上漂移。算法层面则用动态阈值代替固定阈值。两条腿一起走,稳定性才会真正提升。

5.2 鱼条表面水汽反光:边缘断裂和假边缘

生鱼条表面带着冷藏水,光照一打就会出现高亮点,有些高光点在鱼条内部,有些正好压在边缘上。压在边缘上的高光点会让边缘看起来像是断了,后续轮廓提取就会得到断成几截的轮廓。

最简单的缓解办法是给光源加偏振片,镜头前也加偏光镜,能滤掉大部分镜面反射。如果已经没法改硬件,算法上可以在边缘提取前把高光区域平滑掉,用gray_opening或者范围更大的中值滤波都行。再不行就接受断裂的现实,对提取到的轮廓做合并判断,把相距很近的断头连接起来。

这个坑仅靠 HALCON 里的固定流程很难彻底解决。我后来跟光学工程师配合,调整了打光角度,改成低角度环形光,让镜面反射偏向镜头视场之外,边缘才算真正干净。

5.3 鱼条粘连:一个区域里挤两条鱼

产线速度提上来后,两条鱼很容易头尾相接甚至轻微重叠。连通域分析时它们会变成一个整体,最小外接矩形框出来的是一个长条,长度测量直接爆表。这个问题的根治不在视觉端,而在理料段,把鱼条的间距拉开。

实在拉不开再考虑图像算法。一种思路是用watersheds找凹点分割,前提是两条鱼之间有明显的脖颈凹陷。还有一种思路是判断区域面积是否超过单条鱼的上限,超过即认为是粘连,直接抛到人工复检工位。工程上我倾向于后一种,稳定且不会误切。

5.4 验证测量稳定性的重复性测试

有朋友问我怎么判断这套系统到底准不准。精度的验证平时已经做了,稳定性验证才是产线考核的重点。我的做法是拿同一批 100 条鱼条,让它们在相机下方匀速通过,采集 10 遍,统计每条鱼长度测量值的标准差。好的系统标准差应该在 0.2mm 以内,如果超过 0.5mm,多半是分割阈值波动或边缘提取不稳定,要回去检查前面的环节。

6. 从例程到产线落地:性能优化、判定逻辑与场景迁移

6.1 分辨率、速度与精度的取舍

相机的分辨率不是越高越好。分辨率越高,处理的数据量越大,帧率越难保证。以 200mm 视场为例,要测到 ±0.5mm 的精度,理论上 400 万像素级别的相机已经绰绰有余;过度追求分辨率只会让每个像素对应的物理尺寸更小,但边缘提取的稳定性不可能无限制提升。工程上要算的是“在这个视场下,多少像素当量能满足我的公差要求”,通常取公差值的 5 到 10 倍作为像素当量的设计目标。

处理速度方面,还是要优先减少 ROI,一次图像只处理传送带中央那一条窄带,计算量可能只有全图的四分之一甚至更少。

6.2 判定逻辑与数据输出

例程最后会将测量结果跟预设规格对比。这里的规格上下限建议做成配置文件,不要写死在代码里,因为客户的规格经常变。判定逻辑里还要加入宽度均匀性的指标,我前面说的卡尺阵列测出来的最大值和最小值,两者差超过阈值,就可以判为不合格。

输出部分,一般会通过 TCP 或者简单的 I/O 信号发给剔除机构,并在界面上显示图像和测量数据。如果是多级分选,直接把长度区间映射到分级档位即可,例如 100~120mm 为一级,90~100mm 为二级,低于 90mm 为不合格。注意是边界值落在哪个区间要提前约定,否则会出现同一批鱼在两个档位之间震荡的情况。

6.3 把同样的思路迁移到其他物体

这类测量逻辑不局限于生鱼条。换到鸡块尺寸分级、饼干外形检测、五金件长度测量,核心流程都一样,差异主要在三个地方:目标跟背景的对比方式、形态学修复的结构元素大小、几何参数的选择。

唯一的建议是不要过度迷信通用例程的默认参数,一定要跑一批真实样本再看效果。我当时用模拟项目 X 把整套逻辑移植到另一种食品检测场景时,就是把阈值下限从 120 调到了 80,形态学半径从 3.5 调到了 8,仅此两个改动就让漏检率降为零。参数本身没有好坏,跟场景匹配才是关键。

最后再分享一个经验:这套测量系统的维护中,最容易忽视的是定期重新标定。相机镜头松动、传送带高度微调、光源支架变形,这些都会悄悄改变像素当量,导致测量值逐渐偏移。我见过一套系统运行三个月后整体偏大 1mm,原因就是支架在震动中歪了一点点。所以建议至少每个月用标准块做一次现场快速校验,发现问题立即重新标定,别等到客户投诉了再去排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询