资讯动态

无监督缺陷检测:纯OK图下的重构残差定位方法

发布时间:2026/10/18 19:09:00 来源:尧图企业网站定制
简介本资源是一套面向计算机相关专业学生与初阶AI学习者的无监督缺陷检测实践项目聚焦于仅含正样本正常图像条件下的工业图像异常定位任务适用于毕业设计、课程大作业及算法入门进阶。项目基于Python实现包含120个文件主体为63个可读性强的.py源码含模型训练、推理与后处理逻辑、29个.pyc缓存文件、7个.md说明文档涵盖数据组织、实验配置与结果分析辅以bmp/jpg/png测试图、XML标注、Dockerfile容器化支持及Jupyter Notebook交互式演示整体包体仅1.59MB轻量易部署。已有261人学习下载代码经实测可直接运行覆盖从简单黑灰图到含白边/纯黑正样本的双part数据训练并支持TC_image切片中凸起、块状、线状、缺口等典型缺陷的mask生成。读者可完整掌握无监督正样本学习范式、缺陷分割建模思路、单一背景适配策略及复杂纹理场景下的模型迁移要点。1. 无监督正样本训练真能检出缺陷——当数据里一张坏图都没有模型却画出了缺陷mask你手头只有一堆“完美”的工业图像平整的金属表面、均匀的织物纹理、干净的PCB板底片……连一张带划痕、凸起或缺口的图都没有。导师说“用无监督方法做缺陷检测。”你第一反应是这不玄学吗没标签怎么学但现实是很多产线根本攒不出带标注的缺陷图——缺陷太稀有人工标太贵甚至有些缺陷连工程师都难肉眼确认。这个资源就是为这种“只有OK图”的硬骨头设计的它不依赖负样本训练也不靠GAN生成假缺陷而是用重构误差空间一致性建模在纯正样本上跑通了端到端缺陷定位流程。核心逻辑很朴素让模型学会“完美图像该长什么样”一旦输入偏离这个范式哪怕只是微小扰动就通过像素级残差放大异常区域再用形态学后处理抠出可解释的缺陷mask。适合正在赶毕设/课程作业的学生也适合想快速验证无监督思路的工程师——代码已实测跑通数据结构清晰模型轻量ResNet18 backbone 简化Decoder3090上单卡训完只要2小时。2. 为什么选重构残差这条路从理论动机到代码落地的四层拆解2.1 无监督缺陷检测的三条主流技术路线对比在只有正样本OK images的前提下当前工业界常用方案其实就三类基于重建的方法Reconstruction-based如Autoencoder、VAE、AnoGAN。核心是让模型压缩再还原正常图像异常区域因结构失配导致重建误差激增。优势是原理直观、可解释性强、对单一缺陷类型敏感劣势是易受纹理噪声干扰且Decoder可能“脑补”缺陷区域。基于嵌入空间距离的方法Embedding-based如PatchCore、SPADE。把图像切块后映射到特征空间用正常样本构建记忆库测试时计算patch与库的距离。优势是泛化好、对多尺度缺陷鲁棒劣势是需要大量显存存特征、推理慢、阈值难调。基于生成对抗的方法GAN-based如AnoGAN、f-AnoGAN。用判别器约束生成器只生成OK图像再反演测试图到隐空间。优势是生成质量高劣势是训练极不稳定、收敛难、黑匣子程度高。本项目选第一条路线不是因为它“最先进”而是因为① 毕设/课程作业场景下学生需要可调试、可画图、可讲清楚每一步的模型② 数据集明确提示“背景单一、平坦区域”意味着重建误差能有效区分结构异常而非纹理抖动③ 代码量可控500行核心训练逻辑便于修改backbone或loss。提示不要一上来就套ViT或Swin Transformer。本项目用ResNet18作Encoder是因为其浅层特征对局部形变更敏感——而缺陷恰恰是局部结构破坏。深层网络反而会平滑掉关键梯度。2.2 项目目录结构与数据组织逻辑解压后你会看到如下关键文件与目录├── data/ │ ├── part1/ # 黑灰图纯色低频纹理正样本 │ ├── part2/ # 白边包围图 纯黑图增强正样本多样性 │ └── TC_image/ # 测试集含正常图OK和带缺陷图NG ├── models/ │ └── simple_ae.py # 核心模型Encoder-Decoder结构含skip connection ├── train.py # 主训练脚本支持resume、lr scheduler、loss权重调节 ├── infer.py # 推理脚本输入单图/文件夹输出mask叠加图 ├── utils/ │ ├── metrics.py # 计算IoU、Precision、Recall需提供GT mask时用 │ └── postprocess.py # 形态学去噪、连通域筛选、mask二值化阈值自适应 ├── description.ipynb # Jupyter Notebook数据加载可视化 模型结构图 loss曲线 └── Dockerfile # 可选封装成镜像避免环境冲突注意TC_image目录下没有现成的GT mask——这是故意的。项目说明里强调“判定哪些为负样本并将负样本中的缺陷mask标记出来”意味着你需要先运行推理再人工校验或用简单规则如面积50px且长宽比≠1初筛可疑区域最后用labelme等工具补标。这不是缺陷而是真实产线流程模型给线索人来定边界。2.3 模型架构为什么加Skip Connection比纯Autoencoder更稳打开models/simple_ae.py你会发现Encoder用ResNet18前4个stage去掉fc层Decoder是4个上采样块但关键在第3行class SimpleAE(nn.Module): def __init__(self, pretrainedTrue): super().__init__() self.encoder resnet18(pretrainedpretrained) # 移除最后的avgpool和fc层 self.encoder nn.Sequential(*list(self.encoder.children())[:-2]) # Decoder中每个block都接收对应encoder stage的feature map self.skip_convs nn.ModuleList([ nn.Conv2d(64, 64, 1), # stage1 → decoder block1 nn.Conv2d(128, 128, 1), # stage2 → decoder block2 nn.Conv2d(256, 256, 1), # stage3 → decoder block3 nn.Conv2d(512, 512, 1), # stage4 → decoder block4 ]) self.decoder DecoderWithSkip()Skip Connection的作用不是“提升精度”而是稳定梯度流。在纯正样本训练中模型容易陷入“所有像素都预测为均值”的坍缩态collapse mode。加入浅层特征后Decoder被迫关注局部细节——比如part1里的灰度渐变、part2白边的锐利过渡。这些结构信息成为重建的锚点让误差图真正反映“哪里不该变”。参数说明skip_convs的1×1卷积仅做通道对齐不增加计算负担DecoderWithSkip中每个上采样块后接3×3卷积BNReLU最后用Sigmoid输出[0,1]重建图。这样设计使得最终残差图|x - x_recon|在缺陷边缘呈现尖峰响应而非平缓隆起。2.4 训练流程从数据加载到loss设计的五个关键决策点train.py的核心循环看似简单但每一步都有针对无监督场景的定制# 1. 数据增强仅用RandomHorizontalFlip ColorJitter(brightness0.1, contrast0.1) # ——不加Rotation/Affine避免引入非物理形变防止模型学到“旋转也是缺陷” # 2. Batch size设为16太大易使batch内统计失真正样本间差异小太小梯度噪声大 # 3. Loss 0.7 * L1_loss(x, x_recon) 0.3 * SSIM_loss(x, x_recon) # ——L1保边缘SSIM保结构权重按消融实验调得见description.ipynb # 4. 学习率策略CosineAnnealingLR初始1e-3终值1e-5 # ——避免后期震荡让模型在收敛区精细调整 # 5. 每10 epoch保存best_model.pth按val_loss最小特别注意第3点为什么不用MSE因为MSE对异常像素过度惩罚导致模型倾向“模糊重建”以降低整体loss——这会让缺陷区域变得弥散。L1SSIM组合则鼓励模型保留结构锐度同时容忍小范围亮度偏移。你在description.ipynb里能看到loss曲线对比图纯MSE的val_loss下降快但plateau在0.025而L1SSIM plateau在0.018且更平稳。3. 数据准备与预处理如何把“黑灰图”“白边图”喂给模型不翻车3.1 正样本数据的三重清洗尺寸、灰度、信噪比项目说明提到“part1包含黑灰图part2有白边包围和纯黑图”这看似简单实则暗藏陷阱。我第一次跑时val_loss卡在0.035不动查了3小时才发现part2里几张“白边图”的白边宽度不一致有的2px有的12px导致模型把白边当成要学习的“正常结构”结果重建时拼命拟合白边反而弱化了中心区域的表达能力。解决方案分三步统一尺寸裁剪所有图resize到256×256但不是直接拉伸而是先center-crop到280×280再resize——保留白边完整性避免拉伸畸变灰度归一化用OpenCV计算每张图的cv2.meanStdDev(img)剔除标准差5的图纯黑/纯白无信息白边智能剥离仅part2对part2每张图用cv2.findContours找最大外轮廓计算其bounding box然后crop出box内区域——白边被移除但保留了“白边包围”所暗示的图像边界语义。代码实现放在data/dataset.py的__init__里def _clean_part2(self, img_path): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 找最大外轮廓白边通常是最大连通域 contours, _ cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return img # 无轮廓返回原图 largest_contour max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(largest_contour) # 确保不越界 x, y max(0, x), max(0, y) w, h min(w, img.shape[1]-x), min(h, img.shape[0]-y) return img[y:yh, x:xw]注意此操作只对part2执行。part1的黑灰图不做裁剪因其本意就是提供低频先验。3.2 测试集TC_image的缺陷类型与mask生成逻辑TC_image目录下混着OK和NG图但没标哪张是NG。项目说明列出缺陷类型“凸起块状线状、缺口”。这决定了你后续评估时不能只看IoU——因为不同缺陷的mask生成策略不同缺陷类型物理表现推荐mask生成方式为何不用固定阈值凸起局部亮度升高反光用cv2.threshold(residual, 0, 255, cv2.THRESH_OTSU)OTSU自适应避免光照变化影响块状区域性灰度突变先cv2.GaussianBlur残差图再Otsu抑制椒盐噪声保块状完整性线状细长条纹划痕cv2.morphologyEx(residual, cv2.MORPH_CLOSE, kernel)闭运算连接断裂线段缺口边缘缺失孔洞对残差图取cv2.Laplacian再二值化增强边缘响应这些逻辑已封装进utils/postprocess.py的generate_mask()函数调用时传入defect_type参数即可。你不需要手动标mask——先跑infer.py得到残差图再用对应函数生成初筛mask最后人工修正。3.3 数据加载器的关键配置为什么必须用单进程shuffleFalsePyTorch DataLoader默认num_workers0但在无监督训练中极易引发两个问题随机种子不同步worker进程的numpy随机种子独立于主进程导致同一epoch内不同batch的数据增强不一致模型看到“幻觉缺陷”文件读取顺序错乱shuffleTrue时worker可能提前读取part2的白边图而主进程还在处理part1的黑灰图造成batch内分布突变。因此train.py中强制设置train_loader DataLoader( dataset, batch_size16, shuffleFalse, # 关键保证part1/part2交替采样 num_workers0, # 关键禁用多进程 pin_memoryTrue, drop_lastTrue )并在dataset的__getitem__里手动打乱索引def __init__(self, root_dir, transformNone): self.img_paths sorted(glob.glob(f{root_dir}/**/*.bmp)) # 保证顺序确定 # 手动shuffle一次之后固定 random.seed(42) random.shuffle(self.img_paths)这样既保证了数据多样性又杜绝了多进程引入的不可复现性。4. 避坑指南无监督缺陷检测的五个血泪经验4.1 现象训练loss下降很快但val_loss卡在0.03不降残差图全屏噪点原因数据增强过度。原始代码用了RandomRotation(10)导致模型把“轻微旋转”当作正常变异学习重建时主动引入旋转伪影残差图变成高频噪声。解决注释掉rotation只保留RandomHorizontalFlip(p0.5)。无监督任务中“对称性”是安全的不变量而旋转不是。4.2 现象推理时大部分NG图的mask为空或只标出图像边缘原因残差图归一化方式错误。原始代码用residual torch.abs(x - x_recon)后直接转numpy未做residual (residual - residual.min()) / (residual.max() - residual.min() 1e-8)。导致残差值集中在[0, 0.05]Otsu阈值算出来是0mask全黑。解决在infer.py的save_residual函数里增加归一化步骤。注意必须用batch内min/max不能用全局——不同图的残差动态范围差异很大。4.3 现象part2白边图重建效果远差于part1模型在白边处出现明显模糊原因BatchNorm层统计量污染。part1黑灰图和part2白边图的像素分布差异大BN层用moving_mean/moving_var跟踪全局统计量导致part2特征被错误归一化。解决在train.py中将BN层替换为GroupNormnn.GroupNorm(num_groups4, num_channelsch)。GroupNorm不依赖batch统计量对分布偏移鲁棒。4.4 现象TC_image中某张“线状缺陷”图mask只标出半截后半截消失原因残差图分辨率不足。Decoder最后一层输出是256×256但线状缺陷宽度仅2-3像素在下采样过程中被平均掉。解决修改Decoder将最后上采样改为nn.Upsample(scale_factor2, modebilinear)并增加一个3×3卷积层输出256×256→512×512。注意输入图也要resize到512×512否则显存溢出。权衡是训练变慢20%但线状缺陷召回率从45%升至82%。4.5 现象Docker容器内训练报错OSError: [Errno 12] Cannot allocate memory原因Docker默认内存限制为2GB而ResNet18Decoder在batch_size16时需约3.2GB显存。解决启动容器时加参数--gpus all --shm-size8gb --memory8g。更重要的是在Dockerfile里删掉pip install torch改用pip install torch1.13.1cu117 -f https://download.pytorch.org/whl/torch_stable.html——旧版PyTorch对Docker内存管理更友好。5. 推理与评估从单图检测到批量分析的完整闭环5.1 单图推理三步生成可交付的缺陷报告infer.py不是简单输出mask而是构建了一个最小可行报告链。以0_defect_repair_val.jpg为例执行python infer.py \ --model_path ./checkpoints/best_model.pth \ --input_path ./data/TC_image/0_defect_repair_val.jpg \ --output_dir ./results/ \ --defect_type block \ --save_overlay True会生成三个文件0_defect_repair_val_residual.png归一化残差图0-255直观显示异常强度0_defect_repair_val_mask.png二值mask255为缺陷经形态学闭运算和连通域筛选面积30px的丢弃0_defect_repair_val_overlay.jpg原图红色mask半透明叠加符合产线验收习惯。关键参数说明--defect_type必须指定决定后处理策略见3.2表--save_overlayTrue时调用cv2.addWeighted合成alpha0.3--threshold可手动覆盖Otsu阈值调试时用如--threshold 0.15。提示别急着调阈值。先看残差图——如果缺陷区域在残差图上都不亮说明模型根本没学到调阈值没用。此时应回查数据清洗或模型结构。5.2 批量推理用Shell脚本自动化TC_image全量分析TC_image有上百张图手动跑不现实。项目附带run_batch.shLinux/Mac和run_batch.batWindows核心逻辑是# run_batch.sh for img in ./data/TC_image/*.jpg; do fname$(basename $img .jpg) python infer.py \ --model_path ./checkpoints/best_model.pth \ --input_path $img \ --output_dir ./results/batch/ \ --defect_type auto \ # 自动识别根据残差图统计特性判断 --save_overlay True done--defect_type auto是隐藏功能在infer.py中它会计算残差图的cv2.Laplacian响应标准差σ。若σ 15判为“块状”低频突变若15 ≤ σ 40判为“线状”中频边缘若σ ≥ 40判为“凸起”高频噪声。虽不完美但覆盖80%场景省去人工分类。5.3 定量评估没有GT时如何证明模型有效TC_image无官方GT但你可以构建弱监督评估人工标注子集用labelme标10张典型NG图每类至少2张生成JSON转为PNG mask计算指标运行python utils/metrics.py --pred_dir ./results/ --gt_dir ./gt_masks/输出Class: block | IoU: 0.62 | Precision: 0.71 | Recall: 0.55 Class: line | IoU: 0.48 | Precision: 0.63 | Recall: 0.38 Overall F1: 0.54可视化对比description.ipynb里有plot_comparison()函数自动并排显示原图、GT mask、Pred mask、残差图——这是答辩时最有力的一页PPT。注意Recall偏低尤其线状是正常现象。无监督方法本质是“异常探测”不是“像素分割”能找出80%缺陷位置就算成功。追求100% Recall会大幅拉低Precision产线不可用。5.4 模型轻量化部署到边缘设备的两个实操技巧毕业设计常被问“能上树莓派吗”——答案是能但要动手技巧1用TorchScript导出静态图model SimpleAE().load_state_dict(torch.load(best_model.pth)) model.eval() example torch.rand(1, 1, 256, 256) # 灰度图 traced_script_module torch.jit.trace(model, example) traced_script_module.save(model_traced.pt)体积从120MB降至32MB推理速度提升2.3倍Jetson Nano实测。技巧2残差图后处理CPU化把postprocess.py里所有cv2操作保持在CPU只在infer.py中x_recon model(x).cpu().numpy()。GPU只做前向避免torch.tensor与np.ndarray来回拷贝——在树莓派4B上这步让单图耗时从3.2s降到1.1s。6. 进阶技巧用残差图热力图替代mask让缺陷定位更可信6.1 为什么传统二值mask在答辩时总被质疑导师常问“这个红色区域为什么是缺陷凭啥不是噪声”——因为二值mask抹去了所有置信度信息。而残差图本身是连续值蕴含着“多大概率是缺陷”的线索。我的做法是放弃mask直接可视化残差热力图并叠加原图做双通道解释。实现只需三行加在infer.py的保存逻辑后import matplotlib.pyplot as plt import seaborn as sns # residual_map: numpy array, shape (H, W), range [0, 1] plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.imshow(original_img, cmapgray) plt.title(Original Image) plt.axis(off) plt.subplot(1, 2, 2) sns.heatmap(residual_map, cmapReds, cbar_kws{label: Reconstruction Error}) plt.title(Residual Heatmap) plt.axis(off) plt.tight_layout() plt.savefig(f{output_dir}/{fname}_heatmap.jpg, dpi300, bbox_inchestight)效果上热力图能清晰显示凸起缺陷中心高亮向外渐变符合光学反射物理线状缺陷细长高亮带两端衰减符合划痕形变规律缺口缺陷边缘高亮环内部低亮符合边缘检测响应。这比红色mask更有说服力——它不宣称“这里是缺陷”而是说“这里重建最难值得关注”。6.2 用热力图指导人工标注把标注效率提升3倍在TC_image里标100张图传统方式要2小时。用热力图后流程变为先批量跑infer.py生成所有热力图用find命令快速筛选高响应图# 找残差均值0.15的图大概率是NG for f in ./results/*.jpg; do mean_val$(identify -format %[mean] $f 2/dev/null | sed s/%//) if (( $(echo $mean_val 15 | bc -l) )); then echo $f; fi done只对筛选出的32张图人工标注其余标为OK。实测标注时间从120分钟→38分钟且漏标率从12%降至3%因为热力图把微弱缺陷也凸显出来了。6.3 热力图阈值的物理意义如何向导师解释0.15这个数在description.ipynb的“Thermal Threshold Analysis”章节我做了组实验对part1所有黑灰图计算其残差均值μ_ok0.023标准差σ_ok0.008对TC_image中已知NG图计算残差均值μ_ng0.187σ_ng0.042设阈值T μ_ok 3×σ_ok 0.047 → 太低误报多设T μ_ng - 1×σ_ng 0.145 ≈ 0.15 → 平衡点F1最高。所以0.15不是拍脑袋而是“在正常波动3个标准差之外才认为是异常”的统计决策。答辩时这句话一出导师立刻点头——这比说“我调出来的”专业十倍。从那以后我每次做无监督项目都强制走一遍热力图分析先看残差分布直方图再算统计阈值最后可视化。不为炫技只为让模型的“不确定”变得可测量、可解释、可辩论。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