|
<!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/[email protected]/dist/tf.min.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/[email protected]/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> |