ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

无监督正样本训练缺陷检测:Python源码实现与缺陷mask生成

无监督正样本训练缺陷检测:Python源码实现与缺陷mask生成

简介:这份资源面向计算机相关专业在校学生、教师及企业开发者,提供一套基于Python的无监督正样本缺陷检测完整源码与项目说明,解决仅有正样本数据时如何训练模型并输出缺陷mask的问题。项目分两个数据部分:part1为黑灰图,part2为白边包围与纯黑图,均属正样本;测试集TC_image包含正常与负样本,负样本涵盖凸起、块状、线状、缺口等缺陷类型。针对背景单一、平坦区域采用较简单模型,而复杂纹理数据集则改用无缺陷大图原图训练,待测图以切片形式输入,体现了从简单到复杂场景的模型选型思路。压缩包共120个文件,以63个py源码、29个pyc编译文件、7个md说明文档为主,另含bmp、jpg、png图像样本、xml标注、ipynb实验笔记及Dockerfile等,整体约1.59MB,结构清晰便于复现。已有261人学习,适合作为毕业设计、课程大作业或项目立项演示,读者可借鉴完整训练流程、数据组织方式与缺陷判定逻辑,并在此基础上修改扩展。

1. 只有正样本也能训缺陷检测:这套 Python 源码到底能解决什么

产线上拿到的样本几乎全是良品,这是缺陷检测项目里最真实的开局。你手里可能只有几百张 OK 图,缺陷图一张没有,或者偶尔冒出来几张还标不全 mask。传统监督检测在这里直接卡死——没有负样本,分类器学不出边界。这套基于 Python 的无监督正样本训练源码,解决的正是这个场景:只用正样本训练,推理时输入一张带缺陷的图,模型输出缺陷所在的 mask 区域。项目里给了两个 part 的数据,part1 是黑灰图,part2 是白边包围加纯黑图,都算正样本;测试集 TC_image 里混了正常样本和负样本,负样本覆盖凸起、块状、线状、缺口几类缺陷。背景单一、属于平坦区域,所以模型结构不复杂,适合拿来当毕设、课程设计或大作业的落地骨架。如果你正在找一份能跑通、能改、能写进论文的缺陷检测源码,这份资源的定位就是「正样本训练 + 缺陷 mask 输出」的最小可用闭环。

2. 无监督正样本训练的原理与数据组织:为什么平坦图能这么干

2.1 正样本训练的核心逻辑:把重建误差当缺陷分数

无监督正样本训练的主流思路是自编码器或重建网络。模型只见过良品,训练目标是把输入图压缩再还原,让重建误差在正样本上尽可能小。推理时喂一张带缺陷的图,缺陷区域因为没在训练分布里出现过,重建质量差,误差图自然在缺陷位置隆起。项目里背景单一、纹理平坦,这个假设成立得比较稳——平坦区域的像素分布集中,模型容易学到「正常长什么样」,缺陷一旦打破局部一致性,误差就冒出来。

常见做法是编码器-解码器结构,中间加一个瓶颈层限制信息容量,逼模型只保留正样本的主要模式。损失函数用 MSE 或 SSIM,前者对像素级差异敏感,后者对结构差异更稳。这份源码走的是较简单的模型路线,没有堆复杂注意力模块,原因就在数据本身:黑灰图、纯黑图、白边图,纹理信息少,上重模型反而容易过拟合到噪声。

提示:正样本训练不是万能钥匙。背景复杂、纹理丰富的图,简单重建模型会把缺陷也当正常纹理重建掉,漏检率飙升。项目正文里专门提到复杂图像数据集要用另一套思路,就是这个原因。

2.2 数据目录与文件角色:r.bmp、t.bmp、k.bmp、s.bmp 各是什么

项目根目录下的几个 bmp 文件和 Dockerfile、description.ipynb 构成了最小工程。r.bmp、t.bmp、k.bmp、s.bmp 是正样本图或测试图,0_defect_repair_val.jpg 是缺陷修复后的验证输出图。description.ipynb 是项目说明 notebook,通常包含数据加载、训练、推理的完整流程演示。Dockerfile 用来固化环境,避免 Python 版本和依赖库版本漂移。

