简介这份资源面向计算机相关专业在校学生、教师及企业员工提供一套基于Python的无监督正样本缺陷检测完整源码与项目说明解决仅有正样本数据时如何训练模型、并对带缺陷图片输出缺陷mask的问题。项目分两个partpart1为黑灰图part2为白边包围与纯黑图均作正样本测试集TC_image含正常与负样本负样本覆盖凸起、块状、线状、缺口等缺陷类型背景单一、属平坦区域适合采用较简单模型复杂图像数据集则以无缺陷大图原图训练纹理更复杂需更换适配模型。压缩包共120个文件以63个py源码、29个pyc、7个md说明为主另含少量bmp、xml、jpg、png及Dockerfile、ipynb等整体约1.59MB结构清晰便于按模块查阅。已有261人学习下载可作为毕业设计、课程大作业或初期项目立项演示帮助读者掌握无监督正样本训练流程、缺陷mask标注思路与模型选型对比并支持在此基础上修改扩展。1. 只有正样本怎么训缺陷检测从一张好图里挤出异常分数产线上跑了几万张良品图缺陷样本一张都拿不到客户却要求下周上线检测。这是工业视觉里最真实的开局。基于Python实现无监督正样本训练并进行图片中缺陷检测解决的正是这个死结训练集里只有 OK 图模型要学会判断什么叫「不像 OK」。它适合两类人——手上只有良品数据、又不想花几个月攒缺陷样本的算法工程师以及想用 Python 快速搭一套可复现基线、再决定要不要上深度方案的视觉从业者。核心思路不复杂让网络只记住正常纹理的分布推理时凡是重建误差、特征距离或密度分数偏离正常范围的位置就判为缺陷。这条路子在布匹、木材、轴承、手机屏幕等表面缺陷检测里被反复验证过难点从来不是「能不能跑」而是阈值怎么定、分辨率怎么选、误报怎么压。下面按我实际落地的顺序把选型、代码、参数和踩过的坑一次讲清。2. 无监督正样本缺陷检测的三条主流路线与选型理由2.1 重建式、特征嵌入式、归一化流到底选哪个工业上真正能落地的无监督正样本方案基本收敛到三条路线理解它们的差异比背论文重要得多。重建式Autoencoder / GAN训练一个只吃正常图的编码器-解码器让它在正常图上重建得极好。推理时缺陷区域因为没被训练过重建误差大误差图就是缺陷热力图。优点是直观、可视化好、对纹理类缺陷敏感缺点是自编码器容易「太强」把缺陷也重建出来导致漏检尤其是小缺陷。常见做法是加瓶颈、加记忆模块MemAE 那类思路来压制泛化能力。特征嵌入式PatchCore / PaDiM / SPADE用预训练骨干ResNet、WideResNet提特征把正常图的 patch 特征存进一个记忆库推理时算测试 patch 到记忆库的最近邻距离距离大就是异常。这是目前工业界综合表现最稳的一类PatchCore 在 MVTec AD 上长期是强基线对小缺陷和纹理缺陷都友好而且不需要训练骨干冷启动快。代价是推理要查记忆库得做 coreset 采样控制内存。归一化流Normalizing Flow如 FastFlow用可逆网络把特征分布映射到标准正态正常样本落在高概率区异常样本概率低。训练稳定、推理快适合对节拍要求高的产线但对超小缺陷的敏感度不如特征嵌入式。选型上我的经验是纹理类布匹、木材、皮革优先重建式或 PatchCore物体类带固定结构轴承、屏幕、磁极优先特征嵌入式节拍卡得死、缺陷尺寸不太小用 FastFlow。新手想一周出结果直接上 PatchCore 思路别一上来啃 GAN。2.2 用 Python 搭一个最小可跑的 PatchCore 基线下面这段是能直接跑通的核心逻辑骨干用 torchvision 的预训练 ResNet18正常特征存记忆库推理算最近邻距离。数据目录按train/good和test/xxx组织这是 MVTec 风格工业项目里也常用。import torch import torch.nn.functional as F from torchvision import models, transforms from PIL import Image import glob, os import numpy as np device cuda if torch.cuda.is_available() else cpu # 1. 骨干取中间层特征浅层保纹理深层保语义 backbone models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) backbone.eval().to(device) feat_layers [layer2, layer3] # 两层拼接兼顾小缺陷与结构 # 2. 预处理统一到 224ImageNet 归一化 tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def extract_feat(img_tensor): 返回多尺度 patch 特征形状 [N, C] feats [] x img_tensor.unsqueeze(0).to(device) for name, module in backbone.named_children(): x module(x) if name in feat_layers: # 全局平均池化前保留空间信息这里用自适应池化到 28x28 f F.adaptive_avg_pool2d(x, (28, 28)) feats.append(f) # 拼接两层通道 out torch.cat(feats, dim1) # [1, C, 28, 28] out out.flatten(2).squeeze(0).T # [784, C] return out # 3. 构建正常记忆库 memory [] for p in glob.glob(data/train/good/*.png): img tf(Image.open(p).convert(RGB)) memory.append(extract_feat(img).cpu()) memory torch.cat(memory, dim0) # [M, C] print(memory bank size:, memory.shape) # 4. coreset 采样随机抽 10% 控制内存工业上更推荐 greedy k-center idx torch.randperm(memory.size(0))[: max(1, memory.size(0) // 10)] memory memory[idx] # 5. 推理算每个 patch 到记忆库的最近邻距离 def anomaly_score(img_path): img tf(Image.open(img_path).convert(RGB)) q extract_feat(img).cpu() # [784, C] # 分块算距离避免显存爆 dists torch.cdist(q, memory) # [784, M] min_d dists.min(dim1).values # 每个 patch 的异常分 score_map min_d.reshape(28, 28) return score_map, min_d.max().item() score_map, img_score anomaly_score(data/test/scratch/001.png) print(image-level score:, img_score)逻辑说明extract_feat取 layer2、layer3 两层特征再拼接是因为浅层对细小划痕敏感、深层对结构性缺陷敏感单层容易顾此失彼。memory存的是所有正常图展平后的 patch 特征推理时用torch.cdist算测试 patch 到每个正常 patch 的欧氏距离取最小值作为该位置的异常分——离正常分布越远越可疑。参数说明feat_layers决定敏感度缺陷极小就加layer1但内存会涨adaptive_avg_pool2d的 28×28 是 patch 网格分辨率产线小缺陷建议提到 56×56代价是记忆库翻四倍coreset 采样比例 10% 是内存和精度的折中缺陷种类多时提到 20%~30%。img_score取 patch 最大异常分做图级判定比平均分更能抓住局部缺陷。2.3 阈值怎么定别用拍脑袋的 0.5无监督方案翻车最多的地方就是阈值。正常图算出来的分数本身有波动你拿一张缺陷图定个阈值换一批光照就全废。正确做法是只用正常验证集定阈值。跑一批没参与建库的正常图统计图级分数的分布取 99% 分位数或均值加 3 倍标准差作为阈值。这样误报率可控产线才敢用。如果客户能接受少量漏检换低误报就把分位数提到 99.5%反之降到 95%。这个数字必须和产线 KPI 对齐不是技术问题是业务问题。3. 从训练到推理的完整工程链路与参数调优3.1 数据组织与增强正样本训练最容易被忽略的一步只有正样本不代表数据可以随便丢。工业现场的正常图往往高度重复——同一角度、同一光照拍几千张模型学到的「正常」其实很窄一换批次就误报。我的做法是训练集里主动加入光照扰动、轻微平移旋转、亮度对比度抖动把正常分布的边界撑开。但注意增强不能引入伪缺陷比如高斯噪声加太猛模型会把噪声当正常真缺陷反而被淹没。train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop((224, 224)), # 轻微位置扰动 transforms.ColorJitter(brightness0.2, contrast0.2), # 光照鲁棒 transforms.RandomRotation(degrees5), # 小角度别超过 10 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomCrop到 224 是为了让模型见过缺陷可能出现的各个位置ColorJitter的 0.2 是经验值再大正常图会失真RandomRotation控制在 5 度内工业相机基本是固定的转太多反而偏离真实分布。这一步做扎实后面阈值能稳很多。3.2 骨干与特征层的选择ResNet18 够不够用很多人一上来就上 WideResNet50 或 ViT觉得越大越好。实际产线上ResNet18 在多数表面缺陷任务里已经够用推理还快。真正影响效果的是特征层组合和输入分辨率不是骨干深度。我的对比经验ResNet18 取 layer2layer3在布匹和屏幕缺陷上和 WideResNet50 的差距通常在 1~2 个点以内但推理速度快三倍以上。只有当缺陷是极细微的纹理异常比如木材的浅色裂纹才值得上更深的骨干。输入分辨率更关键。224 是通用值但产线缺陷可能只有几个像素。这时候要么提高输入到 512要么用滑窗切图。切图的好处是单张小图分辨率等效提高坏处是推理次数变多、边界处容易漏。我一般先试 512 整图节拍不够再切图。3.3 后处理把热力图变成产线能用的判定模型输出的是 28×28 的异常热力图产线要的是「这张图 OK 还是 NGNG 在哪」。中间需要后处理import cv2 def postprocess(score_map, thresh0.6, min_area20): 热力图 - 二值掩码 - 缺陷框 m score_map.numpy() m (m - m.min()) / (m.max() - m.min() 1e-8) # 归一化到 0~1 m cv2.resize(m, (224, 224)) binary (m thresh).astype(np.uint8) * 255 # 形态学去噪去掉孤立噪点 kernel np.ones((3, 3), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [cv2.boundingRect(c) for c in contours if cv2.contourArea(c) min_area] return boxesthresh是像素级阈值和前面图级阈值不是一回事要单独在正常图上标定min_area过滤小噪点太小会误报太大会漏掉小缺陷按实际缺陷最小尺寸的 1/4 来设。形态学开运算能去掉大部分孤立误报这一步在产线上能省掉大量人工复判。3.4 评估没有缺陷样本怎么证明模型有效这是无监督最尴尬的地方——测试集里缺陷样本少甚至没有。我的做法是留一部分正常图做验证集算误报率再人工合成缺陷贴图、划痕、遮挡算检出率。合成缺陷不能完全代表真实缺陷但能横向对比不同方案的敏感度。真实上线后前两周一定要人工全检把误报和漏检都记下来反推阈值和特征层要不要调。别指望一次调好无监督方案是「上线后持续标定」的活。4. 无监督正样本缺陷检测的避坑与排查清单4.1 坑一模型把缺陷也重建/匹配出来了漏检现象明显划痕异常分数却很低热力图一片平静。原因重建式方案里自编码器泛化太强或者特征嵌入式里记忆库采样太稀缺陷 patch 恰好落在某个正常 patch 附近。解决重建式加瓶颈或记忆模块特征嵌入式提高 coreset 采样比例或改用 greedy k-center 而不是随机采样保证记忆库覆盖正常分布的各个角落。4.2 坑二正常图误报一片产线天天报警现象良品图异常分数忽高忽低阈值怎么调都压不住。原因训练集正常分布太窄或者预处理和推理不一致比如训练用了 ColorJitter推理没归一化。解决训练集加光照和位置扰动撑开分布严格保证训练和推理的归一化参数一致阈值只用正常验证集定别拿缺陷图反推。4.3 坑三换批次、换光照就崩现象同一产品换个批次误报率飙升。原因模型学到的是「这批图的正常」不是「这个产品的正常」。解决训练集尽量覆盖多个批次、多种光照推理前做白平衡或直方图均衡把输入拉到训练分布附近实在不行每换批次用少量新正常图更新记忆库这是 PatchCore 类方案的优势不用重训。4.4 坑四小缺陷检测不到热力图糊成一团现象几个像素的缺陷热力图分辨率不够定位不准。原因特征图下采样太多28×28 网格太粗。解决提高输入分辨率到 512或把特征层往上提到 layer1或改用滑窗切图。代价是内存和推理时间要按节拍权衡。4.5 坑五显存爆了记忆库太大现象正常图几千张记忆库几十万 patchtorch.cdist直接 OOM。解决coreset 采样到 1%~10%分块计算距离别一次算全量用 FAISS 做近邻检索比暴力 cdist 快几个数量级。工业上记忆库控制在几万 patch 是合理范围。5. 把无监督缺陷检测推到产线可用的几个进阶技巧5.1 用 FAISS 替换暴力检索推理提速十倍前面用torch.cdist是为了讲清原理真上产线必须换 FAISS。记忆库几万条时暴力检索每张图要几百毫秒FAISS 的 IVF 索引能压到几十毫秒。import faiss mem memory.numpy().astype(float32) dim mem.shape[1] # IVF 索引nlist 是聚类中心数一般取 sqrt(N) quantizer faiss.IndexFlatL2(dim) index faiss.IndexIVFFlat(quantizer, dim, nlist256, faiss.METRIC_L2) index.train(mem) index.add(mem) index.nprobe 16 # 搜索的聚类数越大越准越慢 def faiss_score(q): q q.numpy().astype(float32) D, I index.search(q, k1) # 每个 patch 找最近邻 return D[:, 0]nlist取记忆库大小的平方根附近nprobe是精度和速度的旋钮产线先设 16漏检多就加到 32。这套替换后单图推理基本能进 50ms 以内满足多数产线节拍。5.2 多尺度融合小缺陷和大缺陷一起抓单一分辨率很难同时抓小缺陷和结构异常。我的做法是跑两个尺度——224 抓整体结构512 抓细节把两张热力图对齐后取逐像素最大值。这样小缺陷靠高分辨率图兜底大缺陷靠低分辨率图兜底误报也能互相印证。代价是推理时间翻倍节拍紧的产线要谨慎。5.3 验证方案是否值得投入的三个判断点第一正常图数量够不够少于 50 张任何无监督方案都不稳先去攒数据。第二缺陷是否可复现如果缺陷形态千变万化、连人都难定义无监督也难考虑半监督或人工规则兜底。第三误报成本 vs 漏检成本误报高就调高阈值牺牲检出漏检代价大就反过来这个权衡必须和产线负责人一起定不是算法单方面能拍板的。我自己踩过最深的坑是早期拿一张缺陷图定阈值上线第一天误报率 30%被产线追着改了两周。后来养成习惯任何阈值只用正常验证集定缺陷图只用来验证敏感度绝不用来调参。这个习惯比任何模型都值钱。希望帮到你。本文还有配套的精品资源点击获取
