Spaces:
Runtime error
Runtime error
| 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 — 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' }}> | |
| ← 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 — BERR 4743</div> | |
| <h1>Generative Medical Imaging<br /><span className="gradient-text">Augmentation Dashboard</span></h1> | |
| <p className="hero-sub"> | |
| PneumoniaMNIST • GAN • EBM | |
| </p> | |
| <div style={{ marginTop: '16px' }}> | |
| <a href="/dashboard" onClick={(e) => navigate('/dashboard', e)} className="nav-link"> | |
| View Model Metrics → | |
| </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 & 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 — Generative Medical Imaging Augmentation — PneumoniaMNIST</p> | |
| </footer> | |
| </div> | |
| ) | |
| } | |