文件/目录角色使用阶段
r.bmp / t.bmp / k.bmp / s.bmp正样本或测试图训练与推理输入
0_defect_repair_val.jpg缺陷修复验证输出推理后可视化
description.ipynb流程说明与演示全流程参考
Dockerfile环境构建部署与复现
project.imlIDE 工程配置开发环境
.gitignore版本控制忽略规则代码管理

数据分两个 part:part1 黑灰图,part2 白边包围加纯黑图。测试集 TC_image 里正常和负样本混在一起,需要先判定哪些是负样本,再把负样本的缺陷 mask 标出来。负样本缺陷类型包括凸起、块状、线状、缺口,这四类在平坦背景下的形态差异明显,mask 标注时要注意边界贴合。

2.3 环境搭建:Python 依赖与 Dockerfile 两条路

环境搭建有两条路。第一条是本地 Python 虚拟环境,第二条是直接用 Dockerfile。本地路线适合调试,Docker 路线适合交付和复现。

# 创建虚拟环境,避免污染系统 Python python -m venv venv # Linux/macOS 激活 source venv/bin/activate # Windows 激活 venv\Scripts\activate # 安装核心依赖,版本按项目实际 requirements 调整 pip install numpy opencv-python torch torchvision matplotlib scikit-image

这段命令先建隔离环境,再装依赖。numpy 和 opencv-python 负责图像读写与预处理,torch 和 torchvision 提供网络与训练工具,matplotlib 做可视化,scikit-image 常用于 SSIM 等结构相似度计算。参数上,torch 版本要和 CUDA 匹配,没有 GPU 就用 CPU 版,训练会慢但平坦图数据量小,能跑通。

# 基于官方 Python 镜像,固定版本保证复现 FROM python:3.9-slim WORKDIR /app COPY . /app RUN pip install --no-cache-dir numpy opencv-python torch torchvision matplotlib scikit-image CMD ["python", "train.py"]

Dockerfile 的逻辑是固定基础镜像、复制代码、装依赖、设启动命令。--no-cache-dir 减小镜像体积,CMD 里的 train.py 按项目实际入口替换。用 Docker 的好处是换机器不用重新配环境,坏处是镜像构建时网络不稳会卡在 pip 安装,建议先本地拉好 wheel 再构建。

注意:项目里没有明确写 requirements.txt 时,依赖版本以 description.ipynb 里实际 import 和运行结果为准,不要盲目装最新版,torch 和 numpy 的大版本不兼容会直接报错。

3. 训练与推理全流程:从正样本到缺陷 mask 的代码落地

3.1 数据加载与预处理:把 bmp 图转成模型能吃的张量

数据加载的第一步是读图、转灰度或保持通道、归一化。平坦图背景单一,归一化到 [0,1] 或 [-1,1] 都行,关键是训练和推理用同一套预处理。

import cv2 import numpy as np import torch from torch.utils.data import Dataset, DataLoader class DefectDataset(Dataset): def __init__(self, image_paths, img_size=256): self.image_paths = image_paths self.img_size = img_size def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # 以灰度方式读取,平坦图不需要彩色信息 img = cv2.imread(self.image_paths[idx], cv2.IMREAD_GRAYSCALE) # 统一尺寸,避免网络输入维度不一致 img = cv2.resize(img, (self.img_size, self.img_size)) # 归一化到 [0,1],并增加通道维度 img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=0) return torch.tensor(img) # 假设正样本路径列表已准备好 train_dataset = DefectDataset(["r.bmp", "t.bmp", "k.bmp", "s.bmp"]) train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)

这段代码定义了数据集类,读图用灰度模式,resize 到 256×256,归一化后扩通道。参数 img_size 决定网络输入分辨率,太小缺陷细节丢失,太大显存吃紧,平坦图 256 够用。batch_size 按显存调,4 是保守值。shuffle=True 打乱顺序,避免模型学到文件顺序的伪相关。

3.2 模型定义:一个够用的卷积自编码器

模型不用复杂,卷积自编码器在平坦图上表现稳定。编码器逐层降维,解码器逐层还原,中间瓶颈层控制信息容量。

import torch.nn as nn class ConvAutoencoder(nn.Module): def __init__(self): super().__init__() # 编码器:3 层卷积,逐步提取特征并降采样 self.encoder = nn.Sequential( nn.Conv2d(1, 16, 3, stride=2, padding=1), # 256 -> 128 nn.ReLU(), nn.Conv2d(16, 32, 3, stride=2, padding=1), # 128 -> 64 nn.ReLU(), nn.Conv2d(32, 64, 3, stride=2, padding=1), # 64 -> 32 nn.ReLU(), ) # 解码器:转置卷积逐步还原尺寸 self.decoder = nn.Sequential( nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1), nn.ReLU(), nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1), nn.ReLU(), nn.ConvTranspose2d(16, 1, 3, stride=2, padding=1, output_padding=1), nn.Sigmoid(), # 输出归一到 [0,1] ) def forward(self, x): z = self.encoder(x) out = self.decoder(z) return out

编码器每层 stride=2 把空间尺寸减半,通道数翻倍,从 1 通道到 64 通道。解码器用 ConvTranspose2d 还原,output_padding=1 补齐奇数尺寸的还原偏差。最后一层 Sigmoid 把输出压到 [0,1],和输入归一化范围对齐。这个结构参数量小,训练快,适合平坦图。如果缺陷极细,可以把第一层通道数提到 32,但别盲目加深,容易过拟合。

3.3 训练循环:损失、优化器与早停

训练目标是最小化重建误差。损失用 MSE,优化器用 Adam,学习率 1e-3 起步。

import torch.optim as optim device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = ConvAutoencoder().to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) epochs = 100 best_loss = float("inf") for epoch in range(epochs): model.train() total_loss = 0 for batch in train_loader: batch = batch.to(device) optimizer.zero_grad() recon = model(batch) loss = criterion(recon, batch) loss.backward() optimizer.step() total_loss += loss.item() avg_loss = total_loss / len(train_loader) # 简单早停:连续多轮不降就停 if avg_loss < best_loss: best_loss = avg_loss torch.save(model.state_dict(), "best_autoencoder.pth") print(f"Epoch {epoch+1}, Loss: {avg_loss:.6f}")

这段循环里,zero_grad 清梯度,forward 算重建,criterion 算 MSE,backward 反传,step 更新。每轮记录平均损失,损失创新低就存模型。早停没写全,实际可以加 patience 计数,连续 10 轮不降就 break。学习率 1e-3 是 Adam 常用起点,损失震荡就降到 1e-4。

3.4 推理与缺陷 mask 生成:误差图阈值化

推理时加载模型,输入测试图,算重建误差,阈值化得到 mask。

import matplotlib.pyplot as plt model.load_state_dict(torch.load("best_autoencoder.pth")) model.eval() def detect_defect(img_path, threshold=0.05): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (256, 256)).astype(np.float32) / 255.0 tensor = torch.tensor(img).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): recon = model(tensor) # 计算逐像素误差 error = torch.abs(recon - tensor).squeeze().cpu().numpy() # 阈值化生成 mask mask = (error > threshold).astype(np.uint8) * 255 return img, error, mask img, error, mask = detect_defect("TC_image/sample.bmp") plt.subplot(1,3,1); plt.imshow(img, cmap="gray"); plt.title("Input") plt.subplot(1,3,2); plt.imshow(error, cmap="hot"); plt.title("Error") plt.subplot(1,3,3); plt.imshow(mask, cmap="gray"); plt.title("Mask") plt.show()

推理流程是读图、预处理、前向、算误差、阈值化。threshold 是关键参数,0.05 是起点,误检多就调高,漏检多就调低。error 图用 hot 色图看,缺陷位置会亮。mask 是二值图,白色为缺陷。实际项目里可以加形态学开闭运算去噪,或者用连通域过滤小面积误检。

提示:阈值不是固定值,不同批次图的光照和对比度会变。稳妥做法是在一批正常图上跑推理,统计误差分布,取 95% 或 99% 分位数当阈值。

4. 避坑与排查:正样本训练里最容易翻车的五个点

4.1 现象:训练损失降得很低,推理却全是误检

原因通常是正样本太少或太单一,模型把训练集背下来了,泛化差。解决是加数据增强,旋转、翻转、亮度扰动,或者用更强的瓶颈约束。平坦图增强幅度别太大,否则正常样本被增强成「异常」。

4.2 现象:缺陷 mask 断断续续,连不成片

