548 lines
19 KiB
HTML
548 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>实时手部检测控制系统</title>
|
|
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f0f0f0;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
h1 {
|
|
text-align: center;
|
|
color: #333;
|
|
margin-bottom: 30px;
|
|
}
|
|
.main-content {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.video-section {
|
|
background: #000;
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
min-height: 480px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.video-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
max-width: 640px;
|
|
max-height: 480px;
|
|
}
|
|
#videoPreview {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
border-radius: 5px;
|
|
}
|
|
.no-video {
|
|
color: #999;
|
|
font-size: 18px;
|
|
text-align: center;
|
|
}
|
|
.control-panel {
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
.status-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
.status-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
padding: 8px 12px;
|
|
background: white;
|
|
border-radius: 5px;
|
|
border: 1px solid #ddd;
|
|
}
|
|
.status-label {
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
.status-value {
|
|
color: #007bff;
|
|
font-family: monospace;
|
|
}
|
|
.angle-display {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.angle-card {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border: 2px solid #ddd;
|
|
text-align: center;
|
|
}
|
|
.angle-card.x-axis {
|
|
border-color: #ff4757;
|
|
}
|
|
.angle-card.y-axis {
|
|
border-color: #2ed573;
|
|
}
|
|
.angle-card.z-axis {
|
|
border-color: #3742fa;
|
|
}
|
|
.angle-label {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
}
|
|
.angle-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
font-family: monospace;
|
|
}
|
|
.action-display {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border: 1px solid #ddd;
|
|
margin-bottom: 20px;
|
|
}
|
|
.action-title {
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
.action-value {
|
|
font-size: 18px;
|
|
padding: 8px 12px;
|
|
background: #e3f2fd;
|
|
border-radius: 5px;
|
|
color: #1976d2;
|
|
font-family: monospace;
|
|
}
|
|
.grip-status {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px;
|
|
background: white;
|
|
border-radius: 5px;
|
|
border: 1px solid #ddd;
|
|
}
|
|
.grip-indicator {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: #ccc;
|
|
transition: background 0.3s ease;
|
|
}
|
|
.grip-indicator.active {
|
|
background: #ff6b6b;
|
|
}
|
|
.connection-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
background: white;
|
|
border-radius: 5px;
|
|
border: 1px solid #ddd;
|
|
margin-bottom: 20px;
|
|
}
|
|
.connection-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #ccc;
|
|
transition: background 0.3s ease;
|
|
}
|
|
.connection-dot.connected {
|
|
background: #2ed573;
|
|
}
|
|
.test-controls {
|
|
margin-top: 20px;
|
|
}
|
|
.video-selector {
|
|
margin-bottom: 15px;
|
|
}
|
|
.video-select {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
font-size: 14px;
|
|
background: white;
|
|
margin-bottom: 10px;
|
|
}
|
|
.video-select:focus {
|
|
outline: none;
|
|
border-color: #007bff;
|
|
}
|
|
.test-button {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: #007bff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
margin-bottom: 10px;
|
|
}
|
|
.test-button:hover {
|
|
background: #0056b3;
|
|
}
|
|
.test-button:disabled {
|
|
background: #ccc;
|
|
cursor: not-allowed;
|
|
}
|
|
.log-section {
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
border: 1px solid #e0e0e0;
|
|
}
|
|
.log-title {
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
color: #333;
|
|
}
|
|
.log-container {
|
|
background: #000;
|
|
color: #0f0;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
.log-entry {
|
|
margin-bottom: 5px;
|
|
}
|
|
.log-timestamp {
|
|
color: #888;
|
|
}
|
|
.fps-display {
|
|
font-size: 14px;
|
|
color: #666;
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🤖 实时手部检测控制系统</h1>
|
|
|
|
<div class="main-content">
|
|
<div class="video-section">
|
|
<div class="video-container">
|
|
<img id="videoPreview" style="display:none;" alt="视频预览">
|
|
<div id="noVideo" class="no-video">
|
|
等待视频流...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="control-panel">
|
|
<div class="connection-status">
|
|
<div id="connectionDot" class="connection-dot"></div>
|
|
<span id="connectionStatus">连接中...</span>
|
|
</div>
|
|
|
|
<div class="status-section">
|
|
<div class="status-item">
|
|
<span class="status-label">FPS:</span>
|
|
<span id="fpsValue" class="status-value">0</span>
|
|
</div>
|
|
<div class="status-item">
|
|
<span class="status-label">速度:</span>
|
|
<span id="speedValue" class="status-value">5</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="angle-display">
|
|
<div class="angle-card x-axis">
|
|
<div class="angle-label" style="color: #ff4757;">X轴 (左右)</div>
|
|
<div id="xAngle" class="angle-value" style="color: #ff4757;">90°</div>
|
|
</div>
|
|
<div class="angle-card y-axis">
|
|
<div class="angle-label" style="color: #2ed573;">Y轴 (上下)</div>
|
|
<div id="yAngle" class="angle-value" style="color: #2ed573;">90°</div>
|
|
</div>
|
|
<div class="angle-card z-axis">
|
|
<div class="angle-label" style="color: #3742fa;">Z轴 (前后)</div>
|
|
<div id="zAngle" class="angle-value" style="color: #3742fa;">90°</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="action-display">
|
|
<div class="action-title">当前动作:</div>
|
|
<div id="actionValue" class="action-value">none</div>
|
|
</div>
|
|
|
|
<div class="grip-status">
|
|
<span>抓取状态:</span>
|
|
<div class="grip-indicator" id="gripIndicator"></div>
|
|
</div>
|
|
|
|
<div class="test-controls">
|
|
<div class="video-selector">
|
|
<label for="videoSelect" style="font-size: 14px; font-weight: bold; margin-bottom: 8px; display: block;">选择测试视频:</label>
|
|
<select id="videoSelect" class="video-select">
|
|
<option value="">加载中...</option>
|
|
</select>
|
|
</div>
|
|
<button id="testButton" class="test-button">开始视频测试</button>
|
|
<button id="refreshButton" class="test-button" style="background: #28a745; margin-top: 5px;">刷新视频列表</button>
|
|
<div style="font-size: 12px; color: #666; margin-top: 5px; text-align: center;">
|
|
选择本地视频文件进行检测
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log-section">
|
|
<div class="log-title">系统日志</div>
|
|
<div class="log-container" id="logContainer"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
class HandDetectionClient {
|
|
constructor() {
|
|
this.socket = io();
|
|
this.isConnected = false;
|
|
this.setupSocketEvents();
|
|
this.setupUI();
|
|
}
|
|
|
|
setupSocketEvents() {
|
|
this.socket.on('connect', () => {
|
|
this.isConnected = true;
|
|
this.updateConnectionStatus('已连接', true);
|
|
this.log('WebSocket连接已建立');
|
|
|
|
// 注册为web预览客户端
|
|
this.socket.emit('register_client', {
|
|
type: 'web_preview'
|
|
});
|
|
});
|
|
|
|
this.socket.on('disconnect', () => {
|
|
this.isConnected = false;
|
|
this.updateConnectionStatus('连接断开', false);
|
|
this.log('WebSocket连接已断开');
|
|
});
|
|
|
|
this.socket.on('status', (data) => {
|
|
this.log(`服务器状态: ${data.message}`);
|
|
});
|
|
|
|
this.socket.on('registration_success', (data) => {
|
|
this.log(`客户端注册成功: ${data.type}`);
|
|
});
|
|
|
|
this.socket.on('detection_results', (data) => {
|
|
this.updateDetectionResults(data);
|
|
});
|
|
|
|
this.socket.on('error', (data) => {
|
|
this.log(`错误: ${data.message}`, 'error');
|
|
});
|
|
|
|
this.socket.on('pong', (data) => {
|
|
// 处理ping响应
|
|
});
|
|
|
|
this.socket.on('test_started', (data) => {
|
|
this.log(`✅ ${data.message}`, 'info');
|
|
});
|
|
|
|
this.socket.on('test_error', (data) => {
|
|
this.log(`❌ ${data.message}`, 'error');
|
|
if (data.help) {
|
|
this.log(`💡 ${data.help}`, 'info');
|
|
}
|
|
});
|
|
|
|
this.socket.on('video_list', (data) => {
|
|
this.updateVideoList(data.videos);
|
|
});
|
|
}
|
|
|
|
setupUI() {
|
|
document.getElementById('testButton').addEventListener('click', () => {
|
|
this.startLocalTest();
|
|
});
|
|
|
|
document.getElementById('refreshButton').addEventListener('click', () => {
|
|
this.refreshVideoList();
|
|
});
|
|
|
|
// 请求视频列表
|
|
this.refreshVideoList();
|
|
|
|
// 定期发送ping
|
|
setInterval(() => {
|
|
if (this.isConnected) {
|
|
this.socket.emit('ping');
|
|
}
|
|
}, 5000);
|
|
}
|
|
|
|
updateConnectionStatus(status, connected) {
|
|
const statusElement = document.getElementById('connectionStatus');
|
|
const dotElement = document.getElementById('connectionDot');
|
|
|
|
statusElement.textContent = status;
|
|
if (connected) {
|
|
dotElement.classList.add('connected');
|
|
} else {
|
|
dotElement.classList.remove('connected');
|
|
}
|
|
}
|
|
|
|
updateDetectionResults(data) {
|
|
const { control_signal, processed_frame, fps } = data;
|
|
|
|
// 更新角度显示
|
|
document.getElementById('xAngle').textContent = `${control_signal.x_angle.toFixed(1)}°`;
|
|
document.getElementById('yAngle').textContent = `${control_signal.y_angle.toFixed(1)}°`;
|
|
document.getElementById('zAngle').textContent = `${control_signal.z_angle.toFixed(1)}°`;
|
|
|
|
// 更新动作显示
|
|
document.getElementById('actionValue').textContent = control_signal.action;
|
|
|
|
// 更新速度显示
|
|
document.getElementById('speedValue').textContent = control_signal.speed;
|
|
|
|
// 更新抓取状态
|
|
const gripIndicator = document.getElementById('gripIndicator');
|
|
if (control_signal.grip === 1) {
|
|
gripIndicator.classList.add('active');
|
|
} else {
|
|
gripIndicator.classList.remove('active');
|
|
}
|
|
|
|
// 更新FPS显示
|
|
document.getElementById('fpsValue').textContent = fps || 0;
|
|
|
|
// 更新视频预览
|
|
if (processed_frame) {
|
|
const videoPreview = document.getElementById('videoPreview');
|
|
const noVideo = document.getElementById('noVideo');
|
|
|
|
videoPreview.src = processed_frame;
|
|
videoPreview.style.display = 'block';
|
|
noVideo.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
startLocalTest() {
|
|
const button = document.getElementById('testButton');
|
|
const videoSelect = document.getElementById('videoSelect');
|
|
const selectedVideo = videoSelect.value;
|
|
|
|
if (!selectedVideo) {
|
|
this.log('❌ 请先选择一个视频文件', 'error');
|
|
return;
|
|
}
|
|
|
|
button.disabled = true;
|
|
button.textContent = '启动中...';
|
|
|
|
// 请求开始本地测试,带上选择的视频
|
|
this.socket.emit('start_local_test', {
|
|
video_path: selectedVideo
|
|
});
|
|
|
|
this.log(`正在启动视频测试: ${selectedVideo}`);
|
|
|
|
setTimeout(() => {
|
|
button.disabled = false;
|
|
button.textContent = '开始视频测试';
|
|
}, 5000);
|
|
}
|
|
|
|
refreshVideoList() {
|
|
this.log('正在刷新视频列表...');
|
|
this.socket.emit('get_video_list');
|
|
}
|
|
|
|
updateVideoList(videos) {
|
|
const videoSelect = document.getElementById('videoSelect');
|
|
videoSelect.innerHTML = '';
|
|
|
|
if (videos.length === 0) {
|
|
const option = document.createElement('option');
|
|
option.value = '';
|
|
option.textContent = '未找到视频文件';
|
|
videoSelect.appendChild(option);
|
|
|
|
this.log('❌ 未找到测试视频文件', 'error');
|
|
this.log('💡 运行 python create_test_video.py 生成测试视频', 'info');
|
|
} else {
|
|
// 添加默认选项
|
|
const defaultOption = document.createElement('option');
|
|
defaultOption.value = '';
|
|
defaultOption.textContent = '请选择视频文件';
|
|
videoSelect.appendChild(defaultOption);
|
|
|
|
// 添加视频文件选项
|
|
videos.forEach(video => {
|
|
const option = document.createElement('option');
|
|
option.value = video.path;
|
|
option.textContent = `${video.name} (${video.size})`;
|
|
videoSelect.appendChild(option);
|
|
});
|
|
|
|
this.log(`✅ 找到 ${videos.length} 个视频文件`);
|
|
}
|
|
}
|
|
|
|
log(message, type = 'info') {
|
|
const logContainer = document.getElementById('logContainer');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = 'log-entry';
|
|
|
|
const color = type === 'error' ? '#f00' : '#0f0';
|
|
logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span> <span style="color: ${color}">${message}</span>`;
|
|
|
|
logContainer.appendChild(logEntry);
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
|
|
// 限制日志数量
|
|
const entries = logContainer.querySelectorAll('.log-entry');
|
|
if (entries.length > 100) {
|
|
entries[0].remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始化客户端
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new HandDetectionClient();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |