Download index.html from devarajns/eyetrack: direct link, hf CLI and curl.
- Browser
- Download file 22.3 kB
-
https://huggingface.co/spaces/devarajns/eyetrack/resolve/main/index.html
- Command line
-
hf download hf://spaces/devarajns/eyetrack/index.html
-
curl -L -o index.html https://huggingface.co/spaces/devarajns/eyetrack/resolve/main/index.html
22.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Webcam Eye Tracker</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #5d69b2; | |
| --secondary-color: #3a416f; | |
| --accent-color: #ff7e5f; | |
| --bg-color: #f5f7fa; | |
| --text-color: #333; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| padding: 2rem; | |
| position: relative; | |
| overflow-x: hidden; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| width: 100%; | |
| } | |
| h1 { | |
| color: var(--primary-color); | |
| margin-bottom: 0.5rem; | |
| font-size: 2.2rem; | |
| } | |
| .subtitle { | |
| color: var(--secondary-color); | |
| opacity: 0.8; | |
| margin-bottom: 1.5rem; | |
| } | |
| .tracker-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| width: 100%; | |
| max-width: 800px; | |
| gap: 2rem; | |
| } | |
| .camera-container { | |
| position: relative; | |
| width: 100%; | |
| max-width: 640px; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
| } | |
| #video { | |
| width: 100%; | |
| display: block; | |
| background-color: #000; | |
| } | |
| #canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| } | |
| .metrics-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| justify-content: center; | |
| width: 100%; | |
| } | |
| .metric-card { | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| min-width: 200px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | |
| flex-grow: 1; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .metric-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| } | |
| .metric-title { | |
| color: var(--primary-color); | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-bottom: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .metric-value { | |
| font-size: 1.8rem; | |
| font-weight: bold; | |
| color: var(--secondary-color); | |
| } | |
| .metric-unit { | |
| font-size: 0.9rem; | |
| color: #888; | |
| margin-left: 0.3rem; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 200px; | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| canvas#gaze-chart { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| button { | |
| padding: 0.8rem 1.5rem; | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| button:hover { | |
| background-color: var(--secondary-color); | |
| transform: translateY(-2px); | |
| } | |
| button.secondary { | |
| background-color: white; | |
| color: var(--primary-color); | |
| border: 1px solid #ddd; | |
| } | |
| button.secondary:hover { | |
| background-color: #f0f0f0; | |
| } | |
| .loading { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 1rem; | |
| padding: 2rem; | |
| background-color: white; | |
| border-radius: 12px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | |
| width: 100%; | |
| } | |
| .spinner { | |
| border: 4px solid rgba(0, 0, 0, 0.1); | |
| border-radius: 50%; | |
| border-top: 4px solid var(--primary-color); | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .error-message { | |
| color: #e74c3c; | |
| background-color: #fceae9; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| max-width: 100%; | |
| text-align: center; | |
| } | |
| footer { | |
| margin-top: 3rem; | |
| text-align: center; | |
| color: #888; | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 600px) { | |
| .tracker-container { | |
| gap: 1rem; | |
| } | |
| .metric-card { | |
| min-width: 150px; | |
| padding: 1rem; | |
| } | |
| .metric-value { | |
| font-size: 1.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1><i class="fas fa-eye"></i> Webcam Eye Tracker</h1> | |
| <p class="subtitle">Real-time eye tracking using your webcam and face detection</p> | |
| </header> | |
| <div class="tracker-container"> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <p>Loading face detection models...</p> | |
| </div> | |
| <div class="camera-container" id="camera-container" style="display: none;"> | |
| <video id="video" width="640" height="480" autoplay muted playsinline></video> | |
| <canvas id="canvas" width="640" height="480"></canvas> | |
| </div> | |
| <div class="metrics-container"> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-crosshairs"></i> Eye Position | |
| </div> | |
| <div class="metric-value" id="eye-position"> | |
| <span id="eye-x">0</span>, <span id="eye-y">0</span> | |
| </div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-running"></i> Movement Speed | |
| </div> | |
| <div class="metric-value" id="movement-speed">0<span class="metric-unit">px/s</span></div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-history"></i> Time Tracked | |
| </div> | |
| <div class="metric-value" id="time-tracked">0<span class="metric-unit">s</span></div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-bullseye"></i> Fixations | |
| </div> | |
| <div class="metric-value" id="fixation-count">0</div> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="gaze-chart"></canvas> | |
| </div> | |
| <div class="controls"> | |
| <button id="start-btn"><i class="fas fa-play"></i> Start Tracking</button> | |
| <button id="reset-btn" class="secondary"><i class="fas fa-redo"></i> Reset</button> | |
| <button id="debug-btn" class="secondary"><i class="fas fa-bug"></i> Toggle Debug</button> | |
| </div> | |
| <div class="error-message" id="error-message" style="display: none;"></div> | |
| </div> | |
| <footer> | |
| <p>Webcam Eye Tracker © 2024 | Uses face-api.js for face and eye detection</p> | |
| </footer> | |
| <!-- Load face-api.js from CDN --> | |
| <script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script> | |
| <!-- Load Chart.js from CDN --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', async function() { | |
| // DOM Elements | |
| const video = document.getElementById('video'); | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| const debugBtn = document.getElementById('debug-btn'); | |
| const eyeX = document.getElementById('eye-x'); | |
| const eyeY = document.getElementById('eye-y'); | |
| const movementSpeed = document.getElementById('movement-speed'); | |
| const timeTracked = document.getElementById('time-tracked'); | |
| const fixationCount = document.getElementById('fixation-count'); | |
| const loadingElement = document.getElementById('loading'); | |
| const cameraContainer = document.getElementById('camera-container'); | |
| const errorMessage = document.getElementById('error-message'); | |
| // Variables | |
| let trackingActive = false; | |
| let startTime = 0; | |
| let lastPosition = { x: 0, y: 0 }; | |
| let lastTime = 0; | |
| let currentSpeed = 0; | |
| let fixations = 0; | |
| let fixationStartTime = 0; | |
| let isFixated = false; | |
| let gazeHistory = []; | |
| let showDebug = false; | |
| let modelsLoaded = false; | |
| let stream = null; | |
| // Chart setup | |
| const chartCtx = document.getElementById('gaze-chart').getContext('2d'); | |
| const gazeChart = new Chart(chartCtx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'Eye Movement Speed (px/s)', | |
| data: [], | |
| borderColor: '#5d69b2', | |
| backgroundColor: 'rgba(93, 105, 178, 0.1)', | |
| tension: 0.4, | |
| fill: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true | |
| } | |
| }, | |
| animation: { | |
| duration: 0 | |
| } | |
| } | |
| }); | |
| // Load face-api.js models | |
| async function loadModels() { | |
| try { | |
| await faceapi.nets.tinyFaceDetector.loadFromUri('https://justadudewhohacks.github.io/face-api.js/models'); | |
| await faceapi.nets.faceLandmark68Net.loadFromUri('https://justadudewhohacks.github.io/face-api.js/models'); | |
| modelsLoaded = true; | |
| // Hide loading and show camera UI | |
| loadingElement.style.display = 'none'; | |
| cameraContainer.style.display = 'block'; | |
| // Prompt for camera access | |
| initCamera(); | |
| } catch (error) { | |
| console.error('Error loading models:', error); | |
| showError("Failed to load face detection models. Please check your internet connection and try again."); | |
| } | |
| } | |
| // Initialize camera | |
| async function initCamera() { | |
| try { | |
| stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { | |
| width: { ideal: 640 }, | |
| height: { ideal: 480 }, | |
| facingMode: 'user' | |
| }, | |
| audio: false | |
| }); | |
| video.srcObject = stream; | |
| video.play(); | |
| } catch (error) { | |
| console.error('Camera error:', error); | |
| if (error.name === 'NotAllowedError') { | |
| showError("Camera access was denied. Please allow camera access to use this feature."); | |
| } else if (error.name === 'NotFoundError') { | |
| showError("No camera found. Please connect a webcam to use this feature."); | |
| } else { | |
| showError("Failed to access camera. Please try again."); | |
| } | |
| } | |
| } | |
| // Show error message | |
| function showError(message) { | |
| errorMessage.textContent = message; | |
| errorMessage.style.display = 'block'; | |
| loadingElement.style.display = 'none'; | |
| } | |
| // Calculate eye position based on landmarks | |
| function getEyePosition(landmarks) { | |
| if (!landmarks || !landmarks.getLeftEye || !landmarks.getRightEye) { | |
| return { x: 0, y: 0 }; | |
| } | |
| const leftEye = landmarks.getLeftEye(); | |
| const rightEye = landmarks.getRightEye(); | |
| // Calculate center of each eye | |
| const leftEyeCenter = leftEye.reduce((sum, point) => { | |
| return { x: sum.x + point.x, y: sum.y + point.y }; | |
| }, { x: 0, y: 0 }); | |
| const rightEyeCenter = rightEye.reduce((sum, point) => { | |
| return { x: sum.x + point.x, y: sum.y + point.y }; | |
| }, { x: 0, y: 0 }); | |
| leftEyeCenter.x /= leftEye.length; | |
| leftEyeCenter.y /= leftEye.length; | |
| rightEyeCenter.x /= rightEye.length; | |
| rightEyeCenter.y /= rightEye.length; | |
| // Calculate midpoint between eyes | |
| return { | |
| x: (leftEyeCenter.x + rightEyeCenter.x) / 2, | |
| y: (leftEyeCenter.y + rightEyeCenter.y) / 2 | |
| }; | |
| } | |
| // Update metrics based on eye position | |
| function updateMetrics(position) { | |
| const now = Date.now(); | |
| const timeElapsed = (now - lastTime) / 1000; | |
| if (timeElapsed > 0) { | |
| const dx = position.x - lastPosition.x; | |
| const dy = position.y - lastPosition.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| currentSpeed = distance / timeElapsed; | |
| // Check for fixation (hovering in the same area) | |
| if (distance < 15) { // 15px threshold for fixation | |
| if (!isFixated) { | |
| isFixated = true; | |
| fixationStartTime = now; | |
| } | |
| // If fixation lasts more than 200ms, count it | |
| if (isFixated && now - fixationStartTime > 200) { | |
| fixations++; | |
| fixationCount.textContent = fixations; | |
| isFixated = false; // Reset to count new fixations | |
| } | |
| } else { | |
| isFixated = false; | |
| } | |
| } | |
| // Update metrics display | |
| movementSpeed.textContent = Math.round(currentSpeed); | |
| timeTracked.textContent = Math.round((now - startTime) / 1000); | |
| eyeX.textContent = Math.round(position.x); | |
| eyeY.textContent = Math.round(position.y); | |
| // Update chart data | |
| if (gazeHistory.length > 50) { | |
| gazeHistory.shift(); | |
| gazeChart.data.labels.shift(); | |
| gazeChart.data.datasets[0].data.shift(); | |
| } | |
| gazeHistory.push(currentSpeed); | |
| gazeChart.data.labels.push(''); | |
| gazeChart.data.datasets[0].data.push(currentSpeed); | |
| gazeChart.update(); | |
| // Update last position and time | |
| lastPosition = position; | |
| lastTime = now; | |
| } | |
| // Process video frame for face detection | |
| async function processVideo() { | |
| if (!trackingActive || !modelsLoaded) { | |
| requestAnimationFrame(processVideo); | |
| return; | |
| } | |
| try { | |
| // Detect faces with landmarks | |
| const options = new faceapi.TinyFaceDetectorOptions({ | |
| inputSize: 128, | |
| scoreThreshold: 0.5 | |
| }); | |
| const result = await faceapi.detectSingleFace(video, options) | |
| .withFaceLandmarks(); | |
| // Clear canvas before drawing | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| if (result) { | |
| const { landmarks, detection } = result; | |
| // Calculate eye position | |
| const eyePosition = getEyePosition(landmarks); | |
| // Update metrics with eye position | |
| updateMetrics(eyePosition); | |
| // Draw debug information if enabled | |
| if (showDebug) { | |
| // Draw face detection box | |
| const box = detection.box; | |
| ctx.strokeStyle = '#00FF00'; | |
| ctx.lineWidth = 2; | |
| ctx.strokeRect(box.x, box.y, box.width, box.height); | |
| // Draw landmarks | |
| faceapi.draw.drawFaceLandmarks(canvas, landmarks); | |
| // Draw eye position | |
| ctx.fillStyle = '#FF0000'; | |
| ctx.beginPath(); | |
| ctx.arc(eyePosition.x, eyePosition.y, 5, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Detection error:', error); | |
| } | |
| requestAnimationFrame(processVideo); | |
| } | |
| // Start tracking | |
| function startTracking() { | |
| if (!modelsLoaded) { | |
| showError("Face detection models not loaded yet. Please wait."); | |
| return; | |
| } | |
| trackingActive = true; | |
| startTime = Date.now(); | |
| lastTime = Date.now(); | |
| startBtn.innerHTML = '<i class="fas fa-pause"></i> Pause Tracking'; | |
| startBtn.style.backgroundColor = '#ff7e5f'; | |
| // Start processing video | |
| processVideo(); | |
| } | |
| // Pause tracking | |
| function pauseTracking() { | |
| trackingActive = false; | |
| startBtn.innerHTML = '<i class="fas fa-play"></i> Resume Tracking'; | |
| startBtn.style.backgroundColor = '#5d69b2'; | |
| } | |
| // Reset tracking | |
| function resetTracking() { | |
| pauseTracking(); | |
| startTime = 0; | |
| currentSpeed = 0; | |
| fixations = 0; | |
| gazeHistory = []; | |
| // Reset metrics display | |
| eyeX.textContent = '0'; | |
| eyeY.textContent = '0'; | |
| movementSpeed.textContent = '0'; | |
| timeTracked.textContent = '0'; | |
| fixationCount.textContent = '0'; | |
| // Reset chart | |
| gazeChart.data.labels = []; | |
| gazeChart.data.datasets[0].data = []; | |
| gazeChart.update(); | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| } | |
| // Event listeners | |
| startBtn.addEventListener('click', function() { | |
| if (trackingActive) { | |
| pauseTracking(); | |
| } else { | |
| startTracking(); | |
| } | |
| }); | |
| resetBtn.addEventListener('click', resetTracking); | |
| debugBtn.addEventListener('click', function() { | |
| showDebug = !showDebug; | |
| debugBtn.innerHTML = showDebug ? | |
| '<i class="fas fa-eye-slash"></i> Hide Debug' : | |
| '<i class="fas fa-eye"></i> Show Debug'; | |
| }); | |
| // Initialize | |
| loadModels(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |