# ========================================================= # IMPORT LIBRARY # ========================================================= import gradio as gr import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image import cv2 import json import numpy as np from collections import Counter # ========================================================= # CONFIGURATION & LABELS # ========================================================= try: with open('class_labels.json', 'r') as f: class_names = json.load(f) except FileNotFoundError: class_names = [ "Water_Disaster", "Infrastructure", "Earthquake", "Human_Damage", "Urban_Fire", "Wild_Fire", "Land_Slide", "Drought", "Safe_1", "Safe_2", "Safe_3", "Safe_4" ] DANGEROUS_CLASSES = [ "Water_Disaster", "Infrastructure", "Earthquake", "Human_Damage", "Urban_Fire", "Wild_Fire", "Land_Slide", "Drought" ] CLASS_ICONS = { "Water_Disaster": "๐ŸŒŠ", "Infrastructure": "๐Ÿ—๏ธ", "Earthquake": "โšก", "Human_Damage": "๐Ÿšจ", "Urban_Fire": "๐Ÿ”ฅ", "Wild_Fire": "๐ŸŒฒโ€๐Ÿ”ฅ", "Land_Slide": "โ›ฐ๏ธ", "Drought": "โ˜€๏ธ", "Safe_1": "โœ…", "Safe_2": "โœ…", "Safe_3": "โœ…", "Safe_4": "โœ…", } # ========================================================= # LOAD MODEL # ========================================================= def init_resnet50(): model = models.resnet50(weights=None) num_ftrs = model.fc.in_features model.fc = nn.Sequential( nn.Linear(num_ftrs, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, 12) ) try: model.load_state_dict( torch.load('best_model_resnet50.pth', map_location='cpu') ) except Exception as e: print(f"Peringatan Load ResNet50: {e}") model.eval() return model def init_mobilenetv2(): model = models.mobilenet_v2(weights=None) model.classifier[1] = nn.Linear(model.last_channel, 12) try: model.load_state_dict( torch.load('best_model_mobilenetv2.pth', map_location='cpu') ) except Exception as e: print(f"Peringatan Load MobileNetV2: {e}") model.eval() return model models_dict = { "ResNet50": init_resnet50(), "MobileNetV2": init_mobilenetv2() } # ========================================================= # IMAGE PREPROCESSING # ========================================================= transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] ) ]) # ========================================================= # RESULT CARD # ========================================================= def build_result_card(model_name, label, confidence, dangerous): icon = CLASS_ICONS.get(label.split(" ")[0], "๐Ÿ”") if dangerous: status_text = "BERBAHAYA / SENSITIF" status_icon = "โš ๏ธ" accent = "#ef4444" accent_light = "#fca5a5" soft_bg = "rgba(239,68,68,0.08)" border_color = "rgba(239,68,68,0.25)" badge_bg = "rgba(239,68,68,0.15)" desc = "Konten terindikasi mengandung unsur bencana alam atau kerusakan fisik serius. Diperlukan tindakan segera." glow = "rgba(239,68,68,0.2)" else: status_text = "AMAN" status_icon = "โœ…" accent = "#10b981" accent_light = "#6ee7b7" soft_bg = "rgba(16,185,129,0.08)" border_color = "rgba(16,185,129,0.25)" badge_bg = "rgba(16,185,129,0.15)" desc = "Konten visual terdeteksi aman dan tidak menunjukkan indikasi bencana maupun konten sensitif." glow = "rgba(16,185,129,0.2)" bar_width = min(confidence, 100) bar_color = f"linear-gradient(90deg, {accent}, {accent_light})" html = f"""
{icon}

Hasil Klasifikasi

{label}

{model_name}
Tingkat Keyakinan Model {confidence:.1f}%
0% 100%
{status_icon}

STATUS : {status_text}

{desc}

