Compare commits

..

2 Commits
qt ... main

Author SHA1 Message Date
gameloader
12d9c59a53 feat(ui): support multi-person skeleton drawing 2025-06-30 19:44:34 +08:00
gameloader
389a7fd16b feat(pose): add joint similarity visualization in skeleton drawing 2025-06-30 18:53:51 +08:00
9 changed files with 104 additions and 1234 deletions

View File

@ -1,403 +0,0 @@
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)

View File

@ -1,27 +0,0 @@
#!/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()

View File

@ -12,6 +12,85 @@ from audio_player import AudioPlayer
from pose_analyzer import PoseSimilarityAnalyzer
from config import REALSENSE_AVAILABLE
def draw_skeleton_with_similarity(img, keypoints, scores, joint_similarities=None, openpose_skeleton=True, kpt_thr=0.43, line_width=2):
"""
自定义骨骼绘制函数根据关节相似度设置颜色
相似度 > 90: 绿色
相似度 80-90: 黄色
相似度 < 80: 红色
支持多人检测将绘制所有检测到的人体骨骼
"""
if keypoints is None or len(keypoints) == 0:
return img
# OpenPose连接关系
skeleton = [
[1, 0], [1, 2], [1, 5], [2, 3], [3, 4], [5, 6], [6, 7],
[1, 8], [8, 9], [9, 10], [1, 11], [11, 12], [12, 13],
[0, 14], [0, 15], [14, 16], [15, 17]
]
# 关节与关节角度的映射
joint_to_angle_mapping = {
(2, 3): 'left_shoulder', (3, 4): 'left_elbow',
(5, 6): 'right_shoulder', (6, 7): 'right_elbow',
(8, 9): 'left_hip', (9, 10): 'left_knee',
(11, 12): 'right_hip', (12, 13): 'right_knee'
}
# 骨骼的默认颜色
default_color = (0, 255, 255) # 黄色
# 确保keypoints和scores的形状正确处理多人情况
if len(keypoints.shape) == 2:
keypoints = keypoints[None, :, :]
scores = scores[None, :, :]
# 遍历所有人
num_instances = keypoints.shape[0]
for person_idx in range(num_instances):
person_kpts = keypoints[person_idx]
person_scores = scores[person_idx]
# 绘制骨骼
for limb_id, limb in enumerate(skeleton):
joint_a, joint_b = limb
if joint_a >= len(person_scores) or joint_b >= len(person_scores):
continue
if person_scores[joint_a] < kpt_thr or person_scores[joint_b] < kpt_thr:
continue
x_a, y_a = person_kpts[joint_a]
x_b, y_b = person_kpts[joint_b]
# 确定线条颜色
color = default_color
if joint_similarities is not None:
# 检查这个连接是否有对应的关节角度
if (joint_a, joint_b) in joint_to_angle_mapping:
angle_name = joint_to_angle_mapping[(joint_a, joint_b)]
if angle_name in joint_similarities:
similarity = joint_similarities[angle_name]
if similarity > 90:
color = (0, 255, 0) # 绿色
elif similarity > 80:
color = (0, 255, 255) # 黄色
else:
color = (0, 0, 255) # 红色
cv2.line(img, (int(x_a), int(y_a)), (int(x_b), int(y_b)), color, thickness=line_width)
# 绘制关键点
for kpt_id, (x, y) in enumerate(person_kpts):
if person_scores[kpt_id] < kpt_thr:
continue
cv2.circle(img, (int(x), int(y)), 3, (255, 0, 255), -1)
return img
if REALSENSE_AVAILABLE:
import pyrealsense2 as rs
@ -119,7 +198,7 @@ class MotionComparisonApp:
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)
frame = draw_skeleton_with_similarity(frame.copy(), keypoints, scores, joint_similarities=None, openpose_skeleton=True, kpt_thr=0.43, line_width=1)
except Exception: pass
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
@ -341,9 +420,17 @@ class MotionComparisonApp:
# --- 绘制与显示 (可以保持原来的逻辑只修改UI更新部分) ---
try:
# 绘制骨骼
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)
# 计算关节相似度
joint_similarities = None
if standard_keypoints is not None and webcam_keypoints is not None:
standard_angles = self.similarity_analyzer.extract_joint_angles(standard_keypoints, standard_scores)
webcam_angles = self.similarity_analyzer.extract_joint_angles(webcam_keypoints, webcam_scores)
if standard_angles and webcam_angles:
joint_similarities = self.similarity_analyzer.calculate_joint_similarities(standard_angles, webcam_angles)
# 绘制骨骼 (线条更窄,根据相似度设置颜色)
standard_with_keypoints = draw_skeleton_with_similarity(standard_frame.copy(), standard_keypoints, standard_scores, joint_similarities=None, openpose_skeleton=True, kpt_thr=0.43, line_width=1)
webcam_with_keypoints = draw_skeleton_with_similarity(webcam_frame.copy(), webcam_keypoints, webcam_scores, joint_similarities=joint_similarities, openpose_skeleton=True, kpt_thr=0.43, line_width=1)
# 更新视频画面 (主线程专注于实时显示)
standard_placeholder.image(cv2.cvtColor(standard_with_keypoints, cv2.COLOR_BGR2RGB), use_container_width=True)

View File

@ -1,145 +0,0 @@
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

View File

@ -93,6 +93,19 @@ class PoseSimilarityAnalyzer:
final_similarity = (weighted_similarity / total_weight) * 100 if total_weight > 0 else 0
return min(max(final_similarity, 0), 100)
def calculate_joint_similarities(self, angles1, angles2):
"""计算每个关节的相似度"""
joint_similarities = {}
if not angles1 or not angles2: return joint_similarities
common_joints = set(angles1.keys()) & set(angles2.keys())
for joint in common_joints:
angle_diff = abs(angles1[joint] - angles2[joint])
similarity = math.exp(-(angle_diff ** 2) / (2 * (30 ** 2)))
joint_similarities[joint] = min(max(similarity * 100, 0), 100)
return joint_similarities
def add_similarity_score(self, score, timestamp=None):
"""Adds a similarity score to the history."""

View File

@ -1,49 +0,0 @@
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()

View File

@ -1,73 +0,0 @@
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

@ -1,259 +0,0 @@
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()

View File

@ -1,274 +0,0 @@
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)