☰
边缘提取后的背景分割:Halcon background_seg算子详解
2026/10/21 18:25:56 网站建设 项目流程

前些天有个做视觉检测的朋友跟我聊,说他们项目里已经能把工件的边缘提取得很干净了,但遇到一个问题:工件周围的那圈背景空间到底怎么划分?两块背景区域之间是连着的还是断开的?他说查了半天Halcon的资料,发现有个background_seg算子专门干这个,但手册写得干巴巴的,想把示例程序background_seg.hdev跑通又不知道每一步在干什么。

这确实是边缘提取之后很容易被忽略的一环。我们可以用threshold、edges_sub_pix这类算子把目标物体区域拿出来,但拿到区域之后,下一步往往是测量尺寸、判断缺陷,很少有人会认真分析这个区域周围的背景在空间上是如何组织的。然而在很多实际场景里,背景的组织方式恰恰是关键信息:元件之间是否共用了同一块基材、焊点的外围是否还有连通的残留物、字符周围的空白区域是否有异常桥接。要回答这类问题,就需要计算给定前景区域的连接背景分量。

这篇内容就围绕Halcon中的background_seg算子展开,结合它在边缘提取流程中的典型配合方式,把background_seg.hdev这个示例背后涉及的概念、算子行为、参数逻辑和实际项目中的坑一次讲清楚。无论你是刚接触Halcon不久,还是已经在做视觉项目的集成,都会用得上。

1. 边缘提取之后为什么还要"分割背景":一个常被忽略的需求

1.1 从"找到目标"到"理解环境"

做机器视觉的人通常都有一个条件反射:拿到图像,第一步就是分割出感兴趣的目标。用threshold切灰度,用edges_sub_pix抠轮廓,用regiongrowing抠纹理区域,这些操作的本质都是把"前景"和"背景"分开。在这条思路下,背景往往是被丢弃的部分,因为测量、定位、计数这些任务只关心前景本身。

但是有一些需求,恰恰需要反过来研究背景。比如在电子元件的贴装检测里,我们需要确认两个相邻元件之间的阻焊层是否连续,如果背景区域在这两个元件之间出现了不该有的"桥接",说明基材表面可能有异物或划伤。又比如在PCB焊点的质量分析中,焊点外围的环形背景区域如果被分割成了多个互不相连的部分,可能意味着锡膏流动出现了异常。这个时候,真正有价值的分析对象不再是前景目标,而是位于前景目标周围的背景连通块。

我在实际项目中还见过一个很有意思的用法:用background_seg来辅助判断一个物体是否被另一个物体的背景所"包围"。两个产品堆叠在一起时,先分别提取它们的前景区域,然后计算其中一个区域的连接背景分量,如果它的背景分量被另一个区域完全阻断,说明两片产品在空间上紧密贴合,可用于叠放姿态的判断。

1.2 连接背景分量可以回答哪几类问题

把背景按连通性进行分割之后,我们能回答的问题大致可以分为三类。

第一类是"背景是否断开"。前景区域外面的背景,究竟是完整的一圈,还是被分隔成了若干独立的部分?例如在一张含有多个字符的图像里,提取某个字符的区域后,计算其连接背景分量,如果分量数量非常多,说明字符周围有大量与它距离很近的背景孤立块,可能来自油墨飞溅或衬底纹理的干扰。

第二类是"背景是否共享"。多个前景目标共同位于同一块大的背景连通区域中,还是各自拥有独立的背景空间?这可以用于目标分组的判断。假设一个托盘上有若干产品,计算每个产品区域的连接背景分量,如果多个产品输出的最大背景分量是同一个,那它们在空间上就属于同一个集群。

第三类是"背景结构是否健康"。在精密测量中,我们常常需要保证前景边缘外侧有一圈干净的过渡带。background_seg输出的分量形态、面积、数量都可以作为特征输入给后续分类器,用来判断过渡带是否被破坏。

1.3 背景分割与边缘提取的配合逻辑

