1. 从一张“鼓包”的金属片说起:凸起缺陷到底难在哪
做过表面缺陷检测的人大概都有体会:划痕、脏污、缺料这类“减法型”缺陷,用阈值分割加形态学基本能搞定;可一旦遇到“加法型”的凸起——比如冲压件上多出来的一小块鼓包、注塑件表面的顶白凸点、金属箔材上的局部隆起——很多人第一反应是“这还不简单,灰度值高的地方就是凸起”。真上手才发现,事情远没有这么直接。
凸起缺陷的核心难点在于:它往往没有明显的灰度跳变边界。一个缓坡状的鼓包,从正常区域过渡到最高点,灰度是渐变的,你拿全局阈值去卡,要么卡不出来,要么把正常的高光区域一起框进去。更麻烦的是,很多工件的正常表面本身就存在反光、纹理、弧度,凸起缺陷的信号很容易被这些“合法”的灰度变化淹没。
这篇内容就是围绕“区域凸起缺陷检测”这个具体场景展开的。我会把 Halcon 里做这类检测的完整思路拆开讲:从怎么把凸起信号从背景里“抠”出来,到怎么用频域和形态学手段放大它,再到怎么稳定地判定和输出结果。适合已经会用 Halcon 做基础 blob 分析、但一遇到低对比度凸起就抓瞎的视觉工程师,也适合想理解“为什么频域方法能抓凸起”的进阶读者。
先说一个我踩过的坑:早期做某冲压件凸起检测,我直接用threshold卡高灰度区域,结果工件边缘的倒角反光全部被误判成凸起,误报率一度超过 30%。后来才明白,凸起检测的关键不在于“找亮的地方”,而在于“找局部比周围鼓起来的地方”——这是一个相对概念,不是绝对灰度概念。这个认知转变,是整篇文章的起点。
2. 凸起缺陷的信号本质:为什么灰度阈值会失效
2.1 凸起在图像里到底长什么样
要抓凸起,先得搞清楚它在相机里成像成什么样。假设一个理想漫反射表面,局部隆起一个半球形鼓包,在均匀环形光照射下,鼓包顶部因为法线朝向光源,接收到的光强最大,成像最亮;鼓包边缘法线逐渐偏离,亮度平滑下降,直到和周围平面区域一致。所以一个凸起在图像上的典型表现是:中心亮、边缘渐变、没有锐利边界。
这和划痕完全不同。划痕是凹下去的,边缘会有阴影,灰度跳变明显,阈值一卡一个准。凸起是“缓坡”,它的空间频率低、对比度低,用一阶微分(比如sobel_amp)去增强,响应也很弱。这就是为什么很多人用边缘检测做凸起,结果什么都检不出来。
2.2 全局阈值为什么必然误报
全局阈值的逻辑是“灰度大于 T 的像素就是缺陷”。但工件上合法的亮区域太多了:倒角、圆角、加工纹路的高点、甚至光源本身的亮度不均,都会产生高灰度像素。你把 T 调高,凸起检不出来;调低,正常区域全进来。本质上,全局阈值丢失了“空间上下文”——它不知道一个亮像素是孤立鼓包,还是大面积正常高光的一部分。
我做过一个统计:在某类金属件上,正常高光区域的灰度峰值能达到 200 以上,而凸起缺陷中心灰度可能只有 160。也就是说,凸起缺陷的绝对灰度甚至可能低于正常区域。这种情况下,任何基于绝对灰度的阈值方法都是死路。
2.3 凸起检测真正该用的信号:局部对比度与空间频率
既然绝对灰度靠不住,就得换信号。凸起有两个可利用的物理特征:
第一,局部对比度。凸起中心虽然绝对灰度不一定最高,但它一定比“紧邻的周围”亮。用一个大核均值滤波得到背景估计,再用原图减去背景,凸起就会变成一个正值区域,而大面积正常高光因为“周围也亮”,相减后接近零。这就是dyn_threshold或背景减除的思路。
第二,空间频率。凸起是一个低频的、平滑的隆起,它的能量集中在低频段。而纹理、噪声这些干扰集中在高频段。通过频域滤波(比如fft_image加低通或带通),可以把凸起的低频信号单独提取出来,同时压制高频纹理。这是频域方法抓凸起的理论依据。
下面这张表对比了几种常见信号提取方式在凸起检测上的表现,是我在实际项目中反复验证过的:
| 方法 | 原理 | 对凸起的响应 | 主要问题 |
|---|---|---|---|
| 全局阈值 | 绝对灰度 | 弱,易漏 | 正常高光误报严重 |
| 边缘检测 | 一阶微分 | 很弱 | 缓坡无锐边,检不出 |
| 背景减除 | 局部对比度 | 强 | 核尺寸选择敏感 |
| 频域带通 | 空间频率 | 强 | 参数调试门槛高 |
| 形态学顶帽 | 局部起伏 | 中强 | 对核形状敏感 |
3. 用背景减除把凸起从缓坡里“抬”出来
3.1 背景估计的核心:核尺寸怎么定
背景减除的思路很朴素:用一个足够大的均值或中值滤波核把图像“糊”一遍,得到一张只保留大尺度亮度趋势的背景图,然后用原图减背景图,剩下的就是局部起伏。凸起作为局部起伏,会在差值图里变成一个明显的亮斑。
关键在于核尺寸。核太小,背景图会把凸起本身也“糊”进去,相减后凸起消失;核太大,背景图跟不上光照的缓慢变化,相减后会出现大面积伪影。我的经验法则是:核直径取凸起预期直径的 3 到 5 倍。比如你预期凸起直径在 20 像素左右,核直径就取 60 到 100 像素。
在 Halcon 里可以这样写:
* 读取图像 read_image (Image, 'workpiece.png') get_image_size (Image, Width, Height) * 背景估计:大核均值滤波 * 核直径取凸起预期直径的4倍左右 mean_image (Image, Background, 81, 81) * 背景减除,得到局部起伏图 sub_image (Image, Background, Diff, 1, 128)这里sub_image的第三个参数是缩放因子,第四个是灰度偏移。加 128 是为了让差值图落在可视范围内,方便后续处理。实际判定时,我们关心的是差值大于零的部分。
3.2 中值滤波 vs 均值滤波:什么时候用哪个
均值滤波做背景估计,优点是快,缺点是对异常值敏感——如果背景里恰好有个大凸起,均值会被拉高,导致该区域背景估计偏亮,相减后凸起被削弱。中值滤波对异常值鲁棒,但速度慢,而且核很大时计算量惊人。
我的选择逻辑是:凸起稀疏且尺寸远小于核时,用中值;凸起密集或对速度敏感时,用均值。还有一个折中方案:先用小核中值去掉噪声,再用大核均值做背景估计。这样兼顾鲁棒性和速度。
* 先去噪 median_image (Image, ImageMed, 'circle', 3, 'mirrored') * 再大核均值做背景 mean_image (ImageMed, Background, 81, 81) sub_image (ImageMed, Background, Diff, 1, 128)3.3 差值图的后处理:从亮斑到缺陷区域
差值图出来后,凸起表现为一个亮斑,但边缘是渐变的,直接阈值分割得到的区域可能偏小或偏大。这时候需要做几件事:
第一,用threshold卡一个相对低的阈值,把凸起的“裙边”也包含进来,保证区域完整。第二,用connection断开连通域,再用select_shape按面积和圆度筛选。第三,用dilation_circle做适度膨胀,把边缘补回来。
* 阈值分割,阈值取差值图均值的1.5倍左右 threshold (Diff, Region, 140, 255) connection (Region, ConnectedRegions) * 按面积筛选,面积下限根据实际凸起最小尺寸定 select_shape (ConnectedRegions, Defects, 'area', 'and', 50, 5000) * 圆度筛选,凸起通常比较圆 select_shape (Defects, Defects, 'circularity', 'and', 0.6, 1.0)这里有个细节:threshold的阈值不能拍脑袋定。我通常先统计差值图的灰度直方图,找到背景噪声的上限,然后取“噪声上限 + 3 到 5 个灰度级”作为阈值。这样能保证在噪声波动下不误报。
注意:背景减除法对光照均匀性有一定要求。如果光源本身明暗差异很大,背景图会残留光照梯度,相减后出现大面积伪影。这种情况下要先做光照校正,或者改用频域方法。
4. 频域带通:把低频凸起从高频纹理里分离
4.1 为什么频域能抓凸起
前面说过,凸起是低频信号,纹理和噪声是高频信号。频域滤波的本质就是“按频率选择性放行”。把图像做傅里叶变换到频域,低频能量集中在频谱中心,高频能量分布在四周。用一个环形带通滤波器,只保留凸起对应的频段,再逆变换回空域,凸起就被单独提取出来了。
这个方法的好处是:它不依赖局部对比度,而是依赖空间尺度。哪怕凸起对比度很低,只要它的空间尺度落在带通范围内,就能被提取。对于背景减除搞不定的“大面积缓坡凸起”,频域方法往往有奇效。
4.2 带通滤波器的参数怎么算
频域滤波的核心参数是带通的中心频率和带宽。中心频率对应凸起的空间尺度:凸起越大,中心频率越低。具体计算方式是:假设凸起直径约 D 像素,图像尺寸为 N×N,那么凸起对应的归一化频率约为 N/D。比如图像 1024×1024,凸起直径 50 像素,中心频率就在 1024/50 ≈ 20 左右。
带宽则决定了提取的“宽容度”。带宽太窄,凸起尺寸稍有变化就漏检;带宽太宽,高频纹理又进来了。我的经验是带宽取中心频率的 0.5 到 1 倍。
在 Halcon 里,频域滤波可以这样实现:
* 傅里叶变换 fft_image (Image, ImageFFT) * 生成带通滤波器 * 中心频率约20,带宽约15 gen_bandpass (ImageBandpass, 20, 15, 'none', 'rft', Width, Height) * 频域滤波 convol_fft (ImageFFT, ImageBandpass, ImageConvol) * 逆变换 fft_image_inv (ImageConvol, ImageFiltered)gen_bandpass的参数需要根据实际图像尺寸和凸起尺度调整。这里给的是归一化频率,实际使用时 Halcon 会自动按图像尺寸换算。
4.3 频域方法的坑:振铃效应与边界处理
频域滤波最烦人的问题是振铃效应。理想带通滤波器在频域是矩形窗,逆变换后在空域会产生 sinc 函数形状的旁瓣,表现为凸起周围出现一圈圈明暗条纹。这些条纹会被后续阈值分割误判成缺陷。
解决办法有两个:一是用高斯带通代替理想带通,gen_bandpass的滤波类型参数选'gauss'而不是'none',高斯窗在频域平滑过渡,空域旁瓣小得多。二是对图像做边界扩展,比如用mirror或continue方式填充边界,减少边界截断带来的频谱泄漏。
* 用高斯带通,减少振铃 gen_bandpass (ImageBandpass, 20, 15, 'gauss', 'rft', Width, Height)实测下来,高斯带通的振铃抑制效果比理想带通好一个数量级,代价是凸起边缘稍微模糊一点,但对检测判定影响不大。
5. 形态学顶帽:一个被低估的凸起检测利器
5.1 顶帽变换的物理含义
形态学顶帽变换(top-hat)的定义是:原图减去开运算结果。开运算是先腐蚀后膨胀,它会抹掉比结构元小的亮区域。所以原图减开运算,剩下的就是“比结构元小的亮区域”——这正好就是凸起的特征。
顶帽变换抓凸起的逻辑和背景减除很像,都是“去掉大尺度背景,保留小尺度起伏”。区别在于:背景减除用的是线性滤波,顶帽用的是非线性形态学。对于边缘陡峭的凸起,顶帽响应更锐利;对于缓坡凸起,背景减除更平滑。
5.2 结构元的选择:圆还是矩形
顶帽变换的结构元形状决定了它对什么形状的凸起敏感。圆形结构元对各向同性的凸起响应均匀,适合检测圆形鼓包;矩形结构元对特定方向的凸起更敏感,适合检测条状隆起。
结构元尺寸的选择原则和背景减除的核尺寸类似:略大于凸起预期尺寸。如果结构元比凸起小,开运算会把凸起也腐蚀掉,顶帽响应消失;如果结构元太大,正常的小尺度纹理也会被保留,噪声增加。
* 圆形结构元,半径取凸起预期半径的1.5倍 gen_circle (StructElement, 15, 15, 20) * 顶帽变换 top_hat (Image, StructElement, TopHat) * 后续阈值分割 threshold (TopHat, Region, 30, 255)5.3 顶帽与背景减除的实测对比
我在同一组金属件图像上对比过两种方法。对于直径 30 到 50 像素、对比度中等的凸起,两者检出率都在 95% 以上,差异不大。但对于直径超过 80 像素的大缓坡凸起,背景减除的检出率明显更高,因为大核均值能更好地拟合缓坡背景;而顶帽需要很大的结构元,计算量剧增且容易引入噪声。
反过来,对于直径小于 15 像素的小凸点,顶帽的响应更干净,因为小结构元的开运算能精确抹掉小凸点,而背景减除的小核会把凸点本身也糊进去。
所以我的建议是:小凸点用顶帽,大缓坡用背景减除,中等尺寸两者都试,看哪个误报低。
6. 多方法融合与判定逻辑:怎么把误报压到最低
6.1 单一方法的局限
前面讲的三种方法——背景减除、频域带通、形态学顶帽——各有各的适用场景,也各有各的误报来源。背景减除怕光照不均,频域怕纹理周期,顶帽怕结构元尺寸不匹配。在实际产线上,单一方法很难同时做到高检出和低误报。
我做过一个统计:在某类冲压件上,背景减除的检出率 96%,误报率 8%;频域带通的检出率 92%,误报率 5%;顶帽的检出率 90%,误报率 6%。单独用任何一个,误报率都偏高。
6.2 融合策略:与运算还是或运算
融合的基本思路是“取长补短”。有两种融合逻辑:
第一种是与运算(AND):只有两种方法都判定为缺陷的区域才算缺陷。这能大幅降低误报,但会牺牲检出率——如果某个凸起只有一种方法能检出,就会被漏掉。
第二种是或运算(OR):任一方法判定为缺陷就算缺陷。这能提高检出率,但误报会增加。
我的经验是:先用或运算保证检出,再用形状和灰度特征做二次筛选压误报。具体做法是:把三种方法的缺陷区域合并,然后用select_shape按面积、圆度、凸度筛选,再用intersection看区域重叠度——真正凸起往往在多种方法下都有响应,重叠度高;而噪声引起的误报通常只在一种方法下出现,重叠度低。
* 三种方法分别得到 Defects1, Defects2, Defects3 * 合并 union2 (Defects1, Defects2, UnionTemp) union2 (UnionTemp, Defects3, AllDefects) connection (AllDefects, AllConnected) * 按面积和圆度筛选 select_shape (AllConnected, Candidates, ['area','circularity'], 'and', [50,0.5], [5000,1.0]) * 计算每个候选区域在三种方法中的重叠次数 * 重叠次数>=2的保留6.3 判定阈值的动态调整
固定阈值在产线换型时往往需要重新调参,很麻烦。我的做法是引入动态阈值:先统计一张“已知合格品”图像的缺陷响应分布,取分布的 99.7% 分位数作为阈值上限。这样阈值会随工件批次自动适应,换型时只需要换一张合格品参考图。
* 用合格品图像统计背景响应 * 假设 Diff 是合格品的差值图 * 计算均值和标准差 intensity (Diff, Diff, Mean, Deviation) * 动态阈值 = 均值 + 3倍标准差 DynamicThreshold := Mean + 3 * Deviation threshold (Diff, Region, DynamicThreshold, 255)这个方法在光照缓慢变化的产线上特别管用,能把因光照漂移引起的误报压下去一大半。
7. 实操中那些文档不会写的细节
7.1 相机与光源的配合比算法更重要
我见过太多人把精力全花在算法调参上,却忽略了成像质量。凸起检测对光源尤其敏感。环形光适合检测各向同性的鼓包,因为环形光从各个方向照射,凸起顶部接收的光强最大,边缘均匀衰减。条形光适合检测特定方向的条状隆起,因为条形光的方向性强,能突出某一方向的坡度。
相机方面,分辨率要保证凸起在图像上至少占 15 到 20 像素。低于这个尺寸,凸起的缓坡特征就退化成几个像素的灰度变化,任何算法都无力回天。镜头畸变要小,因为畸变会引入额外的灰度梯度,干扰背景估计。
7.2 标定与 ROI 的取舍
全图处理还是 ROI 处理,这是个实际问题。全图处理速度快,但容易受无关区域干扰;ROI 处理针对性强,但需要额外的定位步骤。我的建议是:如果工件位置稳定,用固定 ROI;如果位置有波动,先用模板匹配定位,再在定位后的坐标系里取 ROI。
ROI 的边界要避开工件边缘和倒角,因为这些地方的灰度梯度天然很大,背景减除后会产生强响应。我通常把 ROI 向内收缩 10 到 20 像素,把边缘区域排除在外。
7.3 参数调试的顺序与记录
调参最忌讳东调一下西调一下。我的顺序是:先调成像(光源、曝光、对焦),再调预处理(去噪、背景估计),再调分割阈值,最后调筛选条件。每一步调完,用同一组测试图验证,记录检出率和误报率。这样出问题时能快速定位是哪一步的参数变了。
建议建一个参数记录表,把每组参数对应的检出率、误报率、处理时间都记下来。换型时直接查表,比重新调参快得多。
| 参数组 | 背景核 | 阈值 | 面积下限 | 检出率 | 误报率 | 耗时 |
|---|---|---|---|---|---|---|
| A | 61 | 135 | 50 | 94% | 12% | 45ms |
| B | 81 | 140 | 50 | 96% | 8% | 52ms |
| C | 101 | 145 | 80 | 93% | 4% | 60ms |
7.4 处理速度的优化
Halcon 的处理速度优化有几个实用技巧:一是用reduce_domain把处理范围限制在 ROI 内,能省掉大量无关计算;二是背景估计用mean_image而不是median_image,前者快很多;三是频域方法只在必要时用,因为 FFT 本身开销不小;四是把select_shape的筛选条件按“先严后宽”排列,先用面积快速筛掉大部分,再用圆度精筛。
如果产线节拍要求高,可以考虑把背景估计的核尺寸减小,或者用binomial_filter近似高斯滤波,速度比mean_image还快。
8. 一个完整的检测流程与代码骨架
把前面的内容串起来,一个可落地的凸起检测流程大致是这样的:
* 1. 读取图像并定位 read_image (Image, 'workpiece.png') * 模板匹配定位(略) * 2. 取ROI reduce_domain (Image, ROI, ImageROI) * 3. 去噪 median_image (ImageROI, ImageMed, 'circle', 3, 'mirrored') * 4. 背景估计与减除 mean_image (ImageMed, Background, 81, 81) sub_image (ImageMed, Background, Diff, 1, 128) * 5. 动态阈值 intensity (Diff, Diff, Mean, Deviation) threshold (Diff, Region, Mean + 3*Deviation, 255) * 6. 形态学后处理 connection (Region, ConnectedRegions) select_shape (ConnectedRegions, Defects, 'area', 'and', 50, 5000) select_shape (Defects, Defects, 'circularity', 'and', 0.5, 1.0) * 7. 频域辅助验证(可选) fft_image (ImageMed, ImageFFT) gen_bandpass (ImageBandpass, 20, 15, 'gauss', 'rft', Width, Height) convol_fft (ImageFFT, ImageBandpass, ImageConvol) fft_image_inv (ImageConvol, ImageFiltered) threshold (ImageFiltered, FreqRegion, 20, 255) * 8. 融合判定 intersection (Defects, FreqRegion, FinalDefects) * 9. 输出结果 count_obj (FinalDefects, Number) if (Number > 0) * 有缺陷,触发报警 endif这个骨架不是死的,实际项目里要根据工件特点增删步骤。比如光照很均匀的场合,可以省掉频域验证;凸起形状很规则的场合,可以加强形状筛选。
最后分享一个我自己的体会:凸起检测的调参过程,本质上是在“检出率”和“误报率”之间找平衡点。没有哪个参数能让两者同时最优,关键是搞清楚你的产线能容忍多少误报、不能容忍多少漏检。如果漏检代价高,就把阈值调低、融合用或运算;如果误报代价高,就反过来。这个取舍没有标准答案,只有适合当前场景的答案。