|
|
"""
|
|
|
Gradio Interface for Indonesian Herbal Plants Classification
|
|
|
"""
|
|
|
import gradio as gr
|
|
|
import torch
|
|
|
import torch.nn.functional as F
|
|
|
from torchvision import transforms
|
|
|
from PIL import Image
|
|
|
import numpy as np
|
|
|
import json
|
|
|
from pathlib import Path
|
|
|
import sys
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
|
|
import config
|
|
|
from models import get_model
|
|
|
|
|
|
|
|
|
class HerbalClassifier:
|
|
|
"""Inference class for herbal plant classification"""
|
|
|
|
|
|
def __init__(self, model_name: str = None):
|
|
|
self.device = config.DEVICE
|
|
|
self.class_names = self._load_class_names()
|
|
|
self.num_classes = len(self.class_names)
|
|
|
|
|
|
|
|
|
if model_name is None:
|
|
|
model_name = self._find_best_model()
|
|
|
|
|
|
self.model_name = model_name
|
|
|
self.model = self._load_model(model_name)
|
|
|
|
|
|
|
|
|
self.transform = transforms.Compose([
|
|
|
transforms.Resize((config.IMAGE_SIZE, config.IMAGE_SIZE)),
|
|
|
transforms.ToTensor(),
|
|
|
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
|
|
])
|
|
|
|
|
|
def _load_class_names(self):
|
|
|
"""Load class names from JSON"""
|
|
|
class_names_path = config.OUTPUT_DIR / "class_names.json"
|
|
|
if class_names_path.exists():
|
|
|
with open(class_names_path, 'r') as f:
|
|
|
return json.load(f)
|
|
|
else:
|
|
|
|
|
|
return sorted([d.name for d in config.DATA_DIR.iterdir() if d.is_dir()])
|
|
|
|
|
|
def _find_best_model(self):
|
|
|
"""Find the best performing model"""
|
|
|
results_path = config.OUTPUT_DIR / "training_results.json"
|
|
|
if results_path.exists():
|
|
|
with open(results_path, 'r') as f:
|
|
|
results = json.load(f)
|
|
|
best_model = max(results.items(), key=lambda x: x[1].get('best_val_acc', 0))
|
|
|
return best_model[0]
|
|
|
return config.MODEL_NAMES[0]
|
|
|
|
|
|
def _load_model(self, model_name: str):
|
|
|
"""Load a trained model"""
|
|
|
model_path = config.MODELS_DIR / f"{model_name.lower()}.pth"
|
|
|
|
|
|
if not model_path.exists():
|
|
|
raise FileNotFoundError(f"Model not found: {model_path}")
|
|
|
|
|
|
checkpoint = torch.load(model_path, map_location=self.device)
|
|
|
model = get_model(model_name, self.num_classes, pretrained=False)
|
|
|
model.load_state_dict(checkpoint['model_state_dict'])
|
|
|
model = model.to(self.device)
|
|
|
model.eval()
|
|
|
|
|
|
print(f"Loaded model: {model_name}")
|
|
|
print(f"Best validation accuracy: {checkpoint.get('best_val_acc', 'N/A')}")
|
|
|
|
|
|
return model
|
|
|
|
|
|
@torch.no_grad()
|
|
|
def predict(self, image: Image.Image) -> tuple:
|
|
|
"""
|
|
|
Predict class for a single image
|
|
|
Returns: (predicted_class, confidence, all_probabilities)
|
|
|
"""
|
|
|
|
|
|
if image.mode != 'RGB':
|
|
|
image = image.convert('RGB')
|
|
|
|
|
|
input_tensor = self.transform(image).unsqueeze(0).to(self.device)
|
|
|
|
|
|
|
|
|
outputs = self.model(input_tensor)
|
|
|
probabilities = F.softmax(outputs, dim=1)[0]
|
|
|
|
|
|
|
|
|
confidence, predicted_idx = torch.max(probabilities, 0)
|
|
|
predicted_class = self.class_names[predicted_idx.item()]
|
|
|
|
|
|
|
|
|
prob_dict = {
|
|
|
self.class_names[i]: float(probabilities[i])
|
|
|
for i in range(self.num_classes)
|
|
|
}
|
|
|
|
|
|
return predicted_class, float(confidence), prob_dict
|
|
|
|
|
|
|
|
|
|
|
|
classifier = None
|
|
|
|
|
|
|
|
|
def load_classifier(model_name: str = None):
|
|
|
"""Load or reload classifier"""
|
|
|
global classifier
|
|
|
classifier = HerbalClassifier(model_name)
|
|
|
return f"β
Loaded model: {classifier.model_name}"
|
|
|
|
|
|
|
|
|
def classify_image(image, model_name: str):
|
|
|
"""Classify an uploaded image"""
|
|
|
global classifier
|
|
|
|
|
|
if image is None:
|
|
|
return "Please upload an image", {}, ""
|
|
|
|
|
|
|
|
|
if classifier is None or (model_name and classifier.model_name != model_name):
|
|
|
try:
|
|
|
load_classifier(model_name if model_name else None)
|
|
|
except Exception as e:
|
|
|
return f"Error loading model: {str(e)}", {}, ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if isinstance(image, np.ndarray):
|
|
|
image = Image.fromarray(image)
|
|
|
|
|
|
|
|
|
predicted_class, confidence, prob_dict = classifier.predict(image)
|
|
|
|
|
|
|
|
|
result_text = f"""
|
|
|
πΏ **Predicted Plant: {predicted_class.upper()}**
|
|
|
π **Confidence: {confidence * 100:.2f}%**
|
|
|
|
|
|
Model used: {classifier.model_name}
|
|
|
"""
|
|
|
|
|
|
|
|
|
sorted_probs = dict(sorted(prob_dict.items(), key=lambda x: x[1], reverse=True)[:5])
|
|
|
|
|
|
|
|
|
herbal_info = get_herbal_info(predicted_class)
|
|
|
|
|
|
return result_text, sorted_probs, herbal_info
|
|
|
|
|
|
except Exception as e:
|
|
|
return f"Error during prediction: {str(e)}", {}, ""
|
|
|
|
|
|
|
|
|
def get_herbal_info(plant_name: str) -> str:
|
|
|
"""Get information about the herbal plant"""
|
|
|
herbal_database = {
|
|
|
"jahe": "π± **Jahe (Zingiber officinale)**\n- Manfaat: Mengatasi mual, radang sendi, nyeri otot\n- Penggunaan: Minuman hangat, bumbu masakan, obat tradisional",
|
|
|
"kunyit": "π± **Kunyit (Curcuma longa)**\n- Manfaat: Anti-inflamasi, antioksidan, kesehatan pencernaan\n- Penggunaan: Jamu kunyit asam, bumbu kari, obat maag",
|
|
|
"kencur": "π± **Kencur (Kaempferia galanga)**\n- Manfaat: Mengatasi batuk, penambah nafsu makan\n- Penggunaan: Beras kencur, bumbu masakan",
|
|
|
"lengkuas": "π± **Lengkuas (Alpinia galanga)**\n- Manfaat: Antibakteri, mengatasi masalah pencernaan\n- Penggunaan: Bumbu masakan, obat tradisional",
|
|
|
"temulawak": "π± **Temulawak (Curcuma xanthorrhiza)**\n- Manfaat: Kesehatan hati, meningkatkan nafsu makan\n- Penggunaan: Jamu temulawak, suplemen kesehatan",
|
|
|
"serai": "π± **Serai (Cymbopogon citratus)**\n- Manfaat: Relaksasi, mengurangi kembung, pengusir nyamuk\n- Penggunaan: Teh serai, bumbu masakan, aromaterapi",
|
|
|
"daun salam": "π± **Daun Salam (Syzygium polyanthum)**\n- Manfaat: Menurunkan kolesterol, mengontrol gula darah\n- Penggunaan: Bumbu masakan, air rebusan",
|
|
|
"cengkeh": "π± **Cengkeh (Syzygium aromaticum)**\n- Manfaat: Pereda nyeri gigi, antiseptik, meningkatkan imunitas\n- Penggunaan: Bumbu masakan, rokok kretek, obat sakit gigi",
|
|
|
"kayu manis": "π± **Kayu Manis (Cinnamomum verum)**\n- Manfaat: Mengontrol gula darah, antioksidan\n- Penggunaan: Minuman hangat, bumbu kue, jamu",
|
|
|
"pala": "π± **Pala (Myristica fragrans)**\n- Manfaat: Membantu tidur, mengurangi nyeri\n- Penggunaan: Bumbu masakan, minuman hangat",
|
|
|
"lada": "π± **Lada (Piper nigrum)**\n- Manfaat: Meningkatkan pencernaan, antioksidan\n- Penggunaan: Bumbu masakan, pengobatan tradisional",
|
|
|
"daun kemangi": "π± **Kemangi (Ocimum basilicum)**\n- Manfaat: Menyegarkan mulut, melancarkan pencernaan\n- Penggunaan: Lalapan, bumbu masakan",
|
|
|
"bawang putih": "π± **Bawang Putih (Allium sativum)**\n- Manfaat: Antibakteri, menurunkan tekanan darah\n- Penggunaan: Bumbu masakan, obat tradisional",
|
|
|
"bawang merah": "π± **Bawang Merah (Allium cepa)**\n- Manfaat: Menurunkan gula darah, antibakteri\n- Penggunaan: Bumbu masakan, obat tradisional",
|
|
|
"kemiri": "π± **Kemiri (Aleurites moluccanus)**\n- Manfaat: Kesehatan rambut, sumber energi\n- Penggunaan: Bumbu masakan, minyak rambut",
|
|
|
}
|
|
|
|
|
|
plant_lower = plant_name.lower()
|
|
|
if plant_lower in herbal_database:
|
|
|
return herbal_database[plant_lower]
|
|
|
|
|
|
return f"π± **{plant_name.title()}**\n- Informasi detail belum tersedia dalam database\n- Tanaman ini termasuk rempah-rempah Indonesia"
|
|
|
|
|
|
|
|
|
def get_available_models():
|
|
|
"""Get list of available trained models"""
|
|
|
models = []
|
|
|
for model_name in config.MODEL_NAMES:
|
|
|
model_path = config.MODELS_DIR / f"{model_name.lower()}.pth"
|
|
|
if model_path.exists():
|
|
|
models.append(model_name)
|
|
|
return models
|
|
|
|
|
|
|
|
|
def create_gradio_interface():
|
|
|
"""Create Gradio interface"""
|
|
|
|
|
|
available_models = get_available_models()
|
|
|
if not available_models:
|
|
|
available_models = config.MODEL_NAMES
|
|
|
|
|
|
|
|
|
custom_css = """
|
|
|
.gradio-container {
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
}
|
|
|
.main-title {
|
|
|
text-align: center;
|
|
|
color: #2e7d32;
|
|
|
}
|
|
|
"""
|
|
|
|
|
|
demo = gr.Blocks(css=custom_css, title="Indonesian Herbal Plants Classifier")
|
|
|
|
|
|
with demo:
|
|
|
gr.Markdown("""
|
|
|
# πΏ Indonesian Herbal Plants Classification
|
|
|
### Klasifikasi 31 Jenis Tanaman Herbal Indonesia menggunakan Deep Learning
|
|
|
|
|
|
Upload gambar tanaman herbal dan sistem akan mengidentifikasi jenisnya beserta informasi khasiatnya.
|
|
|
""")
|
|
|
|
|
|
with gr.Row():
|
|
|
with gr.Column(scale=1):
|
|
|
input_image = gr.Image(
|
|
|
label="π· Upload Gambar Tanaman",
|
|
|
type="pil",
|
|
|
height=300
|
|
|
)
|
|
|
|
|
|
model_dropdown = gr.Dropdown(
|
|
|
choices=available_models,
|
|
|
value=available_models[0] if available_models else None,
|
|
|
label="π€ Pilih Model",
|
|
|
info="Pilih model deep learning untuk klasifikasi"
|
|
|
)
|
|
|
|
|
|
classify_btn = gr.Button("π Identifikasi Tanaman", variant="primary", size="lg")
|
|
|
|
|
|
gr.Markdown("""
|
|
|
### π Daftar Tanaman yang Dapat Dikenali:
|
|
|
adas, andaliman, asam jawa, bawang bombai, bawang merah, bawang putih,
|
|
|
biji ketumbar, bunga lawang, cengkeh, daun jeruk, daun kemangi, daun ketumbar,
|
|
|
daun salam, jahe, jinten, kapulaga, kayu manis, kayu secang, kemiri, kemukus,
|
|
|
kencur, kluwek, kunyit, lada, lengkuas, pala, saffron, serai, vanili, wijen
|
|
|
""")
|
|
|
|
|
|
with gr.Column(scale=1):
|
|
|
result_text = gr.Markdown(label="π Hasil Prediksi")
|
|
|
|
|
|
confidence_plot = gr.Label(
|
|
|
label="π Top 5 Probabilitas",
|
|
|
num_top_classes=5
|
|
|
)
|
|
|
|
|
|
herbal_info = gr.Markdown(label="π± Informasi Tanaman")
|
|
|
|
|
|
|
|
|
gr.Markdown("### πΈ Contoh Gambar")
|
|
|
gr.Examples(
|
|
|
examples=[
|
|
|
["examples/jahe.jpg"],
|
|
|
["examples/kunyit.jpg"],
|
|
|
["examples/lengkuas.jpg"],
|
|
|
] if Path("examples").exists() else [],
|
|
|
inputs=input_image,
|
|
|
label="Klik untuk mencoba"
|
|
|
)
|
|
|
|
|
|
|
|
|
classify_btn.click(
|
|
|
fn=classify_image,
|
|
|
inputs=[input_image, model_dropdown],
|
|
|
outputs=[result_text, confidence_plot, herbal_info]
|
|
|
)
|
|
|
|
|
|
gr.Markdown("""
|
|
|
---
|
|
|
### π Tentang Aplikasi
|
|
|
- **5 Model Deep Learning SOTA**: YOLOv11 (95.08%), EfficientNetV2 (95.08%), ConvFormer (94.77%), ConvNeXtV2 (93.95%), InternImage (89.86%)
|
|
|
- **31 Kelas Tanaman**: Rempah dan tanaman herbal Indonesia
|
|
|
- **Dataset**: Indonesian Spices Dataset (6,510 gambar, perfectly balanced)
|
|
|
- **Training**: 10 epochs, Mixed Precision (AMP), AdamW + OneCycleLR
|
|
|
|
|
|
π Best Models: **EfficientNetV2-S & YOLOv11-cls** (95.08% accuracy)
|
|
|
|
|
|
Made with β€οΈ for Indonesian Herbal Heritage | Powered by Claude Sonnet 4.5
|
|
|
""")
|
|
|
|
|
|
return demo
|
|
|
|
|
|
|
|
|
|
|
|
demo = create_gradio_interface()
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
demo.launch(
|
|
|
share=True,
|
|
|
server_name="0.0.0.0",
|
|
|
server_port=7860,
|
|
|
show_error=True
|
|
|
)
|
|
|
|