ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Halcon+形态学实现刀具刃口缺陷检测的实战解析

Halcon+形态学实现刀具刃口缺陷检测的实战解析

做了快十年机器视觉项目,我发现刀具外观检测这个需求几乎是每个视觉团队都躲不开的“必修课”。一支硬质合金铣刀出厂之前,刃口只要崩掉0.1mm见方的小块,装到CNC主轴上转起来就是实打实的损失——轻则工件表面出现振纹,重则整把刀当场崩裂。这篇想聊的,就是一个用Halcon视觉库配合形态学算子做刀具刃口缺陷检测的真实项目:方案怎么定、形态学在中间起了什么作用、核心代码怎么落、以及我在车间里调参踩过的坑。

先给结论:整个项目的核心链路是“预处理 + 形态学净化 + 轮廓拟合 + 差影分析”。Halcon负责提供稳定高效的图像处理算子,形态学则承担三件事——去噪、连接断裂刃口、把微小缺陷从规则轮廓中“逼”出来。现场车间节拍要求单支刀具检测时间在15秒以内,这套纯CPU方案实测稳定在5秒上下,识别准确率能做到98%以上。这篇文章适合刚接手刀具质检项目的视觉工程师,也适合正在传统图像算法和深度学习方案之间摇摆的开发者,看完能少走不少弯路。

1. 刀具检测项目:需求拆解与方案取舍

1.1 产线质检到底盯哪些缺陷指标

先明确任务边界。刀具检测在不同工序、不同刀具类型下,检测项天差地别,但车间里最常被提出来的一定是这几样:崩刃、缺角、毛刺和刃口磨损。以最常见的硬质合金立铣刀为例,出厂终检规格通常是这样:

缺陷类型判定标准图像表现形态学影响
崩刃刃口缺口宽度超0.05mm或深度超0.1mm刃口连续线上出现凹陷闭运算半径过大会直接填平
缺角刀尖缺损投影面积超0.03mm²刀尖部位块状缺失开运算可能把它误当毛刺
毛刺残留高度超过0.02mm刃口外缘细小凸起开运算的主要清除对象
刃面磨损反光区域灰度渐变异常刃面变暗发白对二值形态学影响较小

这些数字看着小,反映到图像上往往就是一两个像素量级的灰度突变。500万像素相机加远心镜头,视场覆盖大约30mm,算下来单像素精度能到6微米左右,0.05mm的崩口折算下来大概8个像素,检测空间是够的。但反过来说,精度要求高也意味着不能随便用大结构元素做形态学处理,不然真实缺陷很容易被“洗”掉。

另一个必须考虑的是节拍。产线上给算法的可用时间通常只有8到15秒,包含自动上料、图像采集、检测、判定和下料。一个现场项目如果算法耗时超过节拍,就算准确率再高也落不了地。这正是形态学方案的优势所在——Halcon底层对形态学算子做了大量优化,一块普通工控机CPU就能处理500万像素的完整链路,不必上GPU。

1.2 传统形态学方案为何比深度学习更先落地

我在好几个相似项目里对比过深度学习方案。深度学习做刀具缺陷检测,在复杂纹理背景下确实可以表现得更好,但它的门槛向来不在模型精度,而在数据。崩刃、缺角在产线上是可遇不可求的小概率事件,要凑几千张正负样本做训练,往往需要连续采集几个月的产线图像再人工标注,项目周期很容易拉长到半年以上。再加上GPU算力、标注人力成本,对中小型制造企业来说是一笔不菲的额外投入。

这个项目最终选了“Halcon传统图像 + 形态学”的路线,原因很直白:形态学运算过程完全透明,参数可以逐条对图调试,出问题能追到具体环节;缺陷在图像上的表现又是典型的“区域连续性突变”,传统方法完全镇得住场子。实际跑下来识别准确率稳定在98%左右,漏检率降到0.5%以下,满足出厂质检要求。更重要的是,方案交付以后,现场维护工程师只需要会调几个形态学半径参数,不用走深度学习那套训练迭代流程,车间维护成本低很多。

