简介:这是一份聚焦“Defect Eye”缺陷检测任务的完整Python项目,面向图像处理、机器视觉方向的本科、硕士研究者,适合用于深度学习目标检测与实例分割教学、科研复现以及工业质检预研。压缩包共227个文件、49.66MB,涵盖97个Python脚本、103张标注图像、Cython/C++扩展模块(如_mask.c、bbox.c、nms.c、maskApi.c)及Jupyter演示笔记,模型权重resnet_v1_101.ckpt与tfrecord格式标注数据也已包含在内,可直接开展推理或继续训练。包内代码与COCO数据集评价协议深度耦合,从maskApi到gason解析均给出具体实现,能让读者清晰理解缺陷掩膜生成、边界框回归与评估指标计算的底层机制,为二次开发提供清晰路径。目前已有657人下载学习,借助随包图像样本和分步示例,可快速上手缺陷检测全流程,包括数据组织、模型调用、可视化与结果评估,对后续自建缺陷数据集和算法改进具有较高的参考价值和实用意义。
1. Defect Eye缺陷检测:为什么传统图像处理在产线仍然值得复现
产线上最让人头疼的检测任务,往往不是“AI 是否炫酷”,而是“今天能不能在预算内跑通一根直线”。标题里的 Defect Eye 就是一套用 Python 写的缺陷检测方案,靠图像处理把缺陷从背景里“揪”出来,而不是靠海量图片喂模型。如果你手里已经有几张缺陷样图,想快速验证检测思路,或者要给项目做一个不依赖 GPU 的初级检测模块,这套逻辑是性价比最高的起点。它适合轴承表面、螺栓螺纹、硅片边缘这类背景纹理相对稳定的场景,也适合拿来做图像处理课程作业的完整流程演示。
很多初学者看到“附 python 代码”就以为是现成黑匣子,解压就能用。实际落地时真正值钱的不是那几百行代码,而是代码背后的缺陷检测链路:读图、灰度化、背景差分、阈值分割、形态学过滤、连通域标注。这套链路在传统视觉领域用了几十年,今天放在 Python 和 OpenCV 环境下依然能解决一大半的表面缺陷检测需求。下面我按自己的落地经验,把整个方案拆开讲清楚,从原理到参数,再到踩坑记录,最后给你一个能扩展成批量检测的工具雏形。
2. Defect Eye的检测思路:边缘、差分和形态学背后的一盘棋
2.1 缺陷检测的本质:异常区域与背景基准的差异
不管叫 Defect Eye 还是叫别的名字,缺陷检测要解决的数学问题只有一个:在图像矩阵中,找到那些与周围像素没有连续性的区域。连续性可以是灰度、梯度、频率,甚至颜色。表面缺陷如划伤、污点、凹坑、裂纹,在图像上通常表现为局部灰度突变。如果你的产线背景是同一批次、同一光照下的平整表面,那么一张干净的参考图就是最好的基准。
灰度差分是最直接的异常度量。设检测图像为 S(x,y),背景参考图为 B(x,y),差分图像 D(x,y)=|S(x,y)-B(x,y)|。差分结果是背景被减掉、缺陷被保留的响应图。接下来用阈值 T 把 D 变成二值图,灰度差大于 T 的点视为候选缺陷像素。Defect Eye 的核心就是这个过程,后面的形态学和连通域处理,只是为了把这些散点整理成可报告的缺陷框。
这里有一个关键前提:参考图和检测图中的同一坐标必须对应同一个物理位置。如果 ROI 区域在背景图里比检测图偏移了哪怕两个像素,差分图会出现一圈明显的边缘残差,很容易被误判成缺陷。所以我在实际使用中,要求相机固定、工作台定位固定,再做一次模板坐标校准。这一点我会在避坑章节展开。
2.2 为什么不直接上深度学习:数据量、标注成本和稳定性
做缺陷检测的同行常常被问“你怎么不用深度学习”。这个问题得分场景回答。深度学习模型确实能在复杂纹理、随机背景上击败传统算法,但代价是数百张标注缺陷图、GPU 训练环境、以及模型在换产线批次之后的未知行为。很多工厂的真实情况是:缺陷样本总共不过几十张,正负样本极端不平衡,质检员也没有时间逐像素标 mask。这时传统图像处理反而更容易解释,也更稳定。
Defect Eye 采用的“背景差分+形态学”方法,本质上是人工定义“什么是缺陷的可接受变化”。光照稍有变化,阈值调一调;缺陷尺寸有下限,面积过滤加一个参数。这种黑匣子程度低、可调点明确的特点,让它在轴承缺陷检测、螺栓缺陷检测这类结构化表面上仍然有一席之地。当然,如果表面纹理是随机分布的布料、皮革,差分法会严重翻车,考虑用纹理统计或深度学习更合适。
2.3 Defect Eye检测链路:预处理、差分、二值化、形态学、连通域
一套完整的 Defect Eye 流程可以拆成六个环节,每个环节都有一个必须控制好的关键点。第一,读图与解码,不要直接用 cv2.imread 读中文路径图片。第二,灰度化与降噪,常用 COLOR_BGR2GRAY 加高斯模糊,目的是让传感器噪声在差分前先被压下去。第三,背景差分,用 cv2.absdiff 计算差分的绝对值,这比直接相减更能保留正负两类灰度偏差。第四,二值化,用固定阈值或者自适应阈值把差分图转成黑白的候选区域。第五,形态学处理,开运算去掉孤立噪点,闭运算把断开的缺陷边缘接回来。第六,连通域分析,用 cv2.connectedComponentsWithStats 得到每个候选区域的外接矩形和面积,按最小面积和最大面积过滤。
顺序不能随便乱换。有人喜欢先二值化再模糊,结果就是噪声被膨胀成大块假目标。有人跳过形态学,连通域里全是碎点。Defect Eye 并不神秘,它就是把这六个环节组合成了稳定流水线。下一个章节我会给出可以直接运行的 Python 代码,并解释每个函数背后的参数选择逻辑。
3. 用Python复现Defect Eye最小检测代码:从读图到缺陷框标注
3.1 代码结构:三部分函数式设计
我不喜欢把检测逻辑全部塞进一个 main 函数里,那样参数调试很痛苦。下面的代码按职责拆成三个函数:load_image 负责解决中文路径和编码问题;detect_defects 负责完整检测链路;main 负责解析命令行参数并调用检测函数。你实际开发时可以继续拆分,比如把阈值参数抽成配置文件,但最小复现版足够你理解全流程。
import cv2 import numpy as np import argparse def load_image(path): # 常规 cv2.imread 在中文路径下会返回 None,这里先用 np.fromfile 读二进制再解码 data = np.fromfile(path, dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR) if img is None: raise ValueError(f"无法读取图片,请检查路径: {path}") return img逻辑说明:cv2.imread 其实是 OpenCV 的一个历史坑,它在 Windows 下遇到含中文或空格的路径时经常返回 None,而程序不会报错,后续 cvtColor 会直接崩掉。用 np.fromfile 加 cv2.imdecode 是兼容性最好的做法,不管路径是中文、日文还是带特殊符号都能读。参数解析方面,np.fromfile 的 dtype 必须指定为 np.uint8,因为图片编码流的原始字节就是无符号 8 位整型,用其他类型会导致解码失败。
3.2 灰度转换与背景差分:先降噪再算差
检测链路的第一步是读检测图和参考图,转灰度,高斯模糊,然后做差分。高斯模糊的核大小直接影响差分结果中的噪点数量。核设置太小,传感器噪声被放大;核设置太大,细小缺陷也被抹平。我一般先固定为 (5,5),后续再针对具体图微调。
def detect_defects(src_path, bg_path, params): src = load_image(src_path) bg = load_image(bg_path) gray_src = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) gray_bg = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY) # 高斯模糊:sigma 由 OpenCV 根据核大小自动计算,这里传 0 即可 gray_src = cv2.GaussianBlur(gray_src, (params['blur_ksize'], params['blur_ksize']), 0) gray_bg = cv2.GaussianBlur(gray_bg, (params['blur_ksize'], params['blur_ksize']), 0) # 背景差分:absdiff 对正负灰度差一样敏感 diff = cv2.absdiff(gray_src, gray_bg) return diff逻辑说明:转灰度后图像从三维变成二维,数据量减少三分之二,而且差分运算只关心灰度强度,颜色信息在这里没有帮助。高斯模糊是低通滤波器,它保留大尺度的区域变化,滤掉高频的传感器噪点。这里有一点容易被忽略:参考图和检测图都要用完全相同核大小的高斯模糊,否则两张图的噪声分布差异会叠加到差分图上,出现很多假边缘。cv2.GaussianBlur 的第三个参数 sigma 传 0,OpenCV 会自动根据核大小计算 sigma,不需要手动指定。
3.3 二值化与形态学过滤:把差分图变成干净的缺陷区域
拿到 diff 图后,需要设定阈值找出“显著差异”的像素。阈值太低会把微小的灰度波动当成缺陷;阈值太高会漏掉浅划痕。二值化之后紧接着做开运算和闭运算。开运算是先腐蚀后膨胀,用来分离粘连的噪点;闭运算是先膨胀后腐蚀,用来填补缺陷内部的微小空洞。
# 固定阈值二值化,cv2.THRESH_BINARY 是大于阈值设为255,小于设为0 _, th = cv2.threshold(diff, params['threshold'], 255, cv2.THRESH_BINARY) # 形态学核:数字越大,越在意大尺寸缺陷,但太小缺陷会被提前抹掉 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (params['morph_m'], params['morph_n'])) opened = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel) closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel)逻辑说明:threshold 的第二个参数是阈值下限,光照稳定时通常设在 20 到 50 之间。这个参数是整个方案里最敏感的,没有之一。形态学核如果是 3x3 大小,可以去掉单像素噪点,但对大块缺陷内部的孔洞无济于事。如果是 5x5 甚至 7x7,小缺陷在开运算阶段就会被当成噪声腐蚀掉。我给生产环境的默认值是 3x3:开运算一次,闭运算一次,在速度和效果之间取平衡。MORPH_RECT 的核形状不一定最优,如果缺陷方向性很强,比如划痕是横向长条,可以试试 MORPH_ELLIPSE 或 MORPH_CROSS,这里不展开。
3.4 连通域标记:从二值图到缺陷框
二值图里可能存在多个互不相连的白色区域。连通域分析会为每个区域分配一个编号,并计算出外接矩形、面积、中心点。Defect Eye 的最终输出就是一系列矩形框,可以在原图上直接叠加。
# connectivity=8 表示只看上下左右和斜对角邻接,能保留更多斜向缺陷 n, labels, stats, centroids = cv2.connectedComponentsWithStats(closed, connectivity=8) out = src.copy() defects = [] for i in range(1, n): x, y, w, h, area = stats[i] # 跳过面积不合理的候选区域 if area < params['min_area'] or area > params['max_area']: continue defects.append((x, y, w, h, area)) cv2.rectangle(out, (x, y), (x + w, y + h), (0, 0, 255), 2) cv2.putText(out, f"defect_{i}", (x, y - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) return out, defects逻辑说明:connectedComponentsWithStats 返回四个值,其中 stats 每行格式是[x, y, w, h, area],注意第 5 列是像素面积,和宽高乘积不同。背景区域编号为 0,所以从 1 开始循环。min_area 和 max_area 是缺陷筛选的粗过滤,建议先设置比较宽的范围,比如 min_area=50,max_area 为图像面积的十分之一,再根据输出结果收窄。绘制时使用红色 BGR 颜色 (0,0,255),线条宽度 2,字体颜色和框一致。
3.5 命令行运行与参数入口
为了让代码可复现,我把所有参数打包成字典,通过 argparse 从命令行读入。这样可以在不修改代码的情况下快速调节阈值、形态学核和面积范围。
def main(): parser = argparse.ArgumentParser(description="Defect Eye 缺陷检测最小实现") parser.add_argument("--src", required=True, help="检测图路径") parser.add_argument("--bg", required=True, help="背景参考图路径") parser.add_argument("--threshold", type=int, default=30, help="灰度差阈值") parser.add_argument("--blur_ksize", type=int, default=5, help="高斯模糊核边长,必须是奇数") parser.add_argument("--morph_m", type=int, default=3, help="形态学核宽") parser.add_argument("--morph_n", type=int, default=3, help="形态学核高") parser.add_argument("--min_area", type=int, default=50, help="最小缺陷面积") parser.add_argument("--max_area", type=int, default=100000, help="最大缺陷面积") args = parser.parse_args() params = { 'threshold': args.threshold, 'blur_ksize': args.blur_ksize, 'morph_m': args.morph_m, 'morph_n': args.morph_n, 'min_area': args.min_area, 'max_area': args.max_area } out, defects = detect_defects(args.src, args.bg, params) print(f"检测到 {len(defects)} 个缺陷") cv2.imwrite("defect_result.jpg", out) cv2.imshow("Defect Eye Result", out) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == "__main__": main()逻辑说明:blur_ksize 必须是奇数,因为高斯核需要中心对称。morph_m 和 morph_n 拆开设置,是为了支持横向长方形核,比如核尺寸 (1, 5) 可以突出横向划痕。max_area 默认给一个很大的值,是防止误设上限导致大缺陷被丢弃。运行命令示例:
python defect_eye.py --src surface_001.bmp --bg reference.bmp --threshold 25输出结果会写在当前目录的 defect_result.jpg 里。这段代码是整个 Defect Eye 方案的最小骨骼,后面参数调优和批量扩展都建立在这三部分结构之上。
4. Defect Eye参数调优:5个必调参数和它们的匹配逻辑
4.1 参数总览与调优顺序
很多人在复现时发现“照着参数跑出来的图全是框”,或者“一个缺陷都没有”,问题往往不是代码 bug,而是参数组合不符合实际情况。我先把影响最大的五个参数列成表,再解释每个参数应该看什么。
| 参数 | 作用 | 常见取值范围 | 调大后的影响 |
|---|---|---|---|
| threshold | 差分图灰度差阈值 | 15~60 | 假阳性减少,小缺陷漏检增加 |
| blur_ksize | 高斯模糊核边长(奇数) | 3~9 | 图像更平滑,小缺陷被弱化 |
| morph_m/n | 形态学核宽高 | 3~7 | 大噪声被清除,但小缺陷会被腐蚀掉 |
| min_area | 最小缺陷面积 | 30~200 | 跳过更小的孤立噪点 |
| max_area | 最大缺陷面积 | 全图的1%~10% | 过滤大块背景干扰 |
调优顺序我一般按照“先粗后细”:先把 max_area 设成整张图像面积,把 min_area 设为 0,threshold 设 30,跑一次知道当前检测方法在什么水平。然后保存 diff 图和二值化图,看阈值选得是否合理。最后再调形态学核和面积范围。不要五个参数一起动,那样你永远不知道是谁导致翻车。
4.2 光照不均时阈值怎么设
固定阈值最大的敌人是渐变光照。比如金属弧面边缘反光,图像左边亮右边暗,差分图里同一个缺陷在亮区灰度差可能达到 80,在暗区只有 25。这时 threshold 设为 30 会在暗区漏检,设为 20 又会在亮区引入大量反光噪点。
一个可行的办法是先用 CLAHE(限制对比度自适应直方图均衡化)对灰度图做光照校正,再做差分。这样做能压缩大范围光照差异,保留局部灰度突变。具体代码片段如下:
# 替换 3.2 节中的 cvtColor 之后加入 CLAHE clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) gray_src = clahe.apply(gray_src) gray_bg = clahe.apply(gray_bg)clipLimit 控制对比度限制强度,调大能增强弱缺陷,但也会放大噪声。tileGridSize 是分块大小,光照变化越剧烈,块应该越小,比如 8x8 或者 16x16。加了 CLAHE 之后,threshold 通常可以下调到 15~25,因为光照不均已经被局部均衡化抹平了。
4.3 形态学核尺寸与缺陷尺寸的匹配
形态学开运算对缺陷尺寸非常敏感。如果缺陷目标最小尺寸是 5 像素宽,而你的核是 7x7,那么开运算的腐蚀阶段会先把这 5 像素的目标整个腐蚀掉,后面膨胀也救不回来。我总结的经验是:核尺寸不要超过最小缺陷尺寸的三分之二。也就是说,最小缺陷宽度 9 像素时,核最多取 5 或 7。
闭运算的作用是补洞。缺陷如果是“月牙形”划痕,内部可能由于灰度变化留出空隙,闭运算会把它们连成一条完整划痕。核大小按缺陷间距选,间距大就用大核,但注意别把两个附近的真实缺陷连成一个超级缺陷。我在生产环境里常用两组组合:暗场细划痕用 (3, 3) 开 + (5, 5) 闭;大块污渍用 (5, 5) 开 + (7, 7) 闭。
4.4 用中间结果图快速定位参数问题
不要只看最终框选结果。把 diff、th、closed 这三张中间图分别用 cv2.imwrite 存到磁盘,你才能知道是哪一步出了问题。比如 diff 图看起来缺陷很明显,但 th 图里缺陷区域全是空洞,说明 threshold 设置偏高;th 图里噪声和缺陷连成一片,说明开运算核不够大;closed 图里缺陷和边界粘连,说明参考图像没有对齐或者 ROI 范围超出了边界。
下面这段辅助代码可以插入 detect_defects 函数里,调试完再删掉:
# 调试模式:保存中间结果 if params.get('debug', False): cv2.imwrite("debug_diff.jpg", diff) cv2.imwrite("debug_th.jpg", th) cv2.imwrite("debug_closed.jpg", closed)参数配置文件也可以从一开始就做成 JSON,避免反复改 argparse 默认值。常见做法是准备三组预设:dark_field、bright_field、normal,分别对应暗场检测、反光检测和常规环境。每组预设的值来自对当前产线 100 张正常样品的统计,调好之后固化,换线时直接切换,省去大量重复试参数的时间。
5. 避坑:Defect Eye在真实图片上翻车的5种典型情况
5.1 反光和阴影被误检成缺陷
现象:检测图与参考相机位姿完全一致,但检测结果框出大片高亮区域或边缘阴影,而这些区域其实是反光在移动,不是真实缺陷。 原因:反光会导致局部灰度发生大范围渐变,差分图里形成边缘响应,而且往往面积不小,能通过面积过滤。 解决:先上 CLAHE 做光照校正,再提高 threshold;如果还不行,就把反光区域用掩膜排除在 ROI 外。掩膜是一个二值图,在差分前乘到 diff 上,固定反光区域直接变为 0。代码写法是diff = cv2.bitwise_and(diff, diff, mask=mask)。
5.2 参考图像与检测图像轻微位移导致整圈边缘残差
现象:背景图里没有缺陷,但检测结果沿零件外沿框出一圈矩形框。 原因:相机固定不稳或治具定位误差使检测图相对参考图有亚像素级平移。差分计算时,边缘处两侧灰度差异被放大。 解决:在换型开始时做一次模板匹配,用 cv2.findTransformECC 估计仿射变换,把检测图先对齐到参考图坐标系。最小实现是使用 cv2.matchTemplate 找到峰值位置,再按偏移量 warpAffine。仅 1 像素的偏移就可能导致差分残差区域面积超过 min_area,所以这一步不能省。
5.3 小缺陷被形态学核或面积过滤吞掉
现象:人眼能看到的细划痕,检测结果里完全没有框。 原因:开运算核尺寸大于划痕宽度,或者 min_area 设得太高,把真实小缺陷误当噪点。 解决:把核尺寸降到 3x3,min_area 降到 10 或者 0,再观察闭运算后的连通域面积分布。通过代码遍历输出所有连通域的面积,你会发现很多候选区域面积只有十几像素。配合直方图画出来,找到真实缺陷对应的面积区间。宁可在后续逻辑里根据长度宽度比做筛选,也不要一开始用裁减成 100 以上的面积过滤。
5.4 中文路径导致程序崩溃或读出空图
现象:代码在英文路径下跑得好好的,放到中文目录下 cv2.imshow 直接报错,或者打印 img 是 None。 原因:老版本 OpenCV 与 Windows 系统的编码不一致,cv2.imread 无法解析中文路径。 解决:统一使用 3.1 节的 load_image 函数。注意这个地方很容易被忽略:cv2.imdecode 得到的图像颜色通道顺序是 BGR,后续所有颜色相关操作要基于 BGR,不需要转 RGB。这是图像处理里的常见绊脚石。
5.5 检测速度不达标:循环效率与多线程设计
现象:一张 200 万像素图片全流程处理要 300 毫秒,产线节拍要求每帧 80 毫秒。 原因:connectedComponentsWithStats 本身不慢,慢的是你在代码里用 Python 的 for 循环遍历像素,或者每帧调用 CLAHE 时 tileGridSize 设得过小。 解决:第一,不要对每个像素做 Python 级循环,所有操作用 OpenCV 向量化实现。第二,如果用多线程抓流,不要把检测逻辑放进 GUI 线程,用队列传递图像帧。第三,ROI 裁剪是最强的提效手段,把检测范围缩小到实际活动区域,往往能减少 70% 以上的数据量。比如 1920x1080 的图只检测中心 800x600 区域,速度和假阳性率都会立刻改善。如果多相机并行,再用 multiprocessing 把每个相机的检测丢进独立进程。
6. 进阶:把Defect Eye改造成批量检测工具并验证检测一致性
单张检测跑通之后,下一件必须做的事是把脚本扩展成批次处理。我一般会对一个文件夹内所有图片循环检测,并把结果写入 CSV,而不是在屏幕上一张张看一眼。下面这段代码是批量检测的核心骨架:
import os import csv def batch_detect(img_dir, bg_path, params, output_csv): files = [f for f in os.listdir(img_dir) if f.lower().endswith(('.bmp', '.jpg', '.png'))] with open(output_csv, 'w', newline='', encoding='utf-8') as fp: writer = csv.writer(fp) writer.writerow(['filename', 'x', 'y', 'w', 'h', 'area']) for name in files: src_path = os.path.join(img_dir, name) out, defects = detect_defects(src_path, bg_path, params) for d in defects: x, y, w, h, area = d writer.writerow([name, x, y, w, h, area]) cv2.imwrite(os.path.join(img_dir, 'result', name), out)验证检测一致性时,不要只看输出图片,要从 CSV 里去核对同一缺陷是否在连续帧中都出现。一般我会对同一片表面连续拍 20 张,用 Defect Eye 分别检测,统计每个位置缺陷的检出次数。如果某个缺陷只在 2 张里出现,大概率是光照抖动或算法不稳定,需要回头看阈值和 CLAHE 参数。用这种方式跑一轮,比肉眼多看几十张图有效得多。
另一个实用技巧是把每张检测图叠加一个纯色标记,比如把缺陷掩膜区域的原图抠出来另存,方便后期与人工标注做对比。如果后续要上机器学习,这些保存下来的缺陷图像就是天然的候选训练集,能省下很多采集时间。我个人的习惯是在项目开始就维护一个“缺陷样本库”,每次检测出的新缺陷区域经过人工复核后自动归档,过一个月再做模型时,你会发现之前的手动复现已经跑不过这个库了。
这一套 Defect Eye 的落地路径,核心并不是让代码看起来多巧妙,而是让你知道每个参数、每个处理步骤在真实产线意味着什么。翻过一次车,你会更明白先对齐参考图、再谈阈值的道理。希望这些经验对你的项目起步有帮助。
本文还有配套的精品资源,点击获取