Spaces:
Running
Running
| <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">×</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> | |