我也要强调一点,这个路线并不是封死了深度学习的路。如果后续刀具型号增加、刀具表面纹理变复杂,完全可以用形态学先把每个刃口的ROI裁出来,再交给分类模型做更细的判别。换句话说,形态学在这里既是完整的解决方案,也是未来算法升级的前置处理单元。

2. 形态学在刀具图像处理中的实战角色

2.1 腐蚀、膨胀、开运算、闭运算在检测里各干什么

形态学的理论不难,但要真正在刀具检测里用顺手,得把四个基本操作各自的实际效果掰开来看。我尽量不用教科书语言。

腐蚀是把区域边缘向内收缩一圈。想象你用橡皮擦沿着铅笔画的轮廓内侧擦一遍,线条自然变细,那些孤立的细小墨点直接被擦没了。在刀具图像里,腐蚀常用于去掉刃口两侧的孤立亮点和细小颗粒。

膨胀恰好相反,它把区域边缘向外扩展一圈,就像用粗马克笔把线条重新描了一遍,两个挨得近的断头可能就此连通。膨胀的典型用途是把刃口上因为反光断裂的小段重新接上。

开运算是先腐蚀再膨胀,整体效果像给木头边缘做一次砂纸打磨——把毛刺、凸起磨平,又不会改变大轮廓。对应到刀具,就是剔除刃口毛刺和背景杂点。

闭运算是先膨胀再腐蚀,整体效果像给墙面裂缝刮腻子——细小的缝隙被填补,但大形状不变。对应到刀具,就是把刃口上因为光源角度造成的黑色断缝修通。

在我的项目流程里,阈值分割出来的刀具区域会先做开运算,把背景反光颗粒和毛刺清掉;紧接着做闭运算,把刃口不连续修通。顺序不能反。如果先闭运算,背景颗粒会和刀具区域黏连,后面开运算又难以完全切开,最终提取的轮廓就会出现假凸起。

2.2 结构元素选圆形还是方形,差别比你想的大

形态学操作里有个容易忽略的参数——结构元素,也就是用来腐蚀或膨胀的“刻刀”形状。针对刀具检测这种刃口朝向不固定的场景,强烈建议用圆盘形结构元素,而不是方形或十字形。原因是圆盘在几何上各向同性,不管刃口是水平、垂直还是斜向,腐蚀和膨胀的收缩量与扩张量在全方位都一致,不会因为方向差异引入新的形变。

方形结构元素对水平垂直边缘更锐利,检测平面刃口时表现不错,但一旦刀具摆放角度不是精确的0度或90度,方形的方向性就会造成边缘位置偏移,检测结果随角度出现波动。十字形结构元素更不适合,它的有效覆盖在四个对角线方向上是缺失的,会把真实的缺陷形态低估掉。所以我在这类项目里统一用圆形,省心得多。

2.3 形态学梯度如何把细微崩刃“放大”成高亮信号

形态学还有一个在检测阶段很有用的用法:形态学梯度。把膨胀后的区域减去腐蚀后的区域,得到的就是一个边缘带。这个操作能在二值图像层面把刃口局部的突变放大,崩刃、缺角这些细微信号会变得更加突出,方便后续生成关注区。

具体做法是对二值化后的刀具区域做一次圆盘膨胀、一次圆盘腐蚀,两者相减,得到刃口边缘带。再把边缘带向外扩展几个像素,形成一个紧凑的刃口关注区。后续所有缺陷判定都在这条带状区域里进行,好处立竿见影:计算量大幅降低,算法只关心刃口附近,刀具中部花纹、型号刻字之类的不相干特征完全不会进入判定环节,误检率也随之下降。这是形态学在项目里性价比最高的一个应用点。

3. Halcon完整检测流程与核心代码实现

3.1 图像采集方案与预处理:先把画面收拾干净

