ARTICLE DETAIL

资讯详情

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

OpenCV实战:金属零件表面污渍、划痕与崩边检测

OpenCV实战:金属零件表面污渍、划痕与崩边检测 1. 场景和思路1.1 一个零件上的三种缺陷要同时抓出来做 OpenCV 图像处理这些年最常被问的一句话就是“这不就是阈值分割加找轮廓吗”等你真把产线图片摆到面前就会发现所谓“抓缺陷”根本不是刷一个 threshold 就能交差的。前段时间有个来料质检的需求一个金属圆片零件表面要求干净、边缘完整。项目目标写得很简单——抓出三个缺陷分别是表面污渍、表面划痕、边缘崩边。这个需求本质上属于工业视觉里的外观缺陷检测。零件图是固定的工位拍的光照相对稳定背景也固定缺陷一共有三类污渍表现为局部暗斑形状不规则面积可大可小划痕表现为细长的高亮线条方向不确定可能有断点崩边表现为圆形轮廓边缘少了一块轮廓形状不完整。如果是真人质检员这三种缺陷他扫一眼就能判断。但换到程序里问题就不一样了污渍是“面”的特征划痕是“线”的特征崩边是“边界形状”的特征。三种缺陷对应三种完全不同的图像特征需要分开处理。这篇实例适合谁看如果你是刚开始接触 OpenCV、想拿一个真实场景练手的同学可以直接照着代码跑一遍。如果你在项目里也遇到过“缺陷检测总是误报一堆”的情况那参数调整和避坑的部分更值得看。1.2 为什么这类项目首选 OpenCV这里先回答一个新人常问的问题OpenCV 是什么它是一套开源的计算机视觉函数库把图像读取、滤波、边缘提取、阈值分割、轮廓查找这些基础操作都封装成了现成函数。你不需要从零写图像处理算法只要会用这些函数组合出你要的效果就行。这个项目选 OpenCV 而不是 Halcon也不是一上来就深度学习原因很简单缺陷形态相对固定场景受控传统图像处理足够解决OpenCV 免费开源Python 接口写起来快调参也直观深度学习需要大量缺陷样本来训练初期往往没有那么多坏件图。当然我也遇到过同行问我“Halcon 不是更专业吗”。后面第五节专门讲两者怎么选这里先说结论中小自动化项目的视觉检测OpenCV 能覆盖大多数场景没必要一上来就上重型商用库。1.3 全局设计一图拆三路拿到一张零件图别急着直接二值化。我的习惯是先看“缺陷和正常区域之间到底差在哪”然后按特征分路处理。这个项目里三路检测是这样拆的缺陷类型灰度特征几何特征对应处理路线污渍局部灰度明显偏低面积有限的块状区域阈值分割 形态学去噪 连通域面积筛选划痕局部灰度偏高或偏低呈细长条长宽比大、连续性好Canny 边缘 霍夫直线检测 / 形态学顶帽崩边边缘区域与模板有差异轮廓局部凹陷或缺失模板差值 / 轮廓几何拟合这样做的好处是互不干扰。污渍检测不会因为划痕细长就把整条线当成一个块崩边检测也不会因为表面有个污渍点就误报。后面代码也是按照这三路来的。2. 三种缺陷的检测原理2.1 污渍从“灰度坑”到连通域把灰度图想象成一张海拔图零件表面是平整的高原污渍就是高原上凹下去的一个坑。阈值分割做的就是“把低于某个海拔的区域挖出来”。阈值选多少是关键。固定阈值比如90在光照变化时很容易失效光线稍微暗一点正常区域也可能低于 90光线强一点污渍又可能高于 90。所以代码里我用 Otsu 自动阈值。Otsu 的原理可以理解为它自动找一个灰度值让前景和背景这两部分各自的类内方差最小、类间方差最大。简单说就是程序自己看灰度直方图上的“两个山峰之间”最合适的位置去切。污渍检测有个容易踩坑的地方如果零件背景也是暗的直接反阈值会把整片黑色背景都当成污渍。所以我会先把“亮表面区域”单独提取出来生成一个掩膜再在这个掩膜内部找暗斑。这个操作很多人会漏漏掉之后误报率会高到你怀疑程序写错了。2.2 划痕直线检测和形态学顶帽划痕和污渍不一样。划痕是一条细线灰度上表现为比周围亮或暗的长条形。直接用阈值分割细线很容易碎成一段一段没法形成完整的连通域。所以抓划痕我提供两条路线第一条是 Canny 边缘检测加霍夫直线检测。划痕两侧存在灰度跳变Canny 能把这种跳变提取成边缘线HoughLinesP 再从边缘点里找连续直线段。只要能找到足够长、超过设定阈值的直线段就认为是划痕。第二条是形态学顶帽。顶帽变换的计算是“原图减去开运算结果”。开运算对细线有“抹平”效果细线在开运算结果里会消失原图减去开运算结果之后细线就被单独留下来了。用这种方法抓弯曲的划痕特别有效霍夫直线只对近似直线的划痕有效一遇到弯弯扭扭的划痕就抓瞎。如果要做更精细的划痕长度统计可以接着做骨架提取把划痕区域细化成单像素线再统计像素个数就能估算真实长度。OpenCV 没有直接提供细化函数但可以用 Zhang-Suen 算法的思路迭代腐蚀或者借助形态学重建来实现。这个属于进阶玩法项目里划痕基本是直线时用 HoughLinesP 就够了。2.3 崩边模板差值与几何拟合崩边是边缘形状问题。它不像污渍和划痕那样在表面形成一块区域而是在轮廓边界上缺了个口子。最直观的方法是模板差值法拿一张没有缺陷的标准件图像和当前待检图做像素级相减。如果当前零件的边缘崩了一块那一块的灰度会和模板明显不同差值图里就会出现一个亮区。把差值图二值化、形态学处理、找轮廓就能定位崩边位置。模板差值法的前提是对齐。零件位置、旋转角度、镜头亮度都要基本一致否则整个边缘都会出现像素差异误报成大面积崩边。如果产线没有精确夹具就得改用几何法提取零件外轮廓对轮廓做最小外接圆拟合然后逐个计算轮廓点到圆心的距离再和半径比较。崩边位置的轮廓点距离会明显小于正常半径连续出现足够多这样的点就判定崩边。几何法不要求严格对齐哪怕零件在画面里平移几个像素也能正常工作代价是代码量稍微多一点而且要求零件本身的几何形状比较规则。2.4 三种方案选型对照表缺陷类型推荐方案优势局限污渍Otsu 阈值 形态学 面积筛选实现简单速度快光照不均时全局阈值不稳直线划痕Canny HoughLinesP定位准确能画直线弯曲划痕容易漏检弯曲划痕顶帽变换 骨架提取对线形不敏感参数多需要反复调崩边模板差值直观适用于固定工位对位置对齐要求高崩边轮廓圆拟合抗平移零件必须近似圆形或规则形状实际项目里我不是闭着眼睛选而是先拿 20 张正常件和 10 张缺陷件跑一遍看哪个方案误报最少再定。这也是我要在这里强调的方案不是越复杂越好而是跟你的相机工位条件匹配最好。3. 核心代码与参数调优3.1 预处理环节先准备好两张图一张待检零件图part_sample.jpg一张标准件模板part_template.jpg。如果手头没有现成图片也可以用手机拍一张干净的圆形物体做模板再拍一张带缺陷的做测试。代码先把图读成灰度图再做高斯滤波。高斯滤波的核我选了5×5sigma 默认取 0。核不能太大不然划痕这类细小特征会被抹平也不能太小太小则噪点没有被压住。滤波的目的是让后面的阈值和边缘检测更稳定而不是把图像变模糊。读图时有个必须养成的习惯判断img是不是None。很多人代码一报cv2.error就懵了其实大部分原因是图片路径写错或者路径里有中文imread返回了空后面cvtColor就崩了。3.2 抓污渍的阈值与掩膜技巧污渍检测的核心代码如下import cv2 import numpy as np def detect_spots(gray, min_area60): # 反向 Otsu 阈值提取暗色区域 _, dark cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) # 正向 Otsu 阈值提取亮表面区域作为掩膜 _, bright cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 只在亮表面区域内找暗斑背景被排除 dark cv2.bitwise_and(dark, bright) # 开运算去掉孤立噪点 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) dark cv2.morphologyEx(dark, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(dark, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) spots [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: x, y, w, h cv2.boundingRect(cnt) spots.append((x, y, w, h, area)) return spots逐行解释一下。三步阈值都用了 Otsu但用途不同dark反阈值提取的是“所有比自动阈值暗的地方”里面既包含工件上的污渍也包含黑色背景bright正向阈值提取亮表面作用是生成掩膜。bitwise_and把两个结果一交背景就被清掉了。这里形态学用MORPH_OPEN也就是先腐蚀后膨胀。腐蚀会把细小噪点删掉膨胀再把保留下来的污渍区域恢复回来。核用椭圆(3, 3)是因为椭圆核更接近真实污渍形态比方形核圆滑。面积阈值min_area我给了 60。这个值怎么定先跑一张图把cv2.contourArea的结果打印出来看噪点和真实污渍的面积分界在哪里再取一个中间值。它不是拍脑袋定的是统计数据定出来的。3.3 抓划痕的Canny与Hough参数划痕检测代码如下def detect_scratches(gray, min_len80): edges cv2.Canny(gray, 80, 200) lines cv2.HoughLinesP(edges, 1, np.pi / 180, threshold60, minLineLengthmin_len, maxLineGap8) scratches [] if lines is not None: for item in lines[:, 0, :]: x1, y1, x2, y2 item length int(np.hypot(x2 - x1, y2 - y1)) if length min_len: scratches.append((x1, y1, x2, y2, length)) return scratchesCanny 有两个阈值80和200。高阈值用来确定哪些边缘是“确定的”强边缘低阈值用来把和高阈值边缘连通的弱边缘也补回来。如果两个阈值太接近边缘会断成一截一截阈值差距太大又会把纹理噪声全勾出来。一般经验是低阈值取高阈值的三分之一到二分之一我这边80 / 200是实践中比较稳的比例。HoughLinesP里的参数值得讲一下threshold60是投票阈值表示一条直线至少需要多少边缘像素点支持。太低了会把零碎短线也当成划痕太高了真实划痕又检不出来minLineLength80是最短直线长度。这里和min_len一起兜底设为 80 像素maxLineGap8是允许同一线段上断裂处的最大像素间隔。划痕经常有断点这个参数就是用来“跨过”那些小断点的。如果表面划痕是弯曲的或者方向复杂我建议换顶帽路线kernel_line cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1)) tophat cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel_line) _, top_bin cv2.threshold(tophat, 40, 255, cv2.THRESH_BINARY)这个(15, 1)结构元专门抓水平方向的细亮线。如果划痕还有垂直方向的就再建一个(1, 15)的结构元分两次处理最后把两张二值图叠加。顶帽抓出来的是线状区域再用细化或连通域分析统计长度效果比霍夫直线更能适应弯曲划痕。3.4 抓崩边的模板差值实现模板差值法代码如下def detect_edge_missing(gray, template_gray, min_area120): diff cv2.absdiff(template_gray, gray) _, zone cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) zone cv2.morphologyEx(zone, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8), iterations2) contours, _ cv2.findContours(zone, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) missing [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: x, y, w, h cv2.boundingRect(cnt) missing.append((x, y, w, h, area)) return missingabsdiff把模板和待检图逐像素相减崩边区域会明显变亮。阈值用固定值30就好因为是在差值图上操作正常区域差值为 0有差异区域超过 30 就触发。这里不能再用 Otsu因为差值图上大多数像素都是 0Otsu 会把阈值算到很低导致边缘微弱的光照差异全部变成误报。MORPH_OPEN用5×5的结构元迭代两次是为了把模板对齐误差造成的小面积边缘噪点清掉。一般正常件和模板之间会有几个像素的成像差异开运算能把这种细碎的边缘差异抹掉只留下真正的崩边区域。如果你那边工位不固定零件会平移旋转那模板差值法就不适用了。崩边的几何检测思路是先提取外轮廓再用cv2.minEnclosingCircle拟合最小外接圆计算每个轮廓点到圆心的距离距离明显小于半径且连续的弧段就是崩边位置。这个方法能够容忍几个像素的平移。3.5 结果标注与判定输出三路检测完最后把它们画到同一张图上def main(): img, gray load_gray(part_sample.jpg) tmp_img, tmp_gray load_gray(part_template.jpg) spots detect_spots(gray) scratches detect_scratches(gray) missing detect_edge_missing(gray, tmp_gray) result img.copy() for x, y, w, h, area in spots: cv2.rectangle(result, (x, y), (x w, y h), (0, 0, 255), 2) cv2.putText(result, spot, (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) for x1, y1, x2, y2, length in scratches: cv2.line(result, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(result, scratch, (x1, y1 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) for x, y, w, h, area in missing: cv2.rectangle(result, (x, y), (x w, y h), (255, 0, 0), 2) cv2.putText(result, missing, (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 0), 2) is_ng len(spots) len(scratches) len(missing) 0 cv2.putText(result, NG if is_ng else OK, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) cv2.imshow(result, result) cv2.waitKey(0) cv2.destroyAllWindows()颜色约定建议固定下来红色框表示污渍绿色线表示划痕蓝色框表示崩边。三个缺陷都抓出来后只要任意一类缺陷存在整张图判为 NG。cv2.putText的坐标要注意OpenCV 里图像原点在左上角y方向向下。文字放在框上方时如果框顶贴到图像边缘文字会跑到图外这时候可以加一个判断把文字位置移进画面内。4. 常见问题与避坑实录4.1 环境报错与读图返回None这个标题对应的是网上被问烂的一类问题比如ModuleNotFoundError: No module named cv2或者cv2.error: OpenCV(4.4.0) ...。No module named cv2基本就是没装好或者装到了别的 Python 环境。用pip install opencv-python安装后在代码运行前先确认一下import cv2 print(cv2.__version__)打印不出版本号说明环境不对。尤其用 PyCharm 或者 Anaconda 的时候经常是终端里能 importIDE 里不能 import这种情况多半是 IDE 的 Python 解释器和你安装包的 pip 不是同一个。去 IDE 设置里把解释器切到安装过 cv2 的那个环境即可。cv2.error类报错更有迷惑性因为错误信息经常指向某个底层函数比如cv2.cvtColor或cv2.findContours。但根子往往在前一步img是None。读图的路径不能有中文保持全英文路径最稳。另外注意cv2.findContours在不同版本里返回值不一样。OpenCV 3.x 返回image, contours, hierarchyOpenCV 4.x 返回contours, hierarchy。网上老代码经常写_, contours, _ cv2.findContours(...)在 4.x 里会直接把contours解构成hierarchy导致报错。统一改成contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)4.2 光照不均引起大量误报这是缺陷检测项目里最容易翻车的点。零件表面如果受环境光影响四角的亮度明显比中心低固定阈值或者 Otsu 会把暗角区域也当成污渍。我之前就被这个坑过一次一张正常的零件图因为边缘反光偏暗污渍检测在四角框出一大片红色矩形。后来想明白这是全局阈值在光照不均下的经典失效。处理办法主要有三种第一用高斯滤波或均值滤波估计背景亮度然后原图减背景bg cv2.GaussianBlur(gray, (101, 101), 0) flat cv2.subtract(gray, bg) # 消除低频亮度差异核越大估计出来的背景越接近大范围光照趋势flat里保留的就是局部灰度起伏。这个操作在我经历过的多个项目里都管用。第二直接用cv2.adaptiveThreshold它在每个像素周围的小窗口里算阈值天然抗全局光照不均但容易把局部纹理放大成噪声。第三如果拍照位置允许把光源调均匀比任何算法都有效。机器视觉圈有句老话好的打光方案能解决一半以上的图像问题。别光在代码里折腾阈值先把光布好。4.3 形态学操作把缺陷弄丢形态学操作是双刃剑。开运算去噪时如果结构元选得太大会把细小的划痕或者小污渍整个腐蚀掉闭运算补洞时如果结构元选得太大又会把两段不相关的区域连成一个。这个问题在三种缺陷同时存在时特别明显。污渍检测里为了去噪用3×3开运算没问题但如果你为了清除污渍内部的纹理把核放大到7×7那旁边的细微划痕就会被一起清掉。虽然污渍检测不需要管划痕但划痕自己有独立的检测通道不受影响。需要注意的是不要图省事只保留一张二值图让三个检测共用。正确做法是三个检测各自拥有从原图灰度出发、独立处理中间结果的权利。表面污渍、表面划痕、边缘崩边三类特征尺度不同形态学参数必须分开调。调参顺序我建议这样走先不加形态学看原始阈值结果分清哪些是目标缺陷、哪些是噪声再把形态学操作加上去一项一项确认是否误删了目标区域最后调节面积阈值把边缘情况收敛。4.4 三类检测相互干扰的处置有时候崩边检测框出的区域同时也会触发污渍检测。原因很直接崩边处边缘缺失往往伴随暗色背景暴露在污渍的暗斑检测里也会形成一个区域。表面看起来“三个缺陷都被抓到了”但这其实是重复报警会给后端判定带来困扰。处置办法是加一个区域屏蔽步骤。先跑崩边检测得到崩边区域的矩形框之后在污渍检测里把落在这些矩形内的轮廓过滤掉for spot in spots[:]: x, y, w, h, _ spot for mx, my, mw, mh, _ in missing: # 判断污渍中心是否落在崩边框内 cx, cy x w / 2, y h / 2 if mx cx mx mw and my cy my mh: spots.remove(spot) break这种处理在所有多缺陷检测项目里都有用。缺陷 A 的高亮区域可能构成缺陷 B 的一部分特征必须明确优先级和屏蔽逻辑。我一般是把形状上更容易定义的缺陷崩边、大块污渍放在前面跑再让其他检测避开它们已经占用的区域。5. 工程落地与扩展建议5.1 批量跑图与NG记录单张图能跑出结果只是第一步真正交付时要处理成百上千张图。我是这么组织批量处理的用一个glob遍历目录下所有图片逐张检测把 NG 结果保存到输出目录同时写进 CSV 记录。import glob import os import csv results [] for path in glob.glob(samples/*.jpg): img, gray load_gray(path) spots detect_spots(gray) scratches detect_scratches(gray) missing detect_edge_missing(gray, tmp_gray) is_ng len(spots) len(scratches) len(missing) 0 if is_ng: result draw_results(img, spots, scratches, missing) cv2.imwrite(fout/{os.path.basename(path)}, result) results.append([path, is_ng, len(spots), len(scratches), len(missing)]) with open(ng_report.csv, w, newline) as f: writer csv.writer(f) writer.writerow([path, ng, spots, scratches, missing]) writer.writerows(results)输出目录里只保留 NG 图既省空间又方便复检。CSV 里的缺陷类型数量很有价值如果一整天统计下来“划痕”明显变多可以反推是上游工序出了问题。视觉检测不只是判 OK/NG它还能当过程统计工具用。5.2 OpenCV 与 Halcon 怎么选说到工业视觉经常会有人拿 OpenCV 和 Halcon 做对比。OpenCV 开源免费算法透明几乎什么都能做但很多时候需要自己拼装处理流程工程化细节要自己踩。Halcon 是商业闭源库里面有大量经过工业验证的算子特别是畸变校正、亚像素轮廓处理、精密测量这些方向封装得非常成熟视觉效果调试工具也好用。缺点是贵而且授权机制比较严项目交付时客户环境也得考虑授权问题。我的建议很直接如果项目里主要是“抓缺陷、找轮廓、算尺寸”这种常规任务而且相机工位固定、现场可控OpenCV 完全够用。如果涉及高精度计量、复杂标定、非标准镜头畸变的处理客户又明确要求成熟可靠那再考虑 Halcon。这个项目里用到的所有操作OpenCV 都能完成性能也足够。实际跑一块1920×1080的图三路检测加起来大概几十毫秒到一百多毫秒完全能满足产线节拍。5.3 什么情况下再上深度学习接触过视觉检测的人总会受“深度学习是不是更准”这个问题困扰。传统 OpenCV 方法的弱点是特征靠人去定义遇到没见过的缺陷形态容易漏检深度学习的强项恰恰是可以自动学习背景和异常的区别。但深度学习不是万能的。首先你得有足够多的缺陷样本几百张起步而且缺陷形态要覆盖全面。其次模型训练需要不断调参、试错、验证周期比传统方法长。最后深度学习的“不可解释性”在工业交付中是个大问题客户会问“为什么把这个区域判定为缺陷”模型很难给出明确答案。我的意见是像“抓出三个缺陷”这种类型相对固定的项目先用 OpenCV 把漏检压到最低只有当你发现缺陷形态实在太随机、背景变化太大、传统方法怎么调都误报一片的时候再把深度学习当成下一层防线用。传统方法打在明处深度学习补在暗处两者并不矛盾。做这类项目到最后我最大的体会是缺陷检测没有一个通用的“银弹”方法。污渍靠面积划痕靠形状崩边靠轮廓每一种缺陷都要找到它最敏感的“那个维度”。代码写到最后反而都很朴素阈值、滤波、找轮廓三板斧而已真正的功夫在参数怎么定、坑怎么避、逻辑怎么彼此不干扰。最后分享一个小技巧新项目启动时拿到十张正常件和十张缺陷件先用最简单的阈值分割把结果打出来再逐步加入形态学、几何约束、区域屏蔽。每加一步就问自己一句“这一步到底是在压误报还是在补漏检”想清楚这个问题比多会十个算子都管用。
返回列表