基于Web的交互式坐标系变换矩阵计算工具
基于Web的交互式坐标系变换矩阵计算工具
- 一、什么是坐标系变换矩阵?
- 二、为什么需要这个工具?
- 三、效果
- 四、功能介绍
- 1、坐标系定义
- 2、交互控制
- 3、变换矩阵计算
- 五、如何使用这个工具
- 六、完整代码
- 七、总结
一、什么是坐标系变换矩阵?
在三维空间中,我们经常需要描述物体之间的位置和方向关系。坐标系变换矩阵就是用来表示从一个坐标系到另一个坐标系转换关系的数学工具。这种4×4矩阵包含了两部分关键信息:
- 旋转分量:表示坐标系之间的方向关系
- 平移分量:表示坐标系之间的位置偏移
这种变换矩阵在计算机图形学、机器人学、计算机视觉和增强现实等领域有广泛应用。例如,当我们需要将3D模型渲染到屏幕上时,必须计算模型坐标系到相机坐标系的变换矩阵。
二、为什么需要这个工具?
手动计算三维空间中的变换矩阵麻烦:
- 需要理解旋转矩阵、四元数、欧拉角等
- 不同领域使用不同的坐标系约定(如计算机图形学通常使用Y向上,而机器人学常用Z向上)
- 旋转顺序对结果有重大影响(Z-Y-X旋转与X-Y-Z旋转结果不同)
这个交互式工具通过可视化界面解决了这些问题:
- 直观展示:三维空间中的坐标系和物体关系一目了然
- 实时反馈:调整参数时立即看到效果
- 自动计算:复杂的数学运算在后台自动完成
- 多表示形式:同时提供矩阵和四元数两种表示法
本工具设计之初 用于计算相机与雷达联合标定的初始外参,所以坐标系由此定义
三、效果
四、功能介绍
1、坐标系定义
# 长方体顶点定义以匹配标准相机坐标系 (X右, Y下, Z前)
def create_box_vertices(length=0.5, width=0.3, height=0.1):vertices = np.array([# 后部 (z = -half_h)[-half_l, -half_w, -half_h], # 后左下[ half_l, -half_w, -half_h], # 后右下...])return vertices
- 基坐标系:X前、Y左、Z上(常用于机器人学)
- 相机坐标系:X右、Y下、Z前(计算机视觉标准)
2、交互控制
dash_app.layout = html.Div([dcc.RadioItems(id='operation-mode',options=[{'label': '移动/旋转整个空间', 'value': 'space'},{'label': '移动/旋转相机(长方体)', 'value': 'camera'}],value='camera'),# 位置输入dcc.Input(id='camera-x-input', type='number', value=0.0),dcc.Input(id='camera-y-input', type='number', value=0.0),dcc.Input(id='camera-z-input', type='number', value=0.0),# 旋转输入dcc.Input(id='camera-yaw-input', type='number', value=0.0),dcc.Input(id='camera-pitch-input', type='number', value=0.0),dcc.Input(id='camera-roll-input', type='number', value=0.0),# 计算按钮html.Button("计算变换矩阵", id="calculate-btn")
])
- 操作模式选择:移动相机或移动整个空间
- 位置控制:输入X/Y/Z坐标值
- 旋转控制:按Z-Y-X顺序输入欧拉角(Yaw、Pitch、Roll)
- 计算按钮:触发变换矩阵计算
3、变换矩阵计算
def calculate_transformation(n_clicks, x, y, z, roll, pitch, yaw):# 创建旋转矩阵(按Z-Y-X顺序)rotation = R.from_euler('zyx', [yaw, pitch, roll], degrees=True)rot_matrix = rotation.as_matrix()# 创建4x4变换矩阵transformation_matrix = np.eye(4)transformation_matrix[:3, :3] = rot_matrix # 旋转部分transformation_matrix[:3, 3] = [x, y, z] # 平移部分# 获取四元数表示quaternion = rotation.as_quat() # [x, y, z, w]return formatted_matrix, formatted_quaternion
核心计算步骤:
- 将欧拉角转换为旋转矩阵(按Z-Y-X顺序)
- 组合旋转矩阵和平移向量为4×4变换矩阵
- 同时计算四元数表示(更紧凑的旋转表示)
五、如何使用这个工具
-
调整位置参数:
- 在X/Y/Z输入框中输入数值(单位:米)
- 正值表示沿该轴正方向移动
-
调整方向参数:
- Yaw(偏航角):绕Z轴旋转(左右转头)
- Pitch(俯仰角):绕Y轴旋转(抬头低头)
- Roll(滚转角):绕X轴旋转(左右倾斜)
-
选择操作模式:
- “移动/旋转相机”:改变相机位置/方向
- “移动/旋转空间”:保持相机不动,移动整个世界
-
计算结果:
- 点击"计算变换矩阵"按钮
- 查看4×4变换矩阵和四元数表示
六、完整代码
- app.py
import dash
from dash import dcc, html, Input, Output, State, no_update
import dash_daq as daq
import plotly.graph_objects as go
import numpy as np
from scipy.spatial.transform import Rotation as R
import re# 初始化应用
dash_app = dash.Dash(__name__, title='坐标系变换矩阵计算工具')
server = dash_app.server# 长方体顶点定义以匹配标准相机坐标系 (X右, Y下, Z前)
def create_box_vertices(length=0.5, width=0.3, height=0.1):half_l = length / 2half_w = width / 2half_h = height / 2# 定义标准相机坐标系顶点:# X: 右为正# Y: 下为正# Z: 前为正vertices = np.array([# 后部 (z = -half_h)[-half_l, -half_w, -half_h], # 后左下 (x左, y上, z后)[ half_l, -half_w, -half_h], # 后右下 (x右, y上, z后)[ half_l, half_w, -half_h], # 后右上 (x右, y下, z后)[-half_l, half_w, -half_h], # 后左上 (x左, y下, z后)# 前部 (z = half_h)[-half_l, -half_w, half_h], # 前左下 (x左, y上, z前)[ half_l, -half_w, half_h], # 前右下 (x右, y上, z前)[ half_l, half_w, half_h], # 前右上 (x右, y下, z前)[-half_l, half_w, half_h] # 前左上 (x左, y下, z前)])return vertices# 创建长方体网格
def create_box_mesh(vertices):faces = [[0, 1, 2, 3], # 底部[4, 5, 6, 7], # 顶部[0, 1, 5, 4], # 前面[2, 3, 7, 6], # 后面[1, 2, 6, 5], # 右面[0, 3, 7, 4] # 左面]x, y, z = vertices[:, 0], vertices[:, 1], vertices[:, 2]i, j, k = [], [], []for face in faces:i.extend([face[0], face[0], face[1], face[1], face[2], face[2], face[3]])j.extend([face[1], face[3], face[0], face[2], face[1], face[3], face[0]])k.extend([face[2], face[1], face[3], face[0], face[3], face[0], face[1]])return go.Mesh3d(x=x, y=y, z=z,i=i, j=j, k=k,color='#1f77b4',opacity=0.8,flatshading=True,name='相机(长方体)')# 创建坐标系
def create_coordinate_frame(origin=[0, 0, 0], scale=1.0, name="基坐标系"):# 坐标系: X前(红), Y左(绿), Z上(蓝)x_axis = go.Scatter3d(x=[origin[0], origin[0] + scale],y=[origin[1], origin[1]],z=[origin[2], origin[2]],mode='lines+text',line=dict(width=5, color='red'),text=['', 'X'],textposition="top center",name=f'{name} X轴')y_axis = go.Scatter3d(x=[origin[0], origin[0]],y=[origin[1], origin[1] + scale],z=[origin[2], origin[2]],mode='lines+text',line=dict(width=5, color='lime'),text=['', 'Y'],textposition="middle right",name=f'{name} Y轴')z_axis = go.Scatter3d(x=[origin[0], origin[0]],y=[origin[1], origin[1]],z=[origin[2], origin[2] + scale],mode='lines+text',line=dict(width=5, color='blue'),text=['', 'Z'],textposition="top center",name=f'{name} Z轴')return [x_axis, y_axis, z_axis]# 创建初始3D场景
def create_initial_scene():# 创建基坐标系axes = create_coordinate_frame(scale=1.5, name="基坐标系")# 创建长方体(相机) - 使用修复后的顶点定义box_vertices = create_box_vertices()box_mesh = create_box_mesh(box_vertices)# 添加相机位置标记camera_pos = go.Scatter3d(x=[0], y=[0], z=[0],mode='markers',marker=dict(size=5, color='gold'),name='相机中心')# 组合所有图形元素data = axes + [box_mesh, camera_pos]# 创建3D场景布局layout = go.Layout(scene=dict(xaxis=dict(title='X (前)', range=[-2, 2]),yaxis=dict(title='Y (左)', range=[-2, 2]),zaxis=dict(title='Z (上)', range=[-2, 2]),aspectmode='cube',camera=dict(eye=dict(x=1.5, y=1.5, z=1.5))),margin=dict(l=0, r=0, b=0, t=0),showlegend=True,legend=dict(x=0, y=0))return go.Figure(data=data, layout=layout)# 应用布局
dash_app.layout = html.Div([html.Div([html.H1("坐标系变换矩阵计算工具", className="header"),html.P("交互式计算相机(长方体)到基坐标系的变换矩阵", className="subtitle")], className="banner"),html.Div([dcc.Graph(id='3d-scene',figure=create_initial_scene(),style={'height': '80vh'},config={'scrollZoom': True})], className="graph-container"), html.Div([html.Div([html.Label("操作模式:", className="control-label"),dcc.RadioItems(id='operation-mode',options=[{'label': '移动/旋转整个空间', 'value': 'space'},{'label': '移动/旋转相机(长方体)', 'value': 'camera'}],value='camera',className="radio-group"),html.Div([html.Div([html.Label("相机位置 (X, Y, Z):", className="control-label"),html.Div([dcc.Input(id='camera-x-input',type='number',value=0.0,step=0.1,className="number-input"),dcc.Input(id='camera-y-input',type='number',value=0.0,step=0.1,className="number-input"),dcc.Input(id='camera-z-input',type='number',value=0.0,step=0.1,className="number-input")], className="input-group")]),], className="slider-group"),html.Div([html.Div([html.Label("相机请按Yaw[Z]->Pitch(Y)->Roll(X)的顺序旋转):", className="control-label"),html.Div([dcc.Input(id='camera-yaw-input',type='number',value=0.0,step=1,className="number-input"), dcc.Input(id='camera-pitch-input',type='number',value=0.0,step=1,className="number-input"), dcc.Input(id='camera-roll-input',type='number',value=0.0,step=1,className="number-input")], className="input-group")]),], className="slider-group"),html.Button("计算变换矩阵", id="calculate-btn", n_clicks=0,className="calculate-btn"),], className="controls"),html.Div([html.H3("变换矩阵结果", className="result-title"),html.Div(id='transformation-matrix', className="matrix-display"),html.Div(id='quaternion-display', className="quaternion-display")], className="results")], className="container")
])# 回调函数:更新3D场景
@dash_app.callback(Output('3d-scene', 'figure'),[Input('camera-x-input', 'value'),Input('camera-y-input', 'value'),Input('camera-z-input', 'value'),Input('camera-roll-input', 'value'),Input('camera-pitch-input', 'value'),Input('camera-yaw-input', 'value')]
)
def update_scene(x_input, y_input, z_input, roll_input, pitch_input, yaw_input):ctx = dash.callback_context# 判断哪个输入触发了回调if not ctx.triggered:trigger_id = Noneelse:trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]x = float(x_input) if x_input is not None else 0y = float(y_input) if y_input is not None else 0z = float(z_input) if z_input is not None else 0roll = float(roll_input) if roll_input is not None else 0pitch = float(pitch_input) if pitch_input is not None else 0yaw = float(yaw_input) if yaw_input is not None else 0# 创建基坐标系axes = create_coordinate_frame(scale=1.5, name="基坐标系")# 创建初始长方体 - 使用修复后的顶点定义box_vertices = create_box_vertices()# 修复2: 使用正确的旋转顺序 'zyx' (yaw->pitch->roll)# 并按照 [yaw, pitch, roll] 顺序传递角度值rotation = R.from_euler('zyx', [yaw, pitch, roll], degrees=True)rotated_vertices = rotation.apply(box_vertices)# 应用平移translated_vertices = rotated_vertices + np.array([x, y, z])# 创建变换后的长方体网格box_mesh = create_box_mesh(translated_vertices)# 添加相机位置标记camera_pos = go.Scatter3d(x=[x], y=[y], z=[z],mode='markers',marker=dict(size=5, color='gold'),name='相机中心')# 添加相机坐标系camera_axes = create_coordinate_frame(origin=[x, y, z], scale=0.7, name="相机坐标系")# 应用旋转到相机坐标系for axis in camera_axes:# 获取原始点orig_x = [axis.x[0], axis.x[1]]orig_y = [axis.y[0], axis.y[1]]orig_z = [axis.z[0], axis.z[1]]# 旋转点points = np.array([[orig_x[0], orig_y[0], orig_z[0]], [orig_x[1], orig_y[1], orig_z[1]]])rotated_points = rotation.apply(points - [x, y, z]) + [x, y, z]# 更新坐标axis.x = rotated_points[:, 0]axis.y = rotated_points[:, 1]axis.z = rotated_points[:, 2]# 组合所有图形元素data = axes + camera_axes + [box_mesh, camera_pos]# 创建3D场景布局layout = go.Layout(scene=dict(xaxis=dict(title='X (前)', range=[-2, 2]),yaxis=dict(title='Y (左)', range=[-2, 2]),zaxis=dict(title='Z (上)', range=[-2, 2]),aspectmode='cube',camera=dict(eye=dict(x=1.5, y=1.5, z=1.5))),margin=dict(l=0, r=0, b=0, t=0),showlegend=True,legend=dict(x=0, y=1))return go.Figure(data=data, layout=layout)def sync_inputs(x_input, y_input, z_input, roll_input, pitch_input, yaw_input):# 此函数未使用,但为了完整性保留return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]# 回调函数:计算变换矩阵
@dash_app.callback([Output('transformation-matrix', 'children'),Output('quaternion-display', 'children')],[Input('calculate-btn', 'n_clicks')],[State('camera-x-input', 'value'),State('camera-y-input', 'value'),State('camera-z-input', 'value'),State('camera-roll-input', 'value'),State('camera-pitch-input', 'value'),State('camera-yaw-input', 'value')]
)
def calculate_transformation(n_clicks, x, y, z, roll, pitch, yaw):if n_clicks == 0:return "", ""# 使用正确的旋转顺序 'zyx' (yaw->pitch->roll)# 并按照 [yaw, pitch, roll] 顺序传递角度值rotation = R.from_euler('zyx', [yaw, pitch, roll], degrees=True)rot_matrix = rotation.as_matrix()# 创建变换矩阵 (4x4)transformation_matrix = np.eye(4)transformation_matrix[:3, :3] = rot_matrixtransformation_matrix[:3, 3] = [x, y, z]# 获取四元数表示quaternion = rotation.as_quat() # [x, y, z, w]# 格式化矩阵显示matrix_html = html.Div([html.H4("4x4 变换矩阵:"),html.Table([html.Tr([html.Td(f"{transformation_matrix[0, 0]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[0, 1]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[0, 2]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[0, 3]:.4f}", className="matrix-cell")], className="matrix-row"),html.Tr([html.Td(f"{transformation_matrix[1, 0]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[1, 1]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[1, 2]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[1, 3]:.4f}", className="matrix-cell")], className="matrix-row"),html.Tr([html.Td(f"{transformation_matrix[2, 0]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[2, 1]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[2, 2]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[2, 3]:.4f}", className="matrix-cell")], className="matrix-row"),html.Tr([html.Td(f"{transformation_matrix[3, 0]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[3, 1]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[3, 2]:.4f}", className="matrix-cell"),html.Td(f"{transformation_matrix[3, 3]:.4f}", className="matrix-cell")], className="matrix-row")], className="matrix-table"),html.P("该矩阵将点从相机坐标系转换到基坐标系")])# 格式化四元数显示quaternion_html = html.Div([html.H4("四元数表示 (旋转部分):"),html.P(f"q = [{quaternion[3]:.6f}, {quaternion[0]:.6f}, {quaternion[1]:.6f}, {quaternion[2]:.6f}]", className="quaternion-formula"),html.P("(w, x, y, z) 格式"),html.Div([html.P("平移向量:"),html.P(f"t = [{x:.4f}, {y:.4f}, {z:.4f}]")], className="translation-display"),html.H4("应用说明:"),html.Ul([html.Li("四元数表示相机的旋转"),html.Li("平移向量表示相机在基坐标系中的位置"),html.Li("变换矩阵 = 旋转矩阵 × 平移矩阵")])])return matrix_html, quaternion_html# 添加CSS样式
dash_app.css.append_css({'external_url': ('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap','https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css')
})dash_app.css.append_css({'inline': open("stype.css").read()
})if __name__ == '__main__':dash_app.run(debug=False)
stype.css
:root {--primary-color: #1f77b4;--secondary-color: #ff7f0e;--dark-color: #2c3e50;--light-color: #ecf0f1;--success-color: #2ecc71;--danger-color: #e74c3c;}body {font-family: 'Roboto', sans-serif;margin: 0;padding: 0;background-color: #f5f7fa;color: #333;
}.banner {background: linear-gradient(135deg, var(--primary-color), #4a90e2);color: white;padding: 1.5rem;text-align: center;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}.header {margin: 0;font-weight: 500;
}.subtitle {margin: 0.5rem 0 0;opacity: 0.9;
}.container {display: flex;padding: 20px;max-width: 1800px;margin: 0 auto;
}.graph-container {flex: 3;background: white;border-radius: 8px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);overflow: hidden;margin-right: 20px;
}.controls {flex: 1;background: white;padding: 20px;border-radius: 8px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}.results {flex: 1;background: white;padding: 20px;border-radius: 8px;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);margin-left: 20px;
}.control-label {display: block;margin: 15px 0 8px;font-weight: 500;color: var(--dark-color);
}.radio-group {padding: 10px 0;border-bottom: 1px solid #eee;
}.slider-group {margin-bottom: 20px;padding-bottom: 15px;border-bottom: 1px solid #eee;
}.slider {margin: 15px 0;width: 100%;
}.input-group {display: flex;justify-content: space-between;margin-bottom: 10px;
}.number-input {width: 30%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;text-align: center;font-size: 14px;
}.number-input:focus {border-color: var(--primary-color);outline: none;box-shadow: 0 0 0 2px rgba(31, 119, 180, 0.2);
}.calculate-btn {background-color: var(--primary-color);color: white;border: none;padding: 12px 20px;border-radius: 4px;cursor: pointer;font-size: 16px;font-weight: 500;width: 100%;margin-top: 10px;transition: background-color 0.3s;
}.calculate-btn:hover {background-color: #1668a6;
}.result-title {color: var(--primary-color);margin-top: 0;border-bottom: 2px solid #eee;padding-bottom: 10px;
}.matrix-display, .quaternion-display {background-color: #f9f9f9;padding: 15px;border-radius: 4px;margin: 15px 0;font-family: monospace;
}.matrix-table {width: 100%;border-collapse: collapse;margin: 10px 0;
}.matrix-row {border: none;
}.matrix-cell {border: 1px solid #ddd;padding: 10px;text-align: center;background-color: white;width: 25%;
}.quaternion-formula {font-size: 16px;background-color: #f0f8ff;padding: 10px;border-radius: 4px;font-family: monospace;
}.translation-display {margin-top: 15px;padding: 10px;background-color: #f0f8ff;border-radius: 4px;font-family: monospace;
}@media (max-width: 1200px) {.container {flex-direction: column;}.graph-container {margin-right: 0;margin-bottom: 20px;}.results {margin-left: 0;margin-top: 20px;}
}
七、总结
这个交互式坐标系变换矩阵计算工具通过可视化界面简化了三维空间变换的计算