☰
DH参数转URDF:机械臂仿真模型转换与验证指南
2026/10/20 6:59:22 网站建设 项目流程

简介:DH2URDF 是一份面向机器人建模与仿真开发者的 JavaScript 工具资源,用于将 Denavit-Hartenberg 参数表转换为统一机器人描述格式(URDF)。它适合已掌握机器人运动学基础、需要在 Web 端快速验证 DH 建模结果的中高级学习者,解决手工编写 URDF 繁琐、易错的问题。压缩包共 9 个文件,约 138KB,包含 4 个 js 脚本、1 个 html 页面、1 个 urdf 示例、1 个 litcoffee 源码及 license、md 说明等,分别承担转换逻辑、可视化演示、机器人描述与文档说明等职责。转换结果会把各轴以 URDF 圆柱体形式放置在 DH 参考系上,便于进一步拼装完整机器人模型。配套演示网站支持查看 URDF 并调节关节值,方便直观核对建模效果。目前已有 1049 人学习下载,适合作为机器人建模入门与 DH 参数验证的轻量参考工具。

1. DH2URDF 到底在解决什么:从一张表到一台能仿真的机器人

如果你手上有机械臂的运动学参数表,却发现在仿真环境里怎么摆都不对,大概率是卡在了 DH 参数到 URDF 的转换这一步。DH2URDF 这个方向做的事很具体:把经典的 Denavit-Hartenberg 参数表,翻译成统一机器人描述格式(URDF),让 RViz、Gazebo、MuJoCo 这类工具能直接加载你的机器人模型。它解决的不是“怎么建模”的问题,而是“模型建好了,怎么让仿真器认”的问题。适合两类人:一类是做过运动学推导、手里有 DH 表但没写过 URDF 的算法工程师;另一类是在仿真里调机械臂、发现关节轴和实际对不上的集成工程师。核心痛点在于,DH 表描述的是相邻连杆坐标系之间的变换关系,而 URDF 描述的是每个连杆相对父连杆的位姿和关节轴方向,两者的“参考系”根本不在一个层面上。直接照抄 DH 表的 a、alpha、d、theta 填进 URDF,十有八九会翻车。

2. DH 参数和 URDF 的坐标系差异:为什么不能直接照抄

2.1 DH 表的四个参数各自锚定在哪个坐标系

标准 DH 参数里,a 是连杆长度,alpha 是连杆扭角,d 是连杆偏距,theta 是关节角。这四个参数描述的是从坐标系 i-1 到坐标系 i 的变换,顺序是绕 X 轴转 alpha、沿 X 轴移 a、绕 Z 轴转 theta、沿 Z 轴移 d。注意这个顺序,它决定了坐标系 i 的原点落在哪里。很多人推导时用的是改进 DH,顺序和锚定方式又不一样。问题在于,URDF 里每个 link 的视觉原点和惯性原点是你自己定的,而 joint 的 origin 描述的是子 link 坐标系相对父 link 坐标系的位姿。DH 表里的坐标系原点是按规则“长”出来的,不一定和你在三维软件里画的 link 原点重合。所以转换的第一步不是写代码,而是先把 DH 表对应的坐标系原点在脑子里或者纸上标出来。

2.2 URDF 的 joint origin 和 axis 到底在描述什么

URDF 里一个 revolute joint 需要三样东西:origin 的 xyz 和 rpy,以及 axis 向量。origin 的 xyz 是子 link 坐标系原点在父 link 坐标系里的位置,rpy 是子 link 坐标系相对父 link 坐标系的旋转。axis 是关节旋转轴在子 link 坐标系里的方向。这里有个容易混的点:DH 表里的关节旋转轴是 Z 轴,但那是 DH 坐标系自己的 Z 轴。到了 URDF,如果你把子 link 坐标系定义得和 DH 坐标系 i 完全一致,那 axis 就是 (0,0,1)。但如果你为了画图方便把 link 坐标系转了个方向,axis 就得跟着变。我一般会建议在转换时先让 URDF 的 link 坐标系和 DH 坐标系一一对应,等模型跑通了再调整视觉模型的原点。

2.3 从 DH 变换矩阵提取 URDF 的 origin 和 axis

