基于DINOv2实现特征匹配异常检测
一、作者介绍作者李逸超西安工程大学电子信息学院2025级研究生张宏伟人工智能课题组研究方向机器视觉与人工智能联系邮箱2317314922qq.com二、AnomalyDINO核心原理2.1工业图像异常检测概述异常检测AnomalyDetection在工业视觉检查领域旨在识别出严重偏离正常数据分布的样本。传统的全样本Full-shot异常检测方法依赖大量正常样本来训练分类器或生成模型。然而工业生产中往往难以低成本地获取海量数据。基于少样本Few-shot的异常检测能在仅提供极少量正常参考图的情况下实现高精度预测极大满足了工业界对快速部署的需求。该任务属于典型的无监督学习范畴无需成对的缺陷图像进行预训练主要依靠对正常样本特征分布的精确理解来定位如划痕、污染、结构缺失等底层视觉异常。2.2AnomalyDINO概述AnomalyDINO是一种专注于工业视觉异常检测的纯视觉架构模型。它遵循经典的补丁级深度最近邻Patch-leveldeepnearestneighbor范式。相较于依赖文本提示的多模态模型该方法凭借DINOv2提取的高质量视觉特征在无需任何微调Fine-tuning和元学习Meta-learning的“免训练”设定下即可实现精准的图像级异常预测与像素级异常分割。它在MVTecAD等权威数据集上达到了业界领先的少样本检测水平因其极低的部署开销和出色的鲁棒性成为工业检测场景的主流方案之一。2.3DINOv2特征提取网络AnomalyDINO的核心骨干网络利用了DINOv2强大的特征表示能力。DINOv2是一个基于视觉TransformerViT的自监督学习框架。其特征提取主要包含以下机制图1 DINOv2模型框架双分支输入与裁剪模型通过让网络“自己认识自己”来学习特征。原图经过全局裁剪GlobalCrops输入给教师网络TeacherViT保留完整的上下文信息经过局部裁剪LocalCrops后输入给学生网络StudentViT迫使模型从局部细节预测全局特征使其对细微纹理变化极其敏感。特征输出CLSPatchTokens网络输出包含代表全局语义的CLSToken以及密集局部特征的PatchTokens。在像素级少样本任务中正是这些高质量的PatchTokens构成了异常检测的特征比对库。联合损失函数DINOiBOT使用DINOLoss对比师生网络对全局理解的交叉熵差异同时引入iBOTLoss强制学生网络还原被掩码Masking区域的局部特征大幅提升了密集预测与细粒度检测能力。不对称参数更新学生网络通过反向传播计算梯度更新参数而教师网络不参与梯度回传仅通过指数移动平均EMA从学生网络的历史参数中平滑拷贝确保教师模型始终作为稳定的特征提取器。2.4自适应前景分离与掩膜AdaptiveMasking工业图像往往背景复杂直接进行全局特征对比会引入大量背景噪声导致误报。AnomalyDINO利用DINOv2强大的零样本注意力机制实现了无监督的前景分割降维提取主成分巧妙利用PCA算法仅提取图像特征的第一主成分FirstPC。第一主成分的值通常能完美区分前景与背景通过阈值操作即可得到初始二值掩膜。自适应反转AdaptiveMasking为防止PCA方向反转导致背景被错误抓取代码加入了自校验逻辑。当图像中心区域的前景像素占比低于35%时算法会自动反转符号确保在各种工业数据集上始终精准剥离背景。形态学后处理结合OpenCV传统的膨胀Dilate和闭运算MORPH_CLOSE操作平滑掩膜边缘并填补前景内部的细小空洞保证提取的特征纯粹且完整。2.5正常特征内存库构建MemoryBank这是少样本检测的核心机制。在构建内存库时模型会遍历少量的正常参考样本执行精准过滤与高效拼接特征提纯依托前一步生成的自适应掩膜代码仅保留前景物体的有效Patch特征。通过np.concatenate将这些提纯后的特征打平并拼接成一个巨大的特征矩阵。这既缩减了显存占用又排除了背景噪声对正常特征分布的干扰。引入FAISS构建高维索引为解决庞大密集纹理特征的检索瓶颈算法引入了Meta开源的FAISS向量检索库。通过faiss.GpuIndexFlatL2在GPU端直接申请计算资源并在存入特征前执行L2归一化normalize_L2为后续计算余弦相似度做好了底层准备。图2 模型框架图2.6距离度量与异常分数计算在推理阶段底层特征的距离如何科学地转化为最终的异常评估分数是关键度量转换在调用检索功能时深度高维特征的绝对模长易受光照或对比度影响而其“方向”更能代表本质的纹理语义。因此利用归一化后的L2距离在数学上等价转化为余弦距离CosineDistance极大提高了复杂特征匹配的鲁棒性。二维空间重构将检索出的一维距离数组重新映射回原图的空间位置。初始化一个与原始掩膜等大的全零矩阵精准填回距离后reshape为二维异常热力图AnomalyMap使得背景区域被安全置零彻底杜绝虚假报警。长尾聚合评分机制为了将像素级的距离转化为单一的图像级异常分数并非采用简单的最大值或平均值而是采用聚合统计量进行评估。通过提取距离分布中Top1%的均值作为最终异常分数$mean(H_{0.01}(\mathcal{D}))$。这种统计学上的截断处理既能捕捉到最强的异常信号又对单一点的离群噪声具有出色的鲁棒性。三、数据集介绍与预处理3.1数据集构成MVTecADMVTecAnomalyDetection数据集是工业视觉异常检测领域最经典、也是被最广泛采用的基准Benchmark数据集。该数据集专门针对真实的工业检测场景设计包含超过5000张高分辨率图像共划分为15个不同的工业产品类别图3 MVTecAD数据集官网图4 MVTecAD数据集内容5类纹理Textures包含地毯Carpet、网格Grid、皮革Leather、瓷砖Tile、木材Wood。这些类别表面呈周期性或复杂的密集纹理拓扑要求模型具备强大的全局纹理一致性建模能力。10类物体Objects包含瓶子Bottle、电缆Cable、胶囊Capsule、榛子Hazelnut、金属螺母MetalNut、药丸Pill、螺丝Screw、牙刷Toothbrush、晶体管Transistor、拉链Zipper。物体类别通常形变较大需要模型重点关注局部结构、对齐方式和逻辑组件的完整性。3.2训练与测试范式为了严谨模拟工业实际生产中缺陷样本极度稀缺的真实环境数据集完全遵循无监督异常检测UnsupervisedAnomalyDetection的任务范式进行构建训练集仅包含绝对无缺陷的正常Good图像。在少样本Few-shot设定下算法仅被允许从中抽取1张或4张样本的高维特征来构建正常基准特征分布。测试集同时包含完全正常的图像以及带有各类真实工业缺陷的异常图像。双层评估体系数据集提供了极其精细的像素级GroundTruth掩码Masks。这使得评估体系不仅包含用于判断整张图片是否存在异常的图像级指标ROC-AUC、F1-max还引入了像素级定位指标Pixel-F1、Pixel-AUPRO。其中区域重叠均值PRO指标能够公平地对待不同大小的缺陷连通域准确反映模型对局部微小疵点的覆盖和定位能力。3.3数据处理与增强由于工业图像的成像环境、目标物体的摆放位置与开源基础模型在自然图像上的预训练分布存在较大差异必须通过精细的少样本预处理流水线对数据进行提纯与增强尺寸统一与自适应多分辨率为了兼顾推理低延迟与密集预测的精度输入图像在送入骨干网络前通常被统一缩放并裁剪至448×448或672×672像素分辨率须为DINOv2补丁步长14的整数倍。实验表明更高的分辨率能够显著增强小面积缺陷的像素级定位效果。零样本前景掩膜生成Masking对于10类具体物体由于其背景通常包含复杂的传送带、金属托盘等结构直接比对会引入大量的工业环境噪声。通过阈值化DINOv2特征的第一主成分FirstPC生成无监督掩膜并利用OpenCV的膨胀Dilate与闭运算MORPH_CLOSE进行平滑和填补空洞从而在不借助任何人工标注的情况下完美剥离背景。对于5类密集纹理则默认不执行掩膜操作。少样本旋转增强Rotation在少样本场景下测试样本在生产线上的旋转角度变化往往大于极少数的参考样本。例如在‘螺丝Screw’类别的1-shot设定下通过对唯一的正常参考图进行多角度旋转增强特征匹配的图像级AUROC能够从65.6%大幅跃升至89.2%。预处理模式划分不可知模式Agnostic默认策略。在完全不知道后续测试样本是否存在旋转或形变规律时默认对所有图像应用旋转增强与自适应掩膜。已知模式Informed在受控的工业自动化场景下或测试图已在线对齐可以关闭旋转增强仅保留前景掩膜提取。这样既能缩减内存库的构建开销又能有效防止将‘晶体管错位misplaced’等涉及角度的语义异常误判为正常特征。3.4数据集目录结构项目内的数据集及特征路径严格遵循标准的工业视觉基准拓扑其目录层级结构如下图5 数据集目录结构图四、免训练算法流程由于AnomalyDINO采用免训练Training-Free的深度最近邻范式模型不需要像传统GAN网络那样进行复杂的对抗训练和参数微调。其核心流程分为两个阶段一是通过少量正常样本构建高维特征内存库的“提纯阶段”二是对待测样本进行检索比对的“推理阶段”。4.1内存库构建流程加载参考样本读取极少量的绝对无缺陷正常样本。执行数据增强可选在不可知Agnostic模式下对输入的正常样本进行多角度旋转增强以扩充正常样本在特征空间中的多视角分布。密集特征提取将增强后的图像送入预训练的DINOv2骨干网络密集提取出每个贴片Patch的高维Token特征表示。自适应前景剥离利用第一主成分FirstPC生成无监督掩膜通过自适应校验检查前景占比并结合OpenCV膨胀与闭运算剔除背景贴片的干扰。特征矩阵拼接将通过掩膜筛选后的有效前景Patch特征进行打平并使用np.concatenate拼接成一个统一的高维正常特征矩阵。矩阵归一化与存入索引对拼接后的特征矩阵进行L2归一化处理normalize_L2随后将其直接添加至GPU加速的FAISS向量检索库中如faiss.GpuIndexFlatL2完成特征数据库M的高效构建。4.2推理与缺陷检测流程读取待测样本输入一张待检测的测试工业图像。分辨率适配预处理将测试样本缩放并裁剪至骨干网络指定分辨率确保其边长为DINOv2步长14的整数倍。计算测试特征将预处理后的测试样本送入DINOv2网络中获取测试图像贴片的局部高维特征向量。高维最近邻检索通过FAISS检索库利用GPU并行计算测试图像中每个贴片特征与内存库M中所有正常特征贴片之间的最近邻L2距离并在数学上严格转化为余弦距离。像素级异常图重构将检索出的一维距离数组精准填回原始掩膜对应的空间位置将背景区域安全置零再经过双线性插值上采样与高斯平滑生成精确的像素级二维异常热力图AnomalyMap。图像级聚合评分对贴片的距离分布进行统计学上的“长尾截断”处理提取距离分布中Top1%最高值的均值作为该张图片的整图异常分数平衡局部缺陷信号的敏感度与单一噪点的鲁棒性输出最终的工业检测结果。图6 结果展示五、代码实现1、主程序代码import argparse import os from argparse import ArgumentParser, Action import yaml from tqdm import trange import csv from src.utils import get_dataset_info from src.detection import run_anomaly_detection from src.post_eval import eval_finished_run from src.visualize import create_sample_plots from src.backbones import get_model class IntListAction(Action): Define a custom action to always return a list. This allows --shots 1 to be treated as a list of one element [1]. def __call__(self, namespace, values): if not isinstance(values, list): values [values] setattr(namespace, self.dest, values) def parse_args(): parser ArgumentParser() parser.add_argument(--dataset, typestr, defaultMVTec) parser.add_argument(--model_name, typestr, defaultdinov2_vits14, helpName of the backbone model. Choose from [dinov2_vits14, dinov2_vitb14, dinov2_vitl14, dinov2_vitg14, vit_b_16].) parser.add_argument(--data_root, typestr, defaultdata/mvtec_anomaly_detection, helpPath to the root directory of the dataset.) parser.add_argument(--preprocess, typestr, defaultagnostic, helpPreprocessing method. Choose from [agnostic, informed, masking_only].) parser.add_argument(--resolution, typeint, default448) parser.add_argument(--knn_metric, typestr, defaultL2_normalized) parser.add_argument(--k_neighbors, typeint, default1) parser.add_argument(--faiss_on_cpu, defaultFalse, actionargparse.BooleanOptionalAction, helpUse GPU for FAISS kNN search. (Conda install faiss-gpu recommended, does usually not work with pip install.)) parser.add_argument(--shots, nargs, typeint, default[1], #actionIntListAction, helpList of shots to evaluate. Full-shot scenario is -1.) parser.add_argument(--num_seeds, typeint, default1) parser.add_argument(--mask_ref_images, defaultFalse) parser.add_argument(--just_seed, typeint, defaultNone) parser.add_argument(--save_examples, defaultTrue, actionargparse.BooleanOptionalAction, helpSave example plots.) parser.add_argument(--eval_clf, defaultTrue, actionargparse.BooleanOptionalAction, helpEvaluate anomaly detection performance.) parser.add_argument(--eval_segm, defaultFalse, actionargparse.BooleanOptionalAction, helpEvaluate anomaly segmentation performance.) parser.add_argument(--device, defaultcuda:0) parser.add_argument(--warmup_iters, typeint, default25, helpNumber of warmup iterations, relevant when benchmarking inference time.) #parser.add_argument(--tag, helpOptional tag for the saving directory.) # tag: 给保存结果的文件夹加一个自定义的后缀标签 parser.add_argument(--tag, typestr, defaulttest, helpOptional tag for the saving directory.) args parser.parse_args() return args if __name____main__: args parse_args() print(fRequested to run {len(args.shots)} (different) shot(s):, args.shots) print(fRequested to repeat the experiments {args.num_seeds} time(s).) objects, object_anomalies, masking_default, rotation_default get_dataset_info(args.dataset, args.preprocess) # set CUDA device os.environ[CUDA_VISIBLE_DEVICES] str(args.device[-1]) model get_model(args.model_name, cuda, smaller_edge_sizeargs.resolution) if not args.model_name.startswith(dinov2): masking_default {o: False for o in objects} print(Caution: Only DINOv2 supports 0-shot masking (for now)!) if args.just_seed ! None: seeds [args.just_seed] else: seeds range(args.num_seeds) for shot in list(args.shots): save_examples args.save_examples results_dir fresults_{args.dataset}/{args.model_name}_{args.resolution}/{shot}-shot_preprocess{args.preprocess} if args.tag ! None: results_dir _ args.tag plots_dir results_dir os.makedirs(f{results_dir}, exist_okTrue) # save preprocessing setups (masking and rotation) to file with open(f{results_dir}/preprocess.yaml, w) as f: yaml.dump({masking: masking_default, rotation: rotation_default}, f) # save arguments to file with open(f{results_dir}/args.yaml, w) as f: yaml.dump(vars(args), f) if args.faiss_on_cpu: print(Warning: Running similarity search on CPU. Consider using faiss-gpu for faster inference.) print(Results will be saved to, results_dir) for seed in seeds: print(f Shot {shot}, Seed {seed} ) if os.path.exists(f{results_dir}/metrics_seed{seed}.json): print(fResults for shot {shot}, seed {seed} already exist. Skipping.) continue else: timeit_file results_dir /time_measurements.csv with open(timeit_file, w, newline) as file: writer csv.writer(file) writer.writerow([Object, Sample, Anomaly_Score, MemoryBank_Time, Inference_Time]) for object_name in objects: if save_examples: os.makedirs(f{plots_dir}/{object_name}, exist_okTrue) os.makedirs(f{plots_dir}/{object_name}/examples, exist_okTrue) # CUDA warmup for _ in trange(args.warmup_iters, descCUDA warmup, leaveFalse): first_image os.listdir(f{args.data_root}/{object_name}/train/good)[0] img_tensor, grid_size model.prepare_image(f{args.data_root}/{object_name}/train/good/{first_image}) features model.extract_features(img_tensor) anomaly_scores, time_memorybank, time_inference run_anomaly_detection( model, object_name, data_root args.data_root, n_ref_samples shot, object_anomalies object_anomalies, plots_dir plots_dir, save_examples save_examples, knn_metric args.knn_metric, knn_neighbors args.k_neighbors, faiss_on_cpu args.faiss_on_cpu, masking masking_default[object_name], mask_ref_images args.mask_ref_images, rotation rotation_default[object_name], seed seed, save_patch_dists args.eval_clf, # save patch distances for detection evaluation save_tiffs args.eval_segm) # save anomaly maps as tiffs for segmentation evaluation # write anomaly scores and inference times to file for counter, sample in enumerate(anomaly_scores.keys()): anomaly_score anomaly_scores[sample] inference_time time_inference[sample] writer.writerow([object_name, sample, f{anomaly_score:.5f}, f{time_memorybank:.5f}, f{inference_time:.5f}]) # print(fMean inference time ({object_name}): {sum(time_inference.values())/len(time_inference):.5f} s/sample) # read inference times from file with open(timeit_file, r) as file: reader csv.reader(file) next(reader) inference_times [float(row[4]) for row in reader] print(fFinished AD for {len(objects)} objects (seed {seed}), mean inference time: {sum(inference_times)/len(inference_times):.5f} s/sample) # evaluate all finished runs and create sample anomaly maps for inspection print(f Evaluate seed {seed} ) eval_finished_run(args.dataset, args.data_root, anomaly_maps_dir results_dir f/anomaly_maps/seed{seed}, output_dir results_dir, seed seed, pro_integration_limit 0.3, eval_clf args.eval_clf, eval_segm args.eval_segm) create_sample_plots(results_dir, anomaly_maps_dir results_dir f/anomaly_maps/seed{seed}, seed seed, dataset args.dataset, data_root args.data_root) # deactivate creation of examples for the next seeds... save_examples False print(Finished and evaluated all runs!)2、detection代码import matplotlib.pyplot as plt import os import cv2 import numpy as np from tqdm import tqdm import faiss import tifffile as tiff import time import torch from src.utils import augment_image, dists2map, plot_ref_images from src.post_eval import mean_top1p def run_anomaly_detection( model, object_name, data_root, n_ref_samples, object_anomalies, plots_dir, save_examples False, masking None, mask_ref_images False, rotation False, knn_metric L2_normalized, knn_neighbors 1, faiss_on_cpu False, seed 0, save_patch_dists True, save_tiffs False): Main function to evaluate the anomaly detection performance of a given object/product. Parameters: - model: The backbone model for feature extraction (and, in case of DINOv2, masking). - object_name: The name of the object/product to evaluate. - data_root: The root directory of the dataset. - n_ref_samples: The number of reference samples to use for evaluation (k-shot). Set to -1 for full-shot setting. - object_anomalies: The anomaly types for each object/product. - plots_dir: The directory to save the example plots. - save_examples: Whether to save example images and plots. Default is True. - masking: Whether to apply DINOv2 to estimate the foreground mask (and discard background patches). - rotation: Whether to augment reference samples with rotation. - knn_metric: The metric to use for kNN search. Default is L2_normalized (1 - cosine similarity) - knn_neighbors: The number of nearest neighbors to consider. Default is 1. - seed: The seed value for deterministic sampling in few-shot setting. Default is 0. - save_patch_dists: Whether to save the patch distances. Default is True. Required to eval detection. - save_tiffs: Whether to save the anomaly maps as TIFF files. Default is False. Required to eval segmentation. assert knn_metric in [L2, L2_normalized] # add good to the anomaly types type_anomalies object_anomalies[object_name] type_anomalies.append(good) # ensure that each type is only evaluated once type_anomalies list(set(type_anomalies)) # Extract reference features features_ref [] images_ref [] masks_ref [] vis_backgroud [] img_ref_folder f{data_root}/{object_name}/train/good/ if n_ref_samples -1: # full-shot setting img_ref_samples sorted(os.listdir(img_ref_folder)) else: # few-shot setting, pick samples in deterministic fashion according to seed img_ref_samples sorted(os.listdir(img_ref_folder))[seed*n_ref_samples:(seed 1)*n_ref_samples] if len(img_ref_samples) n_ref_samples: print(fWarning: Not enough reference samples for {object_name}! Only {len(img_ref_samples)} samples available.) with torch.inference_mode(): # start measuring time (feature extraction/memory bank set up) start_time time.time() for img_ref_n in tqdm(img_ref_samples, descBuilding memory bank, leaveFalse): # load reference image... img_ref f{img_ref_folder}{img_ref_n} image_ref cv2.cvtColor(cv2.imread(img_ref, cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB) # augment reference image (if applicable)... if rotation: img_augmented augment_image(image_ref) else: img_augmented [image_ref] for i in range(len(img_augmented)): image_ref img_augmented[i] image_ref_tensor, grid_size1 model.prepare_image(image_ref) features_ref_i model.extract_features(image_ref_tensor) # compute background mask and discard background patches mask_ref model.compute_background_mask(features_ref_i, grid_size1, threshold10, masking_type(mask_ref_images and masking)) features_ref.append(features_ref_i[mask_ref]) if save_examples: images_ref.append(image_ref) vis_image_background model.get_embedding_visualization(features_ref_i, grid_size1, mask_ref) masks_ref.append(mask_ref) vis_backgroud.append(vis_image_background) features_ref np.concatenate(features_ref, axis0).astype(float32) if faiss_on_cpu: # similariy search on CPU knn_index faiss.IndexFlatL2(features_ref.shape[1]) else: # similariy search on GPU res faiss.StandardGpuResources() knn_index faiss.GpuIndexFlatL2(res, features_ref.shape[1]) # knn_index faiss.IndexFlatL2(features_ref.shape[1]) # knn_index faiss.index_cpu_to_gpu(res, int(model.device[-1]), knn_index) if knn_metric L2_normalized: faiss.normalize_L2(features_ref) knn_index.add(features_ref) # end measuring time (for memory bank set up; in seconds, same for all test samples of this object) time_memorybank time.time() - start_time # plot some reference samples for inspection if save_examples: plots_dir_ f{plots_dir}/{object_name}/ plot_ref_images(images_ref, masks_ref, vis_backgroud, grid_size1, plots_dir_, title Reference Images, img_names img_ref_samples) inference_times {} anomaly_scores {} idx 0 # Evaluate anomalies for each anomaly type (and good) for type_anomaly in tqdm(type_anomalies, desc fprocessing test samples ({object_name})): data_dir f{data_root}/{object_name}/test/{type_anomaly} if save_patch_dists or save_tiffs: os.makedirs(f{plots_dir}/anomaly_maps/seed{seed}/{object_name}/test/{type_anomaly}, exist_okTrue) for idx, img_test_nr in enumerate(sorted(os.listdir(data_dir))): # start measuring time (inference) start_time time.time() image_test_path f{data_dir}/{img_test_nr} # Extract test features image_test cv2.cvtColor(cv2.imread(image_test_path, cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB) image_tensor2, grid_size2 model.prepare_image(image_test) features2 model.extract_features(image_tensor2) # Compute background mask if masking: mask2 model.compute_background_mask(features2, grid_size2, threshold10, masking_typemasking) else: mask2 np.ones(features2.shape[0], dtypebool) if save_examples and idx 3: vis_image_test_background model.get_embedding_visualization(features2, grid_size2, mask2) # Discard irrelevant features features2 features2[mask2] # Compute distances to nearest neighbors in M if knn_metric L2: distances, match2to1 knn_index.search(features2, k knn_neighbors) if knn_neighbors 1: distances distances.mean(axis1) distances np.sqrt(distances) elif knn_metric L2_normalized: faiss.normalize_L2(features2) distances, match2to1 knn_index.search(features2, k knn_neighbors) if knn_neighbors 1: distances distances.mean(axis1) distances distances / 2 # equivalent to cosine distance (1 - cosine similarity) output_distances np.zeros_like(mask2, dtypefloat) output_distances[mask2] distances.squeeze() d_masked output_distances.reshape(grid_size2) # save inference time torch.cuda.synchronize() # Synchronize CUDA kernels before measuring time inf_time time.time() - start_time inference_times[f{type_anomaly}/{img_test_nr}] inf_time anomaly_scores[f{type_anomaly}/{img_test_nr}] mean_top1p(output_distances.flatten()) # Save the anomaly maps (raw as .npy or full resolution .tiff files) img_test_nr img_test_nr.split(.)[0] if save_tiffs: anomaly_map dists2map(d_masked, image_test.shape) tiff.imwrite(f{plots_dir}/anomaly_maps/seed{seed}/{object_name}/test/{type_anomaly}/{img_test_nr}.tiff, anomaly_map) if save_patch_dists: np.save(f{plots_dir}/anomaly_maps/seed{seed}/{object_name}/test/{type_anomaly}/{img_test_nr}.npy, d_masked) # Save some example plots (3 per anomaly type) if save_examples and idx 3: fig, (ax1, ax2, ax3, ax4,) plt.subplots(1, 4, figsize(18, 4.5)) # plot test image, PCA mask ax1.imshow(image_test) ax2.imshow(vis_image_test_background) # plot patch distances d_masked[~mask2.reshape(grid_size2)] 0.0 plt.colorbar(ax3.imshow(d_masked), axax3, fraction0.12, pad0.05, orientationhorizontal) # compute image level anomaly score (mean(top 1%) of patches empirical tail value at risk for quantile 0.99) score_top1p mean_top1p(distances) ax4.axvline(score_top1p, colorr, linestyledashed, linewidth1, labelround(score_top1p, 2)) ax4.legend() ax4.hist(distances.flatten())