edges_sub_pix得到的是亚像素轮廓,直接用来计算背景分量是行不通的,因为background_seg接收的是区域,不是轮廓。所以常规做法是先把轮廓转成区域:用gen_region_contour_xld把边缘轮廓变成细线区域,再用dilation_circle稍微加粗堵住细小断口,或者用fill_up填补内部空洞,得到一个干净的前景区域。如果轮廓闭合得不好,还需要先close_contours_xld做闭合,再生成区域。

这个过程说起来简单,实际做的时候却有不少细节。轮廓加粗的半径如果太大,前景区域会把相邻的细小背景缝隙吞掉,导致背景分量数量减少;半径太小,轮廓断口又堵不上。我通常在dilation_circle之后会用opening_rectangle1或erosion_circle做一次形态学清理,去掉锯齿毛刺,让后续background_seg算出的背景带更规整。这个配合在background_seg.hdev示例中也有体现,只是示例代码往往写得比较精简,单独拎出来讲的人不多。

2. background_seg 到底怎么算的:算子的工作机制与参数语义

2.1 算子签名与输入输出

先看HDevelop里这个算子的调用形式:

background_seg(Region : BackgroundRegions : MinDistance, MaxDistance : )

Region是输入,请一定记住这里输入的是"前景区域",要分割的对象是它周围的背景。BackgroundRegions是输出,它是一个区域元组,其中每个元素都是独立连通的背景块。MinDistance和MaxDistance是距离参数,单位是像素,具体含义后面详细说。

第一次用这个算子的朋友特别容易把它和difference、move_region这类算子混淆。difference能算出前景之外的整个补集,但那是一个大区域,没有把连通性分出来;move_region只能平移区域,不涉及背景分析。background_seg真正做的,是在前景区域周围挖出一个指定宽度的"环带",再把环带里互不相连的部分拆开。

2.2 距离参数的空间含义

从宏观上看,可以把输入区域想象成一个城市里的封闭街区,background_seg要统计的是街区外围一圈马路以外空间被分成了几块。MinDistance相当于"从街区围墙往外走多少步才开始统计",MaxDistance相当于"最多统计到多远为止"。两者之差(MaxDistance - MinDistance)就是统计带的宽度。

如果MinDistance取0,环带紧贴着前景区域的边界开始;如果取5,前景边界外侧那5个像素宽的缓冲带会被忽略,不纳入任何背景分量。MaxDistance决定统计的最远边界,环带以外的背景像素不会出现在结果中。

在实现机制上,算子会分别对输入区域做两个半径的膨胀,大半径膨胀得到的区域减去小半径膨胀得到的区域,这个差值区域就是在距离范围之内的背景带。接着在这个背景带内做连通性标注,每一个连通区域单独输出。理解了这个机制,后面调参的时候心里就有数了。

2.3 连接背景分量的连通性判定

Halcon的区域默认按8邻域判定连通性,也就是说一个像素只要在其上、下、左、右、左上、右上、左下、右下八个方向之一与另一个像素相接,就属于同一个连通块。background_seg的输出也遵循这套规则。不过有一个需要注意的细节:狭窄的背景缝隙在8邻域下可能是连通的,改用4邻域规则后就会被切断成两个独立分量。

我之前遇到过一个很典型的案例。A工程师在用background_seg统计芯片引脚间的背景块数量时,发现结果忽多忽少。后来仔细分析,发现是两排引脚之间有一条极窄的背景缝隙,宽度只有1到2个像素。在8邻域下这条缝隙是连续的,被算作一个分量;但只要某处噪声稍微增大,缝隙在其中一点断开,这个分量就会裂成两个。他当时没有意识到连通性判定带来的这种脆弱性,单纯以为是阈值参数没调好,查了很久才定位到这个问题。

2.4 background_seg 与常见区域运算的区别

很多时候我们手头现成的方法也能做出"背景带",但那种结果和background_seg输出的语义完全不同。用dilation_circle把区域外扩R像素,再difference减掉原区域,得到的是区域外扩后的壳,这个壳并不关心与原有区域的相对位置关系,也不会把壳内不相连的部分单独分成不同的区域。要得到分量,还得自己再connection一次。

