guiBackend / gui /frontend /src /App.jsx
BrianLov's picture
Upload folder using huggingface_hub (part 10)
84cd834 verified
Raw
History Blame Contribute Delete
20.2 kB
import { useState, useRef, useCallback, useEffect } from 'react'
import './App.css'
const API = 'http://localhost:8000'
const MODELS = ['Baseline', 'GAN', 'EBM']
const MODEL_META = {
GAN: { color: '#4f8ef7', desc: 'ConvTranspose2d Generator + Spectral Norm Discriminator' },
EBM: { color: '#a855f7', desc: 'Energy-Based Model via Langevin Dynamics (MCMC sampling)' },
}
// ── Upload Zone ───────────────────────────────────────────────────────────────
function UploadZone({ onUpload, preview }) {
const [dragging, setDragging] = useState(false)
const inputRef = useRef()
const handle = useCallback((file) => {
if (file && file.type.startsWith('image/')) onUpload(file)
}, [onUpload])
return (
<div
id="upload-zone"
className={`upload-zone ${dragging ? 'dragging' : ''} ${preview ? 'has-preview' : ''}`}
onDragOver={e => { e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={e => { e.preventDefault(); setDragging(false); handle(e.dataTransfer.files[0]) }}
onClick={() => inputRef.current.click()}
>
<input ref={inputRef} type="file" accept="image/*" hidden
onChange={e => handle(e.target.files[0])} />
{preview ? (
<div className="preview-wrap">
<img src={preview} alt="Uploaded X-ray" className="preview-img" />
<span className="preview-change">Click to change</span>
</div>
) : (
<>
<div className="upload-icon">🫁</div>
<p className="upload-title">Drop your chest X-ray here</p>
<p className="upload-sub">or click to browse &mdash; PNG, JPG, JPEG</p>
</>
)}
</div>
)
}
// ── Augmentation Grid ─────────────────────────────────────────────────────────
function AugGrid({ items, loading }) {
if (loading) return <div className="spinner-wrap"><div className="spinner" /></div>
if (!items.length) return <p className="empty">Upload an image to see augmented versions.</p>
return (
<div className="img-grid">
{items.map((item, i) => (
<div key={i} className="img-card">
<img src={`data:image/png;base64,${item.image}`} alt={item.label} />
<span className="img-label">{item.label}</span>
</div>
))}
</div>
)
}
// ── Model Sample Column ───────────────────────────────────────────────────────
function ModelColumn({ name, images }) {
const meta = MODEL_META[name]
return (
<div className="model-col">
<div className="model-col-header" style={{ borderColor: meta.color }}>
<span className="model-name" style={{ color: meta.color }}>{name}</span>
<span className="model-desc">{meta.desc}</span>
</div>
<div className="model-img-stack">
{images.length === 0
? [0].map(i => <div key={i} className="img-placeholder" />)
: images.map((img, i) => (
<div key={i} className="img-card">
<img src={`data:image/png;base64,${img}`} alt={`${name} sample`} />
<span className="img-label">Sample</span>
</div>
))
}
</div>
</div>
)
}
// ── Confidence Bar ────────────────────────────────────────────────────────────
function ConfidenceBar({ label, value, color }) {
return (
<div className="conf-row">
<span className="conf-label">{label}</span>
<div className="conf-track">
<div className="conf-fill" style={{ width: `${value * 100}%`, background: color }} />
</div>
<span className="conf-pct">{(value * 100).toFixed(1)}%</span>
</div>
)
}
// ── Dashboard Component ───────────────────────────────────────────────────────
function Dashboard({ navigate }) {
const [metrics, setMetrics] = useState(null)
const [samples, setSamples] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
Promise.all([
fetch(`${API}/metrics_images`).then(r => r.json()),
fetch(`${API}/dataset_samples`).then(r => r.json())
])
.then(([metricsData, samplesData]) => {
setMetrics(metricsData)
setSamples(samplesData)
setLoading(false)
})
.catch(() => setLoading(false))
}, [])
return (
<div className="app">
<header className="hero">
<div className="hero-content">
<a href="/" onClick={(e) => navigate('/', e)} className="nav-link" style={{ marginBottom: '16px', display: 'inline-block' }}>
&larr; Back to Generator
</a>
<h1>Model Evaluation <span className="gradient-text">Metrics</span></h1>
<p className="hero-sub">Confusion Matrices and ROC Curves for Baseline, GAN, and EBM</p>
</div>
</header>
<main className="main">
{loading ? (
<div className="spinner-wrap"><div className="spinner" /><p className="spinner-label">Loading metrics and dataset samples...</p></div>
) : !metrics ? (
<p className="empty">Failed to load metrics. Check backend.</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
{/* Dataset Samples Section */}
{samples && (
<section className="card">
<div className="card-header">
<h2>PneumoniaMNIST Dataset Samples</h2>
</div>
<p className="card-desc">Real examples from the test set used to evaluate the models.</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', padding: '16px 0' }}>
<div className="model-col">
<div className="model-col-header" style={{ borderColor: '#10b981' }}>
<span className="model-name" style={{ color: '#10b981' }}>Normal (Healthy)</span>
<span className="model-desc">The minority class in training (Needs augmentation)</span>
</div>
<div className="model-img-stack" style={{ flexDirection: 'row', justifyContent: 'center' }}>
{samples.normal.map((img, i) => (
<img key={i} src={`data:image/png;base64,${img}`} style={{ width: '30%', borderRadius: '4px' }} alt="Normal" />
))}
</div>
</div>
<div className="model-col">
<div className="model-col-header" style={{ borderColor: '#f87171' }}>
<span className="model-name" style={{ color: '#f87171' }}>Pneumonia</span>
<span className="model-desc">The majority class (Notice the cloudy opacities)</span>
</div>
<div className="model-img-stack" style={{ flexDirection: 'row', justifyContent: 'center' }}>
{samples.pneumonia.map((img, i) => (
<img key={i} src={`data:image/png;base64,${img}`} style={{ width: '30%', borderRadius: '4px' }} alt="Pneumonia" />
))}
</div>
</div>
</div>
</section>
)}
{/* Explanation Section */}
<section className="card">
<div className="card-header">
<h2>Why did EBM outperform GAN?</h2>
</div>
<div style={{ padding: '16px', fontSize: '14px', color: 'var(--text)', lineHeight: '1.6' }}>
<p style={{ marginBottom: '12px' }}><strong>1. No Mode Collapse:</strong> GANs often suffer from mode collapse, where the generator finds a few realistic examples and repeats them. This creates sharp but repetitive images, failing to capture the full natural diversity of human lungs.</p>
<p style={{ marginBottom: '12px' }}><strong>2. Diverse Distribution Modeling:</strong> EBMs (via Langevin dynamics) explore the entire continuous space of the data distribution. Because they assign low "energy" to all realistic lung states, EBMs generate a massive, diverse variety of healthy lungs including subtle variations in rib cage shapes and tissue densities.</p>
<p><strong>Conclusion:</strong> When the EBM-generated "Normal" images were added to the dataset, they provided the classifier with a much wider, richer spectrum of healthy lungs to learn from. This led to a more robust decision boundary, drastically reducing <strong>False Positives</strong> from 10 (Baseline) to 5 (EBM), pushing Specificity to 96.30%.</p>
</div>
</section>
{/* Metrics Section */}
<section>
<h2 style={{ fontSize: '18px', marginBottom: '16px' }}>Model Metrics</h2>
<div className="metrics-grid">
{['Baseline', 'GAN', 'EBM'].map(model => (
<section key={model} className="card">
<div className="card-header">
<h2 style={{ color: MODEL_META[model]?.color || 'var(--text)' }}>{model} Metrics</h2>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px', padding: '16px' }}>
<div>
<h3 style={{ fontSize: '12px', textTransform: 'uppercase', color: 'var(--muted)', marginBottom: '8px', textAlign: 'center' }}>Confusion Matrix</h3>
{metrics[model]?.cm ? (
<img src={`data:image/png;base64,${metrics[model].cm}`} alt={`${model} CM`} style={{ width: '100%', borderRadius: '8px', border: '1px solid var(--border)' }} />
) : <div className="img-placeholder" />}
</div>
<div>
<h3 style={{ fontSize: '12px', textTransform: 'uppercase', color: 'var(--muted)', marginBottom: '8px', textAlign: 'center' }}>ROC Curve</h3>
{metrics[model]?.roc ? (
<img src={`data:image/png;base64,${metrics[model].roc}`} alt={`${model} ROC`} style={{ width: '100%', borderRadius: '8px', border: '1px solid var(--border)' }} />
) : <div className="img-placeholder" />}
</div>
</div>
</section>
))}
</div>
</section>
</div>
)}
</main>
</div>
)
}
// ── Main App ──────────────────────────────────────────────────────────────────
export default function App() {
const [currentRoute, setCurrentRoute] = useState(window.location.pathname)
const [file, setFile] = useState(null)
const [preview, setPreview] = useState('')
const [augments, setAugments] = useState([])
const [genData, setGenData] = useState({})
const [result, setResult] = useState(null)
const [model, setModel] = useState('Baseline')
const [loadingAug, setLoadingAug] = useState(false)
const [loadingGen, setLoadingGen] = useState(false)
const [loadingClf, setLoadingClf] = useState(false)
useEffect(() => {
const handleLocationChange = () => setCurrentRoute(window.location.pathname)
window.addEventListener('popstate', handleLocationChange)
return () => window.removeEventListener('popstate', handleLocationChange)
}, [])
const navigate = (path, e) => {
if (e) e.preventDefault()
window.history.pushState({}, '', path)
setCurrentRoute(path)
}
const handleUpload = async (f) => {
setFile(f)
setPreview(URL.createObjectURL(f))
setAugments([]); setGenData({}); setResult(null)
setLoadingAug(true)
setLoadingGen(true)
try {
const fd = new FormData(); fd.append('file', f)
const [augRes, genRes] = await Promise.all([
fetch(`${API}/augment`, { method: 'POST', body: fd }),
fetch(`${API}/generate?n=1`, { method: 'POST' })
])
const [augData, genData] = await Promise.all([augRes.json(), genRes.json()])
setAugments(augData.augmentations)
setGenData(genData)
} catch { alert('Backend not reachable. Make sure FastAPI is running on port 8000.') }
finally { setLoadingAug(false); setLoadingGen(false) }
}
const handleClassify = async () => {
if (!file) return
setLoadingClf(true); setResult(null)
try {
const fd = new FormData(); fd.append('file', file); fd.append('model_name', model)
const res = await fetch(`${API}/classify`, { method: 'POST', body: fd })
setResult(await res.json())
} catch { alert('Classification failed. Check backend.') }
finally { setLoadingClf(false) }
}
if (currentRoute === '/dashboard') {
return <Dashboard navigate={navigate} />
}
return (
<div className="app">
<header className="hero">
<div className="hero-glow" />
<div className="hero-content">
<div className="hero-badge">CVPR 2026 &mdash; BERR 4743</div>
<h1>Generative Medical Imaging<br /><span className="gradient-text">Augmentation Dashboard</span></h1>
<p className="hero-sub">
PneumoniaMNIST &bull; GAN &bull; EBM
</p>
<div style={{ marginTop: '16px' }}>
<a href="/dashboard" onClick={(e) => navigate('/dashboard', e)} className="nav-link">
View Model Metrics &rarr;
</a>
</div>
</div>
</header>
<main className="main">
<section className="card">
<div className="card-header">
<h2>Upload Chest X-ray</h2>
<span className="badge">Step 1</span>
</div>
<UploadZone onUpload={handleUpload} preview={preview} />
</section>
{/* Traditional Augmentations */}
<section className="card">
<div className="card-header">
<h2>Traditional Augmentations</h2>
<span className="badge">Row 1</span>
</div>
<p className="card-desc">Standard image transforms applied to the uploaded X-ray — flips, rotations, brightness, blur, and cropping.</p>
<AugGrid items={augments} loading={loadingAug} />
</section>
{/* Generative Samples — GAN & EBM */}
<section className="card">
<div className="card-header">
<h2>Generative Model Samples</h2>
<span className="badge gen-badge">Row 2 — GAN & EBM</span>
</div>
<p className="card-desc">
Synthetic <strong>Normal</strong> lung images auto-generated when you upload — these are what each model added to the training set to fix class imbalance.
</p>
{loadingGen && (
<div className="spinner-wrap">
<div className="spinner" />
<p className="spinner-label">Generating GAN &amp; EBM samples…</p>
</div>
)}
{!loadingGen && Object.keys(genData).length === 0 && (
<p className="empty">Upload an image above to auto-generate GAN & EBM samples.</p>
)}
{Object.keys(genData).length > 0 && (
<div className="gen-all-grid" style={{ gridTemplateColumns: '1fr 1fr' }}>
<ModelColumn name="GAN" images={genData.gan || []} />
<ModelColumn name="EBM" images={genData.ebm || []} />
</div>
)}
</section>
{/* Classification */}
<section className="card">
<div className="card-header">
<h2>Classification</h2>
<span className="badge">Step 2 — Run Inference</span>
</div>
<p className="card-desc">
Select a ResNet50 model trained with each augmentation strategy and classify your uploaded X-ray.
</p>
<div className="clf-controls">
<div className="select-wrap">
<select id="model-select" value={model} onChange={e => setModel(e.target.value)}>
{MODELS.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
<button id="btn-classify" className="btn-primary" onClick={handleClassify}
disabled={!file || loadingClf}>
{loadingClf ? 'Classifying…' : '🔬 Classify'}
</button>
</div>
{loadingClf && <div className="spinner-wrap"><div className="spinner" /></div>}
{result && (
<div className={`result-box ${result.prediction === 'Normal' ? 'normal' : 'pneumonia'}`}>
<div className="result-header">
<span className="result-icon">{result.prediction === 'Normal' ? '✅' : '⚠️'}</span>
<div>
<div className="result-label">Prediction</div>
<div className="result-pred">{result.prediction}</div>
</div>
<div className="result-model-badge">{result.model}</div>
</div>
<div className="conf-bars">
<ConfidenceBar label="Normal" value={result.prob_normal} color="var(--green)" />
<ConfidenceBar label="Pneumonia" value={result.prob_pneumonia} color="var(--red)" />
</div>
</div>
)}
</section>
{/* Results Summary Table */}
<section className="card">
<div className="card-header">
<h2>Model Performance Summary</h2>
<span className="badge">PneumoniaMNIST Test Set</span>
</div>
<div className="table-wrap">
<table className="results-table">
<thead>
<tr>
<th>Model</th><th>Accuracy</th><th>Sensitivity</th><th>Specificity</th><th>AUC</th><th>FP ↓</th><th>FN ↓</th>
</tr>
</thead>
<tbody>
{[
{ name: 'Baseline', acc: 97.90, sens: 99.74, spec: 92.59, auc: 0.9983, fp: 10, fn: 1 },
{ name: 'GAN', acc: 98.47, sens: 99.74, spec: 94.81, auc: 0.9979, fp: 7, fn: 1 },
{ name: 'EBM', acc: 98.85, sens: 99.74, spec: 96.30, auc: 0.9993, fp: 5, fn: 1, best: true },
].map(r => (
<tr key={r.name} className={r.best ? 'row-best' : ''}>
<td>
<span className="table-model-name">
{r.name !== 'Baseline' && (
<span className="table-dot" style={{ background: MODEL_META[r.name]?.color }} />
)}
<strong>{r.name}</strong>
{r.best && <span className="trophy">🏆 Best</span>}
</span>
</td>
<td className={r.best ? 'cell-best' : ''}><strong>{r.acc.toFixed(2)}%</strong></td>
<td>{r.sens.toFixed(2)}%</td>
<td>{r.spec.toFixed(2)}%</td>
<td>{r.auc.toFixed(4)}</td>
<td className={r.fp <= 3 ? 'cell-fp-low' : r.fp >= 10 ? 'cell-worst' : ''}>{r.fp}</td>
<td className={r.fn <= 1 ? 'cell-fp-low' : r.fn >= 4 ? 'cell-worst' : ''}>{r.fn}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</main>
<footer className="footer">
<p>CVPR Assignment &mdash; Generative Medical Imaging Augmentation &mdash; PneumoniaMNIST</p>
</footer>
</div>
)
}