假设你已经把 DH 表转成了每个关节的齐次变换矩阵 T_i,这个矩阵描述的是从坐标系 i-1 到坐标系 i 的变换。在 URDF 里,如果你让 link i 的坐标系就是 DH 坐标系 i,那么 joint i 的 origin 就是 T_i 的位置部分和旋转部分,axis 就是 (0,0,1)。但这里有个细节:URDF 的 joint origin 是父 link 到子 link 的变换,而 DH 的 T_i 也是从 i-1 到 i 的变换,两者在数学上是一致的。所以你可以直接把 T_i 分解成 xyz 和 rpy。分解旋转矩阵到 rpy 时要注意万向锁,一般用 ZYX 顺序的欧拉角,对应 URDF 的 rpy 定义。下面这段 Python 代码演示了从 DH 参数构造变换矩阵并提取 URDF origin 的过程。

import numpy as np from scipy.spatial.transform import Rotation as R def dh_to_matrix(a, alpha, d, theta): """标准 DH 参数转齐次变换矩阵""" ct, st = np.cos(theta), np.sin(theta) ca, sa = np.cos(alpha), np.sin(alpha) return np.array([ [ct, -st*ca, st*sa, a*ct], [st, ct*ca, -ct*sa, a*st], [0, sa, ca, d], [0, 0, 0, 1] ]) def matrix_to_urdf_origin(T): """从变换矩阵提取 URDF 的 xyz 和 rpy""" xyz = T[:3, 3] rot = R.from_matrix(T[:3, :3]) rpy = rot.as_euler('xyz') # URDF 默认 rpy 是绕固定轴 X-Y-Z return xyz, rpy # 示例:一个 6 轴机械臂的 DH 表,每行是 a, alpha, d, theta dh_table = [ (0, np.pi/2, 0.1, 0), (0.5, 0, 0, 0), (0.5, 0, 0, 0), (0, np.pi/2, 0.2, 0), (0, -np.pi/2, 0, 0), (0, 0, 0.1, 0), ] for i, (a, alpha, d, theta) in enumerate(dh_table): T = dh_to_matrix(a, alpha, d, theta) xyz, rpy = matrix_to_urdf_origin(T) print(f"Joint {i+1}: xyz={xyz}, rpy={rpy}")

这段代码的逻辑很直接:先把每个关节的 DH 参数转成 4x4 齐次变换矩阵,再从矩阵里拿平移向量和旋转矩阵。旋转矩阵转欧拉角时用的是xyz顺序,对应 URDF 里 rpy 的定义,也就是先绕 X 转、再绕 Y 转、最后绕 Z 转。注意scipy的as_euler('xyz')返回的是 extrinsic 旋转,和 URDF 的 rpy 语义一致。如果你用的 DH 表是改进型,变换矩阵的构造方式会不同,但提取 origin 的逻辑不变。参数方面,a和d的单位要统一,一般用米;alpha和theta用弧度。如果你的 DH 表角度是度,记得先转弧度再传进去。

3. 手写一个 DH2URDF 转换脚本:从参数表到 .urdf 文件

3.1 定义输入格式和 link 命名规则

输入最好是一张 CSV 或者一个 Python 列表,每行包含关节名、a、alpha、d、theta、关节类型、关节限位。命名规则我习惯用link_0到link_n,joint_1到joint_n,base link 叫base_link。这样在 RViz 里一眼就能看出层级关系。如果你是从 SolidWorks 或者别的软件导出的 DH 表,注意它的关节顺序和你的命名要对应上,否则后面查错很痛苦。下面是一个输入表的示例,用 CSV 格式存,方便非程序员改参数。

joint_name,a,alpha,d,theta,type,lower,upper,effort,velocity joint_1,0,1.5708,0.1,0,revolute,-3.14,3.14,100,1.0 joint_2,0.5,0,0,0,revolute,-1.57,1.57,100,1.0 joint_3,0.5,0,0,0,revolute,-1.57,1.57,100,1.0 joint_4,0,1.5708,0.2,0,revolute,-3.14,3.14,100,1.0 joint_5,0,-1.5708,0,0,revolute,-1.57,1.57,100,1.0 joint_6,0,0,0.1,0,revolute,-3.14,3.14,100,1.0

这个表里alpha和theta都是弧度,d和a是米。type目前只考虑 revolute,如果你有 prismatic 关节,axis 还是 (0,0,1),但 origin 的提取方式一样,只是关节运动方向沿 Z 轴平移。限位和动力学参数先随便填,后面可以在 URDF 里改。注意theta这一列在标准 DH 里是关节变量,对于 revolute 关节,它对应 URDF 里的关节角,但 URDF 不需要你填这个,因为关节角是运行时变量。这里填 0 只是占位。

3.2 用 Python 生成 URDF 的完整代码

