mistpe commited on
Commit
a08bbea
·
verified ·
1 Parent(s): f2d8d0d

Update templates/video_app.html

Browse files
Files changed (1) hide show
  1. templates/video_app.html +662 -601
templates/video_app.html CHANGED
@@ -1,602 +1,663 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>3D Pose Estimation App</title>
7
- <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
- <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
9
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
11
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
12
- <style>
13
- :root {
14
- --primary-color: #4a90e2;
15
- --secondary-color: #f5a623;
16
- --background-color: #f0f4f8;
17
- --card-background: #ffffff;
18
- --text-color: #333333;
19
- --border-radius: 12px;
20
- --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
21
- }
22
-
23
- * {
24
- box-sizing: border-box;
25
- margin: 0;
26
- padding: 0;
27
- }
28
-
29
- body {
30
- font-family: 'Roboto', sans-serif;
31
- background: var(--background-color);
32
- color: var(--text-color);
33
- line-height: 1.6;
34
- }
35
-
36
- .container {
37
- max-width: 1400px;
38
- margin: 0 auto;
39
- padding: 20px;
40
- }
41
-
42
- .app-header {
43
- text-align: center;
44
- margin-bottom: 30px;
45
- }
46
-
47
- .app-title {
48
- font-size: 2.5em;
49
- color: var(--primary-color);
50
- margin-bottom: 10px;
51
- }
52
-
53
- .app-description {
54
- font-size: 1.1em;
55
- color: var(--text-color);
56
- opacity: 0.8;
57
- }
58
-
59
- .card-grid {
60
- display: grid;
61
- grid-template-columns: repeat(2, 1fr);
62
- gap: 20px;
63
- }
64
-
65
- .card {
66
- background: var(--card-background);
67
- border-radius: var(--border-radius);
68
- box-shadow: var(--box-shadow);
69
- overflow: hidden;
70
- transition: transform 0.3s ease;
71
- }
72
-
73
- .card:hover {
74
- transform: translateY(-5px);
75
- }
76
-
77
- .card-header {
78
- background: var(--primary-color);
79
- color: white;
80
- padding: 15px;
81
- font-size: 1.2em;
82
- font-weight: bold;
83
- display: flex;
84
- align-items: center;
85
- }
86
-
87
- .card-header i {
88
- margin-right: 10px;
89
- }
90
-
91
- .card-body {
92
- padding: 20px;
93
- }
94
-
95
- .view {
96
- width: 100%;
97
- padding-bottom: 56.25%; /* 16:9 aspect ratio */
98
- position: relative;
99
- border-radius: var(--border-radius);
100
- overflow: hidden;
101
- }
102
-
103
- .view img, .view canvas {
104
- position: absolute;
105
- top: 0;
106
- left: 0;
107
- width: 100%;
108
- height: 100%;
109
- object-fit: contain;
110
- }
111
-
112
- .controls {
113
- margin-top: 20px;
114
- }
115
-
116
- .slider-container {
117
- display: flex;
118
- align-items: center;
119
- margin-bottom: 15px;
120
- }
121
-
122
- .slider-container label {
123
- width: 80px;
124
- margin-right: 10px;
125
- }
126
-
127
- .slider {
128
- flex-grow: 1;
129
- -webkit-appearance: none;
130
- height: 5px;
131
- background: #d7dcdf;
132
- outline: none;
133
- opacity: 0.7;
134
- transition: opacity .2s;
135
- border-radius: 5px;
136
- }
137
-
138
- .slider:hover {
139
- opacity: 1;
140
- }
141
-
142
- .slider::-webkit-slider-thumb {
143
- -webkit-appearance: none;
144
- appearance: none;
145
- width: 18px;
146
- height: 18px;
147
- background: var(--secondary-color);
148
- cursor: pointer;
149
- border-radius: 50%;
150
- }
151
-
152
- .btn {
153
- padding: 12px 20px;
154
- border: none;
155
- border-radius: var(--border-radius);
156
- background-color: var(--primary-color);
157
- color: white;
158
- cursor: pointer;
159
- transition: background-color 0.3s ease, transform 0.1s ease;
160
- margin-top: 10px;
161
- width: 100%;
162
- display: flex;
163
- align-items: center;
164
- justify-content: center;
165
- font-size: 1em;
166
- font-weight: bold;
167
- }
168
-
169
- .btn:hover {
170
- background-color: #3a7bd5;
171
- transform: translateY(-2px);
172
- }
173
-
174
- .btn:active {
175
- transform: translateY(0);
176
- }
177
-
178
- .btn i {
179
- margin-right: 10px;
180
- }
181
-
182
- .chart-container {
183
- display: flex;
184
- flex-wrap: wrap;
185
- justify-content: space-around;
186
- gap: 20px;
187
- }
188
-
189
- .chart-container canvas {
190
- max-width: 100%;
191
- height: auto !important;
192
- }
193
-
194
- #message-box {
195
- position: fixed;
196
- top: 20px;
197
- left: 50%;
198
- transform: translateX(-50%);
199
- background: var(--primary-color);
200
- color: white;
201
- padding: 15px 20px;
202
- border-radius: var(--border-radius);
203
- z-index: 1000;
204
- display: none;
205
- box-shadow: var(--box-shadow);
206
- }
207
-
208
- #loading-indicator {
209
- position: fixed;
210
- top: 50%;
211
- left: 50%;
212
- transform: translate(-50%, -50%);
213
- z-index: 1000;
214
- display: none;
215
- background: rgba(0, 0, 0, 0.8);
216
- color: white;
217
- padding: 20px;
218
- border-radius: var(--border-radius);
219
- box-shadow: var(--box-shadow);
220
- }
221
-
222
- #loading-indicator i {
223
- margin-right: 10px;
224
- }
225
-
226
- @media (max-width: 1024px) {
227
- .card-grid {
228
- grid-template-columns: 1fr;
229
- }
230
-
231
- .app-title {
232
- font-size: 2em;
233
- }
234
-
235
- .app-description {
236
- font-size: 1em;
237
- }
238
- }
239
-
240
- @media (max-width: 768px) {
241
- .container {
242
- padding: 10px;
243
- }
244
-
245
- .card-header {
246
- font-size: 1.1em;
247
- }
248
-
249
- .slider-container {
250
- flex-direction: column;
251
- align-items: flex-start;
252
- }
253
-
254
- .slider-container label {
255
- margin-bottom: 5px;
256
- }
257
-
258
- .btn {
259
- padding: 10px 15px;
260
- }
261
- }
262
- </style>
263
- </head>
264
- <body>
265
- <div id="message-box">
266
- <span id="message-text"></span>
267
- </div>
268
-
269
- <div id="loading-indicator">
270
- <i class="fas fa-spinner fa-spin"></i> Processing...
271
- </div>
272
-
273
- <div class="container">
274
- <header class="app-header">
275
- <h1 class="app-title">3D Pose Estimation App</h1>
276
- <p class="app-description">Analyze and visualize human pose in real-time</p>
277
- </header>
278
-
279
- <div class="card-grid">
280
- <div class="card">
281
- <div class="card-header">
282
- <i class="fas fa-video"></i> Video Input
283
- </div>
284
- <div class="card-body">
285
- <div class="view" id="video-container">
286
- <img id="video-feed" src="{{ url_for('video_feed') }}" alt="Video Feed">
287
- </div>
288
- <div class="controls">
289
- <div class="slider-container">
290
- <label for="video-scale"><i class="fas fa-search"></i> Scale:</label>
291
- <input type="range" min="0.5" max="2" step="0.1" value="1" class="slider" id="video-scale" aria-label="Video Scale Slider">
292
- </div>
293
- <label for="video-upload" class="btn">
294
- <i class="fas fa-upload"></i> Upload Video
295
- <input type="file" id="video-upload" accept="video/*" style="display: none;">
296
- </label>
297
- </div>
298
- </div>
299
- </div>
300
-
301
- <div class="card">
302
- <div class="card-header">
303
- <i class="fas fa-cube"></i> 3D Model Visualization
304
- </div>
305
- <div class="card-body">
306
- <div class="view" id="model-container"></div>
307
- <div class="controls">
308
- <div class="slider-container">
309
- <label for="model-scale"><i class="fas fa-search"></i> Scale:</label>
310
- <input type="range" min="0.5" max="2" step="0.1" value="1" class="slider" id="model-scale" aria-label="3D Model Scale Slider">
311
- </div>
312
- </div>
313
- </div>
314
- </div>
315
-
316
- <div class="card">
317
- <div class="card-header">
318
- <i class="fas fa-tachometer-alt"></i> Speed Analysis
319
- </div>
320
- <div class="card-body">
321
- <canvas id="speedChart" aria-label="Speed Chart" role="img"></canvas>
322
- </div>
323
- </div>
324
-
325
- <div class="card">
326
- <div class="card-header">
327
- <i class="fas fa-bolt"></i> Acceleration Analysis
328
- </div>
329
- <div class="card-body">
330
- <canvas id="accelerationChart" aria-label="Acceleration Chart" role="img"></canvas>
331
- </div>
332
- </div>
333
- </div>
334
- </div>
335
-
336
-
337
- <script>
338
- // 连接到Socket.IO服务器
339
- const socket = io();
340
-
341
- // Three.js设置
342
- const scene = new THREE.Scene();
343
- scene.background = new THREE.Color(0xf0f4f8);
344
- const aspect = 4 / 3;
345
- const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
346
- const renderer = new THREE.WebGLRenderer({ antialias: true });
347
- const modelContainer = document.getElementById('model-container');
348
- renderer.setSize(modelContainer.clientWidth, modelContainer.clientWidth / aspect);
349
- modelContainer.appendChild(renderer.domElement);
350
-
351
- camera.position.set(0, 0, 1.5);
352
- camera.lookAt(0, 0, 0);
353
-
354
- // 添加灯光
355
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
356
- scene.add(ambientLight);
357
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
358
- directionalLight.position.set(1, 1, 1);
359
- scene.add(directionalLight);
360
-
361
- // 创建用于标记关键点的球体
362
- const spheres = [];
363
- for (let i = 0; i < 33; i++) {
364
- const geometry = new THREE.SphereGeometry(0.015, 32, 32);
365
- const material = new THREE.MeshPhongMaterial({
366
- color: 0x3498db,
367
- shininess: 100,
368
- specular: 0x111111
369
- });
370
- const sphere = new THREE.Mesh(geometry, material);
371
- scene.add(sphere);
372
- spheres.push(sphere);
373
- }
374
-
375
- // 创建圆柱体的函数
376
- const cylinders = [];
377
- function createCylinder(point1, point2) {
378
- const direction = new THREE.Vector3().subVectors(point2, point1);
379
- const cylinder = new THREE.Mesh(
380
- new THREE.CylinderGeometry(0.007, 0.007, direction.length(), 8, 1),
381
- new THREE.MeshPhongMaterial({
382
- color: 0x2c3e50,
383
- shininess: 100,
384
- specular: 0x111111
385
- })
386
- );
387
- cylinder.position.copy(point1);
388
- cylinder.position.addScaledVector(direction, 0.5);
389
- cylinder.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.normalize());
390
- scene.add(cylinder);
391
- return cylinder;
392
- }
393
-
394
- // 初始化图表
395
- const speedChartCtx = document.getElementById('speedChart').getContext('2d');
396
- const accelerationChartCtx = document.getElementById('accelerationChart').getContext('2d');
397
- const speedChart = new Chart(speedChartCtx, {
398
- type: 'line',
399
- data: {
400
- labels: [],
401
- datasets: [{
402
- label: 'Speed',
403
- data: [],
404
- borderColor: 'rgba(75, 192, 192, 1)',
405
- borderWidth: 1,
406
- fill: false
407
- }]
408
- },
409
- options: {
410
- scales: {
411
- y: { beginAtZero: true }
412
- }
413
- }
414
- });
415
- const accelerationChart = new Chart(accelerationChartCtx, {
416
- type: 'line',
417
- data: {
418
- labels: [],
419
- datasets: [{
420
- label: 'Acceleration',
421
- data: [],
422
- borderColor: 'rgba(255, 99, 132, 1)',
423
- borderWidth: 1,
424
- fill: false
425
- }]
426
- },
427
- options: {
428
- scales: {
429
- y: { beginAtZero: true }
430
- }
431
- }
432
- });
433
-
434
- // 更新图表的函数
435
- function updateChart(chart, newData) {
436
- const currentTime = new Date().toLocaleTimeString();
437
- chart.data.labels.push(currentTime);
438
- chart.data.datasets.forEach((dataset) => {
439
- dataset.data.push(newData.reduce((a, b) => a + b, 0) / newData.length);
440
- });
441
- chart.update();
442
- }
443
-
444
- // 监听从服务器发送的pose_data事件
445
- socket.on('pose_data', function(data) {
446
- // 更新3D模型
447
- const landmarks = data.landmarks;
448
- landmarks.forEach((coord, index) => {
449
- spheres[index].position.set((coord[0] - 0.5) * 2, -(coord[1] - 0.5) * 2, -coord[2] * 0.5);
450
- });
451
-
452
- // 清除现有的圆柱体
453
- cylinders.forEach(cylinder => scene.remove(cylinder));
454
- cylinders.length = 0;
455
-
456
- // 创建新的圆柱体
457
- const connections = [
458
- // Face
459
- [0, 1], [1, 4], [4, 7], [7, 8], [8, 5], [5, 2], [2, 0],
460
- [0, 9], [9, 10], [0, 10],
461
- // Arms
462
- [11, 13], [13, 15], [15, 17], [15, 19], [15, 21],
463
- [12, 14], [14, 16], [16, 18], [16, 20], [16, 22],
464
- // Body
465
- [11, 12], [11, 23], [12, 24], [23, 24],
466
- // Legs
467
- [23, 25], [25, 27], [27, 29],
468
- [24, 26], [26, 28], [28, 30], [28, 32]
469
- ];
470
-
471
- connections.forEach(([i, j]) => {
472
- cylinders.push(createCylinder(spheres[i].position, spheres[j].position));
473
- });
474
-
475
- // 更新速度和加速度图表
476
- if (data.velocities) {
477
- updateChart(speedChart, data.velocities);
478
- }
479
- if (data.accelerations) {
480
- updateChart(accelerationChart, data.accelerations);
481
- }
482
- });
483
-
484
- function animate() {
485
- requestAnimationFrame(animate);
486
- renderer.render(scene, camera);
487
- }
488
- animate();
489
-
490
- function showMessage(message) {
491
- const messageBox = document.getElementById('message-box');
492
- const messageText = document.getElementById('message-text');
493
- messageText.textContent = message;
494
- messageBox.style.display = 'block';
495
-
496
- // 2秒后自动隐藏
497
- setTimeout(() => {
498
- messageBox.style.display = 'none';
499
- }, 2000);
500
- }
501
-
502
- // 视频上传处理
503
- document.getElementById('video-upload').addEventListener('change', function(e) {
504
- const file = e.target.files[0];
505
- if (file) {
506
- const formData = new FormData();
507
- formData.append('video', file);
508
-
509
- // 显示加载指示器
510
- showLoadingIndicator(true);
511
-
512
- fetch('/upload_video', {
513
- method: 'POST',
514
- body: formData
515
- }).then(response => response.json())
516
- .then(data => {
517
- showLoadingIndicator(false); // 隐藏加载指示器
518
- if (data.success) {
519
- showMessage(data.message);
520
- document.getElementById('video-feed').src = "{{ url_for('video_feed') }}?" + new Date().getTime();
521
- } else {
522
- showMessage('Error uploading video');
523
- }
524
- }).catch(error => {
525
- showLoadingIndicator(false); // 隐藏加载指示器
526
- showMessage('Upload failed: ' + error.message);
527
- });
528
- }
529
- });
530
-
531
- // 切换到相机模式
532
- document.getElementById('camera-switch').addEventListener('click', function() {
533
- showLoadingIndicator(true); // 显示加载指示器
534
-
535
- fetch('/switch_to_camera', {
536
- method: 'POST'
537
- }).then(response => response.json())
538
- .then(data => {
539
- showLoadingIndicator(false); // 隐藏加载指示器
540
- if (data.success) {
541
- showMessage(data.message);
542
- document.getElementById('video-feed').src = "{{ url_for('video_feed') }}?" + new Date().getTime();
543
- } else {
544
- showMessage('Error switching to camera');
545
- }
546
- }).catch(error => {
547
- showLoadingIndicator(false); // 隐藏加载指示器
548
- showMessage('Switch failed: ' + error.message);
549
- });
550
- });
551
- function showLoadingIndicator(isLoading) {
552
- const loadingIndicator = document.getElementById('loading-indicator');
553
- loadingIndicator.style.display = isLoading ? 'block' : 'none';
554
- }
555
-
556
-
557
- // 缩放处理
558
- document.getElementById('video-scale').addEventListener('input', function(e) {
559
- document.getElementById('video-feed').style.transform = `scale(${e.target.value})`;
560
- });
561
-
562
- document.getElementById('model-scale').addEventListener('input', function(e) {
563
- const newWidth = modelContainer.clientWidth * e.target.value;
564
- const newHeight = newWidth / aspect;
565
- renderer.setSize(newWidth, newHeight);
566
- });
567
-
568
- // 窗口大小调整处理
569
- window.addEventListener('resize', function() {
570
- const width = modelContainer.clientWidth;
571
- const height = width / aspect;
572
- camera.aspect = aspect;
573
- camera.updateProjectionMatrix();
574
- renderer.setSize(width, height);
575
- });
576
-
577
- navigator.mediaDevices.getUserMedia({
578
- video: {
579
- facingMode: 'user',
580
- width: { ideal: 640 },
581
- height: { ideal: 480 },
582
- frameRate: { ideal: 15 }
583
- }
584
- })
585
- .then(stream => {
586
- const video = document.createElement('video');
587
- video.srcObject = stream;
588
- video.play();
589
- const videoFeed = document.getElementById('video-feed');
590
- videoFeed.srcObject = stream;
591
- videoFeed.play();
592
- })
593
- .catch(error => {
594
- console.error('Error accessing camera: ', error);
595
- });
596
-
597
-
598
-
599
- </script>
600
-
601
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>3D Pose Estimation App</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
10
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
12
+ <style>
13
+ :root {
14
+ --primary-color: #4a90e2;
15
+ --secondary-color: #f5a623;
16
+ --background-color: #f0f4f8;
17
+ --card-background: #ffffff;
18
+ --text-color: #333333;
19
+ --border-radius: 12px;
20
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ margin: 0;
26
+ padding: 0;
27
+ }
28
+
29
+ body {
30
+ font-family: 'Roboto', sans-serif;
31
+ background: var(--background-color);
32
+ color: var(--text-color);
33
+ line-height: 1.6;
34
+ }
35
+
36
+ .container {
37
+ max-width: 1400px;
38
+ margin: 0 auto;
39
+ padding: 20px;
40
+ }
41
+
42
+ .app-header {
43
+ text-align: center;
44
+ margin-bottom: 30px;
45
+ }
46
+
47
+ .app-title {
48
+ font-size: 2.5em;
49
+ color: var(--primary-color);
50
+ margin-bottom: 10px;
51
+ }
52
+
53
+ .app-description {
54
+ font-size: 1.1em;
55
+ color: var(--text-color);
56
+ opacity: 0.8;
57
+ }
58
+
59
+ .card-grid {
60
+ display: grid;
61
+ grid-template-columns: repeat(2, 1fr);
62
+ gap: 20px;
63
+ }
64
+
65
+ .card {
66
+ background: var(--card-background);
67
+ border-radius: var(--border-radius);
68
+ box-shadow: var(--box-shadow);
69
+ overflow: hidden;
70
+ transition: transform 0.3s ease;
71
+ }
72
+
73
+ .card:hover {
74
+ transform: translateY(-5px);
75
+ }
76
+
77
+ .card-header {
78
+ background: var(--primary-color);
79
+ color: white;
80
+ padding: 15px;
81
+ font-size: 1.2em;
82
+ font-weight: bold;
83
+ display: flex;
84
+ align-items: center;
85
+ }
86
+
87
+ .card-header i {
88
+ margin-right: 10px;
89
+ }
90
+
91
+ .card-body {
92
+ padding: 20px;
93
+ }
94
+
95
+ .view {
96
+ width: 100%;
97
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
98
+ position: relative;
99
+ border-radius: var(--border-radius);
100
+ overflow: hidden;
101
+ }
102
+
103
+ .view img, .view canvas {
104
+ position: absolute;
105
+ top: 0;
106
+ left: 0;
107
+ width: 100%;
108
+ height: 100%;
109
+ object-fit: contain;
110
+ }
111
+
112
+ .controls {
113
+ margin-top: 20px;
114
+ }
115
+
116
+ .slider-container {
117
+ display: flex;
118
+ align-items: center;
119
+ margin-bottom: 15px;
120
+ }
121
+
122
+ .slider-container label {
123
+ width: 80px;
124
+ margin-right: 10px;
125
+ }
126
+
127
+ .slider {
128
+ flex-grow: 1;
129
+ -webkit-appearance: none;
130
+ height: 5px;
131
+ background: #d7dcdf;
132
+ outline: none;
133
+ opacity: 0.7;
134
+ transition: opacity .2s;
135
+ border-radius: 5px;
136
+ }
137
+
138
+ .slider:hover {
139
+ opacity: 1;
140
+ }
141
+
142
+ .slider::-webkit-slider-thumb {
143
+ -webkit-appearance: none;
144
+ appearance: none;
145
+ width: 18px;
146
+ height: 18px;
147
+ background: var(--secondary-color);
148
+ cursor: pointer;
149
+ border-radius: 50%;
150
+ }
151
+
152
+ .btn {
153
+ padding: 12px 20px;
154
+ border: none;
155
+ border-radius: var(--border-radius);
156
+ background-color: var(--primary-color);
157
+ color: white;
158
+ cursor: pointer;
159
+ transition: background-color 0.3s ease, transform 0.1s ease;
160
+ margin-top: 10px;
161
+ width: 100%;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ font-size: 1em;
166
+ font-weight: bold;
167
+ }
168
+
169
+ .btn:hover {
170
+ background-color: #3a7bd5;
171
+ transform: translateY(-2px);
172
+ }
173
+
174
+ .btn:active {
175
+ transform: translateY(0);
176
+ }
177
+
178
+ .btn i {
179
+ margin-right: 10px;
180
+ }
181
+
182
+ .chart-container {
183
+ display: flex;
184
+ flex-wrap: wrap;
185
+ justify-content: space-around;
186
+ gap: 20px;
187
+ }
188
+
189
+ .chart-container canvas {
190
+ max-width: 100%;
191
+ height: auto !important;
192
+ }
193
+
194
+ #message-box {
195
+ position: fixed;
196
+ top: 20px;
197
+ left: 50%;
198
+ transform: translateX(-50%);
199
+ background: var(--primary-color);
200
+ color: white;
201
+ padding: 15px 20px;
202
+ border-radius: var(--border-radius);
203
+ z-index: 1000;
204
+ display: none;
205
+ box-shadow: var(--box-shadow);
206
+ }
207
+
208
+ #loading-indicator {
209
+ position: fixed;
210
+ top: 50%;
211
+ left: 50%;
212
+ transform: translate(-50%, -50%);
213
+ z-index: 1000;
214
+ display: none;
215
+ background: rgba(0, 0, 0, 0.8);
216
+ color: white;
217
+ padding: 20px;
218
+ border-radius: var(--border-radius);
219
+ box-shadow: var(--box-shadow);
220
+ }
221
+
222
+ #loading-indicator i {
223
+ margin-right: 10px;
224
+ }
225
+
226
+ @media (max-width: 1024px) {
227
+ .card-grid {
228
+ grid-template-columns: 1fr;
229
+ }
230
+
231
+ .app-title {
232
+ font-size: 2em;
233
+ }
234
+
235
+ .app-description {
236
+ font-size: 1em;
237
+ }
238
+ }
239
+
240
+ @media (max-width: 768px) {
241
+ .container {
242
+ padding: 10px;
243
+ }
244
+
245
+ .card-header {
246
+ font-size: 1.1em;
247
+ }
248
+
249
+ .slider-container {
250
+ flex-direction: column;
251
+ align-items: flex-start;
252
+ }
253
+
254
+ .slider-container label {
255
+ margin-bottom: 5px;
256
+ }
257
+
258
+ .btn {
259
+ padding: 10px 15px;
260
+ }
261
+ }
262
+ </style>
263
+ </head>
264
+ <body>
265
+ <div id="message-box">
266
+ <span id="message-text"></span>
267
+ </div>
268
+
269
+ <div id="loading-indicator">
270
+ <i class="fas fa-spinner fa-spin"></i> Processing...
271
+ </div>
272
+
273
+ <div class="container">
274
+ <header class="app-header">
275
+ <h1 class="app-title">3D Pose Estimation App</h1>
276
+ <p class="app-description">Analyze and visualize human pose in real-time</p>
277
+ </header>
278
+
279
+ <div class="card-grid">
280
+ <div class="card">
281
+ <div class="card-header">
282
+ <i class="fas fa-video"></i> Video Input
283
+ </div>
284
+ <div class="card-body">
285
+ <div class="view" id="video-container">
286
+ <img id="video-feed" src="{{ url_for('video_feed') }}" alt="Video Feed">
287
+ </div>
288
+ <div class="controls">
289
+ <div class="slider-container">
290
+ <label for="video-scale"><i class="fas fa-search"></i> Scale:</label>
291
+ <input type="range" min="0.5" max="2" step="0.1" value="1" class="slider" id="video-scale" aria-label="Video Scale Slider">
292
+ </div>
293
+ <label for="video-upload" class="btn">
294
+ <i class="fas fa-upload"></i> Upload Video
295
+ <input type="file" id="video-upload" accept="video/*" style="display: none;">
296
+ </label>
297
+ </div>
298
+ </div>
299
+ </div>
300
+
301
+ <div class="card">
302
+ <div class="card-header">
303
+ <i class="fas fa-cube"></i> 3D Model Visualization
304
+ </div>
305
+ <div class="card-body">
306
+ <div class="view" id="model-container"></div>
307
+ <div class="controls">
308
+ <div class="slider-container">
309
+ <label for="model-scale"><i class="fas fa-search"></i> Scale:</label>
310
+ <input type="range" min="0.5" max="2" step="0.1" value="1" class="slider" id="model-scale" aria-label="3D Model Scale Slider">
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
+ <div class="card">
317
+ <div class="card-header">
318
+ <i class="fas fa-tachometer-alt"></i> Speed Analysis
319
+ </div>
320
+ <div class="card-body">
321
+ <canvas id="speedChart" aria-label="Speed Chart" role="img"></canvas>
322
+ </div>
323
+ </div>
324
+
325
+ <div class="card">
326
+ <div class="card-header">
327
+ <i class="fas fa-bolt"></i> Acceleration Analysis
328
+ </div>
329
+ <div class="card-body">
330
+ <canvas id="accelerationChart" aria-label="Acceleration Chart" role="img"></canvas>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </div>
335
+
336
+
337
+ <script>
338
+ // Three.js setup
339
+ const scene = new THREE.Scene();
340
+ scene.background = new THREE.Color(0xf0f4f8);
341
+ const aspect = 4 / 3;
342
+ const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
343
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
344
+ const modelContainer = document.getElementById('model-container');
345
+ renderer.setSize(modelContainer.clientWidth, modelContainer.clientWidth / aspect);
346
+ modelContainer.appendChild(renderer.domElement);
347
+
348
+ camera.position.set(0, 0, 1.5);
349
+ camera.lookAt(0, 0, 0);
350
+
351
+ // Add lighting
352
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
353
+ scene.add(ambientLight);
354
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
355
+ directionalLight.position.set(1, 1, 1);
356
+ scene.add(directionalLight);
357
+
358
+ // Create spheres for keypoints
359
+ const spheres = [];
360
+ for (let i = 0; i < 33; i++) {
361
+ const geometry = new THREE.SphereGeometry(0.015, 32, 32);
362
+ const material = new THREE.MeshPhongMaterial({
363
+ color: 0x3498db,
364
+ shininess: 100,
365
+ specular: 0x111111
366
+ });
367
+ const sphere = new THREE.Mesh(geometry, material);
368
+ scene.add(sphere);
369
+ spheres.push(sphere);
370
+ }
371
+
372
+ // Function to create cylinders
373
+ const cylinders = [];
374
+ function createCylinder(point1, point2) {
375
+ const direction = new THREE.Vector3().subVectors(point2, point1);
376
+ const cylinder = new THREE.Mesh(
377
+ new THREE.CylinderGeometry(0.007, 0.007, direction.length(), 8, 1),
378
+ new THREE.MeshPhongMaterial({
379
+ color: 0x2c3e50,
380
+ shininess: 100,
381
+ specular: 0x111111
382
+ })
383
+ );
384
+ cylinder.position.copy(point1);
385
+ cylinder.position.addScaledVector(direction, 0.5);
386
+ cylinder.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.normalize());
387
+ scene.add(cylinder);
388
+ return cylinder;
389
+ }
390
+
391
+ // Initialize charts
392
+ const speedChartCtx = document.getElementById('speedChart').getContext('2d');
393
+ const accelerationChartCtx = document.getElementById('accelerationChart').getContext('2d');
394
+ const speedChart = new Chart(speedChartCtx, {
395
+ type: 'line',
396
+ data: {
397
+ labels: [],
398
+ datasets: [{
399
+ label: 'Speed',
400
+ data: [],
401
+ borderColor: 'rgba(75, 192, 192, 1)',
402
+ borderWidth: 1,
403
+ fill: false
404
+ }]
405
+ },
406
+ options: {
407
+ scales: {
408
+ y: { beginAtZero: true }
409
+ },
410
+ animation: false
411
+ }
412
+ });
413
+ const accelerationChart = new Chart(accelerationChartCtx, {
414
+ type: 'line',
415
+ data: {
416
+ labels: [],
417
+ datasets: [{
418
+ label: 'Acceleration',
419
+ data: [],
420
+ borderColor: 'rgba(255, 99, 132, 1)',
421
+ borderWidth: 1,
422
+ fill: false
423
+ }]
424
+ },
425
+ options: {
426
+ scales: {
427
+ y: { beginAtZero: true }
428
+ },
429
+ animation: false
430
+ }
431
+ });
432
+
433
+ // Global variables
434
+ let allResults = [];
435
+ let currentFrame = 0;
436
+ let fps = 30;
437
+ let isPlaying = false;
438
+ let videoElement;
439
+
440
+ // Function to update 3D model
441
+ function update3DModel(landmarks) {
442
+ landmarks.forEach((coord, index) => {
443
+ spheres[index].position.set((coord[0] - 0.5) * 2, -(coord[1] - 0.5) * 2, -coord[2] * 0.5);
444
+ });
445
+
446
+ // Clear existing cylinders
447
+ cylinders.forEach(cylinder => scene.remove(cylinder));
448
+ cylinders.length = 0;
449
+
450
+ // Create new cylinders
451
+ const connections = [
452
+ // Face
453
+ [0, 1], [1, 4], [4, 7], [7, 8], [8, 5], [5, 2], [2, 0],
454
+ [0, 9], [9, 10], [0, 10],
455
+ // Arms
456
+ [11, 13], [13, 15], [15, 17], [15, 19], [15, 21],
457
+ [12, 14], [14, 16], [16, 18], [16, 20], [16, 22],
458
+ // Body
459
+ [11, 12], [11, 23], [12, 24], [23, 24],
460
+ // Legs
461
+ [23, 25], [25, 27], [27, 29],
462
+ [24, 26], [26, 28], [28, 30], [28, 32]
463
+ ];
464
+
465
+ connections.forEach(([i, j]) => {
466
+ cylinders.push(createCylinder(spheres[i].position, spheres[j].position));
467
+ });
468
+ }
469
+
470
+ // Function to update charts
471
+ function updateCharts(frame) {
472
+ const frameData = allResults[frame];
473
+ if (frameData) {
474
+ speedChart.data.labels.push(frame);
475
+ speedChart.data.datasets[0].data.push(frameData.velocities.reduce((a, b) => a + b, 0) / frameData.velocities.length);
476
+
477
+ accelerationChart.data.labels.push(frame);
478
+ accelerationChart.data.datasets[0].data.push(frameData.accelerations.reduce((a, b) => a + b, 0) / frameData.accelerations.length);
479
+
480
+ // Keep only the last 100 data points
481
+ if (speedChart.data.labels.length > 100) {
482
+ speedChart.data.labels.shift();
483
+ speedChart.data.datasets[0].data.shift();
484
+ }
485
+ if (accelerationChart.data.labels.length > 100) {
486
+ accelerationChart.data.labels.shift();
487
+ accelerationChart.data.datasets[0].data.shift();
488
+ }
489
+
490
+ speedChart.update();
491
+ accelerationChart.update();
492
+ }
493
+ }
494
+
495
+ // Function to animate the scene
496
+ function animate() {
497
+ requestAnimationFrame(animate);
498
+ renderer.render(scene, camera);
499
+
500
+ if (isPlaying && allResults.length > 0) {
501
+ const frameData = allResults[currentFrame];
502
+ if (frameData) {
503
+ update3DModel(frameData.landmarks);
504
+ updateCharts(currentFrame);
505
+ currentFrame = (currentFrame + 1) % allResults.length;
506
+ if (videoElement) {
507
+ videoElement.currentTime = currentFrame / fps;
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ // Start animation
514
+ animate();
515
+
516
+ // Function to show messages
517
+ function showMessage(message) {
518
+ const messageBox = document.getElementById('message-box');
519
+ const messageText = document.getElementById('message-text');
520
+ messageText.textContent = message;
521
+ messageBox.style.display = 'block';
522
+
523
+ setTimeout(() => {
524
+ messageBox.style.display = 'none';
525
+ }, 2000);
526
+ }
527
+
528
+ // Function to show/hide loading indicator
529
+ function showLoadingIndicator(isLoading) {
530
+ const loadingIndicator = document.getElementById('loading-indicator');
531
+ loadingIndicator.style.display = isLoading ? 'block' : 'none';
532
+ }
533
+
534
+ // Video upload handling
535
+ document.getElementById('video-upload').addEventListener('change', function(e) {
536
+ const file = e.target.files[0];
537
+ if (file) {
538
+ const formData = new FormData();
539
+ formData.append('video', file);
540
+
541
+ showLoadingIndicator(true);
542
+
543
+ fetch('/upload_video', {
544
+ method: 'POST',
545
+ body: formData
546
+ }).then(response => response.json())
547
+ .then(data => {
548
+ showLoadingIndicator(false);
549
+ if (data.success) {
550
+ showMessage(data.message);
551
+ loadResults();
552
+ } else {
553
+ showMessage('Error uploading video');
554
+ }
555
+ }).catch(error => {
556
+ showLoadingIndicator(false);
557
+ showMessage('Upload failed: ' + error.message);
558
+ });
559
+ }
560
+ });
561
+
562
+ // Function to load results
563
+ function loadResults() {
564
+ fetch('/get_results')
565
+ .then(response => response.json())
566
+ .then(data => {
567
+ allResults = data.results;
568
+ fps = data.fps;
569
+ currentFrame = 0;
570
+
571
+ // Reset charts
572
+ speedChart.data.labels = [];
573
+ speedChart.data.datasets[0].data = [];
574
+ accelerationChart.data.labels = [];
575
+ accelerationChart.data.datasets[0].data = [];
576
+
577
+ // Load video
578
+ videoElement = document.createElement('video');
579
+ videoElement.src = '/uploads/temp_video.mp4';
580
+ videoElement.load();
581
+
582
+ const videoContainer = document.getElementById('video-container');
583
+ videoContainer.innerHTML = '';
584
+ videoContainer.appendChild(videoElement);
585
+
586
+ // Add play/pause button
587
+ const playPauseBtn = document.createElement('button');
588
+ playPauseBtn.textContent = 'Play';
589
+ playPauseBtn.onclick = togglePlayPause;
590
+ videoContainer.appendChild(playPauseBtn);
591
+
592
+ showMessage('Video and results loaded successfully');
593
+ })
594
+ .catch(error => {
595
+ showMessage('Error loading results: ' + error.message);
596
+ });
597
+ }
598
+
599
+ // Function to toggle play/pause
600
+ function togglePlayPause() {
601
+ isPlaying = !isPlaying;
602
+ const playPauseBtn = document.querySelector('#video-container button');
603
+ playPauseBtn.textContent = isPlaying ? 'Pause' : 'Play';
604
+ if (isPlaying) {
605
+ videoElement.play();
606
+ } else {
607
+ videoElement.pause();
608
+ }
609
+ }
610
+
611
+ // Sync video with 3D model and charts
612
+ videoElement.addEventListener('timeupdate', function() {
613
+ currentFrame = Math.floor(videoElement.currentTime * fps);
614
+ const frameData = allResults[currentFrame];
615
+ if (frameData) {
616
+ update3DModel(frameData.landmarks);
617
+ updateCharts(currentFrame);
618
+ }
619
+ });
620
+
621
+ // Slider for video and 3D model scale
622
+ document.getElementById('video-scale').addEventListener('input', function(e) {
623
+ videoElement.style.transform = `scale(${e.target.value})`;
624
+ });
625
+
626
+ document.getElementById('model-scale').addEventListener('input', function(e) {
627
+ const newWidth = modelContainer.clientWidth * e.target.value;
628
+ const newHeight = newWidth / aspect;
629
+ renderer.setSize(newWidth, newHeight);
630
+ });
631
+
632
+ // Window resize handling
633
+ window.addEventListener('resize', function() {
634
+ const width = modelContainer.clientWidth;
635
+ const height = width / aspect;
636
+ camera.aspect = aspect;
637
+ camera.updateProjectionMatrix();
638
+ renderer.setSize(width, height);
639
+ });
640
+
641
+ // Initialize the application
642
+ document.addEventListener('DOMContentLoaded', function() {
643
+ // Check if there's a previously uploaded video
644
+ fetch('/get_results')
645
+ .then(response => {
646
+ if (response.ok) {
647
+ return response.json();
648
+ } else {
649
+ throw new Error('No previous results found');
650
+ }
651
+ })
652
+ .then(data => {
653
+ allResults = data.results;
654
+ fps = data.fps;
655
+ loadResults();
656
+ })
657
+ .catch(error => {
658
+ console.log('No previous video found. Please upload a new video.');
659
+ });
660
+ });
661
+ </script>
662
+ </body>
663
  </html>