364 lines
13 KiB
HTML
364 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Pose Detection Visualization</title>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
}
|
|
.page-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
h1, h2, h3 {
|
|
color: #2c3e50;
|
|
}
|
|
.container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.panel {
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.video-container {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
}
|
|
.video-feed {
|
|
width: 100%;
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.canvas-container {
|
|
position: relative;
|
|
width: 100%;
|
|
min-height: 480px;
|
|
}
|
|
#skeletonCanvas {
|
|
border: 1px solid #ddd;
|
|
background-color: #f8f9fa;
|
|
border-radius: 4px;
|
|
}
|
|
.data-container {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
max-width: 500px;
|
|
}
|
|
.raw-data {
|
|
background-color: #282c34;
|
|
color: #abb2bf;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
overflow: auto;
|
|
max-height: 400px;
|
|
font-size: 14px;
|
|
}
|
|
.controls {
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
background-color: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
button {
|
|
background-color: #3498db;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 15px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-right: 10px;
|
|
font-size: 14px;
|
|
}
|
|
button:hover {
|
|
background-color: #2980b9;
|
|
}
|
|
.connection-status {
|
|
display: inline-block;
|
|
margin-left: 15px;
|
|
font-weight: bold;
|
|
}
|
|
.connected {
|
|
color: #27ae60;
|
|
}
|
|
.disconnected {
|
|
color: #e74c3c;
|
|
}
|
|
select {
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ddd;
|
|
margin-right: 10px;
|
|
}
|
|
footer {
|
|
margin-top: 30px;
|
|
text-align: center;
|
|
color: #7f8c8d;
|
|
font-size: 14px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page-container">
|
|
<h1>Pose Detection Visualization</h1>
|
|
|
|
<div class="controls">
|
|
<button id="connectBtn">Connect to Server</button>
|
|
<select id="colorTheme">
|
|
<option value="default">Default Colors</option>
|
|
<option value="neon">Neon</option>
|
|
<option value="pastel">Pastel</option>
|
|
<option value="grayscale">Grayscale</option>
|
|
</select>
|
|
<span id="connectionStatus" class="connection-status disconnected">Disconnected</span>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="panel video-container">
|
|
<h2>Server Video Feeds</h2>
|
|
<h3>Raw Video Feed</h3>
|
|
<img id="rawVideoFeed" class="video-feed" src="http://localhost:5000/video_feed" alt="Raw Video Feed" />
|
|
|
|
<h3>Annotated Video Feed</h3>
|
|
<img id="annotatedVideoFeed" class="video-feed" src="http://localhost:5000/video_feed/annotated" alt="Annotated Video Feed" />
|
|
</div>
|
|
|
|
<div class="panel data-container">
|
|
<h2>Raw Landmark Data</h2>
|
|
<pre id="rawData" class="raw-data">Waiting for data...</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h2>Custom Skeleton Visualization</h2>
|
|
<div class="canvas-container">
|
|
<canvas id="skeletonCanvas" width="640" height="480"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<footer>
|
|
<p>Powered by MediaPipe, OpenCV, and Socket.IO</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
|
<script>
|
|
// DOM Elements
|
|
const rawDataElement = document.getElementById('rawData');
|
|
const skeletonCanvas = document.getElementById('skeletonCanvas');
|
|
const ctx = skeletonCanvas.getContext('2d');
|
|
const connectBtn = document.getElementById('connectBtn');
|
|
const connectionStatus = document.getElementById('connectionStatus');
|
|
const colorThemeSelect = document.getElementById('colorTheme');
|
|
|
|
// Set canvas dimensions
|
|
const canvasWidth = skeletonCanvas.width;
|
|
const canvasHeight = skeletonCanvas.height;
|
|
|
|
// Socket connection
|
|
let socket;
|
|
let isConnected = false;
|
|
let landmarksData = null;
|
|
|
|
// Color themes
|
|
const colorThemes = {
|
|
default: {
|
|
background: '#f8f9fa',
|
|
joints: '#e74c3c',
|
|
torso: '#e74c3c',
|
|
arms: '#3498db',
|
|
legs: '#2ecc71',
|
|
shoulders: '#f39c12',
|
|
head: '#9b59b6'
|
|
},
|
|
neon: {
|
|
background: '#121212',
|
|
joints: '#ff00ff',
|
|
torso: '#ff0099',
|
|
arms: '#00ffff',
|
|
legs: '#00ff00',
|
|
shoulders: '#ffff00',
|
|
head: '#ff9900'
|
|
},
|
|
pastel: {
|
|
background: '#f8f9fa',
|
|
joints: '#f08080',
|
|
torso: '#f08080',
|
|
arms: '#98d8c8',
|
|
legs: '#b5ead7',
|
|
shoulders: '#ffdac1',
|
|
head: '#c7ceea'
|
|
},
|
|
grayscale: {
|
|
background: '#ffffff',
|
|
joints: '#333333',
|
|
torso: '#444444',
|
|
arms: '#666666',
|
|
legs: '#888888',
|
|
shoulders: '#aaaaaa',
|
|
head: '#555555'
|
|
}
|
|
};
|
|
|
|
let currentColorTheme = colorThemes.default;
|
|
|
|
// Connect to the Socket.IO server
|
|
function connectToServer() {
|
|
if (isConnected) {
|
|
socket.disconnect();
|
|
isConnected = false;
|
|
connectBtn.textContent = 'Connect to Server';
|
|
connectionStatus.textContent = 'Disconnected';
|
|
connectionStatus.className = 'connection-status disconnected';
|
|
return;
|
|
}
|
|
|
|
socket = io('http://localhost:5000');
|
|
|
|
socket.on('connect', function() {
|
|
isConnected = true;
|
|
connectBtn.textContent = 'Disconnect';
|
|
connectionStatus.textContent = 'Connected';
|
|
connectionStatus.className = 'connection-status connected';
|
|
console.log('Connected to server');
|
|
});
|
|
|
|
socket.on('disconnect', function() {
|
|
isConnected = false;
|
|
connectBtn.textContent = 'Connect to Server';
|
|
connectionStatus.textContent = 'Disconnected';
|
|
connectionStatus.className = 'connection-status disconnected';
|
|
console.log('Disconnected from server');
|
|
});
|
|
|
|
socket.on('landmarks', function(data) {
|
|
landmarksData = JSON.parse(data);
|
|
|
|
// Update raw data display
|
|
rawDataElement.textContent = JSON.stringify(landmarksData, null, 2);
|
|
|
|
// Draw skeleton
|
|
drawSkeleton(landmarksData);
|
|
});
|
|
}
|
|
|
|
// Draw skeleton on canvas
|
|
function drawSkeleton(data) {
|
|
// Clear canvas
|
|
ctx.fillStyle = currentColorTheme.background;
|
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
|
|
if (!data || !data.landmarks || data.landmarks.length === 0) {
|
|
// No landmarks to draw
|
|
ctx.font = '20px Arial';
|
|
ctx.fillStyle = '#999';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('No pose detected', canvasWidth/2, canvasHeight/2);
|
|
return;
|
|
}
|
|
|
|
const imageWidth = data.image_width;
|
|
const imageHeight = data.image_height;
|
|
|
|
// Scale factors to map coordinates to canvas
|
|
const scaleX = canvasWidth / imageWidth;
|
|
const scaleY = canvasHeight / imageHeight;
|
|
|
|
// Draw connections
|
|
if (data.connections) {
|
|
data.connections.forEach(connection => {
|
|
const startLandmark = data.landmarks.find(l => l.idx === connection.start);
|
|
const endLandmark = data.landmarks.find(l => l.idx === connection.end);
|
|
|
|
if (startLandmark && endLandmark) {
|
|
// Get connection color based on body part
|
|
let color = currentColorTheme.joints;
|
|
|
|
// Determine which body part this connection belongs to
|
|
// Shoulders (connection between shoulder landmarks)
|
|
if ((connection.start === 11 && connection.end === 12) ||
|
|
(connection.start === 12 && connection.end === 11)) {
|
|
color = currentColorTheme.shoulders;
|
|
}
|
|
// Torso (connections between shoulders and hips)
|
|
else if ((connection.start >= 11 && connection.start <= 12 && connection.end >= 23 && connection.end <= 24) ||
|
|
(connection.start >= 23 && connection.start <= 24 && connection.end >= 11 && connection.end <= 12) ||
|
|
(connection.start === 23 && connection.end === 24) ||
|
|
(connection.start === 24 && connection.end === 23)) {
|
|
color = currentColorTheme.torso;
|
|
}
|
|
// Arms (connections involving elbows and wrists)
|
|
else if ((connection.start >= 11 && connection.start <= 16) ||
|
|
(connection.end >= 11 && connection.end <= 16)) {
|
|
color = currentColorTheme.arms;
|
|
}
|
|
// Legs (connections involving knees and ankles)
|
|
else if ((connection.start >= 23 && connection.start <= 32) ||
|
|
(connection.end >= 23 && connection.end <= 32)) {
|
|
color = currentColorTheme.legs;
|
|
}
|
|
|
|
// Draw line
|
|
ctx.beginPath();
|
|
ctx.moveTo(startLandmark.x * scaleX, startLandmark.y * scaleY);
|
|
ctx.lineTo(endLandmark.x * scaleX, endLandmark.y * scaleY);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Draw landmarks
|
|
data.landmarks.forEach(landmark => {
|
|
const x = landmark.x * scaleX;
|
|
const y = landmark.y * scaleY;
|
|
|
|
// Skip drawing low visibility landmarks
|
|
if (landmark.visibility < 0.5) return;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 5, 0, 2 * Math.PI);
|
|
ctx.fillStyle = currentColorTheme.joints;
|
|
ctx.fill();
|
|
});
|
|
}
|
|
|
|
// Event listeners
|
|
connectBtn.addEventListener('click', connectToServer);
|
|
|
|
colorThemeSelect.addEventListener('change', function() {
|
|
const theme = this.value;
|
|
currentColorTheme = colorThemes[theme];
|
|
|
|
// Redraw if we have data
|
|
if (landmarksData) {
|
|
drawSkeleton(landmarksData);
|
|
}
|
|
});
|
|
|
|
// Auto-connect on page load
|
|
window.addEventListener('load', function() {
|
|
// Draw empty skeleton
|
|
ctx.fillStyle = currentColorTheme.background;
|
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
ctx.font = '20px Arial';
|
|
ctx.fillStyle = '#999';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('Connect to see skeleton visualization', canvasWidth/2, canvasHeight/2);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |