"""
Batik Classification Web App - Gradio
Upload gambar batik dan model akan mendeteksi motifnya!
"""
import gradio as gr
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image
import json
import numpy as np
import os
# Setup
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Loading model on {device}...")
# Load config
with open('model_config_final.json', 'r') as f:
config = json.load(f)
num_classes = config['num_classes']
class_names = config['class_names']
# Build model
vgg16 = models.vgg16(pretrained=False)
num_features = vgg16.classifier[0].in_features
vgg16.classifier = nn.Sequential(
nn.Linear(num_features, 4096),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(4096, num_classes)
)
# Load weights
checkpoint = torch.load('vgg16_batik_best.pth', map_location=device)
if 'model_state_dict' in checkpoint:
vgg16.load_state_dict(checkpoint['model_state_dict'])
best_acc = checkpoint.get('best_acc', 0)
else:
vgg16.load_state_dict(checkpoint)
best_acc = 0
vgg16.to(device)
vgg16.eval()
# Transforms
transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
print("Model loaded successfully!")
print(f"Classes: {num_classes}")
if best_acc > 0:
print(f"Model accuracy: {best_acc:.2f}%")
def predict_batik(image):
"""Predict batik motif from uploaded image"""
if image is None:
return None, """
📤 Upload gambar terlebih dahulu
Silakan upload atau drag & drop gambar batik
"""
# Convert to PIL if needed
if not isinstance(image, Image.Image):
image = Image.fromarray(image)
# Preprocess
image_rgb = image.convert('RGB')
input_tensor = transform(image_rgb).unsqueeze(0).to(device)
# Predict
with torch.no_grad():
outputs = vgg16(input_tensor)
probabilities = torch.nn.functional.softmax(outputs, dim=1)
confidence, predicted = torch.max(probabilities, 1)
# Get top 10 predictions
topk_prob, topk_idx = torch.topk(probabilities, min(10, len(class_names)))
# Format results for Gradio
predicted_class = class_names[predicted.item()]
confidence_score = confidence.item() * 100
# Create label dict for top predictions
predictions_dict = {
class_names[idx.item()]: prob.item()
for idx, prob in zip(topk_idx[0], topk_prob[0])
}
# Extract region and pattern
region = "Unknown"
pattern = predicted_class
if '_' in predicted_class:
parts = predicted_class.split('_', 1)
region = parts[0]
pattern = parts[1]
# Confidence interpretation
if confidence_score >= 90:
confidence_emoji = "🎯"
confidence_text = "SANGAT YAKIN"
confidence_color = "#10b981"
confidence_desc = "Prediksi sangat akurat dan dapat dipercaya!"
elif confidence_score >= 70:
confidence_emoji = "✅"
confidence_text = "CUKUP YAKIN"
confidence_color = "#3b82f6"
confidence_desc = "Prediksi cukup akurat"
else:
confidence_emoji = "⚠️"
confidence_text = "KURANG YAKIN"
confidence_color = "#f59e0b"
confidence_desc = "Gambar mungkin blur atau motif tidak umum"
# Create beautiful result HTML
result_html = f"""
{confidence_emoji}
{predicted_class}
Confidence Score
{confidence_score:.2f}%
{confidence_text}
💡 Info Batik
Motif {pattern} berasal dari daerah {region}.
Batik ini merupakan bagian dari warisan budaya Indonesia yang kaya dan beragam.
"""
return predictions_dict, result_html
# Custom CSS for beautiful UI
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
* {
font-family: 'Poppins', sans-serif !important;
}
.gradio-container {
max-width: 1400px !important;
margin: auto;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.main-header {
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3rem 2rem;
border-radius: 20px;
margin-bottom: 2rem;
box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4);
animation: slideDown 0.6s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.main-header h1 {
font-size: 2.8rem !important;
font-weight: 700 !important;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
/* Card styling */
.card {
background: white;
border-radius: 20px;
padding: 2rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 50px rgba(0,0,0,0.15);
}
/* Upload area */
.image-container {
background: white;
border-radius: 20px;
padding: 1.5rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.image-container img {
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
/* Button styling */
button.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border: none !important;
padding: 1rem 2rem !important;
font-size: 1.1rem !important;
font-weight: 600 !important;
border-radius: 50px !important;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4) !important;
transition: all 0.3s ease !important;
text-transform: uppercase;
letter-spacing: 1px;
}
button.primary:hover {
transform: translateY(-3px) !important;
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.6) !important;
}
/* Label styling */
.label-container {
background: white;
border-radius: 20px;
padding: 1.5rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.label-item {
padding: 0.8rem;
border-radius: 10px;
margin-bottom: 0.5rem;
background: linear-gradient(90deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
transition: all 0.3s ease;
}
.label-item:hover {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
transform: translateX(5px);
}
/* Info boxes */
.info-box {
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
border-left: 4px solid #667eea;
padding: 1.5rem;
border-radius: 15px;
margin: 1rem 0;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.tip-box {
background: linear-gradient(135deg, #f093fb15 0%, #f5576c15 100%);
border-left: 4px solid #f093fb;
padding: 1rem;
border-radius: 10px;
margin: 0.5rem 0;
}
/* Markdown content styling */
.markdown-text h3 {
color: #667eea;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.markdown-text ul {
list-style: none;
padding-left: 0;
}
.markdown-text li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.markdown-text li:before {
content: "✓";
color: #667eea;
font-weight: bold;
position: absolute;
left: 0;
}
/* Footer */
footer {
text-align: center;
margin-top: 3rem;
padding: 2rem;
background: white;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
/* Examples styling */
.examples {
margin-top: 2rem;
}
.example-image {
border-radius: 15px;
transition: transform 0.3s ease;
cursor: pointer;
}
.example-image:hover {
transform: scale(1.05);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.6s ease-out;
}
"""
# Example images (if exist)
example_images = []
example_folder = "data/test"
if os.path.exists(example_folder):
# Get first image from each class (max 6 examples)
for class_name in class_names[:6]:
class_path = os.path.join(example_folder, class_name)
if os.path.exists(class_path):
images = [f for f in os.listdir(class_path) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
if images:
example_images.append(os.path.join(class_path, images[0]))
# Build Gradio interface
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(
primary_hue="purple",
secondary_hue="blue",
neutral_hue="gray",
font=["Poppins", "sans-serif"]
)) as demo:
# Header
accuracy_text = f"• Accuracy: {best_acc:.1f}%" if best_acc > 0 else ""
gr.HTML(
f"""
🎨 Batik Nusantara Classification
Deteksi Motif Batik Indonesia dengan Kecerdasan Buatan
📚 {num_classes} Motif Batik
🤖 VGG16 Deep Learning
{f'🎯 {best_acc:.1f}% Accuracy' if best_acc > 0 else ''}
"""
)
with gr.Row():
with gr.Column(scale=1):
gr.HTML("""
📤 Upload Gambar Batik
""")
image_input = gr.Image(
label="",
type="pil",
height=400,
elem_classes="image-container"
)
predict_btn = gr.Button(
"🔍 Deteksi Motif Batik",
variant="primary",
size="lg",
elem_classes="primary"
)
gr.HTML("""
💡 Tips Terbaik:
- Upload gambar batik yang jelas dan fokus
- Format: JPG, PNG, atau GIF
- Resolusi tinggi untuk hasil optimal
- Hindari gambar blur atau terlalu gelap
""")
with gr.Column(scale=1):
gr.HTML("""
📊 Hasil Prediksi
""")
result_html = gr.HTML(
"""
🎨
Siap Mendeteksi!
Upload gambar batik dan klik tombol "Deteksi Motif Batik"
""",
elem_classes="card"
)
gr.HTML("""
📈 Top 10 Predictions
""")
predictions_output = gr.Label(
label="",
num_top_classes=10,
elem_classes="label-container"
)
# Examples section
if example_images:
gr.HTML("""
💡 Contoh Gambar Batik
""")
gr.Examples(
examples=example_images,
inputs=image_input,
outputs=[predictions_output, result_html],
fn=predict_batik,
cache_examples=False
)
# Info section
gr.HTML(f"""
📚 Tentang Motif Batik
Model ini dapat mengenali **{num_classes} motif batik** dari berbagai daerah di Indonesia:
- **Jawa Tengah**: Truntum, Parang, Kawung, Semarangan, dll
- **Jawa Timur**: Gentongan, Pring, dll
- **Bali**: Barong, Merak
- **Papua**: Asmat, Cendrawasih, Tifa
- **Sumatra**: Boraspati, Rumah Minang
- **Kalimantan**: Dayak, Insang
- Dan banyak lagi...
### 🎯 Cara Menggunakan
1. Upload gambar batik atau drag & drop ke area upload
2. Klik tombol "Deteksi Motif Batik"
3. Lihat hasil prediksi dengan confidence score
4. Cek Top 10 predictions untuk alternatif motif yang mirip
### 📊 Interpretasi Hasil
- **>90%**: Model sangat yakin dengan prediksi
- **70-90%**: Model cukup yakin
- **<70%**: Model kurang yakin (gambar mungkin blur atau motif tidak umum)
""")
# Footer
gr.Markdown("""
---
🇮🇩 Batik Classification Model • VGG16 Architecture • PyTorch
Preserving Indonesian Cultural Heritage through AI
""")
# Button action
predict_btn.click(
fn=predict_batik,
inputs=image_input,
outputs=[predictions_output, result_html]
)
# Launch app
if __name__ == "__main__":
print("\n" + "="*80)
print("Starting Gradio Web App...")
print("="*80)
demo.launch(
share=False, # Set True untuk public link
server_name="0.0.0.0", # Accessible dari network
server_port=7860,
show_error=True,
inbrowser=True # Auto open browser
)