| import os |
| import numpy as np |
| from PIL import Image |
| import tensorflow as tf |
| import cv2 |
| from tensorflow.keras.models import Model |
| from tensorflow.keras.layers import Conv2D |
|
|
| |
| |
| gpus = tf.config.list_physical_devices("GPU") |
| if gpus: |
| try: |
| for g in gpus: |
| tf.config.experimental.set_memory_growth(g, True) |
| except Exception as e: |
| |
| print("Warning: Could not set memory growth:", e) |
|
|
| print("Num GPUs Available:", len(gpus)) |
| print("TensorFlow version:", tf.__version__) |
|
|
| |
| MODEL_PATH = os.getenv("MODEL_PATH", "saved_model/InceptionV3_Brain_Tumor_MRI.h5") |
| print("Loading model from:", MODEL_PATH) |
| model = tf.keras.models.load_model(MODEL_PATH) |
| model.trainable = False |
|
|
| |
| |
| last_conv_layer = None |
| for layer in reversed(model.layers): |
| if isinstance(layer, Conv2D): |
| last_conv_layer = layer |
| break |
| if last_conv_layer is None: |
| raise RuntimeError("No Conv2D layer found in the model; cannot build Grad-CAM.") |
|
|
| target_layer = model.get_layer(last_conv_layer.name) |
| grad_model = Model(inputs=model.inputs, outputs=[target_layer.output, model.output]) |
| print("Built grad_model with target layer:", target_layer.name) |
|
|
| |
| CLASS_NAMES = ["glioma", "meningioma", "notumor", "pituitary"] |
|
|
| |
| def preprocess_image_pil(img: Image.Image, target_size=(512, 512)): |
| """ |
| Accepts PIL.Image, returns float32 numpy array shaped (1,H,W,3) with values in [0,1]. |
| """ |
| img = img.convert("RGB") |
| img = img.resize(target_size, resample=Image.BILINEAR) |
| arr = np.asarray(img).astype("float32") / 255.0 |
| arr = np.expand_dims(arr, axis=0) |
| return arr |
|
|
| def pil_to_tf_tensor(img: Image.Image, target_size=(512, 512)): |
| """ |
| Convert PIL image to a TF tensor float32 (1,H,W,3) scaled to [0,1]. |
| Uses TF ops to allow better GPU pipeline. |
| """ |
| arr = preprocess_image_pil(img, target_size=target_size) |
| return tf.convert_to_tensor(arr, dtype=tf.float32) |
|
|
| |
| def predict(img: Image.Image): |
| """ |
| Returns (label, confidence, prob_dict) |
| """ |
| input_tensor = preprocess_image_pil(img) |
| |
| preds = model(input_tensor, training=False) |
| probs = preds.numpy()[0] |
| class_idx = int(np.argmax(probs)) |
| confidence = float(np.max(probs)) |
| prob_dict = {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))} |
| return CLASS_NAMES[class_idx], confidence, prob_dict |
|
|
| |
| |
| @tf.function |
| def _compute_conv_and_grads(img_input, class_index): |
| with tf.GradientTape() as tape: |
| conv_outputs, preds = grad_model(img_input) |
|
|
| |
| if isinstance(preds, (list, tuple)): |
| preds = preds[0] |
|
|
| class_logits = preds[:, class_index] |
|
|
| grads = tape.gradient(class_logits, conv_outputs) |
| return conv_outputs, grads, preds |
|
|
| def compute_gradcam_overlay(img: Image.Image, interpolant=0.5, target_size=(512,512)): |
| """ |
| High-level wrapper: |
| -> builds input tensor |
| -> obtains predicted class index (fast forward) |
| -> calls compiled grad function to get conv features + grads |
| -> computes heatmap and overlay efficiently |
| Returns: overlay as uint8 HxWx3 numpy array |
| """ |
| |
| input_tf = pil_to_tf_tensor(img, target_size=target_size) |
|
|
| |
| preds = model(input_tf, training=False) |
| pred_np = preds.numpy()[0] |
| class_idx = int(np.argmax(pred_np)) |
|
|
| |
| conv_out, grads, _ = _compute_conv_and_grads(input_tf, tf.constant(class_idx, dtype=tf.int64)) |
|
|
| |
| conv_out_np = conv_out.numpy() |
| grads_np = grads.numpy() if grads is not None else None |
|
|
| if grads_np is None: |
| |
| H = input_tf.shape[1] |
| W = input_tf.shape[2] |
| original_img = np.array(img.resize((W, H))).astype("uint8") |
| if original_img.ndim == 2: |
| original_img = np.stack([original_img]*3, axis=-1) |
| return original_img |
|
|
| |
| if conv_out_np.ndim == 4 and conv_out_np.shape[0] == 1: |
| conv_out_np = conv_out_np[0] |
| |
| if grads_np.ndim == 4 and grads_np.shape[0] == 1: |
| grads_np = grads_np[0] |
|
|
| |
| pooled_grads = np.mean(grads_np, axis=(0,1)) |
|
|
| |
| heatmap = np.sum(conv_out_np * pooled_grads[np.newaxis, np.newaxis, :], axis=-1) |
| heatmap = np.maximum(heatmap, 0.0) |
| max_val = np.max(heatmap) if heatmap.size else 0.0 |
| if max_val > 0: |
| heatmap = heatmap / (max_val + 1e-9) |
| else: |
| heatmap = np.zeros_like(heatmap, dtype=np.float32) |
|
|
| |
| H = input_tf.shape[1] |
| W = input_tf.shape[2] |
| original_img = np.array(img.resize((W, H))).astype("float32") |
| if original_img.ndim == 2: |
| original_img = np.stack([original_img]*3, axis=-1) |
|
|
| heatmap_resized = cv2.resize((heatmap * 255.0).astype("uint8"), (W, H)) |
| heatmap_color = cv2.applyColorMap(heatmap_resized, cv2.COLORMAP_JET) |
| heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB).astype("float32") |
|
|
| |
| orig_uint8 = np.clip(original_img, 0, 255).astype("uint8") |
|
|
| |
| overlay = np.clip(orig_uint8.astype("float32") * interpolant + heatmap_color * (1.0 - interpolant), 0, 255).astype("uint8") |
| return overlay |
|
|
| |
| __all__ = ["model", "grad_model", "predict", "compute_gradcam_overlay", "CLASS_NAMES"] |
| |
| def gradcam(img: Image.Image, interpolant=0.5): |
| return compute_gradcam_overlay(img, interpolant=interpolant) |