ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于OpenCV的工业缺陷检测:从图像预处理到形态学与特征判定

基于OpenCV的工业缺陷检测:从图像预处理到形态学与特征判定 简介面向机器视觉缺陷检测场景的完整工程代码包适合本科、硕士及教研人员开展图像处理与目标检测实验。项目以Python实现为主包含97个py源文件并配套大量jpg示例图片、模型权重、C语言扩展模块及标注解析脚本整体结构完整可直接运行验证。压缩包共227个文件大小约49.66MB涵盖模型配置、COCO格式数据处理、推理与评估等关键环节便于系统理解缺陷检测流程。资源由具备多年Matlab仿真与算法开发经验的作者整理内容聚焦缺陷检测这一细分方向可辅助学习深度学习检测模型的搭建与调试。已有657人浏览学习适合需要从零上手缺陷检测工程实现、参考完整代码组织方式或进行实验复现的研究者。1. 缺陷检测为什么值得动手Defect Eye 解决的是哪类工业问题产线质检这个场景里缺陷检测早就不稀奇了但你真去问一线工程师大家普遍承认的一件事是真正把缺陷检测方案稳定跑起来、能扛住换料换光照的靠的往往不是多聪明的模型而是一套把图像处理流水线每个环节都调明白的工程方案。Defect Eye 这个名字听起来像个产品本质上就是一套用 PythonOpenCV 那套图像处理栈实现的缺陷检测思路读图、预处理、分割出可疑区域、按特征判定是不是缺陷、最后输出 NG/OK。适合谁做工业视觉调试的、刚接手产线质检项目的、以及在学校里拿图像处理课题练手的同学。我最早接触这类需求是帮一个做轴承端面检测的供应商调方案他们在 halcon 里做得挺好但换成 Python 就抓瞎阈值不会选、形态学参数全靠试同一个缺陷换个角度就漏检。后来我把整个流程拆成图像处理的标准动作才发现 90% 的坑都出在「把图像变成候选区域」这一步而不是最后的判定逻辑。这篇文章就顺着 Defect Eye 这类方案的完整落地路径走一遍从预处理到形态学从特征提取到现场排错最后给你一个能用的验证习惯。2. 方案选型为什么缺陷检测先用传统图像处理而不是一上来就上深度学习2.1 传统视觉和深度学习的边界什么时候该用 Defect Eye 这类思路很多刚接触缺陷检测的人第一反应是「现在不是都深度学习了吗怎么还在讲阈值和形态学」。这个想法可以理解但你到产线待两个月就明白了深度学习需要足够多的缺陷样本才能训得稳而实际产线上良品率 95% 以上缺陷样本攒一个月可能也就几百张其中还有大量是同类缺陷的不同形态。你拿几十张样本去训 YOLO换一个机台的光照就崩这是非常常见的翻车现场。Defect Eye 这类基于 OpenCV 传统图像处理的方案定位恰好落在「缺陷和背景有可量化的灰度差异」这个前提下划痕、脏污、缺料、裂纹这些缺陷本质上都是图像局部区域的灰度突变或纹理突变。传统视觉用阈值分割把突变区域抠出来再用几何特征判断它是不是真正的缺陷。它不需要大量训练样本参数调整有明确的物理意义而且处理速度在 CPU 上就能跑到毫秒级对产线来说可解释性和稳定性往往比花哨的模型更值钱。2.2 缺陷检测的通用流水线从读图到判定的五个环节我一般把缺陷检测拆成五个环节图像采集与 ROI 裁剪、预处理灰度化、去噪、阈值分割、形态学修正、特征提取与判定。每个环节的输入输出都是图像或区域数据任何一环的参数没调好后面都是白搭。以下是一段最小可运行的 Python 代码把前三个环节串起来你拿一张有缺陷的图就能跑通流程import cv2 import numpy as np # 1. 读图并裁剪 ROI产线原图往往包含背景先手动框出检测区域 img cv2.imread(bearing_endface.png) roi img[100:500, 200:600] # 根据实际视野调整越小越稳 # 2. 灰度化 高斯滤波灰度化是为了去掉颜色干扰滤波是为了去传感器噪声 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5, 5), 0) # 核越大越糊5x5 是通用起点 # 3. 自适应阈值分割把灰度突变区域变成前景 thresh cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize51, C10 ) cv2.imwrite(thresh_result.png, thresh)这段代码的逻辑很直白先按坐标把图像裁剪到检测区域避免视野边缘的无关物体干扰再做灰度化和滤波把图像从「彩色照片」变成「干净的黑白灰度图」最后用自适应阈值分割把「和周围明显不一样」的像素标记出来。自适应阈值比全局阈值聪明的地方在于它会在每个像素周围的小窗口内单独计算阈值产线光照不均匀的时候不会出现一边检得出、一边全黑的情况。参数说明上有三个值得注意的点。blockSize计算阈值的邻域大小必须设成奇数取值越大对光照变化的适应性越强但太大了会把小缺陷也当成背景一般取缺陷尺寸的 2~3 倍C是阈值偏移量越大分割越严格候选区域越少THRESH_BINARY_INV是根据缺陷是暗是亮选择的极性暗缺陷如凹陷、黑点用INV把缺陷提成白色前景亮缺陷如凸起、白斑去掉INV即可。2.3 为什么自适应阈值不是万能解全局阈值的适用场景自适应阈值不是什么时候都好用。如果你的图像背景非常干净、光照稳定比如暗箱环境下的手机屏幕检测全局阈值OTSU反而更稳。OTSU 自动寻找一个把灰度直方图分成两类的阈值计算简单、没有邻域参数要调而且跑得比自适应快不少。反过来如果图像是金属面、反光面或者光照本身有渐变全局阈值就很容易把暗角区域误判成缺陷。我处理过一块 PCB 板检测相机视野边缘光照衰减明显全局阈值把板子四角全检成了脏污换成自适应阈值瞬间正常。实际项目里我的做法是固定光照环境优先用 OTSU光照没法完全控制就上自适应同一个项目里也可以在 ROI 裁剪后对每个 ROI 单独跑一次阈值而不是整张图统一处理。3. 形态学去伪与连通域分析把「候选区域」变成「完整缺陷」的 Python 实现3.1 阈值分割之后为什么还要形态学处理膨胀与腐蚀的本质分割出来的二值图往往很「脏」噪点被当成前景、缺陷内部有空洞、同一个缺陷因为灰度不均断成好几块。这时候直接去做特征提取面积算出来会严重偏小可能把一个真实的划痕漏掉。形态学处理就是解决这个问题的核心是膨胀和腐蚀这两个基础操作。膨胀让前景区域向外扩张能把断裂的缺陷连起来腐蚀让前景区域向内收缩能去掉孤立的小噪点。先腐蚀后膨胀叫开运算作用是去噪先膨胀后腐蚀叫闭运算作用是填补空洞和连接断裂。理解这两个组合就够用了产线缺陷检测里 80% 的形态学需求都是开闭运算的组合。3.2 用开运算去噪、闭运算连接缺陷完整代码与 kernel 参数经验值以下代码在第二节的thresh结果上继续处理目标是得到一张干净的、缺陷区域完整的二值图然后做连通域分析# 1. 开运算先腐蚀再膨胀去掉阈值分割残留的孤立噪点 kernel_open cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opened cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel_open, iterations1) # 2. 闭运算先膨胀再腐蚀把同一缺陷断裂的部分连成整体 kernel_close cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel_close, iterations2) # 3. 连通域标记给每个连通的白色区域一个独立编号 num_labels, labels, stats, centroids cv2.connectedComponentsWithStats( closed, connectivity8 ) print(f找到 {num_labels - 1} 个候选区域) # 减 1 是去掉背景这段代码里开运算用3x3的矩形核做一次迭代能去掉大部分独立的散点噪声又不会伤到稍大一点的缺陷区域闭运算用5x5的椭圆核做两次迭代椭圆核比矩形核更贴合真实缺陷的形状不至于把两个相邻缺陷糊成一团。connectedComponentsWithStats是这一步的核心它返回四个值区域总数、每个像素的标签图、每个区域的统计信息左上角坐标、宽高、面积以及每个区域的中心点坐标。stats是一个num_labels x 5的数组五列分别对应left, top, width, height, area写特征提取逻辑的时候要用cv2.cc_STAT_LEFT、cv2.cc_STAT_AREA这些索引去取不要硬记数字下标代码可读性会好很多。connectivity8表示上下左右和四个对角都算连通缺陷检测一般用 8 连通因为划痕、裂纹这类缺陷经常是对角方向延伸的。3.3 从候选区域到缺陷特征连通域输出的几何参数怎么读拿到stats之后每个候选区域都有五个基本几何参数位置left, top、外接矩形尺寸width, height和像素面积area。这些参数就是后续判定缺陷的原始素材。但要注意这里的面积单位是「像素」不同相机分辨率、不同工作距离下同一个缺陷的像素面积差异巨大。我见过有人直接把面积阈值写死成 100 像素换了个 500 万像素的相机之后所有微小瑕疵全部报警最后只能把所有阈值重新标定一遍。一个成熟的做法是把面积阈值和图像分辨率关联起来。比如先标定出「在实际缺陷最小尺寸下对应的像素面积是多少」再把这个值作为阈值的基准。换算公式是缺陷实际尺寸除以像素当量mm/pixel得到像素面积下限。下一节详细讲判定逻辑时我会给出这套参数的具体设计方法。4. 缺陷判定与分级特征提取逻辑与三个必调参数4.1 为什么不能只看面积缺陷判定需要多维几何特征很多初版方案只用一个面积阈值就敢上产线结果误检率高得离谱。原因很简单面积只能反映缺陷「有多大」反映不了缺陷「长什么样」。同样是面积 500 像素的区域可能是脏污也可能是反光造成的亮斑还可能是边缘的一道毛刺。判定的维度至少要有四个面积area、宽高比aspect ratio、填充率fill ratio、灰度对比度contrast。宽高比用来识别划痕和裂纹这类细长缺陷它们的宽高比往往在 5 到 20 之间而脏污通常更接近圆形填充率是区域面积除以外接矩形面积用来区分「实心块」和「细长条」也能识别出边界毛刺毛刺的填充率极低灰度对比度是缺陷区域和周围背景的灰度差一个区域再大如果它和背景灰度几乎一样那大概率是误分割出来的不是真缺陷。4.2 构造缺陷判定函数规则写在代码里参数留作接口以下代码把判定逻辑封装成一个函数每个参数都作为函数的入参暴露出来方便后续批量调参import cv2 import numpy as np def is_defect(area, width, height, fill_ratio, min_area100, min_aspect0.2, max_aspect5.0, min_fill0.3): 基于几何特征判断候选区域是否为缺陷 # 1. 面积下限过滤小噪点数值依赖相机分辨率和缺陷最小尺寸 if area min_area: return False # 2. 宽高比过滤正常纹理和规则边缘 aspect width / max(height, 1) # 防止除零 if aspect min_aspect or aspect max_aspect: return False # 3. 填充率过滤边界毛刺和细长背景纹理 if fill_ratio min_fill: return False return True # 遍历连通域逐个判定 def scan_defects(closed_img, labels, stats, centroids): defects [] for i in range(1, stats.shape[0]): # 从 1 开始0 是背景 area stats[i, cv2.cc_STAT_AREA] left stats[i, cv2.cc_STAT_LEFT] top stats[i, cv2.cc_STAT_TOP] width stats[i, cv2.cc_STAT_WIDTH] height stats[i, cv2.cc_STAT_HEIGHT] fill_ratio area / max(width * height, 1) if is_defect(area, width, height, fill_ratio): defects.append((left, top, width, height, area)) return defects # 标记检测结果绿色框表示判定为缺陷的区域 result_img cv2.cvtColor(closed, cv2.COLOR_GRAY2BGR) for (x, y, w, h, area) in scan_defects(closed, labels, stats, centroids): cv2.rectangle(result_img, (x, y), (x w, y h), (0, 255, 0), 2)判定逻辑分三条链先过滤掉面积不达标的微小噪点再用宽高比排除「太扁」或「太细长」的正常纹理最后用填充率排除边界毛刺和规则背景。注意width / max(height, 1)这里的max是为了防止高度为 0 时除零报错。scan_defects函数遍历所有连通域取出几何参数送给判定函数把判定通过的区域的坐标和面积收集起来最终在图像上画框可视化。三个必调参数是min_area、max_aspect和min_fill。min_area的设置方法前面讲过按像素当量换算max_aspect一般设 5~8如果检测对象是长划痕这个值要往上调因为它直接决定多长的细线算缺陷min_fill是 0.2~0.4 之间设高了会把细长划痕误杀设低了会把边界毛刺放进来。这三个参数你在is_defect的函数签名里直接改默认值就行不要写在函数内部接口留好后面才好做批量回归测试。4.3 缺陷分级同一套判定逻辑怎么输出 NG 等级产线实际需要往往不是简单的 OK/NG而是分级处理A 级缺陷直接报废B 级缺陷返修C 级缺陷放行。分级逻辑可以在is_defect的基础上扩展比如面积超过某个上限判为 A 级宽高比特别大但面积小判为 C 级。另外这类几何判定逻辑和 halcon 里的region_features算子是一一对应的你在 halcon 里调的area、width、height、fill_up换成 Python 就是connectedComponentsWithStats的stats输出概念完全打通只是 API 不同。5. 避坑排查缺陷检测最容易翻车的五个现场问题5.1 光照不均导致同一缺陷半边检得出、半边检不出现象阈值分割结果里图像一侧的缺陷清晰可见另一侧同样的缺陷却和背景融为一体检测结果时好时坏。原因固定阈值只能适应一个灰度水平而产线光源衰减、反光、环境光变化都会让图像不同区域的灰度基准不一致。解决优先换成自适应阈值把blockSize调成缺陷直径的 2~3 倍如果自适应还不够就做光照校正——先对原图做一个大核的均值滤波得到背景图再用原图减背景图把光照不均匀的部分去掉这是顶帽变换的思路对金属面反光很有效。5.2 运动模糊让细小划痕直接消失现象静止测试时能检出的细小划痕产线一跑起来就漏检图像放大看边缘是糊的。原因传送带在动相机曝光时间太长导致运动模糊细长缺陷的灰度对比度被模糊抹平了。解决优先缩短曝光时间配合频闪光源补光这是硬件侧最有效的方案算法侧可以加一步锐化cv2.filter2D配拉普拉斯核把边缘对比度拉回来但锐化也会放大噪声属于折中方案。这个问题说明白一点缺陷检测的算法再强也扛不住图像采集端的先天不足。5.3 闭运算迭代次数太多把相邻缺陷连成一片现象两个相距很近的独立缺陷在检测结果里变成一个巨大的缺陷区域面积远大于实际误判成严重缺陷。原因闭运算迭代次数设置过大MORPH_CLOSE把两个缺陷之间的间隙也填平了。解决闭运算迭代次数控制在 1~2 次核的尺寸不要超过缺陷间距的一半。检查方法很直接把闭运算前后的图像叠在一起看如果发现原本分离的区域连上了就减小迭代次数或核尺寸。5.4 形态学核选太大细长划痕被直接腐蚀没了现象开运算之后划痕类缺陷大面积消失检测结果反而比不开运算时更差。原因开运算的腐蚀步骤会把宽度小于核尺寸的结构整个抹掉。一条宽 2 像素的划痕遇到5x5的矩形核直接就没影了。解决开运算核的尺寸取缺陷最小宽度的 1/2 到 1/3宁可用小核多迭代两次也不要用大核一次到位。另外对细长缺陷开运算时优先用椭圆核而不是矩形核各向同性的结构元素对方向不敏感。5.5 在样本 A 上调好的阈值换一批料全面崩盘现象调试时用 10 张图反复调参效果完美上了产线换一批材料误检率飙升到 30% 以上。原因参数过拟合到了调试样本上这批料本身的灰度分布和表面纹理跟上一批有细微差异。解决从第一天就开始积累样本库覆盖至少 30 张包含各类缺陷形态的图每调整一次参数就在这 30 张上做批量回归统计漏检数和误检数。手动一张张看效果永远无法发现参数是不是过拟合了这是血泪经验。注意任何参数调整行为都要记录「改了哪个参数、从多少改成多少、为什么改」。没有记录的调参等于没调下次翻车时连回滚的依据都没有。6. 用最小样本集回归验证算法再上产线一个值得养成的调试习惯与其每次都在现场拿几张图反复试参数不如花半天时间搭一个最小回归脚本把「调参」变成「可量化的训练循环」。做法是建一个样本目录按照good/和ng/分类放好图片每张 NG 图配一个手工标注的缺陷框可以用一个简单的 JSON 文件记录坐标脚本跑完自动对比检测结果和标注结果输出漏检率和误检率。import os import json import cv2 def evaluate_alg_on_dataset(ng_dir, good_dir): tp fp fn 0 # 遍历 NG 样本检测出的缺陷框和标注框有交集就算检出 for name in os.listdir(ng_dir): img cv2.imread(os.path.join(ng_dir, name)) ann_path os.path.join(ng_dir, name.replace(.png, .json)) with open(ann_path, r) as f: ann json.load(f) dets run_defect_detection(img) # 复用前文封装好的流程 hit any(calc_iou(d[bbox], ann[bbox]) 0.5 for d in dets) if hit: tp 1 else: fn 1 # 遍历 OK 样本只要出现任何检测框就算误检 for name in os.listdir(good_dir): img cv2.imread(os.path.join(good_dir, name)) dets run_defect_detection(img) if len(dets) 0: fp 1 recall tp / max(tp fn, 1) false_alarm fp / max(len(os.listdir(good_dir)), 1) return recall, false_alarm这个脚本的run_defect_detection就是你前几节写完的那条流水线calc_iou是计算两个框交并比的辅助函数通常用「检测框中心点落在标注框内」当命中标准比完整 IOU 更方便实现。第一次跑出结果后把你的当前参数作为基线记录下来以后每次调参只允许一个指标变差不超过 1%否则就回滚。我现在的习惯是参数只增不减但每改一个参数都跑一遍这个回归脚本30 张图几秒钟出结果长不了多少时间换来的是换料之后睡得着觉。这套验证流程坚持用下来你会发现自己对缺陷检测的理解从「调阈值」变成了「建基线、做回归、控回归」应对新项目时也会有底气得多。希望你也能少走我趟过的那些坑。本文还有配套的精品资源点击获取
返回列表