相比之下,background_seg把"取背景带"和"按连通性分组"两步合并成了一个操作,而且在MinDistance与MaxDistance的配合上更加灵活。当需要统计背景分量的数量、面积、形态时,用background_seg可以少写好几行代码,也减少了手动拆分时可能引入的出错概率。

3. 从一张图到背景分量:结合边缘提取的完整流程拆解

3.1 示例程序的总体步骤

background_seg.hdev这个示例,整体思路可以概括成三步:先把原始图像中的目标前景区域找出来,再调用background_seg计算连接背景分量,最后把每个背景分量用不同颜色显示出来,直观地检查分割效果。示例代码不长,但它把算子最基本的调用方式、参数传参顺序和结果查看方式都展示得很清楚。

具体到流程上,我习惯把它拆成下面几个环节:

  1. 读取图像并做必要的预处理;
  2. 通过阈值分割或边缘提取得到前景区域;
  3. 对前景区域做清理和形态学修正;
  4. 调用background_seg生成背景分量;
  5. 对分量区域做统计或可视化。

在HDevelop里按F4逐步执行,每一步的中间结果会实时显示在图形窗口里,非常适合用来理解算子行为。不过示例代码默认的阈值和参数是针对它的示例图像调的,直接套用到自己的项目图像上肯定不行,需要理解参数含义后重新设置。

3.2 用边缘提取构造干净的前景区域

这里以一个模拟的矩形工件为例。图像中的工件亮度均匀,但背景有一圈渐变阴影。如果直接用threshold,很容易把阴影也切进来,导致前景区域边界不准确。更稳的做法是先做边缘提取:

read_image (Image, 'part_image') gauss_filter (Image, ImageGauss, 3) sobel_amp (ImageGauss, EdgeAmplitude, 'sum_abs', 3) threshold (EdgeAmplitude, EdgeRegion, 20, 255) skeleton (EdgeRegion, SkeletonRegion) connection (SkeletonRegion, ConnectedEdges) select_shape (ConnectedEdges, SelectedEdges, ['area','circularity'], 'and', [50, 0.5], [999999, 1.0])

这段代码先做了平滑,再用Sobel算子得到边缘强度图,阈值化后形成边缘线区域,然后经过骨架化和连通域筛选,把可能是噪声的碎边缘去掉。得到的SelectedEdges只是边缘线,并不代表完整的工件区域。要生成background_seg需要的封闭前景区域,还需要把边缘线围成的内部空间填满。

填满的方法有很多,我习惯直接用形态学操作:

union1 (SelectedEdges, SingleEdgeRegion) dilation_circle (SingleEdgeRegion, EdgeDilated, 3.5) fill_up (EdgeDilated, FilledRegion)

dilation_circle先把断开的边缘线连起来,半径取3.5像素左右是为了在噪声边缘之间搭桥;fill_up把内部孔洞填平。如果边缘提取结果不错,这样得到的前景区域已经足够干净,可以作为background_seg的输入。

3.3 调用 background_seg 并组织输出

前景区域构造完成后,调用本身非常直接:

background_seg (FilledRegion, BackgroundRegions, 0, 30) count_obj (BackgroundRegions, NumBackground)

这里MinDistance取0,表示背景分量从工件边界处就开始统计;MaxDistance取30,表示只统计工件周围30像素以内的背景空间。输出的BackgroundRegions是一个区域元组,NumBackground就是背景分量的个数。

要查看每个分量,可以这样遍历并统计:

for i := 1 to NumBackground by 1 select_obj (BackgroundRegions, BackgroundSingle, i) area_center (BackgroundSingle, Area, Row, Column) endfor

在HDevelop里直接把BackgroundRegions写到图形窗口,它会自动用不同颜色显示每个连通块,一眼就能看出背景被分成了几块、每块大概在哪。

3.4 结果可视化与验证的细节

很多人跑完示例程序,只看一眼图形窗口里的彩色区域就觉得"效果不错",然后直接把NumBackground拿去做判断。这在调试阶段是远远不够的。我建议一定要做两个验证步骤:一个是把BackgroundRegions和原始图像叠加显示,看每个分量是否真的落在了预期位置;另一个是统计分量面积,把面积异常小或异常大的分量单独挑出来观察。

