Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
487f8d1d55 |
403
control_panel_widget.py
Normal file
403
control_panel_widget.py
Normal 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
27
main_qt.py
Normal 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()
|
@ -12,85 +12,6 @@ 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
|
||||
|
||||
@ -198,7 +119,7 @@ class MotionComparisonApp:
|
||||
if self.body_detector:
|
||||
try:
|
||||
keypoints, scores = self.body_detector(frame)
|
||||
frame = draw_skeleton_with_similarity(frame.copy(), keypoints, scores, joint_similarities=None, openpose_skeleton=True, kpt_thr=0.43, line_width=1)
|
||||
frame = draw_skeleton(frame.copy(), keypoints, scores, openpose_skeleton=True, kpt_thr=0.43)
|
||||
except Exception: pass
|
||||
|
||||
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
@ -420,17 +341,9 @@ class MotionComparisonApp:
|
||||
# --- 绘制与显示 (可以保持原来的逻辑,只修改UI更新部分) ---
|
||||
try:
|
||||
|
||||
# 计算关节相似度
|
||||
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_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)
|
||||
|
||||
# 更新视频画面 (主线程专注于实时显示)
|
||||
standard_placeholder.image(cv2.cvtColor(standard_with_keypoints, cv2.COLOR_BGR2RGB), use_container_width=True)
|
||||
|
145
motion_app_qt.py
Normal file
145
motion_app_qt.py
Normal 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
|
@ -93,19 +93,6 @@ 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."""
|
||||
|
49
qt_main.py
Normal file
49
qt_main.py
Normal 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
73
qt_widgets.py
Normal 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()
|
259
similarity_display_widget.py
Normal file
259
similarity_display_widget.py
Normal 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
274
video_display_widget.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user