""" return html # ========================================================= # IMAGE PREDICTION # ========================================================= def predict_image(image, model_name): if image is None: return _empty_state("Silakan unggah gambar terlebih dahulu.") if isinstance(image, np.ndarray): image = Image.fromarray(image.astype('uint8'), 'RGB') model = models_dict[model_name] img_tensor = transform(image).unsqueeze(0) with torch.no_grad(): outputs = model(img_tensor) probabilities = torch.nn.functional.softmax(outputs, dim=1)[0] * 100 _, predicted = outputs.max(1) label = class_names[predicted.item()] confidence = probabilities[predicted.item()].item() dangerous = label in DANGEROUS_CLASSES return build_result_card(model_name, label, confidence, dangerous) # ========================================================= # VIDEO PREDICTION # ========================================================= def predict_video(video_path, model_name, progress=gr.Progress()): if video_path is None: return _empty_state("Silakan unggah video terlebih dahulu.") cap = cv2.VideoCapture(video_path) if not cap.isOpened(): return _empty_state("Gagal membuka file video.") total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) fps = cap.get(cv2.CAP_PROP_FPS) frame_interval = max(1, int(fps)) frame_count = 0 predictions = [] model = models_dict[model_name] progress(0, desc="Menganalisis frame video...") while cap.isOpened(): ret, frame = cap.read() if not ret: break if frame_count % frame_interval == 0: frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(frame_rgb) img_tensor = transform(pil_img).unsqueeze(0) with torch.no_grad(): outputs = model(img_tensor) probabilities = torch.nn.functional.softmax(outputs, dim=1)[0] * 100 _, predicted = outputs.max(1) label = class_names[predicted.item()] confidence = probabilities[predicted.item()].item() predictions.append((label, confidence)) frame_count += 1 if total_frames > 0: progress(min(0.99, frame_count / total_frames)) cap.release() if not predictions: return _empty_state("Tidak ada frame valid untuk dianalisis.") labels_only = [p[0] for p in predictions] counter = Counter(labels_only) most_common_label, occurrence = counter.most_common(1)[0] avg_confidence = np.mean([p[1] for p in predictions if p[0] == most_common_label]) dangerous = most_common_label in DANGEROUS_CLASSES return build_result_card( model_name, f"{most_common_label} ({occurrence} frame)", avg_confidence, dangerous ) def _empty_state(msg): return f"""
๐Ÿ”

{msg}

