import torch
import gradio as gr
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
import torchvision.transforms as transforms
from torchvision.models import resnet50
# ============================================================
# CONFIG
# ============================================================
MODEL_PATH = r"C:\Users\LOQ\Desktop\Oral Diseases Image Classification\checkpoints\best_model.pth"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
# ============================================================
# LOAD MODEL
# ============================================================
checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)
CLASS_NAMES = checkpoint["class_names"]
TEST_F1 = checkpoint["test_f1"]
model = resnet50(weights=None)
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(model.fc.in_features, len(CLASS_NAMES))
)
model.load_state_dict(checkpoint["state_dict"])
model.to(DEVICE)
model.eval()
eval_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
# ============================================================
# INFERENCE
# ============================================================
def predict(image):
if image is None:
return (
gr.update(value="—", visible=True),
"—",
{},
gr.update(visible=False),
)
image = image.convert("RGB")
tensor = eval_transform(image).unsqueeze(0).to(DEVICE)
with torch.no_grad():
output = model(tensor)
probs = F.softmax(output, dim=1)[0]
index = torch.argmax(probs).item()
prediction = CLASS_NAMES[index]
confidence = probs[index].item() * 100
results = {CLASS_NAMES[i]: float(probs[i]) for i in range(len(CLASS_NAMES))}
# Build a small verdict badge depending on confidence level
if confidence >= 85:
badge = f'
✓ High Confidence — {confidence:.1f}%
'
elif confidence >= 60:
badge = f'! Moderate Confidence — {confidence:.1f}%
'
else:
badge = f'? Low Confidence — {confidence:.1f}%
'
return (
prediction,
f"{confidence:.2f}%",
results,
gr.update(value=badge, visible=True),
)
def clear_all():
return None, "—", "—", {}, gr.update(visible=False)
# ============================================================
# STYLING
# ============================================================
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500&display=swap');
:root {
--bg-primary: #0a0e1a;
--bg-secondary: #10162a;
--bg-card: #131a30;
--border-subtle: #232b45;
--accent: #14b8a6;
--accent-soft: #14b8a622;
--accent-2: #6366f1;
--text-primary: #e8ecf5;
--text-secondary: #8892b0;
--text-muted: #5b6485;
--radius: 14px;
}
* { font-family: 'Inter', sans-serif !important; }
.gradio-container {
background: radial-gradient(circle at 10% 0%, #101a33 0%, #080b14 55%, #05070d 100%) !important;
max-width: 1180px !important;
margin: 0 auto !important;
}
footer { display: none !important; }
/* ---------- HEADER ---------- */
.app-header {
padding: 30px 8px 22px 8px;
border-bottom: 1px solid var(--border-subtle);
margin-bottom: 26px;
display: flex;
align-items: center;
justify-content: space-between;
}
.app-header .brand {
display: flex;
align-items: center;
gap: 14px;
}
.app-header .logo-badge {
width: 46px;
height: 46px;
border-radius: 12px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
box-shadow: 0 8px 24px -6px #14b8a655;
flex-shrink: 0;
}
.app-header h1 {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
letter-spacing: -0.02em;
}
.app-header p {
font-size: 13px;
color: var(--text-muted);
margin: 2px 0 0 0;
}
.app-header .tag {
font-size: 11px;
font-weight: 600;
color: var(--accent);
background: var(--accent-soft);
border: 1px solid #14b8a640;
padding: 6px 14px;
border-radius: 999px;
letter-spacing: 0.03em;
text-transform: uppercase;
}
/* ---------- CARDS ---------- */
.card {
background: var(--bg-card) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: var(--radius) !important;
padding: 18px !important;
}
.card-title {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.card-title::before {
content: "";
width: 4px;
height: 14px;
background: var(--accent);
border-radius: 2px;
display: inline-block;
}
/* ---------- UPLOAD ZONE ---------- */
.upload-zone, .upload-zone > div {
background: var(--bg-card) !important;
border: 1.5px dashed #2b3454 !important;
border-radius: var(--radius) !important;
}
.upload-zone:hover {
border-color: var(--accent) !important;
}
/* ---------- BUTTONS ---------- */
#analyze-btn {
background: linear-gradient(135deg, #14b8a6, #0d9488) !important;
color: #05170f !important;
font-weight: 700 !important;
border: none !important;
border-radius: 10px !important;
box-shadow: 0 10px 24px -8px #14b8a670 !important;
letter-spacing: 0.01em;
transition: transform .15s ease, box-shadow .15s ease;
}
#analyze-btn:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px -8px #14b8a690 !important;
}
#clear-btn {
background: transparent !important;
color: var(--text-secondary) !important;
border: 1px solid var(--border-subtle) !important;
border-radius: 10px !important;
}
#clear-btn:hover {
border-color: #3a4468 !important;
color: var(--text-primary) !important;
}
/* ---------- RESULT FIELDS ---------- */
#pred-box textarea, #conf-box textarea {
background: #0d1326 !important;
border: 1px solid var(--border-subtle) !important;
color: var(--text-primary) !important;
font-weight: 700 !important;
font-size: 17px !important;
border-radius: 10px !important;
}
#conf-box textarea {
color: var(--accent) !important;
font-family: 'JetBrains Mono', monospace !important;
}
label span {
color: var(--text-muted) !important;
font-size: 11.5px !important;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600 !important;
}
/* ---------- VERDICT BADGE ---------- */
.verdict {
padding: 10px 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
text-align: center;
margin-bottom: 14px;
border: 1px solid transparent;
}
.verdict-high { background: #14b8a61a; color: #2dd4bf; border-color: #14b8a640; }
.verdict-mid { background: #f59e0b1a; color: #fbbf24; border-color: #f59e0b40; }
.verdict-low { background: #ef44441a; color: #f87171; border-color: #ef444440; }
/* ---------- PROBABILITY BARS (gr.Label) ---------- */
.label-wrap {
background: transparent !important;
border: none !important;
}
#prob-label .container {
background: transparent !important;
}
/* ---------- FOOTER ---------- */
.app-footer {
margin-top: 30px;
padding: 18px 4px 10px 4px;
border-top: 1px solid var(--border-subtle);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.app-footer .meta {
font-size: 12px;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
.app-footer .meta b { color: var(--text-secondary); }
.app-footer .credit {
font-size: 12px;
color: var(--text-muted);
}
.app-footer .credit b { color: var(--text-secondary); }
.disclaimer {
font-size: 11.5px;
color: var(--text-muted);
background: #0d132666;
border: 1px solid var(--border-subtle);
border-radius: 10px;
padding: 10px 14px;
margin-top: 16px;
line-height: 1.6;
}
"""
# ============================================================
# UI
# ============================================================
with gr.Blocks(
theme=gr.themes.Soft(primary_hue="teal", secondary_hue="slate"),
css=CUSTOM_CSS,
title="Oral Disease Classifier"
) as demo:
gr.HTML(
f"""
"""
)
with gr.Row(equal_height=True):
with gr.Column(scale=5):
gr.HTML('Input Image
')
image_input = gr.Image(
type="pil",
label="",
show_label=False,
elem_classes="upload-zone",
height=340,
)
with gr.Row():
clear_btn = gr.Button("Clear", elem_id="clear-btn")
button = gr.Button("Analyze Image", elem_id="analyze-btn")
gr.HTML(
"""
⚠ Decision-support tool only. Predictions are generated by an automated
model and are not a substitute for professional clinical diagnosis.
"""
)
with gr.Column(scale=5):
gr.HTML('Analysis Result
')
verdict_html = gr.HTML(visible=False)
with gr.Row():
prediction = gr.Textbox(label="Predicted Class", elem_id="pred-box", interactive=False)
confidence = gr.Textbox(label="Confidence", elem_id="conf-box", interactive=False)
gr.HTML('Class Probability Distribution
')
probabilities = gr.Label(
label="",
show_label=False,
elem_id="prob-label",
num_top_classes=len(CLASS_NAMES),
)
gr.HTML(
f"""
"""
)
button.click(
predict,
inputs=image_input,
outputs=[prediction, confidence, probabilities, verdict_html],
)
clear_btn.click(
clear_all,
inputs=None,
outputs=[image_input, prediction, confidence, probabilities, verdict_html],
)
if __name__ == "__main__":
demo.launch()