DigitClassifier / index.html
vigilante2099's picture
Update index.html
7b384a6 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>Handwritten Digit Classifier</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Orbitron', sans-serif;
background: #0a0a0a;
color: #ffffff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
#starfield {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.container {
position: relative;
z-index: 1;
width: 90%;
max-width: 1100px;
padding: 40px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid #333;
border-radius: 2px;
}
h1 {
text-align: center;
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 40px;
letter-spacing: 2px;
color: #ffffff;
}
.main-grid {
display: grid;
grid-template-columns: 420px 1fr;
gap: 40px;
margin-bottom: 30px;
}
.canvas-section {
display: flex;
flex-direction: column;
}
#canvas {
width: 100%;
height: 420px;
background: #000000;
border: 2px solid #333;
cursor: crosshair;
display: block;
}
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.btn {
padding: 12px 0;
background: transparent;
color: #ffffff;
border: 2px solid #ffffff;
font-family: 'Orbitron', sans-serif;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
letter-spacing: 1px;
transition: all 0.2s;
}
.btn:active {
background: #ffffff;
color: #000000;
}
.prediction-section {
border: 2px solid #333;
padding: 30px;
display: flex;
flex-direction: column;
}
.prediction-header {
text-align: center;
margin-bottom: 30px;
}
.prediction-header h2 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 20px;
letter-spacing: 1px;
}
.prediction-result {
font-size: 2.5rem;
font-weight: 700;
color: #ffffff;
}
.bars-container {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 10px;
height: 350px;
margin-top: auto;
}
.bar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
}
.percentage {
font-size: 0.75rem;
margin-bottom: 5px;
height: 20px;
color: #999;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
}
.bar-container {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
position: relative;
}
.bar {
width: 100%;
background: #ffffff;
transition: height 0.6s ease;
position: relative;
}
.digit-label {
margin-top: 8px;
font-size: 1rem;
font-weight: 700;
color: #ffffff;
}
.footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 15px;
margin-top: 20px;
}
.footer-text {
font-size: 0.9rem;
font-weight: 400;
letter-spacing: 1px;
}
.about-btn {
width: 40px;
height: 40px;
border: 2px solid #ffffff;
border-radius: 50%;
background: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
position: relative;
}
.about-btn:active {
background: #cccccc;
}
.about-btn svg {
width: 20px;
height: 20px;
stroke: #000000;
fill: none;
transition: stroke 0.2s;
}
.about-btn:active svg {
stroke: #000000;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: #1a1a1a;
border: 2px solid #333;
padding: 40px;
max-width: 600px;
width: 90%;
position: relative;
max-height: 80vh;
overflow-y: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
}
.modal-close {
position: absolute;
top: 20px;
right: 20px;
font-size: 2rem;
color: #ffffff;
cursor: pointer;
border: none;
background: none;
font-weight: 300;
line-height: 1;
}
.modal-content h2 {
font-size: 1.8rem;
margin-bottom: 25px;
letter-spacing: 1px;
font-family: 'Orbitron', sans-serif;
color: #ffffff;
}
.modal-content h3 {
font-size: 1.2rem;
margin-top: 25px;
margin-bottom: 15px;
color: #ffffff;
letter-spacing: 0.5px;
font-family: 'Orbitron', sans-serif;
}
.modal-content p {
line-height: 1.8;
margin-bottom: 15px;
color: #ccc;
font-size: 0.95rem;
}
.modal-content ul {
list-style: none;
padding-left: 0;
}
.modal-content li {
padding: 8px 0;
color: #ccc;
font-size: 0.95rem;
}
.modal-content li::before {
content: "▹ ";
color: #ffffff;
font-weight: bold;
}
.placeholder {
text-align: center;
padding: 60px 20px;
color: #666;
}
.loading {
display: none;
text-align: center;
padding: 60px 20px;
}
.loading.active {
display: block;
}
.spinner {
width: 40px;
height: 40px;
margin: 0 auto 20px;
border: 3px solid #333;
border-top: 3px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.status-banner {
display: none;
}
@media (max-width: 968px) {
.main-grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 1.5rem;
}
.container {
padding: 20px;
}
}
</style>
</head>
<body>
<canvas id="starfield"></canvas>
<div id="statusBanner" class="status-banner status-disconnected">
🔴 Backend not connected
</div>
<div class="container">
<h1>Handwritten Digit Classifier</h1>
<div class="main-grid">
<div class="canvas-section">
<canvas id="canvas"></canvas>
<div class="button-group">
<button class="btn" id="checkBtn">CHECK</button>
<button class="btn" id="clearBtn">CLEAR</button>
</div>
</div>
<div class="prediction-section">
<div class="prediction-header">
<h2>Prediction</h2>
<div class="prediction-result" id="predictedDigit">-</div>
</div>
<div id="placeholder" class="placeholder">
Draw a digit and click CHECK
</div>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>Analyzing...</p>
</div>
<div id="barsContainer" class="bars-container" style="display: none;">
</div>
</div>
</div>
<div class="footer">
<span class="footer-text">made by Tauseef</span>
<button class="about-btn" id="aboutBtn" title="About">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M12 16v-4m0-4h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<div class="modal" id="aboutModal">
<div class="modal-content">
<button class="modal-close" id="modalClose">&times;</button>
<h2>About This Project</h2>
<h3>Dataset</h3>
<p>This classifier is trained on the <strong>MNIST dataset</strong>. Each image is 28×28 pixels in grayscale.</p>
<h3>Neural Network Architecture</h3>
<p>The model uses a <strong>Convolutional Neural Network (CNN)</strong> with the following structure:</p>
<ul>
<li><strong>Input Layer:</strong> 28×28×1 grayscale images</li>
<li><strong>Conv Block 1:</strong> 32 filters (3×3) + BatchNorm + MaxPool + Dropout</li>
<li><strong>Conv Block 2:</strong> 64 filters (3×3) + BatchNorm + MaxPool + Dropout</li>
<li><strong>Conv Block 3:</strong> 128 filters (3×3) + BatchNorm + MaxPool + Dropout</li>
<li><strong>Dense Layers:</strong> 256 → 128 neurons with Dropout</li>
<li><strong>Output Layer:</strong> 10 classes (digits 0-9) with Softmax</li>
</ul>
<h3>How It Works</h3>
<p>When you draw a digit, the system:</p>
<ul>
<li>Captures your drawing from the canvas</li>
<li>Detects the bounding box of your digit</li>
<li>Crops, centers, and resizes to 28×28 pixels</li>
<li>Sends to the CNN model for classification</li>
<li>Returns probabilities for each digit (0-9)</li>
</ul>
</div>
</div>
<script>
const BACKEND_URL = '';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const canvasWidth = 420;
const canvasHeight = 420;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
let isDrawing = false;
let lastX = 0;
let lastY = 0;
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 20;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove);
canvas.addEventListener('touchend', stopDrawing);
function startDrawing(e) {
isDrawing = true;
const rect = canvas.getBoundingClientRect();
lastX = e.clientX - rect.left;
lastY = e.clientY - rect.top;
}
function draw(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
lastX = x;
lastY = y;
}
function stopDrawing() {
isDrawing = false;
}
function handleTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
isDrawing = true;
lastX = touch.clientX - rect.left;
lastY = touch.clientY - rect.top;
}
function handleTouchMove(e) {
e.preventDefault();
if (!isDrawing) return;
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(x, y);
ctx.stroke();
lastX = x;
lastY = y;
}
document.getElementById('clearBtn').addEventListener('click', () => {
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
document.getElementById('placeholder').style.display = 'block';
document.getElementById('barsContainer').style.display = 'none';
document.getElementById('predictedDigit').textContent = '-';
});
document.getElementById('checkBtn').addEventListener('click', async () => {
const imageData = canvas.toDataURL('image/png');
document.getElementById('placeholder').style.display = 'none';
document.getElementById('barsContainer').style.display = 'none';
document.getElementById('loading').classList.add('active');
try {
const response = await fetch(`${BACKEND_URL}/predict`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageData }),
mode: 'cors'
});
if (!response.ok) {
throw new Error('Prediction failed');
}
const data = await response.json();
displayResults(data);
} catch (error) {
console.error('Error:', error);
alert('Failed to get prediction. Make sure backend is running.');
} finally {
document.getElementById('loading').classList.remove('active');
}
});
function displayResults(data) {
document.getElementById('predictedDigit').textContent = data.predicted_digit;
const barsContainer = document.getElementById('barsContainer');
barsContainer.innerHTML = '';
barsContainer.style.display = 'flex';
for (let i = 0; i < 10; i++) {
const prob = data.probabilities[i.toString()];
const percentage = (prob * 100).toFixed(1);
const height = Math.max(prob * 100, 2); // Minimum 2% height for visibility
const wrapper = document.createElement('div');
wrapper.className = 'bar-wrapper';
wrapper.innerHTML = `
<div class="percentage">${percentage}%</div>
<div class="bar-container">
<div class="bar" style="height: ${height}%"></div>
</div>
<div class="digit-label">${i}</div>
`;
barsContainer.appendChild(wrapper);
}
}
const aboutBtn = document.getElementById('aboutBtn');
const aboutModal = document.getElementById('aboutModal');
const modalClose = document.getElementById('modalClose');
aboutBtn.addEventListener('click', () => {
aboutModal.classList.add('active');
});
modalClose.addEventListener('click', () => {
aboutModal.classList.remove('active');
});
aboutModal.addEventListener('click', (e) => {
if (e.target === aboutModal) {
aboutModal.classList.remove('active');
}
});
async function checkBackend() {
const banner = document.getElementById('statusBanner');
try {
const response = await fetch(`${BACKEND_URL}/health`);
const data = await response.json();
if (data.model_loaded) {
banner.className = 'status-banner status-connected';
banner.textContent = '🟢 Connected';
}
} catch (error) {
banner.className = 'status-banner status-disconnected';
banner.textContent = '🔴 Backend not connected';
}
}
checkBackend();
setInterval(checkBackend, 5000);
const starfield = document.getElementById('starfield');
const starCtx = starfield.getContext('2d');
starfield.width = window.innerWidth;
starfield.height = window.innerHeight;
const stars = [];
const numStars = 200;
for (let i = 0; i < numStars; i++) {
stars.push({
x: Math.random() * starfield.width,
y: Math.random() * starfield.height,
radius: Math.random() * 1.5,
opacity: Math.random(),
twinkleSpeed: Math.random() * 0.02 + 0.005
});
}
function animateStars() {
starCtx.clearRect(0, 0, starfield.width, starfield.height);
stars.forEach(star => {
star.opacity += star.twinkleSpeed;
if (star.opacity > 1 || star.opacity < 0) {
star.twinkleSpeed = -star.twinkleSpeed;
}
starCtx.beginPath();
starCtx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
starCtx.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
starCtx.fill();
});
requestAnimationFrame(animateStars);
}
animateStars();
window.addEventListener('resize', () => {
starfield.width = window.innerWidth;
starfield.height = window.innerHeight;
});
</script>
</body>
</html>