""" # ========================================================= # THEME # ========================================================= ai_theme = gr.themes.Base( primary_hue="emerald", secondary_hue="teal", neutral_hue="slate", font=[gr.themes.GoogleFont("DM Sans"), "Segoe UI", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"], ).set( body_background_fill="#0b1120", body_background_fill_dark="#0b1120", body_text_color="#e2e8f0", body_text_color_dark="#e2e8f0", block_background_fill="#111827", block_background_fill_dark="#111827", block_border_color="#1e293b", block_border_color_dark="#1e293b", block_label_text_color="#94a3b8", block_label_text_color_dark="#94a3b8", block_radius="20px", button_primary_background_fill="linear-gradient(135deg,#10b981,#059669)", button_primary_background_fill_hover="linear-gradient(135deg,#059669,#047857)", button_primary_text_color="#ffffff", button_primary_border_color="transparent", button_large_radius="14px", button_large_padding="14px 28px", input_background_fill="#1e293b", input_background_fill_dark="#1e293b", input_border_color="#334155", input_border_color_dark="#334155", input_placeholder_color="#475569", checkbox_label_background_fill_selected="#10b981", slider_color="#10b981", table_even_background_fill="#1e293b", table_odd_background_fill="#111827", ) # ========================================================= # CSS โ€” Minimal overrides to avoid breaking Gradio internals # ========================================================= custom_css = """ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,600;0,9..40,700;0,9..40,800;0,9..40,900;1,9..40,400&family=JetBrains+Mono:wght@400;700&display=swap'); /* โ”€โ”€ Global โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ footer { visibility: hidden !important; } body, .gradio-container { background: #0b1120 !important; font-family: 'DM Sans', 'Segoe UI', sans-serif !important; min-height: 100vh; } /* Subtle animated grid background */ .gradio-container::before { content: ''; position: fixed; inset: 0; background-image: linear-gradient(rgba(16,185,129,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(16,185,129,0.03) 1px, transparent 1px); background-size: 48px 48px; pointer-events: none; z-index: 0; } /* โ”€โ”€ Main wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .main-box { max-width: 1080px; margin: 0 auto; padding: 24px 20px 48px; position: relative; z-index: 1; } /* โ”€โ”€ Block cards โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .block.gradio-group, .block.gradio-column, .form { background: rgba(17,24,39,0.85) !important; backdrop-filter: blur(20px) !important; border: 1px solid rgba(255,255,255,0.06) !important; border-radius: 20px !important; box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important; } /* โ”€โ”€ Labels โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ label span, .block label > span { color: #94a3b8 !important; font-size: 12px !important; font-weight: 700 !important; letter-spacing: 1.5px !important; text-transform: uppercase !important; } /* โ”€โ”€ Inputs (text, selects) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ input[type="text"], textarea { background: #1e293b !important; color: #e2e8f0 !important; border: 1px solid #334155 !important; border-radius: 12px !important; font-family: 'DM Sans', sans-serif !important; } input[type="text"]:focus, textarea:focus { border-color: #10b981 !important; box-shadow: 0 0 0 3px rgba(16,185,129,0.15) !important; } /* โ”€โ”€ Model Toggle Buttons (Radio styled as pill buttons) โ”€ */ #model-toggle .wrap { display: flex !important; flex-direction: row !important; gap: 12px !important; background: transparent !important; border: none !important; padding: 0 !important; } #model-toggle label { flex: 1 !important; cursor: pointer !important; margin: 0 !important; } /* Hide the actual radio circle */ #model-toggle input[type="radio"] { display: none !important; } /* Style each label as a card button */ #model-toggle .wrap label > span { display: flex !important; align-items: center !important; justify-content: center !important; gap: 10px !important; width: 100% !important; padding: 16px 20px !important; background: rgba(30,41,59,0.8) !important; border: 1px solid rgba(255,255,255,0.08) !important; border-radius: 16px !important; color: #64748b !important; font-size: 15px !important; font-weight: 700 !important; font-family: 'DM Sans', sans-serif !important; letter-spacing: 0.3px !important; text-transform: none !important; transition: all 0.2s ease !important; cursor: pointer !important; } #model-toggle .wrap label > span:hover { background: rgba(16,185,129,0.08) !important; border-color: rgba(16,185,129,0.3) !important; color: #10b981 !important; transform: translateY(-1px) !important; } /* Selected state */ #model-toggle .wrap label:has(input:checked) > span { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; border-color: transparent !important; color: #ffffff !important; box-shadow: 0 6px 20px rgba(16,185,129,0.35) !important; transform: translateY(-1px) !important; } /* โ”€โ”€ Tabs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .tabs > .tab-nav { background: transparent !important; border: none !important; gap: 10px !important; padding: 0 !important; margin-bottom: 16px !important; } .tabs > .tab-nav > button { background: rgba(30,41,59,0.7) !important; border: 1px solid rgba(255,255,255,0.07) !important; color: #64748b !important; border-radius: 14px !important; font-weight: 700 !important; font-size: 14px !important; padding: 12px 24px !important; transition: all 0.2s ease !important; font-family: 'DM Sans', sans-serif !important; letter-spacing: 0.3px !important; } .tabs > .tab-nav > button:hover { background: rgba(16,185,129,0.08) !important; color: #10b981 !important; border-color: rgba(16,185,129,0.25) !important; } .tabs > .tab-nav > button.selected { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: #ffffff !important; border-color: transparent !important; box-shadow: 0 6px 20px rgba(16,185,129,0.35) !important; } /* โ”€โ”€ Primary button โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ button.lg.primary { background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: #fff !important; font-weight: 800 !important; font-size: 15px !important; border: none !important; border-radius: 14px !important; padding: 14px 28px !important; letter-spacing: 0.3px !important; transition: all 0.2s ease !important; box-shadow: 0 6px 20px rgba(16,185,129,0.3) !important; width: 100% !important; font-family: 'DM Sans', sans-serif !important; } button.lg.primary:hover { transform: translateY(-2px) !important; box-shadow: 0 10px 28px rgba(16,185,129,0.45) !important; } button.lg.primary:active { transform: translateY(0) !important; } /* โ”€โ”€ Upload / Image / Video areas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .image-container img, .image-preview img { border-radius: 14px !important; object-fit: contain !important; } .upload-container, .empty.svelte-1occ011, .wrap.svelte-1occ011 { background: rgba(30,41,59,0.5) !important; border: 2px dashed rgba(16,185,129,0.25) !important; border-radius: 18px !important; color: #475569 !important; transition: border-color 0.2s ease !important; } .upload-container:hover, .wrap.svelte-1occ011:hover { border-color: rgba(16,185,129,0.5) !important; } /* โ”€โ”€ Progress bar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .progress-bar, progress { accent-color: #10b981 !important; } /* โ”€โ”€ Scrollbar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: #0f172a; } ::-webkit-scrollbar-thumb { background: #334155; border-radius: 999px; } ::-webkit-scrollbar-thumb:hover { background: #10b981; } /* โ”€โ”€ Remove extra Gradio padding inside tabs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ .tabitem > .gradio-group { margin: 0 !important; } """ # ========================================================= # INTERFACE # ========================================================= with gr.Blocks( theme=ai_theme, title="Disaster Detector AI", css=custom_css ) as demo: with gr.Column(elem_classes="main-box"): # โ”€โ”€โ”€ HEADER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ gr.HTML("""
AI Computer Vision System ยท UDINUS

