Spaces:
Paused
Paused
| """ | |
| src/gradcam.py | |
| Grad-CAM implementation for visualizing model decisions | |
| """ | |
| import cv2 | |
| import numpy as np | |
| import tensorflow as tf | |
| from typing import Optional, Tuple, Union | |
| class GradCAM: | |
| """ | |
| Gradient-weighted Class Activation Mapping (Grad-CAM) | |
| Visualizes which regions of an image the model focuses on for its predictions. | |
| Based on the original Grad-CAM paper: "Grad-CAM: Visual Explanations from Deep Neural Networks via Gradient-based Localization" | |
| """ | |
| def __init__(self, model, last_conv_layer_name: str = 'out_relu'): | |
| """ | |
| Initialize Grad-CAM with a Keras model. | |
| Args: | |
| model: Keras model | |
| last_conv_layer_name: Name of the last convolutional layer to use | |
| """ | |
| self.model = model | |
| self.last_conv_layer_name = last_conv_layer_name | |
| # Create the gradient model once | |
| self.grad_model = self._create_gradient_model() | |
| def _create_gradient_model(self): | |
| """ | |
| Create a model that connects the last conv layer and model output. | |
| Returns: | |
| Keras Model that outputs both the last conv layer and predictions | |
| """ | |
| return tf.keras.Model( | |
| [self.model.inputs], | |
| [self.model.get_layer(self.last_conv_layer_name).output, self.model.output] | |
| ) | |
| def generate_heatmap(self, image_array: np.ndarray, class_index: int) -> np.ndarray: | |
| """ | |
| Generate a Grad-CAM heatmap for a given image and class. | |
| Args: | |
| image_array: Preprocessed image array (with batch dimension) | |
| class_index: Index of the class to generate heatmap for | |
| Returns: | |
| Generated heatmap (numpy array, shape: H x W) | |
| """ | |
| with tf.GradientTape() as tape: | |
| last_conv_layer_output, preds = self.grad_model(image_array) | |
| class_channel = preds[:, class_index] | |
| grads = tape.gradient(class_channel, last_conv_layer_output) | |
| # Pool gradients across spatial dimensions | |
| pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2)) | |
| # Get the output of the last conv layer | |
| last_conv_layer_output = last_conv_layer_output[0] | |
| # Compute the weighted combination of feature maps | |
| heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis] | |
| heatmap = tf.squeeze(heatmap) | |
| # Apply ReLU to get positive weights | |
| heatmap = tf.maximum(heatmap, 0) | |
| # Normalize heatmap to [0, 1] | |
| max_val = tf.math.reduce_max(heatmap) | |
| if max_val > 0: | |
| heatmap = heatmap / max_val | |
| return heatmap.numpy() | |
| def overlay_heatmap(self, original_image: np.ndarray, heatmap: np.ndarray, | |
| alpha: float = 0.5) -> np.ndarray: | |
| """ | |
| Overlay heatmap on original image using JET colormap. | |
| Args: | |
| original_image: Original image (numpy array, RGB) | |
| heatmap: Grad-CAM heatmap (normalized to [0, 1]) | |
| alpha: Weight for heatmap in overlay (0-1) | |
| Returns: | |
| Image with heatmap overlay (numpy array, RGB) | |
| """ | |
| # Resize heatmap to match original image dimensions | |
| heatmap = cv2.resize(heatmap, (original_image.shape[1], original_image.shape[0])) | |
| # Convert heatmap to 8-bit format and apply JET colormap | |
| heatmap = np.uint8(255 * heatmap) | |
| heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) | |
| # Convert original image to float for blending | |
| original_float = original_image.astype(np.float32) | |
| heatmap_float = heatmap.astype(np.float32) | |
| # Blend original image with heatmap | |
| superimposed_img = heatmap_float * alpha + original_float | |
| superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8) | |
| return superimposed_img | |
| def preprocess_frame(frame: np.ndarray, target_size: Tuple[int, int] = (224, 224)): | |
| """ | |
| Preprocess a video frame for model inference. | |
| Args: | |
| frame: Input frame (numpy array, BGR format from OpenCV) | |
| target_size: Target size for model input | |
| Returns: | |
| Preprocessed frame (numpy array) | |
| """ | |
| # Resize to model input size | |
| resized = cv2.resize(frame, target_size) | |
| # Normalize to [0, 1] | |
| normalized = resized / 255.0 | |
| # Add batch dimension | |
| batched = np.expand_dims(normalized, axis=0) | |
| return batched | |
| def get_confidence_and_prediction(predictions: np.ndarray, | |
| class_labels: list) -> Tuple[str, float, np.ndarray]: | |
| """ | |
| Get prediction label, confidence, and full probability array. | |
| Args: | |
| predictions: Model output probabilities (array of shape [1, 3]) | |
| class_labels: List of class labels | |
| Returns: | |
| Tuple of (predicted_label, confidence, probability_array) | |
| """ | |
| prob_array = predictions[0] | |
| max_idx = np.argmax(prob_array) | |
| confidence = prob_array[max_idx] | |
| label = class_labels[max_idx] | |
| return label, confidence, prob_array | |
| def get_text_overlay_params(label: str, color_map: dict): | |
| """ | |
| Get parameters for text overlay on frames. | |
| Args: | |
| label: Predicted class label | |
| color_map: Dictionary mapping labels to colors | |
| Returns: | |
| Tuple of (text_color, background_color) | |
| """ | |
| text_color = color_map.get(label, (255, 255, 255)) # White default | |
| background_color = (0, 0, 0) # Black background | |
| return text_color, background_color | |
| def create_text_overlay(frame: np.ndarray, text: str, | |
| text_color: Tuple[int, int, int], | |
| bg_color: Tuple[int, int, int] = (0, 0, 0)): | |
| """ | |
| Create a text overlay with background on the frame. | |
| Args: | |
| frame: Input frame | |
| text: Text to display | |
| text_color: Color of the text | |
| bg_color: Background color | |
| Returns: | |
| Frame with text overlay | |
| """ | |
| # Get text size | |
| font = cv2.FONT_HERSHEY_SIMPLEX | |
| font_scale = 0.7 | |
| thickness = 2 | |
| (text_width, text_height), baseline = cv2.getTextSize(text, font, font_scale, thickness) | |
| # Define text position with padding | |
| text_x, text_y = 10, 40 | |
| rect_x1, rect_y1 = text_x - 5, text_y - text_height - baseline - 5 | |
| rect_x2, rect_y2 = text_x + text_width + 5, text_y + baseline + 5 | |
| # Draw background rectangle | |
| cv2.rectangle(frame, (rect_x1, rect_y1), (rect_x2, rect_y2), bg_color, -1) | |
| # Draw text | |
| cv2.putText(frame, text, (text_x, text_y), font, font_scale, text_color, thickness) | |
| return frame | |