尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于特征嵌入与PatchCore的工业缺陷检测实战指南

发布时间:2026/9/24 11:37:44

资讯中心
01
ARTICLE

基于特征嵌入与PatchCore的工业缺陷检测实战指南

基于特征嵌入与PatchCore的工业缺陷检测实战指南
简介服务于工业缺陷检测方向的PatchCore算法实践资料面向具备一定编程基础的本科生、研究生及数据科学从业者。资源以PDF形式提供完整大作业内容涵盖基于重构与基于嵌入两类检测思路对比重点讲解PatchCore的特征提取、核心集采样、最近邻搜索与热力图生成流程并给出MVTec AD及相关工业数据集上的实验分析要求。资料共1个PDF文件压缩包大小192KB便于下载后直接阅读。已有459人学习。读者可据此复现完整项目流程掌握像素级/样本级AUC评估、ROC与PR曲线绘制方法并借助功能实现、实验报告与代码规范等评分标准自查提升。适合正在完成课程设计或入门工业视觉缺陷检测的读者系统学习。1. 特征嵌入、PatchCore与工业缺陷检测为什么缺缺陷样本也能建检测系统我在产线视觉项目里最常被问的一句话是“我们只有几百张好品图片坏品还没攒够能上检测吗”传统监督方案答不了这个问题——缺陷样本不够分类器和目标检测都学不动。而 PatchCore 这类基于特征嵌入的异常检测算法恰恰是为这个场景设计的只用正常样本建一个“记忆库”新品图进来后对比它离记忆库有多远距离大就是缺陷。这篇笔记顺着 PatchCore 的核心链路往下拆特征嵌入怎么提、记忆库怎么建、异常分数怎么算、参数怎么调、部署有哪些坑。我尽量按自己在本地复现 PatchCore 时的顺序写新手照着敲能跑通熟手可以跳过原理直接看第 5 章的排错和第 6 章的阈值标定。2. 特征嵌入为什么选 ResNet 而不选分类头残差结构、层间分辨率和记忆库的工作方式2.1 为什么是特征嵌入而不是像素差工业缺陷检测最早期的做法是差影法固定工位、固定光照拿好品模板和当前图像逐像素相减差值超过阈值就报警。这套方案在“产品不动、光源不动、相机不动”的完美条件下确实能用但产线不是实验室产品换型、光源老化、轻微震动都会让像素差整体变大误杀率高到没法用。特征嵌入的思路是把图像送进一个在大规模自然图像上预训练好的卷积网络取出中间层特征图来用。特征图上的一个点不是原图上一个像素而是原图上一块感受野的抽象表示浅层记录边缘、角点、纹理走向深层记录局部形状和结构。于是产品版型变了、光照变了像素差全变了但只要产品的“语义结构”没变特征分布就是稳定的。这就是特征嵌入在工业缺陷检测里能成立的根本原因。2.2 为什么默认选 ResNet以及选哪几层做特征嵌入时骨干网络的选择常见做法是直接用 ResNet50而不是更深的 ResNet101 或更强的 Vision Transformer。我一般会优先考虑ImageNet 预训练权重对所有开源框架都是现成的复现成本最低ResNet 残差结构让梯度在深层的传播稳定我们敢同时取多个中间层特征ViT 不是不行但它在 Patch 切分时对输入尺寸更敏感预训练权重对工业现场机械纹理的适配未必比 CNN 稳。以 PyTorch 官方模型为例输入 224×224 时layer2 输出 28×28×512layer3 输出 14×14×1024。PatchCore 的常见取法是组合 layer2 和 layer3layer2 分辨率高能捕捉细小划痕、凹坑这类小尺度缺陷layer3 语义更全局对形变、大面积污染更敏感。只取最后一层分类前的特征缺陷信息会被全局池化平均掉小缺陷几乎完全丢失。所以“选哪层”不是偏好问题是分辨率与语义的权衡。2.3 记忆库、Coreset 采样和最近邻打分PatchCore 的推理链路可以压缩成三个动作。第一步把所有正常训练图逐张提取 patch 特征拼成一个大的特征集合叫记忆库Memory Bank。第二步用贪心的 Coreset 采样把记忆库压缩到原来的几个百分点因为直接存全部特征每张图 28×28 加上 14×14 共 980 个 patch一千张训练图就是近百万条特征推理时做最近邻搜索的时间不可接受。第三步新品图提取同样的 patch 特征后在压缩后的记忆库里找最近邻最近邻的欧氏距离就是该 patch 的异常分数分数越高越可能是缺陷。这里要特别说明记忆库里只有正常样本。工业现场缺陷样本稀缺甚至根本没有PatchCore 能在这种数据条件下工作这是它比监督方案更适合冷启动的原因。整个流程里没有反向传播训练只有特征提取、采样和距离计算所以它在普通 GPU 上就能跑CPU 也能接受。2.4 为什么 patch 级特征比图像级特征稳如果把整张图压成一个全局特征向量再算距离图像里 99% 是正常区域局部缺陷的信息会被大面积正常区域稀释。一个小划痕在全局向量里可能只体现在几个维度上距离变化完全被淹没。patch 级特征把图像按空间位置拆开每个 patch 独立打分最后合成一张异常分数图缺陷在哪、多大、什么形状都保留了下来。这也是 PatchCore 在 MVTec 这类基准上表现好的原因它输出的不是“这张图有没有问题”的单一分数而是一张可以叠加到原图上的热力图。现场工程师拿热力图去定位缺陷位置就能判断是真实缺陷还是正常纹理波动这对产线信任度很重要。3. 用 Python 在本地复现 PatchCore从最小依赖到异常分数图输出3.1 从 patchcore 代码复现到本地跑通最小依赖与目录约定我在跑 patchcore 代码复现时最小依赖就四个torch、torchvision、numpy、scikit-learn。可视化再加 opencv-python进度显示加 tqdm。建议先把 torch 装好再装其他包因为 torchvision 的版本直接决定 ResNet 预训练权重的加载方式。pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy scikit-learn opencv-python tqdm安装时注意一点torch 和 torchvision 的版本必须匹配否则 import torchvision 时会直接报错。我的建议是装完后立刻执行下面这行验证能顺利打印版本号再继续避免后面所有报错都糊在一起。python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)数据集目录我建议按这个结构放训练只读 train/good测试按产品类型分子目录。MVTec 数据集就是这个结构换成你自己的产线数据时保持同样约定后续脚本就不用改路径逻辑。data/ train/ good/ 000.png 001.png test/ good/ 000.png defect/ 000.png3.2 特征提取与记忆库构建hook 接在哪层维度怎么算特征提取我不用修改模型结构直接注册 forward hook 把 layer2、layer3 的输出捞出来。以 ResNet50 输入 224×224 为例layer2 输出 [1, 512, 28, 28]layer3 输出 [1, 1024, 14, 14]通道数不同、分辨率不同拼接前先把 layer2 上采样到 14×14。这段代码是整个流程的基础后面采样和推理都依赖它。import torch import torch.nn.functional as F from torchvision import transforms, models from PIL import Image import numpy as np device cuda if torch.cuda.is_available() else cpu model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1).to(device) model.eval() feature_layers [layer2, layer3] features {} def make_hook(name): def hook(module, input, output): features[name] output return hook for name in feature_layers: model._modules[name].register_forward_hook(make_hook(name)) transform transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) def extract_patch_features(img_path): img Image.open(img_path).convert(RGB) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): _ model(x) target_h, target_w features[layer3].shape[-2:] feats [] for name in feature_layers: f features[name] if f.shape[-2] ! target_h: f F.interpolate(f, size(target_h, target_w), modebilinear, align_cornersFalse) feats.append(f) feat torch.cat(feats, dim1) # [1, C, H, W] b, c, h, w feat.shape patch_feats feat.permute(0, 2, 3, 1).reshape(h * w, c).cpu().numpy() patch_feats patch_feats / (np.linalg.norm(patch_feats, axis1, keepdimsTrue) 1e-12) return patch_feats这段代码里最容易被忽略的是最后的 L2 归一化。不同层的特征数值范围不同layer3 的响应值普遍比 layer2 大不归一化的话最近邻距离会被数值大的维度主导等于 layer2 的白提。加一个小常数 1e-12 是为了防止某个 patch 特征为零向量时除零。建记忆库就是遍历训练集所有正常图把每张图的 patch 特征累积拼起来。正常图哪怕只有 200 张一张图 196 个 patch也有接近四万条特征足够支撑第一版验证。from pathlib import Path from tqdm import tqdm train_good_dir Path(data/train/good) bank [] for img_path in tqdm(sorted(train_good_dir.glob(*.png))): patch_feats extract_patch_features(str(img_path)) bank.append(patch_feats) memory_bank np.concatenate(bank, axis0) # [N, C] np.save(memory_bank.npy, memory_bank) print(memory bank shape:, memory_bank.shape)3.3 贪心 Coreset 采样把记忆库压缩到几个百分点记忆库直接用于推理不是不行但四万条特征做最近邻搜索每张测试图要算四万次距离单张图还能忍批量验证时就很难受。贪心 Coreset 的思路是从记忆库里选出一个子集使得子集里每个点都能代表附近一大片特征这样推理时只需要在这个子集里找最近邻。def greedy_coreset(memory, sample_num, random_seed0): rng np.random.default_rng(random_seed) idx [int(rng.integers(0, len(memory)))] dist np.linalg.norm(memory - memory[idx[0]], axis1) for _ in tqdm(range(sample_num - 1), desccoreset sampling): next_idx int(np.argmax(dist)) idx.append(next_idx) d np.linalg.norm(memory - memory[next_idx], axis1) dist np.minimum(dist, d) return memory[idx]这段代码的逻辑是每次选一个离“已选集合”最远的点加入选出来的点子集能最大程度覆盖原始特征空间。采样数量 sample_num 我一般按记忆库总量的 5% 到 10% 来设先跑通流程再回来调。注意这是一个 O(MND) 的循环M 是采样数N 是记忆库总量特征维度 C 是 1536数据量大时建议把 memory 切块分批计算或者干脆先随机抽样一个子集再在这个子集上做贪心速度会快很多。3.4 推理时如何打分最近邻距离与异常图上采样推理阶段用 scikit-learn 的 NearestNeighbors 在采样后的记忆库上建索引每张测试图的每个 patch 特征查一次最近邻距离就是异常分数。分数 reshape 回 14×14 后上采样到原图尺寸就可以可视化叠加了。from sklearn.neighbors import NearestNeighbors import cv2 memory_sub greedy_coreset(memory_bank, sample_numint(len(memory_bank) * 0.05)) nn NearestNeighbors(n_neighbors1, metriceuclidean, algorithmbrute) nn.fit(memory_sub) def score_image(img_path, original_size(224, 224)): patch_feats extract_patch_features(img_path) dist, _ nn.kneighbors(patch_feats) # [H*W, 1] h, w 14, 14 score_map dist.reshape(h, w).astype(np.float32) score_map cv2.resize(score_map, original_size, interpolationcv2.INTER_LINEAR) return score_map这里有两个细节。第一n_neighbors 在公开复现里常见取 9取 1 时分数噪声大对边缘敏感但定位更锐利取 9 会更平滑适合筛选阶段。第二algorithm 用 brute 而不是默认的 auto因为特征维度 1536 远超 kd-tree 的适用维度默认算法在高维下反而慢。score_map 可以直接用 cv2.applyColorMap 映射成热力图保存也可以先做一次高斯模糊再叠加到原图上。4. PatchCore 调参3 个决定检测上限的硬参数和 1 个必须盯住的离线指标4.1 features_list层组合是 PatchCore 的“玄学开关”layer2 加 layer3 是默认组合但它不是万能的。缺陷是细划痕、针孔这类小目标时layer2 的 28×28 分辨率仍然不够需要把 layer1 也加进来代价是特征维度涨到 1792记忆库内存和推理耗时都明显上升。缺陷是大面积的脏污、氧化时layer3 就够用加 layer2 反而会让正常纹理的 patch 级波动变大误杀率升高。我调层组合的习惯是先固定其他参数单独跑 layer3、layer2layer3、layer2layer3layer1 三组离线验证看正常样本最高分和缺陷样本最低分之间的间隔。这个间隔比 AUC 更直观间隔越大阈值越好定上线后越稳。不要一上来就追求全层组合“层越多越准”在 PatchCore 里不成立。4.2 Coreset 采样比例公开基准上的“5%”为什么不该照抄采样比例直接决定两个东西记忆库的覆盖度和推理延迟。比例越低覆盖度越差正常样本中没被代表的那部分特征在推理时会得到偏高的分数误杀率上升比例越高推理越慢显存占用也越大。公开基准上常见做法是取 5% 到 10%但那是基于 MVTec 数据集的纹理和物体类别得出的换到你的产线产品上这个比例不一定合适。我一般会先按 5% 跑一遍然后把测试集里正常样本的分数分布画出来看 99 分位分数是否显著高于训练集正常样本的中位数。如果高说明记忆库覆盖不够把比例拉到 10% 再比一轮。注意采样比例不是精度指标它是在“记忆覆盖率”和“推理速度”之间做权衡产线节拍紧张时10% 的采样可能就得换更强的硬件。4.3 输入尺寸与 k 近邻个数分辨率变了分数分布跟着变输入尺寸对 PatchCore 的影响常被低估。用 224×224 输入一张产线大图里的微小缺陷可能只占几个像素特征提取后直接消失。常见做法是把输入分辨率提到 384 甚至 512但这不是免费的layer2、layer3 的 patch 数量从 980 涨到约 2888记忆库总量和推理耗时同步上涨。k 近邻个数的调法更像个经验活。k1 时分数锐利缺陷定位准但对正常样本的纹理波动也敏感k9 是默认值分数平滑小缺陷可能被邻居平均掉。我的经验是缺陷本身像素级很小用 k1缺陷是区域性的用 k9。这个参数单独调意义不大要和分数图的后处理配合看。4.4 用 AUC 还是漏检率来验证离线指标选错会让调参白做很多人在复现 PatchCore 时只看 AUCAUC 高就觉得调参到位了。但工业落地真正关心的是两个数漏检率和误杀率。AUC 是 ROC 曲线下的面积它衡量的是分数排序能力不代表一个固定阈值下的实际表现。你完全可以调出一个 AUC 很高的模型但在某个阈值下误杀率高达 5%产线上根本没法用。我的验证习惯是固定一个候选阈值输出三件事缺陷样本中被漏掉的张数和具体图正常样本中被误杀的张数和具体图以及正常样本分数分布的 99 分位和 99.9 分位。前两件事决定方案能不能上线第三件事决定阈值怎么标定。调参过程中每改一个参数都记录这三个数比盯着 AUC 调更可靠。5. PatchCore 复现与部署排错5 个会让检测结果崩掉的细节5.1 没做特征 L2 归一化分数被某一维特征主导现象正常样本和缺陷样本的分数分布完全重叠热力图上整张图都是红的缺陷定位不出来。换不同缺陷图跑分数排名也和直觉完全不符。原因layer2 和 layer3 拼接后的特征维度是 1536其中 layer3 的响应值数值范围比 layer2 大得多。没做归一化时欧氏距离基本由 layer3 的少数高响应维度决定layer2 的纹理细节等于白提。解决在提取函数里加一行patch_feats patch_feats / (np.linalg.norm(patch_feats, axis1, keepdimsTrue) 1e-12)。注意训练建记忆库和推理打分必须用同一套归一化逻辑改了提取函数就要重新生成 memory_bank.npy否则新旧特征分布对不上。5.2 训练目录里混入了坏图正常基准被污染现象某种缺陷完全检不出来其他缺陷都正常。排查发现该缺陷在训练集里出现过几张被当成正常样本存进了记忆库。原因数据准备阶段只看了文件名没看图把带缺陷的图放进了 train/good。PatchCore 对正常样本的纯度非常敏感记忆库里一旦存在缺陷特征推理时同类缺陷的 patch 就能在记忆库中找到很近的邻居分数被压成正常水平。解决建库前先用人工或简单的图像统计筛一遍训练集重点看分辨率异常、亮度异常、有明显大块黑斑的图。更稳妥的做法是每次训练前单独抽 10% 的正常样本不进记忆库用它来验证“这个库对纯正常样本的分数是否足够低”。5.3 torchvision 版本变换出现权重加载失败现象从网上复制的代码跑到models.resnet50(pretrainedTrue)时报警告甚至直接报错说某个权重 key 不匹配。多见于把项目从一台机器搬到另一台机器时。原因torchvision 0.13 之后把pretrainedTrue标记为过时推荐用weightsmodels.ResNet50_Weights.IMAGENET1K_V1的枚举方式。旧代码的传参方式在新的 torchvision 里行为不一致部分版本还会加载到不同来源的权重。解决代码里显式写weightsmodels.ResNet50_Weights.IMAGENET1K_V1并且把 torch 和 torchvision 的版本号固定到 requirements 文件里。项目迁移后先跑一次第 3.1 节的版本验证命令再继续后面的流程。5.4 推理时整批图同时进显存导致 OOM现象单张图推理正常改成批量推理后爆显存报 CUDA out of memory。特征提取部分能过NearestNeighbors 查询也时报错。原因批量推理时 forward 会同时保留多张图的中间层特征layer2 和 layer3 的特征图叠加后显存开销不小。更隐蔽的是有些实现会在 GPU 上保留完整特征图用于后续处理没有及时转回 CPU 或释放。解决推理循环里逐张 forward拿到 patch 特征后立刻.cpu().numpy()并del掉中间变量。异常图的上采样用 cv2 在 CPU 上做不要用 torch 的 interpolate 再开一张 GPU tensor。如果确实需要批量加速把 batch size 压到 4 以下并在每个 batch 结束后手动清一次缓存。5.5 训练和推理的预处理不一致分数完全失效现象训练时分数分布正常上线后所有测试图的分数普遍偏高连正常样本都接近异常阈值。重新跑离线验证又是好的代码看起来没动过。原因训练流程用的预处理是 Resize(256) CenterCrop(224)推理服务里为了方便写成了 Resize(224)。输入分辨率变了layer2 和 layer3 的输出特征图尺寸也变了同一个位置的 patch 对应的感受野完全不同分数自然对不上。解决把预处理封装成一个函数或配置对象训练、验证、推理三处引用同一个定义。上线前写一个冒烟测试拿三张训练集里的正常图走一遍完整推理链路确认分数与离线验证时一致再放开流量。6. 从离线到上线PatchCore 上线前最后三步验证与阈值标定离线通过不代表能上线。我通常按三步走完才敢把 PatchCore 的跑批任务挂进产线。第一步从正常样本里固定抽出 5% 作为“验证正常集”永远不放进记忆库专门用来测误杀率再混入已确认的缺陷图构成“验证异常集”。第二步跑完整推理后画出分数分布图正常集和异常集之间只要有明显间隔这个方案基本成立。第三步确定阈值。阈值标定我不用均值加三倍标准差而是取验证正常集分数的 99.9 分位作为初始阈值再拿验证异常集里漏检率最高的那张图往回看确认是缺陷本身太轻微还是被正常特征覆盖。这种做法牺牲一点误杀率换取漏检率可控因为在工业现场漏检一块瑕疵板流出的成本通常远高于多停一次机。候选阈值位置漏检率误杀率适用场景正常集 95 分位低高缺陷容忍度极低、误杀可人工复判正常集 99 分位中中常规产线首版正常集 99.9 分位高低缺陷不致命、误杀代价高上线后还要留一张“后悔药”式的底牌把每次检测输出的最高分、正常样本分数分布、误杀图片都落盘存下来至少要存两周。我吃过一次亏上线时阈值定了第三周产品换了一个表面处理工艺正常样本特征分布整体偏移误杀率直接飙到 6%。当时没留分布数据排查了一天才发现是工艺变更导致的正常波动而不是模型失效。所以我现在养成的习惯是任何一次版本改动跑完验证必须把验证集、分数分布、阈值、误杀样本四样东西打包存档下次复现时先对比存档再调参数。这个习惯帮我把 PatchCore 从实验脚本迁到产线批任务时省了很多时间。如果你照这套流程跑下来还是有问题先按第 5 章的顺序排查特别是预处理一致性和数据目录纯度这两项大多数翻车都出在这里。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。