rneuralnets's picture
Upload 5 files
ea6c640 verified
Raw
History Blame Contribute Delete
22 kB
import streamlit as st
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image
import numpy as np
# ── Page configuration ──────────────────────────────────
st.set_page_config(
page_title="DermaScan AI",
page_icon="πŸ”¬",
layout="wide",
initial_sidebar_state="expanded"
)
# ── Custom CSS styling ──────────────────────────────────
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap');
:root {
--bg-base: #0A0E1A;
--bg-surface: #111827;
--bg-elevated: #1A2235;
--border: rgba(99,179,237,0.12);
--border-bright: rgba(99,179,237,0.35);
--accent-cyan: #63B3ED;
--accent-teal: #4FD1C5;
--accent-green: #68D391;
--accent-amber: #F6AD55;
--accent-red: #FC8181;
--text-primary: #EDF2F7;
--text-secondary:#A0AEC0;
--text-muted: #4A5568;
}
html, body, [data-testid="stAppViewContainer"] {
background: var(--bg-base) !important;
font-family: 'DM Sans', sans-serif;
color: var(--text-primary);
}
[data-testid="stSidebar"] {
background: var(--bg-surface) !important;
border-right: 1px solid var(--border) !important;
}
[data-testid="stSidebar"] * {
color: var(--text-primary) !important;
}
/* Hide default streamlit chrome */
#MainMenu, footer, header { visibility: hidden; }
[data-testid="stDecoration"] { display: none; }
/* ── Hero banner ── */
.hero {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 2.2rem 2.5rem;
background: linear-gradient(135deg, #0D1B2E 0%, #112240 60%, #0D2137 100%);
border: 1px solid var(--border-bright);
border-radius: 16px;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -40px; right: -40px;
width: 220px; height: 220px;
background: radial-gradient(circle, rgba(99,179,237,0.12) 0%, transparent 70%);
border-radius: 50%;
}
.hero-icon {
font-size: 3rem;
line-height: 1;
filter: drop-shadow(0 0 12px rgba(99,179,237,0.5));
}
.hero-text h1 {
font-family: 'Syne', sans-serif;
font-size: 2rem;
font-weight: 800;
color: var(--text-primary);
margin: 0 0 0.2rem 0;
letter-spacing: -0.5px;
}
.hero-text p {
font-size: 0.95rem;
color: var(--text-secondary);
margin: 0;
font-weight: 300;
letter-spacing: 0.02em;
}
.hero-badge {
margin-left: auto;
background: rgba(99,179,237,0.1);
border: 1px solid var(--border-bright);
color: var(--accent-cyan);
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
/* ── Upload zone ── */
.upload-label {
font-family: 'Syne', sans-serif;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent-cyan);
margin-bottom: 0.5rem;
}
[data-testid="stFileUploader"] {
background: var(--bg-elevated) !important;
border: 1.5px dashed var(--border-bright) !important;
border-radius: 12px !important;
padding: 0.5rem !important;
transition: border-color 0.2s;
}
[data-testid="stFileUploader"]:hover {
border-color: var(--accent-cyan) !important;
}
[data-testid="stFileUploader"] * { color: var(--text-secondary) !important; }
/* ── Section headers ── */
.section-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 1rem;
}
.section-header span.icon { font-size: 1rem; }
.section-header span.label {
font-family: 'Syne', sans-serif;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--accent-cyan);
}
.section-divider {
flex: 1;
height: 1px;
background: var(--border);
margin-left: 0.5rem;
}
/* ── Image panel ── */
.img-panel {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.2rem;
overflow: hidden;
}
.img-panel img {
border-radius: 10px !important;
width: 100% !important;
}
.img-meta {
display: flex;
justify-content: space-between;
margin-top: 0.8rem;
padding-top: 0.8rem;
border-top: 1px solid var(--border);
}
.img-meta-item {
text-align: center;
}
.img-meta-item .val {
font-family: 'Syne', sans-serif;
font-size: 0.9rem;
font-weight: 700;
color: var(--text-primary);
}
.img-meta-item .key {
font-size: 0.7rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ── Primary diagnosis card ── */
.diagnosis-card {
background: linear-gradient(135deg, #0D1B2E 0%, #0D2137 100%);
border: 1px solid var(--border-bright);
border-radius: 14px;
padding: 1.5rem 1.8rem;
margin-bottom: 1.2rem;
position: relative;
overflow: hidden;
}
.diagnosis-card::after {
content: '';
position: absolute;
bottom: -30px; right: -30px;
width: 120px; height: 120px;
background: radial-gradient(circle, rgba(79,209,197,0.1) 0%, transparent 70%);
border-radius: 50%;
}
.diagnosis-label {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.4rem;
}
.diagnosis-name {
font-family: 'Syne', sans-serif;
font-size: 1.5rem;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 0.8rem;
line-height: 1.2;
}
.confidence-pill {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.85rem;
border-radius: 20px;
font-size: 0.82rem;
font-weight: 600;
}
.pill-high { background: rgba(104,211,145,0.15); color: var(--accent-green); border: 1px solid rgba(104,211,145,0.3); }
.pill-medium { background: rgba(246,173,85,0.15); color: var(--accent-amber); border: 1px solid rgba(246,173,85,0.3); }
.pill-low { background: rgba(252,129,129,0.15); color: var(--accent-red); border: 1px solid rgba(252,129,129,0.3); }
.pill-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: currentColor;
}
/* ── Prediction rows ── */
.pred-row {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.9rem 1.2rem;
margin-bottom: 0.6rem;
transition: border-color 0.2s;
}
.pred-row:hover { border-color: var(--border-bright); }
.pred-row-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.pred-rank {
font-family: 'Syne', sans-serif;
font-size: 0.65rem;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.1em;
}
.pred-name {
font-weight: 500;
color: var(--text-primary);
font-size: 0.92rem;
}
.pred-pct {
font-family: 'Syne', sans-serif;
font-size: 0.88rem;
font-weight: 700;
color: var(--accent-cyan);
}
.pred-bar-track {
height: 4px;
background: var(--bg-base);
border-radius: 4px;
overflow: hidden;
}
.pred-bar-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, var(--accent-cyan) 0%, var(--accent-teal) 100%);
transition: width 0.6s ease;
}
/* ── Clinical note ── */
.clinical-note {
background: rgba(99,179,237,0.06);
border: 1px solid rgba(99,179,237,0.2);
border-left: 3px solid var(--accent-cyan);
border-radius: 10px;
padding: 1rem 1.2rem;
margin-top: 1.5rem;
}
.clinical-note .cn-title {
font-family: 'Syne', sans-serif;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent-cyan);
margin-bottom: 0.3rem;
}
.clinical-note p {
font-size: 0.83rem;
color: var(--text-secondary);
margin: 0;
line-height: 1.6;
}
/* ── Sidebar styles ── */
.sidebar-section-title {
font-family: 'Syne', sans-serif;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.13em;
text-transform: uppercase;
color: #63B3ED !important;
margin-bottom: 0.6rem;
}
.sidebar-info {
background: rgba(99,179,237,0.07) !important;
border: 1px solid rgba(99,179,237,0.18) !important;
border-radius: 10px !important;
padding: 0.9rem !important;
font-size: 0.83rem !important;
color: #A0AEC0 !important;
line-height: 1.6 !important;
}
.model-spec-row {
display: flex;
justify-content: space-between;
padding: 0.45rem 0;
border-bottom: 1px solid rgba(99,179,237,0.1);
font-size: 0.82rem;
}
.model-spec-row:last-child { border-bottom: none; }
.spec-key { color: #4A5568; }
.spec-val { color: #EDF2F7; font-weight: 500; }
/* ── Empty state ── */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--bg-surface);
border: 1.5px dashed var(--border);
border-radius: 16px;
margin-top: 1rem;
}
.empty-state .es-icon { font-size: 3.5rem; margin-bottom: 1rem; opacity: 0.5; }
.empty-state h3 {
font-family: 'Syne', sans-serif;
color: var(--text-secondary);
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.empty-state p { color: var(--text-muted); font-size: 0.9rem; }
/* ── Streamlit overrides ── */
[data-testid="stMarkdownContainer"] p { color: var(--text-secondary); }
.stSpinner > div { color: var(--accent-cyan) !important; }
[data-testid="stDataFrame"] {
background: var(--bg-surface) !important;
border: 1px solid var(--border) !important;
border-radius: 10px !important;
}
[data-testid="stProgress"] > div > div > div > div {
background: linear-gradient(90deg, #63B3ED, #4FD1C5) !important;
border-radius: 4px;
}
[data-testid="stInfo"] {
background: rgba(99,179,237,0.07) !important;
border: 1px solid rgba(99,179,237,0.2) !important;
color: var(--text-secondary) !important;
border-radius: 10px !important;
}
</style>
""", unsafe_allow_html=True)
# ── Load model ──────────────────────────────────────────
@st.cache_resource
def load_model():
classes = np.load('classes.npy', allow_pickle=True)
num_classes = len(classes)
model = models.resnet50(weights=None)
num_features = model.fc.in_features
model.fc = nn.Sequential(
nn.Linear(num_features, 1024), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(1024, 512), nn.ReLU(), nn.Dropout(0.1),
nn.Linear(512, 128), nn.ReLU(),
nn.Linear(128, num_classes)
)
model.load_state_dict(torch.load('skin_lesion_resnet50_best.pth', map_location='cpu'))
model.eval()
return model, classes
def preprocess(image):
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
return transform(image).unsqueeze(0)
def confidence_pill(conf):
if conf >= 0.80:
return f'<span class="confidence-pill pill-high"><span class="pill-dot"></span>{conf*100:.1f}% High Confidence</span>'
elif conf >= 0.60:
return f'<span class="confidence-pill pill-medium"><span class="pill-dot"></span>{conf*100:.1f}% Moderate</span>'
else:
return f'<span class="confidence-pill pill-low"><span class="pill-dot"></span>{conf*100:.1f}% Low Confidence</span>'
# ── Hero ─────────────────────────────────────────────────
st.markdown("""
<div class="hero">
<div class="hero-icon">πŸ”¬</div>
<div class="hero-text">
<h1>DermaScan AI</h1>
<p>Deep learning–powered dermoscopy analysis &nbsp;Β·&nbsp; ResNet50 classification engine</p>
</div>
<div class="hero-badge">⚑ Live Inference</div>
</div>
""", unsafe_allow_html=True)
# ── Sidebar ──────────────────────────────────────────────
with st.sidebar:
st.markdown('<p class="sidebar-section-title">About</p>', unsafe_allow_html=True)
st.markdown("""
<div class="sidebar-info">
DermaScan AI uses a fine-tuned <strong style="color:#EDF2F7">ResNet50</strong> convolutional network
trained on a comprehensive dermoscopy dataset to classify skin lesion types from uploaded images.
</div>
""", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
st.markdown('<p class="sidebar-section-title">Model Specs</p>', unsafe_allow_html=True)
st.markdown("""
<div style="background:rgba(99,179,237,0.05); border:1px solid rgba(99,179,237,0.15); border-radius:10px; padding:0.7rem 1rem;">
<div class="model-spec-row"><span class="spec-key">Architecture</span><span class="spec-val">ResNet50</span></div>
<div class="model-spec-row"><span class="spec-key">Framework</span><span class="spec-val">PyTorch</span></div>
<div class="model-spec-row"><span class="spec-key">Input Size</span><span class="spec-val">224 Γ— 224 px</span></div>
<div class="model-spec-row"><span class="spec-key">Task</span><span class="spec-val">Multi-class</span></div>
<div class="model-spec-row"><span class="spec-key">Normalization</span><span class="spec-val">ImageNet</span></div>
</div>
""", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
st.markdown('<p class="sidebar-section-title">Instructions</p>', unsafe_allow_html=True)
st.markdown("""
<div style="font-size:0.82rem; color:#718096; line-height:1.8;">
1. Upload a dermoscopy image (JPG / PNG)<br>
2. Wait for model inference to complete<br>
3. Review ranked predictions and confidence scores<br>
4. Consult a dermatologist for clinical decisions
</div>
""", unsafe_allow_html=True)
# ── Upload ────────────────────────────────────────────────
st.markdown("""
<div class="section-header">
<span class="icon">πŸ“‚</span>
<span class="label">Image Upload</span>
<div class="section-divider"></div>
</div>
""", unsafe_allow_html=True)
uploaded_file = st.file_uploader(
"Drop a dermoscopy image here, or click to browse",
type=["jpg", "jpeg", "png"],
help="Supported: JPG, JPEG, PNG"
)
# ── Results ────────────────────────────────────────────────
if uploaded_file:
image = Image.open(uploaded_file).convert("RGB")
w, h = image.size
col1, col2 = st.columns([1, 1], gap="large")
with col1:
st.markdown("""
<div class="section-header" style="margin-top:1.2rem">
<span class="icon">πŸ–Ό</span>
<span class="label">Input Image</span>
<div class="section-divider"></div>
</div>
""", unsafe_allow_html=True)
st.markdown('<div class="img-panel">', unsafe_allow_html=True)
st.image(image, use_column_width=True)
st.markdown(f"""
<div class="img-meta">
<div class="img-meta-item">
<div class="val">{w} Γ— {h}</div>
<div class="key">Resolution</div>
</div>
<div class="img-meta-item">
<div class="val">{uploaded_file.name.split('.')[-1].upper()}</div>
<div class="key">Format</div>
</div>
<div class="img-meta-item">
<div class="val">{uploaded_file.size // 1024} KB</div>
<div class="key">File Size</div>
</div>
<div class="img-meta-item">
<div class="val">RGB</div>
<div class="key">Color Mode</div>
</div>
</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="section-header" style="margin-top:1.2rem">
<span class="icon">🎯</span>
<span class="label">Analysis Results</span>
<div class="section-divider"></div>
</div>
""", unsafe_allow_html=True)
with st.spinner("Running model inference…"):
model, classes = load_model()
tensor = preprocess(image)
with torch.no_grad():
outputs = model(tensor)
probs = torch.softmax(outputs, dim=1)[0]
top_prob, top_idx = torch.topk(probs, 3)
top_prediction = classes[top_idx[0]]
top_confidence = float(top_prob[0])
# Primary diagnosis
st.markdown(f"""
<div class="diagnosis-card">
<div class="diagnosis-label">Primary Diagnosis</div>
<div class="diagnosis-name">{top_prediction}</div>
{confidence_pill(top_confidence)}
</div>
""", unsafe_allow_html=True)
# Top-3 predictions
st.markdown("""
<div class="section-header" style="margin-top:1rem">
<span class="icon">πŸ“Š</span>
<span class="label">Ranked Predictions</span>
<div class="section-divider"></div>
</div>
""", unsafe_allow_html=True)
rank_labels = ["1st", "2nd", "3rd"]
for i, (prob, idx) in enumerate(zip(top_prob, top_idx)):
conf = float(prob)
bar_width = int(conf * 100)
st.markdown(f"""
<div class="pred-row">
<div class="pred-row-top">
<div>
<span class="pred-rank">{rank_labels[i]}&nbsp;&nbsp;</span>
<span class="pred-name">{classes[idx]}</span>
</div>
<span class="pred-pct">{conf*100:.1f}%</span>
</div>
<div class="pred-bar-track">
<div class="pred-bar-fill" style="width:{bar_width}%"></div>
</div>
</div>
""", unsafe_allow_html=True)
# ── Full results table ──
st.markdown("<br>", unsafe_allow_html=True)
st.markdown("""
<div class="section-header">
<span class="icon">πŸ“‹</span>
<span class="label">Classification Summary</span>
<div class="section-divider"></div>
</div>
""", unsafe_allow_html=True)
results_data = {
"Rank": [f"#{i+1}" for i in range(len(top_prob))],
"Diagnosis": [classes[idx] for idx in top_idx],
"Confidence": [f"{prob*100:.2f}%" for prob in top_prob],
"Status": [
"βœ… Primary" if i == 0 else ("⚠️ Alternate" if i == 1 else "ℹ️ Low prob")
for i in range(len(top_prob))
]
}
st.dataframe(results_data, use_container_width=True, hide_index=True)
# ── Clinical note ──
st.markdown("""
<div class="clinical-note">
<div class="cn-title">βš•οΈ Clinical Disclaimer</div>
<p>This AI classification is intended for <strong style="color:#EDF2F7">informational and research purposes only</strong>.
It does not constitute a medical diagnosis. Always consult a board-certified dermatologist
for clinical evaluation, diagnosis, and treatment recommendations.</p>
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div class="empty-state">
<div class="es-icon">πŸ”¬</div>
<h3>No Image Uploaded</h3>
<p>Upload a dermoscopy image above to begin skin lesion classification</p>
</div>
""", unsafe_allow_html=True)