原因是误差图阈值化后噪声多,或者缺陷本身对比度低。解决是先对 error 图做高斯模糊再阈值化,或者用形态学闭运算把断开的区域连起来。阈值调低能连片,但误检会增加,需要权衡。

4.3 现象:白边包围的 part2 图被当成缺陷

原因是白边在训练集里出现次数少,模型没学好。解决是把 part2 的白边图充分纳入训练,或者预处理时裁掉白边区域,只保留纯黑部分。项目正文专门区分 part1 和 part2,说明这两类正样本的分布差异需要显式处理。

4.4 现象:换一台机器跑,报 numpy 或 torch 版本不兼容

原因是依赖版本没锁。解决是用 Dockerfile 或 requirements.txt 固定版本,别用 pip install 不带版本号。torch 和 numpy 的大版本升级经常改 API,旧代码直接崩。

4.5 现象:推理速度慢,产线节拍跟不上

原因是模型在 CPU 上跑,或者输入分辨率太高。解决是导出 ONNX 或 TensorRT 加速,或者把 img_size 从 256 降到 128。平坦图降分辨率对精度影响有限,速度提升明显。

5. 进阶技巧:把误差图变成可交付的缺陷报告

5.1 用连通域分析给缺陷分类和计数

阈值化得到 mask 后,用 OpenCV 的 connectedComponentsWithStats 做连通域分析,能拿到每个缺陷的面积、外接矩形、质心。结合形态特征可以粗分类:线状缺陷长宽比大,块状缺陷面积大且紧凑,凸起和缺口看边界曲率。

num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) for i in range(1, num_labels): area = stats[i, cv2.CC_STAT_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] # 过滤小面积噪声 if area < 20: continue aspect_ratio = max(w, h) / max(min(w, h), 1) defect_type = "线状" if aspect_ratio > 3 else "块状" print(f"缺陷 {i}: 类型={defect_type}, 面积={area}, 位置=({x},{y})")

这段代码遍历连通域,过滤小面积噪声,按长宽比粗分线状和块状。area 阈值 20 是经验值,按实际分辨率调。aspect_ratio 大于 3 判线状,否则块状。凸起和缺口需要更细的轮廓分析,可以用轮廓近似或曲率计算。

5.2 用 SSIM 替代 MSE 提升结构缺陷敏感度

MSE 对像素级差异敏感,但对结构变化不敏感。SSIM 衡量结构相似度,对线状、缺口这类结构缺陷更灵敏。scikit-image 的 structural_similarity 可以直接用。

from skimage.metrics import structural_similarity as ssim def ssim_error_map(img, recon): # SSIM 返回相似度图和总分 score, sim_map = ssim(img, recon, full=True, data_range=1.0) # 相似度取反得到误差图 error_map = 1 - sim_map return error_map

SSIM 的 full=True 返回逐像素相似度图,1 减去它得到误差图。data_range=1.0 对应归一化输入。SSIM 误差图比 MSE 更平滑,阈值化后 mask 更完整,但计算量稍大。实际可以 MSE 和 SSIM 加权融合,兼顾像素和结构。

5.3 验证方法:用正常样本统计误检率

模型上线前必须验证误检率。拿一批没参与训练的正常图跑推理,统计有多少像素被误判为缺陷。误检率高于 1% 就要调阈值或加后处理。

验证项方法合格标准
误检率正常图推理,统计误判像素占比< 1%
漏检率已知缺陷图推理,统计未检出缺陷占比< 5%
推理耗时单张图端到端计时满足产线节拍
mask 完整度人工核对缺陷区域是否连片无明显断裂

验证要在独立测试集上做,不能拿训练集自测。TC_image 里正常和负样本混在一起,正好用来做这个验证。先跑正常样本统误检,再跑负样本统漏检,两个指标都达标才算可用。

5.4 我踩过的坑:阈值调参别在训练集上做

刚做这个项目时,我在训练集上调阈值,看着 mask 很漂亮,换到 TC_image 上误检一片。后来才明白,训练集上的误差分布和测试集不一样,阈值必须在独立正常样本上统计。从那以后我每次调阈值都强制走一遍独立验证集,先跑正常图看误检,再跑缺陷图看漏检,两个都过了才敢往下走。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进