当前位置:   article > 正文

PaDiM 无监督异常检测和定位-论文和源码阅读

padim

目录

1. 论文

1.1 检测效果

1.2 优缺点

1.3 框架 

1.2.1 特征提取embedding extraction

1.2.2 正样本学习Learning of the normality

1.2.3 计算异常图 inference: computation of the anomaly map

2. 源码

2.1 dataset

2.2 model

2.3 提取特征

2.4 infer


1. 论文

https://arxiv.org/abs/2011.08785

思路:数据特征的分布被假定为一个多元高斯分布,异常值通常在多元高斯分布中表现为远离数据集的中心(均值向量)的数据点。协方差矩阵可以描述各个特征之间的相关性和离散程度。通过计算数据点相对于协方差矩阵的马氏距离,可以识别潜在的异常点。 

1.1 检测效果

 

SPADE精度不错,但是推理会非常慢,因为算法是基于KNN,测试的时候要访问整个训练正常数据集的embedding vectors。

PaDiM backbone是resnet18,然后随机选100个特征,和backbone是Wide ResNet-50-2 (WR50) 然后随机选550个特征,可以看到后者的精度更高。

1.2 优缺点

优点:(1)不需要训练过程,输入无缺陷数据,直接使用resnet或者efficient等预训练网络的特征提取层前三层,进行特征,拼接在一起对无缺陷数据建模,速度比起训练快了很多很多;(2)建模过程只需要无缺陷数据;

缺点:(1)由于是使用预训练网络,所以图像需要缩放到对应网络的输入尺度下,才能使用预训练权重,比如efficient和resnet官方预训练网络的输入都是224,则对于小缺陷,缩放到224会损失精度;(2)误判问题,训练的无缺陷数据需要足够多样,否则测试图像稍微和建模的数据不同则会误判(3)阈值问题,测试时会计算测试特征和建模特征之间的距离,然后设定一个阈值,超过阈值则为缺陷,但是这个阈值有时很难界定。

1.3 框架 

步骤如下: 

1.2.1 特征提取embedding extraction

将与此图像块相对应的三个激活图区域特征concat在一起,组成一个特征向量(这样向量就包含了多尺度信息)。N张图,就有N个特征向量。组成X_ij特征空间,该过程不需要训练,直接用预训练权重比如resnet18、resnet50;

(1) infer

layer1: batch infer -> (b,256,56,56) ->cat->(img_num,256,56,56)

layer2: batch infer -> (b,512,28,28) ->cat->(img_num,512,28,28)

layer3: batch infer -> (b,1024,14,14) ->cat->(img_num,1024,14,14)

全部cat在一起, 得到embedding_vectors (img_num,1792,56,56).

(2) 随机选择d个特征

embedding_vectors有56*56个特征块,每个块有1792长度的特征,随机选d(550)个特征。

embedding_vectors.shape -> (img_num,550,56,56).

1.2.2 正样本学习Learning of the normality

为了求整个正样本数据分布情况,作者假设数据是符合多元高斯分布,然后求取此分布的参数: 均值mean和协方差covariance。

(1)均值:特征均值是某一特征(或变量)在数据集中的平均值。它是所有样本中某一特征的数值之和除以样本数量。特征均值用来表示数据在这一特征上的中心趋势。

  1. B, C, H, W = embedding_vectors.size()
  2. embedding_vectors = embedding_vectors.view(B, C, H * W) # (img_num,550,56,56) -> (img_num,550,3136). 3136块
  3. mean = torch.mean(embedding_vectors, dim=0).numpy() # (550,3136). 整个数据集的均值

(2)协方差:协方差反映了多个变量之间的相关性以及它们与各自均值的偏离程度,如果一个变量的值在均值附近波动较大(方差大),那么它对协方差的贡献也会较大。

