Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MediaPipe Live Preview - Computer Vision Studio</title> | |
| <!-- MediaPipe Libraries --> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_detection/face_detection.js"></script> | |
| <!-- Font Awesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary: #4285f4; | |
| --primary-dark: #3367d6; | |
| --secondary: #34a853; | |
| --accent: #fbbc05; | |
| --danger: #ea4335; | |
| --dark: #202124; | |
| --dark-light: #292a2d; | |
| --light: #ffffff; | |
| --gray: #5f6368; | |
| --gray-light: #e8eaed; | |
| --gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --shadow: 0 10px 40px rgba(0, 0, 0, 0.1); | |
| --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.15); | |
| } | |
| body { | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: var(--dark); | |
| overflow-x: hidden; | |
| } | |
| /* Animated Background */ | |
| .bg-animation { | |
| position: fixed; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| z-index: -1; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| overflow: hidden; | |
| } | |
| .bg-animation::before { | |
| content: ''; | |
| position: absolute; | |
| width: 200%; | |
| height: 200%; | |
| top: -50%; | |
| left: -50%; | |
| background: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| animation: bgMove 20s linear infinite; | |
| } | |
| @keyframes bgMove { | |
| 0% { transform: translate(0, 0); } | |
| 100% { transform: translate(50px, 50px); } | |
| } | |
| /* Header */ | |
| header { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| padding: 1rem 2rem; | |
| box-shadow: var(--shadow); | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| animation: slideDown 0.5s ease-out; | |
| } | |
| @keyframes slideDown { | |
| from { | |
| transform: translateY(-100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| .header-content { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: var(--primary); | |
| } | |
| .logo i { | |
| font-size: 2rem; | |
| background: var(--gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .brand-link { | |
| color: var(--primary); | |
| text-decoration: none; | |
| transition: all 0.3s ease; | |
| font-size: 0.9rem; | |
| opacity: 0.8; | |
| } | |
| .brand-link:hover { | |
| opacity: 1; | |
| transform: translateY(-2px); | |
| } | |
| .stats-bar { | |
| display: flex; | |
| gap: 2rem; | |
| align-items: center; | |
| } | |
| .stat-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 0.5rem 1rem; | |
| background: var(--gray-light); | |
| border-radius: 10px; | |
| transition: all 0.3s ease; | |
| } | |
| .stat-item:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .stat-value { | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| color: var(--primary); | |
| } | |
| .stat-label { | |
| font-size: 0.75rem; | |
| color: var(--gray); | |
| text-transform: uppercase; | |
| } | |
| /* Main Container */ | |
| .container { | |
| max-width: 1400px; | |
| margin: 2rem auto; | |
| padding: 0 1rem; | |
| } | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 300px 1fr 300px; | |
| gap: 2rem; | |
| animation: fadeIn 0.8s ease-out; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Control Panels */ | |
| .control-panel { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 1.5rem; | |
| box-shadow: var(--shadow); | |
| height: fit-content; | |
| animation: slideIn 0.6s ease-out; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .panel-title { | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| margin-bottom: 1.5rem; | |
| color: var(--dark); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .panel-title i { | |
| color: var(--primary); | |
| } | |
| .feature-toggle { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0.75rem; | |
| margin-bottom: 0.75rem; | |
| background: var(--gray-light); | |
| border-radius: 10px; | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| } | |
| .feature-toggle:hover { | |
| background: #dce1e6; | |
| transform: translateX(5px); | |
| } | |
| .feature-toggle.active { | |
| background: linear-gradient(135deg, #667eea20, #764ba220); | |
| border: 1px solid var(--primary); | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 50px; | |
| height: 26px; | |
| background: var(--gray); | |
| border-radius: 13px; | |
| transition: all 0.3s ease; | |
| } | |
| .toggle-switch.active { | |
| background: var(--primary); | |
| } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| width: 22px; | |
| height: 22px; | |
| background: white; | |
| border-radius: 50%; | |
| top: 2px; | |
| left: 2px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
| } | |
| .toggle-switch.active::after { | |
| left: 26px; | |
| } | |
| /* Video Container */ | |
| .video-container { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 1.5rem; | |
| box-shadow: var(--shadow-lg); | |
| position: relative; | |
| animation: scaleIn 0.7s ease-out; | |
| } | |
| @keyframes scaleIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.9); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| .video-wrapper { | |
| position: relative; | |
| width: 100%; | |
| padding-bottom: 56.25%; | |
| background: var(--dark); | |
| border-radius: 15px; | |
| overflow: hidden; | |
| box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3); | |
| } | |
| video, canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| border-radius: 15px; | |
| } | |
| canvas { | |
| z-index: 2; | |
| } | |
| .video-controls { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1rem; | |
| margin-top: 1.5rem; | |
| } | |
| .control-btn { | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .control-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); | |
| } | |
| .control-btn:active { | |
| transform: translateY(0); | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: var(--primary-dark); | |
| } | |
| .btn-secondary { | |
| background: var(--secondary); | |
| color: white; | |
| } | |
| .btn-danger { | |
| background: var(--danger); | |
| color: white; | |
| } | |
| .btn-accent { | |
| background: var(--accent); | |
| color: var(--dark); | |
| } | |
| /* Settings Panel */ | |
| .setting-item { | |
| margin-bottom: 1.5rem; | |
| } | |
| .setting-label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| font-weight: 600; | |
| color: var(--dark); | |
| } | |
| .slider { | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: var(--gray-light); | |
| outline: none; | |
| -webkit-appearance: none; | |
| cursor: pointer; | |
| } | |
| .slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .slider::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| box-shadow: 0 0 10px rgba(66, 133, 244, 0.5); | |
| } | |
| .slider-value { | |
| display: inline-block; | |
| margin-left: 0.5rem; | |
| padding: 0.25rem 0.5rem; | |
| background: var(--gray-light); | |
| border-radius: 5px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| } | |
| /* Info Cards */ | |
| .info-card { | |
| background: linear-gradient(135deg, #667eea10, #764ba210); | |
| border: 1px solid rgba(102, 126, 234, 0.3); | |
| border-radius: 10px; | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| } | |
| .info-card h4 { | |
| color: var(--primary); | |
| margin-bottom: 0.5rem; | |
| } | |
| .info-card p { | |
| font-size: 0.875rem; | |
| color: var(--gray); | |
| line-height: 1.5; | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| border-radius: 15px; | |
| z-index: 10; | |
| transition: opacity 0.3s ease; | |
| } | |
| .loading-overlay.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 4px solid rgba(255, 255, 255, 0.3); | |
| border-top-color: white; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| color: white; | |
| margin-top: 1rem; | |
| font-size: 1.1rem; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| background: white; | |
| padding: 1rem 1.5rem; | |
| border-radius: 10px; | |
| box-shadow: var(--shadow-lg); | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| transform: translateX(400px); | |
| transition: transform 0.3s ease; | |
| z-index: 2000; | |
| } | |
| .toast.show { | |
| transform: translateX(0); | |
| } | |
| .toast-icon { | |
| font-size: 1.5rem; | |
| } | |
| .toast.success .toast-icon { | |
| color: var(--secondary); | |
| } | |
| .toast.error .toast-icon { | |
| color: var(--danger); | |
| } | |
| .toast.info .toast-icon { | |
| color: var(--primary); | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 1200px) { | |
| .main-grid { | |
| grid-template-columns: 250px 1fr; | |
| } | |
| .control-panel:last-child { | |
| grid-column: 1 / -1; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 1rem; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .main-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .header-content { | |
| flex-direction: column; | |
| text-align: center; | |
| } | |
| .stats-bar { | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .video-controls { | |
| flex-wrap: wrap; | |
| } | |
| } | |
| /* Performance Monitor */ | |
| .performance-monitor { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| background: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| font-family: monospace; | |
| font-size: 0.875rem; | |
| z-index: 5; | |
| } | |
| .fps-counter { | |
| color: #4ade80; | |
| font-weight: bold; | |
| } | |
| /* Color Picker */ | |
| .color-picker-group { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-top: 0.5rem; | |
| } | |
| .color-option { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| border: 3px solid transparent; | |
| transition: all 0.3s ease; | |
| } | |
| .color-option:hover { | |
| transform: scale(1.1); | |
| } | |
| .color-option.selected { | |
| border-color: var(--dark); | |
| box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-animation"></div> | |
| <header> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <i class="fas fa-eye"></i> | |
| <span>MediaPipe Live Preview</span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link"> | |
| Built with anycoder | |
| </a> | |
| </div> | |
| <div class="stats-bar"> | |
| <div class="stat-item"> | |
| <span class="stat-value" id="fpsValue">0</span> | |
| <span class="stat-label">FPS</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-value" id="detectionCount">0</span> | |
| <span class="stat-label">Detections</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-value" id="activeFeatures">0</span> | |
| <span class="stat-label">Active</span> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <main class="container"> | |
| <div class="main-grid"> | |
| <!-- Left Panel - Features --> | |
| <aside class="control-panel"> | |
| <h2 class="panel-title"> | |
| <i class="fas fa-sliders-h"></i> | |
| Detection Features | |
| </h2> | |
| <div class="feature-toggle" data-feature="faceMesh"> | |
| <span> | |
| <i class="fas fa-user"></i> Face Mesh | |
| </span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| <div class="feature-toggle" data-feature="hands"> | |
| <span> | |
| <i class="fas fa-hand-paper"></i> Hands | |
| </span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| <div class="feature-toggle" data-feature="pose"> | |
| <span> | |
| <i class="fas fa-running"></i> Pose | |
| </span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| <div class="feature-toggle" data-feature="faceDetection"> | |
| <span> | |
| <i class="fas fa-user-circle"></i> Face Detection | |
| </span> | |
| <div class="toggle-switch"></div> | |
| </div> | |
| <div class="info-card"> | |
| <h4>Pro Tip</h4> | |
| <p>Enable multiple features simultaneously for comprehensive tracking. Each feature uses different models optimized for specific tasks.</p> | |
| </div> | |
| </aside> | |
| <!-- Center - Video Preview --> | |
| <section class="video-container"> | |
| <div class="video-wrapper"> | |
| <video id="inputVideo" autoplay muted playsinline></video> | |
| <canvas id="outputCanvas"></canvas> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="spinner"></div> | |
| <div class="loading-text">Initializing Camera...</div> | |
| </div> | |
| <div class="performance-monitor"> | |
| <div>Performance: <span class="fps-counter" id="fpsMonitor">0 FPS</span></div> | |
| </div> | |
| </div> | |
| <div class="video-controls"> | |
| <button class="control-btn btn-primary" id="startBtn"> | |
| <i class="fas fa-play"></i> Start Camera | |
| </button> | |
| <button class="control-btn btn-danger" id="stopBtn" disabled> | |
| <i class="fas fa-stop"></i> Stop | |
| </button> | |
| <button class="control-btn btn-secondary" id="screenshotBtn" disabled> | |
| <i class="fas fa-camera"></i> Screenshot | |
| </button> | |
| <button class="control-btn btn-accent" id="recordBtn" disabled> | |
| <i class="fas fa-video"></i> Record | |
| </button> | |
| </div> | |
| </section> | |
| <!-- Right Panel - Settings --> | |
| <aside class="control-panel"> | |
| <h2 class="panel-title"> | |
| <i class="fas fa-cog"></i> | |
| Settings | |
| </h2> | |
| <div class="setting-item"> | |
| <label class="setting-label"> | |
| Detection Confidence | |
| <span class="slider-value" id="confidenceValue">0.5</span> | |
| </label> | |
| <input type="range" class="slider" id="confidenceSlider" min="0" max="1" step="0.1" value="0.5"> | |
| </div> | |
| <div class="setting-item"> | |
| <label class="setting-label"> | |
| Max Faces | |
| <span class="slider-value" id="maxFacesValue">1</span> | |
| </label> | |
| <input type="range" class="slider" id="maxFacesSlider" min="1" max="5" step="1" value="1"> | |
| </div> | |
| <div class="setting-item"> | |
| <label class="setting-label"> | |
| Max Hands | |
| <span class="slider-value" id="maxHandsValue">2</span> | |
| </label> | |
| <input type="range" class="slider" id="maxHandsSlider" min="1" max="4" step="1" value="2"> | |
| </div> | |
| <div class="setting-item"> | |
| <label class="setting-label">Landmark Color</label> | |
| <div class="color-picker-group"> | |
| <div class="color-option selected" style="background: #00FF00" data-color="#00FF00"></div> | |
| <div class="color-option" style="background: #FF0000" data-color="#FF0000"></div> | |
| <div class="color-option" style="background: #00FFFF" data-color="#00FFFF"></div> | |
| <div class="color-option" style="background: #FFFF00" data-color="#FFFF00"></div> | |
| <div class="color-option" style="background: #FF00FF" data-color="#FF00FF"></div> | |
| <div class="color-option" style="background: #FFFFFF" data-color="#FFFFFF"></div> | |
| </div> | |
| </div> | |
| <div class="info-card"> | |
| <h4>Performance</h4> | |
| <p>Adjust confidence threshold to balance accuracy and performance. Lower values detect more but may include false positives.</p> | |
| </div> | |
| </aside> | |
| </div> | |
| </main> | |
| <!-- Toast Notification --> | |
| <div class="toast" id="toast"> | |
| <i class="toast-icon fas fa-check-circle"></i> | |
| <div class="toast-message" id="toastMessage">Operation successful!</div> | |
| </div> | |
| <script> | |
| // Global Variables | |
| let camera = null; | |
| let isStreaming = false; | |
| let activeFeatures = new Set(); | |
| let landmarkColor = '#00FF00'; | |
| let frameCount = 0; | |
| let lastTime = performance.now(); | |
| let fps = 0; | |
| let detectionCount = 0; | |
| let isRecording = false; | |
| let mediaRecorder = null; | |
| let recordedChunks = []; | |
| // MediaPipe Models | |
| let faceMesh = null; | |
| let hands = null; | |
| let pose = null; | |
| let faceDetection = null; | |
| // DOM Elements | |
| const inputVideo = document.getElementById('inputVideo'); | |
| const outputCanvas = document.getElementById('outputCanvas'); | |
| const canvasCtx = outputCanvas.getContext('2d'); | |
| const loadingOverlay = document.getElementById('loadingOverlay'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const stopBtn = document.getElementById('stopBtn'); | |
| const screenshotBtn = document.getElementById('screenshotBtn'); | |
| const recordBtn = document.getElementById('recordBtn'); | |
| // Initialize MediaPipe Models | |
| function initializeMediaPipe() { | |
| // Face Mesh | |
| faceMesh = new FaceMesh({ | |
| locateFile: (file) => { | |
| return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`; | |
| } | |
| }); | |
| faceMesh.setOptions({ | |
| maxNumFaces: parseInt(document.getElementById('maxFacesSlider').value), | |
| refineLandmarks: true, | |
| minDetectionConfidence: parseFloat(document.getElementById('confidenceSlider').value), | |
| minTrackingConfidence: 0.5 | |
| }); | |
| // Hands | |
| hands = new Hands({ | |
| locateFile: (file) => { | |
| return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; | |
| } | |
| }); | |
| hands.setOptions({ | |
| maxNumHands: parseInt(document.getElementById('maxHandsSlider').value), | |
| modelComplexity: 1, | |
| minDetectionConfidence: parseFloat(document.getElementById('confidenceSlider').value), | |
| minTrackingConfidence: 0.5 | |
| }); | |
| // Pose | |
| pose = new Pose({ | |
| locateFile: (file) => { | |
| return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`; | |
| } | |
| }); | |
| pose.setOptions({ | |
| modelComplexity: 1, | |
| smoothLandmarks: true, | |
| minDetectionConfidence: parseFloat(document.getElementById('confidenceSlider').value), | |
| minTrackingConfidence: 0.5 | |
| }); | |
| // Face Detection | |
| faceDetection = new FaceDetection({ | |
| locateFile: (file) => { | |
| return `https://cdn.jsdelivr.net/npm/@mediapipe/face_detection/${file}`; | |
| } | |
| }); | |
| faceDetection.setOptions({ | |
| model: 'short', | |
| minDetectionConfidence: parseFloat(document.getElementById('confidenceSlider').value) | |
| }); | |
| } | |
| // Start Camera | |
| async function startCamera() { | |
| try { | |
| loadingOverlay.classList.remove('hidden'); | |
| if (!camera) { | |
| camera = new Camera(inputVideo, { | |
| onFrame: async () => { | |
| if (isStreaming) { | |
| await processFrame(); | |
| } | |
| }, | |
| width: 1280, | |
| height: 720 | |
| }); | |
| } | |
| await camera.start(); | |
| isStreaming = true; | |
| startBtn.disabled = true; | |
| stopBtn.disabled = false; | |
| screenshotBtn.disabled = false; | |
| recordBtn.disabled = false; | |
| loadingOverlay.classList.add('hidden'); | |
| showToast('Camera started successfully!', 'success'); | |
| } catch (error) { | |
| console.error('Error starting camera:', error); | |
| showToast('Failed to start camera. Please check permissions.', 'error'); | |
| loadingOverlay.classList.add('hidden'); | |
| } | |
| } | |
| // Stop Camera | |
| function stopCamera() { | |
| if (camera) { | |
| camera.stop(); | |
| isStreaming = false; | |
| startBtn.disabled = false; | |
| stopBtn.disabled = true; | |
| screenshotBtn.disabled = true; | |
| recordBtn.disabled = true; | |
| if (isRecording) { | |
| stopRecording(); | |
| } | |
| // Clear canvas | |
| canvasCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); | |
| showToast('Camera stopped', 'info'); | |
| } | |
| } | |
| // Process Frame | |
| async function processFrame() { | |
| if (!isStreaming) return; | |
| canvasCtx.save(); | |
| canvasCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); | |
| detectionCount = 0; | |
| // Process active features | |
| if (activeFeatures.has('faceMesh')) { | |
| await faceMesh.send({image: inputVideo}); | |
| } | |
| if (activeFeatures.has('hands')) { | |
| await hands.send({image: inputVideo}); | |
| } | |
| if (activeFeatures.has('pose')) { | |
| await pose.send({image: inputVideo}); | |
| } | |
| if (activeFeatures.has('faceDetection')) { | |
| await faceDetection.send({image: inputVideo}); | |
| } | |
| canvasCtx.restore(); | |
| // Update FPS | |
| updateFPS(); | |
| updateStats(); | |
| } | |
| // Set up MediaPipe results handlers | |
| function setupResultHandlers() { | |
| // Face Mesh Results | |
| faceMesh.onResults((results) => { | |
| if (results.multiFaceLandmarks) { | |
| for (const landmarks of results.multiFaceLandmarks) { | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_TESSELATION, | |
| {color: landmarkColor, lineWidth: 1}); | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYE, | |
| {color: landmarkColor, lineWidth: 1}); | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_RIGHT_EYEBROW, | |
| {color: landmarkColor, lineWidth: 1}); | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYE, | |
| {color: landmarkColor, lineWidth: 1}); | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_LEFT_EYEBROW, | |
| {color: landmarkColor, lineWidth: 1}); | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_FACE_OVAL, | |
| {color: landmarkColor, lineWidth: 1}); | |
| drawConnectors(canvasCtx, landmarks, FACEMESH_LIPS, | |
| {color: landmarkColor, lineWidth: 1}); | |
| } | |
| detectionCount += results.multiFaceLandmarks.length; | |
| } | |
| }); | |
| // Hands Results | |
| hands.onResults((results) => { | |
| if (results.multiHandLandmarks) { | |
| for (const landmarks of results.multiHandLandmarks) { | |
| drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, | |
| {color: landmarkColor, lineWidth: 3}); | |
| drawLandmarks(canvasCtx, landmarks, | |
| {color: landmarkColor, lineWidth: 1, radius: 3}); | |
| } | |
| detectionCount += results.multiHandLandmarks.length; | |
| } | |
| }); | |
| // Pose Results | |
| pose.onResults((results) => { | |
| if (results.poseLandmarks) { | |
| drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS, | |
| {color: landmarkColor, lineWidth: 3}); | |
| drawLandmarks(canvasCtx, results.poseLandmarks, | |
| {color: landmarkColor, lineWidth: 1, radius: 3}); | |
| detectionCount++; | |
| } | |
| }); | |
| // Face Detection Results | |
| faceDetection.onResults((results) => { | |
| if (results.detections) { | |
| for (const detection of results.detections) { | |
| drawRectangle(canvasCtx, detection.boundingBox, | |
| {color: landmarkColor, lineWidth: 3, fillColor: landmarkColor + '20'}); | |
| } | |
| detectionCount += results.detections.length; | |
| } | |
| }); | |
| } | |
| // Update FPS | |
| function updateFPS() { | |
| frameCount++; | |
| const currentTime = performance.now(); | |
| const deltaTime = currentTime - lastTime; | |
| if (deltaTime >= 1000) { | |
| fps = Math.round((frameCount * 1000) / deltaTime); | |
| document.getElementById('fpsValue').textContent = fps; | |
| document.getElementById('fpsMonitor').textContent = fps + ' FPS'; | |
| frameCount = 0; | |
| lastTime = currentTime; | |
| } | |
| } | |
| // Update Statistics | |
| function updateStats() { | |
| document.getElementById('detectionCount').textContent = detectionCount; | |
| document.getElementById('activeFeatures').textContent = activeFeatures.size; | |
| } | |
| // Take Screenshot | |
| function takeScreenshot() { | |
| const link = document.createElement('a'); | |
| link.download = `mediapipe-screenshot-${Date.now()}.png`; | |
| link.href = outputCanvas.toDataURL(); | |
| link.click(); | |
| showToast('Screenshot saved!', 'success'); | |
| } | |
| // Start Recording | |
| function startRecording() { | |
| const stream = outputCanvas.captureStream(30); | |
| mediaRecorder = new MediaRecorder(stream, { | |
| mimeType: 'video/webm' | |
| }); | |
| recordedChunks = []; | |
| mediaRecorder.ondataavailable = (event) => { | |
| if (event.data.size > 0) { | |
| recordedChunks.push(event.data); | |
| } | |
| }; | |
| mediaRecorder.onstop = () => { | |
| const blob = new Blob(recordedChunks, { | |
| type: 'video/webm' | |
| }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `mediapipe-recording-${Date.now()}.webm`; | |
| link.click(); | |
| showToast('Recording saved!', 'success'); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| recordBtn.innerHTML = '<i class="fas fa-stop"></i> Stop Recording'; | |
| recordBtn.classList.remove('btn-accent'); | |
| recordBtn.classList.add('btn-danger'); | |
| showToast('Recording started', 'info'); | |
| } | |
| // Stop Recording | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| recordBtn.innerHTML = '<i class="fas fa-video"></i> Record'; | |
| recordBtn.classList.remove('btn-danger'); | |
| recordBtn.classList.add('btn-accent'); | |
| } | |
| } | |
| // Show Toast Notification | |
| function showToast(message, type = 'info') { | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| const toastIcon = toast.querySelector('.toast-icon'); | |
| toastMessage.textContent = message; | |
| toast.className = `toast ${type} show`; | |
| // Update icon based on type | |
| if (type === 'success') { | |
| toastIcon.className = 'toast-icon fas fa-check-circle'; | |
| } else if (type === 'error') { | |
| toastIcon.className = 'toast-icon fas fa-exclamation-circle'; | |
| } else { | |
| toastIcon.className = 'toast-icon fas fa-info-circle'; | |
| } | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| // Event Listeners | |
| startBtn.addEventListener('click', startCamera); | |
| stopBtn.addEventListener('click', stopCamera); | |
| screenshotBtn.addEventListener('click', takeScreenshot); | |
| recordBtn.addEventListener('click', () => { | |
| if (isRecording) { | |
| stopRecording(); | |
| } else { | |
| startRecording(); | |
| } | |
| }); | |
| // Feature Toggles | |
| document.querySelectorAll('.feature-toggle').forEach(toggle => { | |
| toggle.addEventListener('click', function() { | |
| const feature = this.dataset.feature; | |
| const switchElement = this.querySelector('.toggle-switch'); | |
| switchElement.classList.toggle('active'); | |
| this.classList.toggle('active'); | |
| if (activeFeatures.has(feature)) { | |
| activeFeatures.delete(feature); | |
| } else { | |
| activeFeatures.add(feature); | |
| } | |
| updateStats(); | |
| showToast(`${feature} ${activeFeatures.has(feature) ? 'enabled' : 'disabled'}`, 'info'); | |
| }); | |
| }); | |
| // Settings Controls | |
| document.getElementById('confidenceSlider').addEventListener('input', function() { | |
| const value = this.value; | |
| document.getElementById('confidenceValue').textContent = value; | |
| if (faceMesh) faceMesh.setOptions({minDetectionConfidence: parseFloat(value)}); | |
| if (hands) hands.setOptions({minDetectionConfidence: parseFloat(value)}); | |
| if (pose) pose.setOptions({minDetectionConfidence: parseFloat(value)}); | |
| if (faceDetection) faceDetection.setOptions({minDetectionConfidence: parseFloat(value)}); | |
| }); | |
| document.getElementById('maxFacesSlider').addEventListener('input', function() { | |
| const value = this.value; | |
| document.getElementById('maxFacesValue').textContent = value; | |
| if (faceMesh) faceMesh.setOptions({maxNumFaces: parseInt(value)}); | |
| }); | |
| document.getElementById('maxHandsSlider').addEventListener('input', function() { | |
| const value = this.value; | |
| document.getElementById('maxHandsValue').textContent = value; | |
| if (hands) hands.setOptions({maxNumHands: parseInt(value)}); | |
| }); | |
| // Color Picker | |
| document.querySelectorAll('.color-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| document.querySelectorAll('.color-option').forEach(opt => opt.classList.remove('selected')); |