rneuralnets commited on
Commit
ea6c640
Β·
verified Β·
1 Parent(s): 6ed223d

Upload 5 files

Browse files
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Model weights
2
+ *.pth
3
+
4
+ # Optional model folder
5
+ models/
6
+
7
+ # Python cache
8
+ __pycache__/
9
+ *.pyc
10
+
11
+ # Jupyter checkpoints
app.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import torch
3
+ import torch.nn as nn
4
+ from torchvision import models, transforms
5
+ from PIL import Image
6
+ import numpy as np
7
+
8
+ # ── Page configuration ──────────────────────────────────
9
+ st.set_page_config(
10
+ page_title="DermaScan AI",
11
+ page_icon="πŸ”¬",
12
+ layout="wide",
13
+ initial_sidebar_state="expanded"
14
+ )
15
+
16
+ # ── Custom CSS styling ──────────────────────────────────
17
+ st.markdown("""
18
+ <style>
19
+ @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');
20
+
21
+ :root {
22
+ --bg-base: #0A0E1A;
23
+ --bg-surface: #111827;
24
+ --bg-elevated: #1A2235;
25
+ --border: rgba(99,179,237,0.12);
26
+ --border-bright: rgba(99,179,237,0.35);
27
+ --accent-cyan: #63B3ED;
28
+ --accent-teal: #4FD1C5;
29
+ --accent-green: #68D391;
30
+ --accent-amber: #F6AD55;
31
+ --accent-red: #FC8181;
32
+ --text-primary: #EDF2F7;
33
+ --text-secondary:#A0AEC0;
34
+ --text-muted: #4A5568;
35
+ }
36
+
37
+ html, body, [data-testid="stAppViewContainer"] {
38
+ background: var(--bg-base) !important;
39
+ font-family: 'DM Sans', sans-serif;
40
+ color: var(--text-primary);
41
+ }
42
+
43
+ [data-testid="stSidebar"] {
44
+ background: var(--bg-surface) !important;
45
+ border-right: 1px solid var(--border) !important;
46
+ }
47
+
48
+ [data-testid="stSidebar"] * {
49
+ color: var(--text-primary) !important;
50
+ }
51
+
52
+ /* Hide default streamlit chrome */
53
+ #MainMenu, footer, header { visibility: hidden; }
54
+ [data-testid="stDecoration"] { display: none; }
55
+
56
+ /* ── Hero banner ── */
57
+ .hero {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 1.5rem;
61
+ padding: 2.2rem 2.5rem;
62
+ background: linear-gradient(135deg, #0D1B2E 0%, #112240 60%, #0D2137 100%);
63
+ border: 1px solid var(--border-bright);
64
+ border-radius: 16px;
65
+ margin-bottom: 2rem;
66
+ position: relative;
67
+ overflow: hidden;
68
+ }
69
+ .hero::before {
70
+ content: '';
71
+ position: absolute;
72
+ top: -40px; right: -40px;
73
+ width: 220px; height: 220px;
74
+ background: radial-gradient(circle, rgba(99,179,237,0.12) 0%, transparent 70%);
75
+ border-radius: 50%;
76
+ }
77
+ .hero-icon {
78
+ font-size: 3rem;
79
+ line-height: 1;
80
+ filter: drop-shadow(0 0 12px rgba(99,179,237,0.5));
81
+ }
82
+ .hero-text h1 {
83
+ font-family: 'Syne', sans-serif;
84
+ font-size: 2rem;
85
+ font-weight: 800;
86
+ color: var(--text-primary);
87
+ margin: 0 0 0.2rem 0;
88
+ letter-spacing: -0.5px;
89
+ }
90
+ .hero-text p {
91
+ font-size: 0.95rem;
92
+ color: var(--text-secondary);
93
+ margin: 0;
94
+ font-weight: 300;
95
+ letter-spacing: 0.02em;
96
+ }
97
+ .hero-badge {
98
+ margin-left: auto;
99
+ background: rgba(99,179,237,0.1);
100
+ border: 1px solid var(--border-bright);
101
+ color: var(--accent-cyan);
102
+ padding: 0.4rem 1rem;
103
+ border-radius: 20px;
104
+ font-size: 0.78rem;
105
+ font-weight: 600;
106
+ letter-spacing: 0.08em;
107
+ text-transform: uppercase;
108
+ white-space: nowrap;
109
+ }
110
+
111
+ /* ── Upload zone ── */
112
+ .upload-label {
113
+ font-family: 'Syne', sans-serif;
114
+ font-size: 0.75rem;
115
+ font-weight: 700;
116
+ letter-spacing: 0.12em;
117
+ text-transform: uppercase;
118
+ color: var(--accent-cyan);
119
+ margin-bottom: 0.5rem;
120
+ }
121
+ [data-testid="stFileUploader"] {
122
+ background: var(--bg-elevated) !important;
123
+ border: 1.5px dashed var(--border-bright) !important;
124
+ border-radius: 12px !important;
125
+ padding: 0.5rem !important;
126
+ transition: border-color 0.2s;
127
+ }
128
+ [data-testid="stFileUploader"]:hover {
129
+ border-color: var(--accent-cyan) !important;
130
+ }
131
+ [data-testid="stFileUploader"] * { color: var(--text-secondary) !important; }
132
+
133
+ /* ── Section headers ── */
134
+ .section-header {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.6rem;
138
+ margin-bottom: 1rem;
139
+ }
140
+ .section-header span.icon { font-size: 1rem; }
141
+ .section-header span.label {
142
+ font-family: 'Syne', sans-serif;
143
+ font-size: 0.72rem;
144
+ font-weight: 700;
145
+ letter-spacing: 0.13em;
146
+ text-transform: uppercase;
147
+ color: var(--accent-cyan);
148
+ }
149
+ .section-divider {
150
+ flex: 1;
151
+ height: 1px;
152
+ background: var(--border);
153
+ margin-left: 0.5rem;
154
+ }
155
+
156
+ /* ── Image panel ── */
157
+ .img-panel {
158
+ background: var(--bg-surface);
159
+ border: 1px solid var(--border);
160
+ border-radius: 14px;
161
+ padding: 1.2rem;
162
+ overflow: hidden;
163
+ }
164
+ .img-panel img {
165
+ border-radius: 10px !important;
166
+ width: 100% !important;
167
+ }
168
+ .img-meta {
169
+ display: flex;
170
+ justify-content: space-between;
171
+ margin-top: 0.8rem;
172
+ padding-top: 0.8rem;
173
+ border-top: 1px solid var(--border);
174
+ }
175
+ .img-meta-item {
176
+ text-align: center;
177
+ }
178
+ .img-meta-item .val {
179
+ font-family: 'Syne', sans-serif;
180
+ font-size: 0.9rem;
181
+ font-weight: 700;
182
+ color: var(--text-primary);
183
+ }
184
+ .img-meta-item .key {
185
+ font-size: 0.7rem;
186
+ color: var(--text-muted);
187
+ text-transform: uppercase;
188
+ letter-spacing: 0.08em;
189
+ }
190
+
191
+ /* ── Primary diagnosis card ── */
192
+ .diagnosis-card {
193
+ background: linear-gradient(135deg, #0D1B2E 0%, #0D2137 100%);
194
+ border: 1px solid var(--border-bright);
195
+ border-radius: 14px;
196
+ padding: 1.5rem 1.8rem;
197
+ margin-bottom: 1.2rem;
198
+ position: relative;
199
+ overflow: hidden;
200
+ }
201
+ .diagnosis-card::after {
202
+ content: '';
203
+ position: absolute;
204
+ bottom: -30px; right: -30px;
205
+ width: 120px; height: 120px;
206
+ background: radial-gradient(circle, rgba(79,209,197,0.1) 0%, transparent 70%);
207
+ border-radius: 50%;
208
+ }
209
+ .diagnosis-label {
210
+ font-size: 0.68rem;
211
+ font-weight: 700;
212
+ letter-spacing: 0.14em;
213
+ text-transform: uppercase;
214
+ color: var(--text-muted);
215
+ margin-bottom: 0.4rem;
216
+ }
217
+ .diagnosis-name {
218
+ font-family: 'Syne', sans-serif;
219
+ font-size: 1.5rem;
220
+ font-weight: 800;
221
+ color: var(--text-primary);
222
+ margin-bottom: 0.8rem;
223
+ line-height: 1.2;
224
+ }
225
+ .confidence-pill {
226
+ display: inline-flex;
227
+ align-items: center;
228
+ gap: 0.4rem;
229
+ padding: 0.35rem 0.85rem;
230
+ border-radius: 20px;
231
+ font-size: 0.82rem;
232
+ font-weight: 600;
233
+ }
234
+ .pill-high { background: rgba(104,211,145,0.15); color: var(--accent-green); border: 1px solid rgba(104,211,145,0.3); }
235
+ .pill-medium { background: rgba(246,173,85,0.15); color: var(--accent-amber); border: 1px solid rgba(246,173,85,0.3); }
236
+ .pill-low { background: rgba(252,129,129,0.15); color: var(--accent-red); border: 1px solid rgba(252,129,129,0.3); }
237
+ .pill-dot {
238
+ width: 6px; height: 6px;
239
+ border-radius: 50%;
240
+ background: currentColor;
241
+ }
242
+
243
+ /* ── Prediction rows ── */
244
+ .pred-row {
245
+ background: var(--bg-elevated);
246
+ border: 1px solid var(--border);
247
+ border-radius: 10px;
248
+ padding: 0.9rem 1.2rem;
249
+ margin-bottom: 0.6rem;
250
+ transition: border-color 0.2s;
251
+ }
252
+ .pred-row:hover { border-color: var(--border-bright); }
253
+ .pred-row-top {
254
+ display: flex;
255
+ justify-content: space-between;
256
+ align-items: center;
257
+ margin-bottom: 0.5rem;
258
+ }
259
+ .pred-rank {
260
+ font-family: 'Syne', sans-serif;
261
+ font-size: 0.65rem;
262
+ font-weight: 700;
263
+ color: var(--text-muted);
264
+ letter-spacing: 0.1em;
265
+ }
266
+ .pred-name {
267
+ font-weight: 500;
268
+ color: var(--text-primary);
269
+ font-size: 0.92rem;
270
+ }
271
+ .pred-pct {
272
+ font-family: 'Syne', sans-serif;
273
+ font-size: 0.88rem;
274
+ font-weight: 700;
275
+ color: var(--accent-cyan);
276
+ }
277
+ .pred-bar-track {
278
+ height: 4px;
279
+ background: var(--bg-base);
280
+ border-radius: 4px;
281
+ overflow: hidden;
282
+ }
283
+ .pred-bar-fill {
284
+ height: 100%;
285
+ border-radius: 4px;
286
+ background: linear-gradient(90deg, var(--accent-cyan) 0%, var(--accent-teal) 100%);
287
+ transition: width 0.6s ease;
288
+ }
289
+
290
+ /* ── Clinical note ── */
291
+ .clinical-note {
292
+ background: rgba(99,179,237,0.06);
293
+ border: 1px solid rgba(99,179,237,0.2);
294
+ border-left: 3px solid var(--accent-cyan);
295
+ border-radius: 10px;
296
+ padding: 1rem 1.2rem;
297
+ margin-top: 1.5rem;
298
+ }
299
+ .clinical-note .cn-title {
300
+ font-family: 'Syne', sans-serif;
301
+ font-size: 0.7rem;
302
+ font-weight: 700;
303
+ letter-spacing: 0.12em;
304
+ text-transform: uppercase;
305
+ color: var(--accent-cyan);
306
+ margin-bottom: 0.3rem;
307
+ }
308
+ .clinical-note p {
309
+ font-size: 0.83rem;
310
+ color: var(--text-secondary);
311
+ margin: 0;
312
+ line-height: 1.6;
313
+ }
314
+
315
+ /* ── Sidebar styles ── */
316
+ .sidebar-section-title {
317
+ font-family: 'Syne', sans-serif;
318
+ font-size: 0.68rem;
319
+ font-weight: 700;
320
+ letter-spacing: 0.13em;
321
+ text-transform: uppercase;
322
+ color: #63B3ED !important;
323
+ margin-bottom: 0.6rem;
324
+ }
325
+ .sidebar-info {
326
+ background: rgba(99,179,237,0.07) !important;
327
+ border: 1px solid rgba(99,179,237,0.18) !important;
328
+ border-radius: 10px !important;
329
+ padding: 0.9rem !important;
330
+ font-size: 0.83rem !important;
331
+ color: #A0AEC0 !important;
332
+ line-height: 1.6 !important;
333
+ }
334
+ .model-spec-row {
335
+ display: flex;
336
+ justify-content: space-between;
337
+ padding: 0.45rem 0;
338
+ border-bottom: 1px solid rgba(99,179,237,0.1);
339
+ font-size: 0.82rem;
340
+ }
341
+ .model-spec-row:last-child { border-bottom: none; }
342
+ .spec-key { color: #4A5568; }
343
+ .spec-val { color: #EDF2F7; font-weight: 500; }
344
+
345
+ /* ── Empty state ── */
346
+ .empty-state {
347
+ text-align: center;
348
+ padding: 4rem 2rem;
349
+ background: var(--bg-surface);
350
+ border: 1.5px dashed var(--border);
351
+ border-radius: 16px;
352
+ margin-top: 1rem;
353
+ }
354
+ .empty-state .es-icon { font-size: 3.5rem; margin-bottom: 1rem; opacity: 0.5; }
355
+ .empty-state h3 {
356
+ font-family: 'Syne', sans-serif;
357
+ color: var(--text-secondary);
358
+ font-size: 1.2rem;
359
+ margin-bottom: 0.5rem;
360
+ }
361
+ .empty-state p { color: var(--text-muted); font-size: 0.9rem; }
362
+
363
+ /* ── Streamlit overrides ── */
364
+ [data-testid="stMarkdownContainer"] p { color: var(--text-secondary); }
365
+ .stSpinner > div { color: var(--accent-cyan) !important; }
366
+ [data-testid="stDataFrame"] {
367
+ background: var(--bg-surface) !important;
368
+ border: 1px solid var(--border) !important;
369
+ border-radius: 10px !important;
370
+ }
371
+ [data-testid="stProgress"] > div > div > div > div {
372
+ background: linear-gradient(90deg, #63B3ED, #4FD1C5) !important;
373
+ border-radius: 4px;
374
+ }
375
+ [data-testid="stInfo"] {
376
+ background: rgba(99,179,237,0.07) !important;
377
+ border: 1px solid rgba(99,179,237,0.2) !important;
378
+ color: var(--text-secondary) !important;
379
+ border-radius: 10px !important;
380
+ }
381
+ </style>
382
+ """, unsafe_allow_html=True)
383
+
384
+ # ── Load model ──────────────────────────────────────────
385
+ @st.cache_resource
386
+ def load_model():
387
+ classes = np.load('classes.npy', allow_pickle=True)
388
+ num_classes = len(classes)
389
+ model = models.resnet50(weights=None)
390
+ num_features = model.fc.in_features
391
+ model.fc = nn.Sequential(
392
+ nn.Linear(num_features, 1024), nn.ReLU(), nn.Dropout(0.2),
393
+ nn.Linear(1024, 512), nn.ReLU(), nn.Dropout(0.1),
394
+ nn.Linear(512, 128), nn.ReLU(),
395
+ nn.Linear(128, num_classes)
396
+ )
397
+ model.load_state_dict(torch.load('skin_lesion_resnet50_best.pth', map_location='cpu'))
398
+ model.eval()
399
+ return model, classes
400
+
401
+ def preprocess(image):
402
+ transform = transforms.Compose([
403
+ transforms.Resize((224, 224)),
404
+ transforms.ToTensor(),
405
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
406
+ ])
407
+ return transform(image).unsqueeze(0)
408
+
409
+ def confidence_pill(conf):
410
+ if conf >= 0.80:
411
+ return f'<span class="confidence-pill pill-high"><span class="pill-dot"></span>{conf*100:.1f}% High Confidence</span>'
412
+ elif conf >= 0.60:
413
+ return f'<span class="confidence-pill pill-medium"><span class="pill-dot"></span>{conf*100:.1f}% Moderate</span>'
414
+ else:
415
+ return f'<span class="confidence-pill pill-low"><span class="pill-dot"></span>{conf*100:.1f}% Low Confidence</span>'
416
+
417
+ # ── Hero ─────────────────────────────────────────────────
418
+ st.markdown("""
419
+ <div class="hero">
420
+ <div class="hero-icon">πŸ”¬</div>
421
+ <div class="hero-text">
422
+ <h1>DermaScan AI</h1>
423
+ <p>Deep learning–powered dermoscopy analysis &nbsp;Β·&nbsp; ResNet50 classification engine</p>
424
+ </div>
425
+ <div class="hero-badge">⚑ Live Inference</div>
426
+ </div>
427
+ """, unsafe_allow_html=True)
428
+
429
+ # ── Sidebar ──────────────────────────────────────────────
430
+ with st.sidebar:
431
+ st.markdown('<p class="sidebar-section-title">About</p>', unsafe_allow_html=True)
432
+ st.markdown("""
433
+ <div class="sidebar-info">
434
+ DermaScan AI uses a fine-tuned <strong style="color:#EDF2F7">ResNet50</strong> convolutional network
435
+ trained on a comprehensive dermoscopy dataset to classify skin lesion types from uploaded images.
436
+ </div>
437
+ """, unsafe_allow_html=True)
438
+
439
+ st.markdown("<br>", unsafe_allow_html=True)
440
+ st.markdown('<p class="sidebar-section-title">Model Specs</p>', unsafe_allow_html=True)
441
+ st.markdown("""
442
+ <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;">
443
+ <div class="model-spec-row"><span class="spec-key">Architecture</span><span class="spec-val">ResNet50</span></div>
444
+ <div class="model-spec-row"><span class="spec-key">Framework</span><span class="spec-val">PyTorch</span></div>
445
+ <div class="model-spec-row"><span class="spec-key">Input Size</span><span class="spec-val">224 Γ— 224 px</span></div>
446
+ <div class="model-spec-row"><span class="spec-key">Task</span><span class="spec-val">Multi-class</span></div>
447
+ <div class="model-spec-row"><span class="spec-key">Normalization</span><span class="spec-val">ImageNet</span></div>
448
+ </div>
449
+ """, unsafe_allow_html=True)
450
+
451
+ st.markdown("<br>", unsafe_allow_html=True)
452
+ st.markdown('<p class="sidebar-section-title">Instructions</p>', unsafe_allow_html=True)
453
+ st.markdown("""
454
+ <div style="font-size:0.82rem; color:#718096; line-height:1.8;">
455
+ 1. Upload a dermoscopy image (JPG / PNG)<br>
456
+ 2. Wait for model inference to complete<br>
457
+ 3. Review ranked predictions and confidence scores<br>
458
+ 4. Consult a dermatologist for clinical decisions
459
+ </div>
460
+ """, unsafe_allow_html=True)
461
+
462
+ # ── Upload ────────────────────────────────────────────────
463
+ st.markdown("""
464
+ <div class="section-header">
465
+ <span class="icon">πŸ“‚</span>
466
+ <span class="label">Image Upload</span>
467
+ <div class="section-divider"></div>
468
+ </div>
469
+ """, unsafe_allow_html=True)
470
+
471
+ uploaded_file = st.file_uploader(
472
+ "Drop a dermoscopy image here, or click to browse",
473
+ type=["jpg", "jpeg", "png"],
474
+ help="Supported: JPG, JPEG, PNG"
475
+ )
476
+
477
+ # ── Results ────────────────────────────────────────────────
478
+ if uploaded_file:
479
+ image = Image.open(uploaded_file).convert("RGB")
480
+ w, h = image.size
481
+
482
+ col1, col2 = st.columns([1, 1], gap="large")
483
+
484
+ with col1:
485
+ st.markdown("""
486
+ <div class="section-header" style="margin-top:1.2rem">
487
+ <span class="icon">πŸ–Ό</span>
488
+ <span class="label">Input Image</span>
489
+ <div class="section-divider"></div>
490
+ </div>
491
+ """, unsafe_allow_html=True)
492
+
493
+ st.markdown('<div class="img-panel">', unsafe_allow_html=True)
494
+ st.image(image, use_column_width=True)
495
+ st.markdown(f"""
496
+ <div class="img-meta">
497
+ <div class="img-meta-item">
498
+ <div class="val">{w} Γ— {h}</div>
499
+ <div class="key">Resolution</div>
500
+ </div>
501
+ <div class="img-meta-item">
502
+ <div class="val">{uploaded_file.name.split('.')[-1].upper()}</div>
503
+ <div class="key">Format</div>
504
+ </div>
505
+ <div class="img-meta-item">
506
+ <div class="val">{uploaded_file.size // 1024} KB</div>
507
+ <div class="key">File Size</div>
508
+ </div>
509
+ <div class="img-meta-item">
510
+ <div class="val">RGB</div>
511
+ <div class="key">Color Mode</div>
512
+ </div>
513
+ </div>
514
+ </div>
515
+ """, unsafe_allow_html=True)
516
+
517
+ with col2:
518
+ st.markdown("""
519
+ <div class="section-header" style="margin-top:1.2rem">
520
+ <span class="icon">🎯</span>
521
+ <span class="label">Analysis Results</span>
522
+ <div class="section-divider"></div>
523
+ </div>
524
+ """, unsafe_allow_html=True)
525
+
526
+ with st.spinner("Running model inference…"):
527
+ model, classes = load_model()
528
+ tensor = preprocess(image)
529
+ with torch.no_grad():
530
+ outputs = model(tensor)
531
+ probs = torch.softmax(outputs, dim=1)[0]
532
+ top_prob, top_idx = torch.topk(probs, 3)
533
+
534
+ top_prediction = classes[top_idx[0]]
535
+ top_confidence = float(top_prob[0])
536
+
537
+ # Primary diagnosis
538
+ st.markdown(f"""
539
+ <div class="diagnosis-card">
540
+ <div class="diagnosis-label">Primary Diagnosis</div>
541
+ <div class="diagnosis-name">{top_prediction}</div>
542
+ {confidence_pill(top_confidence)}
543
+ </div>
544
+ """, unsafe_allow_html=True)
545
+
546
+ # Top-3 predictions
547
+ st.markdown("""
548
+ <div class="section-header" style="margin-top:1rem">
549
+ <span class="icon">πŸ“Š</span>
550
+ <span class="label">Ranked Predictions</span>
551
+ <div class="section-divider"></div>
552
+ </div>
553
+ """, unsafe_allow_html=True)
554
+
555
+ rank_labels = ["1st", "2nd", "3rd"]
556
+ for i, (prob, idx) in enumerate(zip(top_prob, top_idx)):
557
+ conf = float(prob)
558
+ bar_width = int(conf * 100)
559
+ st.markdown(f"""
560
+ <div class="pred-row">
561
+ <div class="pred-row-top">
562
+ <div>
563
+ <span class="pred-rank">{rank_labels[i]}&nbsp;&nbsp;</span>
564
+ <span class="pred-name">{classes[idx]}</span>
565
+ </div>
566
+ <span class="pred-pct">{conf*100:.1f}%</span>
567
+ </div>
568
+ <div class="pred-bar-track">
569
+ <div class="pred-bar-fill" style="width:{bar_width}%"></div>
570
+ </div>
571
+ </div>
572
+ """, unsafe_allow_html=True)
573
+
574
+ # ── Full results table ──
575
+ st.markdown("<br>", unsafe_allow_html=True)
576
+ st.markdown("""
577
+ <div class="section-header">
578
+ <span class="icon">πŸ“‹</span>
579
+ <span class="label">Classification Summary</span>
580
+ <div class="section-divider"></div>
581
+ </div>
582
+ """, unsafe_allow_html=True)
583
+
584
+ results_data = {
585
+ "Rank": [f"#{i+1}" for i in range(len(top_prob))],
586
+ "Diagnosis": [classes[idx] for idx in top_idx],
587
+ "Confidence": [f"{prob*100:.2f}%" for prob in top_prob],
588
+ "Status": [
589
+ "βœ… Primary" if i == 0 else ("⚠️ Alternate" if i == 1 else "ℹ️ Low prob")
590
+ for i in range(len(top_prob))
591
+ ]
592
+ }
593
+ st.dataframe(results_data, use_container_width=True, hide_index=True)
594
+
595
+ # ── Clinical note ──
596
+ st.markdown("""
597
+ <div class="clinical-note">
598
+ <div class="cn-title">βš•οΈ Clinical Disclaimer</div>
599
+ <p>This AI classification is intended for <strong style="color:#EDF2F7">informational and research purposes only</strong>.
600
+ It does not constitute a medical diagnosis. Always consult a board-certified dermatologist
601
+ for clinical evaluation, diagnosis, and treatment recommendations.</p>
602
+ </div>
603
+ """, unsafe_allow_html=True)
604
+
605
+ else:
606
+ st.markdown("""
607
+ <div class="empty-state">
608
+ <div class="es-icon">πŸ”¬</div>
609
+ <h3>No Image Uploaded</h3>
610
+ <p>Upload a dermoscopy image above to begin skin lesion classification</p>
611
+ </div>
612
+ """, unsafe_allow_html=True)
classes.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:591d581f6d5af2122399c75052b256dbf06ee4945254890509b8182866a29a52
3
+ size 320
skin-cancer-classification.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
skin_lesion_resnet50_best.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:036d9e997e9fc17e9a746fb0125914c91f55bd2ae477f2739e5cbeab07de8685
3
+ size 105116363