diff --git a/control_panel_widget.py b/control_panel_widget.py new file mode 100644 index 0000000..73b5e51 --- /dev/null +++ b/control_panel_widget.py @@ -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) + + + diff --git a/main_qt.py b/main_qt.py new file mode 100644 index 0000000..3a4b60f --- /dev/null +++ b/main_qt.py @@ -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() diff --git a/motion_app_qt.py b/motion_app_qt.py new file mode 100644 index 0000000..6020ebe --- /dev/null +++ b/motion_app_qt.py @@ -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 diff --git a/qt_main.py b/qt_main.py new file mode 100644 index 0000000..1afc7db --- /dev/null +++ b/qt_main.py @@ -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() diff --git a/qt_widgets.py b/qt_widgets.py new file mode 100644 index 0000000..af64fcb --- /dev/null +++ b/qt_widgets.py @@ -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() diff --git a/similarity_display_widget.py b/similarity_display_widget.py new file mode 100644 index 0000000..66dab24 --- /dev/null +++ b/similarity_display_widget.py @@ -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() diff --git a/video_display_widget.py b/video_display_widget.py new file mode 100644 index 0000000..8b7ad7c --- /dev/null +++ b/video_display_widget.py @@ -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)