1. 区域凸起缺陷检测到底在解决什么问题
1.1 从一条产线投诉说起
做过工业视觉的人大概都遇到过类似的场景:产线上某个冲压件、注塑件或者金属壳体,表面偶尔会出现一些不规则的凸起。这种凸起不像划痕那样有明确的线性特征,也不像脏污那样可以通过灰度阈值一刀切掉。它往往和背景材质颜色接近,边缘过渡平缓,用常规的阈值分割或者边缘检测去抓,要么漏检,要么把正常纹理也一起框进来。
我之前接触过一个模拟项目,做的是某类金属薄片表面的质量检测。产品本身有拉丝纹理,表面还分布着一些工艺允许的微小起伏。客户反馈说,偶尔会有凸起缺陷流到下游,装配时导致贴合不严。他们试过用灰度阈值加形态学,效果很不稳定,白天和晚上的检测结果都能差出一截。后来换成基于区域形态的凸起检测思路,才把这个问题压下去。
这就是“区域凸起缺陷检测”要解决的核心问题:在复杂背景纹理下,稳定地找出那些局部隆起、与周围区域存在高度或形态差异的缺陷区域。它不追求检测所有类型的缺陷,而是专门针对“凸起”这一类形态特征做文章。
1.2 适合谁来参考这套方法
这套方法适合几类人:一是刚接触Halcon、正在做表面缺陷检测的视觉工程师;二是产线设备调试人员,需要快速判断某个凸起缺陷能不能用视觉方案覆盖;三是做算法方案选型的技术负责人,想了解区域形态学方法在实际项目中的边界在哪里。
它不需要你有很深的深度学习背景,但需要你对Halcon的基础算子有一定了解,比如阈值分割、形态学处理、区域特征提取这些。如果你之前只用过Blob分析做简单的有无检测,那这篇文章正好可以帮你把思路从“像素级判断”升级到“区域级形态分析”。
1.3 为什么不用深度学习
这里得说清楚一个选型逻辑。深度学习做缺陷检测确实强,尤其是面对纹理复杂、缺陷样本多的场景。但凸起缺陷有个特点:它的形态特征相对明确,而且往往可以通过光学打光手段强化出来。如果打光到位,凸起区域在图像上会呈现出与周围不同的灰度分布或者阴影特征,这时候用传统区域形态学方法,速度快、可解释性强、调试周期短。
另一个现实原因是样本数量。很多中小批量产线,凸起缺陷的样本可能就几十张,甚至十几张。拿这个量去训练深度学习模型,过拟合风险很高,而且换一个产品型号就得重新标注、重新训练。区域形态学方法的好处是,参数调好后,换产品只需要微调阈值和结构元素尺寸,迁移成本低得多。
当然,这不是说深度学习没用。如果凸起缺陷形态差异极大、背景纹理极其复杂、样本又充足,那深度学习仍然是首选。但在相当一部分工业场景里,区域凸起检测这套传统方法依然能打,而且打得很稳。
2. 核心思路拆解:从灰度到区域的形态学路径
2.1 凸起在图像上到底长什么样
要检测凸起,先得理解凸起在图像里的表现形式。假设我们用低角度环形光或者侧向光去打一个金属表面,正常平整区域的反光是比较均匀的,而凸起区域因为表面法线方向发生变化,会在某一侧形成亮斑,另一侧形成暗影。这个亮暗分布就是凸起在图像上的“指纹”。
但问题在于,这个指纹不是固定的。凸起的高度、曲率、材质反射特性不同,亮暗的对比度和分布范围都会变。更麻烦的是,产品本身的纹理、加工痕迹、甚至灰尘,也可能产生类似的局部灰度变化。所以,单纯靠灰度阈值去抓亮斑或暗斑,误检率会很高。
区域凸起检测的思路不是直接找亮斑,而是先通过形态学操作把凸起区域从背景中“分离”出来,再利用区域特征做筛选。这个分离过程,核心用到的就是灰度形态学中的顶帽变换或者底帽变换。
2.2 顶帽变换为什么能突出凸起
顶帽变换的数学定义是:原图减去开运算结果。开运算本身是先腐蚀后膨胀,它的作用是去掉比结构元素小的亮细节,保留大的亮区域。所以原图减去开运算结果,剩下的就是那些被去掉的、比结构元素小的亮细节。
对于凸起缺陷来说,如果凸起在图像上表现为局部亮斑,而且这个亮斑的尺寸小于我们设定的结构元素尺寸,那么顶帽变换就能把它单独提取出来。结构元素选多大,取决于你预期凸起的最大尺寸。一般来说,结构元素的直径要略大于凸起在图像上的投影尺寸,这样才能保证凸起被完整地“顶”出来,而不是被开运算保留下来。
底帽变换则相反,它是闭运算结果减去原图,用来提取比结构元素小的暗细节。如果凸起在图像上表现为暗影,那就用底帽变换。实际项目中,我通常会先看一眼凸起在图像上的灰度表现,再决定用顶帽还是底帽,或者两个都用然后合并结果。
2.3 区域形态学在其中的角色
灰度形态学处理完之后,我们得到的是经过增强的图像,凸起区域和背景的对比度被拉大了。接下来要做的是把这个增强后的图像转换成区域,也就是做分割。这时候阈值分割就派上用场了,但阈值怎么选,不能拍脑袋。
我的习惯是先用Halcon的灰度直方图工具看一下增强后图像的灰度分布。如果凸起区域和背景形成了明显的双峰,那阈值就取双峰之间的谷底。如果分布没那么理想,那就用动态阈值或者局部阈值,比如dyn_threshold,用一个参考图和偏移量来做分割。分割完之后,会得到一堆候选区域,这里面既有真正的凸起,也有噪声和纹理干扰。
接下来就是区域形态学的筛选环节。常用的手段包括:按面积筛选,去掉太小和太大的区域;按圆度或矩形度筛选,去掉形状明显不对的;按区域内的灰度均值或方差筛选,去掉对比度不够的。这一步的参数没有固定值,得根据实际图像去调,但逻辑是固定的:用凸起的几何和灰度特征,把候选区域里的“李鬼”筛掉。
3. 实操流程:从图像采集到缺陷输出
3.1 图像采集与打光的关键控制点
在动手写代码之前,打光方案必须先定下来。区域凸起检测对打光的要求比一般缺陷检测更高,因为凸起的灰度表现直接决定了后续形态学处理的效果。
我试过几种打光方式,效果差异很大。环形光正面打,凸起区域和背景的对比度最弱,因为光线均匀反射,凸起的高度差体现不出来。低角度侧向光效果最好,光线从侧面掠射,凸起的一侧会形成明显阴影,另一侧形成亮边,对比度能拉高好几倍。如果是高反光金属表面,还可以考虑加偏振片,把镜面反射压下去,让凸起的漫反射特征更突出。
相机镜头的选择也有讲究。视场要覆盖整个检测区域,但分辨率不能太低。凸起缺陷的尺寸如果只有零点几毫米,那像素当量至少得做到零点零几毫米每像素,否则凸起在图像上就几个像素,形态学处理根本没法做。我一般会按“凸起最小尺寸在图像上至少占5到10个像素”这个标准来反推镜头和相机选型。
提示:打光方案定下来之后,不要急着写算法。先连续采集几十张正常品和缺陷品的图像,用Halcon的灰度剖面工具看一下凸起区域和背景的灰度差异到底有多大。如果差异小于20个灰度级,那打光还得再调,否则后面算法再优化也事倍功半。
3.2 灰度形态学参数的计算与选择
假设我们已经采集到了一张灰度图像,凸起表现为局部亮斑。第一步是做顶帽变换。Halcon里对应的算子组合是:
read_image (Image, 'sample.png') gen_rectangle1 (ROI, 100, 100, 500, 500) reduce_domain (Image, ROI, ImageReduced) gray_opening_rect (ImageReduced, ImageOpening, 15, 15) sub_image (ImageReduced, ImageOpening, ImageTopHat, 1, 0)这里的关键参数是结构元素的尺寸。gray_opening_rect里的15和15,代表矩形结构元素的宽和高。这个尺寸怎么定?我的经验是:结构元素尺寸取凸起在图像上最大投影尺寸的1.5到2倍。比如凸起在图像上最大宽度大概是10个像素,那结构元素就取15到20。太小了,凸起会被开运算保留下来,顶帽变换就提不出来;太大了,背景的缓慢灰度变化也会被当成细节提出来,引入噪声。
底帽变换的写法类似,只是用gray_closing_rect然后做减法:
gray_closing_rect (ImageReduced, ImageClosing, 15, 15) sub_image (ImageClosing, ImageReduced, ImageBottomHat, 1, 0)如果凸起既有亮斑又有暗影,可以把顶帽和底帽的结果合并:
add_image (ImageTopHat, ImageBottomHat, ImageCombined, 1, 0)合并之后,凸起区域的对比度会进一步增强,但噪声也会叠加。所以合并之后通常需要做一次平滑,比如用gauss_filter或者median_filter,把孤立的噪声点压下去。
3.3 区域分割与形态学筛选的完整链路
增强后的图像接下来要做分割。我一般先用threshold做一个全局阈值,看看效果。如果背景纹理比较干净,全局阈值就够了。如果背景有缓慢的灰度变化,那就用dyn_threshold:
mean_image (ImageCombined, ImageMean, 31, 31) dyn_threshold (ImageCombined, ImageMean, RegionDyn, 8, 'light')这里的31, 31是均值滤波的窗口大小,8是偏移量。窗口大小决定了参考图的平滑程度,偏移量决定了分割的灵敏度。偏移量越小,分割越敏感,但噪声也越多。我通常从10开始试,根据误检情况上下调整。
分割完之后,用connection把区域断开成独立连通域,然后开始筛选。筛选的维度我一般按这个顺序来:
| 筛选维度 | 对应算子 | 典型参数范围 | 筛选目的 |
|---|---|---|---|
| 面积 | select_shape | 50到5000像素 | 去掉噪声点和过大区域 |
| 圆度 | select_shape | 0.3到1.0 | 去掉细长划痕和纹理 |
| 矩形度 | select_shape | 0.5到1.0 | 去掉形状不规则的干扰 |
| 灰度均值 | intensity | 根据实际图像定 | 去掉对比度不足的区域 |
| 灰度方差 | intensity | 根据实际图像定 | 去掉灰度均匀的伪缺陷 |
面积筛选是最先做的,因为噪声点通常面积很小,先去掉能减少后续计算量。圆度和矩形度用来区分凸起和划痕,凸起一般比较紧凑,圆度较高,而划痕是细长的,圆度很低。灰度均值和方差用来做最后一道把关,确保留下来的区域确实有足够的灰度对比。
筛选完之后,如果还有零星误检,可以用opening_circle或者closing_circle做一次形态学开闭运算,把区域边缘修整一下,去掉毛刺。最后用area_center或者smallest_rectangle1输出缺陷的位置和尺寸信息。
4. 常见问题与排查技巧实录
4.1 为什么凸起区域总是断成好几块
这是新手最容易遇到的问题。顶帽变换之后,凸起区域可能不是完整的一块,而是断断续续的几块。原因通常有两个:一是凸起本身的灰度不均匀,中间有暗区;二是结构元素尺寸选得不对,导致凸起被部分保留、部分提取。
解决办法分两步走。先检查结构元素尺寸,按前面说的1.5到2倍原则重新算一遍。如果尺寸没问题,那就对顶帽变换后的图像做一次闭运算,把断裂的区域连起来:
closing_circle (RegionDyn, RegionClosed, 3.5)这里的3.5是闭运算的圆半径,取值要略大于断裂间隙的一半。如果断裂间隙有10个像素,那半径就取6左右。闭运算之后再做连通域分析,通常就能得到完整的凸起区域了。
4.2 背景纹理被误检成凸起怎么办
背景纹理误检是另一个高频问题。拉丝纹理、磨砂面、加工刀纹,这些在顶帽变换后都可能产生局部亮斑,和凸起混在一起。
我的处理思路是“先压后筛”。压,是在形态学处理之前,先用median_filter或者gauss_filter把高频纹理压下去。中值滤波对拉丝纹理特别有效,窗口大小取纹理周期的1到2倍就行。筛,是在区域筛选阶段,增加一个“灰度方差”筛选条件。凸起区域的灰度方差通常比纹理区域大,因为凸起有亮暗过渡,而纹理区域的灰度变化相对均匀。
还有一个技巧是用select_shape里的compactness特征。凸起区域一般比较紧凑,而纹理误检区域往往形状不规则,compactness值会偏低。把compactness阈值设在0.5以上,能筛掉不少纹理干扰。
4.3 阈值调来调去总是不稳定
阈值不稳定,根源往往不在阈值本身,而在打光和图像预处理。如果打光不稳定,同一批产品在不同时间采集的图像灰度分布会漂移,那阈值怎么调都白搭。
我的建议是:先把打光做稳,再谈阈值。打光稳不稳,可以用一个简单方法验证:连续采集20张正常品图像,算一下同一位置的灰度均值,如果标准差超过5个灰度级,那打光就有问题。这时候应该先检查光源亮度是否稳定、相机曝光是否固定、环境光是否有干扰,而不是继续调阈值。
如果打光确实稳了,但阈值还是不好定,那就改用动态阈值。dyn_threshold的好处是它不依赖全局灰度分布,而是用局部参考图来做比较,对缓慢的灰度漂移不敏感。偏移量参数可以先用gray_histo看一下增强后图像的灰度分布,取双峰之间的谷底值作为初始偏移量,然后再微调。
4.4 凸起和凹坑怎么区分
有些产品表面既有凸起又有凹坑,两者在顶帽变换后都可能表现为亮斑,怎么区分?
如果打光方案是侧向光,凸起和凹坑的亮暗分布是相反的。凸起是一侧亮一侧暗,凹坑也是一侧亮一侧暗,但亮暗方向相反。这时候可以用derivate_vector_field或者简单的方向滤波来区分。但更简单的方法是:用顶帽和底帽分别处理,然后看区域在原始图像上的灰度均值。凸起区域在原始图像上的灰度均值通常高于周围背景,凹坑则低于周围背景。用intensity算子算一下区域内的灰度均值,和背景均值比较,就能区分开。
如果打光方案是正面均匀光,凸起和凹坑的灰度差异可能很小,那就得换打光方案。侧向光加低角度,是区分凸起和凹坑最有效的手段。
4.5 检测速度跟不上产线节拍
区域凸起检测的耗时主要在形态学处理和区域筛选上。如果图像分辨率很高,比如500万像素以上,gray_opening_rect这种算子会比较慢。
提速的思路有几个。一是缩小ROI,只对产品有效区域做处理,不要全图跑。二是降低图像分辨率,如果凸起尺寸允许,可以把图像缩小一半再处理,速度能提升三到四倍。三是用gray_opening_shape代替gray_opening_rect,圆形结构元素的运算速度通常比矩形快。四是把区域筛选的条件尽量往前放,先用面积筛掉大部分候选区域,再做复杂的形状和灰度筛选。
我实测下来,一张500万像素的图像,全流程跑下来大概在80到120毫秒之间,具体取决于参数和硬件。如果产线节拍要求50毫秒以内,那就得考虑降分辨率或者用更高效的算子组合。
5. 参数调优的实战经验与避坑指南
5.1 结构元素尺寸的快速估算方法
结构元素尺寸是这套方法里最关键的参数,但很多人不知道怎么快速估算。我的做法是:先在图像上手动量一下凸起区域的最大宽度,比如用Halcon的测量工具或者直接看像素坐标。假设量出来是12个像素,那结构元素尺寸就取18到24之间。先取中间值21试一版,看顶帽变换后的效果。如果凸起提取得不完整,就减小尺寸;如果背景噪声太多,就增大尺寸。
还有一个更工程化的方法:用gen_rectangle1在凸起区域画一个矩形,然后用smallest_rectangle1获取这个矩形的宽高,取宽高中的较大值乘以1.8,就是结构元素尺寸的推荐值。这个方法比目测准,而且可重复。
5.2 动态阈值偏移量的二分法调试
dyn_threshold的偏移量参数,我习惯用二分法来调。先取一个中间值,比如10,跑一遍看结果。如果误检太多,说明偏移量太小,分割太敏感,那就往上加,取15再试。如果漏检太多,说明偏移量太大,分割太保守,那就往下减,取7再试。每次调整幅度减半,一般三到四轮就能找到比较合适的值。
但要注意,偏移量不是越小越好。偏移量太小的时候,背景纹理的微小灰度波动也会被分割出来,导致误检暴增。所以调偏移量的时候,要同时观察误检和漏检的变化趋势,找到一个平衡点。
5.3 区域筛选条件的优先级排序
区域筛选的条件很多,但优先级不能乱。我的排序逻辑是:先筛面积,再筛形状,最后筛灰度。
面积筛选放在最前面,因为面积计算最快,而且能一下子去掉大部分噪声区域。形状筛选放在中间,因为形状特征的计算量比面积大,但比灰度统计小。灰度筛选放在最后,因为灰度统计需要对每个区域遍历像素,计算量最大,只对前面筛剩下的少量区域做,整体耗时就降下来了。
另外,筛选条件不要一次设太死。比如面积筛选,可以先设一个宽范围,比如30到8000,把明显不对的去掉,后面再用其他条件精细筛。如果一上来就把面积范围卡得很窄,可能会把一些形态稍有不同的真实缺陷也筛掉。
5.4 光照变化下的鲁棒性处理
产线环境的光照不可能完全恒定,尤其是白天和晚上、开灯和关灯的时候,环境光会有变化。如果算法对这种变化很敏感,那稳定性就没法保证。
提升鲁棒性的手段有几个。一是用emphasize算子做局部对比度增强,它能在一定程度上抵消全局光照变化的影响。二是用equ_histo_image做直方图均衡化,把图像的灰度分布标准化,减少光照漂移带来的影响。三是用illuminate算子做光照校正,它可以根据参考图像把当前图像的光照不均匀性校正过来。
但最根本的解决办法还是把光源和环境光控制好。加遮光罩、用稳定电源、固定曝光时间,这些硬件层面的措施比任何算法补偿都有效。我见过太多项目,算法调得再好,打光一塌糊涂,最后稳定性还是不行。
5.5 误检与漏检的权衡策略
在实际产线上,误检和漏检的代价往往不一样。漏检意味着缺陷流到下游,可能导致客户投诉甚至安全事故;误检意味着正常品被误判,导致产线停机或者人工复检成本增加。
对于凸起缺陷,我一般倾向于宁可误检,不可漏检。因为凸起缺陷往往影响装配和功能,漏检的后果更严重。所以参数设置上,我会把阈值调得稍微敏感一点,让误检率略高,然后通过后续的人工复检或者二次检测来过滤误检。
但如果误检率太高,产线根本没法跑,那就得重新权衡。这时候可以考虑增加检测工位,用两个不同参数的检测流程串联,第一个流程负责高灵敏度检测,第二个流程负责高特异性确认。两个流程都报警的区域,才判定为缺陷。这样既能保证不漏检,又能把误检压到可接受的范围。
6. 从单帧检测到产线集成的扩展思路
6.1 多相机与多工位的协同
单相机检测往往有盲区,尤其是产品形状复杂的时候。凸起缺陷可能出现在侧面、底面或者内腔,一个相机覆盖不了。这时候就需要多相机协同。
多相机协同的关键是坐标统一。每个相机有自己的坐标系,检测到的缺陷位置需要转换到产品坐标系下,才能判断是不是同一个缺陷。Halcon里可以用vector_to_hom_mat2d或者hom_mat3d来做坐标变换,把不同相机的检测结果映射到同一个参考系下。
另一个问题是触发同步。多相机同时采集,需要硬件触发信号来保证时间对齐。如果产品在运动,还要考虑运动模糊和位置偏移,这时候需要用编码器信号或者飞拍技术来补偿。
6.2 与PLC和MES系统的数据对接
检测结果最终要输出到产线控制系统。Halcon本身不直接和PLC通信,但可以通过TCP/IP、串口或者OPC UA把结果传出去。我一般用Halcon的socket算子建立TCP连接,把缺陷的坐标、面积、类型打包成字符串或者JSON格式发出去。
和MES系统对接的时候,除了缺陷信息,通常还需要上传检测时间、产品序列号、检测结果统计等数据。这些数据可以用数据库接口写入,或者通过消息队列异步传输。关键是数据格式要提前和IT部门对齐,避免后期返工。
6.3 模型参数的版本管理与追溯
产线换型的时候,检测参数往往需要调整。如果参数管理混乱,今天调好的参数明天就找不到了,那维护成本会很高。
我的做法是把所有参数存成配置文件,用产品型号作为文件名,比如model_A_params.json。每次换型的时候,加载对应的配置文件就行。配置文件里记录结构元素尺寸、阈值、筛选条件等所有可调参数,并且加上版本号和修改日期。如果某个参数调整后效果变好,就更新配置文件并记录修改原因。这样即使过了半年,也能追溯当时为什么这么调。
如果条件允许,还可以把参数文件和检测图像一起存档,方便后期复盘和优化。我见过一些团队,用Git来管理参数文件,每次修改都提交一次,这样版本追溯就非常清晰了。
6.4 持续优化:从误检样本中学习
算法上线不是终点,而是起点。产线跑起来之后,会不断产生新的误检和漏检样本。这些样本是优化算法的最好素材。
我习惯每周收集一次误检样本,分类整理。如果是打光问题导致的,就调整打光;如果是参数问题导致的,就优化参数;如果是新出现的缺陷类型,就评估是否需要增加新的检测逻辑。这个过程持续下去,算法的稳定性和覆盖率会越来越高。
还有一个小技巧:把误检样本的ROI区域截取出来,单独存成一个图像集,然后用Halcon的create_shape_model或者create_scaled_shape_model训练一个快速排除模型。检测流程里先跑区域凸起检测,得到候选区域后,再用这个排除模型过一遍,把已知的误检模式筛掉。这个方法对减少重复误检特别有效。
7. 我个人在实际操作中的几点体会
这套区域凸起检测方法,我从最早用灰度阈值硬怼,到后来系统性地用形态学加区域筛选,中间踩了不少坑。最大的体会是:打光决定上限,算法决定下限。打光做不好,算法再花哨也白搭;打光做好了,算法只需要做基本的形态学处理和筛选,就能跑得很稳。
另一个体会是参数不要贪多。我见过一些方案,用了七八个筛选条件,每个条件都卡得很死,结果换一个批次的产品就全乱了。筛选条件在精不在多,抓住面积、形状、灰度这三个核心维度,每个维度设一两个关键条件,足够了。条件太多,过拟合风险高,迁移性差。
最后说一个容易被忽视的点:检测结果的可视化。产线操作员不关心你用了什么算子,他们只关心缺陷在哪里、有多大、要不要拦截。所以检测结果的可视化一定要清晰,缺陷区域用醒目的颜色标出来,旁边标注面积和坐标。如果可能,把原始图像、增强图像、分割结果、最终判定做成四宫格显示,操作员一眼就能看明白。这个细节做好了,产线接受度会高很多。
这套方法不是万能的,但在它适用的场景里,稳定性和可维护性都经得起考验。如果你正在做类似的凸起缺陷检测项目,不妨先从打光和形态学参数入手,把基础打牢,后面的调优会顺很多。