ARTICLE DETAIL

资讯详情

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

基于AD-DINOv3的零样本缺陷检测:原理、代码与MVTec AD实战

基于AD-DINOv3的零样本缺陷检测:原理、代码与MVTec AD实战 做工业质检的朋友应该都有过这种体验产线那边临时要加外观检测环节你过去一看能拿到的缺陷样本就几十张还全是一种类型的。拿这点数据去训分类网络别说遇到新缺陷连同一批次的正常波动都扛不住。我踩了大半年坑后转向了一条完全不同的路线——零样本缺陷检测核心思路不是教模型认识每种缺陷而是让它深刻理解“正常”长什么样。最近我把DINOv3的特征提取针对异常检测场景做了适配在MVTec AD标准数据集上完整验证了一遍效果相当能打。这篇文章就把这套方案的原理、代码和坑完整记录下来适合工业视觉从业者和刚接触异常检测的朋友。1. 传统质检到底卡在哪零样本思路为什么能破局1.1 真实产线三大困境数据少、形态多、标注贵在工业外观检测里大家第一反应都是走经典监督路线拍图、标注、训练分类或者目标检测模型。这套思路在实验室里跑通很容易真正丢到产线上就会撞上三堵墙。第一堵墙是缺陷数据极度稀缺。工厂里的正常品每天能堆成山缺陷品却往往只有几十张而且这些缺陷还集中来自同一批工艺错误。你要是拿几十张缺陷图去微调一个深度模型最好的结局就是过拟合到这几张图上换一条产线立刻失效。这个问题的本质在于工业过程本身追求的就是“少出缺陷”缺陷样本少不是运气不好而是产线运转正常的直接结果。第二堵墙是缺陷形态永远在变。划痕、凹陷、脏污、断裂、色差、变形每一种缺陷内部还有轻重缓急的区分。更麻烦的是你不可能预先知道明天会出什么新的不良模式。监督方案本质上是在列举缺陷而真实缺陷是开放集合列举不完。我在项目的早期阶段就吃过这个亏花了两周时间把常见缺陷类型都标了一遍结果第三周产线上出现了之前从没见过的水渍型缺陷模型直接漏检。从那之后我就明白跟缺陷做枚举对抗是个无底洞。第三堵墙是标注成本。要训练一个能用的分割模型像素级标注一张缺陷图少说也要好几分钟。一个类别几百张标注图累计就是几百小时的工作量这还不算人工复核和标准统一带来的额外沟通成本。对工厂来说质检方案如果不能快速部署、快速换线那它在成本账上就很难看。这三堵墙决定了传统监督方案在很多中小批量、多品种的生产线上根本算不过账。但反过来看正常样本是永远不缺的而且“正常”的定义相对稳固。那我们为什么不换个角度把模型的注意力全部放在认识正常上1.2 不认缺陷只认正常零样本检测的底层逻辑零样本缺陷检测的思路其实非常朴素给模型一批纯正常样本让它记住这些正常样本的特征分布来了新图片只要看它和正常特征库差了多少差得多的区域就是缺陷。整个过程不需要一张缺陷样本参与训练所以天然不受缺陷形态限制。这里就引出了整个方案能不能成立的关键——特征表示。你得有一个足够强的特征提取器能在一张正常图上提取出密集的特征这些特征既要能表达局部纹理信息又要能区分细微的异常变化。早期有人用ImageNet预训练的ResNet做这件事效果还行但特征是从分类任务训练出来的更关注“物体是什么”而不是“这个区域是不是正常”所以在细粒度缺陷上经常漏检。后来PatchCore等方法用多尺度特征缓解了一部分问题但特征的判别力本质上仍然被预训练任务限制着。直到DINO系列出来之后情况才真正发生变化。DINOv2证明了自监督学习出来的视觉特征在细粒度匹配和局部语义上远超ImageNet监督特征DINOv3又在数据规模、训练稳定性和特征质量上更进一步。而且这类特征本身是patch级组织起来的天然适合做密集定位。AD-DINOv3要做的事情就是把DINOv3这个强大的特征基座接上一套高效、稳定的最近邻比对逻辑让它变成可以直接上线的异常检测工具。接下来的章节我就把这个方案的原理拆开讲。2. AD-DINOv3是怎么做零样本检测的原理与组件拆解2.1 DINOv3特征凭什么能当质检的眼睛要理解AD-DINOv3为什么有效得先理解DINO系列特征的特点。DINOv3沿用了自蒸馏的预训练范式也就是让模型自己和自己学通过对比学习把图像里语义相近的区域映射到特征空间里的相近位置。这个训练过程没有标签但逼着模型去理解图像内容本身学出来的特征对不同物体、不同纹理、不同局部结构都有清晰的分层表达。对异常检测来说最有用的一条是DINOv3的特征会把“模式上的局部偏离”放大成特征空间里的明显距离。举个例子一块地毯的纹理在两个相邻patch之间应该是连续的、相似的但如果其中一个patch里混进了一条划痕这个patch的特征向量就会和周围patch以及参考特征库里的正常特征明显拉开距离。这种敏感性不是靠标注样本学出来的而是自监督预训练对视觉世界统计规律的建模结果所以它对任何没见过的新缺陷类型同样有效。另一个对异常检测特别友好的点是patch级特征的空间组织。输入图片被切分成一个个patch每个patch经过Transformer编码后输出一条高维特征向量这些特征向量天然保留了空间位置关系。这就意味着我们可以直接拿特征向量做逐patch比对得到的异常分数再上采样回像素级缺陷定位几乎不需要额外的结构设计。AD-DINOv3的适配工作重点就是围绕这个特征基座做工程优化。一方面选择合适的特征层组合让特征既能覆盖局部纹理又能感知一定范围的上下文另一方面把特征提取、特征库维护、最近邻检索这几个环节做成一个轻量级的推理管线保证在质检机上跑得动、看得准。2.2 零样本异常检测的完整流程特征库加最近邻整套方案的推理流程用一句话概括建库、查库、打分。建库阶段准备一批正常样本全部输入AD-DINOv3提取每个patch的特征向量汇总成一个特征库。这里有几个细节值得展开。第一参考特征库的规模并不需要特别大我实测下来每个类别用30到50张正常图做出来的特征库已经能覆盖该类别的正常形态变化更多样本的边际收益明显递减但内存占用和检索耗时一直在涨。第二特征库里的数据可以先做随机采样或者降维控制后续近邻检索的开销。第三特征要归一化因为后续比对我们更关心特征的方向差异而不是模长差异归一化之后可以直接用余弦距离代替欧氏距离。查库阶段把待检测图片同样送入模型提取patch特征然后对每个patch特征在特征库中找出距离最近的若干个特征向量。这个最近邻距离就是异常分数的核心依据。距离越大说明该patch和所有正常样本都不像它属于缺陷区域的概率就越高。打分阶段把patch级的距离组成热力图再上采样到和原图一致的分辨率。根据设定的阈值把热力图中超过阈值的像素标记出来就得到缺陷区域。整个过程不需要训练模型不需要反向传播新产线换产品类型时只需要重新拍几张正常样本建库即可。这里我想专门说明一下为什么用最近邻而不是直接算和平均特征的偏离。参考集里的正常样本本身存在些微差异直接和均值比较个别正常样本的偏移会被当成异常而最近邻检索只看“最像的那一个或几个”对正常样本的多样性更加鲁棒。拿产线场景来说同一批次里零件的光泽、摆放角度、环境光都会有细微不同最近邻方式能在这些正常波动面前保持稳定只在真正偏离正常分布的区域报警。2.3 和其他零样本方案比AD-DINOv3赢在哪零样本异常检测近几年方案不少最有名的是PatchCore和WinCLIP。我简单从工程使用者的角度做个对比。方案特征来源是否需要额外训练定位精度推理成本PatchCoreImageNet预训练CNN特征否中中WinCLIPCLIP多模态特征否中低中AD-DINOv3DINOv3自监督特征否高略高PatchCore是残差网络特征加coreset核心集采样优点是复现容易、推理快但CNN特征的判别力在细粒度纹理异常上确实弱于DINO特征。我之前拿PatchCore在皮革和瓷砖两个类别上测过像素级定位结果漏检率明显比AD-DINOv3高。WinCLIP借助CLIP的图文对齐能力可以做多模态prompt零样本检测但CLIP特征的空间分辨率不高像素级定位本来就吃亏。AD-DINOv3直接吃DINOv3的自监督视觉特征在密集定位这件事上有先天优势。当然AD-DINOv3也不是没有短板。patch级特征的感受野决定了它对某些跨区域的轮廓型缺陷反应不够剧烈比如瓶子的整个轮廓变形可能每个局部patch看起来都还算正常但整体结构已经不对了。这种情况建议在方案里加入全局token的对比分数作为辅助或者引入多尺度特征融合。后面实战章节我会给出具体的处理建议。3. MVTec AD实战从环境配置到缺陷热力图全流程3.1 环境准备、数据下载与目录结构先说环境。我实验用的机器是一张12GB显存的卡实测跑DINOv3的base模型加后续近邻检索显存压力并不大12GB完全够用16GB会宽松很多。软件方面Python版本建议3.9以上PyTorch用2.0以上版本CUDA用11.8或12.x都行。核心依赖是torch、torchvision、numpy、opencv-python、scipy和scikit-learn其中scikit-learn没有直接用但调试阶段比较特征维度、做PCA降维时很方便。MVTec AD数据集直接搜MVTec Anomaly Detection官网就能找到下载下来是一个压缩包体积在5GB上下。解压后的目录结构是这样的mvtec_anomaly_detection/ ├── bottle/ │ ├── train/ │ │ └── good/ │ ├── test/ │ │ ├── broken_large/ │ │ ├── broken_small/ │ │ └── good/ │ └── ground_truth/ │ └── broken_large/ └── carpet/ ...MVTec AD一共包含15个类别覆盖瓶、胶囊、电缆、地毯、皮革、瓷砖、木材、金属螺母、药片、螺丝等常见工业品。每个类别的train目录下只有正常图test目录下包含正常图和各类缺陷图ground_truth目录下存的是对应缺陷图的像素级掩码。我们用train做特征库用test做推理评估。这里要提醒一下千万不要把不同类别的训练数据混到一个特征库里。瓶子的正常特征和地毯的正常特征在特征空间里差异巨大混在一起会让最近邻检索直接失效。每个类别独立建模、独立评估这是MVTec AD实验的基本纪律也是你在自己数据集上做验证时要保持的习惯。3.2 特征提取核心代码实现特征提取是整个方案的关键环节先把这段代码拆明白。以我工程里封装的AD-DINOv3特征提取器为例核心部分是这样的import torch import torch.nn as nn from torchvision import transforms from PIL import Image import numpy as np import os # 加载AD-DINOv3特征提取器 # 这里以本地封装为例完整实现相当于DINOv3特征基座 适配层 model AD_DINOv3_Backbone.from_pretrained(weights/ad_dinov3_vitb14.pth) model.eval().cuda() # 预处理resize到统一分辨率然后归一化 transform transforms.Compose([ transforms.Resize((448, 448)), 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) tensor transform(img).unsqueeze(0).cuda() with torch.no_grad(): # forward_features返回中间token序列包括CLS和patch token output model.forward_features(tensor) # 去掉CLS token只保留patch token # 448x448输入patch size 14得到32x321024个patch token patch_feats output[:, 1:, :] # shape: [1, 1024, dim] # L2归一化方便后续用欧氏距离近似余弦距离 patch_feats torch.nn.functional.normalize(patch_feats, dim-1) return patch_feats[0].cpu().numpy()这里有几个点要讲清楚。第一forward_features拿到的输出里第0个token通常是CLS token后面才是patch token所以用output[:, 1:, :]把CLS去掉。第二为什么要对特征做L2归一化因为后续比较距离用的是余弦相似度归一化后两个向量间的夹角可以近似转化为欧氏距离来比较这样能直接使用cKDTree加速这是工程上的一个小技巧。第三resize到448x448是我调出来的平衡值太小丢细节太大显存和检索耗时都会上去这个在踩坑章节还会详细说。如果你只有原版DINOv3权重没有AD-DINOv3的适配层也不用担心。直接拿原版权重走同样的流程前面2.1里说的那些特征性质已经足够支撑整套零样本检测适配层更多是在特征层组合和归一化策略上做优化。也就是说先把流程跑通再考虑要不要换更强或者更精简的权重。特征库的构建就是继续用这个函数遍历某个类别的train/good目录把所有正常图的patch特征拼成一个矩阵train_dir mvtec_anomaly_detection/bottle/train/good ref_list [] for img_name in os.listdir(train_dir): feat extract_patch_features(os.path.join(train_dir, img_name)) # feat shape: [1024, dim] ref_list.append(feat) ref_features np.concatenate(ref_list, axis0) # shape: [N, dim] # 随机采样控制特征库规模 np.random.seed(42) if len(ref_features) 20000: idx np.random.choice(len(ref_features), 20000, replaceFalse) ref_features ref_features[idx]特征库规模这里多说一句。如果每个类别用50张正常图每张图1024个patch一共就是51200个特征向量直接用完整特征库跑最近邻检索cKDTree查询单张图大约几十毫秒还算能接受。但如果你要在高分辨率图或者大量产品型号上做实时检测建议做一次采样把特征库控制在2万到3万个特征向量以内精度损失通常很小速度收益非常明显。3.3 缺陷打分与热力图生成特征库建好之后进入打分环节。测试图同样提取patch特征然后对每个patch在特征库中寻找最近邻取前k个最近邻距离的平均值作为这个patch的异常分数from scipy.spatial import cKDTree # 构建KDTree加速近邻检索 tree cKDTree(ref_features) def compute_defect_map(img_path, tree, k5): patch_feats extract_patch_features(img_path) # shape: [1024, dim] # 查询前k个最近邻 dist, _ tree.query(patch_feats, kk) # 前k近邻的平均距离作为patch级异常分数 anomaly_map dist.mean(axis1) # shape: [1024] # 把patch级向量reshape成特征图尺寸 grid_size int(np.sqrt(anomaly_map.shape[0])) anomaly_map anomaly_map.reshape(grid_size, grid_size) # 上采样到原图分辨率 from PIL import Image as PILImage anomaly_map (anomaly_map / anomaly_map.max() * 255).astype(np.uint8) anomaly_map PILImage.fromarray(anomaly_map) anomaly_map anomaly_map.resize((448, 448), PILImage.BICUBIC) return np.asarray(anomaly_map).astype(np.float32) / 255.0这段代码的核心逻辑是异常分数取每个patch和特征库前k近邻的平均距离。为什么用前k近邻平均而不是只用最近第1个因为只用最近邻的话只要正常特征库里有任何一个特征恰好和它比较接近分数就会很低稳定性不好。取平均可以让判断更稳我一般用k5效果比k1更平滑误报也少一些。如果你发现定位出来的缺陷区域边缘比较毛躁可以试着调大k到7到10相当于提高了局部投票的窗口热力图会更干净。打分完成后把热力图叠加到原始测试图上就能直观看到缺陷区域。如果你在产线上部署这一步可以直接输出二值化mask配合开闭运算形态学滤波把零散噪点去掉再把检测结果通过Modbus或者工业相机SDK传给上位的PLC或者视觉软件。3.4 评估指标解读与我的实测数据MVTec AD标准评估指标有两个图像级AUROC和像素级AUROC。图像级AUROC衡量的是“整张图是否存在缺陷”的二分类能力像素级AUROC衡量的是“每个像素是不是缺陷”的定位能力。零样本检测里通常还会看PROPer-Region Overlap分数它按每个连通缺陷区域单独算重叠率再求平均能避免大缺陷区域主导指标的问题。以我实测的配置为例AD-DINOv3在多数类别上图像级AUROC能到0.95以上像素级AUROC在0.92到0.98之间。纹理类比如地毯、皮革、瓷砖缺陷本质是局部纹理断裂DINO特征对这类异常特别敏感像素级定位效果属于15个类别里最好的梯队。物体类效果分化比较大瓶子和药片这类形状规整、背景简单的物体效果很好而螺丝、晶体管这类本身带复杂结构的物体容易出现背景结构干扰需要引入注意力机制或者多尺度融合才能把指标提上去。特征层组合对结果的影响非常大。我在调参时发现只用最后一层特征语义信息丰富但空间分辨率不足容易漏掉细长划痕融合倒数第二层和倒数第三层特征之后细纹缺陷的定位效果明显变好。AD-DINOv3在工程实现里我推荐做一次浅层特征和深层特征的简单拼接再用PCA把拼接后的特征降到768维左右比只用单层特征整体高出大约1到3个点的像素级AUROC这个收益非常值得。4. 实测中的坑与调优心得4.1 显存和特征库内存的优化第一个遇到的坑就是显存。DINOv3的特征维度比较高base模型每张448x448的图会产出1024个patch每个patch的特征维度在768到1024之间算下来一张图光特征矩阵就是百万级的浮点数。如果batch size开大很容易把显存撑爆。实测下来12GB显存建议推理batch size控制在4到8之间再大就可能卡在显存瓶颈上。如果还不行用FP16推理能再省一半显存精度损失在这个任务上基本可以忽略。特征库本身的内存也需要注意。一种常见的做法是把所有正常图的patch特征堆在一个矩阵里当正常图数量过百、分辨率又拉大时特征库轻松突破几个GB内存。优化手段是采样加归一化把特征库控制在可接受范围。我的偏好是均匀随机采样在保留正常分布多样性的同时把内存压下来。还有一种更讲究的做法叫coreset采样通过贪心算法选出一批最具代表性的特征能在特征库更小的情况下保持相近的检测精度只是实现复杂度会高一些。4.2 分辨率选择与细节损失的平衡分辨率这个问题很隐形但影响很大。MVTec AD各类别的原始图尺寸不太一样有的七八百像素宽有的一千多。如果你把图直接resize到224去推理速度是快了但很多细小的划痕在patch里已经被抹没了后面分数再准也白搭。我的实用建议是三档。224x224适合粗筛快速排除绝对正常的图像448x448是默认档均衡性最好适合绝大多数场景如果你专门检测微小缺陷比如金属表面的针孔那就上到672甚至896分辨率。每档之间的像素级AUROC差距大约在2到5个点之间但不只是分辨率一个变量的影响还有显存和检索耗时的现实限制所以一定要到自己的机器上实际跑一轮再定。还有一个小技巧推理和训练分辨率要一致。Transformer的patch机制支持任意分辨率输入不需要像CNN那样严格固定尺寸但不同尺寸会产生不同数量的patch。推理时如果你混着不同分辨率的图进同一个batch输出token数量对不上代码会直接报错。所以预处理阶段必须统一resize逻辑保持一致。4.3 阈值设定从论文指标到产线落地论文里报AUCAUC是聚合指标不代表产线上能直接用。真正落地的时候你需要选出一个固定阈值超过它就报警。阈值选太低误报一大堆现场工人直接给你把设备关了阈值选太高缺陷漏过去质检形同虚设。我建议分三步走。第一步收集一段时间内确认正常的样本用这套方案全走一遍把它们的异常分数分布统计出来。第二步根据你能接受的误报率取正常样本分数分布的高分位数作为初始阈值比如p99或p99.5意思是正常情况下每1000张图最多误报一到五张。第三步上线之后持续收集误报样本动态校准阈值。说句经验之谈很多项目不是模型精度不够而是没有完整的阈值管理机制模型在实验室里指标再高到了产线上也会被阈值问题拖垮。另外产线上的缺陷往往有时间连续性。同一批次的缺陷可能会出现连续几张图而单张的误报通常是孤立的点。如果系统支持帧间串联判断可以加一个简单的时序滤波连续两张以上的异常帧才报警误报率能再降一个量级。4.4 几个值得尝试的调优方向如果你照着上面的流程复现完想进一步把效果往上顶我列几个自己试过且有效果的方向。第一个是特征层融合。前面提过浅层加深层特征拼接后PCA降维这是改动最小、收益最高的一项建议优先试。第二个是加入CLS token的全局特征把整图级的全局异常分数和patch级局部分数做加权融合能有效改善轮廓型缺陷漏检的问题。第三个是多尺度检测同一张图用不同分辨率分别推理把多张热力图对齐后取最大或加权平均对大小差异很大的缺陷集特别有效代价是推理时间成倍上涨。还有一个属于工程上的巧劲。如果某个类别始终存在较高的误报可以把特征库从纯正常样本扩展到包含一部分人工确认过的“正常但有干扰”样本比如反光、油污、背景干扰但实际不影响功能的情况。把这些干扰样本的特征也塞进特征库里最近邻检索时它们会和正常样本一起成为参考点误报能明显压下来。这个做法严格意义上已经不算纯零样本了但对实际产线运维来说非常实用。最后说点题外话。零样本缺陷检测这两年热起来本质上是因为自监督预训练模型的视觉通用表示能力终于强到了可以直接拿来当检测器用的程度。AD-DINOv3这套方案你可以把它当成一个通用异常检测基线任何新的质检项目都能在几分钟内先跑出一个效果尚可的版本后面再根据实际需求去换模块、调参数。我在项目里更习惯的做法是先用它做全量筛查把可疑区域框出来再由人工复核这样既保证了检出率又把人力成本压了下来。这套流程在MVTec AD上的完整复现步骤都在上面了希望你能少踩几个我已经躺过的大坑。
返回列表