(3)协方差矩阵:有p个特征,协方差矩阵是一个p x p的矩阵,其中每个元素(i, j)表示特征i和特征j之间的协方差。协方差矩阵通常用Σ来表示。 协方差矩阵的对角线上的元素是各个特征的方差(方差是协方差自己和自己的情况),而非对角线上的元素是不同特征之间的协方差。协方差矩阵计算公式如下。

x_{ij}^k是第k个图像的图像块ij的特征向量(向量长度是550,所以得到的协方差矩阵大小是(550,550)), u_{ij}是整个正样本数据集的图像块ij位置上的特征均值向量()。\epsilon I是正常项。N是数据集中正常图像个数。每个块都有自己的协方差矩阵。

1.2.3 计算异常图 inference: computation of the anomaly map

基于前面求得的正常样本均值mean和协方差矩阵covariance的逆,再求测试图像块特征的马氏距离,此距离作为每个块的异常分数。

其中x_{ij}是测试图像块的patch embedding. μ和∑是前面正常样本求得的均值和协方差。T是转置,-1是求逆。

2. 源码

https://github.com/xiahaifeng1995/PaDiM-Anomaly-Detection-Localization-master

2.1 dataset

没有数据增强,只有个中心裁剪,因为作者提出的方法没有训练过程,直接使用预训练模型,提取激活层的特征向量。

  1. import os
  2. # import tarfile
  3. from PIL import Image
  4. from tqdm import tqdm
  5. # import urllib.request
  6. import torch
  7. from torch.utils.data import Dataset
  8. from torchvision import transforms as T
  9. # URL = 'ftp://guest:GU.205dldo@ftp.softronics.ch/mvtec_anomaly_detection/mvtec_anomaly_detection.tar.xz'
  10. CLASS_NAMES = ['bottle', 'cable', 'capsule', 'carpet', 'grid',
  11. 'hazelnut', 'leather', 'metal_nut', 'pill', 'screw',
  12. 'tile', 'toothbrush', 'transistor', 'wood', 'zipper']
  13. # CLASS_NAMES = ['len_base']
  14. class MVTecDataset(Dataset):
  15. def __init__(self, dataset_path='D:/dataset/mvtec_anomaly_detection', class_name='bottle', is_train=True,
  16. resize=256, cropsize=224):
  17. assert class_name in CLASS_NAMES, 'class_name: {}, should be in {}'.format(class_name, CLASS_NAMES)
  18. self.dataset_path = dataset_path
  19. self.class_name = class_name # 'bottle', 'cable' or 'capsule' et al.
  20. self.is_train = is_train # bool.
  21. self.resize = resize # 256
  22. self.cropsize = cropsize # 224 CenterCrop
  23. # load dataset: x:整个数据集的图片路径,对应的label(0/1),对应的缺陷mask图像路径(good类别使用None)。
  24. self.x, self.y, self.mask = self.load_dataset_folder()
  25. # set transforms
  26. self.transform_x = T.Compose([T.Resize(resize, Image.ANTIALIAS),
  27. T.CenterCrop(cropsize),
  28. T.ToTensor(),
  29. T.Normalize(mean=[0.485, 0.456, 0.406],
  30. std=[0.229, 0.224, 0.225])])
  31. self.transform_mask = T.Compose([T.Resize(resize, Image.NEAREST), # 注意mask
  32. T.CenterCrop(cropsize),
  33. T.ToTensor()])
  34. def __getitem__(self, idx):
  35. x, y, mask = self.x[idx], self.y[idx], self.mask[idx] # 获取路径
  36. x = Image.open(x).convert('RGB')
  37. x = self.transform_x(x)
  38. if y == 0: # good图像的mask路径是None,所以这里生成全0缺陷mask
  39. mask = torch.zeros([1, self.cropsize, self.cropsize])
  40. else:
  41. mask = Image.open(mask)
  42. mask = self.transform_mask(mask)
  43. return x, y, mask
  44. def __len__(self):
  45. return len(self.x)
  46. def load_dataset_folder(self):
  47. phase = 'train' if self.is_train else 'test'
  48. x, y, mask = [], [], [] # x:整个数据集的图片路径,对应的label(0/1),对应的缺陷mask图像路径(good类别使用None)。
  49. img_dir = os.path.join(self.dataset_path, self.class_name, phase)
  50. gt_dir = os.path.join(self.dataset_path, self.class_name, 'ground_truth')
  51. img_types = sorted(os.listdir(img_dir)) # train文件夹下只有good文件夹
  52. for img_type in img_types: # 遍历不同的文件夹,比如train目录下只有good文件夹。test目录下有各种缺陷文件夹。
  53. # load images
  54. img_type_dir = os.path.join(img_dir, img_type)
  55. if not os.path.isdir(img_type_dir):
  56. continue
  57. img_fpath_list = sorted([os.path.join(img_type_dir, f)
  58. for f in os.listdir(img_type_dir)
  59. if f.endswith('.png')])
  60. x.extend(img_fpath_list)
  61. # load gt labels。二分类问题:good or not.
  62. if img_type == 'good':
  63. y.extend([0] * len(img_fpath_list)) # 如果是good类别,则当前所有图像label都0
  64. mask.extend([None] * len(img_fpath_list)) # good类别没有缺陷mask
  65. else:
  66. y.extend([1] * len(img_fpath_list)) # 不是good,则当前所有图像label都是1
  67. gt_type_dir = os.path.join(gt_dir, img_type)
  68. img_fname_list = [os.path.splitext(os.path.basename(f))[0] for f in img_fpath_list]
  69. gt_fpath_list = [os.path.join(gt_type_dir, img_fname + '_mask.png')
  70. for img_fname in img_fname_list]
  71. mask.extend(gt_fpath_list) # 并保存对应的mask图像路径
  72. assert len(x) == len(y), 'number of x and y should be same'
  73. return list(x), list(y), list(mask)

