feat: add ControlPanelWidget for application UI

This commit is contained in:
game-loader 2025-06-22 16:51:48 +08:00
parent 1d94e77557
commit 487f8d1d55
7 changed files with 1230 additions and 0 deletions

403
control_panel_widget.py Normal file
View File

@ -0,0 +1,403 @@
import os
import tempfile
import cv2
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QComboBox, QRadioButton, QButtonGroup,
QFileDialog, QTextEdit, QGroupBox, QFrame, QScrollArea)
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QPalette
class ControlPanelWidget(QScrollArea):
video_selected = pyqtSignal(str)
start_comparison = pyqtSignal(str)
stop_comparison = pyqtSignal()
initialize_system = pyqtSignal()
def __init__(self, motion_app):
super().__init__()
self.motion_app = motion_app
self.current_video_path = None
self.setup_ui()
def setup_ui(self):
# Create main widget for scroll area
main_widget = QWidget()
main_layout = QVBoxLayout(main_widget)
main_layout.setSpacing(10)
main_layout.setContentsMargins(15, 15, 15, 15)
# Title
title_label = QLabel("🎛️ 控制面板")
title_font = QFont()
title_font.setPointSize(14)
title_font.setBold(True)
title_label.setFont(title_font)
main_layout.addWidget(title_label)
# Add separator
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
main_layout.addWidget(separator)
# Display settings group
self.setup_display_settings(main_layout)
# Video source selection group
self.setup_video_source(main_layout)
# System initialization group
self.setup_system_init(main_layout)
# System status group
self.setup_system_status(main_layout)
# Stretch to push everything to top
main_layout.addStretch()
# Set scroll area properties
self.setWidget(main_widget)
self.setWidgetResizable(True)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Style the scroll area
self.setStyleSheet("""
QScrollArea {
border: none;
background-color: #ffffff;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 10px 0 10px;
}
""")
def setup_display_settings(self, parent_layout):
group = QGroupBox("显示设置")
layout = QVBoxLayout(group)
# Resolution selection
res_label = QLabel("显示分辨率:")
self.resolution_combo = QComboBox()
self.resolution_combo.addItems([
"高清 (1280x800)",
"中等 (960x720)",
"标准 (640x480)"
])
self.resolution_combo.setCurrentIndex(1) # Default to medium
self.resolution_combo.currentTextChanged.connect(self.on_resolution_changed)
layout.addWidget(res_label)
layout.addWidget(self.resolution_combo)
parent_layout.addWidget(group)
def setup_video_source(self, parent_layout):
group = QGroupBox("视频来源")
layout = QVBoxLayout(group)
# Radio buttons for video source
self.video_source_group = QButtonGroup()
self.preset_radio = QRadioButton("预设视频")
self.upload_radio = QRadioButton("上传视频")
self.preset_radio.setChecked(True)
self.video_source_group.addButton(self.preset_radio, 0)
self.video_source_group.addButton(self.upload_radio, 1)
layout.addWidget(self.preset_radio)
layout.addWidget(self.upload_radio)
# Preset video selection
self.preset_combo = QComboBox()
# --- FIX STARTS HERE ---
# The incorrect call to self.load_preset_videos() has been removed from here.
# --- FIX ENDS HERE ---
# Upload button
self.upload_button = QPushButton("选择文件...")
self.upload_button.clicked.connect(self.upload_video)
self.upload_button.setEnabled(False)
# Video info display - This is now created BEFORE it is used.
self.video_info = QLabel("未选择视频")
self.video_info.setWordWrap(True)
self.video_info.setStyleSheet("color: #666; font-size: 9pt;")
layout.addWidget(self.preset_combo)
layout.addWidget(self.upload_button)
layout.addWidget(self.video_info)
# Connect signals
self.preset_radio.toggled.connect(self.on_video_source_changed)
self.preset_combo.currentTextChanged.connect(self.on_preset_video_changed)
parent_layout.addWidget(group)
# Load available presets after all UI elements are set up. This is the correct place.
self.load_preset_videos()
def setup_system_init(self, parent_layout):
group = QGroupBox("⚙️ 系统初始化")
layout = QVBoxLayout(group)
self.init_button = QPushButton("🚀 初始化系统")
self.init_button.clicked.connect(self.initialize_system_clicked)
self.init_button.setMinimumHeight(40)
self.init_button.setStyleSheet("""
QPushButton {
background-color: #0086d3;
color: white;
border: none;
border-radius: 5px;
font-weight: bold;
}
QPushButton:hover {
background-color: #006ba3;
}
QPushButton:pressed {
background-color: #004d73;
}
""")
# Control buttons
button_layout = QHBoxLayout()
self.preview_button = QPushButton("📷 预览")
self.start_button = QPushButton("🚀 开始比较")
self.preview_button.clicked.connect(self.preview_camera)
self.start_button.clicked.connect(self.start_comparison_clicked)
self.preview_button.setEnabled(False)
self.start_button.setEnabled(False)
button_layout.addWidget(self.preview_button)
button_layout.addWidget(self.start_button)
layout.addWidget(self.init_button)
layout.addLayout(button_layout)
parent_layout.addWidget(group)
def setup_system_status(self, parent_layout):
group = QGroupBox(" 系统状态")
layout = QVBoxLayout(group)
self.status_info = QTextEdit()
self.status_info.setMaximumHeight(100)
self.status_info.setReadOnly(True)
self.update_system_status()
layout.addWidget(self.status_info)
parent_layout.addWidget(group)
def load_preset_videos(self):
preset_videos = {
"六字诀": "liuzi.mp4",
"六字诀精简": "liuzi-short.mp4",
}
preset_folder = "preset_videos"
if os.path.exists(preset_folder):
available_presets = []
for display_name, filename in preset_videos.items():
full_path = os.path.join(preset_folder, filename)
if os.path.exists(full_path):
available_presets.append(display_name)
self.preset_combo.clear() # Clear previous items before adding new ones
if available_presets:
self.preset_combo.addItems(available_presets)
# Set default selection
if available_presets:
self.on_preset_video_changed(available_presets[0])
else:
self.preset_combo.addItem("无可用预设视频")
else:
self.preset_combo.addItem("preset_videos 文件夹不存在")
def on_video_source_changed(self, checked):
if self.preset_radio.isChecked():
self.preset_combo.setEnabled(True)
self.upload_button.setEnabled(False)
if self.preset_combo.count() > 0 and self.preset_combo.currentText() != "无可用预设视频":
self.on_preset_video_changed(self.preset_combo.currentText())
else:
self.preset_combo.setEnabled(False)
self.upload_button.setEnabled(True)
self.current_video_path = None
self.video_info.setText("请选择上传文件")
def on_preset_video_changed(self, display_name):
preset_videos = {
"六字诀": "liuzi.mp4",
"六字诀精简": "liuzi-short.mp4",
}
if display_name in preset_videos:
filename = preset_videos[display_name]
video_path = os.path.join("preset_videos", filename)
if os.path.exists(video_path):
self.current_video_path = video_path
self.update_video_info()
self.video_selected.emit(video_path)
else:
self.video_info.setText("视频文件不存在")
def upload_video(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "选择视频文件", "",
"Video Files (*.mp4 *.avi *.mov *.mkv);;All Files (*)"
)
if file_path:
self.current_video_path = file_path
self.update_video_info()
self.video_selected.emit(file_path)
def update_video_info(self):
if self.current_video_path:
try:
cap = cv2.VideoCapture(self.current_video_path)
if cap.isOpened():
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = frame_count / fps if fps > 0 else 0
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
info_text = f"时长: {duration:.1f}s\n帧率: {fps:.1f}\n分辨率: {width}×{height}"
self.video_info.setText(info_text)
cap.release()
else:
self.video_info.setText("无法读取视频信息")
except Exception as e:
self.video_info.setText(f"错误: {str(e)}")
else:
self.video_info.setText("未选择视频")
def on_resolution_changed(self, text):
resolution_map = {
"高清 (1280x800)": "high",
"中等 (960x720)": "medium",
"标准 (640x480)": "low"
}
mode = resolution_map.get(text, "medium")
self.motion_app.display_settings['resolution_mode'] = mode
def initialize_system_clicked(self):
self.init_button.setEnabled(False)
self.init_button.setText("正在初始化...")
# Enable this after successful initialization
QTimer.singleShot(100, self.do_initialize)
def do_initialize(self):
success = self.motion_app.initialize_all()
if success:
self.init_button.setText("✅ 初始化完成")
self.preview_button.setEnabled(True)
if self.current_video_path:
self.start_button.setEnabled(True)
self.update_system_status()
else:
self.init_button.setText("❌ 初始化失败")
self.init_button.setEnabled(True)
def preview_camera(self):
# Emit signal to show camera preview
pass
def start_comparison_clicked(self):
if self.current_video_path:
self.start_comparison.emit(self.current_video_path)
def update_system_status(self):
import torch
from config import REALSENSE_AVAILABLE, PYGAME_AVAILABLE
status_text = f"""计算设备: {'GPU (CUDA)' if torch.cuda.is_available() else 'CPU'}
摄像头: {'RealSense' if REALSENSE_AVAILABLE else 'USB 摄像头'}
音频: {'启用' if PYGAME_AVAILABLE else '禁用'}
检测器: {'已初始化' if self.motion_app.body_detector else '未初始化'}"""
self.status_info.setText(status_text)

