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