2.2 model

  1. from torchvision.models import wide_resnet50_2, resnet18
  2. def main():
  3. args = parse_args()
  4. # 1, load model
  5. if args.arch == 'resnet18':
  6. model = resnet18(pretrained=True, progress=True)
  7. t_d = 448
  8. d = 100
  9. elif args.arch == 'wide_resnet50_2':
  10. model = wide_resnet50_2(pretrained=True, progress=True)
  11. t_d = 1792 #
  12. d = 550
  13. model.to(device)
  14. model.eval()
  15. # 这一行将Python内置的random模块的随机种子设置为1024。设置种子可以确保在相同种子的情况下,使用该模块进行的任何随机操作都会生成相同的随机数序列。通常用于实现可重现性。
  16. random.seed(1024)
  17. # 这一行将PyTorch的随机数生成器的种子设置为1024。这用于确保在PyTorch内进行的任何随机操作(例如神经网络权重的初始化)在相同种子下生成相同的结果。
  18. torch.manual_seed(1024)
  19. if use_cuda:
  20. # 这一行设置PyTorch的CUDA(GPU)随机数生成器的种子。这将确保在GPU上进行的随机操作在使用种子1024时也能生成可重现的结果。
  21. torch.cuda.manual_seed_all(1024)
  22. idx = torch.tensor(sample(range(0, t_d), d)) # 在范围[0,1792]范围内随机生成550个整数。
  23. # set model's intermediate outputs。 只使用中间3个layer输出,第四个layer输出没有使用
  24. outputs = []
  25. # 提取layer1,layer2,layer3层输出结果。
  26. def hook(module, input, output):
  27. outputs.append(output)
  28. model.layer1[-1].register_forward_hook(hook) # 每次前向都会调用一次hook,然后output会保持到outputs中。
  29. model.layer2[-1].register_forward_hook(hook)
  30. model.layer3[-1].register_forward_hook(hook)

2.3 提取特征

