pose / static /js /holistic1.js
mistpe's picture
Rename static/js/holistic.js to static/js/holistic1.js
5012b9e verified
raw
history blame
16.8 kB
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' },
]);
function initCharts() {
const speedChartCtx = document.getElementById('speedChart').getContext('2d');
const accelerationChartCtx = document.getElementById('accelerationChart').getContext('2d');
speedChart = new Chart(speedChartCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Movement Speed (px/s)',
data: [],
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.4,
borderWidth: 2,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Speed'
}
},
x: {
title: {
display: true,
text: 'Time'
}
}
}
}
});
accelerationChart = new Chart(accelerationChartCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Acceleration (px/s²)',
data: [],
borderColor: 'rgba(255, 99, 132, 1)',
tension: 0.4,
borderWidth: 2,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Acceleration'
}
},
x: {
title: {
display: true,
text: 'Time'
}
}
}
}
});
}
// 计算姿态变化的速度和加速度
function calculateMotionMetrics(currentPose, timestamp) {
if (!previousPoseData || !currentPose.poseLandmarks) {
previousPoseData = currentPose;
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;
currentPose.poseLandmarks.forEach((landmark, index) => {
if (previousPoseData.poseLandmarks[index]) {
const dx = landmark.x - previousPoseData.poseLandmarks[index].x;
const dy = landmark.y - previousPoseData.poseLandmarks[index].y;
const displacement = Math.sqrt(dx * dx + dy * dy);
totalDisplacement += displacement;
validPoints++;
}
});
const averageDisplacement = validPoints > 0 ? totalDisplacement / validPoints : 0;
const currentSpeed = averageDisplacement / deltaTime;
// 计算加速度
const previousSpeed = speedChart.data.datasets[0].data[speedChart.data.datasets[0].data.length - 1] || 0;
const acceleration = (currentSpeed - previousSpeed) / deltaTime;
// 更新先前数据
previousPoseData = currentPose;
lastTimestamp = timestamp;
return { speed: currentSpeed, acceleration: acceleration };
}
// 更新图表数据
function updateCharts(metrics) {
const timestamp = new Date().toLocaleTimeString();
const maxDataPoints = 50; // 限制数据点数量
// 更新速度图表
speedChart.data.labels.push(timestamp);
speedChart.data.datasets[0].data.push(metrics.speed);
if (speedChart.data.labels.length > maxDataPoints) {
speedChart.data.labels.shift();
speedChart.data.datasets[0].data.shift();
}
speedChart.update('none');
// 更新加速度图表
accelerationChart.data.labels.push(timestamp);
accelerationChart.data.datasets[0].data.push(metrics.acceleration);
if (accelerationChart.data.labels.length > maxDataPoints) {
accelerationChart.data.labels.shift();
accelerationChart.data.datasets[0].data.shift();
}
accelerationChart.update('none');
}
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()。
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);
// 更新帧率。
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);
}
// 连接肘部到手部。首先做这个,这样其他图形将绘制在这些标记之上。
canvasCtx.lineWidth = 5;
if (results.poseLandmarks) {
if (results.rightHandLandmarks) {
canvasCtx.strokeStyle = 'white';
connect(canvasCtx, [[
results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW],
results.rightHandLandmarks[0]
]]);
}
if (results.leftHandLandmarks) {
canvasCtx.strokeStyle = 'white';
connect(canvasCtx, [[
results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW],
results.leftHandLandmarks[0]
]]);
}
}
// 计算并更新动作指标
const metrics = calculateMotionMetrics(results, performance.now());
updateCharts(metrics);
// 姿势...
drawingUtils.drawConnectors(canvasCtx, results.poseLandmarks, mpHolistic.POSE_CONNECTIONS, { color: 'white' });
drawingUtils.drawLandmarks(canvasCtx, Object.values(mpHolistic.POSE_LANDMARKS_LEFT)
.map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(255,138,0)' });
drawingUtils.drawLandmarks(canvasCtx, Object.values(mpHolistic.POSE_LANDMARKS_RIGHT)
.map(index => results.poseLandmarks[index]), { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(0,217,231)' });
// 手...
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);
}
});
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);
}
});
// 面部...
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_TESSELATION, { color: '#C0C0C070', lineWidth: 1 });
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYE, { color: 'rgb(0,217,231)' });
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYEBROW, { color: 'rgb(0,217,231)' });
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYE, { color: 'rgb(255,138,0)' });
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYEBROW, { color: 'rgb(255,138,0)' });
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_FACE_OVAL, { color: '#E0E0E0', lineWidth: 5 });
drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LIPS, { color: '#E0E0E0', lineWidth: 5 });
canvasCtx.restore();
}
// 视频上传处理
function handleVideoUpload(file) {
const formData = new FormData();
formData.append('video', file);
// 重置图表数据
speedChart.data.labels = [];
speedChart.data.datasets[0].data = [];
accelerationChart.data.labels = [];
accelerationChart.data.datasets[0].data = [];
previousPoseData = null;
lastTimestamp = 0;
// 发送视频到服务器
fetch('/upload_video', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 更新视频源
videoElement.src = URL.createObjectURL(file);
videoElement.play();
} else {
console.error('Video upload failed:', data.error);
}
})
.catch(error => {
console.error('Error uploading video:', error);
});
}
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();
});