27
main_qt.py Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""
Qt版本的动作比较与姿态分析系统
"""
import sys
import os
import torch
# Add current directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Set environment variables for performance
os.environ['OMP_NUM_THREADS'] = '1'
os.environ['MKL_NUM_THREADS'] = '1'
try:
torch.set_num_threads(1)
except:
pass
# Set Qt path workaround for torch
torch.classes.__path__ = [os.path.join(torch.__path__[0], 'classes')]
if __name__ == "__main__":
from qt_main import main
main()

145
motion_app_qt.py Normal file
View File

@ -0,0 +1,145 @@
import cv2
import time
import os
import numpy as np
import torch
import threading
import queue
from rtmlib import Body, draw_skeleton
from audio_player import AudioPlayer
from pose_analyzer import PoseSimilarityAnalyzer
from config import REALSENSE_AVAILABLE
if REALSENSE_AVAILABLE:
import pyrealsense2 as rs
class MotionComparisonAppQt:
"""Qt version of the motion comparison application."""
def __init__(self):
self.body_detector = None
self.is_running = False
self.standard_video_path = None
self.webcam_cap = None
self.standard_cap = None
self.similarity_analyzer = PoseSimilarityAnalyzer()
self.frame_counter = 0
self.audio_player = AudioPlayer()
self.display_settings = {'resolution_mode': 'medium', 'target_width': 960, 'target_height': 720}
self.realsense_pipeline = None
self.is_realsense_active = False
self.last_error_time = 0
self.error_count = 0
# Async processing components
self.pose_data_queue = queue.Queue(maxsize=50)
self.similarity_thread = None
self.similarity_stop_flag = threading.Event()
def get_display_resolution(self):
modes = {'high': (1280, 800), 'medium': (960, 720), 'low': (640, 480)}
mode = self.display_settings.get('resolution_mode', 'medium')
return modes.get(mode, (960, 720))
def initialize_detector(self):
if self.body_detector is None:
try:
device = 'cuda' if torch.cuda.is_available() else 'cpu'
self.body_detector = Body(mode='lightweight', to_openpose=True, backend='onnxruntime', device=device)
print(f"Keypoint detector initialized on device: {device}")
return True
except Exception as e:
print(f"Detector initialization failed: {e}")
return False
return True
def initialize_camera(self):
if REALSENSE_AVAILABLE:
try:
self.realsense_pipeline = rs.pipeline()
config = rs.config()
width, height = self.get_display_resolution()
config.enable_stream(rs.stream.color, width, height, rs.format.bgr8, 30)
profile = self.realsense_pipeline.start(config)
device = profile.get_device().get_info(rs.camera_info.name)
print(f"✅ RealSense摄像头初始化成功: {device} ({width}x{height})")
self.is_realsense_active = True
return True
except Exception as e:
print(f"RealSense初始化失败: {e}. 切换到USB摄像头.")
return self._initialize_webcam()
else:
return self._initialize_webcam()
def _initialize_webcam(self):
try:
self.webcam_cap = cv2.VideoCapture(0)
if self.webcam_cap.isOpened():
self.webcam_cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
width, height = self.get_display_resolution()
self.webcam_cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.webcam_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
self.webcam_cap.set(cv2.CAP_PROP_FPS, 30)
actual_w = int(self.webcam_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_h = int(self.webcam_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"✅ USB摄像头初始化成功 ({actual_w}x{actual_h})")
return True
else:
print("❌ 无法打开USB摄像头")
return False
except Exception as e:
print(f"❌ USB摄像头初始化失败: {e}")
return False
def initialize_all(self):
"""Initialize both detector and camera."""
detector_success = self.initialize_detector()
camera_success = self.initialize_camera()
return detector_success and camera_success
def read_camera_frame(self):
if self.is_realsense_active and self.realsense_pipeline:
try:
frames = self.realsense_pipeline.wait_for_frames(timeout_ms=1000)
color_frame = frames.get_color_frame()
if not color_frame:
return False, None
return True, np.asanyarray(color_frame.get_data())
except Exception:
return False, None
elif self.webcam_cap and self.webcam_cap.isOpened():
return self.webcam_cap.read()
return False, None
def get_camera_preview_frame(self):
ret, frame = self.read_camera_frame()
if not ret or frame is None:
return None
frame = cv2.flip(frame, 1)
if self.body_detector:
try:
keypoints, scores = self.body_detector(frame)
frame = draw_skeleton(frame.copy(), keypoints, scores, openpose_skeleton=True, kpt_thr=0.43)
except Exception:
pass
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
def cleanup(self):
"""Cleans up all resources."""
# Stop similarity calculation thread
if self.similarity_thread and self.similarity_thread.is_alive():
self.similarity_stop_flag.set()
self.similarity_thread.join(timeout=2)
if self.standard_cap:
self.standard_cap.release()
if self.webcam_cap:
self.webcam_cap.release()
if self.is_realsense_active and self.realsense_pipeline:
self.realsense_pipeline.stop()
self.audio_player.cleanup()
self.is_running = False

49
qt_main.py Normal file
View File

@ -0,0 +1,49 @@
import sys
import os
from PyQt5.QtWidgets import QApplication, QMainWindow, QHBoxLayout, QVBoxLayout, QWidget
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon
from qt_widgets import MainWidget
from motion_app_qt import MotionComparisonAppQt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("🏃 动作比较与姿态分析系统")
self.setGeometry(100, 100, 1400, 900)
# Set application icon if available
if os.path.exists("icon.ico"):
self.setWindowIcon(QIcon("icon.ico"))
# Create main widget
self.main_widget = MainWidget()
self.setCentralWidget(self.main_widget)
# Apply dark theme styling
self.setStyleSheet("""
QMainWindow {
background-color: #f0f2f6;
}
QWidget {
font-family: "Segoe UI", Arial, sans-serif;
font-size: 10pt;
}
""")
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion') # Modern look
# Set application properties
app.setApplicationName("Motion Comparison App")
app.setApplicationVersion("1.0")
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

73
qt_widgets.py Normal file
View File

@ -0,0 +1,73 @@
import os
import tempfile
import cv2
import numpy as np
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QComboBox, QRadioButton, QButtonGroup,
QFileDialog, QTextEdit, QProgressBar, QFrame,
QScrollArea, QGroupBox, QGridLayout, QSlider,
QSpinBox, QCheckBox, QTabWidget, QSplitter)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QMutex, QMutexLocker
from PyQt5.QtGui import QPixmap, QImage, QPalette, QFont, QMovie
from motion_app_qt import MotionComparisonAppQt
from video_display_widget import VideoDisplayWidget
from similarity_display_widget import SimilarityDisplayWidget
from control_panel_widget import ControlPanelWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
self.motion_app = MotionComparisonAppQt()
self.setup_ui()
def setup_ui(self):
# Main layout - horizontal splitter
main_splitter = QSplitter(Qt.Horizontal)
# Control panel (sidebar) - fixed width
self.control_panel = ControlPanelWidget(self.motion_app)
self.control_panel.setMinimumWidth(300)
self.control_panel.setMaximumWidth(350)
# Main content area
content_widget = QWidget()
content_layout = QVBoxLayout(content_widget)
# Video display area - priority for real-time performance
self.video_display = VideoDisplayWidget(self.motion_app)
content_layout.addWidget(self.video_display, stretch=2)
# Similarity analysis area - can have latency
self.similarity_display = SimilarityDisplayWidget(self.motion_app)
content_layout.addWidget(self.similarity_display, stretch=1)
# Add widgets to splitter
main_splitter.addWidget(self.control_panel)
main_splitter.addWidget(content_widget)
main_splitter.setStretchFactor(0, 0) # Control panel fixed
main_splitter.setStretchFactor(1, 1) # Content area expandable
# Main layout
main_layout = QHBoxLayout(self)
main_layout.addWidget(main_splitter)
main_layout.setContentsMargins(0, 0, 0, 0)
# Connect signals
self.setup_connections()
def setup_connections(self):
# Connect control panel signals to main app
self.control_panel.video_selected.connect(self.video_display.load_video)
self.control_panel.start_comparison.connect(self.start_comparison)
self.control_panel.stop_comparison.connect(self.stop_comparison)
self.control_panel.initialize_system.connect(self.motion_app.initialize_all)
def start_comparison(self, video_path):
self.video_display.start_comparison(video_path)
self.similarity_display.start_analysis()
def stop_comparison(self):
self.video_display.stop_comparison()
self.similarity_display.stop_analysis()
self.motion_app.cleanup()

View File

@ -0,0 +1,259 @@
import numpy as np
import threading
import queue
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QGridLayout)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QPalette
# Placeholder for matplotlib widget since we'll need it for plotting
try:
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
class SimilarityThread(QThread):
similarity_update = pyqtSignal(float, float) # current_similarity, average_similarity
plot_update = pyqtSignal()
def __init__(self, motion_app):
super().__init__()
self.motion_app = motion_app
self.is_running = False
self.should_stop = False
def start_analysis(self):
self.is_running = True
self.should_stop = False
self.start()
def stop_analysis(self):
self.should_stop = True
self.is_running = False
def run(self):
while self.is_running and not self.should_stop:
try:
# Get pose data from queue with timeout
pose_data = self.motion_app.pose_data_queue.get(timeout=1.0)
if pose_data is None: # Poison pill
break
elapsed_time, standard_angles, webcam_angles = pose_data
if standard_angles and webcam_angles:
current_similarity = self.motion_app.similarity_analyzer.calculate_similarity(
standard_angles, webcam_angles)
self.motion_app.similarity_analyzer.add_similarity_score(
current_similarity, elapsed_time)
# Calculate average
history = self.motion_app.similarity_analyzer.similarity_history
if history:
avg_similarity = sum(history) / len(history)
self.similarity_update.emit(current_similarity, avg_similarity)
# Emit plot update signal (less frequently)
if len(history) % 10 == 0:
self.plot_update.emit()
self.motion_app.pose_data_queue.task_done()
except queue.Empty:
continue
except Exception as e:
continue
class SimilarityPlotWidget(QWidget):
def __init__(self):
super().__init__()
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
if MATPLOTLIB_AVAILABLE:
# Create matplotlib figure
self.figure = Figure(figsize=(8, 4))
self.canvas = FigureCanvas(self.figure)
layout.addWidget(self.canvas)
self.ax = self.figure.add_subplot(111)
self.ax.set_title('Similarity Trend')
self.ax.set_xlabel('Time (s)')
self.ax.set_ylabel('Score (%)')
self.ax.set_ylim(0, 100)
self.ax.grid(True, alpha=0.3)
# Initialize empty line
self.line, = self.ax.plot([], [], 'b-', linewidth=2, label='Similarity')
self.avg_line = self.ax.axhline(y=0, color='r', linestyle='--', alpha=0.7, label='Average')
self.ax.legend()
self.figure.tight_layout()
self.canvas.draw()
else:
# Fallback to simple text display
self.plot_label = QLabel("matplotlib不可用无法显示图表")
self.plot_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.plot_label)
def update_plot(self, timestamps, similarities):
if not MATPLOTLIB_AVAILABLE or not timestamps or not similarities:
return
# Update line data
self.line.set_data(timestamps, similarities)
# Update average line
if similarities:
avg = sum(similarities) / len(similarities)
self.avg_line.set_ydata([avg, avg])
# Update axis limits
if timestamps:
self.ax.set_xlim(0, max(timestamps) + 1)
self.canvas.draw()
class SimilarityDisplayWidget(QWidget):
def __init__(self, motion_app):
super().__init__()
self.motion_app = motion_app
self.similarity_thread = SimilarityThread(motion_app)
self.setup_ui()
self.connect_signals()
def setup_ui(self):
layout = QVBoxLayout(self)
# Title
title_label = QLabel("📊 动作相似度分析")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 16pt; font-weight: bold; margin: 10px;")
layout.addWidget(title_label)
# Metrics display
metrics_layout = QGridLayout()
# Current similarity
self.current_sim_label = QLabel("当前相似度")
self.current_sim_value = QLabel("0.0%")
self.current_sim_value.setAlignment(Qt.AlignCenter)
self.current_sim_value.setStyleSheet("font-size: 24pt; font-weight: bold; color: #0086d3;")
# Average similarity
self.avg_sim_label = QLabel("平均相似度")
self.avg_sim_value = QLabel("0.0%")
self.avg_sim_value.setAlignment(Qt.AlignCenter)
self.avg_sim_value.setStyleSheet("font-size: 24pt; font-weight: bold; color: #2e7d32;")
metrics_layout.addWidget(self.current_sim_label, 0, 0)
metrics_layout.addWidget(self.current_sim_value, 1, 0)
metrics_layout.addWidget(self.avg_sim_label, 0, 1)
metrics_layout.addWidget(self.avg_sim_value, 1, 1)
layout.addLayout(metrics_layout)
# Plot widget
self.plot_widget = SimilarityPlotWidget()
layout.addWidget(self.plot_widget)
# Final statistics (hidden initially)
self.stats_widget = self.create_statistics_widget()
self.stats_widget.hide()
layout.addWidget(self.stats_widget)
def create_statistics_widget(self):
stats_widget = QFrame()
stats_widget.setFrameStyle(QFrame.StyledPanel)
stats_widget.setStyleSheet("background-color: #f0f8ff; border: 1px solid #ccc; border-radius: 5px;")
layout = QVBoxLayout(stats_widget)
self.final_title = QLabel("🎉 比较完成!")
self.final_title.setAlignment(Qt.AlignCenter)
self.final_title.setStyleSheet("font-size: 18pt; font-weight: bold; color: #2e7d32; margin: 10px;")
self.performance_label = QLabel()
self.performance_label.setAlignment(Qt.AlignCenter)
self.performance_label.setStyleSheet("font-size: 14pt; font-weight: bold; margin: 5px;")
# Final metrics
final_metrics_layout = QGridLayout()
self.final_avg_label = QLabel("平均相似度")
self.final_avg_value = QLabel("0.0%")
self.final_max_label = QLabel("最高相似度")
self.final_max_value = QLabel("0.0%")
self.final_min_label = QLabel("最低相似度")
self.final_min_value = QLabel("0.0%")
final_metrics_layout.addWidget(self.final_avg_label, 0, 0)
final_metrics_layout.addWidget(self.final_avg_value, 1, 0)
final_metrics_layout.addWidget(self.final_max_label, 0, 1)
final_metrics_layout.addWidget(self.final_max_value, 1, 1)
final_metrics_layout.addWidget(self.final_min_label, 0, 2)
final_metrics_layout.addWidget(self.final_min_value, 1, 2)
layout.addWidget(self.final_title)
layout.addWidget(self.performance_label)
layout.addLayout(final_metrics_layout)
return stats_widget
def connect_signals(self):
self.similarity_thread.similarity_update.connect(self.update_similarity_display)
self.similarity_thread.plot_update.connect(self.update_plot)
def start_analysis(self):
self.stats_widget.hide()
self.motion_app.similarity_analyzer.reset()
self.similarity_thread.start_analysis()
def stop_analysis(self):
self.similarity_thread.stop_analysis()
self.show_final_statistics()
def update_similarity_display(self, current_similarity, average_similarity):
self.current_sim_value.setText(f"{current_similarity:.1f}%")
self.avg_sim_value.setText(f"{average_similarity:.1f}%")
def update_plot(self):
analyzer = self.motion_app.similarity_analyzer
if hasattr(analyzer, 'similarity_history') and hasattr(analyzer, 'frame_timestamps'):
timestamps = list(analyzer.frame_timestamps)
similarities = list(analyzer.similarity_history)
self.plot_widget.update_plot(timestamps, similarities)
def show_final_statistics(self):
history = self.motion_app.similarity_analyzer.similarity_history
if not history:
return
final_avg = sum(history) / len(history)
final_max = max(history)
final_min = min(history)
# Set performance level and color
if final_avg >= 80:
level = "非常棒! 👏"
color = "green"
elif final_avg >= 60:
level = "整体不错! 👍"
color = "blue"
else:
level = "需要改进! 💪"
color = "orange"
self.performance_label.setText(f"整体表现: {level}")
self.performance_label.setStyleSheet(f"font-size: 14pt; font-weight: bold; margin: 5px; color: {color};")
self.final_avg_value.setText(f"{final_avg:.1f}%")
self.final_max_value.setText(f"{final_max:.1f}%")
self.final_min_value.setText(f"{final_min:.1f}%")
self.stats_widget.show()

274
video_display_widget.py Normal file
View File

@ -0,0 +1,274 @@
import cv2
import numpy as np
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QProgressBar
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QMutex, QMutexLocker
from PyQt5.QtGui import QPixmap, QImage, QPainter, QPen
class VideoThread(QThread):
frame_ready = pyqtSignal(np.ndarray, np.ndarray) # standard_frame, webcam_frame
progress_update = pyqtSignal(float, str) # progress, status_text
comparison_finished = pyqtSignal()
def __init__(self, motion_app):
super().__init__()
self.motion_app = motion_app
self.video_path = None
self.is_running = False
self.should_stop = False
self.should_restart = False
def load_video(self, video_path):
self.video_path = video_path
def start_comparison(self, video_path):
self.video_path = video_path
self.is_running = True
self.should_stop = False
self.should_restart = False
self.start()
def stop_comparison(self):
self.should_stop = True
self.is_running = False
def restart_comparison(self):
self.should_restart = True
def run(self):
if not self.video_path or not self.motion_app.body_detector:
return
# Initialize video capture
standard_cap = cv2.VideoCapture(self.video_path)
if not standard_cap.isOpened():
return
# Initialize camera
if not self.motion_app.initialize_camera():
standard_cap.release()
return
# Get video properties
total_frames = int(standard_cap.get(cv2.CAP_PROP_FRAME_COUNT))
video_fps = standard_cap.get(cv2.CAP_PROP_FPS)
if video_fps == 0:
video_fps = 30
video_duration = total_frames / video_fps
target_width, target_height = self.motion_app.get_display_resolution()
# Start audio if available
audio_loaded = self.motion_app.audio_player.load_audio(self.video_path)
if audio_loaded:
self.motion_app.audio_player.play()
import time
start_time = time.time()
frame_counter = 0
while self.is_running and not self.should_stop:
if self.should_restart:
standard_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
start_time = time.time()
if audio_loaded:
self.motion_app.audio_player.restart()
self.should_restart = False
continue
elapsed_time = time.time() - start_time
if elapsed_time >= video_duration:
break
# Get standard video frame based on elapsed time
target_frame_idx = int(elapsed_time * video_fps)
standard_cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame_idx)
ret_standard, standard_frame = standard_cap.read()
if not ret_standard:
continue
# Get webcam frame
ret_webcam, webcam_frame = self.motion_app.read_camera_frame()
if not ret_webcam or webcam_frame is None:
continue
# Process frames
frame_counter += 1
webcam_frame = cv2.flip(webcam_frame, 1)
standard_frame = cv2.resize(standard_frame, (target_width, target_height))
webcam_frame = cv2.resize(webcam_frame, (target_width, target_height))
# Pose detection
try:
standard_keypoints, standard_scores = self.motion_app.body_detector(standard_frame)
webcam_keypoints, webcam_scores = self.motion_app.body_detector(webcam_frame)
# Draw skeletons
from rtmlib import draw_skeleton
standard_with_keypoints = draw_skeleton(
standard_frame.copy(), standard_keypoints, standard_scores,
openpose_skeleton=True, kpt_thr=0.43
)
webcam_with_keypoints = draw_skeleton(
webcam_frame.copy(), webcam_keypoints, webcam_scores,
openpose_skeleton=True, kpt_thr=0.43
)
# Emit frames for display
self.frame_ready.emit(standard_with_keypoints, webcam_with_keypoints)
# Send pose data to similarity analyzer (in separate thread)
if standard_keypoints is not None and webcam_keypoints is not None:
standard_angles = self.motion_app.similarity_analyzer.extract_joint_angles(
standard_keypoints, standard_scores)
webcam_angles = self.motion_app.similarity_analyzer.extract_joint_angles(
webcam_keypoints, webcam_scores)
if standard_angles and webcam_angles:
try:
self.motion_app.pose_data_queue.put_nowait(
(elapsed_time, standard_angles, webcam_angles))
except:
pass
except Exception as e:
continue
# Update progress (less frequently)
if frame_counter % 10 == 0:
progress = min(elapsed_time / video_duration, 1.0)
processing_fps = frame_counter / elapsed_time if elapsed_time > 0 else 0
status_text = f"时间: {elapsed_time:.1f}s / {video_duration:.1f}s | 处理帧率: {processing_fps:.1f} FPS"
self.progress_update.emit(progress, status_text)
# Cleanup
if audio_loaded:
self.motion_app.audio_player.stop()
standard_cap.release()
self.comparison_finished.emit()
self.is_running = False
class VideoDisplayWidget(QWidget):
def __init__(self, motion_app):
super().__init__()
self.motion_app = motion_app
self.video_thread = VideoThread(motion_app)
self.setup_ui()
self.connect_signals()
def setup_ui(self):
layout = QVBoxLayout(self)
# Title
title_label = QLabel("📺 视频比较")
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("font-size: 16pt; font-weight: bold; margin: 10px;")
layout.addWidget(title_label)
# Video displays
video_layout = QHBoxLayout()
# Standard video
standard_layout = QVBoxLayout()
standard_title = QLabel("🎯 标准动作视频")
standard_title.setAlignment(Qt.AlignCenter)
standard_title.setStyleSheet("font-weight: bold; margin: 5px;")
self.standard_label = QLabel()
self.standard_label.setAlignment(Qt.AlignCenter)
self.standard_label.setMinimumSize(480, 360)
self.standard_label.setStyleSheet("border: 1px solid #ccc; background-color: #f0f0f0;")
self.standard_label.setText("等待视频...")
standard_layout.addWidget(standard_title)
standard_layout.addWidget(self.standard_label)
# Webcam video
webcam_layout = QVBoxLayout()
webcam_title = QLabel("📹 实时影像")
webcam_title.setAlignment(Qt.AlignCenter)
webcam_title.setStyleSheet("font-weight: bold; margin: 5px;")
self.webcam_label = QLabel()
self.webcam_label.setAlignment(Qt.AlignCenter)
self.webcam_label.setMinimumSize(480, 360)
self.webcam_label.setStyleSheet("border: 1px solid #ccc; background-color: #f0f0f0;")
self.webcam_label.setText("等待摄像头...")
webcam_layout.addWidget(webcam_title)
webcam_layout.addWidget(self.webcam_label)
video_layout.addLayout(standard_layout)
video_layout.addLayout(webcam_layout)
layout.addLayout(video_layout)
# Control buttons
button_layout = QHBoxLayout()
self.stop_button = QPushButton("⏹️ 停止")
self.restart_button = QPushButton("🔄 重新开始")
self.stop_button.clicked.connect(self.stop_comparison)
self.restart_button.clicked.connect(self.restart_comparison)
self.stop_button.setEnabled(False)
self.restart_button.setEnabled(False)
button_layout.addWidget(self.stop_button)
button_layout.addWidget(self.restart_button)
layout.addLayout(button_layout)
# Progress bar
self.progress_bar = QProgressBar()
self.status_label = QLabel("准备就绪")
layout.addWidget(self.progress_bar)
layout.addWidget(self.status_label)
def connect_signals(self):
self.video_thread.frame_ready.connect(self.update_frames)
self.video_thread.progress_update.connect(self.update_progress)
self.video_thread.comparison_finished.connect(self.on_comparison_finished)
def load_video(self, video_path):
self.video_thread.load_video(video_path)
def start_comparison(self, video_path):
self.stop_button.setEnabled(True)
self.restart_button.setEnabled(True)
self.video_thread.start_comparison(video_path)
def stop_comparison(self):
self.video_thread.stop_comparison()
self.stop_button.setEnabled(False)
self.restart_button.setEnabled(False)
def restart_comparison(self):
self.video_thread.restart_comparison()
def update_frames(self, standard_frame, webcam_frame):
# Convert frames to Qt format and display
standard_pixmap = self.numpy_to_pixmap(standard_frame)
webcam_pixmap = self.numpy_to_pixmap(webcam_frame)
self.standard_label.setPixmap(standard_pixmap.scaled(
self.standard_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.webcam_label.setPixmap(webcam_pixmap.scaled(
self.webcam_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
def update_progress(self, progress, status_text):
self.progress_bar.setValue(int(progress * 100))
self.status_label.setText(status_text)
def on_comparison_finished(self):
self.stop_button.setEnabled(False)
self.restart_button.setEnabled(False)
self.status_label.setText("比较完成")
def numpy_to_pixmap(self, frame):
"""Convert numpy array (BGR) to QPixmap"""
# Convert BGR to RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_frame.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
return QPixmap.fromImage(qt_image)