ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于轻量化重构网络的表面缺陷视觉检测:从模型压缩到边缘部署实战

基于轻量化重构网络的表面缺陷视觉检测:从模型压缩到边缘部署实战 简介本资源为基于轻量化重构网络的表面缺陷视觉检测项目面向计算机、人工智能、自动化、电子信息等专业的在校学生、教师及企业员工也适合作为毕设、课程设计或项目立项演示的参考方案。项目代码经过实际运行测试功能完整下载后可直接上手学习基础较好的读者还能在此基础上修改扩展实现其他检测功能。压缩包共562个文件约62.08MB包含17个Python源码文件、5个pth模型权重、8个xml配置、1个md说明文档以及大量png图像样本、bat批处理脚本和dell、pyc等辅助文件覆盖数据准备、模型训练与推理的完整流程。目前已有64人学习下载。资源提供了清晰的目录结构与运行脚本读者可借此理解轻量化重构网络在表面缺陷检测中的实现思路掌握数据预处理、模型加载与结果可视化的关键环节并积累排错与调试经验适合作为视觉检测方向的入门与进阶实践素材。1. 从一张产线照片说起轻量化重构网络到底在解决什么一条 3C 结构件产线工业相机每拍一张 2048×2048 的金属外壳图质检工位要在 300ms 内判断有没有划痕、凹坑、脏污。传统做法是拿 ResNet50 或 VGG16 做骨干精度够但单张推理在普通工控机上要 400ms 以上模型文件 100MB 起步部署到边缘盒子直接爆显存。这就是「基于轻量化重构网络的表面缺陷视觉检测」这个标题真正要回答的问题在保证缺陷召回率的前提下把模型压到能在边缘设备上实时跑。轻量化重构网络的核心思路不是简单地把卷积核砍小而是用「编码器压缩 解码器重构」的结构让网络学会正常纹理的分布缺陷作为偏离重构结果的异常区域被凸显出来。相比直接做分类或检测重构路线对缺陷样本数量要求低适合工业场景里缺陷样本稀缺的现实。这套方案适合两类人一是做工业质检落地、被模型体积和延迟卡住的工程师二是想拿一个完整 Python 项目练手、理解轻量化网络设计的学生和转行者。项目源码和文档说明通常会包含网络定义、训练脚本、推理接口和一份环境配置说明下面我按能复现的顺序把它拆开讲。2. 轻量化重构网络的选型逻辑与最小可跑结构2.1 为什么是重构而不是分类或检测工业表面缺陷检测有三条主流路线分类判断整图有没有缺陷、检测框出缺陷位置、重构重建正常纹理比对差异。分类路线需要大量标注且只能告诉你「有」不能告诉你「在哪」检测路线依赖 YOLO 这类框架标注成本高小缺陷容易漏。重构路线的逻辑是只用正常样本训练一个自编码器或 U-Net 式结构让网络学会重建正常纹理推理时缺陷区域重建误差大阈值一卡就出掩码。这条路线的优势在样本端。产线上正常品永远比缺陷品多重构网络只吃正常样本就能训省掉大量标注。代价是它对纹理变化敏感光照、材质批次一变重构误差分布就漂移需要重新校准阈值。所以选型时要先确认你的场景纹理是否稳定如果同一工位一天换三种材质重构路线会很难受。轻量化体现在两个地方一是编码器用深度可分离卷积或 MobileNet 系列倒残差块替代标准卷积参数量降到原来的 1/8 到 1/10二是解码器不做全分辨率重建而是用跳跃连接把浅层特征引过来减少上采样层数。常见做法是编码器 5 层下采样解码器 4 层上采样最后一层用 1×1 卷积输出单通道重构图。2.2 用 PyTorch 搭一个可跑的重构网络下面这段代码是一个最小可跑版本编码器用倒残差块解码器用双线性插值加卷积。你可以直接存成model.py在 Python 3.8 以上、PyTorch 1.10 以上环境跑通。import torch import torch.nn as nn import torch.nn.functional as F class InvertedResidual(nn.Module): 倒残差块先升维再深度卷积再降维轻量化核心单元 def __init__(self, in_ch, out_ch, stride1, expand_ratio4): super().__init__() hidden in_ch * expand_ratio self.use_res (stride 1 and in_ch out_ch) layers [] if expand_ratio ! 1: layers.append(nn.Conv2d(in_ch, hidden, 1, biasFalse)) layers.append(nn.BatchNorm2d(hidden)) layers.append(nn.ReLU6(inplaceTrue)) layers.extend([ nn.Conv2d(hidden, hidden, 3, stride, 1, groupshidden, biasFalse), nn.BatchNorm2d(hidden), nn.ReLU6(inplaceTrue), nn.Conv2d(hidden, out_ch, 1, biasFalse), nn.BatchNorm2d(out_ch), ]) self.conv nn.Sequential(*layers) def forward(self, x): if self.use_res: return x self.conv(x) return self.conv(x) class LightReconNet(nn.Module): 轻量化重构网络编码器压缩解码器重建 def __init__(self, base_ch16): super().__init__() # 编码器4 次下采样通道 16-24-32-64-96 self.enc1 nn.Sequential( nn.Conv2d(3, base_ch, 3, 2, 1, biasFalse), nn.BatchNorm2d(base_ch), nn.ReLU6(inplaceTrue)) self.enc2 InvertedResidual(base_ch, base_ch*2, 2) self.enc3 InvertedResidual(base_ch*2, base_ch*4, 2) self.enc4 InvertedResidual(base_ch*4, base_ch*6, 2) # 解码器上采样 跳跃连接 self.dec3 nn.Sequential( nn.Conv2d(base_ch*6 base_ch*4, base_ch*4, 3, 1, 1, biasFalse), nn.BatchNorm2d(base_ch*4), nn.ReLU6(inplaceTrue)) self.dec2 nn.Sequential( nn.Conv2d(base_ch*4 base_ch*2, base_ch*2, 3, 1, 1, biasFalse), nn.BatchNorm2d(base_ch*2), nn.ReLU6(inplaceTrue)) self.dec1 nn.Sequential( nn.Conv2d(base_ch*2 base_ch, base_ch, 3, 1, 1, biasFalse), nn.BatchNorm2d(base_ch), nn.ReLU6(inplaceTrue)) self.head nn.Conv2d(base_ch, 1, 1) def forward(self, x): e1 self.enc1(x) # 1/2 e2 self.enc2(e1) # 1/4 e3 self.enc3(e2) # 1/8 e4 self.enc4(e3) # 1/16 d3 F.interpolate(e4, scale_factor2, modebilinear, align_cornersFalse) d3 self.dec3(torch.cat([d3, e3], dim1)) d2 F.interpolate(d3, scale_factor2, modebilinear, align_cornersFalse) d2 self.dec2(torch.cat([d2, e2], dim1)) d1 F.interpolate(d2, scale_factor2, modebilinear, align_cornersFalse) d1 self.dec1(torch.cat([d1, e1], dim1)) out F.interpolate(d1, scale_factor2, modebilinear, align_cornersFalse) return torch.sigmoid(self.head(out)) if __name__ __main__: net LightReconNet() x torch.randn(2, 3, 256, 256) y net(x) print(输入, x.shape, 输出, y.shape) print(参数量, sum(p.numel() for p in net.parameters()) / 1e6, M)这段代码的关键点有三个。第一InvertedResidual里的groupshidden是深度可分离卷积把标准卷积的参数量从k*k*in*out降到k*k*in in*out这是轻量化的主要来源。第二expand_ratio4控制升维倍数设 2 会更轻但表达力下降设 6 接近 MobileNetV2 原版我一般从 4 起步。第三解码器的torch.cat跳跃连接把编码器浅层特征引过来弥补下采样丢掉的细节这对小缺陷重建很关键。跑通后你会看到参数量在 0.3M 到 0.5M 之间输入 256×256 时单张推理在 CPU 上约 20msGPU 上 3ms 以内。2.3 训练脚本与损失函数怎么配重构网络训练只用正常样本损失函数用 MSE 或 L1 都行我一般用 L1 加 SSIM 的组合L1 保证像素级重建SSIM 保证结构相似。下面是一个训练循环的骨架。import torch from torch.utils.data import DataLoader from torchvision import transforms from PIL import Image import glob, os class NormalDataset(torch.utils.data.Dataset): 只加载正常样本缺陷样本不参与训练 def __init__(self, root, size256): self.files glob.glob(os.path.join(root, *.png)) self.tf transforms.Compose([ transforms.Resize((size, size)), transforms.ToTensor(), ]) def __len__(self): return len(self.files) def __getitem__(self, i): img Image.open(self.files[i]).convert(RGB) return self.tf(img) def ssim_loss(pred, target): 简化 SSIM用均值方差近似工程上够用 mu_p, mu_t pred.mean([2,3]), target.mean([2,3]) var_p pred.var([2,3]); var_t target.var([2,3]) cov ((pred - mu_p.unsqueeze(-1).unsqueeze(-1)) * (target - mu_t.unsqueeze(-1).unsqueeze(-1))).mean([2,3]) c1, c2 0.01**2, 0.03**2 return 1 - ((2*mu_p*mu_t c1)*(2*cov c2)) / \ ((mu_p**2 mu_t**2 c1)*(var_p var_t c2)) def train(root, epochs50, batch8, lr1e-3): device cuda if torch.cuda.is_available() else cpu net LightReconNet().to(device) loader DataLoader(NormalDataset(root), batch_sizebatch, shuffleTrue) opt torch.optim.Adam(net.parameters(), lrlr) for ep in range(epochs): net.train(); total 0 for x in loader: x x.to(device) y net(x) loss F.l1_loss(y, x) 0.5 * ssim_loss(y, x) opt.zero_grad(); loss.backward(); opt.step() total loss.item() print(fepoch {ep1} loss {total/len(loader):.4f}) torch.save(net.state_dict(), recon.pth) if __name__ __main__: train(./normal_images)参数上lr1e-3配 Adam 是稳妥起点loss 不降就降到 5e-4。batch8在 8G 显存上跑 256×256 没问题显存小就降到 4。SSIM 权重 0.5 是我试出来的平衡点调高会让重建偏平滑、小缺陷反而被抹掉调低则结构信息不足。训练轮数看 loss 曲线一般 30 到 50 轮收敛正常样本少于 200 张时容易过拟合要做随机裁剪和翻转增强。3. 推理与阈值标定把重构误差变成缺陷掩码3.1 推理流程与误差图生成训练完只是第一步真正决定检出率的是推理阶段的误差图和后处理。推理时把待检图送进网络得到重构图逐像素算绝对差再高斯模糊一下抑制噪声最后卡阈值出二值掩码。import cv2 import numpy as np import torch def infer(net, img_path, thresh0.08, blur5): 推理单张图返回缺陷掩码和误差图 device next(net.parameters()).device img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (256, 256)) x torch.from_numpy(img).permute(2,0,1).float().unsqueeze(0) / 255.0 x x.to(device) with torch.no_grad(): recon net(x) diff torch.abs(recon - x).squeeze().cpu().numpy() diff cv2.GaussianBlur(diff, (blur, blur), 0) mask (diff thresh).astype(np.uint8) * 255 return mask, diff if __name__ __main__: net LightReconNet() net.load_state_dict(torch.load(recon.pth, map_locationcpu)) net.eval() mask, diff infer(net, test_defect.png) cv2.imwrite(mask.png, mask) print(缺陷像素占比, (mask 0).mean())thresh是最关键的参数没有之一。它不能拍脑袋定要用一批正常样本跑一遍统计误差图的均值和标准差阈值取均值 3×标准差到均值 5×标准差之间。blur用 5×5 高斯核太大糊掉小缺陷太小噪声点多。缺陷像素占比超过 0.5% 就报警这个比例按你的最小缺陷尺寸反推。3.2 阈值标定的实操方法标定阈值我一般分三步走。第一步收集 50 到 100 张产线正常图跑推理得到每张的误差图把所有像素误差拉平算全局均值和标准差。第二步用均值 k×标准差生成候选阈值k 从 2 到 6 每隔 0.5 试一次在验证集上算误报率和漏报率。第三步画 ROC 曲线找拐点工业场景通常优先保召回误报率可以放宽到 5%。def calibrate(net, normal_dir, k_rangenp.arange(2, 6.5, 0.5)): 用正常样本标定阈值输出各 k 值下的误报率 import glob errors [] for p in glob.glob(os.path.join(normal_dir, *.png)): _, diff infer(net, p, thresh1.0) # 阈值设 1 拿全误差图 errors.append(diff.ravel()) errors np.concatenate(errors) mu, sigma errors.mean(), errors.std() print(f误差均值 {mu:.4f} 标准差 {sigma:.4f}) for k in k_range: t mu k * sigma fp (errors t).mean() print(fk{k:.1f} 阈值{t:.4f} 正常样本误报率{fp*100:.3f}%) return mu, sigma if __name__ __main__: net LightReconNet() net.load_state_dict(torch.load(recon.pth, map_locationcpu)) net.eval() calibrate(net, ./normal_images)跑完你会看到误报率随 k 增大快速下降k4 时通常能压到 0.1% 以下。但注意这个阈值只对当前光照和材质有效换批次要重新标。我见过最坑的情况是白天标好阈值夜班灯光一变误报率飙到 20%所以产线上要么固定光源要么加自动白平衡预处理。4. 避坑与排查重构网络落地时最容易翻车的五件事4.1 现象训练 loss 降到很低但推理全是误报原因通常是训练集和推理集的预处理不一致。训练时用了Resize加ToTensor推理时如果忘了归一化到 0-1或者 BGR 和 RGB 没对齐重构图会整体偏移误差图全图偏高。解决方法是把预处理写成一个函数训练和推理共用别在两处各写一遍。4.2 现象小划痕检不出大缺陷反而漏这是解码器跳跃连接没配好。浅层特征引少了小缺陷在重建时被平滑掉引多了大缺陷区域的重建误差被稀释。我一般把编码器前两层的特征都引到解码器第三层视情况。另外 SSIM 权重别超过 0.7否则重建图过于平滑小缺陷直接消失。4.3 现象换一批材料后误报率暴涨重构网络对纹理分布敏感训练集只有一种材质换材质就是分布外样本。解决方法是每换一种材质补采 30 到 50 张正常图做微调只训解码器最后两层学习率降到 1e-4训 5 到 10 轮就够。别全网络重训容易把之前学的纹理忘掉。4.4 现象推理速度达不到产线节拍先看瓶颈在哪。用torch.cuda.synchronize()卡时间如果预处理占了大头把 resize 和归一化换成 GPU 上的torch.nn.functional.interpolate。如果网络本身慢把base_ch从 16 降到 12或者输入从 256 降到 192精度掉 1 到 2 个点速度能快 40%。导出 ONNX 再用 TensorRT 跑通常还能再快一倍。4.5 现象模型文件不大但显存占用高参数量小不等于显存小。推理时如果没加torch.no_grad()中间激活值会保留计算图显存翻几倍。另外F.interpolate的align_cornersFalse在部分 PyTorch 版本上有显存泄漏的坑升级到 1.12 以上能缓解。部署到边缘盒子时用torch.jit.trace导出能省掉 Python 解释器开销。5. 把重构误差图用到位从掩码到分级报警的一个技巧很多人做到二值掩码就停了其实误差图本身携带的信息比掩码多。我的习惯是把误差图做连通域分析按面积和最大误差值给缺陷分级面积小于 50 像素且最大误差低于 2 倍阈值的标为「疑似」只记录不报警面积 50 到 500 像素或误差超 3 倍阈值的标为「确认」触发报警面积超 500 像素的直接停机。这样产线不会被小噪点频繁打断又能抓住真缺陷。def grade_defect(mask, diff, thresh): 按连通域面积和误差强度给缺陷分级 num, labels, stats, _ cv2.connectedComponentsWithStats(mask, 8) results [] for i in range(1, num): area stats[i, cv2.CC_STAT_AREA] region diff[labels i] peak region.max() if region.size else 0 if area 50 and peak 2 * thresh: level 疑似 elif area 500 or peak 3 * thresh: level 确认 else: level 停机 results.append({area: int(area), peak: float(peak), level: level}) return results这个分级逻辑的好处是把「阈值」从一个硬边界变成两个维度面积和强度。单看面积细长划痕面积小但强度高会被漏掉单看强度大片浅脏污强度低但面积大也会漏。两个维度一起卡召回和误报的平衡好调很多。参数上50 和 500 这两个面积阈值按你的相机分辨率换算我用的 256×256 输入对应原图 2048×2048实际面积要乘 64。验证分级效果的方法拿一批带标注的缺陷图跑完推理后统计每个等级的召回率和误报率画混淆矩阵。如果「确认」等级的召回低于 90%把面积下限从 50 降到 30 再试。如果「疑似」等级误报太多把峰值条件从 2 倍阈值提到 2.5 倍。这套调参没有万能值但方向是明确的先保召回再压误报最后用分级把剩余误报挡在报警之外。我自己踩过最深的一个坑是早期版本没做分级产线每两分钟报一次警操作工直接把报警器关了等于白做。后来加了分级和连续三帧确认才稳住。做工业检测算法精度是一方面报警策略能不能让人愿意用是另一方面。希望帮到你。本文还有配套的精品资源点击获取
返回列表