没有训练,则不需要定义loss函数、optimizer等等。直接dataset取数据,infer模型推理,获取中间的三个layer输出结果,然后提取特征向量的均值和协方差。

  1. import random
  2. from random import sample
  3. import argparse
  4. import numpy as np
  5. import os
  6. import pickle
  7. from tqdm import tqdm
  8. from collections import OrderedDict
  9. import matplotlib.pyplot as plt
  10. import torch
  11. from torch.utils.data import DataLoader
  12. from torchvision.models import wide_resnet50_2, resnet18
  13. import datasets.mvtec as mvtec
  14. # device setup
  15. from utils import embedding_concat
  16. use_cuda = torch.cuda.is_available()
  17. device = torch.device('cuda' if use_cuda else 'cpu')
  18. def parse_args():
  19. parser = argparse.ArgumentParser('PaDiM')
  20. parser.add_argument('--data_path', type=str, default='D:/dataset/mvtec_anomaly_detection')
  21. parser.add_argument('--save_path', type=str, default='./mvtec_result')
  22. parser.add_argument('--arch', type=str, choices=['resnet18', 'wide_resnet50_2'], default='wide_resnet50_2')
  23. return parser.parse_args()
  24. def main():
  25. args = parse_args()
  26. # 1, load model
  27. if args.arch == 'resnet18':
  28. model = resnet18(pretrained=True, progress=True)
  29. t_d = 448
  30. d = 100
  31. elif args.arch == 'wide_resnet50_2':
  32. model = wide_resnet50_2(pretrained=True, progress=True)
  33. t_d = 1792 # 三个feature通道数的和256+512+1024. 在范围[0,1792]范围内随机生成550个整数。
  34. d = 550 # 随机选择550个值
  35. model.to(device) # to cuda
  36. model.eval()
  37. # 这一行将Python内置的random模块的随机种子设置为1024。设置种子可以确保在相同种子的情况下,使用该模块进行的任何随机操作都会生成相同的随机数序列。通常用于实现可重现性。
  38. random.seed(1024)
  39. # 这一行将PyTorch的随机数生成器的种子设置为1024。这用于确保在PyTorch内进行的任何随机操作(例如神经网络权重的初始化)在相同种子下生成相同的结果。
  40. torch.manual_seed(1024)
  41. if use_cuda:
  42. # 这一行设置PyTorch的CUDA(GPU)随机数生成器的种子。这将确保在GPU上进行的随机操作在使用种子1024时也能生成可重现的结果。
  43. torch.cuda.manual_seed_all(1024)
  44. idx = torch.tensor(sample(range(0, t_d), d)) # 在范围[0,1792]范围内随机生成550个整数。
  45. # set model's intermediate outputs。 只使用中间3个layer输出,第四个layer输出没有使用
  46. outputs = []
  47. # 提取layer1,layer2,layer3层输出结果。
  48. def hook(module, input, output):
  49. outputs.append(output)
  50. model.layer1[-1].register_forward_hook(hook) # 每次前向都会调用一次hook,然后output会保持到outputs中。
  51. model.layer2[-1].register_forward_hook(hook)
  52. model.layer3[-1].register_forward_hook(hook)
  53. # 创建特征pickle文件保存路径,其实就是存放[mean, cov],每个数据集都有一个pickle文件。
  54. os.makedirs(os.path.join(args.save_path, 'temp_%s' % args.arch), exist_ok=True)
  55. fig, ax = plt.subplots(1, 2, figsize=(20, 10)) # 一行两列fig
  56. for class_name in mvtec.CLASS_NAMES: # 遍历不同的数据集
  57. # 2, dataset
  58. train_dataset = mvtec.MVTecDataset(args.data_path, class_name=class_name, is_train=True)
  59. train_dataloader = DataLoader(train_dataset, batch_size=32, pin_memory=True)
  60. # layer1:所有数据集layer1输出向量,每个向量的shape是[b,256,56,56]
  61. # layer2: 所有数据集layer2输出向量,每个向量的shape是[b,512,28,28]
  62. # layer3: 所有数据集layer3输出向量,每个向量的shape是[b,1024,14,14]
  63. train_outputs = OrderedDict([('layer1', []), ('layer2', []), ('layer3', [])])
  64. # 3, extract train set features (pickle文件保存)
  65. train_feature_filepath = os.path.join(args.save_path, 'temp_%s' % args.arch, 'train_%s.pkl' % class_name)
  66. if not os.path.exists(train_feature_filepath): # 不存在此数据集的特征pickle文件,则进行特征提取
  67. for (x, _, _) in tqdm(train_dataloader, '| feature extraction | train | %s |' % class_name):
  68. # model prediction
  69. with torch.no_grad(): # 没有训练,x(b,32,224,224) to cuda,然后直接 batch infer
  70. _ = model(x.to(device)) # 没有使用返回结果,因为前面定义了hook保存了中间结果到outputs
  71. # get intermediate layer outputs
  72. for k, v in zip(train_outputs.keys(), outputs): # 保存3个cuda tensor到字典中
  73. train_outputs[k].append(v.cpu().detach())
  74. # initialize hook outputs
  75. outputs = [] # 置空,迭代赋值
  76. for k, v in train_outputs.items():
  77. train_outputs[k] = torch.cat(v, 0) # list of tensor(b,256,56,56). -> (img_num,256,56,56)
  78. # Embedding concat
  79. embedding_vectors = train_outputs['layer1']
  80. for layer_name in ['layer2', 'layer3']:
  81. embedding_vectors = embedding_concat(embedding_vectors, train_outputs[layer_name])
  82. # randomly select d dimension
  83. embedding_vectors = torch.index_select(embedding_vectors, 1, idx) # 在范围[0,1792]范围内随机生成550个整数。
  84. # calculate multivariate Gaussian distribution
  85. B, C, H, W = embedding_vectors.size()
  86. embedding_vectors = embedding_vectors.view(B, C, H * W) # (img_num,550,56,56) -> (img_num,550,3136). 3136块
  87. mean = torch.mean(embedding_vectors, dim=0).numpy() # (550,3136). 整个数据集的均值
  88. cov = torch.zeros(C, C, H * W).numpy() # (550,550,3136)协方差矩阵
  89. I = np.identity(n=C) # (550,550).单位矩阵
  90. for i in range(H * W): # 依次求每个块的协方差矩阵(550,550)。每个块都有自己的协方差矩阵。
  91. # cov[:, :, i] = LedoitWolf().fit(embedding_vectors[:, :, i].numpy()).covariance_
  92. # np.cov: covariance matrix
  93. # rowvar=False: each column represents a variable(特征、变量), while the rows contain observations(观察,样本).
  94. cov[:, :, i] = np.cov(embedding_vectors[:, :, i].numpy(), rowvar=False) + 0.01 * I
  95. # save learned distribution
  96. train_outputs = [mean, cov] # list of numpy. mean.shape=(550,3136). cov.shape=(550,550,3136)
  97. with open(train_feature_filepath, 'wb') as f:
  98. print("start dump feature: %s --------" % class_name)
  99. pickle.dump(train_outputs, f)
  100. print("finish dump-----------")
  101. else: # 已存在,则直接载入。
  102. print('The file already exists: %s' % train_feature_filepath)
  103. if __name__ == '__main__':
  104. main()