下面这个脚本读 CSV,生成 URDF 字符串,并写入文件。它处理了 base_link 到第一个 link 的固定关节,以及后续的 revolute 关节。每个 link 的视觉和碰撞模型先用一个简单的圆柱代替,惯性矩阵用单位矩阵占位,保证 URDF 能加载。

import csv import numpy as np from scipy.spatial.transform import Rotation as R def dh_to_matrix(a, alpha, d, theta): ct, st = np.cos(theta), np.sin(theta) ca, sa = np.cos(alpha), np.sin(alpha) return np.array([ [ct, -st*ca, st*sa, a*ct], [st, ct*ca, -ct*sa, a*st], [0, sa, ca, d], [0, 0, 0, 1] ]) def matrix_to_origin(T): xyz = T[:3, 3] rpy = R.from_matrix(T[:3, :3]).as_euler('xyz') return xyz, rpy def generate_urdf(csv_path, output_path): with open(csv_path, 'r') as f: reader = csv.DictReader(f) rows = list(reader) urdf = ['<?xml version="1.0"?>', '<robot name="dh_robot">'] urdf.append(' <link name="base_link"/>') prev_link = 'base_link' for i, row in enumerate(rows): joint_name = row['joint_name'] link_name = f'link_{i+1}' a = float(row['a']) alpha = float(row['alpha']) d = float(row['d']) theta = float(row['theta']) lower = float(row['lower']) upper = float(row['upper']) effort = float(row['effort']) velocity = float(row['velocity']) T = dh_to_matrix(a, alpha, d, theta) xyz, rpy = matrix_to_origin(T) # 添加 link,视觉和碰撞用简单圆柱占位 urdf.append(f' <link name="{link_name}">') urdf.append(' <visual>') urdf.append(' <geometry><cylinder length="0.1" radius="0.05"/></geometry>') urdf.append(' </visual>') urdf.append(' <collision>') urdf.append(' <geometry><cylinder length="0.1" radius="0.05"/></geometry>') urdf.append(' </collision>') urdf.append(' <inertial>') urdf.append(' <mass value="1.0"/>') urdf.append(' <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/>') urdf.append(' </inertial>') urdf.append(' </link>') # 添加 joint urdf.append(f' <joint name="{joint_name}" type="revolute">') urdf.append(f' <parent link="{prev_link}"/>') urdf.append(f' <child link="{link_name}"/>') urdf.append(f' <origin xyz="{xyz[0]:.6f} {xyz[1]:.6f} {xyz[2]:.6f}" ' f'rpy="{rpy[0]:.6f} {rpy[1]:.6f} {rpy[2]:.6f}"/>') urdf.append(' <axis xyz="0 0 1"/>') urdf.append(f' <limit lower="{lower}" upper="{upper}" effort="{effort}" velocity="{velocity}"/>') urdf.append(' </joint>') prev_link = link_name urdf.append('</robot>') with open(output_path, 'w') as f: f.write('\n'.join(urdf)) print(f'URDF written to {output_path}') if __name__ == '__main__': generate_urdf('dh_table.csv', 'robot.urdf')

代码的逻辑分三步:读 CSV、对每行算变换矩阵并提取 origin、拼 XML 字符串。关键参数是xyz和rpy的格式化,保留 6 位小数足够用。axis固定为 (0,0,1),因为我们的 link 坐标系和 DH 坐标系对齐。limit里的effort和velocity先填了 100 和 1.0,实际用的时候要根据电机参数改。惯性矩阵用了对角 0.01,质量 1.0,这是为了让 URDF 能通过检查,真正做动力学仿真时得替换成实际值。视觉和碰撞模型用圆柱占位,长度 0.1 半径 0.05,你可以在 CAD 里导出 STL 后替换 geometry 标签。

3.3 在 RViz 里验证模型是否对齐

生成 URDF 后,用urdf_tutorial或者自己写一个 launch 文件在 RViz 里加载。先看关节轴方向对不对:在 RViz 里添加 RobotModel,把每个 joint 的 axis 显示出来,和你在 DH 表里推导的 Z 轴方向对比。如果某个关节轴反了,大概率是 alpha 的符号或者 theta 的初始值搞错了。再看 link 之间的相对位置,如果末端执行器的位置和正运动学算出来的差很多,检查d和a是不是填错了列。我一般会先用一个 2 轴或者 3 轴的简单例子验证脚本,确认无误后再上 6 轴。RViz 里还可以用joint_state_publisher_gui拖动关节,看运动是否符合预期。如果拖动时某个关节绕的轴不对,回去检查axis和origin的 rpy。

