简介:针对图像缺陷检测任务,这里提供一套完整的 Defect Eye 缺陷检测实现,配套 Python 代码、预训练模型与说明文档。资源主要面向本科、硕士阶段的教研学习,也适合机器视觉开发者参考完整检测流程的工程落地。压缩包共包含 227 个文件,整体大小约 49.66 MB;其中 JPG 图像用于样本展示和结果可视化,Python 脚本覆盖模型定义、训练、验证与推理,Markdown 文档给出使用说明,C/C++ 源码用于加速掩膜计算和边界框处理,TFRecord 数据文件提供标准数据格式,COCO 评估 Notebook 演示了评估流程,另有预训练权重可供迁移学习。目前已有 657 人浏览/学习过这份资源,说明其具备一定参考价值。借助源码与权重,可复现基于 Mask R-CNN 的缺陷检测流程;Notebook 示例能够帮助理解从数据加载到结果评估的完整链路。内置运行结果支持对照验证,便于快速定位模型效果。这套实现可用于产品表面缺陷定位与识别,适合课程设计、论文复现与项目预研等场景。
1. 缺陷检测的「黑匣子」从哪打开:Defect Eye到底解决什么问题
刚接触图像处理的人最容易把缺陷检测想象成一个万能相机——对着产品拍一张照,坏没坏AI说了算。实际在现场待过的人都明白,真正决定检测项目成败的往往不是算法有多高级,而是图像采集能不能把缺陷从背景里「逼」出来。Defect Eye这类缺陷检测方案的核心,不是某个模型多聪明,而是把差异分析、阈值分割和形态学后处理串成一条可落地的流水线,让没有深度学习团队的小型质检项目也能在Python里跑通闭环。这篇文章要解决的就是一个问题:给你一个带Python代码的缺陷检测项目,你该怎么拆、怎么调、怎么让它经受住产线上真实样本的考验。适合正在做工业质检、零件外观检测、表面缺陷识别的人,尤其是打算从传统视觉切入的工程师——这条路比一上来就训U-Net靠谱得多。
2. 缺陷检测的起点在图像采集:三个变量决定检测上限
2.1 缺陷能不能被「看见」:分辨率、曝光和光源的现场逻辑
很多Python代码跑不出效果,问题不在代码,在相机拍到的东西压根没法用。缺陷检测里有个长期被忽略的事实:算法只能放大差异,不能创造差异。一个划痕如果宽度小于单像素对应的物理尺寸,那不管后面的差影、OTSU、形态学写得多漂亮,它都不会出现在二值图里。所以拿到项目的第一件事永远是确认分辨率——一个视野100mm的相机配500万像素,单像素大概0.02mm,想检测0.05mm的细微划痕,理论上够,但边缘模糊后灰度过渡区会把真实边界吞掉,所以真正可靠的经验是让最小缺陷占到5到7个像素以上,否则检测就是个概率游戏。
曝光同样敏感。缺陷检测跟人眼看图不一样,人眼能自动适应亮暗,传感器不能。曝光过强会让浅色缺陷的灰度差被压缩到几个灰度级以内,差影后根本拉不出峰值;曝光不足则让暗色背景和泛白缺陷混在一起,阈值分割永远分不开。现场最快的标定方法是:放一个合格品,调曝光让背景灰度落在120到180之间,这个区间能留给缺陷至少40到80个灰度级的冗余空间,后续无论是固定阈值还是OTSU都有得玩。
光源方向更要命。轴承缺陷检测、机油盖表面划痕这类场景,缺陷是带有方向性的——拉丝、车削纹、磕碰的阴影方向都不一样。正面打光只能让划痕变浅,低角度环形光能让微小凹凸产生显著阴影差,这也是为什么很多Halcon缺陷检测方案里都会强调光源角度。Python代码里加再多滤波都是后期补救,不如前期花半天时间把光源调到「缺陷自带阴影」的状态,检测难度直接降一个量级。
2.2 用Python做图像采集侧预处理:一套最小可运行管线
图像采集端的核心目标就两个字:稳定。同一个产品今天拍和明天拍,灰度分布必须高度一致,否则差影法的基准模板就会失效。我一般会在代码入口先做三步固定处理:统一尺寸、转灰度、光照归一化。尺寸统一是为了让模板差分时像素一一对应,光照归一化则是为了让不同批次采样时环境光的缓慢漂移不影响缺陷判断。
import cv2 import numpy as np def preprocess(image, target_size=(512, 512)): # 统一尺寸:缩放时用INTER_AREA,避免缩小过程产生额外的高频噪声 resized = cv2.resize(image, target_size, interpolation=cv2.INTER_AREA) # 转灰度:缺陷检测几乎不需要颜色信息,灰度图能减少计算量并增强对比度 gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) # 光照归一化:用高斯差分去除低频背景照度不均 blur = cv2.GaussianBlur(gray, (31, 31), 0) normalized = cv2.subtract(gray, blur) # 归一化到0-255范围,保证后续阈值分割有稳定的灰度分布 normalized = cv2.normalize(normalized, None, 0, 255, cv2.NORM_MINMAX) return normalized这套管线的逻辑是:先用高斯模糊估计背景光照分布,再用原图减去背景得到高频差异部分,这一步在OpenCV里叫背景扣除,效果很接近顶帽变换但参数更好控制。高斯核大小这里用了31,对于512分辨率的图来说,核越大保留的低频信息越少;如果缺陷本身是大面积脏污或氧化色斑,核就要缩到15左右,否则会把缺陷当成背景一起减掉。cv2.subtract比直接用-运算符更安全,它能自动做饱和截断,防止出现负像素值导致后续处理异常。
光照归一化不是万能的,一旦现场光源出现频闪或光源老化,灰度漂移会直接传导到差影结果里。所以预处理管线的输出建议在调试阶段做可视化保存,每隔一段时间随机抽几张图确认灰度直方图形状是否稳定,这是成本最低的质量监控手段。
2.3 采集端的三个自检问题:调试前先回答
拿到任何缺陷样本,第一件事不是跑代码,而是问三句话:缺陷在灰度图上的对比度是多少个灰度级?缺陷区域占图片总面积的比例是多少?缺陷边缘是否清晰锐利?对比度低于20个灰度级的,先回头调光;占比小于0.1%的,先确认分辨率够不够;边缘模糊到看不出轮廓边界的,用再强的边缘检测算子也是白费。这三句话能筛掉项目里八成以上的无效加班,是比任何算法参数都重要的前置经验。
3. 用传统视觉思路跑通Defect Eye核心流程:差影、阈值与形态学
3.1 为什么传统方案比深度学习更适合起步
不少人一听说缺陷检测就想着PyTorch、YOLO、分割模型,但真实的小批量质检场景里,传统视觉方案的优势反而更明显。首先,传统方案不需要标注数据,一个合格品模板加几十张缺陷样本就能把流程跑通;而深度学习做分割,最少也要几百张精细标注图,这个成本在小项目里根本摊不平。其次,传统方案的决策过程可解释,缺陷判定的依据是阈值、面积、连通域这些物理量,现场人员能用逻辑排查问题,不是对着一个黑匣子发呆。
Defect Eye这类工具的典型结构其实就是传统视觉思路:模板差分找差异、自适应阈值分离前景、形态学剔除假阳性。这套组合在轴承端面划痕、机油盖毛刺、PCB板缺件、纺织品瑕疵上都能用,只是参数要按场景重调。它的本质是「异常检测」——不管缺陷长什么样,只要跟合格品有显著差异就被抓住,这种思路对未知缺陷的泛化能力反而是深度学习方法不具备的。
3.2 核心检测代码:差影法加OTSU加闭运算的最小闭环
传统缺陷检测的主干逻辑可以浓缩成四个步骤:做差影、自适应阈值、形态学连通、面积过滤。下面这段代码就是Defect Eye这类方案的最小核心,它可以Fork成任何项目的基础推理模块。
import cv2 import numpy as np def detect_defect(test_img, template_img, min_area=150, morph_size=7): # 第一步:差影法,逐像素相减并取绝对值,保留两图之间的差异 diff = cv2.absdiff(test_img, template_img) # 第二步:OTSU自适应阈值,自动寻找分出前景和背景的最佳灰度界限 _, thresh = cv2.threshold(diff, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 第三步:闭运算,先膨胀再腐蚀,把缺陷内的小空洞和不连续断口连起来 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (morph_size, morph_size)) closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2) # 第四步:连通域分析,用面积过滤掉孤立噪声点 num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(closed, connectivity=8) defects = [] for i in range(1, num_labels): area = stats[i, cv2.CC_STAT_AREA] if area >= min_area: x, y, w, h = stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], \ stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] defects.append((x, y, w, h)) return defects这段代码里最关键的两个自由度是min_area和morph_size。min_area是缺陷面积下限,它决定什么规模的噪声能晋级为缺陷;设太小会把灰尘、反光点全部拉进来,设太大又会放过真实的小缺陷。我一般会统计合格品图上所有连通域的面积分布,取最大值的1.5到2倍作为初始值。morph_size控制闭运算的结构元大小,值越大连通能力越强,细小划痕会被连成完整主线;但如果缺陷本身就是细长线条,核太大会把相邻的几条划痕并成一块,丢失缺陷数量信息。
OTSU是这套方案里唯一不需要手调的参数,但它的弱点是依赖灰度直方图呈双峰分布。当缺陷面积太小、在直方图里形不成独立峰时,OTSU会把阈值推高到假阴性区域,缺陷直接被吞掉。遇到这种样本,就把差影后的灰度图做一次线性拉伸再进OTSU,缺陷和背景的峰会被拉开,分割效果明显改善。
3.3 特征提取:把「有没有异常」升级成「缺陷长什么样」
定位出缺陷区域只完成了第一步,实际产线还要分类——划痕、脏污、凹坑、毛刺的处理流程完全不同。此时要进入特征提取阶段,从每个连通域上计算物理特征。最常见的做法是提取面积、长宽比、灰度均值和轮廓周长,这几个特征足以区分绝大多数简单缺陷。
def extract_features(binary_img, labels, stats, defect_boxes): features = [] gray = cv2.cvtColor(binary_img, cv2.COLOR_BGR2GRAY) if len(binary_img.shape) == 3 else binary_img for i, (x, y, w, h) in enumerate(defect_boxes): roi_label = (labels[y:y+h, x:x+w] == (i + 1)).astype(np.uint8) # 轮廓周长反映缺陷边界的复杂度,边缘越曲折越可能是刮伤 contours, _ = cv2.findContours(roi_label, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) perimeter = cv2.arcLength(contours[0], True) if contours else 0 roi_gray = gray[y:y+h, x:x+w] area = stats[i + 1, cv2.CC_STAT_AREA] aspect_ratio = w / h if h > 0 else 0 features.append({ 'area': area, 'aspect_ratio': aspect_ratio, 'perimeter': perimeter, 'mean_gray': roi_gray.mean(), 'bbox': (x, y, w, h) }) return features长宽比对划痕和凹坑的区分度最高:划痕的长宽比往往大于5,凹坑接近1。灰度均值则能区分暗缺陷和亮缺陷,脏污通常是暗的,金属反光点是亮的。把这些特征输出成表格后,可以按缺陷类型设定分类阈值,也可以用简单的KNN分类器做自动分类。这一步的价值在于让缺陷检测从「检测有无」升级到「识别类别」,是产线自动分拣的前提。
4. 避坑:缺陷检测项目里最常见的5个翻车现场
4.1 漏检集中在某个灰度区间,调阈值就误报
现象:检测结果里特定批次的浅划痕总是漏掉,同一张图把OTSU阈值手动调低后,合格品的边缘反光又被全部误判成缺陷。
原因:这类问题的根源通常是差影图的灰度分布不是标准双峰,缺陷灰度差异集中在30到50之间时,OTSU会把阈值选在背景峰和缺陷峰之间的谷底偏上方,导致低对比度缺陷被截断。阈值调低后又把噪声峰纳入了前景。
解决:不要硬调全局阈值,改为在OTSU结果上叠加一个灰度差异下限。做法是先用固定阈值30做一次粗分割,再用OTSU做精分割,两个结果取交集。这样低对比度缺陷能被保留,而孤立噪声点因为缺乏空间连续性会被面积过滤吃掉。
4.2 同一套代码换台相机就失灵
现象:实验室用工业相机调试好参数后,项目交付到现场换了另一品牌相机,同一批合格品误检率从1%飙到20%。
原因:不同相机的传感器响应曲线、增益策略和自动白平衡默认值不同,导致相同光照下输出的灰度值整体偏移。更隐蔽的是有些相机会自动开启边缘增强,让同一条划痕的灰度梯度在两台相机上完全不同。
解决:把相机厂商的SDK里所有自动增益、自动曝光、自动白平衡全部关闭,改用固定参数采集。如果相机不支持完全关闭,就在预处理管线里对灰度做直方图匹配,以模板图的累积分布函数为基准,把现场图重新映射到同一灰度分布上。注意这个操作必须在差影之前完成,否则模板和新图之间会自带系统性偏差。
4.3 差影后边缘一圈全是噪声
现象:模板图和待测图差影后,产品轮廓边缘出现一圈高亮响应,连通域分析后边缘处冒出大量小面积假缺陷。
原因:产品在相机视野里的位置有轻微偏移,或者传送带抖动让同一像素点对应了产品的不同物理位置,边缘区域的结构光变化被差影当成差异。这个问题在轴承、齿轮这类圆形工件上尤其明显,圆周边界的亚像素位移就能产生巨大灰度跃变。
解决:最可靠的做法是检测前先做配准,用模板匹配找到待测图相对模板图的平移量,再对图做仿射变换对齐。在OpenCV里用cv2.matchTemplate配合TM_CCOEFF_NORMED找峰值偏移即可,亚像素精度可以再对峰值周围做二次插值。如果配准成本太高,至少要在差影后用掩膜把产品边缘区域排除掉,别让边缘响应进入面积统计。
4.4 形态学参数当玄学调,不如重新审视图像质量
现象:闭运算核的尺寸从3一路调到15,漏检率始终降不下来,误报倒是涨了一截,越调越灰心。
原因:形态学参数本质上是后期补偿手段,它的调节空间很有限。闭运算可以填平缺陷内部的不连续断口,但它无法创造原本不存在的灰度差异。如果缺陷在差影图和阈值分割阶段就没被完整分离出来,靠形态学补救等于把放大镜换成显微镜看一张模糊照片。
解决:遇到调参无效的情况,先回到采集端。检查缺陷区域灰度对比度是否足够,检查归一化参数是否被子图内容带偏。我常用的判断标准是:把缺陷区域从差影图上抠出来,看它的最大灰度值是否达到背景最大值的两倍以上,达不到就先调光源和曝光,而不是继续调形态学参数。形态学永远只处理「已经有但连不起来」的缺陷,处理不了「完全看不见」的缺陷。
4.5 面积过滤规则和产品换型不兼容
现象:A型号产品跑得好好的检测配置,切到B型号后漏检率上升,把面积阈值调小又出现大面积误报。
原因:不同型号的产品尺寸、表面纹理和加工痕迹不同,合格品自身的连通域面积分布也不同。A型号上噪声连通域面积都在100像素以内,B型号的表面纹理本身就产生200像素以上的连通域,沿用A的面积阈值自然不成立。
解决:把面积阈值从硬编码改成随产品型号加载的配置项,换型时自动切换。更好的方案是用合格品图像自动统计噪声面积分布,以95分位数乘1.5作为动态阈值,逻辑是每个型号上线时先拍5张合格品做一次自动标定。我在现场项目里都是这么干的,换型不再是一场手动调参灾难。
5. 验证进阶:从检测框到F1曲线,再到该不该上深度学习
5.1 用F1值而不是肉眼验收
传统视觉方案最容易陷入的陷阱是「看着图片觉得检测效果不错」。真实的验收标准必须是可量化的F1值,也就是精确率与召回率的调和平均。要算F1,手头必须有两类样本:标注过缺陷位置的缺陷图50到100张,干净合格品图100张以上。跑完检测后,把算法输出框跟标注框做IOU匹配,IOU大于0.5算检测到,统计出精确率和召回率。注意合格品图上的误报率同样重要——一个在合格品上频繁报警的检测系统,产线永远不会接受。
5.2 做样本增强让系统更耐造
传统视觉方案不需要数据训练,但需要验证数据能覆盖现场的真实变化。最实用的做法是收集不同批次、不同时段、不同光照偏移下的合格品图,用它们反复验证差影方案是否会产生误报。还可以在代码里模拟光照漂移和图像噪声,对验证图做微量旋转和平移加高斯噪声,检验检测系统的鲁棒性边界。这一步的花费很低,但能让交付后的维护电话少一大半。
5.3 什么时候必须转向深度学习
传统方案的边界很清楚:缺陷与背景灰度差异明显、缺陷种类有限、样本形态稳定时,它是最划算的方案。但当缺陷是纹理类异常——织物瑕疵、木材表面的细微色差、铸件表面的复杂氧化——灰度差影已经分不出前景时,就该考虑用分割模型。常见做法是先收集几百张标注图,用U-Net或DeepLabV3做粗糙分割,把它当传统方案的「增强差影模块」,输出二值图后再接连通域和面积过滤。这个混合路线的落地可靠性远高于直接端到端输出检测框。
说到这想起一个教训:我在一个机油盖毛刺项目里,光学调试花了三周没搞定,后来发现问题根本不是打光角度,而是相机增益没关导致灰度一直在漂——代码里一行camera.gain = 0解决的事。从那以后我给自己立了个规矩:差分没有稳定的输入,就没有稳定的输出。希望帮到你。
本文还有配套的精品资源,点击获取