Spaces:
Sleeping
Sleeping
| import cv2 | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| class GradCAM: | |
| """ | |
| Grad-CAM implementation for PyTorch models. | |
| Supports visualizing the regions of the input image that are most important | |
| for the model's prediction. | |
| """ | |
| def __init__(self, model, target_layer): | |
| self.model = model | |
| self.target_layer = target_layer | |
| self.gradients = None | |
| self.activations = None | |
| # Register hooks | |
| self.target_layer.register_forward_hook(self.save_activation) | |
| self.target_layer.register_full_backward_hook(self.save_gradient) | |
| def save_activation(self, module, input, output): | |
| if torch.backends.mps.is_available(): | |
| torch.mps.synchronize() | |
| self.activations = output.clone().detach() | |
| import logging | |
| logger = logging.getLogger("GradCAM") | |
| logger.info(f"[Hook] Captured Activations - Max: {self.activations.max().item():.4f}, Min: {self.activations.min().item():.4f}") | |
| def save_gradient(self, module, grad_input, grad_output): | |
| # grad_output is a tuple; we want the first element | |
| self.gradients = grad_output[0] | |
| def generate_cam(self, input_tensor, target_class=None): | |
| """ | |
| Generates the Class Activation Map (CAM). | |
| Args: | |
| input_tensor (torch.Tensor): Preprocessed image tensor (1, C, H, W) | |
| target_class (int, optional): The class to generate CAM for. | |
| If None, uses the class with highest score. | |
| Returns: | |
| np.ndarray: The normalized CAM (H, W) in range [0, 1]. | |
| """ | |
| # Ensure we have gradients enabled for this forward/backward pass | |
| self.model.zero_grad() | |
| # Forward pass | |
| output = self.model(input_tensor) | |
| if target_class is None: | |
| target_class = output.argmax(dim=1).item() | |
| # Extract the score for the target class | |
| score = output[0, target_class] | |
| # Backward pass | |
| score.backward() | |
| # Get activations and gradients from the hooks | |
| gradients = self.gradients.detach().cpu().numpy()[0] # (C, H, W) | |
| activations = self.activations.detach().cpu().numpy()[0] # (C, H, W) | |
| # Compute the channel weights (global average pooling of gradients) | |
| weights = np.mean(gradients, axis=(1, 2)) # (C,) | |
| import logging | |
| logger = logging.getLogger("GradCAM") | |
| logger.info(f"Gradients - Max: {np.max(gradients):.4e}, Min: {np.min(gradients):.4e}, Sum: {np.sum(gradients):.4e}") | |
| logger.info(f"Activations - Max: {np.max(activations):.4f}, Min: {np.min(activations):.4f}, Sum: {np.sum(activations):.4f}") | |
| logger.info(f"Weights - Max: {np.max(weights):.4e}, Min: {np.min(weights):.4e}, Sum: {np.sum(weights):.4e}") | |
| # Compute the weighted sum of activations | |
| cam = np.zeros(activations.shape[1:], dtype=np.float32) | |
| for i, w in enumerate(weights): | |
| cam += w * activations[i, :, :] | |
| import logging | |
| logger = logging.getLogger("GradCAM") | |
| logger.info(f"Raw CAM - Max: {np.max(cam):.4f}, Min: {np.min(cam):.4f}") | |
| # Apply ReLU to keep only features that have a positive influence on the target class | |
| cam = np.maximum(cam, 0) | |
| # Normalize the CAM to [0, 1] | |
| cam = cv2.resize(cam, (input_tensor.shape[3], input_tensor.shape[2])) | |
| cam_max = np.max(cam) | |
| cam_min = np.min(cam) | |
| logger.info(f"Post-ReLU Resized CAM - Max: {cam_max:.4f}, Min: {cam_min:.4f}") | |
| cam = cam - cam_min | |
| cam = cam / (cam_max + 1e-8) | |
| return cam | |
| def overlay_cam(img_pil: Image.Image, cam: np.ndarray, alpha: float = 0.5) -> Image.Image: | |
| """ | |
| Overlays the CAM heatmap onto the original image. | |
| Args: | |
| img_pil (PIL.Image.Image): Original image (RGB). | |
| cam (np.ndarray): Normalized CAM (H, W) in [0, 1]. | |
| alpha (float): Blending factor. | |
| Returns: | |
| PIL.Image.Image: Superimposed image. | |
| """ | |
| # Ensure cam is same size as image | |
| if cam.shape != img_pil.size[::-1]: | |
| cam = cv2.resize(cam, img_pil.size) | |
| # Convert PIL to cv2 (RGB to BGR for colormap) | |
| img_cv2 = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) | |
| # Convert CAM to 8-bit heatmap | |
| heatmap = np.uint8(255 * cam) | |
| heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) | |
| # Superimpose | |
| superimposed = cv2.addWeighted(img_cv2, 1 - alpha, heatmap, alpha, 0) | |
| # Convert back to PIL | |
| superimposed = cv2.cvtColor(superimposed, cv2.COLOR_BGR2RGB) | |
| return Image.fromarray(superimposed) | |