Disaster & Sensitive Content Detector

Sistem analisis citra berbasis Deep Learning untuk mendeteksi 12 kategori visual bencana alam, kerusakan infrastruktur, dan konten sensitif secara otomatis.

""") # โ”€โ”€โ”€ STATS STRIP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ gr.HTML("""
๐Ÿท๏ธ

12

Kategori

๐Ÿง 

2

Arsitektur

๐Ÿ“ธ

224px

Input Size

๐ŸŽ“

UDINUS

Final Project

""") # โ”€โ”€โ”€ MODEL SELECTOR (Toggle Buttons) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ gr.HTML("""

๐Ÿง   Arsitektur Deep Learning

""") model_selector = gr.Radio( show_label=False, choices=["ResNet50", "MobileNetV2"], value="ResNet50", label="", interactive=True, elem_id="model-toggle", ) # โ”€โ”€โ”€ TABS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with gr.Tabs(elem_classes="tabs"): # โ”€โ”€ IMAGE TAB โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with gr.TabItem("๐Ÿ–ผ๏ธ Pengujian Gambar"): with gr.Row(equal_height=True): with gr.Column(scale=1, min_width=300): image_input = gr.Image( type="numpy", label="Unggah Gambar", height=280, ) btn_image = gr.Button( "๐Ÿ” Analisis Gambar", variant="primary", size="lg", ) with gr.Column(scale=1, min_width=300): image_result = gr.HTML( value=_empty_state("Unggah gambar, lalu klik Analisis.") ) btn_image.click( fn=predict_image, inputs=[image_input, model_selector], outputs=image_result, ) # โ”€โ”€ VIDEO TAB โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ with gr.TabItem("๐ŸŽฅ Pengujian Video"): with gr.Row(equal_height=True): with gr.Column(scale=1, min_width=300): video_input = gr.Video( label="Unggah Video", height=280, ) btn_video = gr.Button( "โšก Analisis Video", variant="primary", size="lg", ) with gr.Column(scale=1, min_width=300): video_result = gr.HTML( value=_empty_state("Unggah video, lalu klik Analisis.") ) btn_video.click( fn=predict_video, inputs=[video_input, model_selector], outputs=video_result, ) # โ”€โ”€โ”€ CATEGORY GUIDE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ gr.HTML("""

Panduan Kategori Klasifikasi

๐ŸŒŠ Water Disaster
๐Ÿ—๏ธ Infrastructure
โšก Earthquake
๐Ÿšจ Human Damage
๐Ÿ”ฅ Urban Fire
๐ŸŒฒ Wild Fire
โ›ฐ๏ธ Land Slide
โ˜€๏ธ Drought
โœ… Safe (1โ€“4)
""") # โ”€โ”€โ”€ FOOTER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ gr.HTML("""
๐ŸŽ“
Disaster Research Team

Final Project โ€” Computer Vision & Image Analysis
Faculty of Computer Science ยท Universitas Dian Nuswantoro (UDINUS), Semarang

""") # ========================================================= # RUN APP # ========================================================= if __name__ == "__main__": demo.queue().launch()