ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Halcon动态阈值dyn_threshold详解:解决光照不均图像分割难题

Halcon动态阈值dyn_threshold详解:解决光照不均图像分割难题 刚从现场回来手头这张图让我想起不少老朋友踩过的坑——一个金属圆柱工件表面车削纹路还没处理干净要检测上面激光打标的字符。打光是单侧条形光图像左半边亮到发白、右半边暗到发灰可字符在肉眼下清清楚楚。丢给threshold之后结果就是亮区字符缺胳膊少腿暗区背景糊成一大片。这种时候老手基本不会纠结全局阈值第一反应就是换成局部阈值试试而Halcon里最常用、最顺手的就是dyn_threshold。这篇文章我打算把dyn_threshold从原理到实战、从参数含义到调优技巧、从优势边界到替代方案全部摊开讲一遍。不管你是刚接触Halcon的新手还是已经在项目里被它折腾过几回的工程师只要手里有光照不均、背景渐变、目标局部有对比度这类图像这篇就能让你少走弯路。我会结合实际项目里验证过的思路和出过问题的细节来讲尽量说人话。1. 光照不均毁掉全局阈值dyn_threshold要解决的现实问题1.1 一个再常见不过的脏图场景在工业视觉现场真正干净的打光环境反而是少数。镜面金属、曲面塑料、带纹理的PCB板只要光源角度、物体形状或者表面材质稍微不配合出来的图像灰度就是带坡度的。比如圆柱面工件光照到曲面两侧的反射强度天然不同又比如大面积塑料壳边缘和中心的漫反射率差异明显。这种图像如果直接走threshold你需要找到一个固定灰度值能把所有前景目标和所有背景区分开。但现实是目标A在亮区灰度值可能是200目标B在暗区灰度值可能只有100而亮区的背景灰度能飙到180。如果固定阈值取150亮区的目标被当成背景滤掉暗区的背景被当成目标捞出来。你拧着阈值调半天要么漏检要么过杀怎么找都找不到那个完美数值。这类问题的本质是背景灰度不是平的。而dyn_threshold的核心思想很朴素既然背景本身就在变化那就先估计出每个位置处的背景灰度再拿原图和背景逐个像素做差分。目标之所以能被看到是因为它比周围背景跳出来一块。这种跳变是局部特性和背景处在什么灰度水平没有关系。1.2 dyn_threshold的适用边界和前提先说清楚它解决什么、不解决什么免得你拿着一幅完全不适合的图像硬套。dyn_threshold适合的场景有三个共同特征背景灰度在空间上变化缓慢比如光照渐变、曲面反光、大面积脏污底色。目标相对局部背景有比较明显的灰度对比哪怕是20个灰度级的差异只要稳定出现就有戏。目标尺寸相对于背景变化尺度较小也就是目标在图像里占的面积不大是局部异常而不是整体结构。反过来如果目标和背景的灰度差异非常小、或者目标本身面积大到覆盖半个视野、又或者图像噪声非常大导致每个像素都在跳变那么dyn_threshold也会力不从心。后面我会专门讲噪声问题这里先记住一个判断标准在怀疑光照或背景不均匀之前先看一眼灰度直方图是否双峰。直方图干干净净双峰直接全局阈值就完事直方图乱成一锅粥才是dyn_threshold的舞台。2. 原理拆解均值背景与逐像素差分比较2.1 数学表达和Halcon实现逻辑dyn_threshold并不是一个独立完成全部计算的魔法算子它的官方定义是这样的dyn_threshold(OrigImage, ThresholdImage : RegionDynThresh : Offset, LightDark)注意它有两个图像参数OrigImage是原始输入图ThresholdImage是阈值图像。也就是说调用dyn_threshold之前你必须自己先把阈值图生成出来。为什么Halcon要这么设计因为背景图像的估计方式有很多种可以用均值滤波、高斯滤波、中值滤波甚至形态学开运算。把阈值图生成交给开发者自己选灵活性更高。但绝大多数项目里最常见的做法就是mean_image先把背景抹平read_image (Image, your_image) mean_image (Image, ImageMean, 15, 15) dyn_threshold (Image, ImageMean, RegionDynThresh, 5, dark)整个计算过程本质上就是在做这样一件事对于每个像素点计算原图灰度值f(x,y)与背景图灰度值g(x,y)的差然后看看这个差是否超过了你设定的偏移量Offset。用数学式表达就是diff(x,y) f(x,y) - g(x,y)如果diff(x,y) Offset且模式是light则该像素进入输出区域。如果diff(x,y) -Offset且模式是dark则该像素进入输出区域。你可以把它理解成背景图给每个像素都当了一次接地参考电平。这样无论这个像素明面上灰度是8还是248只要它比周围背景跳变出足够大的差值就会被捞出来。全局阈值是拿一把固定尺子量所有人局部阈值是给每个人量身定制一把尺子。2.2 LightDark四种取值的实际含义LightDark参数有四个可选值理解它们不要死记硬背只需要记住原图f和背景g的关系就好参数值保留条件典型用途lightf(x,y) g(x,y) Offset提取比背景亮的缺陷、字符、高亮点darkf(x,y) g(x,y) - Offset提取比背景暗的缺陷、刻字、阴影equalg(x,y) - Offset f(x,y) g(x,y) Offset提取和背景灰度接近的均匀区域not_equal|f(x,y) - g(x,y)| Offset同时提取亮和暗的异常区域我实际项目里用得最多的是dark和not_equal。尤其当你对目标究竟偏亮还是偏暗拿不准的时候先用not_equal跑一遍观察输出的区域形态再决定要不要收敛到单侧。注意not_equal有个副作用如果图像边缘有强反光带或者噪声点它们也会被一并捞出来后面形态学处理要跟上。2.3 阈值图不只有mean_image一种生成方式虽然mean_image是最常见的阈值图来源但实践里根据图像特征还有几种选择mean_image均值滤波平滑力度可控速度快对随机颗粒噪声有一定抑制作用。缺点是会保留比较明显的边缘残影尤其是目标边缘对比度很大时背景图会在边缘位置出现灰边。我一般用它处理纹理不太重的表面。median_image中值滤波对椒盐噪声特别有效生成的背景图更干净边界保持方面比均值好一点。代价是计算慢大核尤其明显。gauss_filter高斯滤波平滑更柔和边缘残影相对小适合目标边缘比较细腻的场景。binomial_filter二项式滤波近似高斯但更快Halcon里很多工程用它替代高斯。形态学开运算用gray_opening之类算子生成背景图对暗目标比较友好因为开运算是先腐蚀后膨胀会抹掉比结构元素小的亮细节。选哪种背景生成方式取决于你的目标特征均值滤波稳、中值滤波抗噪、高斯滤波柔。但记住一条经验先用均值滤波把整个流程跑通确认逻辑没问题再去换更优的背景生成方式。不要一开始就在滤波器选择上纠结半天。3. 参数逐项拆解与选型依据3.1 Offset的意义不是绝对阈值而是灰度差很多新手看到Offset这个参数名容易把它等同于全局阈值里的灰度值。完全不是一回事。Offset的单位虽然是灰度级但它衡量的是原图和背景图在每个像素位置上的灰度差下限。举个例子一个像素处原图灰度150背景图灰度142差值是8。如果Offset设为5在light模式下这个像素满足150 142 5被选中。如果Offset设为10则这个像素不满足条件被丢弃。Offset到底该给多大我总结出一个经验区间可以拿去当起点8位灰度图目标和背景的对比度明显明显到肉眼一眼看出Offset从3到8之间开始扫。对比度弱需要仔细盯屏幕才能分辨边界Offset从1到3之间开始扫配合降噪预处理。对比度强、目标边缘锐利Offset给到10到20都可能噪声会被大幅压制。Offset越大提取的区域越少、越干净Offset越小提取的区域越完整、噪声越多。它不是越高越好你用差分图看一眼目标与背景的灰度差分布就能心里有数。后面在调优章节我会具体讲怎么看差分图。3.2 尺度参数与目标尺寸的匹配关系真正决定dyn_threshold成败的关键其实是ThresholdImage生成时用的滤波核尺寸也就是mean_image的MaskWidth和MaskHeight。这个参数比Offset重要得多也最容易翻车。为什么因为背景图要看不见目标只看得见背景。如果滤波核尺寸比目标还小均值滤波会把目标本身的灰度掺进背景估计里导致目标处的背景值被拉向目标值差分结果被削弱甚至变成0目标区域直接消失。如果滤波核尺寸太大背景估计虽然平但对光照变化的跟随能力变弱本来该适应局部背景的现在变成了全局平均值局部阈值的优势就丢了。经验法则是滤波核尺寸至少是目标最大尺寸的2倍以上。比如字符笔画宽度大概5到8个像素mean_image的核用15到25都行如果检测的是划痕长度可能很大但宽度很细这时候核的宽度方向不需要太大重点考虑的是目标在背景里不要被抹平。我踩过最典型的坑是目标尺寸约50像素我图省事直接用15×15的核结果目标边缘出来一圈晕影内部反而是空的——这正是因为滤波核太小目标被背景图吃掉了一部分。后来把核加到100以上问题立刻消失。所以记住这个比例关系核尺寸和目标尺寸是强关联关系两者要一起调。3.3 预处理对dyn_threshold效果的影响dyn_threshold本质上是一个高频细节提取过程它把图像里的局部跳变放大出来。这就带来一个副作用它不懂区分有用的跳变和噪声的跳变。图像上的随机椒盐噪声、传感器热点、细小的表面颗粒全都会被当成候选。所以我在项目里几乎不会直接拿原始图像就跑dyn_threshold而是先做一步轻量级平滑median_image (Image, ImageMedian, circle, 3, mirrored)这个median_image用3×3圆形窗口做中值滤波专门干掉孤立的椒盐噪声同时不会像均值滤波那样把边缘糊掉。预处理这一步的分寸很关键处理太狠目标边缘的对比度也被抹掉Offset必须降得很低否则提不出完整目标处理太轻噪声没压住后面形态学清理会花很多时间。我建议的默认流程是先中值滤波去噪声再均值滤波生成背景图再做dyn_threshold。这个三步组合在大部分工业场景下都稳。4. 实战案例一金属圆柱面字符提取的完整流程4.1 先评估图像确定思路回到开头说的那张金属圆柱面字符图。我拿到图像后第一步不是写代码而是在HDevelop里用灰度直方图工具看一眼整幅图的灰度范围大概从60到220跨度很大。直方图没有明显双峰而是连成一片的宽峰。亮区的字符暗部灰度大约90背景大约180对比度约90个灰度级暗区的字符暗部灰度大约50背景大约110对比度约60个灰度级。这说明全局阈值肯定不靠谱——如果你定阈值在100暗区的字符灰度50被保留但亮区的字符灰度90也会被保留一部分而亮区背景180会被滤掉暗区背景110又会被捞进来。于是字符和背景搅在一起区域乱七八糟。思路就很清晰了字符比背景暗用dark模式背景灰度在空间上变化明显需要生成一张平滑背景图来跟随。4.2 HDevelop代码与参数推导字符笔画宽度我大概量了一下在8到12像素之间。按至少2倍以上的经验mean_image的核我直接从25开始* 1. 读取图像 read_image (Image, metal_cylinder_chars) get_image_size (Image, Width, Height) * 2. 中值滤波去除表面颗粒噪声 median_image (Image, ImageMedian, circle, 3, mirrored) * 3. 生成背景参考图核尺寸取字符宽度的2~3倍 MeanMaskSize : 25 mean_image (ImageMedian, ImageMean, MeanMaskSize, MeanMaskSize) * 4. 局部阈值提取暗字符Offset先给5观察 dyn_threshold (ImageMedian, ImageMean, RegionChars, 5, dark) * 5. 形态学后处理断开噪声点、剔除细碎区域 connection (RegionChars, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, [area, width, height], and, [50, 3, 3], [99999, 200, 200])跑完第四行我在变量窗口里查看RegionChars的显示结果如何字符大致出来了但有几处问题部分字符的边缘有锯齿、局部地方有碎点噪声。这很正常。connection之后用小面积阈值select_shape过滤掉面积小于50的碎块再用宽度高度约束筛掉长条状干扰字符区域就干净多了。4.3 结果后处理与质检标准到这里是不是就算完了还早。dyn_threshold输出的区域是候选区域在真正的检测项目里你还要根据业务要求做最终判定。比如在这个字符检测项目里我的判定标准是提取出的字符区域数量是否等于预期数量每个字符的宽度、高度、面积是否在合理范围如果区域数量对不上说明有漏提或错提就要往回追溯——是Offset太高导致字符断裂还是核尺寸不合适导致字符内部被挖空。一个容易被忽视的细节是字符是凹陷的激光刻字打光角度变化时同一个字符可能在某些区位显示为暗、在某些反光强烈的地方显示为亮。这种情况下单靠dark模式会漏掉反光区域里的字符。我遇到过一次最终方案是在同一个位置分别跑dark和light两个结果取并集再用字符的几何约束去重。代价是多一倍的算力和调参时间但漏检率从3%降到0.1%以内值。5. 实战案例二表面划痕检测中的not_equal用法5.1 划痕检测的逻辑差异字符检测时目标相对背景是稳定的偏向一侧更暗或更亮。但划痕不一样。划痕在图像上的表现取决于缺陷形态和打光方式凹下去的划痕可能成暗线凸起来的划痕磨擦发亮可能成亮线更常见的是同一条划痕在不同段上有明有暗。这就是为什么划痕检测我通常直接用not_equal先不管明暗把所有偏离背景的东西都抓出来再靠形态学筛选。另一个差异是划痕的形状特征细、长、方向不定。这意味着它不像字符那样有明确的外接矩形约束select_shape时主要靠面积、长宽比、长度这些细长目标特征来筛。5.2 代码流程这是一张不锈钢表面的划痕图背景有轻微拉丝纹理划痕较浅对比度大约只有10到20个灰度级。因为背景纹理本身有一定高频成分我先把背景图生成核加大让纹理被平滑掉只保留背景亮度变化* 1. 读取并去噪 read_image (Image, steel_scratch) median_image (Image, ImageMedian, circle, 3, mirrored) * 2. 背景图核加大目标划痕宽度只有2~3像素核用到51 mean_image (ImageMedian, ImageMean, 51, 51) * 3. 明暗同时提取Offset给8 dyn_threshold (ImageMedian, ImageMean, RegionScratchAll, 8, not_equal) * 4. 细碎噪声通过开运算去掉 opening_circle (RegionScratchAll, RegionOpening, 2) * 5. 按面积和长度筛选划痕 connection (RegionOpening, ConnectedScratches) select_shape (ConnectedScratches, FinalScratches, [area, width], and, [100, 5], [99999, 500])这里有个容易翻车的点Offset8对于对比度只有10~20的划痕来说已经偏高了。实际跑下来浅划痕确实有部分断裂。如果是缺陷检测而不是字符识别断裂不一定致命——我可以通过closing把断线接起来或者用区域的骨架化再延展。5.3 结果评估与验收划痕检测的验收比字符检测更难因为没有绝对正确的标准答案。我的做法是拿50张标注过的样本图跑一遍统计检出率、漏检率和误检率。dyn_threshold的参数选择要让漏检率优先降到可接受范围误检靠形态学后处理去清。原因很简单漏检的缺陷流出产线是质量事故误检多几个只是需要人工复检。如果发现漏检主要是浅划痕我优先降低Offset同时加大形态学清理力度如果误检主要是纹理干扰我优先调大核尺寸或者换背景生成方式。多试几轮直到指标满足要求。记住一点dyn_threshold很少一次到位多轮迭代是常态。6. 参数调优的六步法6.1 用差分图先定Offset的大致范围与其盲目扫参数不如先看一眼差分图。Halcon里可以用sub_image把原图和背景图直接相减sub_image (ImageMedian, ImageMean, ImageDiff, 1, 0)ImageDiff就是每个像素的差值图。在HDevelop的灰度直方图工具里打开它观察目标区域的差值集中在什么范围、噪声差值又集中什么范围。比如你看到字符对应的差值峰值在12左右而背景噪声差值的尾巴延伸到6那Offset取7到9就既能保住目标又能压住噪声。这个方法能帮你把调参范围从瞎扫变成有依据的扫描效率提升非常明显。6.2 逐步逼近从粗调到细调我一般按照先结构、后参数、再多图验证的顺序操作确认明暗方向先用not_equal跑一遍看目标被提取出来是亮的还是暗的再决定最后的LightDark。固定Offset为中值先用5试跑确认区域大致形状没跑偏。扫Offset以3为步长从1扫到20观察目标完整性和噪声量的平衡曲线。目标完整度明显下降的那个点附近就是合理取值。再调核尺寸固定Offset后把核尺寸从目标尺寸的1倍扫到4倍。你会看到一个大趋势核太小目标被吃掉核太大光照跟随变差。找到目标区域最饱满且背景干扰最小的区间。定形态学后处理根据残余噪声的尺寸、形状决定用opening_circle去点、closing_circle补洞还是select_shape按面积过滤。多图验证每次参数调整后至少跑10张不同样本图统计稳定性和边缘样本的表现。只看单张图定参数十有八九会在下张图翻车。6.3 亮暗不平衡时的双通道策略如果同一张图像里目标有的区域比背景亮、有的区域比背景暗例如曲面反光造成的复杂光照单用light或dark都会漏检。我的做法是分别跑两次结果做union1合并dyn_threshold (ImageMedian, ImageMean, RegionLight, 5, light) dyn_threshold (ImageMedian, ImageMean, RegionDark, 5, dark) union1 (RegionLight, RegionLightUnion) union1 (RegionDark, RegionDarkUnion) concat_obj (RegionLightUnion, RegionDarkUnion, CombinedRegions) union2 (RegionLightUnion, RegionDarkUnion, RegionCombined)这里的union2是把两个区域的并集求出来。注意两次阈值可以分别用不同的Offset亮目标如果对比度强就可以给大点暗目标如果对比度弱就给小点。灵活性是分开跑最大的好处。7. dyn_threshold与同类局部阈值算子的横向对比7.1 几类常用阈值算子差异Halcon里不止dyn_threshold一个局部阈值方案。我在项目里遇到过几种不同需求用的算子也不太一样。整理成表格方便你快速选型算子阈值来源适用场景主要限制threshold固定灰度值光照均匀、直方图双峰干净光照稍变就翻车dyn_threshold外部提供的背景图常用均值滤波生成 Offset光照不均、背景渐变、目标局部有对比度需要自己配背景图对核尺寸敏感var_threshold局部均值 局部标准差目标对比度随位置变化很大、噪声需要自适应抑制参数更多调试成本高bin_threshold自动计算直方图阈值目标与背景双峰明显、追求快速自动化对光照不均适应能力弱watersheds_threshold分水岭 灰度阈值目标粘连、需要分割的复杂场景计算量大容易过分割我个人的选型逻辑是这样先看光照是否均匀均匀就threshold或bin_threshold不均匀但目标形状简单dyn_threshold是第一选择目标对比度随位置变化特别明显比如同一幅图里同时有高反光区和阴影区var_threshold更容易处理目标粘连要分开计数考虑分水岭方向。7.2 var_threshold值得留意var_threshold和dyn_threshold经常被一起提起。它的阈值不是简单的外部背景图Offset而是基于局部均值mean和局部标准差deviationthreshold mean /- max(StdDevScale * deviation, AbsThreshold)好处是在噪声大的区域局部标准差大阈值自动抬高不容易把噪声捞进来在目标对比度低的区域阈值会自动降低保证浅目标也能被捕获。坏处是参数更多调起来费劲。如果你手里的图像噪声分布很不均匀var_threshold可能是比dyn_threshold更稳的选择但它学习成本更高建议先把dyn_threshold玩熟再碰它。7.3 组合使用不同算子互补实际项目中我经常把几个算子组合着用。比如先用threshold把大面积的高亮背景干掉省得dyn_threshold在高亮区浪费算力或者先用dyn_threshold提取候选区域再用binary_threshold在候选区域内做二次细分。Halcon的好处是区域之间可以灵活做交集、并集、差集多算子配合往往比单算子硬扛效果好得多。8. 我在实际项目中踩过的三个坑8.1 核尺寸和目标尺寸的匹配坑这是我和dyn_threshold磨合期翻得最狠的一次。当时检测的是PCB板上的焊盘缺陷焊盘尺寸约30×40像素我图省事用了15×15的核。结果输出区域里焊盘边缘出现一圈边框焊盘内部全是空的像空心方块。原因前面讲过核尺寸比目标小均值滤波把目标本身的灰度也算进了背景估计导致目标中心区域的像素和背景图差异极小被判定为背景只有目标边缘处因为和外部背景的差值大才被捞出来。把核加到80×80之后焊盘内部的差分值恢复了区域立刻变完整。教训就是背景图必须不认识目标。判断标准很简单生成背景图后显示出来看一眼如果你能看到目标的轮廓残影说明核太小需要加大。这是最快、最直观的检验方式。8.2 噪声被放大Offset再大也压不住有一个阶段我在做薄膜表面缺陷检测薄膜本身有细微的材质纹理图像信噪比不高。用dyn_threshold跑出来整个候选区域密密麻麻全是点状噪声我把Offset从5一路加到30噪声少了但真正的浅缺陷也没了陷入两难。后来解决的思路不是继续加Offset而是回到预处理先做3×3中值滤波去掉孤立热点再做一次轻量高斯平滑把纹理压平然后生成背景图时才用更大的核。噪声少了Offset甚至还能降一点浅缺陷反而提出来了。这让我意识到dyn_threshold不是噪声过滤器把噪声挡在入口处比在出口处清理有效得多。8.3 边缘反光带和阴影误检有一次检测金属件表面的黑点缺陷。金属件边缘有一道因为倒角产生的强反光线这道反光线比周围背景亮很多。我用not_equal跑了一遍输出区域里除了黑点金属件边缘整整一圈都被当成目标框出来了。边缘区域会形成一段连续的高响应带形态学过滤也去不掉。最后我的处理方式是先用一个稍微扩大的ROI把边缘区域排除在外只让dyn_threshold在有效检测区内工作。或者你也可以针对边缘带单独建一个掩膜。这个方法虽然粗暴但很实用——与其在算法层面掉头解决边缘问题不如在ROI设计层面绕开它。ROI设计本来就是视觉项目里最被低估的一环。还有一个衍生坑如果无ROI约束时误检特别集中在图像的四条边很可能和mean_image的边界填充方式有关。Halcon默认的边界处理是镜像填充但在图像边界附近滤波器采样区域不足背景图会出现异常边界值导致差分值假性偏大。这种情况下把ROI往内缩5到10个像素通常就能消除边界误检。最后分享一个我一直在用的小技巧无论调哪个参数都不要在HDevelop里反复手改数字。用for循环把Offset或核尺寸做成变量一次性跑完多个参数组合把每组的输出区域面积、数量、噪声占比记录下来一目了然地选趋势。这样比你盯着屏幕一张张试快得多也更不容易漏掉中间的最优解。dyn_threshold这个算子上限很高但它的脾气主要在这几个参数上摸透了光照不均的图就不再是难题了。
返回列表