| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>OBS AI Background Remover</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.11.0/dist/tf.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix@2.0.5/dist/body-pix.min.js"></script> |
| <style> |
| .video-container { |
| position: relative; |
| width: 100%; |
| height: 0; |
| padding-bottom: 56.25%; |
| } |
| .video-element { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| } |
| .processing-overlay { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background-color: rgba(0, 0, 0, 0.7); |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| color: white; |
| font-size: 1.5rem; |
| z-index: 10; |
| } |
| .hidden { |
| display: none; |
| } |
| .settings-panel { |
| transition: all 0.3s ease; |
| max-height: 0; |
| overflow: hidden; |
| } |
| .settings-panel.open { |
| max-height: 500px; |
| } |
| .preview-thumbnail { |
| transition: all 0.2s ease; |
| } |
| .preview-thumbnail:hover { |
| transform: scale(1.05); |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
| } |
| .preview-thumbnail.active { |
| border: 3px solid #3b82f6; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-white"> |
| <div class="container mx-auto px-4 py-8"> |
| <div class="flex flex-col md:flex-row gap-8"> |
| |
| <div class="flex-1"> |
| <div class="bg-gray-800 rounded-lg shadow-xl p-6"> |
| <h1 class="text-3xl font-bold mb-6 text-blue-400">OBS AI Background Remover</h1> |
| |
| |
| <div class="mb-8"> |
| <h2 class="text-xl font-semibold mb-4">Live Preview</h2> |
| <div class="video-container bg-gray-700 rounded-lg overflow-hidden relative"> |
| <video id="videoInput" class="video-element" autoplay muted></video> |
| <canvas id="outputCanvas" class="video-element hidden"></canvas> |
| <div id="processingOverlay" class="processing-overlay hidden"> |
| <div class="text-center"> |
| <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-400 mx-auto mb-4"></div> |
| <p>Loading AI Model...</p> |
| </div> |
| </div> |
| <div id="noVideoOverlay" class="processing-overlay"> |
| <p>No video source selected</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mb-8"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-xl font-semibold">Background Replacement</h2> |
| <button id="settingsToggle" class="text-blue-400 hover:text-blue-300"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> |
| </svg> |
| </button> |
| </div> |
| |
| |
| <div id="settingsPanel" class="settings-panel bg-gray-700 rounded-lg p-4 mb-4"> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-sm font-medium mb-1">AI Model Quality</label> |
| <select id="modelQuality" class="w-full bg-gray-600 border border-gray-500 rounded-md px-3 py-2"> |
| <option value="0.5">Fast (Low Quality)</option> |
| <option value="0.75" selected>Balanced</option> |
| <option value="1.0">High Quality (Slow)</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium mb-1">Edge Smoothness</label> |
| <select id="edgeSmoothness" class="w-full bg-gray-600 border border-gray-500 rounded-md px-3 py-2"> |
| <option value="1">Low</option> |
| <option value="2" selected>Medium</option> |
| <option value="3">High</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium mb-1">Background Blur</label> |
| <input id="bgBlur" type="range" min="0" max="10" value="0" class="w-full"> |
| <div class="flex justify-between text-xs text-gray-400"> |
| <span>Off</span> |
| <span>Max</span> |
| </div> |
| </div> |
| <div> |
| <label class="block text-sm font-medium mb-1">Foreground Brightness</label> |
| <input id="fgBrightness" type="range" min="80" max="120" value="100" class="w-full"> |
| <div class="flex justify-between text-xs text-gray-400"> |
| <span>Darker</span> |
| <span>Brighter</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4"> |
| |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer active" id="bgTransparent"> |
| <div class="aspect-w-16 aspect-h-9 bg-gradient-to-br from-gray-600 to-gray-800 flex items-center justify-center"> |
| <div class="text-center p-2"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> |
| </svg> |
| <span class="text-xs mt-1">Transparent</span> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgBlack"> |
| <div class="aspect-w-16 aspect-h-9 bg-black flex items-center justify-center"> |
| <span class="text-xs">Solid Black</span> |
| </div> |
| </div> |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgGray"> |
| <div class="aspect-w-16 aspect-h-9 bg-gray-600 flex items-center justify-center"> |
| <span class="text-xs">Solid Gray</span> |
| </div> |
| </div> |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgGreen"> |
| <div class="aspect-w-16 aspect-h-9 bg-green-600 flex items-center justify-center"> |
| <span class="text-xs">Solid Green</span> |
| </div> |
| </div> |
| |
| |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer relative" id="bgCustom"> |
| <div class="aspect-w-16 aspect-h-9 bg-gradient-to-br from-purple-600 to-blue-600 flex items-center justify-center"> |
| <div class="text-center p-2"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> |
| </svg> |
| <span class="text-xs mt-1">Custom Image</span> |
| </div> |
| </div> |
| <input type="file" id="customBgInput" accept="image/*, video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"> |
| </div> |
| |
| |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer relative" id="bgVideo"> |
| <div class="aspect-w-16 aspect-h-9 bg-gradient-to-br from-red-600 to-yellow-600 flex items-center justify-center"> |
| <div class="text-center p-2"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mx-auto text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /> |
| </svg> |
| <span class="text-xs mt-1">Custom Video</span> |
| </div> |
| </div> |
| <input type="file" id="customVideoInput" accept="video/*" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"> |
| </div> |
| |
| |
| <div class="preview-thumbnail bg-gray-700 rounded-lg overflow-hidden cursor-pointer" id="bgBlurred"> |
| <div class="aspect-w-16 aspect-h-9 bg-gray-600 flex items-center justify-center relative overflow-hidden"> |
| <div class="absolute inset-0 bg-gray-400 filter blur-md"></div> |
| <span class="text-xs relative z-10">Blurred</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="w-full md:w-80"> |
| <div class="bg-gray-800 rounded-lg shadow-xl p-6 sticky top-6"> |
| <h2 class="text-xl font-semibold mb-4">Controls</h2> |
| |
| |
| <div class="mb-6"> |
| <label class="block text-sm font-medium mb-2">Video Source</label> |
| <select id="videoSource" class="w-full bg-gray-600 border border-gray-500 rounded-md px-3 py-2"> |
| <option value="">Select a video source</option> |
| </select> |
| </div> |
| |
| |
| <div class="space-y-4 mb-6"> |
| <div> |
| <label class="inline-flex items-center"> |
| <input type="checkbox" id="toggleEffect" class="form-checkbox h-5 w-5 text-blue-400"> |
| <span class="ml-2">Enable Background Removal</span> |
| </label> |
| </div> |
| <div> |
| <label class="inline-flex items-center"> |
| <input type="checkbox" id="togglePreview" class="form-checkbox h-5 w-5 text-blue-400" checked> |
| <span class="ml-2">Show Preview</span> |
| </label> |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-700 rounded-lg p-4 mb-6"> |
| <h3 class="text-sm font-medium mb-2 text-gray-400">Performance</h3> |
| <div class="grid grid-cols-2 gap-2 text-sm"> |
| <div> |
| <div class="text-gray-400">Processing Time:</div> |
| <div id="processingTime">0 ms</div> |
| </div> |
| <div> |
| <div class="text-gray-400">FPS:</div> |
| <div id="fpsCounter">0</div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-blue-900 bg-opacity-20 rounded-lg p-4 border border-blue-800"> |
| <h3 class="text-sm font-medium mb-2 text-blue-400">OBS Integration</h3> |
| <p class="text-xs text-gray-400 mb-3">Add this as a Browser Source in OBS with these settings:</p> |
| <div class="text-xs space-y-1"> |
| <div class="flex justify-between"> |
| <span class="text-gray-400">Width:</span> |
| <span>1920</span> |
| </div> |
| <div class="flex justify-between"> |
| <span class="text-gray-400">Height:</span> |
| <span>1080</span> |
| </div> |
| <div class="flex justify-between"> |
| <span class="text-gray-400">Custom CSS:</span> |
| <span>None</span> |
| </div> |
| </div> |
| <button id="copyOBSLink" class="mt-3 w-full bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded-md text-sm flex items-center justify-center"> |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /> |
| </svg> |
| Copy OBS Browser Source URL |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const videoInput = document.getElementById('videoInput'); |
| const outputCanvas = document.getElementById('outputCanvas'); |
| const processingOverlay = document.getElementById('processingOverlay'); |
| const noVideoOverlay = document.getElementById('noVideoOverlay'); |
| const videoSourceSelect = document.getElementById('videoSource'); |
| const toggleEffect = document.getElementById('toggleEffect'); |
| const togglePreview = document.getElementById('togglePreview'); |
| const modelQuality = document.getElementById('modelQuality'); |
| const edgeSmoothness = document.getElementById('edgeSmoothness'); |
| const bgBlur = document.getElementById('bgBlur'); |
| const fgBrightness = document.getElementById('fgBrightness'); |
| const settingsToggle = document.getElementById('settingsToggle'); |
| const settingsPanel = document.getElementById('settingsPanel'); |
| const processingTimeElement = document.getElementById('processingTime'); |
| const fpsCounterElement = document.getElementById('fpsCounter'); |
| const copyOBSLink = document.getElementById('copyOBSLink'); |
| |
| |
| const bgThumbnails = document.querySelectorAll('.preview-thumbnail'); |
| const bgTransparent = document.getElementById('bgTransparent'); |
| const bgBlack = document.getElementById('bgBlack'); |
| const bgGray = document.getElementById('bgGray'); |
| const bgGreen = document.getElementById('bgGreen'); |
| const bgCustom = document.getElementById('bgCustom'); |
| const bgVideo = document.getElementById('bgVideo'); |
| const bgBlurred = document.getElementById('bgBlurred'); |
| const customBgInput = document.getElementById('customBgInput'); |
| const customVideoInput = document.getElementById('customVideoInput'); |
| |
| |
| let net; |
| let isProcessing = false; |
| let lastTimestamp = 0; |
| let frameCount = 0; |
| let fps = 0; |
| let currentBackground = 'transparent'; |
| let customBackgroundImage = null; |
| let customBackgroundVideo = null; |
| let backgroundVideoElement = null; |
| |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| |
| loadVideoSources(); |
| |
| |
| settingsToggle.addEventListener('click', () => { |
| settingsPanel.classList.toggle('open'); |
| }); |
| |
| |
| bgThumbnails.forEach(thumb => { |
| thumb.addEventListener('click', () => { |
| bgThumbnails.forEach(t => t.classList.remove('active')); |
| thumb.classList.add('active'); |
| |
| if (thumb === bgTransparent) { |
| currentBackground = 'transparent'; |
| } else if (thumb === bgBlack) { |
| currentBackground = 'black'; |
| } else if (thumb === bgGray) { |
| currentBackground = 'gray'; |
| } else if (thumb === bgGreen) { |
| currentBackground = 'green'; |
| } else if (thumb === bgCustom) { |
| currentBackground = 'customImage'; |
| customBgInput.click(); |
| } else if (thumb === bgVideo) { |
| currentBackground = 'customVideo'; |
| customVideoInput.click(); |
| } else if (thumb === bgBlurred) { |
| currentBackground = 'blurred'; |
| } |
| }); |
| }); |
| |
| |
| customBgInput.addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (file) { |
| const reader = new FileReader(); |
| reader.onload = (event) => { |
| customBackgroundImage = new Image(); |
| customBackgroundImage.src = event.target.result; |
| customBackgroundImage.onload = () => { |
| bgCustom.classList.add('active'); |
| }; |
| }; |
| reader.readAsDataURL(file); |
| } |
| }); |
| |
| |
| customVideoInput.addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (file) { |
| if (backgroundVideoElement) { |
| backgroundVideoElement.pause(); |
| backgroundVideoElement.remove(); |
| } |
| |
| backgroundVideoElement = document.createElement('video'); |
| backgroundVideoElement.autoplay = true; |
| backgroundVideoElement.loop = true; |
| backgroundVideoElement.muted = true; |
| backgroundVideoElement.src = URL.createObjectURL(file); |
| backgroundVideoElement.style.display = 'none'; |
| document.body.appendChild(backgroundVideoElement); |
| |
| bgVideo.classList.add('active'); |
| } |
| }); |
| |
| |
| toggleEffect.addEventListener('change', async () => { |
| if (toggleEffect.checked) { |
| await initBodyPix(); |
| } else { |
| stopProcessing(); |
| } |
| }); |
| |
| |
| togglePreview.addEventListener('change', () => { |
| if (togglePreview.checked) { |
| if (toggleEffect.checked) { |
| outputCanvas.classList.remove('hidden'); |
| videoInput.classList.add('hidden'); |
| } else { |
| videoInput.classList.remove('hidden'); |
| } |
| } else { |
| outputCanvas.classList.add('hidden'); |
| videoInput.classList.add('hidden'); |
| } |
| }); |
| |
| |
| videoSourceSelect.addEventListener('change', () => { |
| const deviceId = videoSourceSelect.value; |
| if (deviceId) { |
| startVideo(deviceId); |
| } else { |
| stopVideo(); |
| } |
| }); |
| |
| |
| copyOBSLink.addEventListener('click', () => { |
| const currentUrl = window.location.href; |
| navigator.clipboard.writeText(currentUrl).then(() => { |
| const originalText = copyOBSLink.textContent; |
| copyOBSLink.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> |
| </svg> |
| Copied! |
| `; |
| setTimeout(() => { |
| copyOBSLink.innerHTML = originalText; |
| }, 2000); |
| }); |
| }); |
| |
| |
| setInterval(updateFPS, 1000); |
| }); |
| |
| |
| async function loadVideoSources() { |
| try { |
| const devices = await navigator.mediaDevices.enumerateDevices(); |
| const videoDevices = devices.filter(device => device.kind === 'videoinput'); |
| |
| videoDevices.forEach(device => { |
| const option = document.createElement('option'); |
| option.value = device.deviceId; |
| option.text = device.label || `Camera ${videoSourceSelect.length + 1}`; |
| videoSourceSelect.appendChild(option); |
| }); |
| |
| |
| if (videoDevices.length > 0) { |
| videoSourceSelect.value = videoDevices[0].deviceId; |
| startVideo(videoDevices[0].deviceId); |
| } |
| } catch (err) { |
| console.error('Error enumerating devices:', err); |
| } |
| } |
| |
| |
| async function startVideo(deviceId) { |
| try { |
| const constraints = { |
| video: { |
| deviceId: { exact: deviceId }, |
| width: { ideal: 1280 }, |
| height: { ideal: 720 } |
| } |
| }; |
| |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); |
| videoInput.srcObject = stream; |
| noVideoOverlay.classList.add('hidden'); |
| |
| |
| if (!toggleEffect.checked) { |
| videoInput.classList.remove('hidden'); |
| outputCanvas.classList.add('hidden'); |
| } |
| |
| |
| if (toggleEffect.checked) { |
| await initBodyPix(); |
| videoInput.classList.add('hidden'); |
| outputCanvas.classList.remove('hidden'); |
| } |
| } catch (err) { |
| console.error('Error starting video:', err); |
| noVideoOverlay.textContent = 'Error accessing camera'; |
| } |
| } |
| |
| |
| function stopVideo() { |
| if (videoInput.srcObject) { |
| videoInput.srcObject.getTracks().forEach(track => track.stop()); |
| videoInput.srcObject = null; |
| } |
| videoInput.classList.add('hidden'); |
| outputCanvas.classList.add('hidden'); |
| noVideoOverlay.classList.remove('hidden'); |
| stopProcessing(); |
| } |
| |
| |
| async function initBodyPix() { |
| if (net) return; |
| |
| processingOverlay.classList.remove('hidden'); |
| outputCanvas.classList.remove('hidden'); |
| |
| try { |
| |
| const multiplier = parseFloat(modelQuality.value); |
| net = await bodyPix.load({ |
| architecture: 'MobileNetV1', |
| outputStride: 16, |
| multiplier: multiplier, |
| quantBytes: 2 |
| }); |
| |
| processingOverlay.classList.add('hidden'); |
| startProcessing(); |
| } catch (err) { |
| console.error('Error loading BodyPix model:', err); |
| processingOverlay.textContent = 'Error loading AI model'; |
| toggleEffect.checked = false; |
| } |
| } |
| |
| |
| function startProcessing() { |
| if (!net || isProcessing) return; |
| |
| isProcessing = true; |
| outputCanvas.width = videoInput.videoWidth; |
| outputCanvas.height = videoInput.videoHeight; |
| |
| processFrame(); |
| } |
| |
| |
| function stopProcessing() { |
| isProcessing = false; |
| if (net) { |
| |
| net = null; |
| } |
| } |
| |
| |
| async function processFrame() { |
| if (!isProcessing || !net || videoInput.readyState < 2) { |
| if (isProcessing) { |
| requestAnimationFrame(processFrame); |
| } |
| return; |
| } |
| |
| const startTime = performance.now(); |
| |
| try { |
| |
| const segmentation = await net.segmentPerson(videoInput, { |
| flipHorizontal: false, |
| internalResolution: 'medium', |
| segmentationThreshold: 0.7 |
| }); |
| |
| |
| const ctx = outputCanvas.getContext('2d'); |
| |
| |
| ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); |
| |
| if (currentBackground === 'transparent') { |
| |
| } else if (currentBackground === 'black') { |
| ctx.fillStyle = 'black'; |
| ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height); |
| } else if (currentBackground === 'gray') { |
| ctx.fillStyle = '#4B5563'; |
| ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height); |
| } else if (currentBackground === 'green') { |
| ctx.fillStyle = '#059669'; |
| ctx.fillRect(0, 0, outputCanvas.width, outputCanvas.height); |
| } else if (currentBackground === 'customImage' && customBackgroundImage) { |
| |
| const imgAspect = customBackgroundImage.width / customBackgroundImage.height; |
| const canvasAspect = outputCanvas.width / outputCanvas.height; |
| |
| if (imgAspect > canvasAspect) { |
| |
| const scale = outputCanvas.width / customBackgroundImage.width; |
| const scaledHeight = customBackgroundImage.height * scale; |
| const y = (outputCanvas.height - scaledHeight) / 2; |
| ctx.drawImage(customBackgroundImage, 0, y, outputCanvas.width, scaledHeight); |
| } else { |
| |
| const scale = outputCanvas.height / customBackgroundImage.height; |
| const scaledWidth = customBackgroundImage.width * scale; |
| const x = (outputCanvas.width - scaledWidth) / 2; |
| ctx.drawImage(customBackgroundImage, x, 0, scaledWidth, outputCanvas.height); |
| } |
| } else if (currentBackground === 'customVideo' && backgroundVideoElement) { |
| |
| const videoAspect = backgroundVideoElement.videoWidth / backgroundVideoElement.videoHeight; |
| const canvasAspect = outputCanvas.width / outputCanvas.height; |
| |
| if (videoAspect > canvasAspect) { |
| |
| const scale = outputCanvas.width / backgroundVideoElement.videoWidth; |
| const scaledHeight = backgroundVideoElement.videoHeight * scale; |
| const y = (outputCanvas.height - scaledHeight) / 2; |
| ctx.drawImage(backgroundVideoElement, 0, y, outputCanvas.width, scaledHeight); |
| } else { |
| |
| const scale = outputCanvas.height / backgroundVideoElement.videoHeight; |
| const scaledWidth = backgroundVideoElement.videoWidth * scale; |
| const x = (outputCanvas.width - scaledWidth) / 2; |
| ctx.drawImage(backgroundVideoElement, x, 0, scaledWidth, outputCanvas.height); |
| } |
| } else if (currentBackground === 'blurred') { |
| |
| ctx.drawImage(videoInput, 0, 0, outputCanvas.width, outputCanvas.height); |
| applyBlurEffect(ctx, outputCanvas.width, outputCanvas.height, parseInt(bgBlur.value)); |
| } |
| |
| |
| const foregroundColor = { r: 255, g: 255, b: 255, a: 255 }; |
| const backgroundColor = { r: 0, g: 0, b: 0, a: 0 }; |
| const edgeBlurAmount = parseInt(edgeSmoothness.value); |
| const brightness = parseInt(fgBrightness.value) / 100; |
| |
| bodyPix.drawBokehEffect( |
| outputCanvas, |
| videoInput, |
| segmentation, |
| parseFloat(bgBlur.value), |
| foregroundColor, |
| backgroundColor, |
| edgeBlurAmount, |
| brightness |
| ); |
| |
| |
| const endTime = performance.now(); |
| const processingTime = endTime - startTime; |
| processingTimeElement.textContent = `${processingTime.toFixed(1)} ms`; |
| |
| frameCount++; |
| |
| |
| requestAnimationFrame(processFrame); |
| } catch (err) { |
| console.error('Error processing frame:', err); |
| isProcessing = false; |
| } |
| } |
| |
| |
| function applyBlurEffect(ctx, width, height, radius) { |
| if (radius <= 0) return; |
| |
| |
| const tempCanvas = document.createElement('canvas'); |
| tempCanvas.width = width; |
| tempCanvas.height = height; |
| const tempCtx = tempCanvas.getContext('2d'); |
| |
| |
| tempCtx.drawImage(outputCanvas, 0, 0, width, height); |
| |
| |
| const iterations = Math.min(radius, 10); |
| const scale = 1 / (iterations * 0.5 + 1); |
| |
| for (let i = 0; i < iterations; i++) { |
| |
| const scaledWidth = width * scale; |
| const scaledHeight = height * scale; |
| |
| tempCtx.drawImage( |
| tempCanvas, |
| 0, 0, width, height, |
| 0, 0, scaledWidth, scaledHeight |
| ); |
| |
| |
| tempCtx.drawImage( |
| tempCanvas, |
| 0, 0, scaledWidth, scaledHeight, |
| 0, 0, width, height |
| ); |
| } |
| |
| |
| ctx.drawImage(tempCanvas, 0, 0, width, height); |
| } |
| |
| |
| function updateFPS() { |
| const now = performance.now(); |
| if (lastTimestamp) { |
| const delta = (now - lastTimestamp) / 1000; |
| fps = Math.round(frameCount / delta); |
| fpsCounterElement.textContent = fps; |
| } |
| lastTimestamp = now; |
| frameCount = 0; |
| } |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=TDN-M/plugin" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |