gajavegs's picture
Added Grad-CAM
259a0f2
<!DOCTYPE html>
<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>