File size: 6,615 Bytes
928b74f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
explainability/gradcam.py
--------------------------
Grad-CAM implementation for the CNN Branch (EfficientNet-B0).

If the CNN model is untrained or fails, generates a meaningful
image-based saliency heatmap so the visualization is always populated.
"""

import numpy as np
import base64
import io
import cv2


# ─────────────────────────────────────────────────────────────────
# Helper: encode numpy image to base64 JPEG string
# ─────────────────────────────────────────────────────────────────

def _encode_b64(img_rgb: np.ndarray) -> str:
    """Encode (H, W, 3) uint8 RGB image to base64 JPEG string."""
    bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
    _, buf = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, 90])
    return base64.b64encode(buf.tobytes()).decode("utf-8")


# ─────────────────────────────────────────────────────────────────
# Saliency-based fallback (works without any model)
# ─────────────────────────────────────────────────────────────────

def _saliency_heatmap(img_tensor_or_array) -> dict:
    """
    Generate a gradient-free saliency/edge prominence heatmap from the
    image itself using Laplacian + Sobel, colorized like a Grad-CAM.
    This ensures the Grad-CAM panel always shows something informative.
    """
    try:
        # Accept tf.Tensor or numpy array
        try:
            arr = img_tensor_or_array.numpy()
        except AttributeError:
            arr = np.array(img_tensor_or_array)

        # Remove batch dim if present
        if arr.ndim == 4:
            arr = arr[0]

        # Convert [0,1] float to uint8
        img_u8 = np.clip(arr * 255, 0, 255).astype(np.uint8)
        gray   = cv2.cvtColor(img_u8, cv2.COLOR_RGB2GRAY)

        # Laplacian edge prominence
        lap = cv2.Laplacian(gray, cv2.CV_64F)
        lap = np.abs(lap)

        # Sobel magnitude
        sx  = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
        sy  = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
        sobel = np.sqrt(sx**2 + sy**2)

        # Combine & blur for smooth heatmap
        combined = 0.5 * lap + 0.5 * sobel
        combined = cv2.GaussianBlur(combined, (21, 21), 0)

        # Normalize to [0, 255]
        mn, mx = combined.min(), combined.max()
        if mx > mn:
            combined = (combined - mn) / (mx - mn)
        heatmap_u8 = np.uint8(255 * combined)

        # Apply JET colormap
        colored = cv2.applyColorMap(heatmap_u8, cv2.COLORMAP_JET)
        colored_rgb = cv2.cvtColor(colored, cv2.COLOR_BGR2RGB)

        # Overlay on original image
        overlay = cv2.addWeighted(img_u8, 0.5, colored_rgb, 0.5, 0)

        return {
            "heatmap_b64": _encode_b64(overlay),
            "available":   True,     # show the image in the panel
            "note":        "Saliency heatmap (CNN weights loading failed β€” showing edge-based proxy)",
        }
    except Exception as e:
        print(f"[GradCAM] Saliency fallback failed: {e}")
        return _fallback_heatmap()


def _fallback_heatmap() -> dict:
    """Minimal fallback β€” tells frontend CNN is unavailable."""
    return {"heatmap_b64": "", "available": False}


# ─────────────────────────────────────────────────────────────────
# True Grad-CAM (requires trained CNN model)
# ─────────────────────────────────────────────────────────────────

def compute_gradcam(feature_model, img_tensor, target_class: int = 1) -> dict:
    """
    Compute Grad-CAM heatmap using the CNN branch feature model.

    Args:
        feature_model : Keras Model returning (conv_output, predictions)
        img_tensor    : tf.Tensor (1, 224, 224, 3)
        target_class  : 1 = fake class

    Returns:
        dict with heatmap_b64, available, overlay_b64
    """
    try:
        import tensorflow as tf

        with tf.GradientTape() as tape:
            inputs = tf.cast(img_tensor, tf.float32)
            tape.watch(inputs)
            conv_outputs, predictions = feature_model(inputs, training=False)
            if predictions.shape[-1] == 1:
                loss = predictions[:, 0]
            else:
                loss = predictions[:, target_class]

        grads = tape.gradient(loss, conv_outputs)
        if grads is None:
            raise ValueError("Gradients are None β€” check feature_model output.")

        pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
        conv_out     = conv_outputs[0]
        heatmap      = tf.reduce_sum(
            conv_out * pooled_grads[tf.newaxis, tf.newaxis, :], axis=-1
        )
        heatmap = np.maximum(heatmap.numpy(), 0)
        if heatmap.max() > 0:
            heatmap /= heatmap.max()

        return _build_gradcam_output(heatmap, img_tensor)

    except Exception as e:
        print(f"[GradCAM] compute_gradcam failed: {e}. Falling back to saliency.")
        return _saliency_heatmap(img_tensor)


def _build_gradcam_output(heatmap: np.ndarray, img_tensor) -> dict:
    """Resize heatmap, colorize, overlay on original, return base64."""
    try:
        # Get original image
        try:
            arr = img_tensor.numpy()
        except AttributeError:
            arr = np.array(img_tensor)
        if arr.ndim == 4:
            arr = arr[0]

        img_u8 = np.clip(arr * 255, 0, 255).astype(np.uint8)
        h, w   = img_u8.shape[:2]

        # Resize heatmap to image size
        hm_resized = cv2.resize(heatmap, (w, h))
        hm_u8      = np.uint8(255 * hm_resized)
        colored    = cv2.applyColorMap(hm_u8, cv2.COLORMAP_JET)
        colored_rgb = cv2.cvtColor(colored, cv2.COLOR_BGR2RGB)

        overlay = cv2.addWeighted(img_u8, 0.55, colored_rgb, 0.45, 0)

        return {
            "heatmap_b64": _encode_b64(overlay),
            "available":   True,
        }
    except Exception as e:
        print(f"[GradCAM] _build_gradcam_output failed: {e}")
        return _fallback_heatmap()