AIOmarRehan's picture
Update app/model.py
bf35e58 verified
# File: app/model.py
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
# GPU setup
# Try to enable memory growth for GPUs to avoid TF pre-allocating all memory
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:
# If setting memory growth fails, just print a warning and continue
print("Warning: Could not set memory growth:", e)
print("Num GPUs Available:", len(gpus))
print("TensorFlow version:", tf.__version__)
# Load model
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
# Find last conv layer and build grad_model once
# Find the last Conv2D layer
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)
# Labels
CLASS_NAMES = ["glioma", "meningioma", "notumor", "pituitary"]
# Preprocessing (use 299x299 for InceptionV3)
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)
# Prediction helper
def predict(img: Image.Image):
"""
Returns (label, confidence, prob_dict)
"""
input_tensor = preprocess_image_pil(img) # numpy (1,H,W,3)
# Try to call model by direct positional input (works for most Keras models)
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
# Compiled Grad-CAM compute function
# We create a tf.function that computes conv features and gradients for a given input and class index
@tf.function
def _compute_conv_and_grads(img_input, class_index):
with tf.GradientTape() as tape:
conv_outputs, preds = grad_model(img_input)
# preds is probably a list -> convert it
if isinstance(preds, (list, tuple)):
preds = preds[0] # take the actual tensor
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
"""
# Build tensor
input_tf = pil_to_tf_tensor(img, target_size=target_size) # (1,H,W,3), float32
# Fast predict to get class index (cheap forward pass)
preds = model(input_tf, training=False)
pred_np = preds.numpy()[0]
class_idx = int(np.argmax(pred_np))
# Use compiled function to compute conv features and grads for that class
conv_out, grads, _ = _compute_conv_and_grads(input_tf, tf.constant(class_idx, dtype=tf.int64))
# Convert to numpy and handle shapes robustly
conv_out_np = conv_out.numpy()
grads_np = grads.numpy() if grads is not None else None
if grads_np is None:
# Fallback: gradients None --> return original image as overlay (no heatmap)
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
# conv_out_np shape (1,Hf,Wf,C) --> take first batch
if conv_out_np.ndim == 4 and conv_out_np.shape[0] == 1:
conv_out_np = conv_out_np[0]
# grads_np shape (1,Hf,Wf,C)
if grads_np.ndim == 4 and grads_np.shape[0] == 1:
grads_np = grads_np[0]
# Global average pooling of gradients over spatial dims (Hf,Wf)
pooled_grads = np.mean(grads_np, axis=(0,1)) # shape (C,)
# Weighted sum of conv feature maps
heatmap = np.sum(conv_out_np * pooled_grads[np.newaxis, np.newaxis, :], axis=-1) # (Hf,Wf)
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)
# Resize heatmap to original image size
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) # BGR
heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB).astype("float32")
# Ensure original image is in uint8 (0,255)
orig_uint8 = np.clip(original_img, 0, 255).astype("uint8")
# Combine using interpolant: (interpolant * original + (1-interpolant) * heatmap_color)
overlay = np.clip(orig_uint8.astype("float32") * interpolant + heatmap_color * (1.0 - interpolant), 0, 255).astype("uint8")
return overlay
# Expose functions for main.py
__all__ = ["model", "grad_model", "predict", "compute_gradcam_overlay", "CLASS_NAMES"]
# Backwards-compatible function name expected by main.py
def gradcam(img: Image.Image, interpolant=0.5):
return compute_gradcam_overlay(img, interpolant=interpolant)