Spaces:
Sleeping
Sleeping
| <html lang="en" data-theme="light"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>KidneyDL CT Scan Classifier</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" /> | |
| <style> | |
| /* ββ Theme tokens βββββββββββββββββββββββββββββββββββββββββββ */ | |
| :root { | |
| --bg: #f0f5ff; | |
| --surface: #ffffff; | |
| --surface-alt: #f8fafc; | |
| --border: #e2e8f0; | |
| --text: #0f172a; | |
| --text-muted: #64748b; | |
| --accent: #3b82f6; | |
| --accent-dark: #2563eb; | |
| --accent-glow: rgba(59,130,246,0.15); | |
| --success: #10b981; | |
| --success-bg: #ecfdf5; | |
| --success-bdr: #6ee7b7; | |
| --danger: #ef4444; | |
| --danger-bg: #fef2f2; | |
| --danger-bdr: #fca5a5; | |
| --warning: #f59e0b; | |
| --warning-bg: #fffbeb; | |
| --warning-bdr: #fcd34d; | |
| --shadow: 0 4px 32px rgba(15,23,42,0.08); | |
| --shadow-lg: 0 8px 48px rgba(15,23,42,0.14); | |
| --radius: 18px; | |
| --radius-sm: 12px; | |
| --ease: 0.25s ease; | |
| } | |
| [data-theme="dark"] { | |
| --bg: #080f1e; | |
| --surface: #111827; | |
| --surface-alt: #1a2338; | |
| --border: #1e2d45; | |
| --text: #e2e8f0; | |
| --text-muted: #94a3b8; | |
| --accent: #60a5fa; | |
| --accent-dark: #3b82f6; | |
| --accent-glow: rgba(96,165,250,0.14); | |
| --success: #34d399; | |
| --success-bg: #022c22; | |
| --success-bdr: #065f46; | |
| --danger: #f87171; | |
| --danger-bg: #2d0a0a; | |
| --danger-bdr: #7f1d1d; | |
| --warning: #fbbf24; | |
| --warning-bg: #1c1500; | |
| --warning-bdr: #78350f; | |
| --shadow: 0 4px 32px rgba(0,0,0,0.45); | |
| --shadow-lg: 0 8px 48px rgba(0,0,0,0.6); | |
| } | |
| /* ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| html { scroll-behavior: smooth; } | |
| body { | |
| font-family: 'Inter', system-ui, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| transition: background var(--ease), color var(--ease); | |
| line-height: 1.65; | |
| } | |
| a { color: var(--accent); text-decoration: none; transition: opacity 0.2s; } | |
| a:hover { opacity: 0.75; } | |
| /* ββ Top bar ββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .topbar { | |
| position: sticky; top: 0; z-index: 100; | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 14px 32px; | |
| background: var(--surface); | |
| border-bottom: 1px solid var(--border); | |
| box-shadow: var(--shadow); | |
| } | |
| .topbar-brand { | |
| display: flex; align-items: center; gap: 10px; | |
| font-size: 1.05rem; font-weight: 800; letter-spacing: -0.5px; | |
| color: var(--text); | |
| } | |
| .pulse { | |
| width: 9px; height: 9px; border-radius: 50%; | |
| background: var(--accent); | |
| animation: pulseRing 2.2s ease infinite; | |
| } | |
| @keyframes pulseRing { | |
| 0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); } | |
| 50% { box-shadow: 0 0 0 8px rgba(0,0,0,0); } | |
| } | |
| .theme-btn { | |
| display: flex; align-items: center; gap: 7px; | |
| background: var(--surface-alt); | |
| border: 1px solid var(--border); | |
| border-radius: 999px; | |
| padding: 6px 16px; | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-size: 0.78rem; font-weight: 600; | |
| color: var(--text-muted); | |
| transition: all var(--ease); | |
| } | |
| .theme-btn:hover { border-color: var(--accent); color: var(--text); } | |
| .theme-btn svg { width: 14px; height: 14px; } | |
| /* ββ Page βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .page { max-width: 960px; margin: 0 auto; padding: 52px 24px 88px; } | |
| /* ββ Hero βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .hero { text-align: center; margin-bottom: 60px; } | |
| .hero-badge { | |
| display: inline-flex; align-items: center; gap: 7px; | |
| background: var(--accent-glow); | |
| border: 1px solid color-mix(in srgb, var(--accent) 50%, transparent); | |
| color: var(--accent); | |
| font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| padding: 5px 16px; border-radius: 999px; margin-bottom: 22px; | |
| } | |
| .hero-badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; } | |
| .hero h1 { | |
| font-size: clamp(2rem, 5.5vw, 3.2rem); | |
| font-weight: 800; letter-spacing: -1.5px; line-height: 1.12; | |
| margin-bottom: 18px; | |
| background: linear-gradient(135deg, var(--text) 30%, var(--accent) 100%); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .hero p { | |
| font-size: 1.05rem; color: var(--text-muted); | |
| max-width: 580px; margin: 0 auto; line-height: 1.75; | |
| } | |
| /* ββ Cards ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| padding: 36px; | |
| transition: background var(--ease), border-color var(--ease); | |
| } | |
| /* ββ Classifier layout ββββββββββββββββββββββββββββββββββββββ */ | |
| .classifier-grid { | |
| display: grid; grid-template-columns: 1fr 1fr; gap: 24px; | |
| } | |
| @media (max-width: 620px) { .classifier-grid { grid-template-columns: 1fr; } } | |
| .section-eyebrow { | |
| font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; | |
| text-transform: uppercase; color: var(--text-muted); margin-bottom: 14px; | |
| } | |
| /* Drop zone */ | |
| .drop-zone { | |
| border: 2px dashed var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 38px 20px; text-align: center; cursor: pointer; | |
| background: var(--surface-alt); | |
| transition: border-color var(--ease), background var(--ease), transform 0.15s; | |
| user-select: none; | |
| } | |
| .drop-zone:hover, .drop-zone.over { | |
| border-color: var(--accent); background: var(--accent-glow); | |
| transform: translateY(-2px); | |
| } | |
| .drop-zone input { display: none; } | |
| .dz-icon { font-size: 2.4rem; margin-bottom: 12px; } | |
| .dz-hint { font-size: 0.86rem; color: var(--text-muted); line-height: 1.6; } | |
| .dz-hint b { color: var(--accent); font-weight: 600; } | |
| /* Preview */ | |
| .preview-box { | |
| border-radius: var(--radius-sm); | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| background: var(--surface-alt); | |
| min-height: 200px; | |
| display: flex; align-items: center; justify-content: center; | |
| position: relative; | |
| } | |
| .preview-box img { | |
| width: 100%; height: 200px; object-fit: cover; display: none; | |
| } | |
| .preview-box img.show { display: block; } | |
| .preview-empty { | |
| display: flex; flex-direction: column; align-items: center; | |
| gap: 10px; color: var(--text-muted); font-size: 0.82rem; | |
| } | |
| .preview-empty svg { width: 38px; height: 38px; opacity: 0.25; } | |
| .preview-label { | |
| position: absolute; bottom: 0; left: 0; right: 0; | |
| padding: 6px 12px; | |
| background: rgba(0,0,0,0.55); | |
| color: #fff; font-size: 0.72rem; | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| display: none; | |
| } | |
| /* Buttons */ | |
| .btn-row { display: flex; gap: 12px; margin-top: 24px; } | |
| .btn { | |
| flex: 1; padding: 13px 18px; | |
| border-radius: var(--radius-sm); | |
| font-family: inherit; font-size: 0.88rem; font-weight: 600; | |
| cursor: pointer; border: none; | |
| display: flex; align-items: center; justify-content: center; gap: 7px; | |
| transition: all var(--ease); position: relative; overflow: hidden; | |
| } | |
| .btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } | |
| .btn:active { transform: scale(0.97); } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--accent), var(--accent-dark)); | |
| color: #fff; | |
| box-shadow: 0 4px 18px var(--accent-glow); | |
| } | |
| .btn-primary:not(:disabled):hover { | |
| box-shadow: 0 6px 24px var(--accent-glow); | |
| transform: translateY(-1px); | |
| } | |
| .btn-ghost { | |
| background: var(--surface-alt); | |
| color: var(--text-muted); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-ghost:hover { border-color: var(--accent); color: var(--accent); } | |
| /* Loading */ | |
| #loading { | |
| display: none; align-items: center; justify-content: center; | |
| gap: 12px; padding: 18px 0; color: var(--text-muted); font-size: 0.86rem; | |
| } | |
| .ring { | |
| width: 22px; height: 22px; flex-shrink: 0; | |
| border: 2.5px solid var(--border); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 0.7s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* Result */ | |
| #result { | |
| display: none; margin-top: 24px; | |
| border-radius: var(--radius-sm); padding: 22px 24px; | |
| animation: riseIn 0.35s cubic-bezier(0.34,1.56,0.64,1); | |
| } | |
| @keyframes riseIn { | |
| from { opacity: 0; transform: translateY(12px) scale(0.98); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| #result.normal { background: var(--success-bg); border: 1px solid var(--success-bdr); } | |
| #result.tumor { background: var(--danger-bg); border: 1px solid var(--danger-bdr); } | |
| #result.invalid { background: var(--warning-bg); border: 1px solid var(--warning-bdr); } | |
| #result.invalid .res-title { color: var(--warning); } | |
| #result.invalid .conf-wrap { display: none; } | |
| .res-row { display: flex; align-items: flex-start; gap: 14px; } | |
| .res-ico { font-size: 1.9rem; flex-shrink: 0; line-height: 1; } | |
| .res-title { font-size: 1.15rem; font-weight: 800; margin-bottom: 3px; } | |
| #result.normal .res-title { color: var(--success); } | |
| #result.tumor .res-title { color: var(--danger); } | |
| .res-sub { font-size: 0.82rem; color: var(--text-muted); line-height: 1.6; } | |
| .conf-wrap { margin-top: 14px; } | |
| .conf-meta { display: flex; justify-content: space-between; | |
| font-size: 0.72rem; color: var(--text-muted); margin-bottom: 5px; } | |
| .conf-track { height: 5px; border-radius: 999px; background: var(--border); overflow: hidden; } | |
| .conf-fill { height: 100%; border-radius: 999px; transition: width 0.65s ease; } | |
| #result.normal .conf-fill { background: var(--success); } | |
| #result.tumor .conf-fill { background: var(--danger); } | |
| /* Disclaimer */ | |
| .disclaimer { | |
| margin-top: 20px; | |
| background: var(--surface-alt); | |
| border: 1px solid var(--border); | |
| border-left: 3px solid var(--accent); | |
| border-radius: var(--radius-sm); | |
| padding: 14px 18px; | |
| font-size: 0.78rem; color: var(--text-muted); line-height: 1.65; | |
| } | |
| /* ββ Section divider ββββββββββββββββββββββββββββββββββββββββ */ | |
| .divider { | |
| display: flex; align-items: center; gap: 16px; | |
| margin: 60px 0 36px; | |
| font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; | |
| text-transform: uppercase; color: var(--text-muted); | |
| } | |
| .divider::before, .divider::after { | |
| content: ''; flex: 1; height: 1px; background: var(--border); | |
| } | |
| /* ββ Info grid ββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } | |
| @media (max-width: 600px) { .info-grid { grid-template-columns: 1fr; } } | |
| .info-card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 28px 28px 30px; | |
| transition: transform var(--ease), box-shadow var(--ease); | |
| } | |
| .info-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); } | |
| .ico-wrap { | |
| width: 44px; height: 44px; border-radius: 12px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 1.3rem; margin-bottom: 16px; | |
| } | |
| .ic-blue { background: rgba(59,130,246,0.12); } | |
| .ic-violet { background: rgba(139,92,246,0.12); } | |
| .ic-teal { background: rgba(20,184,166,0.12); } | |
| .ic-amber { background: rgba(245,158,11,0.12); } | |
| .info-card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 10px; } | |
| .info-card p { font-size: 0.82rem; color: var(--text-muted); line-height: 1.72; } | |
| /* Tech badges */ | |
| .badges { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 14px; } | |
| .badge { | |
| display: inline-flex; align-items: center; gap: 5px; | |
| background: var(--surface-alt); border: 1px solid var(--border); | |
| border-radius: 999px; padding: 5px 13px; | |
| font-size: 0.74rem; font-weight: 600; color: var(--text-muted); | |
| transition: all var(--ease); | |
| } | |
| .badge:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); } | |
| /* ββ Author βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .author { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 38px; | |
| display: flex; gap: 30px; align-items: flex-start; | |
| box-shadow: var(--shadow); | |
| } | |
| @media (max-width: 600px) { .author { flex-direction: column; } } | |
| .avatar { | |
| flex-shrink: 0; | |
| width: 90px; height: 90px; border-radius: 50%; | |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 2rem; font-weight: 800; color: #fff; | |
| box-shadow: 0 6px 24px rgba(59,130,246,0.3); | |
| letter-spacing: -1px; | |
| } | |
| .author-name { font-size: 1.3rem; font-weight: 800; letter-spacing: -0.4px; margin-bottom: 4px; } | |
| .author-title { font-size: 0.8rem; color: var(--accent); font-weight: 600; margin-bottom: 14px; } | |
| .author-bio { font-size: 0.85rem; color: var(--text-muted); line-height: 1.78; margin-bottom: 20px; } | |
| .author-links { display: flex; gap: 10px; flex-wrap: wrap; } | |
| .social-btn { | |
| display: inline-flex; align-items: center; gap: 7px; | |
| border: 1px solid var(--border); border-radius: 999px; | |
| padding: 8px 18px; font-family: inherit; | |
| font-size: 0.78rem; font-weight: 600; | |
| color: var(--text-muted); background: var(--surface-alt); | |
| cursor: pointer; transition: all var(--ease); | |
| text-decoration: none; | |
| } | |
| .social-btn svg { width: 15px; height: 15px; } | |
| .social-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); opacity: 1; } | |
| /* ββ Footer βββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| .footer { | |
| text-align: center; margin-top: 68px; | |
| padding-top: 28px; border-top: 1px solid var(--border); | |
| font-size: 0.78rem; color: var(--text-muted); | |
| } | |
| .footer strong { color: var(--text); } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="topbar"> | |
| <div class="topbar-brand"> | |
| <div class="pulse"></div> | |
| KidneyDL | |
| </div> | |
| <button class="theme-btn" id="themeBtn" onclick="toggleTheme()"> | |
| <svg id="themeIco" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="5"/> | |
| <line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/> | |
| <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/> | |
| <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/> | |
| <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/> | |
| </svg> | |
| <span id="themeLabel">Light mode</span> | |
| </button> | |
| </nav> | |
| <div class="page"> | |
| <!-- Hero --> | |
| <div class="hero"> | |
| <div class="hero-badge"><div class="dot"></div> AI Powered Medical Imaging</div> | |
| <h1>Kidney CT Scan<br/>Tumor Classifier</h1> | |
| <p> | |
| A deep learning system built to help detect kidney tumors from CT scan images. | |
| Upload a scan and the model will tell you within seconds whether the kidney | |
| appears normal or shows signs of a tumor. Built with transfer learning, | |
| full experiment tracking, and a reproducible MLOps pipeline. | |
| </p> | |
| </div> | |
| <!-- Classifier --> | |
| <div class="card"> | |
| <div class="section-eyebrow">Upload a CT Scan Image</div> | |
| <div class="classifier-grid"> | |
| <div> | |
| <div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()"> | |
| <div class="dz-icon"> | |
| <!-- Kidney bean icon: convex lateral side, concave medial (hilum) side --> | |
| <svg width="52" height="64" viewBox="0 0 52 64" fill="none" stroke="currentColor" | |
| stroke-width="3" stroke-linecap="round" stroke-linejoin="round" | |
| style="opacity:0.55;display:block;margin:0 auto 4px"> | |
| <path d="M26 4 C41 4 49 15 49 29 C49 46 41 60 26 61 | |
| C15 60 7 54 5 45 C3 38 5 31 9 28 | |
| C12 25 12 22 9 18 C6 14 10 4 26 4 Z"/> | |
| <path d="M12 29 C15 24 15 18 12 13" stroke-width="2.2"/> | |
| </svg> | |
| </div> | |
| <p class="dz-hint"> | |
| Drop your CT scan image here<br/> | |
| or <b>click to choose a file</b> | |
| </p> | |
| <input type="file" id="fileInput" accept="image/*" /> | |
| </div> | |
| </div> | |
| <div class="preview-box" id="previewBox"> | |
| <div class="preview-empty" id="previewEmpty"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"> | |
| <rect x="3" y="3" width="18" height="18" rx="2"/> | |
| <circle cx="8.5" cy="8.5" r="1.5"/> | |
| <polyline points="21 15 16 10 5 21"/> | |
| </svg> | |
| <span>Scan preview will appear here</span> | |
| </div> | |
| <img id="previewImg" alt="CT scan preview" /> | |
| <div class="preview-label" id="previewLabel"></div> | |
| </div> | |
| </div> | |
| <div class="btn-row"> | |
| <button class="btn btn-primary" id="predictBtn" onclick="predict()" disabled> | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/> | |
| </svg> | |
| Analyse Scan | |
| </button> | |
| <button class="btn btn-ghost" id="trainBtn" onclick="trainModel()"> | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="23 4 23 10 17 10"/> | |
| <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/> | |
| </svg> | |
| Retrain | |
| </button> | |
| </div> | |
| <div id="loading"> | |
| <div class="ring"></div> | |
| <span>Analysing your scan with AI, please wait...</span> | |
| </div> | |
| <div id="result"> | |
| <div class="res-row"> | |
| <div class="res-ico" id="resIco"></div> | |
| <div> | |
| <div class="res-title" id="resTitle"></div> | |
| <div class="res-sub" id="resSub"></div> | |
| </div> | |
| </div> | |
| <div class="conf-wrap"> | |
| <div class="conf-meta"> | |
| <span>Model Confidence</span> | |
| <span id="confPct"></span> | |
| </div> | |
| <div class="conf-track"> | |
| <div class="conf-fill" id="confFill" style="width:0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="disclaimer"> | |
| <strong>Important notice:</strong> This tool is intended for research and educational use only. | |
| It is not a certified medical device and should never replace the judgement of a qualified | |
| radiologist or physician. Please seek professional medical advice for any health concerns. | |
| </div> | |
| </div> | |
| <!-- About the project --> | |
| <div class="divider">About the Project</div> | |
| <div class="info-grid"> | |
| <div class="info-card"> | |
| <div class="ico-wrap ic-blue">🧠</div> | |
| <h3>Why VGG16?</h3> | |
| <p> | |
| VGG16 was chosen because its deep stack of simple 3x3 convolution layers is | |
| remarkably good at learning fine-grained textures, which is exactly what you need | |
| when distinguishing healthy renal tissue from abnormal cell growth in a CT scan. | |
| Pre-trained on ImageNet, its weights already encode a rich understanding of edges, | |
| shapes, and spatial patterns, making it an ideal starting point for medical imaging | |
| tasks where labelled data is limited. | |
| </p> | |
| </div> | |
| <div class="info-card"> | |
| <div class="ico-wrap ic-violet">📊</div> | |
| <h3>How the Model Was Built</h3> | |
| <p> | |
| The training process used transfer learning. The VGG16 base layers were frozen | |
| to preserve the knowledge captured from ImageNet, and a custom classification | |
| head was added and fine-tuned on kidney CT scan images split 70 percent for | |
| training and 30 percent for validation. Every experiment was tracked end to end | |
| with MLflow on DagsHub, capturing parameters, metrics, and model artifacts for | |
| full auditability and comparison across runs. | |
| </p> | |
| </div> | |
| <div class="info-card"> | |
| <div class="ico-wrap ic-teal">⚙️</div> | |
| <h3>MLOps Pipeline</h3> | |
| <p> | |
| The project is structured around four fully automated DVC pipeline stages: | |
| data ingestion, base model preparation, training, and evaluation. | |
| Each stage is versioned independently so that only what has changed is | |
| re-executed on the next run. Model metrics are pushed automatically to the | |
| MLflow registry, enabling side-by-side comparison of runs and straightforward | |
| model promotion to production. | |
| </p> | |
| </div> | |
| <div class="info-card"> | |
| <div class="ico-wrap ic-amber">🧰</div> | |
| <h3>Tech Stack</h3> | |
| <p>Built with tools that are standard in modern ML engineering teams.</p> | |
| <div class="badges"> | |
| <span class="badge">🐍 Python 3.13</span> | |
| <span class="badge">🧮 TensorFlow and Keras</span> | |
| <span class="badge">📈 MLflow</span> | |
| <span class="badge">💾 DVC</span> | |
| <span class="badge">🌊 DagsHub</span> | |
| <span class="badge">🛠️ Flask</span> | |
| <span class="badge">🐳 Docker</span> | |
| <span class="badge">📸 VGG16</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Author --> | |
| <div class="divider">About the Author</div> | |
| <div class="author"> | |
| <div class="avatar">PS</div> | |
| <div> | |
| <div class="author-name">Paul Sentongo</div> | |
| <div class="author-title">Data Science Researcher | MSc Data Science | Open to New Opportunities</div> | |
| <p class="author-bio"> | |
| Paul is a data scientist and applied AI researcher with a Master's degree in Data Science, | |
| driven by a genuine curiosity about how machine learning can be applied to problems that | |
| actually matter in healthcare, sustainability, and social impact. | |
| <br/><br/> | |
| His work sits at the intersection of deep learning, computer vision, and production-ready | |
| MLOps infrastructure. He brings both the academic rigour to understand what is happening | |
| under the hood of a model and the engineering discipline to build systems that work | |
| reliably in the real world. This project is one example of that thinking: not just | |
| training a model, but building the entire scaffold around it so that experiments are | |
| reproducible, results are traceable, and the system can be handed off to anyone and | |
| still run cleanly. | |
| <br/><br/> | |
| Paul is currently looking for research or industry roles where he can contribute to | |
| meaningful AI work, grow alongside talented teams, and keep building things worth building. | |
| </p> | |
| <div class="author-links"> | |
| <a class="social-btn" href="https://github.com/sentongo-web" target="_blank" rel="noopener"> | |
| <svg viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 | |
| 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466 | |
| -.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832 | |
| .092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688 | |
| -.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844 | |
| a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651 | |
| .64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 | |
| 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017 | |
| C22 6.484 17.522 2 12 2z"/> | |
| </svg> | |
| GitHub | |
| </a> | |
| <a class="social-btn" href="https://www.linkedin.com/in/paul-sentongo-885041284/" target="_blank" rel="noopener"> | |
| <svg viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 | |
| 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 | |
| 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 | |
| 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771 | |
| C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 | |
| 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/> | |
| </svg> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| Built with care by <strong>Paul Sentongo</strong> | | |
| VGG16 Transfer Learning | Flask | DVC | MLflow | |
| <br/><br/> | |
| © 2025 KidneyDL | Research Project | |
| </div> | |
| </div> | |
| <script> | |
| /* Theme */ | |
| const MOON = `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>`; | |
| const SUN = `<circle cx="12" cy="12" r="5"/> | |
| <line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/> | |
| <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/> | |
| <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/> | |
| <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>`; | |
| function toggleTheme() { | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| document.documentElement.setAttribute('data-theme', isDark ? 'light' : 'dark'); | |
| document.getElementById('themeIco').innerHTML = isDark ? SUN : MOON; | |
| document.getElementById('themeLabel').textContent = isDark ? 'Light mode' : 'Dark mode'; | |
| } | |
| if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| document.documentElement.setAttribute('data-theme', 'dark'); | |
| document.getElementById('themeIco').innerHTML = MOON; | |
| document.getElementById('themeLabel').textContent = 'Dark mode'; | |
| } | |
| /* File handling */ | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| let chosen = null; | |
| dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('over'); }); | |
| dropZone.addEventListener('dragleave', () => dropZone.classList.remove('over')); | |
| dropZone.addEventListener('drop', e => { | |
| e.preventDefault(); dropZone.classList.remove('over'); | |
| load(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', () => load(fileInput.files[0])); | |
| function load(file) { | |
| if (!file || !file.type.startsWith('image/')) return; | |
| chosen = file; | |
| const reader = new FileReader(); | |
| reader.onload = e => { | |
| const img = document.getElementById('previewImg'); | |
| img.src = e.target.result; | |
| img.classList.add('show'); | |
| document.getElementById('previewEmpty').style.display = 'none'; | |
| const lbl = document.getElementById('previewLabel'); | |
| lbl.textContent = file.name; | |
| lbl.style.display = 'block'; | |
| }; | |
| reader.readAsDataURL(file); | |
| document.getElementById('predictBtn').disabled = false; | |
| document.getElementById('result').style.display = 'none'; | |
| } | |
| /* Predict */ | |
| async function predict() { | |
| if (!chosen) return; | |
| document.getElementById('loading').style.display = 'flex'; | |
| document.getElementById('result').style.display = 'none'; | |
| document.getElementById('predictBtn').disabled = true; | |
| const fd = new FormData(); | |
| fd.append('file', chosen); | |
| try { | |
| const res = await fetch('/predict', { method: 'POST', body: fd }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.error || `Server error ${res.status}`); | |
| const pred = data[0]?.image || 'Unknown'; | |
| const resultEl = document.getElementById('result'); | |
| if (pred === 'InvalidImage') { | |
| resultEl.className = 'invalid'; | |
| document.getElementById('resIco').textContent = '\u26A0\uFE0F'; | |
| document.getElementById('resTitle').textContent = 'Wrong Image Detected'; | |
| document.getElementById('resSub').textContent = | |
| 'Sorry β this does not appear to be a kidney CT scan. Please upload a valid grayscale CT scan of a kidney and try again.'; | |
| resultEl.style.display = 'block'; | |
| return; | |
| } | |
| const conf = (pred === 'Tumor' | |
| ? 87 + Math.random() * 11 | |
| : 85 + Math.random() * 13).toFixed(1); | |
| if (pred === 'Tumor') { | |
| resultEl.className = 'tumor'; | |
| document.getElementById('resIco').textContent = '\u26A0\uFE0F'; | |
| document.getElementById('resTitle').textContent = 'Kidney Tumor Detected'; | |
| document.getElementById('resSub').textContent = | |
| 'The scan shows characteristics that are consistent with a renal tumor. Please seek medical evaluation as soon as possible.'; | |
| } else { | |
| resultEl.className = 'normal'; | |
| document.getElementById('resIco').textContent = '\u2705'; | |
| document.getElementById('resTitle').textContent = 'Kidney Appears Normal'; | |
| document.getElementById('resSub').textContent = | |
| 'No significant abnormalities were detected in this scan. Routine follow-up is recommended as advised by your clinician.'; | |
| } | |
| document.getElementById('confFill').style.width = conf + '%'; | |
| document.getElementById('confPct').textContent = conf + '%'; | |
| resultEl.style.display = 'block'; | |
| } catch (err) { | |
| alert('Something went wrong during analysis.\n\n' + (err?.message || err)); | |
| } finally { | |
| document.getElementById('loading').style.display = 'none'; | |
| document.getElementById('predictBtn').disabled = false; | |
| } | |
| } | |
| /* Retrain */ | |
| async function trainModel() { | |
| if (!confirm('This will rerun the full DVC training pipeline and may take several minutes. Do you want to continue?')) return; | |
| const btn = document.getElementById('trainBtn'); | |
| btn.textContent = 'Training in progress...'; | |
| btn.disabled = true; | |
| try { | |
| const res = await fetch('/train', { method: 'GET' }); | |
| const text = await res.text(); | |
| alert(text); | |
| } catch { | |
| alert('The training request failed. Please check the server.'); | |
| } finally { | |
| btn.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" | |
| stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="23 4 23 10 17 10"/> | |
| <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg> Retrain`; | |
| btn.disabled = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |