MetaWhiz/scripts/ui/calibration.py
2025-04-17 13:00:39 +08:00

524 lines
20 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - Calibration UI
Provides the UI for calibrating skeleton and joint positions
"""
import os
import sys
import maya.cmds as cmds
try:
from PySide2 import QtCore, QtGui, QtWidgets
except ImportError:
try:
from PySide6 import QtCore, QtGui, QtWidgets
except ImportError:
from PySide import QtCore, QtGui, QtWidgets
# Import configuration
import config
class CalibrationWidget(QtWidgets.QWidget):
"""Widget for calibrating skeleton and joint positions"""
def __init__(self, parent=None):
"""Initialize the calibration widget"""
super(CalibrationWidget, self).__init__(parent)
# Initialize UI
self._create_widgets()
self._create_layouts()
self._create_connections()
def _create_widgets(self):
"""Create widgets for the calibration interface"""
# Skeleton Selection section
self.skeleton_group = QtWidgets.QGroupBox("Skeleton Selection")
self.skeleton_label = QtWidgets.QLabel("Skeleton Root:")
self.skeleton_combo = QtWidgets.QComboBox()
self.skeleton_combo.setEditable(True)
self.skeleton_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
self.refresh_skeleton_button = QtWidgets.QPushButton()
self.refresh_skeleton_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png")))
self.refresh_skeleton_button.setToolTip("Refresh skeletons in the scene")
self.refresh_skeleton_button.setFixedSize(24, 24)
self.select_skeleton_button = QtWidgets.QPushButton("Select Skeleton")
self.select_skeleton_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "select.png")))
# Reference Skeleton section
self.reference_group = QtWidgets.QGroupBox("Reference Skeleton")
self.reference_label = QtWidgets.QLabel("Reference Skeleton:")
self.reference_combo = QtWidgets.QComboBox()
self.reference_combo.addItems(["MetaHuman Standard", "Custom"])
self.load_reference_button = QtWidgets.QPushButton("Load Reference")
self.load_reference_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "load.png")))
# Joint List section
self.joints_group = QtWidgets.QGroupBox("Joint List")
self.joints_tree = QtWidgets.QTreeWidget()
self.joints_tree.setHeaderLabels(["Joint Name", "Position", "Rotation", "Status"])
self.joints_tree.setColumnWidth(0, 150)
self.joints_tree.setColumnWidth(1, 150)
self.joints_tree.setColumnWidth(2, 150)
self.joints_tree.setAlternatingRowColors(True)
# Calibration Controls section
self.controls_group = QtWidgets.QGroupBox("Calibration Controls")
self.auto_calibrate_button = QtWidgets.QPushButton("Auto Calibrate")
self.auto_calibrate_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "auto.png")))
self.manual_calibrate_button = QtWidgets.QPushButton("Manual Calibrate")
self.manual_calibrate_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "manual.png")))
self.reset_calibration_button = QtWidgets.QPushButton("Reset Calibration")
self.reset_calibration_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "reset.png")))
self.apply_calibration_button = QtWidgets.QPushButton("Apply Calibration")
self.apply_calibration_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "apply.png")))
# Visualization section
self.visual_group = QtWidgets.QGroupBox("Visualization")
self.show_reference_check = QtWidgets.QCheckBox("Show Reference Skeleton")
self.show_reference_check.setChecked(True)
self.show_current_check = QtWidgets.QCheckBox("Show Current Skeleton")
self.show_current_check.setChecked(True)
self.show_differences_check = QtWidgets.QCheckBox("Show Differences")
self.show_differences_check.setChecked(True)
def _create_layouts(self):
"""Create layouts for the calibration interface"""
# Skeleton Selection layout
skeleton_layout = QtWidgets.QGridLayout()
skeleton_layout.addWidget(self.skeleton_label, 0, 0)
skeleton_layout.addWidget(self.skeleton_combo, 0, 1)
skeleton_layout.addWidget(self.refresh_skeleton_button, 0, 2)
skeleton_layout.addWidget(self.select_skeleton_button, 1, 1)
self.skeleton_group.setLayout(skeleton_layout)
# Reference Skeleton layout
reference_layout = QtWidgets.QGridLayout()
reference_layout.addWidget(self.reference_label, 0, 0)
reference_layout.addWidget(self.reference_combo, 0, 1)
reference_layout.addWidget(self.load_reference_button, 1, 1)
self.reference_group.setLayout(reference_layout)
# Joint List layout
joints_layout = QtWidgets.QVBoxLayout()
joints_layout.addWidget(self.joints_tree)
self.joints_group.setLayout(joints_layout)
# Calibration Controls layout
controls_layout = QtWidgets.QHBoxLayout()
controls_layout.addWidget(self.auto_calibrate_button)
controls_layout.addWidget(self.manual_calibrate_button)
controls_layout.addWidget(self.reset_calibration_button)
controls_layout.addWidget(self.apply_calibration_button)
self.controls_group.setLayout(controls_layout)
# Visualization layout
visual_layout = QtWidgets.QHBoxLayout()
visual_layout.addWidget(self.show_reference_check)
visual_layout.addWidget(self.show_current_check)
visual_layout.addWidget(self.show_differences_check)
visual_layout.addStretch()
self.visual_group.setLayout(visual_layout)
# Main layout
main_layout = QtWidgets.QVBoxLayout()
main_layout.addWidget(self.skeleton_group)
main_layout.addWidget(self.reference_group)
main_layout.addWidget(self.joints_group)
main_layout.addWidget(self.controls_group)
main_layout.addWidget(self.visual_group)
self.setLayout(main_layout)
def _create_connections(self):
"""Create signal/slot connections for the calibration interface"""
# Connect buttons
self.refresh_skeleton_button.clicked.connect(self._refresh_skeletons)
self.select_skeleton_button.clicked.connect(self._select_skeleton)
self.load_reference_button.clicked.connect(self._load_reference)
self.auto_calibrate_button.clicked.connect(self._auto_calibrate)
self.manual_calibrate_button.clicked.connect(self._manual_calibrate)
self.reset_calibration_button.clicked.connect(self._reset_calibration)
self.apply_calibration_button.clicked.connect(self._apply_calibration)
# Connect combo boxes
self.skeleton_combo.currentIndexChanged.connect(self._on_skeleton_changed)
self.reference_combo.currentIndexChanged.connect(self._on_reference_changed)
# Connect checkboxes
self.show_reference_check.stateChanged.connect(self._update_visualization)
self.show_current_check.stateChanged.connect(self._update_visualization)
self.show_differences_check.stateChanged.connect(self._update_visualization)
# Connect tree widget
self.joints_tree.itemClicked.connect(self._on_joint_selected)
# Initial refresh
self._refresh_skeletons()
def _refresh_skeletons(self):
"""Refresh the list of skeletons in the scene"""
current_text = self.skeleton_combo.currentText()
self.skeleton_combo.clear()
# Get all joint hierarchies in the scene
root_joints = []
all_joints = cmds.ls(type="joint")
for joint in all_joints:
# Check if it's a root joint (no joint parent)
parent = cmds.listRelatives(joint, parent=True)
if not parent or cmds.nodeType(parent[0]) != "joint":
root_joints.append(joint)
# Add root joints to the combo box
root_joints.sort()
self.skeleton_combo.addItems(root_joints)
# Restore previous selection if possible
index = self.skeleton_combo.findText(current_text)
if index >= 0:
self.skeleton_combo.setCurrentIndex(index)
# Update joint tree
self._update_joint_tree()
def _select_skeleton(self):
"""Select the current skeleton in the scene"""
skeleton = self.skeleton_combo.currentText()
if skeleton:
cmds.select(skeleton)
def _load_reference(self):
"""Load reference skeleton"""
reference_type = self.reference_combo.currentText()
if reference_type == "MetaHuman Standard":
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
result = joint_utils.load_metahuman_reference()
if result:
self._update_joint_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
"成功加载 MetaHuman 标准骨骼参考"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"加载 MetaHuman 标准骨骼参考失败"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"加载骨骼参考出错: {str(e)}"
)
elif reference_type == "Custom":
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
self,
"选择参考骨骼文件",
"",
"Maya Files (*.ma *.mb);;JSON Files (*.json);;All Files (*.*)"
)
if file_path:
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
result = joint_utils.load_custom_reference(file_path)
if result:
self._update_joint_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功加载自定义骨骼参考: {os.path.basename(file_path)}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"加载自定义骨骼参考失败: {os.path.basename(file_path)}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"加载骨骼参考出错: {str(e)}"
)
def _auto_calibrate(self):
"""Auto-calibrate the skeleton"""
skeleton = self.skeleton_combo.currentText()
if not skeleton:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先选择一个骨骼"
)
return
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
result = joint_utils.auto_calibrate(skeleton)
if result:
self._update_joint_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功自动校准骨骼: {skeleton}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"自动校准骨骼失败: {skeleton}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"自动校准骨骼出错: {str(e)}"
)
def _manual_calibrate(self):
"""Manual calibrate the skeleton"""
skeleton = self.skeleton_combo.currentText()
if not skeleton:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先选择一个骨骼"
)
return
# Get selected joint from tree
selected_items = self.joints_tree.selectedItems()
if not selected_items:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先在关节列表中选择一个关节"
)
return
joint_name = selected_items[0].text(0)
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
# Select the joint in the scene
cmds.select(f"{skeleton}|{joint_name}")
# Show manual calibration UI
result = joint_utils.show_manual_calibration_ui(f"{skeleton}|{joint_name}")
if result:
self._update_joint_tree()
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"手动校准关节失败: {joint_name}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"手动校准关节出错: {str(e)}"
)
def _reset_calibration(self):
"""Reset calibration to default"""
skeleton = self.skeleton_combo.currentText()
if not skeleton:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先选择一个骨骼"
)
return
# Confirm reset
result = QtWidgets.QMessageBox.question(
self,
"确认",
"确定要重置所有校准数据吗?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if result == QtWidgets.QMessageBox.Yes:
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
result = joint_utils.reset_calibration(skeleton)
if result:
self._update_joint_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功重置骨骼校准: {skeleton}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"重置骨骼校准失败: {skeleton}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"重置骨骼校准出错: {str(e)}"
)
def _apply_calibration(self):
"""Apply calibration to the skeleton"""
skeleton = self.skeleton_combo.currentText()
if not skeleton:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先选择一个骨骼"
)
return
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
result = joint_utils.apply_calibration(skeleton)
if result:
self._update_joint_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"Successfully applied skeleton calibration: {skeleton}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"Failed to apply skeleton calibration: {skeleton}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"Error applying skeleton calibration: {str(e)}"
)
def _on_skeleton_changed(self, index):
"""Handle skeleton combo box change"""
if index >= 0:
# Update joint tree
self._update_joint_tree()
def _on_reference_changed(self, index):
"""Handle reference combo box change"""
if index >= 0:
# Update joint tree
self._update_joint_tree()
def _on_joint_selected(self, item, column):
"""Handle joint tree item selection"""
joint_name = item.text(0)
skeleton = self.skeleton_combo.currentText()
if skeleton and joint_name:
# Select the joint in the scene
try:
cmds.select(f"{skeleton}|{joint_name}")
except Exception:
pass
def _update_joint_tree(self):
"""Update the joint tree with current skeleton information"""
self.joints_tree.clear()
skeleton = self.skeleton_combo.currentText()
if not skeleton:
return
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
# Get joint information
joints_info = joint_utils.get_joints_info(skeleton)
if joints_info:
# Add joints to tree
for joint_name, joint_info in joints_info.items():
position = joint_info.get("position", "")
rotation = joint_info.get("rotation", "")
status = joint_info.get("status", "Not Calibrated")
item = QtWidgets.QTreeWidgetItem(self.joints_tree, [
joint_name,
str(position),
str(rotation),
status
])
# Set item color based on status
if status == "Calibrated":
item.setForeground(3, QtGui.QBrush(QtGui.QColor("green")))
elif status == "Partially Calibrated":
item.setForeground(3, QtGui.QBrush(QtGui.QColor("orange")))
else:
item.setForeground(3, QtGui.QBrush(QtGui.QColor("red")))
except Exception as e:
print(f"Error updating joint tree: {str(e)}")
def _update_visualization(self):
"""Update visualization based on checkbox states"""
show_reference = self.show_reference_check.isChecked()
show_current = self.show_current_check.isChecked()
show_differences = self.show_differences_check.isChecked()
# Import the joint utils here to avoid circular imports
from utils.joint_utils import JointUtils
joint_utils = JointUtils()
try:
joint_utils.update_visualization(
show_reference=show_reference,
show_current=show_current,
show_differences=show_differences
)
except Exception as e:
print(f"Error updating visualization: {str(e)}")