import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import cv2 from PIL import Image import matplotlib.pyplot as plt import datetime from Load_process.file_processing import Process_File class GradCAM: def __init__(self, model, target_layer): """ 初始化 Grad-CAM Args: model: 訓練好的 ModifiedXception 模型 target_layer: 要計算 Grad-CAM 的目標層名稱 (例如 'base_model') """ self.model = model self.target_layer = target_layer self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') self.model.eval() self.model.to(self.device) # 用於儲存特徵圖和梯度 self.features = None self.gradients = None # 註冊 hook self._register_hooks() def _register_hooks(self): """註冊前向和反向傳播的 hook""" def forward_hook(module, input, output): self.features = output def backward_hook(module, grad_in, grad_out): self.gradients = grad_out[0] # 獲取目標層 target_module = dict(self.model.named_modules())[self.target_layer] target_module.register_forward_hook(forward_hook) target_module.register_backward_hook(backward_hook) def generate_cam(self, input_image, target_class=None): """ 生成 Grad-CAM 熱力圖 Args: input_image: 輸入影像 (torch.Tensor, shape: [1, C, H, W]) target_class: 目標類別索引 (若為 None,使用預測最高分數的類別) Returns: cam: Grad-CAM 熱力圖 (numpy array) """ input_image = input_image.to(self.device) # 前向傳播 output = self.model(input_image) if target_class is None: target_class = torch.argmax(output, dim=1).item() # 清除梯度 self.model.zero_grad() # 反向傳播計算梯度 one_hot = torch.zeros_like(output) one_hot[0][target_class] = 1 output.backward(gradient=one_hot, retain_graph=True) # 計算 Grad-CAM gradients = self.gradients.data.cpu().numpy()[0] features = self.features.data.cpu().numpy()[0] # 全局平均池化梯度 weights = np.mean(gradients, axis=(1, 2)) # 計算加權和 cam = np.zeros(features.shape[1:], dtype=np.float32) for i, w in enumerate(weights): cam += w * features[i] # ReLU 激活 cam = np.maximum(cam, 0) # 歸一化到 0-1 cam = cam - np.min(cam) cam = cam / np.max(cam) # 調整大小到輸入影像尺寸 h, w = input_image.shape[2:] cam = cv2.resize(cam, (w, h)) return cam def overlay_cam(self, original_image, cam, alpha=0.5): """ 將 Grad-CAM 熱力圖疊加到原始影像上 Args: original_image: 原始影像 (numpy array, shape: [H, W, C]) cam: Grad-CAM 熱力圖 alpha: 透明度 Returns: overlay_img: 疊加後的影像 """ # 將熱力圖轉為 RGB heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) heatmap = np.float32(heatmap) / 255 # 確保原始影像格式正確 if original_image.max() > 1: original_image = original_image / 255.0 # 疊加熱力圖 overlay_img = heatmap * alpha + original_image * (1 - alpha) overlay_img = np.clip(overlay_img, 0, 1) return overlay_img def visualize(self, input_image, original_image, target_class=None, File_Name=None, model_name = None): """ 可視化 Grad-CAM 結果 Args: input_image: 輸入影像 (torch.Tensor) original_image: 原始影像 (numpy array) target_class: 目標類別索引 save_path: 保存路徑 (可選) """ File = Process_File() # 生成 CAM cam = self.generate_cam(input_image, target_class) # 疊加到原始影像 overlay = self.overlay_cam(original_image, cam) # 顯示結果 plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plt.imshow(original_image) plt.title('Original Image') plt.axis('off') plt.subplot(1, 2, 2) plt.imshow(overlay) plt.title(f'Grad-CAM (Class {target_class})') plt.axis('off') model_dir = '../Result/Grad-CAM( ' + str(datetime.date.today()) + " )" File.JudgeRoot_MakeDir(model_dir) modelfiles = File.Make_Save_Root(str(model_name) + " " + File_Name + ".png", model_dir) plt.savefig(modelfiles) plt.close("all") # 關閉圖表