dk2430098's picture
Upload folder using huggingface_hub
928b74f verified
"""
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()