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"""

PneumoScan

Empowering Diagnostics. Leading Medicine. Building the Future.

""") # --- 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:
""") # 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()