一、程序结构展示
上篇笔记讲解了training_utils.py文件,本篇笔记主要讲解visualization.py文件。
二、代码功能介绍
本篇代码主要用于图像和特征可视化。下面是各个函数的解释:
-
plot_sample_cv2(names, imgs, scores_: dict, gts, save_folder=None)
: 这个函数用于绘制图像样本及其对应的真实标签和分数(或其他指标)的热力图。具体步骤包括:- 将分数进行归一化处理,以便与图像进行叠加显示。
- 根据真实标签,在图像上标记出异常区域。
- 将原始图像、标记的异常区域图像和叠加了热力图的图像保存到指定文件夹中。
-
plot_anomaly_score_distributions(scores: dict, ground_truths_list, save_folder, class_name)
: 这个函数用于绘制异常分数的分布直方图。具体步骤包括:- 从给定的分数字典中提取分数。
- 根据真实标签将分数分为正常样本和异常样本。
- 绘制正常样本和异常样本的分布直方图,并将其保存到指定文件夹中。
-
visualize_feature(features, labels, legends, n_components=3, method='TSNE')
: 这个函数用于可视化特征。具体步骤包括:- 根据选择的方法(如 TSNE 或 PCA)将特征降维到 2 维或 3 维。
- 根据标签对降维后的特征进行散点图可视化。
- 可选地,将图例添加到图像中。
-
scatter_3d(feat_proj, label)
: 这个函数用于绘制 3 维特征的散点图。它会创建一个 3D 散点图,并根据标签对特征进行着色。 -
scatter_2d(feat_proj, label)
: 这个函数用于绘制 2 维特征的散点图。它会创建一个 2D 散点图,并根据标签对特征进行着色。
这些函数可以帮助分析图像数据和特征,发现其中的模式和异常,从而更好地理解数据和模型的表现。
三、代码逐行注释
import cv2 # 导入 OpenCV 库,用于图像处理和操作。
import matplotlib # 导入 matplotlib 库,用于数据可视化,包括绘制图表和图像等
matplotlib.use("Agg") # 设置 matplotlib 使用的后端为 "Agg",这是一个无界面的后端,用于在不显示图形的情况下保存图像
import matplotlib.pyplot as plt # 导入 matplotlib 中的 pyplot 模块,提供了类似 MATLAB 风格的绘图函数,方便快速绘制各种图形
import numpy as np # 导入 NumPy 库,用于数组操作和数学计算
import os # 导入 os 模块,用于与操作系统进行交互,例如创建文件夹、读取文件等
import seaborn as sns # 导入 seaborn 库,是 matplotlib 的一个高级接口,提供了更多样式和绘图选项,用于创建更漂亮的统计图表
##
from sklearn.manifold import TSNE # 从 scikit-learn 库中导入 t-SNE(t-distributed Stochastic Neighbor Embedding)算法,用于降维和可视化高维数据
from sklearn.decomposition import PCA # 从 scikit-learn 库中导入 PCA(Principal Component Analysis)算法,也用于降维和数据压缩
##
import matplotlib.ticker as mtick # 导入 matplotlib 中的 ticker 模块,用于设置刻度标签的格式
def plot_sample_cv2(names, imgs, scores_: dict, gts, save_folder=None): # names: 图像名称的列表。imgs: 图像数据的列表。scores_: 一个字典,包含了每个图像的分数信息。gts: 图像的真实标签。save_folder: 可选参数,指定保存结果图像的文件夹路径
# get subplot number
total_number = len(imgs) # 获取图像数量,即图像列表 imgs 的长度
scores = scores_.copy() # 复制分数字典,以便后续对分数进行归一化处理而不影响原始数据
# normarlisze anomalies
for k, v in scores.items(): # 遍历分数字典中的每个项目
max_value = np.max(v) # 获取当前分数数组中的最大值
min_value = np.min(v) # 获取当前分数数组中的最小值
scores[k] = (scores[k] - min_value) / max_value * 255 # 对当前分数数组进行归一化处理,将分数值缩放到 0 到 255 的范围内
scores[k] = scores[k].astype(np.uint8) # 将归一化后的分数数组转换为 np.uint8 数据类型,以便后续图像处理
# draw gts
mask_imgs = [] # 初始化一个空列表,用于存储带有标签的图像
for idx in range(total_number): # 遍历每张图像的索引
gts_ = gts[idx] # 获取当前图像的真实标签
mask_imgs_ = imgs[idx].copy() # 复制当前图像的数据,以便后续绘制标签
mask_imgs_[gts_ > 0.5] = (0, 0, 255) # 将真实标签值大于 0.5 的像素点设为红色(0, 0, 255)
mask_imgs.append(mask_imgs_) # 将带有标签的图像添加到 mask_imgs 列表中
# save imgs
for idx in range(total_number): # 再次遍历每张图像的索引,用于保存图像结果
cv2.imwrite(os.path.join(save_folder, f'{names[idx]}_ori.jpg'), imgs[idx]) # 将原始图像保存为文件,文件名包含图像名称和 _ori.jpg 后缀
cv2.imwrite(os.path.join(save_folder, f'{names[idx]}_gt.jpg'), mask_imgs[idx]) # 将带有标签的图像保存为文件,文件名包含图像名称和 _gt.jpg 后缀
for key in scores: # 遍历每个分数对应的键
heat_map = cv2.applyColorMap(scores[key][idx], cv2.COLORMAP_JET) # 将当前图像的分数数组转换为热力图
visz_map = cv2.addWeighted(heat_map, 0.5, imgs[idx], 0.5, 0) # 将热力图与原始图像进行加权叠加
cv2.imwrite(os.path.join(save_folder, f'{names[idx]}_{key}.jpg'), # 将叠加后的可视化图像保存为文件,文件名包含图像名称和对应分数的键
visz_map)
def plot_anomaly_score_distributions(scores: dict, ground_truths_list, save_folder, class_name): # 接受参数:scores: 一个字典,包含了不同类别的异常分数;ground_truths_list: 真实标签的列表
ground_truths = np.stack(ground_truths_list, axis=0) # 将真实标签列表堆叠成一个 numpy 数组,用于后续计算
N_COUNT = 100000 # 定义一个常量,表示每个分布样本的数量
for k, v in scores.items(): # 遍历异常分数字典中的每一项
layer_score = np.stack(v, axis=0) # 将当前类别的异常分数列表堆叠成一个 numpy 数组
normal_score = layer_score[ground_truths == 0] # 从当前类别的异常分数中提取真实标签为正常的分数
abnormal_score = layer_score[ground_truths != 0] # 从当前类别的异常分数中提取真实标签为异常的分数
plt.clf() # 清空当前图形
plt.figure(figsize=(4, 3)) # 创建一个新的图形,指定图形大小为 4x3
ax = plt.gca() # 获取当前图形的坐标轴
ax.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.2f')) # 设置坐标轴的刻度格式为保留两位小数
ax.xaxis.set_major_formatter(mtick.FormatStrFormatter('%.2f'))
# with plt.style.context(['science', 'ieee', 'no-latex']):
sns.histplot(np.random.choice(normal_score, N_COUNT), color="green", bins=50, label='${d(p_n)}$',
stat='probability', alpha=.75) # 绘制正常分数的直方图,并指定颜色、分组数、标签等参数
sns.histplot(np.random.choice(abnormal_score, N_COUNT), color="red", bins=50, label='${d(p_a)}$',
stat='probability', alpha=.75) # 绘制异常分数的直方图
plt.xlim([0, 3]) # 设置 x 轴的范围为 0 到 3
save_path = os.path.join(save_folder, f'distributions_{class_name}_{k}.jpg') # 构建保存结果图像的文件路径,文件名包含类别名称和分数键
plt.savefig(save_path, bbox_inches='tight', dpi=300) # 将当前图形保存为图像文件
valid_feature_visualization_methods = ['TSNE', 'PCA'] # 定义了一个列表,包含了有效的特征可视化方法,即 t-SNE 和 PCA
def visualize_feature(features, labels, legends, n_components=3, method='TSNE'): # 接受参数为features: 特征数据,即待可视化的数据;labels: 数据对应的标签;legends: 图例;n_components: 降维后的维度,默认为 3;method: 使用的降维方法,默认为 t-SNE
assert method in valid_feature_visualization_methods # 断言降维方法是否在有效的可视化方法列表中
assert n_components in [2, 3] # 断言降维后的维度是否为 2 或 3
if method == 'TSNE': # 根据指定的方法选择降维模型,若方法为 t-SNE,则使用 t-SNE 模型;若方法为 PCA,则使用 PCA 模型
model = TSNE(n_components=n_components) # 创建相应的 t-SNE 或 PCA 模型对象,并指定降维后的维度
elif method == 'PCA':
model = PCA(n_components=n_components)
else:
raise NotImplementedError
feat_proj = model.fit_transform(features) # 使用特征数据拟合降维模型,并进行降维操作,得到降维后的特征表示
if n_components == 2:
ax = scatter_2d(feat_proj, labels) # 调用 scatter_2d 函数绘制二维散点图,传入降维后的特征表示和对应的标签
elif n_components == 3:
ax = scatter_3d(feat_proj, labels) # 调用 scatter_3d 函数绘制三维散点图,传入降维后的特征表示和对应的标签
else:
raise NotImplementedError
plt.legend(legends) # 添加图例
plt.axis('off') # 关闭坐标轴
def scatter_3d(feat_proj, label): # 定义了一个名为 scatter_3d 的函数,接受特征投影和标签作为参数
plt.clf() # 清空当前图形
ax1 = plt.axes(projection='3d') # 创建一个三维坐标轴
label_unique = np.unique(label) # 获取标签中的唯一值
for l in label_unique: # 遍历标签中的每一个唯一值
ax1.scatter3D(feat_proj[label == l, 0],
feat_proj[label == l, 1],
feat_proj[label == l, 2], s=5) # 绘制三维散点图,其中 feat_proj[label == l, 0] 表示特征投影中标签为 l 的样本在第一个特征维度上的值,feat_proj[label == l, 1] 和 feat_proj[label == l, 2] 分别表示第二个和第三个特征维度上的值,s=5 表示散点的大小为 5
return ax1
def scatter_2d(feat_proj, label): # 定义了一个名为 scatter_2d 的函数,与 scatter_3d 类似,用于绘制二维散点图
plt.clf() # 清空当前图形
ax1 = plt.axes() # 创建一个二维坐标轴
label_unique = np.unique(label) # 获取标签中的唯一值
for l in label_unique: # 遍历标签中的每一个唯一值
ax1.scatter(feat_proj[label == l, 0],
feat_proj[label == l, 1], s=5) # 绘制二维散点图,其中 feat_proj[label == l, 0] 表示特征投影中标签为 l 的样本在第一个特征维度上的值,feat_proj[label == l, 1] 表示第二个特征维度上的值,s=5 表示散点的大小为 5
return ax1