先定采集硬件。这个项目用的是500万像素工业面阵相机、120mm远心镜头和背光源。选背光源是因为刀具在背光下呈现清晰的黑色剪影,刃口轮廓对比度极高,阈值分割非常稳定;同时背光源能最大程度弱化刀具表面自身的反光干扰,这对后续形态学操作非常友好。

采集到的原始图像肯定不能直接用。传感器噪声和环境杂散光会让阈值分割甩出一堆假区域,直接进入形态学流程的话,这些假信号会被当成真实特征放大。我的习惯是先做5x5高斯平滑,再用半径3的圆形中值滤波去掉脉冲噪声。这两个操作几乎不改变刃口宏观形状,但能大幅降低后续干扰:

* 读取图像 read_image (Image, 'blade_sample_01.png') * 转灰度 rgb1_to_gray (Image, GrayImage) * 高斯平滑,滤除传感器高频噪声 gauss_filter (GrayImage, ImageGauss, 5) * 中值滤波,去脉冲噪点 median_image (ImageGauss, ImageMedian, 'circle', 3, 'mirror')

注意中值滤波半径不要超过4。我试过用5,刃口本身的细小缺陷直接被抹平,后续检测完全抓不到崩刃信号。半径取3是个比较安全的平衡点。

3.2 阈值分割与两步形态学净化:从噪声中还原刀具本尊

预处理完成后做区域分割。背光图像下刀具是暗色,背景是亮色,固定阈值就能稳定分离:

* 阈值分割,提取刀具暗色区域 threshold (ImageMedian, RegionBlade, 0, 120)

但分割出来的区域不够干净:边缘可能黏着背景亮点,内部可能因为背光不均匀出现孔洞。这时候形态学开始发挥主要作用。先是开运算剔除刃口外侧的毛刺状干扰,再用闭运算补上刃口内侧因为轻微反光形成的黑色空洞:

* 开运算:清除毛刺与孤立噪点 opening_circle (RegionBlade, RegionOpened, 2.5) * 闭运算:连接刃口的断裂区域 closing_circle (RegionOpened, RegionClosed, 3.5)

这两个半径值是多轮实验调出来的。开运算半径2.5意味着直径小于5像素的孤立干扰都会被清除;闭运算半径3.5能把宽度不超过7像素的断裂缝填补。现场光照如果更差,可以把这两个数值各放大1到1.5,但一定要警惕:开运算半径超过4以后,真实崩口会被当毛刺消掉,这是形态学预处理的高风险区。

区域净化后做连通域分析和形状筛选,把面积最大的主体区域提取为唯一ROI:

* 连通域分析 connection (RegionClosed, ConnectedRegions) * 按面积筛选最大区域作为刀具主体 select_shape_std (ConnectedRegions, RegionFinal, 'max_area') * 把后续图像处理域缩小到刀具区域 reduce_domain (ImageMedian, RegionFinal, ImageReduced)

reduce_domain这个操作非常关键。它把之后所有运算集中在刀具区域上,既减少计算量,也避免背景干扰进入后续环节。这一步算是整个项目对效率提升最有帮助的代码之一。

3.3 刃口轮廓提取与差影分析:让缺陷无处可藏

拿到干净的刀具区域后,提取刃口轮廓。先用边界算子从二值区域得到轮廓带,再用形态学梯度放大刃口变化,两者结合得到刃口关注区:

* 提取刀具区域边界 boundary (RegionFinal, RegionBorder, 'inner') * 形态学梯度,放大小缺陷信号 dilation_circle (RegionBorder, RegionDilate, 2) erosion_circle (RegionBorder, RegionErode, 2) difference (RegionDilate, RegionErode, RegionGradient) * 沿刃口扩展生成关注区 dilation_circle (RegionGradient, RegionROI, 5)

