File size: 7,207 Bytes
1feed70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""
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