ILDA手指的逆运动学仿真

整体架构
使用Streamlit的交互式3D手指运动学仿真平台,主要包含以下几个核心模块:
1. 运动学模型核心类 ILDAFingerKinematics
class ILDAFingerKinematics:
def __init__(self):
# 精确的机械参数设置
self.L1 = 42.5 # 掌骨长度
self.L2 = 35.2 # 近指节长度
self.L3 = 28.7 # 远指节长度
# ... 其他参数
核心功能:
逆运动学计算:根据目标位姿计算所需的关节角度和电机位移
正运动学计算:根据关节状态计算机械臂末端位置
雅可比矩阵计算:用于速度分析和奇异性检测
工作空间分析:计算手指可达的运动范围
2. 专业可视化系统
代码提供了丰富的可视化功能:
def draw_mechanism_diagram(ax, mcp_pos, pip_pos, dip_pos, title, trajectory=None, show_guides=True):
# 绘制机构原理图,包含辅助线和坐标投影
可视化特性:
多视角显示:3D视角、俯视图、正视图、侧视图
辅助线系统:坐标投影线、角度圆弧标注、关节参考线
轨迹跟踪:实时显示指尖运动轨迹
专业标注:关节名称、角度值、坐标指示
3. 动画和交互系统
def create_kinematics_animation(ilda, target_params, mode, num_frames=30):
# 创建平滑的动画序列,使用缓动函数使运动更自然
动画特性:
双向运动学:支持逆运动学和正运动学两种模式
实时数据反馈:显示关节角度、电机位移、末端位置
可配置参数:动画速度、帧数、视角角度
进度跟踪:使用Streamlit的进度条和状态显示
建模计算
1. 并联机构建模
代码实现了Two-PSS并联机构,这是灵巧手设计的先进方案:
两个电机驱动:通过线性位移控制掌指关节的3自由度旋转
运动学耦合:MCP关节的旋转通过复杂的几何关系与电机位移关联
2. 旋转矩阵计算
def rotation_matrix(self, rx, ry, rz):
# 按照ZYX旋转顺序计算,符合机器人学标准
return Rz @ Ry @ Rx # 矩阵乘法顺序很重要
3. 工作空间分析算法
def calculate_workspace(self, resolution=10):
# 通过离散化关节空间来计算可达工作空间
# 使用凸包算法分析工作空间边界
4. 性能分析功能
雅可比矩阵计算:用于分析机构的运动学性能
条件数分析:评估机构的奇异性
可操作度度量:量化机构的灵活程度
代码
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import math
import time
import os
from matplotlib import font_manager as fm
import matplotlib.patches as patches
from scipy.optimize import minimize
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import proj3d
class Arrow3D(FancyArrowPatch):
def __init__(self, xs, ys, zs, *args, **kwargs):
super().__init__((0, 0), (0, 0), *args, **kwargs)
self._verts3d = xs, ys, zs
def do_3d_projection(self, renderer=None):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return np.min(zs)
class ILDAFingerKinematics:
def __init__(self):
# 精确的机构参数 - 基于ILDA论文实际尺寸
self.L1 = 42.5 # 掌骨长度 (mm)
self.L2 = 35.2 # 近指节长度 (mm)
self.L3 = 28.7 # 远指节长度 (mm)
self.L7 = 18.3 # 驱动连杆长度 (mm)
self.base_width = 52.0 # 基座宽度 (mm)
self.platform_width = 24.0 # 动平台宽度 (mm)
# 机构固定点位置
self.P0 = np.array([0, 0, 0]) # 基座原点
self.P1 = np.array([-self.base_width / 2, 0, 0]) # 电机1基座位置
self.P2 = np.array([self.base_width / 2, 0, 0]) # 电机2基座位置
# 动平台上的固定点(局部坐标系)
self.P3_local = np.array([-self.platform_width / 2, 0, 0])
self.P4_local = np.array([self.platform_width / 2, 0, 0])
self.P5_local = np.array([0, self.L1, 0]) # PIP驱动点
# 运动范围限制
self.rho_min = 30.0 # 电机最小位移
self.rho_max = 80.0 # 电机最大位移
self.angle_min = -math.pi / 3 # 最小关节角度
self.angle_max = math.pi / 3 # 最大关节角度
def rotation_matrix(self, rx, ry, rz):
"""精确的旋转矩阵计算 - 按照ZYX旋转顺序"""
# 绕X轴旋转
Rx = np.array([
[1, 0, 0],
[0, math.cos(rx), -math.sin(rx)],
[0, math.sin(rx), math.cos(rx)]
])
# 绕Y轴旋转
Ry = np.array([
[math.cos(ry), 0, math.sin(ry)],
[0, 1, 0],
[-math.sin(ry), 0, math.cos(ry)]
])
# 绕Z轴旋转
Rz = np.array([
[math.cos(rz), -math.sin(rz), 0],
[math.sin(rz), math.cos(rz), 0],
[0, 0, 1]
])
# ZYX旋转顺序
return Rz @ Ry @ Rx
def mcp_inverse_kinematics(self, target_orientation):
"""精确的掌指关节逆运动学 - Two-PSS并联机构"""
rx, ry, rz = target_orientation
# 计算旋转矩阵
R = self.rotation_matrix(rx, ry, rz)
# 动平台上的点在全局坐标系中的位置
P3_global = R @ self.P3_local
P4_global = R @ self.P4_local
# 计算电机位移
rho1 = np.linalg.norm(P3_global - self.P1)
rho2 = np.linalg.norm(P4_global - self.P2)
# 添加实际机构的运动范围限制
rho1 = max(self.rho_min, min(self.rho_max, rho1))
rho2 = max(self.rho_min, min(self.rho_max, rho2))
return rho1, rho2, R
def pip_inverse_kinematics(self, mcp_orientation, pip_angle):
"""精确的近端指间关节逆运动学"""
rx, ry, rz = mcp_orientation
# 基于MCP姿态计算PIP关节位置
R_mcp = self.rotation_matrix(rx, ry, rz)
# 简化的计算 - 实际应该使用优化方法
theta2 = pip_angle * 0.8 # 比例关系
return theta2
def dip_inverse_kinematics(self, pip_angle, dip_angle):
"""精确的远端指间关节逆运动学"""
# 简化的计算
theta3 = dip_angle * 0.7 # 比例关系
return theta3
def forward_kinematics(self, rho1, rho2, theta2, theta3, mcp_orientation=None):
"""精确的正运动学计算"""
if mcp_orientation is None:
mcp_orientation = (0, 0, 0)
rx, ry, rz = mcp_orientation
R_mcp = self.rotation_matrix(rx, ry, rz)
# MCP关节位置
mcp_pos = R_mcp @ np.array([0, self.L1, 0])
# PIP关节位置
pip_local = np.array([
self.L2 * math.sin(theta2),
self.L2 * math.cos(theta2),
0
])
pip_pos = mcp_pos + R_mcp @ pip_local
# DIP关节位置(指尖)
dip_local = np.array([
self.L3 * math.sin(theta2 + theta3),
self.L3 * math.cos(theta2 + theta3),
0
])
dip_pos = pip_pos + R_mcp @ dip_local
return mcp_pos, pip_pos, dip_pos
def calculate_jacobian(self, joint_angles):
"""计算雅可比矩阵用于速度分析"""
theta2, theta3 = joint_angles
J = np.zeros((3, 2))
J[0, 0] = -self.L2 * math.sin(theta2) - self.L3 * math.sin(theta2 + theta3)
J[0, 1] = -self.L3 * math.sin(theta2 + theta3)
J[1, 0] = self.L2 * math.cos(theta2) + self.L3 * math.cos(theta2 + theta3)
J[1, 1] = self.L3 * math.cos(theta2 + theta3)
return J
def calculate_workspace(self, resolution=10):
"""计算手指工作空间 - 修复版本"""
workspace_points = []
# 使用进度条显示计算进度
progress_bar = st.progress(0)
status_text = st.empty()
total_iterations = resolution ** 3
current_iteration = 0
for i, rx in enumerate(np.linspace(self.angle_min, self.angle_max, resolution)):
for j, ry in enumerate(np.linspace(self.angle_min, self.angle_max, resolution)):
for k, pip in enumerate(np.linspace(0, math.pi / 2, resolution)):
try:
# 简化计算,只考虑主要运动
rz = 0 # 固定Z轴旋转
dip = pip * 0.8 # DIP与PIP的比例关系
# 计算逆运动学
rho1, rho2, R = self.mcp_inverse_kinematics((rx, ry, rz))
theta2 = self.pip_inverse_kinematics((rx, ry, rz), pip)
theta3 = self.dip_inverse_kinematics(pip, dip)
# 正运动学验证
mcp_pos, pip_pos, dip_pos = self.forward_kinematics(
rho1, rho2, theta2, theta3, (rx, ry, rz)
)
workspace_points.append(dip_pos)
except Exception as e:
# 忽略无效位置
continue
# 更新进度
current_iteration += 1
progress = current_iteration / total_iterations
progress_bar.progress(progress)
status_text.text(f"计算进度: {progress * 100:.1f}%")
progress_bar.empty()
status_text.empty()
return np.array(workspace_points)
def setup_chinese_font():
"""设置中文字体,解决中文显示问题"""
try:
# 尝试使用系统中常见的中文字体
font_paths = [
'C:/Windows/Fonts/simhei.ttf', # 黑体
'C:/Windows/Fonts/msyh.ttc', # 微软雅黑
'C:/Windows/Fonts/simsun.ttc', # 宋体
'/System/Library/Fonts/Arial Unicode.ttf', # Mac系统
'/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf' # Linux
]
for font_path in font_paths:
if os.path.exists(font_path):
font_prop = fm.FontProperties(fname=font_path)
font_name = font_prop.get_name()
plt.rcParams['font.sans-serif'] = [font_name]
plt.rcParams['axes.unicode_minus'] = False
return True
# 如果找不到字体,使用默认设置
plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
return False
except Exception as e:
plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
return False
def draw_mechanism_diagram(ax, mcp_pos, pip_pos, dip_pos, title, trajectory=None, show_guides=True):
"""绘制机构原理图,包含辅助线"""
ax.clear()
# 绘制基座
ax.plot([-30, 30], [0, 0], [0, 0], 'k-', linewidth=4, label='基座')
# 绘制手指连杆
points = np.array([[0, 0, 0], mcp_pos, pip_pos, dip_pos])
ax.plot(points[:, 0], points[:, 1], points[:, 2], 'bo-',
linewidth=3, markersize=8, label='手指连杆')
# 标注关节
ax.text(mcp_pos[0], mcp_pos[1], mcp_pos[2], 'MCP', fontsize=10, fontweight='bold')
ax.text(pip_pos[0], pip_pos[1], pip_pos[2], 'PIP', fontsize=10, fontweight='bold')
ax.text(dip_pos[0], dip_pos[1], dip_pos[2], 'DIP', fontsize=10, fontweight='bold')
if show_guides:
# 绘制辅助线 - 坐标轴投影
# X轴投影
ax.plot([dip_pos[0], dip_pos[0]], [dip_pos[1], 0], [0, 0], 'r--', alpha=0.5, linewidth=1)
ax.plot([dip_pos[0], dip_pos[0]], [0, 0], [0, dip_pos[2]], 'r--', alpha=0.5, linewidth=1)
# Y轴投影
ax.plot([dip_pos[0], 0], [dip_pos[1], dip_pos[1]], [0, 0], 'g--', alpha=0.5, linewidth=1)
ax.plot([0, 0], [dip_pos[1], dip_pos[1]], [0, dip_pos[2]], 'g--', alpha=0.5, linewidth=1)
# Z轴投影
ax.plot([dip_pos[0], 0], [0, 0], [dip_pos[2], dip_pos[2]], 'b--', alpha=0.5, linewidth=1)
ax.plot([0, 0], [dip_pos[1], 0], [dip_pos[2], dip_pos[2]], 'b--', alpha=0.5, linewidth=1)
# 绘制角度辅助线
draw_angle_guides(ax, points)
# 绘制坐标轴指示
ax.quiver(0, 0, 0, 20, 0, 0, color='r', linewidth=2, arrow_length_ratio=0.1, label='X轴')
ax.quiver(0, 0, 0, 0, 20, 0, color='g', linewidth=2, arrow_length_ratio=0.1, label='Y轴')
ax.quiver(0, 0, 0, 0, 0, 20, color='b', linewidth=2, arrow_length_ratio=0.1, label='Z轴')
# 绘制轨迹
if trajectory is not None and len(trajectory) > 0:
traj_points = np.array(trajectory)
ax.plot(traj_points[:, 0], traj_points[:, 1], traj_points[:, 2],
'r-', linewidth=2, alpha=0.7, label='轨迹')
ax.set_xlabel('X (mm)')
ax.set_ylabel('Y (mm)')
ax.set_zlabel('Z (mm)')
ax.set_title(title, fontsize=12, fontweight='bold')
ax.legend()
ax.grid(True)
def draw_angle_guides(ax, points):
"""绘制角度辅助线"""
origin, mcp, pip, dip = points
# 计算角度
v1 = mcp - origin
v2 = pip - mcp
v3 = dip - pip
# 计算各段的角度(在XY平面上的投影)
if np.linalg.norm(v1[:2]) > 0.1:
angle_mcp = math.atan2(v1[0], v1[1])
draw_angle_arc(ax, origin, angle_mcp, 15, 'MCP角')
if np.linalg.norm(v2[:2]) > 0.1:
angle_pip = math.atan2(v2[0], v2[1])
draw_angle_arc(ax, mcp, angle_pip, 10, 'PIP角')
if np.linalg.norm(v3[:2]) > 0.1:
angle_dip = math.atan2(v3[0], v3[1])
draw_angle_arc(ax, pip, angle_dip, 8, 'DIP角')
# 绘制关节处的角度指示线
draw_joint_angle_lines(ax, points)
def draw_angle_arc(ax, center, angle, radius, label):
"""绘制角度圆弧"""
start_angle = 0
end_angle = math.degrees(angle)
# 在XY平面上绘制圆弧
theta = np.linspace(start_angle, end_angle, 20)
theta_rad = np.radians(theta)
x = center[0] + radius * np.sin(theta_rad)
y = center[1] + radius * np.cos(theta_rad)
z = np.full_like(x, center[2])
ax.plot(x, y, z, 'm-', linewidth=2, alpha=0.7)
# 标注角度
if abs(end_angle) > 5: # 只标注较大的角度
mid_angle = end_angle / 2
mid_rad = np.radians(mid_angle)
label_x = center[0] + (radius + 2) * np.sin(mid_rad)
label_y = center[1] + (radius + 2) * np.cos(mid_rad)
label_z = center[2]
ax.text(label_x, label_y, label_z, f'{label}\n{end_angle:.1f}°',
fontsize=8, ha='center', va='center')
def draw_joint_angle_lines(ax, points):
"""绘制关节角度指示线"""
origin, mcp, pip, dip = points
# 在MCP关节处绘制参考线
ax.plot([origin[0], mcp[0]], [origin[1], mcp[1]], [origin[2], mcp[2]],
'c--', alpha=0.7, linewidth=1)
# 在PIP关节处绘制参考线
ax.plot([mcp[0], pip[0]], [mcp[1], pip[1]], [mcp[2], pip[2]],
'y--', alpha=0.7, linewidth=1)
# 在DIP关节处绘制参考线
ax.plot([pip[0], dip[0]], [pip[1], dip[1]], [pip[2], dip[2]],
'm--', alpha=0.7, linewidth=1)
def draw_multi_view_diagram(fig, mcp_pos, pip_pos, dip_pos, title, view_angles, show_guides=True):
"""绘制多视角图表,包含辅助线"""
views = [
("3D视角", 221, view_angles[0]),
("俯视图(XY)", 222, view_angles[1]),
("正视图(XZ)", 223, view_angles[2]),
("侧视图(YZ)", 224, view_angles[3])
]
for i, (view_name, subplot_num, (elev, azim)) in enumerate(views):
ax = fig.add_subplot(subplot_num, projection='3d')
# 绘制机构
points = np.array([[0, 0, 0], mcp_pos, pip_pos, dip_pos])
ax.plot(points[:, 0], points[:, 1], points[:, 2], 'bo-',
linewidth=2, markersize=6)
if show_guides and i == 0: # 只在3D视角显示辅助线
# 绘制辅助线
draw_angle_guides(ax, points)
# 绘制坐标投影
dip = points[3]
ax.plot([dip[0], dip[0]], [dip[1], 0], [0, 0], 'r--', alpha=0.3, linewidth=1)
ax.plot([dip[0], 0], [dip[1], dip[1]], [0, 0], 'g--', alpha=0.3, linewidth=1)
ax.plot([dip[0], 0], [0, 0], [dip[2], dip[2]], 'b--', alpha=0.3, linewidth=1)
# 设置视角
ax.view_init(elev=elev, azim=azim)
# 设置坐标轴
ax.set_xlim([-50, 50])
ax.set_ylim([-10, 100])
ax.set_zlim([-10, 50])
ax.set_xlabel('X (mm)')
ax.set_ylabel('Y (mm)')
ax.set_zlabel('Z (mm)')
ax.set_title(view_name, fontsize=10)
ax.grid(True)
def create_kinematics_animation(ilda, target_params, mode, num_frames=30):
"""创建运动学动画序列"""
frames = []
trajectory = []
for i in range(num_frames):
t = i / (num_frames - 1) if num_frames > 1 else 1
# 使用缓动函数使动画更自然
t_ease = 0.5 - 0.5 * math.cos(t * math.pi)
if mode == "inverse":
rx, ry, rz, pip_angle, dip_angle = target_params
# 线性插值
current_rx = t_ease * rx
current_ry = t_ease * ry
current_rz = t_ease * rz
current_pip = t_ease * pip_angle
current_dip = t_ease * dip_angle
# 计算逆运动学
rho1, rho2, R = ilda.mcp_inverse_kinematics((current_rx, current_ry, current_rz))
theta2 = ilda.pip_inverse_kinematics((current_rx, current_ry, current_rz), current_pip)
theta3 = ilda.dip_inverse_kinematics(current_pip, current_dip)
# 正运动学验证
mcp_pos, pip_pos, dip_pos = ilda.forward_kinematics(
rho1, rho2, theta2, theta3, (current_rx, current_ry, current_rz)
)
frame_data = {
'type': 'inverse',
'mcp_angles': (current_rx, current_ry, current_rz),
'pip_angle': current_pip,
'dip_angle': current_dip,
'motor_positions': (rho1, rho2, theta2, theta3),
'mcp_pos': mcp_pos,
'pip_pos': pip_pos,
'dip_pos': dip_pos,
'frame_progress': t
}
else: # 正运动学
rho1, rho2, theta2, theta3 = target_params
# 线性插值
current_rho1 = t_ease * rho1
current_rho2 = t_ease * rho2
current_theta2 = t_ease * theta2
current_theta3 = t_ease * theta3
# 计算正运动学
mcp_pos, pip_pos, dip_pos = ilda.forward_kinematics(
current_rho1, current_rho2, current_theta2, current_theta3
)
frame_data = {
'type': 'forward',
'motor_positions': (current_rho1, current_rho2, current_theta2, current_theta3),
'mcp_pos': mcp_pos,
'pip_pos': pip_pos,
'dip_pos': dip_pos,
'frame_progress': t
}
frames.append(frame_data)
trajectory.append(dip_pos)
return frames, trajectory
def main():
# 页面设置
st.set_page_config(
page_title="ILDA手指专业运动学仿真",
layout="wide",
initial_sidebar_state="expanded"
)
# 设置中文字体
setup_chinese_font()
# 初始化运动学模型
ilda = ILDAFingerKinematics()
# 应用标题
st.title(" ILDA灵巧手指专业运动学仿真平台")
st.markdown("""
**精密建模 | 多维度分析 | 专业可视化 | 增强辅助线**
基于ILDA灵巧手的精确运动学模型,提供专业的仿真分析功能。
""")
# 初始化session state
if 'animate_inverse' not in st.session_state:
st.session_state.animate_inverse = False
if 'animate_forward' not in st.session_state:
st.session_state.animate_forward = False
if 'current_frame_inverse' not in st.session_state:
st.session_state.current_frame_inverse = 0
if 'current_frame_forward' not in st.session_state:
st.session_state.current_frame_forward = 0
if 'inverse_frames' not in st.session_state:
st.session_state.inverse_frames = []
if 'forward_frames' not in st.session_state:
st.session_state.forward_frames = []
if 'inverse_trajectory' not in st.session_state:
st.session_state.inverse_trajectory = []
if 'forward_trajectory' not in st.session_state:
st.session_state.forward_trajectory = []
if 'workspace_calculated' not in st.session_state:
st.session_state.workspace_calculated = False
if 'workspace_points' not in st.session_state:
st.session_state.workspace_points = None
# 侧边栏控制
st.sidebar.header("️ 仿真控制面板")
with st.sidebar.expander(" 显示设置", expanded=True):
animation_speed = st.slider("动画速度 (ms/帧)", 100, 1000, 300, key="anim_speed")
num_frames = st.slider("动画帧数", 10, 100, 30, key="num_frames")
show_trajectory = st.checkbox("显示指尖轨迹", value=True, key="show_traj")
show_guides = st.checkbox("显示辅助线", value=True, key="show_guides")
show_angle_arcs = st.checkbox("显示角度圆弧", value=True, key="show_arcs")
with st.sidebar.expander("️ 视角设置", expanded=True):
elev_3d = st.slider("3D视角仰角", -90, 90, 30, key="elev_3d")
azim_3d = st.slider("3D视角方位角", 0, 360, 45, key="azim_3d")
xy_azim = st.slider("俯视图方位角", 0, 360, 90, key="xy_azim")
xz_elev = st.slider("正视图仰角", -90, 90, 0, key="xz_elev")
yz_azim = st.slider("侧视图方位角", 0, 360, 0, key="yz_azim")
view_angles = [
(elev_3d, azim_3d), # 3D视角
(90, xy_azim), # 俯视图
(xz_elev, 0), # 正视图
(0, yz_azim) # 侧视图
]
# 主内容区域 - 使用标签页组织
tab1, tab2, tab3 = st.tabs([" 实时仿真", " 运动分析", "⚙️ 系统配置"])
with tab1:
st.header("实时运动学仿真")
# 使用列布局
col_control, col_viz = st.columns([1, 2])
with col_control:
st.subheader("控制面板")
# 运动模式选择
kin_mode = st.radio(
"运动学模式",
["逆运动学仿真", "正运动学仿真"],
index=0,
key="kin_mode"
)
if kin_mode == "逆运动学仿真":
with st.expander("逆运动学参数", expanded=True):
rx = st.slider("MCP-X旋转 (rad)", -1.57, 1.57, 0.5, 0.01, key="rx")
ry = st.slider("MCP-Y旋转 (rad)", -1.57, 1.57, 0.3, 0.01, key="ry")
rz = st.slider("MCP-Z旋转 (rad)", -1.57, 1.57, 0.2, 0.01, key="rz")
pip_angle = st.slider("PIP关节角 (rad)", 0.0, 2.0, 1.2, 0.01, key="pip")
dip_angle = st.slider("DIP关节角 (rad)", 0.0, 2.0, 0.8, 0.01, key="dip")
if st.button(" 生成逆运动学动画", use_container_width=True, key="gen_inverse"):
target_params = (rx, ry, rz, pip_angle, dip_angle)
frames, trajectory = create_kinematics_animation(
ilda, target_params, "inverse", num_frames
)
st.session_state.inverse_frames = frames
st.session_state.inverse_trajectory = trajectory
st.session_state.current_frame_inverse = 0
st.success(f"已生成 {len(frames)} 帧逆运动学动画!")
else: # 正运动学
with st.expander("正运动学参数", expanded=True):
rho1 = st.slider("电机1位移 ρ1 (mm)", 30.0, 80.0, 50.0, 1.0, key="rho1")
rho2 = st.slider("电机2位移 ρ2 (mm)", 30.0, 80.0, 55.0, 1.0, key="rho2")
theta2 = st.slider("θ2角度 (rad)", 0.0, 2.0, 1.0, 0.01, key="theta2")
theta3 = st.slider("θ3角度 (rad)", 0.0, 2.0, 0.6, 0.01, key="theta3")
if st.button(" 生成正运动学动画", use_container_width=True, key="gen_forward"):
target_params = (rho1, rho2, theta2, theta3)
frames, trajectory = create_kinematics_animation(
ilda, target_params, "forward", num_frames
)
st.session_state.forward_frames = frames
st.session_state.forward_trajectory = trajectory
st.session_state.current_frame_forward = 0
st.success(f"已生成 {len(frames)} 帧正运动学动画!")
# 动画控制按钮
col_btn1, col_btn2 = st.columns(2)
with col_btn1:
if st.button("▶️ 开始动画", use_container_width=True, key="start_anim"):
if kin_mode == "逆运动学仿真" and st.session_state.inverse_frames:
st.session_state.animate_inverse = True
elif kin_mode == "正运动学仿真" and st.session_state.forward_frames:
st.session_state.animate_forward = True
else:
st.warning("请先生成动画序列")
with col_btn2:
if st.button("⏹️ 停止动画", use_container_width=True, key="stop_anim"):
st.session_state.animate_inverse = False
st.session_state.animate_forward = False
# 实时数据显示
if st.session_state.inverse_frames or st.session_state.forward_frames:
st.subheader("实时数据")
if kin_mode == "逆运动学仿真" and st.session_state.inverse_frames:
current_frame = st.session_state.current_frame_inverse
if current_frame < len(st.session_state.inverse_frames):
frame_data = st.session_state.inverse_frames[current_frame]
col1, col2 = st.columns(2)
with col1:
st.metric("指尖X", f"{frame_data['dip_pos'][0]:.2f} mm")
st.metric("指尖Y", f"{frame_data['dip_pos'][1]:.2f} mm")
st.metric("指尖Z", f"{frame_data['dip_pos'][2]:.2f} mm")
with col2:
st.metric("电机1 ρ1", f"{frame_data['motor_positions'][0]:.1f} mm")
st.metric("电机2 ρ2", f"{frame_data['motor_positions'][1]:.1f} mm")
st.metric("θ2", f"{math.degrees(frame_data['motor_positions'][2]):.1f}°")
elif kin_mode == "正运动学仿真" and st.session_state.forward_frames:
current_frame = st.session_state.current_frame_forward
if current_frame < len(st.session_state.forward_frames):
frame_data = st.session_state.forward_frames[current_frame]
col1, col2 = st.columns(2)
with col1:
st.metric("指尖X", f"{frame_data['dip_pos'][0]:.2f} mm")
st.metric("指尖Y", f"{frame_data['dip_pos'][1]:.2f} mm")
st.metric("指尖Z", f"{frame_data['dip_pos'][2]:.2f} mm")
with col2:
st.metric("电机1 ρ1", f"{frame_data['motor_positions'][0]:.1f} mm")
st.metric("电机2 ρ2", f"{frame_data['motor_positions'][1]:.1f} mm")
st.metric("θ2", f"{math.degrees(frame_data['motor_positions'][2]):.1f}°")
with col_viz:
st.subheader("三维可视化")
# 主可视化区域
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
if kin_mode == "逆运动学仿真" and st.session_state.inverse_frames:
current_frame = st.session_state.current_frame_inverse
if current_frame < len(st.session_state.inverse_frames):
frame_data = st.session_state.inverse_frames[current_frame]
trajectory = st.session_state.inverse_trajectory if show_trajectory else None
draw_mechanism_diagram(
ax,
frame_data['mcp_pos'],
frame_data['pip_pos'],
frame_data['dip_pos'],
"逆运动学仿真",
trajectory,
show_guides
)
elif kin_mode == "正运动学仿真" and st.session_state.forward_frames:
current_frame = st.session_state.current_frame_forward
if current_frame < len(st.session_state.forward_frames):
frame_data = st.session_state.forward_frames[current_frame]
trajectory = st.session_state.forward_trajectory if show_trajectory else None
draw_mechanism_diagram(
ax,
frame_data['mcp_pos'],
frame_data['pip_pos'],
frame_data['dip_pos'],
"正运动学仿真",
trajectory,
show_guides
)
else:
# 显示初始状态
mcp_pos, pip_pos, dip_pos = ilda.forward_kinematics(40, 40, 0, 0)
draw_mechanism_diagram(ax, mcp_pos, pip_pos, dip_pos, "初始状态", show_guides=show_guides)
st.pyplot(fig)
# 多视角图表
if ((st.session_state.inverse_frames and kin_mode == "逆运动学仿真") or
(st.session_state.forward_frames and kin_mode == "正运动学仿真")) and st.sidebar.checkbox("显示多视角",
value=True,
key="show_multi"):
st.subheader("多视角分析")
fig_multi = plt.figure(figsize=(12, 10))
if kin_mode == "逆运动学仿真" and st.session_state.inverse_frames:
current_frame = st.session_state.current_frame_inverse
if current_frame < len(st.session_state.inverse_frames):
frame_data = st.session_state.inverse_frames[current_frame]
draw_multi_view_diagram(
fig_multi,
frame_data['mcp_pos'],
frame_data['pip_pos'],
frame_data['dip_pos'],
"逆运动学多视角",
view_angles,
show_guides
)
elif kin_mode == "正运动学仿真" and st.session_state.forward_frames:
current_frame = st.session_state.current_frame_forward
if current_frame < len(st.session_state.forward_frames):
frame_data = st.session_state.forward_frames[current_frame]
draw_multi_view_diagram(
fig_multi,
frame_data['mcp_pos'],
frame_data['pip_pos'],
frame_data['dip_pos'],
"正运动学多视角",
view_angles,
show_guides
)
plt.tight_layout()
st.pyplot(fig_multi)
with tab2:
st.header("运动学分析")
col_analysis1, col_analysis2 = st.columns(2)
with col_analysis1:
st.subheader("工作空间分析")
# 工作空间计算控制
resolution = st.slider("计算分辨率", 5, 20, 8, key="ws_res")
if st.button(" 计算工作空间", key="calc_workspace"):
with st.spinner("计算工作空间中..."):
try:
workspace_points = ilda.calculate_workspace(resolution=resolution)
st.session_state.workspace_points = workspace_points
st.session_state.workspace_calculated = True
st.success(f"工作空间计算完成!共 {len(workspace_points)} 个可达点")
except Exception as e:
st.error(f"工作空间计算失败: {str(e)}")
if st.session_state.workspace_calculated and st.session_state.workspace_points is not None:
workspace_points = st.session_state.workspace_points
# 显示工作空间
fig_ws = plt.figure(figsize=(10, 8))
ax_ws = fig_ws.add_subplot(111, projection='3d')
# 绘制工作空间点云
ax_ws.scatter(workspace_points[:, 0], workspace_points[:, 1], workspace_points[:, 2],
c='blue', alpha=0.3, s=10, label='可达工作空间')
# 绘制凸包边界
from scipy.spatial import ConvexHull
if len(workspace_points) > 4:
try:
hull = ConvexHull(workspace_points)
for simplex in hull.simplices:
ax_ws.plot(workspace_points[simplex, 0], workspace_points[simplex, 1],
workspace_points[simplex, 2], 'r-', alpha=0.3)
except:
pass # 如果凸包计算失败,忽略
ax_ws.set_xlabel('X (mm)')
ax_ws.set_ylabel('Y (mm)')
ax_ws.set_zlabel('Z (mm)')
ax_ws.set_title('手指工作空间分析')
ax_ws.legend()
ax_ws.grid(True)
st.pyplot(fig_ws)
# 工作空间统计信息
st.metric("工作空间体积点数", f"{len(workspace_points)}")
st.metric("X方向范围",
f"{np.min(workspace_points[:, 0]):.1f} - {np.max(workspace_points[:, 0]):.1f} mm")
st.metric("Y方向范围",
f"{np.min(workspace_points[:, 1]):.1f} - {np.max(workspace_points[:, 1]):.1f} mm")
st.metric("Z方向范围",
f"{np.min(workspace_points[:, 2]):.1f} - {np.max(workspace_points[:, 2]):.1f} mm")
with col_analysis2:
st.subheader("性能分析")
if st.session_state.inverse_frames or st.session_state.forward_frames:
if kin_mode == "逆运动学仿真" and st.session_state.inverse_frames:
current_frame = st.session_state.current_frame_inverse
if current_frame < len(st.session_state.inverse_frames):
frame_data = st.session_state.inverse_frames[current_frame]
# 计算雅可比矩阵
J = ilda.calculate_jacobian(frame_data['motor_positions'][2:])
cond_number = np.linalg.cond(J)
st.metric("雅可比矩阵条件数", f"{cond_number:.2f}")
st.metric("矩阵秩", f"{np.linalg.matrix_rank(J)}")
# 可操作度分析
manipulability = np.sqrt(np.linalg.det(J @ J.T))
st.metric("可操作度", f"{manipulability:.4f}")
elif kin_mode == "正运动学仿真" and st.session_state.forward_frames:
current_frame = st.session_state.current_frame_forward
if current_frame < len(st.session_state.forward_frames):
frame_data = st.session_state.forward_frames[current_frame]
# 计算雅可比矩阵
J = ilda.calculate_jacobian(frame_data['motor_positions'][2:])
cond_number = np.linalg.cond(J)
st.metric("雅可比矩阵条件数", f"{cond_number:.2f}")
st.metric("矩阵秩", f"{np.linalg.matrix_rank(J)}")
# 可操作度分析
manipulability = np.sqrt(np.linalg.det(J @ J.T))
st.metric("可操作度", f"{manipulability:.4f}")
with tab3:
st.header("系统配置")
col_config1, col_config2 = st.columns(2)
with col_config1:
st.subheader("机构参数")
st.number_input("掌骨长度 L1 (mm)", value=42.5, key="config_L1")
st.number_input("近指节长度 L2 (mm)", value=35.2, key="config_L2")
st.number_input("远指节长度 L3 (mm)", value=28.7, key="config_L3")
st.number_input("基座宽度 (mm)", value=52.0, key="config_base_width")
with col_config2:
st.subheader("运动范围")
st.slider("电机最小位移 (mm)", 20.0, 50.0, 30.0, key="config_rho_min")
st.slider("电机最大位移 (mm)", 60.0, 100.0, 80.0, key="config_rho_max")
st.slider("最小关节角度 (rad)", -2.0, 0.0, -1.05, 0.01, key="config_angle_min")
st.slider("最大关节角度 (rad)", 0.0, 2.0, 1.05, 0.01, key="config_angle_max")
if st.button("应用配置", key="apply_config"):
# 更新机构参数
ilda.L1 = st.session_state.config_L1
ilda.L2 = st.session_state.config_L2
ilda.L3 = st.session_state.config_L3
ilda.base_width = st.session_state.config_base_width
ilda.rho_min = st.session_state.config_rho_min
ilda.rho_max = st.session_state.config_rho_max
ilda.angle_min = st.session_state.config_angle_min
ilda.angle_max = st.session_state.config_angle_max
st.success("机构参数已更新!")
# 动画更新逻辑
if st.session_state.animate_inverse and st.session_state.inverse_frames:
if st.session_state.current_frame_inverse < len(st.session_state.inverse_frames) - 1:
st.session_state.current_frame_inverse += 1
else:
st.session_state.animate_inverse = False
if st.session_state.animate_forward and st.session_state.forward_frames:
if st.session_state.current_frame_forward < len(st.session_state.forward_frames) - 1:
st.session_state.current_frame_forward += 1
else:
st.session_state.animate_forward = False
# 自动重新运行以更新动画
if st.session_state.animate_inverse or st.session_state.animate_forward:
time.sleep(animation_speed / 1000)
st.rerun()
# 页脚信息
st.markdown("---")
st.caption(
f" 实时更新频率: {1000 / animation_speed:.1f}Hz | 计算精度: 0.001mm | ⚡ 最后更新: {time.strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
main()
浙公网安备 33010602011771号