这个操作在边缘提取质量不稳定的时候尤其重要。因为边缘区域的微小变化会引起前景区域边界的变化,进而导致背景分量的数量和形态剧烈变化。先把异常分量找出来看,能极大缩小排查范围。

4. 参数调优的实战经验:距离参数、分辨率与对象尺度

4.1 MinDistance 和 MaxDistance 怎么选

选这两个参数,核心是搞清楚前景区域的尺度有多大,以及背景中有哪些结构是你关心的。如果前景区域是芯片引脚这样的小目标,边缘到边缘的距离可能只有几个像素,MaxDistance取10到20就够;如果前景区域是板材或大型结构件,相互间距可能在几百像素,MaxDistance往往要取到200以上。

反过来,MinDistance也不是越小越好。有些场景里前景区域边缘本身有毛刺或反光造成的伪边缘,如果MinDistance取0,这些伪边缘附近的背景像素会被直接统计进去,污染背景分量。这时候把MinDistance设成2到3像素,相当于在前景和背景之间留一道"隔离带",只统计离前景有一定距离的背景空间,结果会稳定很多。

我在一个项目里用过一个判断依据,大家也可以参考:把MinDistance设成图像中最小测量特征宽度的1/3到1/2。比如要考察的背景缝隙最小宽度是6像素,那MinDistance取2到3就能避开紧贴边缘的干扰,又不至于把缝隙里的背景空间完全排除。

4.2 参数变化与分量形态的对应关系

调参时最直观的现象是:MaxDistance越大,参与统计的背景带越宽,距离较远的背景空间会被纳入,原本分开的分量可能因为"绕了一圈"而重新连到一起。所以分量数量不一定随MaxDistance增大而单调变化,中间可能先增多,后又减少。

举例来说,一个矩形工件放在黑色背景台上,周围散落着几个小杂物。MinDistance取0时,MaxDistance取15,矩形周围一圈背景被分成4个分量,分别对应四条边外侧的4个方向。当MaxDistance增大到40时,四个角外侧的背景空间彼此连接,4个分量合并成1个大的外围背景块。如果这时候还在用"分量个数等于4"作为判断条件,程序会直接出错。

正因为这种非单调性,我强烈建议在调参时做一个扫描实验:固定MinDistance,让MaxDistance从10逐步变化到100,记录每次的分量数量,画一条曲线。曲线变化趋势能帮你快速发现参数取值区间,也能直观看出哪些参数范围会导致分量数突变,那个突变点往往是语义变化的临界位置。

4.3 与实际像素当量的换算

有一定标定经验的工程师都知道,相同物理尺寸在不同分辨率的相机下对应像素数不同,所以MinDistance和MaxDistance的像素值不能跨系统直接复用。假设标定得到像素当量为0.05mm/pixel,那么10像素就对应0.5mm的物理距离,这组参数在一台0.025mm/pixel的相机上就要改成20像素,才能保持同样的物理统计范围。

如果项目里换了相机或调整了物距,即使代码逻辑完全不变,这些距离参数也要同步换算。我见过有人把这组参数写死在程序里,换了更高清的相机后所有背景判断全部失效,排查了很久才想到是参数没有随分辨率换算引起的。

更稳妥的做法是,在程序初始化阶段根据像素当量和物理长度动态设置MinDistance和MaxDistance。比如希望统计工件外沿0.2mm到1.5mm范围内的背景空间,就先把物理值除以像素当量得到像素值,再传给算子。这样即使相机分辨率调整,代码也不需要改动。

4.4 一个调参实验的记录

用一张包含两个圆形工件的模拟图像做实验,工件直径120像素,两工件最小间距76像素,固定MinDistance为0,逐个改变MaxDistance,背景分量数量的变化如下:

MaxDistance (像素)背景分量数量说明
103两个工件周围被分成多个小块,边界紧贴轮廓
203分量略有合并,但两工件之间的背景缝隙仍独立
384两工件之间的缝隙背景开始拆分,出现新分量
604外围背景连成整体,缝隙分量稳定存在
905外围背景与缝隙背景形状改变,多出独立小块
1202两工件外围背景完全连通,缝隙背景也被覆盖

这个结果印证了前面说的非单调现象。在这个具体场景里,如果项目目标只是判断两个工件是否靠得过近,MaxDistance取38到60之间比较合适,因为此时缝隙背景分量和外围背景分量刚好可以区分开。如果取90以上,缝隙被淹没在外围背景中,判断条件就会失效。

5. 实际项目中踩过与见过的坑

5.1 把背景分量当成前景目标

这个问题在刚接触background_seg的团队里特别常见。因为输出区域和前景区域一样都显示成彩色区域,颜色随机分配,如果不注意区分数据来源,很容易把某个背景分量当成额外探测到的目标,直接污染后续的测量结果。

建议在程序中始终沿用固定的命名规范,比如前景区域统一叫ObjRegion,背景分量统一叫BackRegion,并在关键判断前对区域数量做断言检查。如果项目中有多级处理流程,还可以给背景分量加一个层级标识,避免在函数间传递时混淆。

5.2 前景区域不闭合导致分量爆炸

边缘提取很少能一次性得到完美闭合的轮廓。当工件边缘有一处缺口时,那部分区域的前景边界实际上不封闭,背景带会顺着缺口"漏"到前景区域内部,导致原本应该分别处理的背景分量粘连成一个。分量数量可能从预期中的几个变成几十个。

排查这类问题时,可以用fill_up_holes查看前景区域内部是否有被误填的孔洞,或者通过opening_circle检查前景边界是否存在细长缺口。最省事的修复方法是把dilation_circle的半径适当加大,先把缺口堵上再调用background_seg。但要注意半径加太大会吞掉细小背景特征,需要结合前文的调参逻辑做平衡。

5.3 边缘毛刺对分量的"割裂"效应

有时候前景区域的边界本身不光滑,布满微小锯齿,这些锯齿会把紧贴边界的背景带切割成许多细小的碎块。结果就是背景分量的数量非常多,但大部分是面积只有几个像素的碎片,真正有价值的连通结构反而被淹没在碎片里。

对于这种场景,我建议在构造前景区域时先做一次谨慎的形态学光滑处理。比如用opening_rectangle1配合erosion_circle,或者采用closing_rectangle1做一次闭环操作,把毛刺磨平。对前景区域做光滑处理,比在background_seg输出之后再过滤碎片要有效得多,因为后者只能删掉碎片,无法恢复被碎片破坏的连通关系。

5.4 大图多目标场景下的性能与内存

当整张图像包含数十个目标,且每个目标都要独立计算背景分量时,background_seg的输出区域元组会非常庞大。区域元组可能包含上千个背景分量,这些分量在可视化、统计和存储时都会消耗资源。调用的频次也要注意,如果放在实时检测的主循环里而输入区域又没做好裁剪,图像尺寸一大,帧率会明显下降。

我的做法是先根据目标外接矩形把处理范围裁剪到最小,充分缩小背景统计的物理范围后再调用background_seg。另外,输出结果只保留需要参与判断的分量数量特征和面积特征,不要在内存中长期保存完整的区域元组。等到需要调试分析时再重新生成,用空间的代价换开发效率,其实是划算的。

我自己用background_seg最大的体会是,它强迫我从"前景视角"切换到"背景视角"。很多视觉问题,只看目标本身是找不到答案的,必须观察目标之间的空间关系,而连接背景分量恰好提供了这样一个观察维度。如果你手头项目里正好有类似的需求,不妨先把背景带取出来,画一条参数扫描曲线,再在这个基础上设定判断条件,会比盲目调参数可靠得多。最后分享一个小技巧:在HDevelop里调试时,把BackgroundRegions用set_color给每一个分量轮番赋予不同颜色再叠加到原图上,比单纯看区域窗口直观得多,能快速发现参数是否合适。

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

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

立即咨询