这段代码的思路是先把刀具边界提取出来,再用膨胀减腐蚀把边界上微小的起伏变化放大。注意RegionGradient是刃口边缘带,而不是整个刀具区,后面再向外扩展5像素得到的RegionROI正好覆盖刃口两侧,是一个理想的缺陷检测带。

缺陷定位的核心叫差影分析,也可以理解成“找离群值”。过程分两步:先用亚像素轮廓提取把刃口边缘转成亚像素精度的曲线,再用直线拟合得到一条理想刃口线:

* 亚像素轮廓提取 edges_sub_pix (ImageReduced, Edges, 'canny', 1.5, 12, 30) * 筛选出主要刃口线段 select_contours_xld (Edges, SelectedEdges, 'length', 20, 1000, -0.5, 0.5) * 用Tukey权重拟合刃口直线,对离群缺陷点不敏感 fit_line_contour_xld (SelectedEdges, 'tukey', -1, 0, 0, RowBegin, ColBegin, RowEnd, ColEnd, Nr, Nc, Dist)

这里用Tukey权重而非最小二乘,是个关键选型。崩刃位置的点就是离群点,如果用最小二乘拟合,这些点会把理想直线往缺陷方向拉,导致缺陷被“隐藏”;改用Tukey权重后,离群点对拟合结果影响很小,拟合出来的直线才真正代表“原本应该有的刃口”。

得到理想直线后,用形态学做差影。先把理想直线生成一个允许波动带宽,再与实际刃口区域做差,差出来的部分就是缺陷候选:

* 将拟合直线生成理想刃口区域 gen_region_line (RegionIdealEdge, RowBegin, ColBegin, RowEnd, ColEnd) * 对理想刃口做3像素膨胀,形成允许波动带 dilation_circle (RegionIdealEdge, RegionAllowanceBand, 3) * 实际刃口与允许带求差,得到缺陷候选 difference (RegionBorder, RegionAllowanceBand, RegionDefectCandidates) * 连通域分析并筛选真实缺陷 connection (RegionDefectCandidates, DefectConnected) select_shape (DefectConnected, SelectedDefects, ['area', 'width'], 'and', [5, 2], [1000, 100])

允许波动带的宽度等于检测容差,展开到像素坐标就是3像素,对应约18微米。缺陷候选经过面积和宽度筛选,面积大于5像素、宽度大于2像素的才保留,明显噪点和可以忽略的极微缺陷都被排除了。需要说明的是,这段代码基于当前项目使用的Halcon版本写成,不同版本算子的参数列表略有差异,运行时请以你本机的算子签名和帮助文档为准。

3.4 缺陷判定逻辑与NG/OK输出

缺陷候选真正判NG,还要同时满足两个条件:宽度阈值和深度阈值。我在项目里把宽度阈值设为对应0.05mm的像素数,深度阈值设为对应0.1mm的像素数,两个条件都达标才输出NG。单个条件达标的候选,先放到“待检”列表,由人工复核确认。

输出逻辑上也做了一点工程化处理。判定结果通过串口信号返回PLC,同时本地保存当前图像、缺陷坐标和判定结果,方便追溯。这里有一点经验:视觉系统判定NG的刀具,最好在图像上把缺陷区域用矩形框标出来,生成一张直观的检测结果图。产线师傅看结果一眼就能找到缺陷位置,而不是面对一堆坐标数字发懵。我最初交付时只给了一串数值坐标,被现场师傅吐槽过好几天,后来改成可视化图才消停。

4. 形态学参数调优实录与常见问题排查

4.1 光照波动场景下如何保持分割稳定

先说一个最典型的现场问题。车间环境光不是恒定的,下午三四点阳光斜射进来,背光源下的图像灰度整体漂移,固定阈值突然失效,刀具区域分割出一堆破洞。第一次遇到时我足足排查半天,最后发现是日光影响。

解决办法分两层。硬件上,给相机加遮光罩,把环境光挡在成像系统之外,这是最省心的做法。但车间总有角落遮不干净,所以算法上也要兜底:把固定阈值改成动态阈值,用Halcon的灰度直方图自动确定背景和前景的谷底作为分割阈值。我在项目里是双管齐下,实测环境光变化两个灰度等级内,检测结果完全不受影响。

