import os
import gc
import numpy as np
import tensorflow as tf
import cv2
import gradio as gr
from PIL import Image
from tensorflow.keras.models import load_model, Model
# ==========================================
# 1. SETUP KELAS & VARIABEL GLOBAL
# ==========================================
class_names = ['Infiltrat Pneumonia', 'Normal']
last_conv_layer_name = 'block5_conv3'
# Dictionary pemetaan Model Dropdown -> Nama File
MODEL_DICT = {
"Eksperimen 1 (LR 1e-1)": "model_vgg16_1.keras",
"Eksperimen 2 (LR 1e-2)": "model_vgg16_2.keras",
"Eksperimen 3 (LR 1e-3)": "model_vgg16_3.keras",
"Eksperimen 4 (LR 1e-4)": "model_vgg16_4.keras",
"Eksperimen 5 (LR 1e-4 + Custom Brightness/Contrast)": "model_vgg16_5.keras",
"Eksperimen 6 (LR 1e-1 + Custom Brightness/Contrast)": "model_vgg16_6.keras"
}
# Variabel Global untuk Lazy Loading (Menghemat RAM HuggingFace)
active_model_name = None
active_model = None
active_grad_model = None
# Warna Brand
BRAND_BLUE = "#3F64A9"
BRAND_RED = "#A93F3F"
BRAND_WHITE = "#FFFFFF"
# ==========================================
# 2. CORE LOGIC (Model Switcher & Grad-CAM)
# ==========================================
def load_selected_model(selected_name):
global active_model_name, active_model, active_grad_model
# Jika model yang diminta sudah aktif, langsung gunakan (tidak perlu load ulang)
if selected_name == active_model_name and active_model is not None:
return active_model, active_grad_model
print(f"[INFO] Beralih ke model: {selected_name}...")
# Bersihkan memori dari model sebelumnya
if active_model is not None:
del active_model
del active_grad_model
gc.collect()
tf.keras.backend.clear_session()
file_name = MODEL_DICT[selected_name]
if not os.path.exists(file_name):
raise FileNotFoundError(f"Model {file_name} tidak ditemukan. Pastikan file sudah terunggah.")
active_model = load_model(file_name)
active_grad_model = Model(
inputs=active_model.inputs,
outputs=[active_model.get_layer(last_conv_layer_name).output, active_model.output]
)
active_model_name = selected_name
return active_model, active_grad_model
def make_gradcam_heatmap(img_array, grad_model):
with tf.GradientTape() as tape:
last_conv_layer_output, preds = grad_model(img_array)
pred_index = tf.argmax(preds[0])
class_channel = preds[:, pred_index]
grads = tape.gradient(class_channel, last_conv_layer_output)
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
last_conv_layer_output = last_conv_layer_output[0]
heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
heatmap = tf.squeeze(heatmap)
heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)
return heatmap.numpy()
def create_superimposed_image(original_img, heatmap, alpha=0.4):
if np.max(heatmap) == 0:
return original_img
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
heatmap = cv2.resize(heatmap, (original_img.shape[1], original_img.shape[0]))
superimposed_img = cv2.addWeighted(heatmap, alpha, original_img, 1 - alpha, 0)
return superimposed_img
def predict_and_explain(image, selected_model_name):
if image is None:
return "
Mohon unggah citra terlebih dahulu.
", None
try:
current_model, current_grad_model = load_selected_model(selected_model_name)
except Exception as e:
return f"Error: {str(e)}
", None
# Preprocessing
img_pil = Image.fromarray(image).resize((224, 224))
img_array = np.array(img_pil)
original_img_visual = img_array.copy()
img_array = img_array.astype('float32') / 255.0
img_array = np.expand_dims(img_array, axis=0)
# Prediction
preds = current_model.predict(img_array)
score = preds[0]
predicted_class_index = int(np.argmax(score))
predicted_label = class_names[predicted_class_index]
confidence = float(np.max(score))
# Grad-CAM
heatmap = make_gradcam_heatmap(img_array, current_grad_model)
gradcam_result = create_superimposed_image(original_img_visual, heatmap)
# Output Formatting
if predicted_label == "Infiltrat Pneumonia":
header_color = BRAND_RED
status_icon = "⚠️"
analysis_text = "AI mendeteksi pola opasitas paru yang mengindikasikan keberadaan Infiltrat Pneumonia. Segera lakukan peninjauan klinis lebih lanjut."
else:
header_color = BRAND_BLUE
status_icon = "✅"
analysis_text = "Paru-paru tampak bersih. Tidak ditemukan indikasi visual opasitas atau kelainan paru."
result_md = f"""
{status_icon} {predicted_label}
Tingkat Kepercayaan: {confidence:.2%}
Menggunakan {selected_model_name}
Analisis Klinis AI:
{analysis_text}
*Perhatikan area berwarna merah/hangat pada gambar Heatmap di samping sebagai fokus deteksi utama.*
"""
return result_md, gradcam_result
# ==========================================
# 3. UI/UX & STYLING
# ==========================================
aidia_css = f"""
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
* {{
font-family: 'SF Pro Display', 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
}}
.gradio-container {{
max-width: 100% !important;
padding: 0 40px !important;
margin: 0 !important;
background-color: #F8F9FA;
}}
.header-container {{
text-align: center;
padding: 40px 20px;
background-color: {BRAND_WHITE};
border-bottom: 4px solid {BRAND_BLUE};
margin-bottom: 30px;
border-radius: 0 0 20px 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
}}
.brand-title {{
color: {BRAND_BLUE};
font-weight: 800;
font-size: 4rem;
letter-spacing: -1.5px;
margin-bottom: 10px;
line-height: 1.1;
}}
.brand-tagline {{
color: #6B7280;
font-weight: 400;
font-size: 1.25rem;
letter-spacing: 0.2px;
}}
.image-box {{
border: 2px solid #E5E7EB;
border-radius: 16px;
overflow: hidden;
background-color: white;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: transform 0.2s;
}}
button.primary {{
background-color: {BRAND_BLUE} !important;
color: white !important;
border: none !important;
font-weight: 600;
font-size: 1.1rem;
padding: 12px 24px;
border-radius: 8px;
transition: all 0.3s ease;
}}
button.primary:hover {{
background-color: #2c4a80 !important;
box-shadow: 0 8px 20px rgba(63, 100, 169, 0.3);
transform: translateY(-2px);
}}
.footer-text {{
text-align: center;
color: #9CA3AF;
font-size: 0.9rem;
margin-top: 50px;
padding: 30px;
border-top: 1px solid #E5E7EB;
}}
"""
theme = gr.themes.Soft(
primary_hue="blue",
neutral_hue="slate",
radius_size="lg",
font=['SF Pro Display', 'Inter', 'sans-serif']
).set(
button_primary_background_fill=BRAND_BLUE,
button_primary_text_color="white",
block_title_text_color=BRAND_BLUE
)
# ==========================================
# 4. BUILDING THE APP
# ==========================================
with gr.Blocks(theme=theme, css=aidia_css, title="PneumoScan") as demo:
# --- HEADER ---
with gr.Row():
gr.HTML(f"""
""")
# --- MAIN CONTENT ---
with gr.Row():
# KOLOM INPUT (KIRI)
with gr.Column(scale=5):
gr.Markdown("### ⚙️ Konfigurasi Model")
model_dropdown = gr.Dropdown(
choices=list(MODEL_DICT.keys()),
value="Eksperimen 4 (LR 1e-4)", # Default Model
label="Pilih Arsitektur Model",
interactive=True
)
gr.Markdown("### 📥 Upload Citra Medis")
input_image = gr.Image(
label="Chest X-Ray Input",
type="numpy",
height=450,
elem_classes="image-box"
)
with gr.Row():
clear_btn = gr.ClearButton(components=[input_image], value="Reset", size="sm")
submit_btn = gr.Button("Analisis Diagnosis", variant="primary", size="lg")
gr.Markdown("""
Petunjuk Penggunaan:
- Pilih skenario model yang ingin diuji dari menu dropdown.
- Upload foto Rontgen Dada (X-Ray) berformat JPG, PNG, atau JPEG.
- Klik Analisis Diagnosis (Penggantian model pertama kali memakan waktu ±3 detik).
""")
# KOLOM OUTPUT (KANAN)
with gr.Column(scale=6):
gr.Markdown("### 🩺 Hasil Diagnosis & Visualisasi")
output_text = gr.HTML(label="Diagnostic Report")
output_image = gr.Image(
label="Explainable AI (Grad-CAM)",
height=550,
elem_classes="image-box",
show_label=True,
show_download_button=True
)
# --- FOOTER ---
gr.HTML("""
""")
# --- INTERACTION ---
submit_btn.click(
fn=predict_and_explain,
inputs=[input_image, model_dropdown],
outputs=[output_text, output_image]
)
# Launch
demo.launch()