Spaces:
Running
Running
| import DeviceDetector from "https://cdn.skypack.dev/[email protected]"; | |
| // ์ฌ์ฉ ๋ฐฉ๋ฒ: testSupport({client?: string, os?: string}[]) | |
| // client์ os๋ ์ ๊ท ํํ์์ ๋๋ค. | |
| // ์ฐธ๊ณ : https://cdn.jsdelivr.net/npm/[email protected]/README.md | |
| // client์ os์ ์ ํจ ๊ฐ ํ์ธ | |
| // ํ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ํฌํธ | |
| // ์๋์ ๊ฐ์๋ ์ฐจํธ๋ฅผ ์ด๊ธฐํ | |
| let speedChart, accelerationChart; | |
| let previousPoseData = null; | |
| let lastTimestamp = 0; | |
| testSupport([ | |
| { client: 'Chrome' }, | |
| ]); | |
| // ์ฐจํธ ๊ด๋ จ ์์ ์ค์ | |
| const CHART_CONFIG = { | |
| maxDataPoints: 50, | |
| updateInterval: 100, // ์ฐจํธ ์ ๋ฐ์ดํธ ๊ฐ๊ฒฉ(ms) | |
| colors: { | |
| speed: { | |
| primary: 'rgba(75, 192, 192, 1)', | |
| background: 'rgba(75, 192, 192, 0.1)' | |
| }, | |
| acceleration: { | |
| primary: 'rgba(255, 99, 132, 1)', | |
| background: 'rgba(255, 99, 132, 0.1)' | |
| } | |
| } | |
| }; | |
| // ์ฐจํธ ์ด๊ธฐํ | |
| function initCharts() { | |
| // ์ฐจํธ๋ฅผ ๋ด์ ์ปจํ ์ด๋ ์์ฑ | |
| const chartsContainer = document.createElement('div'); | |
| chartsContainer.className = 'charts-container'; | |
| document.querySelector('.container').appendChild(chartsContainer); | |
| // ์๋ ์ฐจํธ ์ปจํ ์ด๋ | |
| const speedChartContainer = document.createElement('div'); | |
| speedChartContainer.className = 'chart-card'; | |
| const speedCanvas = document.createElement('canvas'); | |
| speedCanvas.id = 'speedChart'; | |
| speedChartContainer.appendChild(speedCanvas); | |
| chartsContainer.appendChild(speedChartContainer); | |
| // ๊ฐ์๋ ์ฐจํธ ์ปจํ ์ด๋ | |
| const accelerationChartContainer = document.createElement('div'); | |
| accelerationChartContainer.className = 'chart-card'; | |
| const accelerationCanvas = document.createElement('canvas'); | |
| accelerationCanvas.id = 'accelerationChart'; | |
| accelerationChartContainer.appendChild(accelerationCanvas); | |
| chartsContainer.appendChild(accelerationChartContainer); | |
| // ์๋ ์ฐจํธ ์ค์ | |
| speedChart = new Chart(speedCanvas.getContext('2d'), { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: '์ด๋ ์๋ (ํฝ์ /์ด)', | |
| data: [], | |
| borderColor: CHART_CONFIG.colors.speed.primary, | |
| backgroundColor: CHART_CONFIG.colors.speed.background, | |
| tension: 0.4, | |
| borderWidth: 2, | |
| fill: true, | |
| pointRadius: 0, | |
| pointHitRadius: 10 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| duration: 0 | |
| }, | |
| interaction: { | |
| intersect: false, | |
| mode: 'index' | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| labels: { | |
| font: { | |
| family: '"Titillium Web", sans-serif', | |
| size: 14 | |
| }, | |
| color: '#333' | |
| } | |
| }, | |
| tooltip: { | |
| enabled: true, | |
| backgroundColor: 'rgba(0, 0, 0, 0.7)', | |
| titleFont: { | |
| family: '"Titillium Web", sans-serif' | |
| }, | |
| bodyFont: { | |
| family: '"Titillium Web", sans-serif' | |
| } | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.1)' | |
| }, | |
| ticks: { | |
| font: { | |
| family: '"Titillium Web", sans-serif' | |
| } | |
| } | |
| }, | |
| x: { | |
| grid: { | |
| display: false | |
| }, | |
| ticks: { | |
| maxRotation: 0, | |
| maxTicksLimit: 5, | |
| font: { | |
| family: '"Titillium Web", sans-serif' | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // ๊ฐ์๋ ์ฐจํธ ์ค์ | |
| accelerationChart = new Chart(accelerationCanvas.getContext('2d'), { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: '๊ฐ์๋ (ํฝ์ /์ดยฒ)', | |
| data: [], | |
| borderColor: CHART_CONFIG.colors.acceleration.primary, | |
| backgroundColor: CHART_CONFIG.colors.acceleration.background, | |
| tension: 0.4, | |
| borderWidth: 2, | |
| fill: true, | |
| pointRadius: 0, | |
| pointHitRadius: 10 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| duration: 0 | |
| }, | |
| interaction: { | |
| intersect: false, | |
| mode: 'index' | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'top', | |
| labels: { | |
| font: { | |
| family: '"Titillium Web", sans-serif', | |
| size: 14 | |
| }, | |
| color: '#333' | |
| } | |
| }, | |
| tooltip: { | |
| enabled: true, | |
| backgroundColor: 'rgba(0, 0, 0, 0.7)', | |
| titleFont: { | |
| family: '"Titillium Web", sans-serif' | |
| }, | |
| bodyFont: { | |
| family: '"Titillium Web", sans-serif' | |
| } | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| grid: { | |
| color: 'rgba(0, 0, 0, 0.1)' | |
| }, | |
| ticks: { | |
| font: { | |
| family: '"Titillium Web", sans-serif' | |
| } | |
| } | |
| }, | |
| x: { | |
| grid: { | |
| display: false | |
| }, | |
| ticks: { | |
| maxRotation: 0, | |
| maxTicksLimit: 5, | |
| font: { | |
| family: '"Titillium Web", sans-serif' | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // ๋ฐ์ํ ์ฒ๋ฆฌ | |
| window.addEventListener('resize', () => { | |
| speedChart.resize(); | |
| accelerationChart.resize(); | |
| }); | |
| } | |
| // ์์ธ ๋ณํ์ ์๋์ ๊ฐ์๋๋ฅผ ๊ณ์ฐ | |
| function calculateMotionMetrics(results, timestamp) { | |
| // ๊ธฐ๋ณธ ๊ฒ์ฆ | |
| if (!results || !results.poseLandmarks || !Array.isArray(results.poseLandmarks)) { | |
| return { speed: 0, acceleration: 0 }; | |
| } | |
| // ์ด๊ธฐ ์ํ | |
| if (!previousPoseData || !previousPoseData.poseLandmarks) { | |
| previousPoseData = { | |
| poseLandmarks: [...results.poseLandmarks] | |
| }; | |
| lastTimestamp = timestamp; | |
| return { speed: 0, acceleration: 0 }; | |
| } | |
| const deltaTime = (timestamp - lastTimestamp) / 1000; // ์ด ๋จ์๋ก ๋ณํ | |
| if (deltaTime === 0) { | |
| return { speed: 0, acceleration: 0 }; | |
| } | |
| // ํคํฌ์ธํธ ํ๊ท ์ด๋๊ฑฐ๋ฆฌ ๊ณ์ฐ | |
| let totalDisplacement = 0; | |
| let validPoints = 0; | |
| try { | |
| // ์ ํจํ ํคํฌ์ธํธ๋ง ์ฌ์ฉ | |
| results.poseLandmarks.forEach((landmark, index) => { | |
| const prevLandmark = previousPoseData.poseLandmarks[index]; | |
| if ( | |
| landmark && prevLandmark && | |
| typeof landmark.x === 'number' && | |
| typeof landmark.y === 'number' && | |
| typeof prevLandmark.x === 'number' && | |
| typeof prevLandmark.y === 'number' && | |
| // ์ ํ ์ฌํญ: ๊ฐ์์ฑ(visibility) ๊ฐ์ด ์ผ์ ๊ธฐ์ค ์ด์์ผ ๋๋ง | |
| (!landmark.visibility || landmark.visibility > 0.5) && | |
| (!prevLandmark.visibility || prevLandmark.visibility > 0.5) | |
| ) { | |
| const dx = landmark.x - prevLandmark.x; | |
| const dy = landmark.y - prevLandmark.y; | |
| const displacement = Math.sqrt(dx * dx + dy * dy); | |
| // ๋น์ ์์ ์ผ๋ก ํฐ ์ด๋ ๊ฑฐ๋ฆฌ๋ ํํฐ๋ง | |
| if (displacement < 1.0) { | |
| totalDisplacement += displacement; | |
| validPoints++; | |
| } | |
| } | |
| }); | |
| } catch (error) { | |
| console.warn('์ด๋ ๊ฑฐ๋ฆฌ ๊ณ์ฐ ์ค ์ค๋ฅ:', error); | |
| return { speed: 0, acceleration: 0 }; | |
| } | |
| // ์ ํจ ํฌ์ธํธ๊ฐ ์๋ค๋ฉด 0 ๋ฐํ | |
| if (validPoints === 0) { | |
| return { speed: 0, acceleration: 0 }; | |
| } | |
| // ํ๊ท ์ด๋๊ฑฐ๋ฆฌ ๋ฐ ์๋ ๊ณ์ฐ | |
| const averageDisplacement = totalDisplacement / validPoints; | |
| const currentSpeed = averageDisplacement / deltaTime; | |
| // ์ด์ ์๋๊ฐ ์์ผ๋ฉด 0 | |
| let previousSpeed = 0; | |
| try { | |
| previousSpeed = speedChart.data.datasets[0].data[ | |
| speedChart.data.datasets[0].data.length - 1 | |
| ] || 0; | |
| } catch (error) { | |
| console.warn('์ด์ ์๋ ์ ๊ทผ ์ค ์ค๋ฅ:', error); | |
| } | |
| // ๊ฐ์๋ ๊ณ์ฐ | |
| const acceleration = (currentSpeed - previousSpeed) / deltaTime; | |
| // ๋ค์ ํ๋ ์ ๊ณ์ฐ์ ์ํด ์ด์ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ | |
| previousPoseData = { | |
| poseLandmarks: [...results.poseLandmarks] | |
| }; | |
| lastTimestamp = timestamp; | |
| // ๊ฐ ๊ฒ์ฆ ๋ฐ ์ ํ | |
| const metrics = { | |
| speed: isFinite(currentSpeed) ? Math.min(Math.max(currentSpeed, 0), 1000) : 0, | |
| acceleration: isFinite(acceleration) ? Math.min(Math.max(acceleration, -1000), 1000) : 0 | |
| }; | |
| return metrics; | |
| } | |
| // ์ฐจํธ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฐ์ดํธํ๋ ํจ์ (์ค๋ฅ ์ฒ๋ฆฌ ํฌํจ) | |
| function updateCharts(metrics) { | |
| if (!metrics || typeof metrics.speed !== 'number' || typeof metrics.acceleration !== 'number') { | |
| console.warn('์๋ชป๋ metrics ๋ฐ์ดํฐ:', metrics); | |
| return; | |
| } | |
| try { | |
| const timestamp = new Date().toLocaleTimeString('ko-KR', { | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }); | |
| // ์ฐจํธ ๊ฐ์ฒด๊ฐ ์ ๋๋ก ์ด๊ธฐํ๋์๋์ง ํ์ธ | |
| if (!speedChart || !speedChart.data || !speedChart.data.labels) { | |
| console.warn('์๋ ์ฐจํธ๊ฐ ์ ์์ ์ผ๋ก ์ด๊ธฐํ๋์ง ์์์ต๋๋ค.'); | |
| return; | |
| } | |
| if (!accelerationChart || !accelerationChart.data || !accelerationChart.data.labels) { | |
| console.warn('๊ฐ์๋ ์ฐจํธ๊ฐ ์ ์์ ์ผ๋ก ์ด๊ธฐํ๋์ง ์์์ต๋๋ค.'); | |
| return; | |
| } | |
| // ์๋ ์ฐจํธ ์ ๋ฐ์ดํธ | |
| speedChart.data.labels.push(timestamp); | |
| speedChart.data.datasets[0].data.push(metrics.speed); | |
| if (speedChart.data.labels.length > CHART_CONFIG.maxDataPoints) { | |
| speedChart.data.labels.shift(); | |
| speedChart.data.datasets[0].data.shift(); | |
| } | |
| // ๊ฐ์๋ ์ฐจํธ ์ ๋ฐ์ดํธ | |
| accelerationChart.data.labels.push(timestamp); | |
| accelerationChart.data.datasets[0].data.push(metrics.acceleration); | |
| if (accelerationChart.data.labels.length > CHART_CONFIG.maxDataPoints) { | |
| accelerationChart.data.labels.shift(); | |
| accelerationChart.data.datasets[0].data.shift(); | |
| } | |
| // requestAnimationFrame์ ์ฌ์ฉํด ์ฐจํธ ์ ๋ฐ์ดํธ ์ต์ ํ | |
| requestAnimationFrame(() => { | |
| try { | |
| speedChart.update('none'); | |
| accelerationChart.update('none'); | |
| } catch (error) { | |
| console.warn('์ฐจํธ ์ ๋ฐ์ดํธ ์ค ์ค๋ฅ:', error); | |
| } | |
| }); | |
| } catch (error) { | |
| console.warn('updateCharts์์ ์ค๋ฅ ๋ฐ์:', error); | |
| } | |
| } | |
| function testSupport(supportedDevices) { | |
| const deviceDetector = new DeviceDetector(); | |
| const detectedDevice = deviceDetector.parse(navigator.userAgent); | |
| let isSupported = false; | |
| for (const device of supportedDevices) { | |
| if (device.client !== undefined) { | |
| const re = new RegExp(`^${device.client}$`); | |
| if (!re.test(detectedDevice.client.name)) { | |
| continue; | |
| } | |
| } | |
| if (device.os !== undefined) { | |
| const re = new RegExp(`^${device.os}$`); | |
| if (!re.test(detectedDevice.os.name)) { | |
| continue; | |
| } | |
| } | |
| isSupported = true; | |
| break; | |
| } | |
| if (!isSupported) { | |
| alert(`์ด ๋ฐ๋ชจ๋ ${detectedDevice.client.name}/${detectedDevice.os.name} ์์ ์คํ๋ ๋ ` + | |
| `์์ ํ ์ง์๋์ง ์์ต๋๋ค. ๊ณ์ ์ฌ์ฉํ ๊ฒฝ์ฐ ๋ณธ์ธ ์ฑ ์ ํ์ ์งํํ์๊ธฐ ๋ฐ๋๋๋ค.`); | |
| } | |
| } | |
| const controls = window; | |
| const mpHolistic = window; | |
| const drawingUtils = window; | |
| const config = { locateFile: (file) => { | |
| return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@` + | |
| `${mpHolistic.VERSION}/${file}`; | |
| } }; | |
| // ์ ๋ ฅ ํ๋ ์์ ์ฌ๊ธฐ์ ๊ฐ์ ธ์ต๋๋ค. | |
| const videoElement = document.getElementsByClassName('input_video')[0]; | |
| const canvasElement = document.getElementsByClassName('output_canvas')[0]; | |
| const controlsElement = document.getElementsByClassName('control-panel')[0]; | |
| const canvasCtx = canvasElement.getContext('2d'); | |
| // ์ดํ tick()์ด ํธ์ถ๋ ๋๋ง๋ค ์ฐธ์กฐํ FPS ์ปจํธ๋กค | |
| const fpsControl = new controls.FPS(); | |
| // ๋ก๋ฉ ์คํผ๋๋ฅผ ์จ๊ธฐ๋ ์ต์ ํ | |
| const spinner = document.querySelector('.loading'); | |
| spinner.ontransitionend = () => { | |
| spinner.style.display = 'none'; | |
| }; | |
| function removeElements(landmarks, elements) { | |
| for (const element of elements) { | |
| delete landmarks[element]; | |
| } | |
| } | |
| function removeLandmarks(results) { | |
| if (results.poseLandmarks) { | |
| removeElements(results.poseLandmarks, [ | |
| 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, | |
| 15, 16, 17, 18, 19, 20, 21, 22 | |
| ]); | |
| } | |
| } | |
| function connect(ctx, connectors) { | |
| const canvas = ctx.canvas; | |
| for (const connector of connectors) { | |
| const from = connector[0]; | |
| const to = connector[1]; | |
| if (from && to) { | |
| if (from.visibility && to.visibility && | |
| (from.visibility < 0.1 || to.visibility < 0.1)) { | |
| continue; | |
| } | |
| ctx.beginPath(); | |
| ctx.moveTo(from.x * canvas.width, from.y * canvas.height); | |
| ctx.lineTo(to.x * canvas.width, to.y * canvas.height); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| let activeEffect = 'mask'; | |
| function onResults(results) { | |
| // ๋ก๋ฉ ์คํผ๋ ์จ๊ธฐ๊ธฐ | |
| document.body.classList.add('loaded'); | |
| // ๊ทธ๋ฆด ํ์ ์๋ ๋๋๋งํฌ ์ ๊ฑฐ | |
| removeLandmarks(results); | |
| // FPS ์ ๋ฐ์ดํธ | |
| fpsControl.tick(); | |
| // ์บ๋ฒ์ค์ ๊ทธ๋ฆฌ๊ธฐ | |
| canvasCtx.save(); | |
| canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); | |
| if (results.segmentationMask) { | |
| canvasCtx.drawImage( | |
| results.segmentationMask, | |
| 0, 0, canvasElement.width, canvasElement.height | |
| ); | |
| // ๊ธฐ์กด ํฝ์ ๋ง ๋ฎ์ด์ฐ๊ธฐ | |
| if (activeEffect === 'mask' || activeEffect === 'both') { | |
| canvasCtx.globalCompositeOperation = 'source-in'; | |
| canvasCtx.fillStyle = '#00FF007F'; | |
| canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height); | |
| } else { | |
| canvasCtx.globalCompositeOperation = 'source-out'; | |
| canvasCtx.fillStyle = '#0000FF7F'; | |
| canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height); | |
| } | |
| // ๋๋ฝ๋ ํฝ์ ๋ง ๋ฎ์ด์ฐ๊ธฐ | |
| canvasCtx.globalCompositeOperation = 'destination-atop'; | |
| canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); | |
| canvasCtx.globalCompositeOperation = 'source-over'; | |
| } else { | |
| canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); | |
| } | |
| // ๋์ ์งํ ๊ณ์ฐ ํ ์ฐจํธ ์ ๋ฐ์ดํธ | |
| const metrics = calculateMotionMetrics(results, performance.now()); | |
| updateCharts(metrics); | |
| // ์์ ๊ฒ์ฌ | |
| if (!results.poseLandmarks || !mpHolistic.POSE_LANDMARKS) { | |
| canvasCtx.restore(); | |
| return; | |
| } | |
| // ํ๊ฟ์น์์ ์๊น์ง ์ฐ๊ฒฐ | |
| canvasCtx.lineWidth = 5; | |
| if (results.poseLandmarks) { | |
| if (results.rightHandLandmarks && results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW]) { | |
| canvasCtx.strokeStyle = 'white'; | |
| connect(canvasCtx, [[ | |
| results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW], | |
| results.rightHandLandmarks[0] | |
| ]]); | |
| } | |
| if (results.leftHandLandmarks && results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW]) { | |
| canvasCtx.strokeStyle = 'white'; | |
| connect(canvasCtx, [[ | |
| results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW], | |
| results.leftHandLandmarks[0] | |
| ]]); | |
| } | |
| } | |
| // ์ ์ ์์ธ ์ฐ๊ฒฐ | |
| if (results.poseLandmarks && mpHolistic.POSE_CONNECTIONS) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.poseLandmarks, | |
| mpHolistic.POSE_CONNECTIONS, | |
| { color: 'white' } | |
| ); | |
| } | |
| // ์ผ์ชฝ ์์ธ ๋๋๋งํฌ | |
| if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_LEFT) { | |
| const leftLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_LEFT) | |
| .map(index => results.poseLandmarks[index]) | |
| .filter(landmark => landmark !== undefined); | |
| if (leftLandmarks.length > 0) { | |
| drawingUtils.drawLandmarks(canvasCtx, leftLandmarks, { | |
| visibilityMin: 0.65, | |
| color: 'white', | |
| fillColor: 'rgb(255,138,0)' | |
| }); | |
| } | |
| } | |
| // ์ค๋ฅธ์ชฝ ์์ธ ๋๋๋งํฌ | |
| if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_RIGHT) { | |
| const rightLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_RIGHT) | |
| .map(index => results.poseLandmarks[index]) | |
| .filter(landmark => landmark !== undefined); | |
| if (rightLandmarks.length > 0) { | |
| drawingUtils.drawLandmarks(canvasCtx, rightLandmarks, { | |
| visibilityMin: 0.65, | |
| color: 'white', | |
| fillColor: 'rgb(0,217,231)' | |
| }); | |
| } | |
| } | |
| // ์ ๋๋๋งํฌ | |
| if (results.rightHandLandmarks && mpHolistic.HAND_CONNECTIONS) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.rightHandLandmarks, | |
| mpHolistic.HAND_CONNECTIONS, | |
| { color: 'white' } | |
| ); | |
| drawingUtils.drawLandmarks(canvasCtx, results.rightHandLandmarks, { | |
| color: 'white', | |
| fillColor: 'rgb(0,217,231)', | |
| lineWidth: 2, | |
| radius: (data) => { | |
| return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1); | |
| } | |
| }); | |
| } | |
| if (results.leftHandLandmarks && mpHolistic.HAND_CONNECTIONS) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.leftHandLandmarks, | |
| mpHolistic.HAND_CONNECTIONS, | |
| { color: 'white' } | |
| ); | |
| drawingUtils.drawLandmarks(canvasCtx, results.leftHandLandmarks, { | |
| color: 'white', | |
| fillColor: 'rgb(255,138,0)', | |
| lineWidth: 2, | |
| radius: (data) => { | |
| return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1); | |
| } | |
| }); | |
| } | |
| // ์ผ๊ตด ๋๋๋งํฌ | |
| if (results.faceLandmarks && mpHolistic.FACEMESH_TESSELATION) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_TESSELATION, | |
| { color: '#C0C0C070', lineWidth: 1 } | |
| ); | |
| if (mpHolistic.FACEMESH_RIGHT_EYE) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_RIGHT_EYE, | |
| { color: 'rgb(0,217,231)' } | |
| ); | |
| } | |
| if (mpHolistic.FACEMESH_RIGHT_EYEBROW) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_RIGHT_EYEBROW, | |
| { color: 'rgb(0,217,231)' } | |
| ); | |
| } | |
| if (mpHolistic.FACEMESH_LEFT_EYE) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_LEFT_EYE, | |
| { color: 'rgb(255,138,0)' } | |
| ); | |
| } | |
| if (mpHolistic.FACEMESH_LEFT_EYEBROW) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_LEFT_EYEBROW, | |
| { color: 'rgb(255,138,0)' } | |
| ); | |
| } | |
| if (mpHolistic.FACEMESH_FACE_OVAL) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_FACE_OVAL, | |
| { color: '#E0E0E0', lineWidth: 5 } | |
| ); | |
| } | |
| if (mpHolistic.FACEMESH_LIPS) { | |
| drawingUtils.drawConnectors( | |
| canvasCtx, | |
| results.faceLandmarks, | |
| mpHolistic.FACEMESH_LIPS, | |
| { color: '#E0E0E0', lineWidth: 5 } | |
| ); | |
| } | |
| } | |
| canvasCtx.restore(); | |
| } | |
| // ์์ ์ ๋ก๋ ์ฒ๋ฆฌ | |
| function handleVideoUpload(file) { | |
| // ๋น๋์ค URL ์์ฑ | |
| const videoUrl = URL.createObjectURL(file); | |
| // ์ฐจํธ ๋ฐ์ดํฐ ์ด๊ธฐํ | |
| speedChart.data.labels = []; | |
| speedChart.data.datasets[0].data = []; | |
| accelerationChart.data.labels = []; | |
| accelerationChart.data.datasets[0].data = []; | |
| previousPoseData = null; | |
| lastTimestamp = 0; | |
| // ์์ธ ๊ฐ์ง ๋ฆฌ์ | |
| holistic.reset(); | |
| // ๋น๋์ค ์์ค ์ ๋ฐ์ดํธ | |
| videoElement.src = videoUrl; | |
| // ๋น๋์ค ๋ก๋ ์๋ฃ ํ ์ฒ๋ฆฌ | |
| videoElement.onloadedmetadata = () => { | |
| // ๋น๋์ค ๋น์จ์ ๋ง์ถฐ ์บ๋ฒ์ค ์ฌ์ด์ฆ ์กฐ์ | |
| const aspect = videoElement.videoHeight / videoElement.videoWidth; | |
| let width, height; | |
| if (window.innerWidth > window.innerHeight) { | |
| height = window.innerHeight; | |
| width = height / aspect; | |
| } else { | |
| width = window.innerWidth; | |
| height = width * aspect; | |
| } | |
| canvasElement.width = width; | |
| canvasElement.height = height; | |
| // ์์์ ์ฒ๋ฆฌํ๊ธฐ ์ํ animation frame ์์ฑ | |
| let animationId; | |
| async function processFrame() { | |
| if (videoElement.paused || videoElement.ended) { | |
| cancelAnimationFrame(animationId); | |
| return; | |
| } | |
| // ํ์ฌ ํ๋ ์์ ์์ธ ๊ฐ์ง์ ์ ์ก | |
| await holistic.send({ | |
| image: videoElement | |
| }); | |
| animationId = requestAnimationFrame(processFrame); | |
| } | |
| // ๋น๋์ค ์ฌ์ ์ฒ๋ฆฌ | |
| videoElement.onplay = () => { | |
| processFrame(); | |
| }; | |
| // ์ฌ์/์ผ์์ ์ง ๋ฒํผ | |
| const playPauseBtn = document.createElement('button'); | |
| playPauseBtn.textContent = '์ฌ์/์ผ์์ ์ง'; | |
| playPauseBtn.className = 'control-button'; | |
| playPauseBtn.onclick = () => { | |
| if (videoElement.paused) { | |
| videoElement.play(); | |
| } else { | |
| videoElement.pause(); | |
| } | |
| }; | |
| // ๋ค์ ์์ ๋ฒํผ | |
| const restartBtn = document.createElement('button'); | |
| restartBtn.textContent = '์ฒ์๋ถํฐ ์ฌ์'; | |
| restartBtn.className = 'control-button'; | |
| restartBtn.onclick = () => { | |
| videoElement.currentTime = 0; | |
| if (videoElement.paused) { | |
| videoElement.play(); | |
| } | |
| }; | |
| // ๋ฒํผ์ ํ๋ฉด์ ์ถ๊ฐ | |
| const controlsContainer = document.createElement('div'); | |
| controlsContainer.className = 'video-controls'; | |
| controlsContainer.appendChild(playPauseBtn); | |
| controlsContainer.appendChild(restartBtn); | |
| // ์ ์ ํ ์์น์ ๋ฒํผ ์ฝ์ | |
| const container = document.querySelector('.container') || document.body; | |
| container.appendChild(controlsContainer); | |
| }; | |
| // ์๋ฌ ์ฒ๋ฆฌ | |
| videoElement.onerror = () => { | |
| console.error('๋น๋์ค ๋ก๋ ์คํจ'); | |
| alert('๋น๋์ค ๋ก๋์ ์คํจํ์ต๋๋ค. ๋ค๋ฅธ ๋น๋์ค ํ์ผ์ ์๋ํด๋ณด์ธ์.'); | |
| }; | |
| } | |
| // ์ผ๋ถ ๊ธฐ๋ณธ ์คํ์ผ ์ถ๊ฐ | |
| const style = document.createElement('style'); | |
| style.textContent = ` | |
| .video-controls { | |
| position: fixed; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 1000; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .control-button { | |
| padding: 10px 20px; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| .control-button:hover { | |
| background-color: rgba(0, 0, 0, 0.9); | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| const holistic = new mpHolistic.Holistic(config); | |
| holistic.onResults(onResults); | |
| // ์ปจํธ๋กค ํจ๋์ ๊ทธ๋ ค์ ์ฌ์ฉ์์๊ฒ ์ต์ ์ ์ด ์ ๊ณต | |
| new controls | |
| .ControlPanel(controlsElement, { | |
| selfieMode: true, | |
| modelComplexity: 1, | |
| smoothLandmarks: true, | |
| enableSegmentation: false, | |
| smoothSegmentation: true, | |
| minDetectionConfidence: 0.5, | |
| minTrackingConfidence: 0.5, | |
| effect: 'background', | |
| }) | |
| .add([ | |
| new controls.StaticText({ title: 'MediaPipe ์ ์ ์์ธ ๊ฐ์ง' }), | |
| fpsControl, | |
| new controls.Toggle({ title: '์ ํผ ๋ชจ๋', field: 'selfieMode' }), | |
| new controls.SourcePicker({ | |
| onSourceChanged: () => { | |
| // ์์ค ๋ณ๊ฒฝ ์ ๋ฆฌ์ . ๋ฆฌ์ ํ ์์ธ๋ฅผ ๋ ์ ๊ฐ์งํ ์ ์์ | |
| holistic.reset(); | |
| }, | |
| onFrame: async (input, size) => { | |
| const aspect = size.height / size.width; | |
| let width, height; | |
| if (window.innerWidth > window.innerHeight) { | |
| height = window.innerHeight; | |
| width = height / aspect; | |
| } else { | |
| width = window.innerWidth; | |
| height = width * aspect; | |
| } | |
| canvasElement.width = width; | |
| canvasElement.height = height; | |
| await holistic.send({ image: input }); | |
| }, | |
| }), | |
| new controls.Slider({ | |
| title: '๋ชจ๋ธ ๋ณต์ก๋', | |
| field: 'modelComplexity', | |
| discrete: ['๊ฒฝ๋', '์์ ', '๊ณ ๊ธ'], | |
| }), | |
| new controls.Toggle({ title: '๋๋๋งํฌ ํํํ', field: 'smoothLandmarks' }), | |
| new controls.Toggle({ title: '์ธ๊ทธ๋จผํ ์ด์ ์ฌ์ฉ', field: 'enableSegmentation' }), | |
| new controls.Toggle({ title: '์ธ๊ทธ๋จผํ ์ด์ ํํํ', field: 'smoothSegmentation' }), | |
| new controls.Slider({ | |
| title: '์ต์ ๊ฐ์ง ์ ๋ขฐ๋', | |
| field: 'minDetectionConfidence', | |
| range: [0, 1], | |
| step: 0.01 | |
| }), | |
| new controls.Slider({ | |
| title: '์ต์ ์ถ์ ์ ๋ขฐ๋', | |
| field: 'minTrackingConfidence', | |
| range: [0, 1], | |
| step: 0.01 | |
| }), | |
| new controls.Slider({ | |
| title: 'ํจ๊ณผ', | |
| field: 'effect', | |
| discrete: { 'background': '๋ฐฐ๊ฒฝ', 'mask': '์ ๊ฒฝ' }, | |
| }), | |
| ]) | |
| .on(x => { | |
| const options = x; | |
| videoElement.classList.toggle('selfie', options.selfieMode); | |
| activeEffect = x['effect']; | |
| holistic.setOptions(options); | |
| }); | |
| // ์ด๊ธฐํ ํจ์ | |
| function initialize() { | |
| // ์ฐจํธ ์ด๊ธฐํ | |
| initCharts(); | |
| // ๋น๋์ค ์ ๋ก๋ ์ฒ๋ฆฌ | |
| const videoUploadInput = document.querySelector('#video-upload'); | |
| if (videoUploadInput) { | |
| videoUploadInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| handleVideoUpload(e.target.files[0]); | |
| } | |
| }); | |
| } | |
| // ์์ธ ๊ฐ์ง ์ด๊ธฐํ | |
| const holistic = new mpHolistic.Holistic(config); | |
| holistic.onResults(onResults); | |
| // ... ๊ธฐ์กด ์ด๊ธฐํ ๋ก์ง์ ์ ์ง ... | |
| } | |
| // ์ ํ๋ฆฌ์ผ์ด์ ์์ | |
| window.addEventListener('load', initialize); | |
| // ์ฐฝ ํฌ๊ธฐ ๋ณ๊ฒฝ ์ฒ๋ฆฌ | |
| window.addEventListener('resize', () => { | |
| const aspect = videoElement.videoHeight / videoElement.videoWidth; | |
| let width, height; | |
| if (window.innerWidth > window.innerHeight) { | |
| height = window.innerHeight; | |
| width = height / aspect; | |
| } else { | |
| width = window.innerWidth; | |
| height = width * aspect; | |
| } | |
| canvasElement.width = width; | |
| canvasElement.height = height; | |
| // ์ฐจํธ ํฌ๊ธฐ ์ฌ์กฐ์ | |
| speedChart.resize(); | |
| accelerationChart.resize(); | |
| }); | |