Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>AlexNet Image Classifier</title> | |
| <style> | |
| /* --- existing styles unchanged --- */ | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); min-height:100vh; display:flex; justify-content:center; align-items:center; padding:20px; } | |
| .container { background:white; border-radius:20px; box-shadow:0 20px 60px rgba(0,0,0,0.3); max-width:900px; width:100%; padding:40px; } | |
| .header { text-align:center; margin-bottom:40px; } | |
| .header h1 { color:#333; font-size:2.5em; margin-bottom:10px; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; } | |
| .header p { color:#666; font-size:1.1em; } | |
| .preset-section { margin-bottom:30px; } | |
| .preset-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; } | |
| .preset-card { border:2px solid #eee; border-radius:12px; overflow:hidden; background:#fafafa; cursor:pointer; transition:all .2s ease; } | |
| .preset-card:hover { transform:translateY(-2px); box-shadow:0 8px 24px rgba(0,0,0,.08); } | |
| .preset-card.selected { border-color:#667eea; box-shadow:0 0 0 3px rgba(102,126,234,.25) inset; } | |
| .preset-thumb { width:100%; height:120px; object-fit:cover; display:block; background:#f0f0f0; } | |
| .preset-label { text-align:center; font-weight:700; padding:10px; color:#444; } | |
| .upload-section { margin-bottom:30px; } | |
| .upload-area { border:3px dashed #ddd; border-radius:15px; padding:24px; text-align:center; transition:all .3s ease; cursor:pointer; background:#fafafa; } | |
| .upload-area:hover { border-color:#667eea; background:#f5f5ff; } | |
| .upload-area.drag-over { border-color:#764ba2; background:#f0f0ff; transform:scale(1.02); } | |
| .upload-icon { font-size:36px; color:#667eea; margin-bottom:8px; } | |
| .upload-text { color:#333; font-size:1.05em; margin-bottom:6px; } | |
| .upload-subtext { color:#666; font-size:.9em; } | |
| .file-input { display:none; } | |
| .image-preview-section { display:none; margin-bottom:30px; } | |
| .image-preview-section.active { display:block; } | |
| .preview-container { display:flex; gap:30px; align-items:start; } | |
| .preview-image-wrapper { flex:1; max-width:400px; } | |
| .preview-image { width:100%; height:auto; border-radius:10px; box-shadow:0 4px 20px rgba(0,0,0,0.1); } | |
| .image-info { flex:1; padding:20px; background:#f8f9fa; border-radius:10px; } | |
| .info-item { display:flex; justify-content:space-between; margin-bottom:10px; padding-bottom:10px; border-bottom:1px solid #e9ecef; } | |
| .info-item:last-child { border-bottom:none; margin-bottom:0; } | |
| .info-label { color:#666; font-weight:500; } | |
| .info-value { color:#333; font-weight:600; } | |
| .button-group { display:flex; gap:15px; margin-top:20px; } | |
| .btn { padding:12px 30px; border:none; border-radius:8px; font-size:1em; font-weight:600; cursor:pointer; transition:all .3s ease; } | |
| .btn-primary { background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); color:white; flex:1; } | |
| .btn-primary:hover { transform:translateY(-2px); box-shadow:0 5px 20px rgba(102,126,234,.4); } | |
| .btn-primary:disabled { background:#ccc; cursor:not-allowed; transform:none; } | |
| .btn-secondary { background:#e9ecef; color:#495057; } | |
| .btn-secondary:hover { background:#dee2e6; } | |
| .results-section { display:none; } | |
| .results-section.active { display:block; animation:slideIn .3s ease; } | |
| @keyframes slideIn { from { opacity:0; transform:translateY(20px);} to { opacity:1; transform:translateY(0);} } | |
| .results-header { background:linear-gradient(135deg,#28a745 0%,#20c997 100%); color:white; padding:20px; border-radius:10px; margin-bottom:20px; } | |
| .predicted-class { font-size:1.8em; font-weight:700; margin-bottom:5px; } | |
| .confidence-score { font-size:1.2em; opacity:.95; } | |
| .probabilities-container { background:#f8f9fa; border-radius:10px; padding:20px; } | |
| .probabilities-title { font-size:1.2em; color:#333; margin-bottom:15px; font-weight:600; } | |
| .probability-item { margin-bottom:15px; } | |
| .probability-label { display:flex; justify-content:space-between; margin-bottom:5px; } | |
| .class-name { color:#495057; font-weight:500; } | |
| .class-prob { color:#333; font-weight:600; } | |
| .probability-bar-bg { height:8px; background:#e9ecef; border-radius:4px; overflow:hidden; } | |
| .probability-bar { height:100%; background:linear-gradient(90deg,#667eea 0%,#764ba2 100%); border-radius:4px; transition:width .5s ease; } | |
| .error-message { display:none; background:#f8d7da; color:#721c24; padding:15px; border-radius:8px; margin-top:20px; } | |
| .error-message.active { display:block; } | |
| .loading-spinner { display:none; text-align:center; padding:20px; } | |
| .loading-spinner.active { display:block; } | |
| .spinner { display:inline-block; width:40px; height:40px; border:4px solid #f3f3f3; border-top:4px solid #667eea; border-radius:50%; animation:spin 1s linear infinite; } | |
| @keyframes spin { 0% { transform:rotate(0deg);} 100% { transform:rotate(360deg);} } | |
| .loading-text { color:#666; margin-top:10px; } | |
| @media (max-width:768px) { | |
| .container { padding:20px; } | |
| .header h1 { font-size:2em; } | |
| .preview-container { flex-direction:column; } | |
| .preview-image-wrapper { max-width:100%; } | |
| .preset-grid { grid-template-columns:repeat(2,1fr); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🧠 AlexNet Classifier</h1> | |
| <p>Select a preset image or upload your own</p> | |
| </div> | |
| <!-- NEW: Preset selector --> | |
| <div class="preset-section"> | |
| <div class="preset-grid" id="presetGrid"> | |
| <!-- Cards are populated by JS using /preset_image/<label> --> | |
| </div> | |
| </div> | |
| <div class="upload-section"> | |
| <div class="upload-area" id="uploadArea"> | |
| <input type="file" id="fileInput" class="file-input" accept="image/*"> | |
| <div class="upload-icon">📸</div> | |
| <div class="upload-text">Click to upload or drag and drop</div> | |
| <div class="upload-subtext">Supports JPG, PNG, GIF, BMP</div> | |
| </div> | |
| </div> | |
| <div class="image-preview-section" id="previewSection"> | |
| <div class="preview-container"> | |
| <div class="preview-image-wrapper"> | |
| <img id="previewImage" class="preview-image" alt="Preview"> | |
| </div> | |
| <div class="image-info"> | |
| <div class="info-item"> | |
| <span class="info-label">Source:</span> | |
| <span class="info-value" id="sourceLabel">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">File Name:</span> | |
| <span class="info-value" id="fileName">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">File Size:</span> | |
| <span class="info-value" id="fileSize">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Image Type:</span> | |
| <span class="info-value" id="fileType">-</span> | |
| </div> | |
| <div class="info-item"> | |
| <span class="info-label">Dimensions:</span> | |
| <span class="info-value" id="imageDimensions">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="button-group"> | |
| <button class="btn btn-secondary" id="clearBtn">Clear</button> | |
| <button class="btn btn-primary" id="classifyBtn">Classify Image</button> | |
| </div> | |
| </div> | |
| <div class="loading-spinner" id="loadingSpinner"> | |
| <div class="spinner"></div> | |
| <div class="loading-text">Analyzing image...</div> | |
| </div> | |
| <div class="results-section" id="resultsSection"> | |
| <div class="results-header"> | |
| <div class="predicted-class" id="predictedClass">-</div> | |
| <div class="confidence-score" id="confidenceScore">-</div> | |
| </div> | |
| <div class="probabilities-container"> | |
| <div class="probabilities-title">All Class Probabilities</div> | |
| <div id="probabilitiesList"></div> | |
| </div> | |
| <div class="gradcam-container" id="gradcamContainer" style="display:none; margin:16px 0 20px;"> | |
| <div class="probabilities-title" style="margin-bottom:10px;">Grad-CAM (Predicted Class)</div> | |
| <img id="gradcamImage" class="preview-image" alt="Grad-CAM visualization" style="max-width:480px; width:100%; border-radius:10px; box-shadow:0 4px 20px rgba(0,0,0,0.08);" /> | |
| </div> | |
| </div> | |
| <div class="error-message" id="errorMessage"></div> | |
| </div> | |
| <script> | |
| const uploadArea = document.getElementById('uploadArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const previewSection = document.getElementById('previewSection'); | |
| const previewImage = document.getElementById('previewImage'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const fileType = document.getElementById('fileType'); | |
| const imageDimensions = document.getElementById('imageDimensions'); | |
| const sourceLabel = document.getElementById('sourceLabel'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const classifyBtn = document.getElementById('classifyBtn'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const predictedClass = document.getElementById('predictedClass'); | |
| const confidenceScore = document.getElementById('confidenceScore'); | |
| const probabilitiesList = document.getElementById('probabilitiesList'); | |
| const errorMessage = document.getElementById('errorMessage'); | |
| const loadingSpinner = document.getElementById('loadingSpinner'); | |
| const presetGrid = document.getElementById('presetGrid'); | |
| const gradcamContainer = document.getElementById('gradcamContainer'); | |
| const gradcamImage = document.getElementById('gradcamImage'); | |
| let currentFile = null; | |
| let currentPreset = null; // 'TP' | 'TN' | 'FN' | 'FP' | null | |
| const PRESETS = [ | |
| { key: 'TN', label: 'True Negative', name: 'Image A' }, | |
| { key: 'FP', label: 'False Positive', name: 'Image B' }, | |
| { key: 'TP', label: 'True Positive', name: 'Image C' }, | |
| { key: 'FN', label: 'False Negative', name: 'Image D' }, | |
| ]; | |
| // Build preset cards | |
| function buildPresetGrid() { | |
| presetGrid.innerHTML = ''; | |
| PRESETS.forEach(p => { | |
| const card = document.createElement('div'); | |
| card.className = 'preset-card'; | |
| card.dataset.key = p.key; | |
| card.innerHTML = ` | |
| <img class="preset-thumb" alt="${p.key}" src="/preset_image/${p.key}"> | |
| <div class="preset-label">${p.name}</div> | |
| `; | |
| card.addEventListener('click', () => selectPreset(p.key,p.name)); | |
| presetGrid.appendChild(card); | |
| }); | |
| } | |
| function markSelectedPreset() { | |
| const cards = presetGrid.querySelectorAll('.preset-card'); | |
| cards.forEach(c => { | |
| if (c.dataset.key === currentPreset) c.classList.add('selected'); | |
| else c.classList.remove('selected'); | |
| }); | |
| } | |
| async function selectPreset(key, name) { | |
| try { | |
| currentPreset = key; | |
| currentFile = null; // uploading not used when preset chosen | |
| hideError(); | |
| hideResults(); | |
| const imgURL = `/preset_image/${key}`; | |
| // Fetch blob to get size + type metadata | |
| const resp = await fetch(imgURL); | |
| if (!resp.ok) throw new Error('Failed to load preset image'); | |
| const blob = await resp.blob(); | |
| previewImage.src = imgURL; | |
| previewImage.onload = () => { | |
| imageDimensions.textContent = `${previewImage.naturalWidth} × ${previewImage.naturalHeight}`; | |
| }; | |
| fileName.textContent = `${name}.jpg`; | |
| fileSize.textContent = formatFileSize(blob.size); | |
| fileType.textContent = blob.type || 'image/jpeg'; | |
| sourceLabel.textContent = `Preset (${name.split(' ')[1]})`; | |
| previewSection.classList.add('active'); | |
| markSelectedPreset(); | |
| } catch (e) { | |
| showError(e.message || 'Could not select preset image.'); | |
| } | |
| } | |
| // Handle upload area | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file) handleFile(file); | |
| }); | |
| uploadArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); uploadArea.classList.add('drag-over'); | |
| }); | |
| uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('drag-over')); | |
| uploadArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); uploadArea.classList.remove('drag-over'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) handleFile(files[0]); | |
| }); | |
| function handleFile(file) { | |
| if (!file.type.startsWith('image/')) { | |
| showError('Please upload an image file'); | |
| return; | |
| } | |
| currentFile = file; | |
| currentPreset = null; | |
| displayPreview(file); | |
| hideError(); | |
| hideResults(); | |
| markSelectedPreset(); | |
| sourceLabel.textContent = 'Uploaded file'; | |
| } | |
| function displayPreview(file) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| previewImage.src = e.target.result; | |
| previewImage.onload = () => { | |
| imageDimensions.textContent = `${previewImage.naturalWidth} × ${previewImage.naturalHeight}`; | |
| }; | |
| }; | |
| reader.readAsDataURL(file); | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| fileType.textContent = file.type || 'Unknown'; | |
| previewSection.classList.add('active'); | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024, sizes = ['Bytes','KB','MB','GB']; | |
| const i = Math.floor(Math.log(bytes)/Math.log(k)); | |
| return Math.round(bytes/Math.pow(k,i)*100)/100 + ' ' + sizes[i]; | |
| } | |
| clearBtn.addEventListener('click', () => { | |
| currentFile = null; | |
| currentPreset = null; | |
| fileInput.value = ''; | |
| previewSection.classList.remove('active'); | |
| hideResults(); | |
| hideError(); | |
| markSelectedPreset(); | |
| }); | |
| classifyBtn.addEventListener('click', async () => { | |
| if (!currentFile && !currentPreset) { | |
| showError('No image selected (choose a preset or upload one).'); | |
| return; | |
| } | |
| try { | |
| showLoading(); hideError(); hideResults(); | |
| let response; | |
| if (currentPreset) { | |
| response = await fetch('/predict_preset', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ preset: currentPreset }) | |
| }); | |
| } else { | |
| const formData = new FormData(); | |
| formData.append('image', currentFile); | |
| response = await fetch('/predict_AlexNet', { method: 'POST', body: formData }); | |
| } | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.error || 'Failed to classify image'); | |
| } | |
| const result = await response.json(); | |
| displayResults(result); | |
| } catch (error) { | |
| showError(error.message || 'Failed to classify image. Please try again.'); | |
| console.error('Classification error:', error); | |
| } finally { | |
| hideLoading(); | |
| } | |
| }); | |
| function displayResults(result) { | |
| predictedClass.textContent = result.class; | |
| confidenceScore.textContent = `${(result.confidence * 100).toFixed(2)}% Confidence`; | |
| // --- NEW: Grad-CAM rendering --- | |
| if (result.gradcam) { | |
| gradcamImage.src = result.gradcam; | |
| gradcamContainer.style.display = 'block'; | |
| } else { | |
| gradcamContainer.style.display = 'none'; | |
| gradcamImage.removeAttribute('src'); | |
| } | |
| const sortedProbs = Object.entries(result.probabilities) | |
| .sort(([, a], [, b]) => b - a) | |
| .slice(0, 10); | |
| probabilitiesList.innerHTML = ''; | |
| sortedProbs.forEach(([className, prob], index) => { | |
| const probPercent = (prob * 100).toFixed(2); | |
| const isTop = index === 0; | |
| const div = document.createElement('div'); | |
| div.className = 'probability-item'; | |
| div.innerHTML = ` | |
| <div class="probability-label"> | |
| <span class="class-name" style="${isTop ? 'font-weight:700;color:#667eea;' : ''}">${className}</span> | |
| <span class="class-prob" style="${isTop ? 'font-weight:700;color:#667eea;' : ''}">${probPercent}%</span> | |
| </div> | |
| <div class="probability-bar-bg"> | |
| <div class="probability-bar" style="width:0%;" data-width="${probPercent}"></div> | |
| </div> | |
| `; | |
| probabilitiesList.appendChild(div); | |
| }); | |
| resultsSection.classList.add('active'); | |
| setTimeout(() => { | |
| probabilitiesList.querySelectorAll('.probability-bar').forEach(bar => { | |
| bar.style.width = bar.getAttribute('data-width') + '%'; | |
| }); | |
| }, 100); | |
| } | |
| function showLoading() { loadingSpinner.classList.add('active'); classifyBtn.disabled = true; } | |
| function hideLoading() { loadingSpinner.classList.remove('active'); classifyBtn.disabled = false; } | |
| function hideResults() { resultsSection.classList.remove('active'); } | |
| function showError(message) { errorMessage.textContent = message; errorMessage.classList.add('active'); } | |
| function hideError() { errorMessage.classList.remove('active'); } | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && (currentFile || currentPreset) && !classifyBtn.disabled) classifyBtn.click(); | |
| if (e.key === 'Escape' && previewSection.classList.contains('active')) clearBtn.click(); | |
| }); | |
| window.addEventListener('load', async () => { | |
| buildPresetGrid(); | |
| try { | |
| const response = await fetch('/health'); | |
| if (!response.ok) showError('Backend server is not responding. Ensure it is running on port 7860.'); | |
| } catch { | |
| showError('Cannot connect to backend server. Ensure it is running on http://0.0.0.0:7860'); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |