FirenetCNN / src /gradcam.py
OpelSpeedster's picture
Update the Project
1feed70 verified
Raw
History Blame Contribute Delete
7.21 kB
"""
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
@staticmethod
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
@staticmethod
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
@staticmethod
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
@staticmethod
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