2.4 infer

  1. import random
  2. from random import sample
  3. import argparse
  4. import numpy as np
  5. import os
  6. import pickle
  7. from tqdm import tqdm
  8. from collections import OrderedDict
  9. from sklearn.metrics import roc_auc_score
  10. from sklearn.metrics import roc_curve
  11. from sklearn.metrics import precision_recall_curve
  12. from scipy.spatial.distance import mahalanobis
  13. from scipy.ndimage import gaussian_filter
  14. import matplotlib.pyplot as plt
  15. import torch
  16. import torch.nn.functional as F
  17. from torch.utils.data import DataLoader
  18. from torchvision.models import wide_resnet50_2, resnet18
  19. import datasets.mvtec as mvtec
  20. # device setup
  21. from utils import embedding_concat, plot_fig
  22. use_cuda = torch.cuda.is_available()
  23. device = torch.device('cuda' if use_cuda else 'cpu')
  24. def parse_args():
  25. parser = argparse.ArgumentParser('PaDiM')
  26. parser.add_argument('--data_path', type=str, default='D:/dataset/mvtec_anomaly_detection')
  27. parser.add_argument('--save_path', type=str, default='./mvtec_result')
  28. parser.add_argument('--arch', type=str, choices=['resnet18', 'wide_resnet50_2'], default='wide_resnet50_2')
  29. return parser.parse_args()
  30. def main():
  31. args = parse_args()
  32. # 1, load model
  33. if args.arch == 'resnet18':
  34. model = resnet18(pretrained=True, progress=True)
  35. t_d = 448
  36. d = 100
  37. elif args.arch == 'wide_resnet50_2':
  38. model = wide_resnet50_2(pretrained=True, progress=True)
  39. t_d = 1792 # 三个feature通道数的和256+512+1024. 在范围[0,1792]范围内随机生成550个整数。
  40. d = 550 # 随机选择550个值
  41. model.to(device) # to cuda
  42. model.eval()
  43. # 这一行将Python内置的random模块的随机种子设置为1024。设置种子可以确保在相同种子的情况下,使用该模块进行的任何随机操作都会生成相同的随机数序列。通常用于实现可重现性。
  44. random.seed(1024)
  45. # 这一行将PyTorch的随机数生成器的种子设置为1024。这用于确保在PyTorch内进行的任何随机操作(例如神经网络权重的初始化)在相同种子下生成相同的结果。
  46. torch.manual_seed(1024)
  47. if use_cuda:
  48. # 这一行设置PyTorch的CUDA(GPU)随机数生成器的种子。这将确保在GPU上进行的随机操作在使用种子1024时也能生成可重现的结果。
  49. torch.cuda.manual_seed_all(1024)
  50. idx = torch.tensor(sample(range(0, t_d), d)) # 在范围[0,1792]范围内随机生成550个整数。
  51. # set model's intermediate outputs。 只使用中间3个layer输出,第四个layer输出没有使用
  52. outputs = []
  53. # 提取layer1,layer2,layer3层输出结果。
  54. def hook(module, input, output):
  55. outputs.append(output)
  56. model.layer1[-1].register_forward_hook(hook) # 每次前向都会调用一次hook,然后output会保持到outputs中。
  57. model.layer2[-1].register_forward_hook(hook)
  58. model.layer3[-1].register_forward_hook(hook)
  59. # 创建特征pickle文件保存路径,其实就是存放[mean, cov],每个数据集都有一个pickle文件。
  60. os.makedirs(os.path.join(args.save_path, 'temp_%s' % args.arch), exist_ok=True)
  61. fig, ax = plt.subplots(1, 2, figsize=(20, 10)) # 一行两列fig
  62. fig_img_rocauc = ax[0] # 第0列fig
  63. fig_pixel_rocauc = ax[1] # 第1列fig
  64. total_roc_auc = [] # test auc list. 不同class的auc
  65. total_pixel_roc_auc = [] #
  66. for class_name in mvtec.CLASS_NAMES: # 遍历不同的数据集
  67. train_feature_filepath = os.path.join(args.save_path, 'temp_%s' % args.arch, 'train_%s.pkl' % class_name)
  68. with open(train_feature_filepath, 'rb') as f:
  69. train_outputs = pickle.load(f)
  70. # 2, dataset
  71. test_dataset = mvtec.MVTecDataset(args.data_path, class_name=class_name, is_train=False)
  72. test_dataloader = DataLoader(test_dataset, batch_size=32, pin_memory=True)
  73. # layer1:所有数据集layer1输出向量,每个向量的shape是[b,256,56,56]
  74. # layer2: 所有数据集layer2输出向量,每个向量的shape是[b,512,28,28]
  75. # layer3: 所有数据集layer3输出向量,每个向量的shape是[b,1024,14,14]
  76. test_outputs = OrderedDict([('layer1', []), ('layer2', []), ('layer3', [])])
  77. gt_list = []
  78. gt_mask_list = []
  79. test_imgs = []
  80. # 4, extract test set features
  81. for (x, y, mask) in tqdm(test_dataloader, '| feature extraction | test | %s |' % class_name):
  82. test_imgs.extend(x.cpu().detach().numpy())
  83. gt_list.extend(y.cpu().detach().numpy())
  84. gt_mask_list.extend(mask.cpu().detach().numpy())
  85. # model prediction
  86. with torch.no_grad():
  87. _ = model(x.to(device)) # x to cuda. x.shape=(32,3,224,224)
  88. # get intermediate layer outputs
  89. for k, v in zip(test_outputs.keys(), outputs):
  90. test_outputs[k].append(v.cpu().detach())
  91. # initialize hook outputs
  92. outputs = []
  93. for k, v in test_outputs.items():
  94. test_outputs[k] = torch.cat(v, 0)
  95. # Embedding concat. 将不同层的输出cat在一起
  96. # layer1(img_num,256,56,56),layer2(img_num,512,28,28),layer3(img_num,1024,14,14)
  97. embedding_vectors = test_outputs['layer1']
  98. for layer_name in ['layer2', 'layer3']:
  99. embedding_vectors = embedding_concat(embedding_vectors, test_outputs[layer_name])
  100. # randomly select d dimension. embedding_vectors.shape=torch.Size([img_num, 1792, 56, 56])
  101. # 注意idx,要和前面train一样。
  102. embedding_vectors = torch.index_select(embedding_vectors, 1, idx) # (img_num,1792,56,56)->(img_num,550,56,56)
  103. # 5. calculate distance matrix
  104. B, C, H, W = embedding_vectors.size()
  105. embedding_vectors = embedding_vectors.view(B, C, H * W).numpy() # (img_num,550,56,56)->(img_num,550,56*56)
  106. dist_list = []
  107. for i in range(H * W): # 遍历每个patch
  108. mean = train_outputs[0][:, i] # train_outputs=[mean(550,3136), cov(550,550,3136)]. 第i块均值
  109. conv_inv = np.linalg.inv(train_outputs[1][:, :, i]) # 第i块协方差矩阵的逆矩阵。(550,550)
  110. # embedding_vectors.shape(img_num,550,3136). sample.shape(550,3136)
  111. # 特征块 sample[:, i].shape(550). 均值mean.shape(550). 协方差矩阵的逆矩阵conv_inv(550,550)
  112. # dist. list of dis. len=(img_num). 每张图像的每个patch有一个距离值. scipy.spatial.distance
  113. dist = [mahalanobis(sample[:, i], mean, conv_inv) for sample in embedding_vectors] # 遍历测试图像,马氏距离
  114. dist_list.append(dist) # {0: list(img_num), 1: list(img_num), patch_idx: list(img_num)}
  115. dist_list = np.array(dist_list).transpose(1, 0).reshape(B, H, W) # (patch_num,image_num)->(83,3136)->(83,56,56)
  116. # 6. upsample distance map.
  117. dist_list = torch.tensor(dist_list)
  118. score_map = F.interpolate(dist_list.unsqueeze(1), size=x.size(2), mode='bilinear',
  119. align_corners=False).squeeze().numpy() # (83,56,56)->(83,224,224)
  120. # 7. apply gaussian smoothing on the score map. 将patch score平滑成image score.
  121. for i in range(score_map.shape[0]): # each test image. scipy.ndimage
  122. score_map[i] = gaussian_filter(score_map[i], sigma=4) # (224,224)
  123. # Normalization. val->(0,1)
  124. max_score = score_map.max()
  125. min_score = score_map.min()
  126. scores = (score_map - min_score) / (max_score - min_score) # (img_num,224,224)
  127. # 8. calculate image-level ROC AUC score. 图像级别的异常分数
  128. img_scores = scores.reshape(scores.shape[0], -1).max(
  129. axis=1) # (img_num,224,224) -> (img_num,224*224) ->(img_num)
  130. gt_list = np.asarray(gt_list) # (img_num,)
  131. fpr, tpr, _ = roc_curve(gt_list, img_scores) # sklearn.metrics. shape (img_num)
  132. img_roc_auc = roc_auc_score(gt_list, img_scores) # auc.
  133. total_roc_auc.append(img_roc_auc) # auc list
  134. print('image ROCAUC: %.3f' % (img_roc_auc)) # 当前数据集类别(bottle)的auc
  135. fig_img_rocauc.plot(fpr, tpr, label='%s img_ROCAUC: %.3f' % (class_name, img_roc_auc))
  136. # get optimal threshold:# 使用最大F值对应的那个阈值(同时考虑召回率和准确率)
  137. gt_mask = np.asarray(gt_mask_list) # list of tensor. -> (img_num,1,224,224). scores.shape(img_num,224,224)
  138. # thresholds不同的阈值,返回不同的准确率和召回率
  139. precision, recall, thresholds = precision_recall_curve(gt_mask.flatten(), scores.flatten()) # sklearn.metrics
  140. a = 2 * precision * recall # 不同的阈值下,求F值
  141. b = precision + recall # a/b
  142. f1 = np.divide(a, b, out=np.zeros_like(a), where=b != 0) # 防止除0操作
  143. threshold = thresholds[np.argmax(f1)] # 使用最大F值对应的那个阈值。
  144. # calculate per-pixel level ROCAUC. (img_num,1,224,224). scores.shape(img_num,224,224)
  145. fpr, tpr, _ = roc_curve(gt_mask.flatten(), scores.flatten())
  146. per_pixel_rocauc = roc_auc_score(gt_mask.flatten(), scores.flatten())
  147. total_pixel_roc_auc.append(per_pixel_rocauc)
  148. print('pixel ROCAUC: %.3f' % (per_pixel_rocauc))
  149. fig_pixel_rocauc.plot(fpr, tpr, label='%s ROCAUC: %.3f' % (class_name, per_pixel_rocauc))
  150. save_dir = args.save_path + '/' + f'pictures_{args.arch}'
  151. os.makedirs(save_dir, exist_ok=True)
  152. # 原图list (3,224,224).
  153. plot_fig(test_imgs, scores, gt_mask_list, threshold, save_dir, class_name)
  154. print('Average ROCAUC: %.3f' % np.mean(total_roc_auc))
  155. fig_img_rocauc.title.set_text('Average image ROCAUC: %.3f' % np.mean(total_roc_auc))
  156. fig_img_rocauc.legend(loc="lower right")
  157. print('Average pixel ROCUAC: %.3f' % np.mean(total_pixel_roc_auc))
  158. fig_pixel_rocauc.title.set_text('Average pixel ROCAUC: %.3f' % np.mean(total_pixel_roc_auc))
  159. fig_pixel_rocauc.legend(loc="lower right")
  160. fig.tight_layout()
  161. fig.savefig(os.path.join(args.save_path, 'roc_curve.png'), dpi=100)
  162. if __name__ == '__main__':
  163. main()

参考:GitHub - xiahaifeng1995/PaDiM-Anomaly-Detection-Localization-master: This is an unofficial implementation of the paper “PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization”.This is an unofficial implementation of the paper “PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization”. - GitHub - xiahaifeng1995/PaDiM-Anomaly-Detection-Localization-master: This is an unofficial implementation of the paper “PaDiM: a Patch Distribution Modeling Framework for Anomaly Detection and Localization”.icon-default.png?t=N7T8https://github.com/xiahaifeng1995/PaDiM-Anomaly-Detection-Localization-master

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/木道寻08/article/detail/747333
推荐阅读
  

闽ICP备14008679号