4.2 结构元素半径的调优方法和一个漏检教训

这是整个项目调参时间最长的一部分。半径太小,毛刺消不干净,刃口关注区全是噪点;半径太大,真实的崩口和缺角被当成毛刺抹掉了。

我的起步方法是理论值加实验修正。开运算半径取目标毛刺最小宽度的像素数的一半多一点,比如毛刺最小宽度是4像素,那半径先取2到3。闭运算半径取刃口断缝最大宽度的三分之一左右,断缝最大宽度约10像素,那半径取3到4。然后拿现场采集的50张包含各类缺陷的真实图像反复迭代,直到缺陷检出率稳定。

调参过程中我踩过一个大坑。为了让刃口断裂完全闭合,把闭运算半径从3.5一路调到6,结果缺陷区域也被“补腻子”填平,漏检率突然升到3%。排查后定位到问题:大半径闭运算在填补断缝的同时,把崩刃凹陷也填掉了。解决思路是分两步检测:先用小半径(2到3像素)做细检测,找微小缺陷;再用大半径(5到6像素)做粗检测,排除毛刺干扰。两路结果做并集,微小缺陷不会漏,毛刺也不会误报。

4.3 常见问题速查表

这些是项目中出现过且实际解决过的问题,整理成表,方便现场排查:

现象可能原因解决方案
刀具区域出现大片孔洞环境光漂移导致分割失效加遮光罩,阈值改为动态阈值
刃口关注区噪点极多开运算半径过小按毛刺宽度一半以上增大半径
崩刃缺陷检测不到闭运算半径过大采用粗细两路半径分开检测
刀具摆放角度变化后漏检结构元素方向性造成形变统一改用圆形结构元素
拟合轮廓被缺陷带偏拟合权重函数未选对改用Tukey权重或RANSAC式拟合
检测耗时超标全图计算形态学与差影先用reduce_domain裁剪ROI

这个表格我打印了一份贴在工控机侧边,现场调试时对照着查,比翻笔记快得多。实际处理问题的时候,多半是表中两条原因叠加造成的,所以排查时不要只动一个参数,要沿着“图像采集—分割—形态学—拟合—判定”的链路逐个环节检查,哪个环节出的问题,症状都会和对应原因对上。

4.4 效率优化:把检测时长从11秒压到5秒的实战操作

项目初期整套HDevelop流程跑下来单支刀具要11秒,超过产线节拍。排查发现主要时间浪费在两点:一是对全图反复做形态学膨胀腐蚀,二是缺陷判定遍历了整个刀具区域的点集。

优化思路一句话:让算法只在真正需要看的地方干活。具体做了三件事。第一,采集后立刻用阈值分割和select_shape_std锁定刀具最小外接矩形,后续处理全部在矩形ROI内进行。第二,做完形态学梯度后,刃口关注区已经生成,立刻用reduce_domain把差影分析限制在关注区内。第三,把判定环节从“全区域逐点扫描”改为“仅在缺陷候选连通域上统计”。

三步做完,耗时从11秒压到5秒以内,精度没有任何下降。对于节拍紧张的生产线,这个优化比换高性能工控机划算得多,也让我意识到,很多时候算法的性能瓶颈并不在算子本身,而在处理域没被提前收紧。

我个人在这类项目里的体会是,形态学是整个检测链路中最不起眼却又最关键的一环。它没有深度学习那么炫,却用最朴素的方式保证了刃口轮廓的稳定性和可分析性。越是在产线上跑得久的项目,越能感受到这种“减法思维”的价值——把干扰一层层剥干净,剩下的就是缺陷本身。如果后续产量需求再翻倍,这套流程也留了扩展空间:用多线程并行处理不同刀具的图像,精度不变,效率还能再上台阶。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进