简介:DH2URDF 是一份将 Denavit-Hartenberg 参数表转换为统一机器人描述格式(URDF)的 JavaScript 工具资源,面向机器人建模、运动学仿真与 ROS 学习方向的开发者及学生。它解决的是手工编写 URDF 时 DH 参数到关节坐标系映射繁琐、易出错的问题,转换结果会把各轴以 URDF 圆柱体形式放置在 DH 参考系上,便于进一步搭建完整机器人模型。压缩包共 9 个文件,约 138KB,包含 4 个 js 脚本、1 个 html 演示页、1 个 urdf 模板、1 个 litcoffee 源码及 license、md 说明等,脚本负责参数解析与格式转换,网页用于可视化查看 URDF 并调整关节值。目前已有 1049 人学习下载。读者可借此快速验证 DH 表与 URDF 的对应关系,理解关节轴布置,并在此基础上扩展出可用的机器人描述文件。
1. 从一张 DH 参数表到能在仿真里动起来的机器人:DH2URDF 到底解决什么问题
如果你手头有一台机械臂的运动学参数,通常拿到的是一张 Denavit-Hartenberg 表——四个参数(连杆长度、连杆扭转、关节偏移、关节转角)排成几行,看着清爽,但没法直接丢进仿真环境里。Gazebo、PyBullet、RViz 这些工具要的是 URDF 或 Xacro 文件,里面得写清楚 link、joint、visual、collision、inertial 一大堆标签。手工把 DH 表翻译成 URDF,关节少的时候还能忍,六轴、七轴一上来,坐标系方向、轴向正负、单位换算,错一个数整条链就歪了。
DH2URDF 就是干这件事的:输入一张 DH 参数表,输出一份结构完整、能直接加载的 URDF。它用 JavaScript 写成,意味着你可以在浏览器里跑,也可以挂在 Node.js 脚本里批量生成。适合两类人:一类是刚拿到机械臂参数、想快速在仿真里验证运动学的开发者;另一类是要把多台不同构型机械臂统一成 URDF 资产、做批量管理的工程团队。这篇就按「它怎么把表变成文件 → 怎么跑起来 → 怎么改造成你要的样子」拆一遍。
2. 核心转换逻辑:DH 参数怎么一步步变成 URDF 的 link 和 joint
2.1 标准 DH 与改进 DH 的区分是第一步
DH 参数有两种约定:标准 DH(Standard DH)和改进 DH(Modified DH)。两者区别在于坐标系挂在连杆的哪一端,导致变换矩阵的乘法顺序不同。标准 DH 的变换是Rz(θ)·Tz(d)·Tx(a)·Rx(α),改进 DH 则是Rx(α)·Tx(a)·Rz(θ)·Tz(d)。这个顺序差异直接决定了 URDF 里 joint 的 origin 怎么算。
DH2URDF 在入口处会要求你指定用的是哪种约定。常见做法是给一个dhType参数,取值'standard'或'modified'。如果你拿到的表没标注,一个快速判断方法是:看第一行的 α 和 a 是不是零——标准 DH 的第一组通常从基座坐标系出发,改进 DH 则把基座变换单独拆出来。搞错这个,后面所有 joint 的位置都会偏。
// 根据 DH 类型选择变换矩阵的构造顺序 function buildTransform(dhType, theta, d, a, alpha) { if (dhType === 'standard') { // 标准 DH: Rz(theta) * Tz(d) * Tx(a) * Rx(alpha) return multiply( rotationZ(theta), multiply(translationZ(d), multiply(translationX(a), rotationX(alpha))) ); } else { // 改进 DH: Rx(alpha) * Tx(a) * Rz(theta) * Tz(d) return multiply( rotationX(alpha), multiply(translationX(a), multiply(rotationZ(theta), translationZ(d))) ); } }这段代码是整个转换的地基。theta是关节变量(转动关节)或固定值(移动关节),d是连杆偏移,a是连杆长度,alpha是连杆扭转角。四个参数各对应一个基本变换矩阵,乘起来得到相邻坐标系之间的齐次变换矩阵。注意multiply的顺序不能交换,矩阵乘法不满足交换律,顺序错了坐标系就翻了。
2.2 从变换矩阵提取 URDF joint 的 origin
URDF 里每个 joint 有一个<origin>标签,包含xyz和rpy两部分,描述子坐标系相对于父坐标系的位姿。DH 变换矩阵算出来是一个 4×4 齐次矩阵,左上角 3×3 是旋转,右上角 3×1 是平移。平移直接取xyz,旋转部分要转成 roll-pitch-yaw 欧拉角。
// 从 4x4 齐次矩阵提取 URDF origin 的 xyz 和 rpy function extractOrigin(transformMatrix) { // 平移部分直接取第四列前三行 const xyz = [ transformMatrix[0][3], transformMatrix[1][3], transformMatrix[2][3] ]; // 旋转部分转欧拉角 (RPY, 固定轴 XYZ 顺序) const R = [ [transformMatrix[0][0], transformMatrix[0][1], transformMatrix[0][2]], [transformMatrix[1][0], transformMatrix[1][1], transformMatrix[1][2]], [transformMatrix[2][0], transformMatrix[2][1], transformMatrix[2][2]] ]; const pitch = Math.atan2(-R[2][0], Math.sqrt(R[0][0]**2 + R[1][0]**2)); const roll = Math.atan2(R[2][1], R[2][2]); const yaw = Math.atan2(R[1][0], R[0][0]); return { xyz, rpy: [roll, pitch, yaw] }; }这里有个容易翻车的地方:欧拉角的提取在万向节锁附近(pitch 接近 ±90°)会出现奇异,atan2的分母趋近零。常见做法是加一个判断,当|R[2][0]|接近 1 时用另一组公式回退。另外 URDF 的 rpy 是固定轴 XYZ 顺序,不是 ZYX,别搞反了。
2.3 生成 URDF 的 link 和 joint 骨架
每个 DH 行对应一个 joint 和一个子 link。基座单独作为一个 base_link,末端加一个 tool0 或 ee_link。joint 类型根据关节是转动还是移动来定,转动关节用revolute或continuous,移动关节用prismatic。
// 根据 DH 表生成 URDF 的 link 和 joint 数组 function generateURDF(dhTable, dhType, robotName) { const links = []; const joints = []; // 基座 link links.push({ name: 'base_link', visual: null }); dhTable.forEach((row, i) => { const parentLink = i === 0 ? 'base_link' : `link_${i}`; const childLink = `link_${i + 1}`; const jointName = `joint_${i + 1}`; // 计算变换矩阵并提取 origin const T = buildTransform(dhType, row.theta, row.d, row.a, row.alpha); const { xyz, rpy } = extractOrigin(T); // 关节类型:转动或移动 const jointType = row.jointType === 'prismatic' ? 'prismatic' : 'revolute'; joints.push({ name: jointName, type: jointType, parent: parentLink, child: childLink, origin: { xyz, rpy }, axis: row.axis || [0, 0, 1], // 默认绕 Z 轴 limit: row.limit || { lower: -Math.PI, upper: Math.PI, effort: 100, velocity: 1.0 } }); links.push({ name: childLink, visual: null }); }); return { robotName, links, joints }; }这段逻辑把 DH 表的每一行映射成一对 link-joint。axis默认是[0,0,1],因为 DH 约定里关节变量绕 Z 轴。如果你的机械臂某个关节轴向不同,需要在表里额外标注。limit对revolute类型是必须的,continuous类型可以省略,但很多仿真器对continuous支持不一致,我一般统一用revolute加 ±π 的范围,省得后面出玄学问题。
3. 跑起来:从安装依赖到生成第一份 URDF 文件
3.1 环境准备和项目结构
DH2URDF 是 JavaScript 项目,运行需要 Node.js 环境。建议用 Node 16 以上,低版本对 ES Module 的支持不完整,容易在import语句上报错。项目结构大致如下:
dh2urdf/ ├── src/ │ ├── index.js # 入口,暴露 convert 接口 │ ├── transform.js # DH 变换矩阵构造 │ ├── urdf-builder.js # URDF 标签拼装 │ └── utils.js # 矩阵运算、欧拉角转换 ├── examples/ │ ├── puma560.json # 示例 DH 表 │ └── ur5.json ├── package.json └── README.mdsrc/下按职责拆成四个文件,transform.js只管矩阵,urdf-builder.js只管 XML 拼装,index.js串起来。这种拆法方便你只替换其中一层,比如想把输出格式从 URDF 换成 Xacro,只改urdf-builder.js就行。
3.2 安装和运行命令
# 克隆项目后进入目录 cd dh2urdf # 安装依赖(主要是矩阵运算库和 XML 构建库) npm install # 用示例 DH 表生成 URDF node src/index.js --input examples/puma560.json --output puma560.urdf --type standard # 如果想在浏览器里交互式使用 npm run dev--input指定 DH 表的 JSON 文件,--output是生成的 URDF 路径,--type指定标准还是改进 DH。npm run dev会启动一个本地开发服务器,打开浏览器能看到一个简单的表单界面,手动输入 DH 参数后点生成,右侧实时预览 URDF 内容。这个交互模式对调试参数特别有用,改一个数就能看到 joint origin 的变化。
3.3 DH 表的 JSON 格式和参数含义
输入文件是一个 JSON 数组,每行对应一个关节:
{ "name": "puma560", "dhType": "standard", "joints": [ { "theta": 0, "d": 0, "a": 0, "alpha": 1.5708, "jointType": "revolute" }, { "theta": 0, "d": 0, "a": 0.4318, "alpha": 0, "jointType": "revolute" }, { "theta": 0, "d": 0.15005,"a": 0.0203, "alpha": -1.5708,"jointType": "revolute" }, { "theta": 0, "d": 0.4318, "a": 0, "alpha": 1.5708, "jointType": "revolute" }, { "theta": 0, "d": 0, "a": 0, "alpha": -1.5708,"jointType": "revolute" }, { "theta": 0, "d": 0, "a": 0, "alpha": 0, "jointType": "revolute" } ] }theta对转动关节是变量,初始值填 0 就行,URDF 里不会固定它。d和a是长度,单位是米,如果你拿到的表是毫米,记得先除以 1000。alpha是弧度,不是角度,从角度转弧度乘Math.PI/180。jointType不填默认revolute。这个 JSON 结构比 CSV 啰嗦一点,但好处是可以带元信息,比如name和dhType,生成 URDF 时直接读,不用在命令行再传一遍。
3.4 验证生成的 URDF 能不能用
生成完别急着往仿真器里丢,先做两步检查。第一步用check_urdf工具(ROS 自带)验证语法:
check_urdf puma560.urdf如果输出里能看到Successfully Parsed XML和完整的关节树,说明结构没问题。第二步在 RViz 或 PyBullet 里加载看一眼:
import pybullet as p p.connect(p.GUI) robot_id = p.loadURDF("puma560.urdf") # 打印关节数量和名称,确认和 DH 表对得上 for i in range(p.getNumJoints(robot_id)): print(p.getJointInfo(robot_id, i)[1])PyBullet 加载后如果机械臂是散架的或者关节位置明显不对,大概率是 DH 类型选错了,或者alpha的正负号搞反了。这时候回到交互模式,逐个关节对比 origin 的 xyz 值,和手算的结果对一对,很快能定位到哪一行出了问题。
4. 避坑与排查:DH 转 URDF 时最容易翻车的五个地方
4.1 生成的机械臂在仿真里「散架」了
现象:URDF 加载后各个 link 之间没有连接,或者连接位置完全不对,看起来像一堆漂浮的零件。
原因:最常见的是 DH 类型选错。标准 DH 和改进 DH 的变换顺序不同,如果表是按改进 DH 给的,你按标准 DH 算,每个 joint 的 origin 都会偏。另一个可能是alpha的正负号搞反了,DH 表里 α 的正负取决于坐标系旋转方向,不同教材的约定可能相反。
解决:先确认 DH 表的来源和约定,在--type参数里选对。如果表里没标注,试着两种都生成一遍,在 RViz 里对比哪个看起来像正常的机械臂。α 的正负号可以逐个关节试,但更靠谱的方法是找一台已知正确 URDF 的机械臂,用同样的 DH 表跑一遍,对比 joint origin 的数值。
4.2 关节转动的轴向不对
现象:仿真里给关节发指令,机械臂往奇怪的方向动,或者两个关节绕同一个轴转。
原因:URDF joint 的axis默认是[0,0,1],但有些机械臂的关节轴向在 DH 坐标系里不是 Z 轴。另外如果 DH 表的坐标系定义和 URDF 的坐标系朝向不一致,也会导致轴向错位。
解决:在 DH 表的 JSON 里给每个关节显式指定axis。判断方法是在 RViz 里显示 joint 的坐标系,看 Z 轴指向和实际关节转动轴是否一致。如果不一致,把axis改成对应的方向向量。注意axis是在 joint 的父坐标系下表达的,不是世界坐标系。
4.3 单位换算导致机械臂尺寸差了一个数量级
现象:URDF 加载后机械臂要么巨大无比,要么小得看不见。
原因:DH 表里的长度单位是毫米,但 URDF 默认用米。直接填进去就差了 1000 倍。
解决:在生成前统一换算。我一般会在index.js里加一个unitScale参数,默认 1.0,如果输入是毫米就传 0.001。这样不用手动改表里的每个数,也避免漏改。角度同理,DH 表常用度,URDF 用弧度,加一个angleUnit参数统一处理。
4.4 欧拉角提取在特定姿态下出现 NaN
现象:某些 DH 参数组合下,生成的 URDF 里 joint origin 的 rpy 出现NaN。
原因:欧拉角提取时pitch接近 ±90°,atan2的分母趋近零,浮点运算出现奇异。这是万向节锁问题,不是代码 bug。
解决:在extractOrigin里加一个阈值判断,当|R[2][0]|大于 0.9999 时,用另一组回退公式:
if (Math.abs(R[2][0]) > 0.9999) { // 奇异位置的回退处理 const pitch = Math.sign(-R[2][0]) * Math.PI / 2; const roll = 0; const yaw = Math.atan2(-R[0][1], R[1][1]); return { xyz, rpy: [roll, pitch, yaw] }; }这个回退不保证唯一解,但至少不会出 NaN,仿真器能正常加载。
4.5 生成的 URDF 缺少 inertial 导致仿真器报错
现象:URDF 在 RViz 里能显示,但丢进 Gazebo 就报错,提示 link 缺少 inertial 或 mass 为零。
原因:DH2URDF 默认只生成 visual 和 collision 的几何占位,不包含质量、惯性矩阵这些动力学参数。Gazebo 做物理仿真时必须有 inertial。
解决:在 URDF 生成后手动补 inertial,或者给 DH2URDF 加一个扩展,从外部 JSON 读入每个 link 的质量和惯性张量。常见做法是先用简单几何体(圆柱、长方体)估算惯性,验证运动学没问题后再换成精确值。如果只是做运动学可视化,RViz 不需要 inertial,可以不管。
5. 二次开发:把 DH2URDF 改造成你自己的 URDF 生成流水线
5.1 扩展输出格式:从 URDF 到 Xacro
URDF 写死了所有参数,改一个尺寸要重新生成。Xacro 支持变量和宏,更适合做参数化模型。改造思路是在urdf-builder.js里加一个format选项,当format === 'xacro'时,把数值替换成${prefix}_link_length这样的变量,并在文件头部生成<xacro:property>定义。
// 在 urdf-builder.js 里增加 Xacro 输出分支 function buildJointXML(joint, format) { const originXYZ = format === 'xacro' ? `\${${joint.name}_x} \${${joint.name}_y} \${joint.name}_z}` : joint.origin.xyz.join(' '); return `<joint name="${joint.name}" type="${joint.type}"> <origin xyz="${originXYZ}" rpy="${joint.origin.rpy.join(' ')}"/> <parent link="${joint.parent}"/> <child link="${joint.child}"/> <axis xyz="${joint.axis.join(' ')}"/> </joint>`; }这样生成的 Xacro 文件可以在 launch 文件里通过<xacro:arg>覆盖参数,不用重新跑生成器。对于要做多构型机械臂管理的场景,这个改造很值。
5.2 批量生成:从单文件到多机器人资产库
如果你手头有十几台机械臂的 DH 表,一台台跑命令行太低效。我一般会写一个批处理脚本,遍历目录下所有 JSON 文件,逐个生成 URDF 并输出到output/目录:
// batch-convert.js const fs = require('fs'); const path = require('path'); const { convert } = require('./src/index'); const inputDir = './dh-tables'; const outputDir = './output'; fs.readdirSync(inputDir) .filter(f => f.endsWith('.json')) .forEach(file => { const dhTable = JSON.parse(fs.readFileSync(path.join(inputDir, file))); const urdf = convert(dhTable); const outName = file.replace('.json', '.urdf'); fs.writeFileSync(path.join(outputDir, outName), urdf); console.log(`Generated ${outName}`); });这个脚本跑完,output/下就是一套完整的 URDF 资产。配合 CI 流程,每次 DH 表更新自动重新生成,省去手工同步的麻烦。
5.3 验证改造结果:三步确认法
改完代码别直接往仿真器里丢,我习惯走三步确认。第一步跑单元测试,对比改造前后同一组 DH 表生成的 joint origin 数值是否一致,确保重构没引入偏差。第二步用check_urdf验证语法,Xacro 的话先xacro展开再 check。第三步在 PyBullet 里加载,用getJointInfo打印每个关节的轴向和位置,和 DH 表手算的结果逐行对。
# 验证脚本:对比 URDF 加载后的关节位置和 DH 表计算值 import pybullet as p import numpy as np p.connect(p.DIRECT) robot_id = p.loadURDF("output/puma560.urdf") for i in range(p.getNumJoints(robot_id)): info = p.getJointInfo(robot_id, i) joint_name = info[1].decode() joint_pos = info[14] # 关节在父坐标系中的位置 print(f"{joint_name}: pos={joint_pos}")如果某个关节的位置和手算差得离谱,回到transform.js里加日志,把中间矩阵打出来,逐项对比。矩阵运算的 bug 肉眼很难看出来,但把中间结果打出来和手算对一遍,基本都能定位。
5.4 一个我踩过的坑
早期我为了省事,直接在extractOrigin里用了一个第三方库的matrixToEuler函数,没注意它默认输出的是 ZYX 顺序,而 URDF 要的是 XYZ。结果生成的 URDF 在 RViz 里看着还行,一进 Gazebo 做动力学仿真,机械臂就疯狂抖动。排查了大半天,最后把欧拉角顺序改过来才正常。从那以后我每次改完转换逻辑,都强制走一遍「手算一个关节的 rpy → 和代码输出对比 → 再进仿真」的流程,不再偷懒直接信库函数。希望这些经验能帮你少走点弯路。
本文还有配套的精品资源,点击获取