AIOmarRehan's picture
Update app/model.py
9a8bbea verified
Raw
History Blame Contribute Delete
6.58 kB
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)