4. 避坑与排查:DH2URDF 转换中最容易翻车的五个地方

4.1 现象:RViz 里模型直接散架,link 飞到奇怪的位置

原因通常是origin的 rpy 顺序搞错了。URDF 的 rpy 是绕固定轴 X-Y-Z 旋转,等价于旋转矩阵 R = Rz(yaw) * Ry(pitch) * Rx(roll)。如果你用scipy的as_euler('xyz'),它默认返回的是 extrinsic 旋转,顺序就是 X 然后 Y 然后 Z,和 URDF 一致。但如果你用了as_euler('XYZ')大写,那是 intrinsic 旋转,结果会完全不同。解决方法是统一用小写xyz,并且在代码里加一行打印旋转矩阵和 rpy 互转的结果,确认一致性。

4.2 现象:关节旋转方向反了,正运动学对不上

原因可能是 DH 表的 alpha 符号和你的直觉相反,或者 theta 的零位定义不同。标准 DH 里 alpha 是绕 X 轴从 Z_{i-1} 转到 Z_i,如果你在 URDF 里把 axis 设成 (0,0,1) 但 origin 的 rpy 没对应上,旋转方向就会反。解决方法是先在 RViz 里单独显示每个 joint 的 axis,和 DH 表推导的 Z 轴画在一起对比。如果反了,把 axis 改成 (0,0,-1) 或者把 rpy 里的 roll 加 pi。我一般会写一个小的 Python 脚本,用正运动学算末端位置,再在 RViz 里读末端 link 的位姿,两者对比。

4.3 现象:URDF 加载报错,提示 inertia 矩阵不是正定

原因是你填的惯性矩阵不满足正定条件,比如 ixx + iyy < izz 之类的。URDF 对惯性矩阵有物理约束,不能随便填。解决方法是先用一个满足三角不等式的对角矩阵,比如 ixx=0.01, iyy=0.01, izz=0.01,这样肯定正定。等模型能加载了,再根据实际质量分布改。如果你从 CAD 导出了惯性张量,注意它的参考坐标系是质心,而 URDF 的 inertial origin 默认在 link 原点,需要做平行轴定理转换。

4.4 现象:Gazebo 里机器人直接掉下去或者抖动

原因可能是质量设得太小或者太大,和关节的 effort、velocity 不匹配。Gazebo 的物理引擎对质量比敏感,如果 base_link 没有固定,整个机器人会自由落体。解决方法是在 URDF 里加一个gazebo标签把 base_link 固定,或者用fixedjoint 把 base_link 连到 world。另外检查effort和velocity是否够大,太小的话关节拖不动。我一般会把 effort 设成 1000,velocity 设成 10,先让机器人能动,再慢慢调小。

4.5 现象:改进 DH 和标准 DH 混用,结果完全不对

原因是你手里的表可能是改进 DH(也叫 Craig DH),它的变换顺序是绕 Z 转 theta、沿 Z 移 d、绕 X 转 alpha、沿 X 移 a。和标准 DH 的顺序不同,导致变换矩阵不一样。解决方法是先确认你的表是哪种 DH。标准 DH 的 a 和 alpha 下标是 i,改进 DH 的 a 和 alpha 下标是 i-1。如果你不确定,用正运动学验证:随便给一组关节角,用你的 DH 表算末端位置,再用 URDF 在 RViz 里读末端位置,对不上就换另一种 DH 的矩阵构造方式。

5. 进阶技巧:用 xacro 参数化 DH 表并做批量验证

5.1 把 DH 表写成 xacro 属性,方便改参数

URDF 本身不支持变量,但 xacro 可以。你可以把 DH 表的每一列定义成 xacro 属性,然后在 joint 里引用。这样改一个参数,整个模型自动更新。下面是一个 xacro 的片段,展示了如何把 a、alpha、d 定义成属性,并在 origin 里用三角函数计算。

<?xml version="1.0"?> <robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="dh_robot"> <xacro:property name="a1" value="0"/> <xacro:property name="alpha1" value="1.5708"/> <xacro:property name="d1" value="0.1"/> <xacro:property name="theta1" value="0"/> <link name="base_link"/> <link name="link_1"/> <joint name="joint_1" type="revolute"> <parent link="base_link"/> <child link="link_1"/> <origin xyz="${a1*cos(theta1)} ${a1*sin(theta1)} ${d1}" rpy="${alpha1} 0 ${theta1}"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="100" velocity="1.0"/> </joint> </robot>

注意这里的 origin 计算是简化版,只适用于 alpha 和 theta 的特定组合。真正通用的做法是在 xacro 里写一个宏,接收 a、alpha、d、theta,内部用 Python 表达式算矩阵再提取 rpy。但 xacro 的数学函数有限,复杂计算还是建议用 Python 脚本生成 URDF,xacro 只用来做参数管理。我一般会用 Python 生成一个包含所有属性的 xacro 文件,然后在 launch 里用xacro命令转成 URDF。

5.2 用正运动学做批量验证的脚本

生成 URDF 后,怎么确认它和 DH 表等价?最可靠的方法是用随机关节角做批量验证。下面这个脚本随机生成 100 组关节角,分别用 DH 表和 URDF 的正运动学算末端位置,比较误差。

import numpy as np from scipy.spatial.transform import Rotation as R def dh_forward_kinematics(dh_table, joint_angles): """用 DH 表算正运动学""" T = np.eye(4) for (a, alpha, d, theta), q in zip(dh_table, joint_angles): theta_total = theta + q ct, st = np.cos(theta_total), np.sin(theta_total) ca, sa = np.cos(alpha), np.sin(alpha) T_i = np.array([ [ct, -st*ca, st*sa, a*ct], [st, ct*ca, -ct*sa, a*st], [0, sa, ca, d], [0, 0, 0, 1] ]) T = T @ T_i return T def urdf_forward_kinematics(urdf_origins, joint_angles): """用 URDF 的 origin 和 axis 算正运动学,假设 axis 都是 (0,0,1)""" T = np.eye(4) for (xyz, rpy), q in zip(urdf_origins, joint_angles): T_origin = np.eye(4) T_origin[:3, :3] = R.from_euler('xyz', rpy).as_matrix() T_origin[:3, 3] = xyz T_joint = np.eye(4) T_joint[:3, :3] = R.from_rotvec([0, 0, q]).as_matrix() T = T @ T_origin @ T_joint return T # 假设 dh_table 和 urdf_origins 已经从上一步得到 dh_table = [ (0, 1.5708, 0.1, 0), (0.5, 0, 0, 0), (0.5, 0, 0, 0), (0, 1.5708, 0.2, 0), (0, -1.5708, 0, 0), (0, 0, 0.1, 0), ] urdf_origins = [] for a, alpha, d, theta in dh_table: ct, st = np.cos(theta), np.sin(theta) ca, sa = np.cos(alpha), np.sin(alpha) T = np.array([ [ct, -st*ca, st*sa, a*ct], [st, ct*ca, -ct*sa, a*st], [0, sa, ca, d], [0, 0, 0, 1] ]) xyz = T[:3, 3] rpy = R.from_matrix(T[:3, :3]).as_euler('xyz') urdf_origins.append((xyz, rpy)) np.random.seed(42) max_error = 0 for _ in range(100): q = np.random.uniform(-1.5, 1.5, 6) T_dh = dh_forward_kinematics(dh_table, q) T_urdf = urdf_forward_kinematics(urdf_origins, q) error = np.linalg.norm(T_dh[:3, 3] - T_urdf[:3, 3]) max_error = max(max_error, error) print(f'Max position error over 100 random configs: {max_error:.6f} m')

这个脚本的核心是对比两种正运动学的末端位置。dh_forward_kinematics直接按 DH 参数连乘变换矩阵,urdf_forward_kinematics按 URDF 的 origin 和 axis 连乘。如果两者误差在 1e-6 米以内,说明转换正确。注意urdf_forward_kinematics里假设了所有关节的 axis 都是 (0,0,1),如果你的 URDF 里 axis 不同,需要把R.from_rotvec([0,0,q])换成绕实际 axis 的旋转。这个验证方法比在 RViz 里肉眼看靠谱得多,而且可以集成到 CI 里,每次改 DH 表自动跑一遍。

5.3 一个我常犯的错误:忘了 base_link 的固定关节

最后说一个血泪教训。我刚开始做 DH2URDF 的时候,生成的 URDF 里 base_link 和 link_1 之间直接用了 revolute joint,结果在 Gazebo 里整个机器人像陀螺一样乱转。后来才明白,base_link 应该固定在世界坐标系里,或者至少用一个 fixed joint 连到一个 world link。如果你不需要机器人移动,就在 URDF 里加一个worldlink,然后用 fixed joint 把 base_link 连上去。这个 fixed joint 的 origin 可以设成全零,不影响运动学。另外,如果你用的是 MoveIt,它默认会找 base_link,所以 base_link 的名字别乱改。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询