mistpe commited on
Commit
c81cd3c
·
verified ·
1 Parent(s): 5012b9e

Create holistic.js

Browse files
Files changed (1) hide show
  1. static/js/holistic.js +837 -0
static/js/holistic.js ADDED
@@ -0,0 +1,837 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import DeviceDetector from "https://cdn.skypack.dev/[email protected]";
2
+ // 用法: testSupport({client?: string, os?: string}[])
3
+ // Client 和 os 是正则表达式。
4
+ // 参见: https://cdn.jsdelivr.net/npm/[email protected]/README.md
5
+ // 了解 client 和 os 的合法值
6
+ // 导入必要的库
7
+
8
+ // 初始化速度和加速度图表
9
+ let speedChart, accelerationChart;
10
+ let previousPoseData = null;
11
+ let lastTimestamp = 0;
12
+ testSupport([
13
+ { client: 'Chrome' },
14
+ ]);
15
+
16
+ // 图表相关的常量配置
17
+ const CHART_CONFIG = {
18
+ maxDataPoints: 50,
19
+ updateInterval: 100, // 图表更新间隔(ms)
20
+ colors: {
21
+ speed: {
22
+ primary: 'rgba(75, 192, 192, 1)',
23
+ background: 'rgba(75, 192, 192, 0.1)'
24
+ },
25
+ acceleration: {
26
+ primary: 'rgba(255, 99, 132, 1)',
27
+ background: 'rgba(255, 99, 132, 0.1)'
28
+ }
29
+ }
30
+ };
31
+
32
+ // 初始化图表
33
+ function initCharts() {
34
+ // 创建图表容器
35
+ const chartsContainer = document.createElement('div');
36
+ chartsContainer.className = 'charts-container';
37
+ document.querySelector('.container').appendChild(chartsContainer);
38
+
39
+ // 速度图表容器
40
+ const speedChartContainer = document.createElement('div');
41
+ speedChartContainer.className = 'chart-card';
42
+ const speedCanvas = document.createElement('canvas');
43
+ speedCanvas.id = 'speedChart';
44
+ speedChartContainer.appendChild(speedCanvas);
45
+ chartsContainer.appendChild(speedChartContainer);
46
+
47
+ // 加速度图表容器
48
+ const accelerationChartContainer = document.createElement('div');
49
+ accelerationChartContainer.className = 'chart-card';
50
+ const accelerationCanvas = document.createElement('canvas');
51
+ accelerationCanvas.id = 'accelerationChart';
52
+ accelerationChartContainer.appendChild(accelerationCanvas);
53
+ chartsContainer.appendChild(accelerationChartContainer);
54
+
55
+ // 速度图表配置
56
+ speedChart = new Chart(speedCanvas.getContext('2d'), {
57
+ type: 'line',
58
+ data: {
59
+ labels: [],
60
+ datasets: [{
61
+ label: '运动速度 (像素/秒)',
62
+ data: [],
63
+ borderColor: CHART_CONFIG.colors.speed.primary,
64
+ backgroundColor: CHART_CONFIG.colors.speed.background,
65
+ tension: 0.4,
66
+ borderWidth: 2,
67
+ fill: true,
68
+ pointRadius: 0,
69
+ pointHitRadius: 10
70
+ }]
71
+ },
72
+ options: {
73
+ responsive: true,
74
+ maintainAspectRatio: false,
75
+ animation: {
76
+ duration: 0
77
+ },
78
+ interaction: {
79
+ intersect: false,
80
+ mode: 'index'
81
+ },
82
+ plugins: {
83
+ legend: {
84
+ position: 'top',
85
+ labels: {
86
+ font: {
87
+ family: '"Titillium Web", sans-serif',
88
+ size: 14
89
+ },
90
+ color: '#333'
91
+ }
92
+ },
93
+ tooltip: {
94
+ enabled: true,
95
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
96
+ titleFont: {
97
+ family: '"Titillium Web", sans-serif'
98
+ },
99
+ bodyFont: {
100
+ family: '"Titillium Web", sans-serif'
101
+ }
102
+ }
103
+ },
104
+ scales: {
105
+ y: {
106
+ beginAtZero: true,
107
+ grid: {
108
+ color: 'rgba(0, 0, 0, 0.1)'
109
+ },
110
+ ticks: {
111
+ font: {
112
+ family: '"Titillium Web", sans-serif'
113
+ }
114
+ }
115
+ },
116
+ x: {
117
+ grid: {
118
+ display: false
119
+ },
120
+ ticks: {
121
+ maxRotation: 0,
122
+ maxTicksLimit: 5,
123
+ font: {
124
+ family: '"Titillium Web", sans-serif'
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ });
131
+
132
+ // 加速度图表配置
133
+ accelerationChart = new Chart(accelerationCanvas.getContext('2d'), {
134
+ type: 'line',
135
+ data: {
136
+ labels: [],
137
+ datasets: [{
138
+ label: '加速度 (像素/秒²)',
139
+ data: [],
140
+ borderColor: CHART_CONFIG.colors.acceleration.primary,
141
+ backgroundColor: CHART_CONFIG.colors.acceleration.background,
142
+ tension: 0.4,
143
+ borderWidth: 2,
144
+ fill: true,
145
+ pointRadius: 0,
146
+ pointHitRadius: 10
147
+ }]
148
+ },
149
+ options: {
150
+ responsive: true,
151
+ maintainAspectRatio: false,
152
+ animation: {
153
+ duration: 0
154
+ },
155
+ interaction: {
156
+ intersect: false,
157
+ mode: 'index'
158
+ },
159
+ plugins: {
160
+ legend: {
161
+ position: 'top',
162
+ labels: {
163
+ font: {
164
+ family: '"Titillium Web", sans-serif',
165
+ size: 14
166
+ },
167
+ color: '#333'
168
+ }
169
+ },
170
+ tooltip: {
171
+ enabled: true,
172
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
173
+ titleFont: {
174
+ family: '"Titillium Web", sans-serif'
175
+ },
176
+ bodyFont: {
177
+ family: '"Titillium Web", sans-serif'
178
+ }
179
+ }
180
+ },
181
+ scales: {
182
+ y: {
183
+ beginAtZero: true,
184
+ grid: {
185
+ color: 'rgba(0, 0, 0, 0.1)'
186
+ },
187
+ ticks: {
188
+ font: {
189
+ family: '"Titillium Web", sans-serif'
190
+ }
191
+ }
192
+ },
193
+ x: {
194
+ grid: {
195
+ display: false
196
+ },
197
+ ticks: {
198
+ maxRotation: 0,
199
+ maxTicksLimit: 5,
200
+ font: {
201
+ family: '"Titillium Web", sans-serif'
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ });
208
+
209
+ // 添加响应式处理
210
+ window.addEventListener('resize', () => {
211
+ speedChart.resize();
212
+ accelerationChart.resize();
213
+ });
214
+ }
215
+
216
+ // 计算姿态变化的速度和加速度
217
+ function calculateMotionMetrics(results, timestamp) {
218
+ // 基本验证
219
+ if (!results || !results.poseLandmarks || !Array.isArray(results.poseLandmarks)) {
220
+ return { speed: 0, acceleration: 0 };
221
+ }
222
+
223
+ // 初始化状态
224
+ if (!previousPoseData || !previousPoseData.poseLandmarks) {
225
+ previousPoseData = {
226
+ poseLandmarks: [...results.poseLandmarks]
227
+ };
228
+ lastTimestamp = timestamp;
229
+ return { speed: 0, acceleration: 0 };
230
+ }
231
+
232
+ const deltaTime = (timestamp - lastTimestamp) / 1000; // 转换为秒
233
+ if (deltaTime === 0) {
234
+ return { speed: 0, acceleration: 0 };
235
+ }
236
+
237
+ // 计算关键点的平均位移
238
+ let totalDisplacement = 0;
239
+ let validPoints = 0;
240
+
241
+ try {
242
+ // 只使用有效的关键点进行计算
243
+ results.poseLandmarks.forEach((landmark, index) => {
244
+ const prevLandmark = previousPoseData.poseLandmarks[index];
245
+
246
+ // 确保当前和前一帧的关键点都存在且有效
247
+ if (landmark && prevLandmark &&
248
+ typeof landmark.x === 'number' &&
249
+ typeof landmark.y === 'number' &&
250
+ typeof prevLandmark.x === 'number' &&
251
+ typeof prevLandmark.y === 'number' &&
252
+ // 可选:检查可见性阈值
253
+ (!landmark.visibility || landmark.visibility > 0.5) &&
254
+ (!prevLandmark.visibility || prevLandmark.visibility > 0.5)) {
255
+
256
+ const dx = landmark.x - prevLandmark.x;
257
+ const dy = landmark.y - prevLandmark.y;
258
+ const displacement = Math.sqrt(dx * dx + dy * dy);
259
+
260
+ // 过滤掉异常大的位移
261
+ if (displacement < 1.0) { // 根据需要调整阈值
262
+ totalDisplacement += displacement;
263
+ validPoints++;
264
+ }
265
+ }
266
+ });
267
+ } catch (error) {
268
+ console.warn('Error calculating displacement:', error);
269
+ return { speed: 0, acceleration: 0 };
270
+ }
271
+
272
+ // 如果没有有效点,返回零值
273
+ if (validPoints === 0) {
274
+ return { speed: 0, acceleration: 0 };
275
+ }
276
+
277
+ // 计算平均位移和速度
278
+ const averageDisplacement = totalDisplacement / validPoints;
279
+ const currentSpeed = averageDisplacement / deltaTime;
280
+
281
+ // 获取上一次的速度,如果不存在则使用0
282
+ let previousSpeed = 0;
283
+ try {
284
+ previousSpeed = speedChart.data.datasets[0].data[speedChart.data.datasets[0].data.length - 1] || 0;
285
+ } catch (error) {
286
+ console.warn('Error accessing previous speed:', error);
287
+ }
288
+
289
+ // 计算加速度
290
+ const acceleration = (currentSpeed - previousSpeed) / deltaTime;
291
+
292
+ // 更新先前数据用于下一帧计算
293
+ previousPoseData = {
294
+ poseLandmarks: [...results.poseLandmarks]
295
+ };
296
+ lastTimestamp = timestamp;
297
+
298
+ // 添加一些基本的数值验证
299
+ const metrics = {
300
+ speed: isFinite(currentSpeed) ? Math.min(Math.max(currentSpeed, 0), 1000) : 0,
301
+ acceleration: isFinite(acceleration) ? Math.min(Math.max(acceleration, -1000), 1000) : 0
302
+ };
303
+
304
+ return metrics;
305
+ }
306
+
307
+ // 更新图表数据的函数也需要添加错误处理
308
+ function updateCharts(metrics) {
309
+ if (!metrics || typeof metrics.speed !== 'number' || typeof metrics.acceleration !== 'number') {
310
+ console.warn('Invalid metrics data:', metrics);
311
+ return;
312
+ }
313
+
314
+ try {
315
+ const timestamp = new Date().toLocaleTimeString('zh-CN', {
316
+ hour: '2-digit',
317
+ minute: '2-digit',
318
+ second: '2-digit'
319
+ });
320
+
321
+ // 检查图表对象是否存在且正确初始化
322
+ if (!speedChart || !speedChart.data || !speedChart.data.labels) {
323
+ console.warn('Speed chart not properly initialized');
324
+ return;
325
+ }
326
+ if (!accelerationChart || !accelerationChart.data || !accelerationChart.data.labels) {
327
+ console.warn('Acceleration chart not properly initialized');
328
+ return;
329
+ }
330
+
331
+ // 更新速度图表
332
+ speedChart.data.labels.push(timestamp);
333
+ speedChart.data.datasets[0].data.push(metrics.speed);
334
+ if (speedChart.data.labels.length > CHART_CONFIG.maxDataPoints) {
335
+ speedChart.data.labels.shift();
336
+ speedChart.data.datasets[0].data.shift();
337
+ }
338
+
339
+ // 更新加速度图表
340
+ accelerationChart.data.labels.push(timestamp);
341
+ accelerationChart.data.datasets[0].data.push(metrics.acceleration);
342
+ if (accelerationChart.data.labels.length > CHART_CONFIG.maxDataPoints) {
343
+ accelerationChart.data.labels.shift();
344
+ accelerationChart.data.datasets[0].data.shift();
345
+ }
346
+
347
+ // 使用 requestAnimationFrame 优化图表更新
348
+ requestAnimationFrame(() => {
349
+ try {
350
+ speedChart.update('none');
351
+ accelerationChart.update('none');
352
+ } catch (error) {
353
+ console.warn('Error updating charts:', error);
354
+ }
355
+ });
356
+ } catch (error) {
357
+ console.warn('Error in updateCharts:', error);
358
+ }
359
+ }
360
+
361
+
362
+ function testSupport(supportedDevices) {
363
+ const deviceDetector = new DeviceDetector();
364
+ const detectedDevice = deviceDetector.parse(navigator.userAgent);
365
+ let isSupported = false;
366
+ for (const device of supportedDevices) {
367
+ if (device.client !== undefined) {
368
+ const re = new RegExp(`^${device.client}$`);
369
+ if (!re.test(detectedDevice.client.name)) {
370
+ continue;
371
+ }
372
+ }
373
+ if (device.os !== undefined) {
374
+ const re = new RegExp(`^${device.os}$`);
375
+ if (!re.test(detectedDevice.os.name)) {
376
+ continue;
377
+ }
378
+ }
379
+ isSupported = true;
380
+ break;
381
+ }
382
+ if (!isSupported) {
383
+ alert(`此演示在 ${detectedDevice.client.name}/${detectedDevice.os.name} 上运行时 ` +
384
+ `目前不能很好地支持,继续使用需自担风险。`);
385
+ }
386
+ }
387
+
388
+ const controls = window;
389
+ const mpHolistic = window;
390
+ const drawingUtils = window;
391
+ const config = { locateFile: (file) => {
392
+ return `https://cdn.jsdelivr.net/npm/@mediapipe/holistic@` +
393
+ `${mpHolistic.VERSION}/${file}`;
394
+ } };
395
+
396
+ // 我们的输入帧将来自这里。
397
+ const videoElement = document.getElementsByClassName('input_video')[0];
398
+ const canvasElement = document.getElementsByClassName('output_canvas')[0];
399
+ const controlsElement = document.getElementsByClassName('control-panel')[0];
400
+ const canvasCtx = canvasElement.getContext('2d');
401
+
402
+ // 我们稍后会将这个添加到控制面板中,但我们会在这里保存它,
403
+ // 以便每次图形运行时都可以调用 tick()。
404
+ const fpsControl = new controls.FPS();
405
+
406
+ // 优化:在隐藏动画完成后关闭动画旋转器。
407
+ const spinner = document.querySelector('.loading');
408
+ spinner.ontransitionend = () => {
409
+ spinner.style.display = 'none';
410
+ };
411
+
412
+ function removeElements(landmarks, elements) {
413
+ for (const element of elements) {
414
+ delete landmarks[element];
415
+ }
416
+ }
417
+
418
+ function removeLandmarks(results) {
419
+ if (results.poseLandmarks) {
420
+ removeElements(results.poseLandmarks, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 19, 20, 21, 22]);
421
+ }
422
+ }
423
+
424
+ function connect(ctx, connectors) {
425
+ const canvas = ctx.canvas;
426
+ for (const connector of connectors) {
427
+ const from = connector[0];
428
+ const to = connector[1];
429
+ if (from && to) {
430
+ if (from.visibility && to.visibility &&
431
+ (from.visibility < 0.1 || to.visibility < 0.1)) {
432
+ continue;
433
+ }
434
+ ctx.beginPath();
435
+ ctx.moveTo(from.x * canvas.width, from.y * canvas.height);
436
+ ctx.lineTo(to.x * canvas.width, to.y * canvas.height);
437
+ ctx.stroke();
438
+ }
439
+ }
440
+ }
441
+
442
+ let activeEffect = 'mask';
443
+
444
+
445
+ function onResults(results) {
446
+ // 隐藏旋转器
447
+ document.body.classList.add('loaded');
448
+
449
+ // 移除我们��想绘制的关键点
450
+ removeLandmarks(results);
451
+
452
+ // 更新帧率
453
+ fpsControl.tick();
454
+
455
+ // 绘制叠加层
456
+ canvasCtx.save();
457
+ canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
458
+
459
+ if (results.segmentationMask) {
460
+ canvasCtx.drawImage(results.segmentationMask, 0, 0, canvasElement.width, canvasElement.height);
461
+
462
+ // 仅覆盖现有像素
463
+ if (activeEffect === 'mask' || activeEffect === 'both') {
464
+ canvasCtx.globalCompositeOperation = 'source-in';
465
+ canvasCtx.fillStyle = '#00FF007F';
466
+ canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
467
+ } else {
468
+ canvasCtx.globalCompositeOperation = 'source-out';
469
+ canvasCtx.fillStyle = '#0000FF7F';
470
+ canvasCtx.fillRect(0, 0, canvasElement.width, canvasElement.height);
471
+ }
472
+
473
+ // 仅覆盖缺失的像素
474
+ canvasCtx.globalCompositeOperation = 'destination-atop';
475
+ canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
476
+ canvasCtx.globalCompositeOperation = 'source-over';
477
+ } else {
478
+ canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
479
+ }
480
+
481
+ // 计算并更新动作指标
482
+ const metrics = calculateMotionMetrics(results, performance.now());
483
+ updateCharts(metrics);
484
+
485
+ // 添加安全检查
486
+ if (!results.poseLandmarks || !mpHolistic.POSE_LANDMARKS) {
487
+ canvasCtx.restore();
488
+ return;
489
+ }
490
+
491
+ // 连接肘部到手部
492
+ canvasCtx.lineWidth = 5;
493
+ if (results.poseLandmarks) {
494
+ if (results.rightHandLandmarks && results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW]) {
495
+ canvasCtx.strokeStyle = 'white';
496
+ connect(canvasCtx, [[
497
+ results.poseLandmarks[mpHolistic.POSE_LANDMARKS.RIGHT_ELBOW],
498
+ results.rightHandLandmarks[0]
499
+ ]]);
500
+ }
501
+ if (results.leftHandLandmarks && results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW]) {
502
+ canvasCtx.strokeStyle = 'white';
503
+ connect(canvasCtx, [[
504
+ results.poseLandmarks[mpHolistic.POSE_LANDMARKS.LEFT_ELBOW],
505
+ results.leftHandLandmarks[0]
506
+ ]]);
507
+ }
508
+ }
509
+
510
+ // 绘制姿态连接点
511
+ if (results.poseLandmarks && mpHolistic.POSE_CONNECTIONS) {
512
+ drawingUtils.drawConnectors(canvasCtx, results.poseLandmarks, mpHolistic.POSE_CONNECTIONS, { color: 'white' });
513
+ }
514
+
515
+ // 绘制左侧姿态关键点
516
+ if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_LEFT) {
517
+ const leftLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_LEFT)
518
+ .map(index => results.poseLandmarks[index])
519
+ .filter(landmark => landmark !== undefined);
520
+
521
+ if (leftLandmarks.length > 0) {
522
+ drawingUtils.drawLandmarks(canvasCtx, leftLandmarks,
523
+ { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(255,138,0)' });
524
+ }
525
+ }
526
+
527
+ // 绘制右侧姿态关键点
528
+ if (results.poseLandmarks && mpHolistic.POSE_LANDMARKS_RIGHT) {
529
+ const rightLandmarks = Object.values(mpHolistic.POSE_LANDMARKS_RIGHT)
530
+ .map(index => results.poseLandmarks[index])
531
+ .filter(landmark => landmark !== undefined);
532
+
533
+ if (rightLandmarks.length > 0) {
534
+ drawingUtils.drawLandmarks(canvasCtx, rightLandmarks,
535
+ { visibilityMin: 0.65, color: 'white', fillColor: 'rgb(0,217,231)' });
536
+ }
537
+ }
538
+
539
+ // 绘制手部
540
+ if (results.rightHandLandmarks && mpHolistic.HAND_CONNECTIONS) {
541
+ drawingUtils.drawConnectors(canvasCtx, results.rightHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' });
542
+ drawingUtils.drawLandmarks(canvasCtx, results.rightHandLandmarks, {
543
+ color: 'white',
544
+ fillColor: 'rgb(0,217,231)',
545
+ lineWidth: 2,
546
+ radius: (data) => {
547
+ return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1);
548
+ }
549
+ });
550
+ }
551
+
552
+ if (results.leftHandLandmarks && mpHolistic.HAND_CONNECTIONS) {
553
+ drawingUtils.drawConnectors(canvasCtx, results.leftHandLandmarks, mpHolistic.HAND_CONNECTIONS, { color: 'white' });
554
+ drawingUtils.drawLandmarks(canvasCtx, results.leftHandLandmarks, {
555
+ color: 'white',
556
+ fillColor: 'rgb(255,138,0)',
557
+ lineWidth: 2,
558
+ radius: (data) => {
559
+ return drawingUtils.lerp(data.from.z, -0.15, .1, 10, 1);
560
+ }
561
+ });
562
+ }
563
+
564
+ // 绘制面部
565
+ if (results.faceLandmarks && mpHolistic.FACEMESH_TESSELATION) {
566
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_TESSELATION,
567
+ { color: '#C0C0C070', lineWidth: 1 });
568
+
569
+ if (mpHolistic.FACEMESH_RIGHT_EYE) {
570
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYE,
571
+ { color: 'rgb(0,217,231)' });
572
+ }
573
+ if (mpHolistic.FACEMESH_RIGHT_EYEBROW) {
574
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_RIGHT_EYEBROW,
575
+ { color: 'rgb(0,217,231)' });
576
+ }
577
+ if (mpHolistic.FACEMESH_LEFT_EYE) {
578
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYE,
579
+ { color: 'rgb(255,138,0)' });
580
+ }
581
+ if (mpHolistic.FACEMESH_LEFT_EYEBROW) {
582
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LEFT_EYEBROW,
583
+ { color: 'rgb(255,138,0)' });
584
+ }
585
+ if (mpHolistic.FACEMESH_FACE_OVAL) {
586
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_FACE_OVAL,
587
+ { color: '#E0E0E0', lineWidth: 5 });
588
+ }
589
+ if (mpHolistic.FACEMESH_LIPS) {
590
+ drawingUtils.drawConnectors(canvasCtx, results.faceLandmarks, mpHolistic.FACEMESH_LIPS,
591
+ { color: '#E0E0E0', lineWidth: 5 });
592
+ }
593
+ }
594
+
595
+ canvasCtx.restore();
596
+ }
597
+ // 视频上传处理
598
+ function handleVideoUpload(file) {
599
+ // 创建视频 URL
600
+ const videoUrl = URL.createObjectURL(file);
601
+
602
+ // 重置图表数据
603
+ speedChart.data.labels = [];
604
+ speedChart.data.datasets[0].data = [];
605
+ accelerationChart.data.labels = [];
606
+ accelerationChart.data.datasets[0].data = [];
607
+ previousPoseData = null;
608
+ lastTimestamp = 0;
609
+
610
+ // 重置姿态检测
611
+ holistic.reset();
612
+
613
+ // 更新视频源
614
+ videoElement.src = videoUrl;
615
+
616
+ // 设置视频加载完成后的处理
617
+ videoElement.onloadedmetadata = () => {
618
+ // 调整画布大小以匹配视频尺寸
619
+ const aspect = videoElement.videoHeight / videoElement.videoWidth;
620
+ let width, height;
621
+ if (window.innerWidth > window.innerHeight) {
622
+ height = window.innerHeight;
623
+ width = height / aspect;
624
+ } else {
625
+ width = window.innerWidth;
626
+ height = width * aspect;
627
+ }
628
+ canvasElement.width = width;
629
+ canvasElement.height = height;
630
+
631
+ // 创建用于视频处理的动画帧
632
+ let animationId;
633
+
634
+ async function processFrame() {
635
+ if (videoElement.paused || videoElement.ended) {
636
+ cancelAnimationFrame(animationId);
637
+ return;
638
+ }
639
+
640
+ // 发送当前帧到姿态检测
641
+ await holistic.send({
642
+ image: videoElement
643
+ });
644
+
645
+ // 继续处理下一帧
646
+ animationId = requestAnimationFrame(processFrame);
647
+ }
648
+
649
+ // 视频播放事件处理
650
+ videoElement.onplay = () => {
651
+ processFrame();
652
+ };
653
+
654
+ // 添加视频控制按钮事件监听
655
+ const playPauseBtn = document.createElement('button');
656
+ playPauseBtn.textContent = '播放/暂停';
657
+ playPauseBtn.className = 'control-button';
658
+ playPauseBtn.onclick = () => {
659
+ if (videoElement.paused) {
660
+ videoElement.play();
661
+ } else {
662
+ videoElement.pause();
663
+ }
664
+ };
665
+
666
+ const restartBtn = document.createElement('button');
667
+ restartBtn.textContent = '重新开始';
668
+ restartBtn.className = 'control-button';
669
+ restartBtn.onclick = () => {
670
+ videoElement.currentTime = 0;
671
+ if (videoElement.paused) {
672
+ videoElement.play();
673
+ }
674
+ };
675
+
676
+ // 添加控制按钮到界面
677
+ const controlsContainer = document.createElement('div');
678
+ controlsContainer.className = 'video-controls';
679
+ controlsContainer.appendChild(playPauseBtn);
680
+ controlsContainer.appendChild(restartBtn);
681
+
682
+ // 找到合适的位置插入控制按钮
683
+ const container = document.querySelector('.container') || document.body;
684
+ container.appendChild(controlsContainer);
685
+ };
686
+
687
+ // 添加错误处理
688
+ videoElement.onerror = () => {
689
+ console.error('视频加载失败');
690
+ alert('视频加载失败,请尝试其他视频文件');
691
+ };
692
+ }
693
+
694
+ // 添加一些基本的样式
695
+ const style = document.createElement('style');
696
+ style.textContent = `
697
+ .video-controls {
698
+ position: fixed;
699
+ bottom: 20px;
700
+ left: 50%;
701
+ transform: translateX(-50%);
702
+ z-index: 1000;
703
+ display: flex;
704
+ gap: 10px;
705
+ }
706
+
707
+ .control-button {
708
+ padding: 10px 20px;
709
+ background-color: rgba(0, 0, 0, 0.7);
710
+ color: white;
711
+ border: none;
712
+ border-radius: 5px;
713
+ cursor: pointer;
714
+ font-size: 14px;
715
+ }
716
+
717
+ .control-button:hover {
718
+ background-color: rgba(0, 0, 0, 0.9);
719
+ }
720
+ `;
721
+ document.head.appendChild(style);
722
+
723
+ const holistic = new mpHolistic.Holistic(config);
724
+ holistic.onResults(onResults);
725
+
726
+ // 呈现一个控制面板,用户可以通过它操作解决方案选项。
727
+ new controls
728
+ .ControlPanel(controlsElement, {
729
+ selfieMode: true,
730
+ modelComplexity: 1,
731
+ smoothLandmarks: true,
732
+ enableSegmentation: false,
733
+ smoothSegmentation: true,
734
+ minDetectionConfidence: 0.5,
735
+ minTrackingConfidence: 0.5,
736
+ effect: 'background',
737
+ })
738
+ .add([
739
+ new controls.StaticText({ title: 'MediaPipe 全身姿态检测' }),
740
+ fpsControl,
741
+ new controls.Toggle({ title: '自拍模式', field: 'selfieMode' }),
742
+ new controls.SourcePicker({
743
+ onSourceChanged: () => {
744
+ // 重置,因为在源更改之间重置时,姿势会给出更好的结果。
745
+ holistic.reset();
746
+ },
747
+ onFrame: async (input, size) => {
748
+ const aspect = size.height / size.width;
749
+ let width, height;
750
+ if (window.innerWidth > window.innerHeight) {
751
+ height = window.innerHeight;
752
+ width = height / aspect;
753
+ }
754
+ else {
755
+ width = window.innerWidth;
756
+ height = width * aspect;
757
+ }
758
+ canvasElement.width = width;
759
+ canvasElement.height = height;
760
+ await holistic.send({ image: input });
761
+ },
762
+ }),
763
+ new controls.Slider({
764
+ title: '模型复杂度',
765
+ field: 'modelComplexity',
766
+ discrete: ['轻量', '完整', '重度'],
767
+ }),
768
+ new controls.Toggle({ title: '平滑关键点', field: 'smoothLandmarks' }),
769
+ new controls.Toggle({ title: '启用分割', field: 'enableSegmentation' }),
770
+ new controls.Toggle({ title: '平滑分割', field: 'smoothSegmentation' }),
771
+ new controls.Slider({
772
+ title: '最小检测置信度',
773
+ field: 'minDetectionConfidence',
774
+ range: [0, 1],
775
+ step: 0.01
776
+ }),
777
+ new controls.Slider({
778
+ title: '最小跟踪置信度',
779
+ field: 'minTrackingConfidence',
780
+ range: [0, 1],
781
+ step: 0.01
782
+ }),
783
+ new controls.Slider({
784
+ title: '效果',
785
+ field: 'effect',
786
+ discrete: { 'background': '背景', 'mask': '前景' },
787
+ }),
788
+ ])
789
+ .on(x => {
790
+ const options = x;
791
+ videoElement.classList.toggle('selfie', options.selfieMode);
792
+ activeEffect = x['effect'];
793
+ holistic.setOptions(options);
794
+ });
795
+ // 初始化函数
796
+ function initialize() {
797
+ // 初始化图表
798
+ initCharts();
799
+
800
+ // 设置视频上传处理
801
+ const videoUploadInput = document.querySelector('#video-upload');
802
+ if (videoUploadInput) {
803
+ videoUploadInput.addEventListener('change', (e) => {
804
+ if (e.target.files.length > 0) {
805
+ handleVideoUpload(e.target.files[0]);
806
+ }
807
+ });
808
+ }
809
+
810
+ // 初始化姿态检测
811
+ const holistic = new mpHolistic.Holistic(config);
812
+ holistic.onResults(onResults);
813
+
814
+ // ... 保持其他原有的初始化逻辑 ...
815
+ }
816
+
817
+ // 启动应用
818
+ window.addEventListener('load', initialize);
819
+
820
+ // 保持原有的窗口大小调整逻辑
821
+ window.addEventListener('resize', () => {
822
+ const aspect = videoElement.videoHeight / videoElement.videoWidth;
823
+ let width, height;
824
+ if (window.innerWidth > window.innerHeight) {
825
+ height = window.innerHeight;
826
+ width = height / aspect;
827
+ } else {
828
+ width = window.innerWidth;
829
+ height = width * aspect;
830
+ }
831
+ canvasElement.width = width;
832
+ canvasElement.height = height;
833
+
834
+ // 重新调整图表大小
835
+ speedChart.resize();
836
+ accelerationChart.resize();
837
+ });