checkhandpi/templates/index.html
2025-08-11 13:15:21 +08:00

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>