live-detect-pose / holistic.js
openfree's picture
Update holistic.js
02ed9d7 verified
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();
});