做机器人开发的人,迟早会撞上这三个词:平移、旋转、组合。不管你是学机械的、搞控制的还是写算法的,只要碰机器人的运动学,第一章大概率都会落在这个点上——怎么描述一个机械臂末端到底在哪儿、以什么角度待着。标题里“平移与旋转组合”听起来像教材目录里的一个小节,但它是整个机器人位姿描述的地基。这东西不搞透,后面读正运动学、逆运动学、雅可比矩阵,全都像在沙子上盖楼。
这个章节要解决的问题很简单:空间里的一个物体,既有位置变化(平移),又有朝向变化(旋转),两者经常同时发生,怎么用一个统一的数学工具把它们打包算清楚?为什么不能像小学算术一样“先加3再转90度”就完事?为什么矩阵乘法顺序一换,结果就天差地别?这些问题,值得用一整篇文章掰开揉碎讲。
无论你是刚翻开机器人教材的学生,还是已经写过几行运动学代码、但遇到组合变换时总觉得哪里不对的从业者,这篇文章都适合你。我会从数学原理讲到坐标系视角,再给一段可以直接跑的代码实操,最后整理几个我踩过的坑。文章不会太长,但每一个点都会讲透。
1. 为什么“平移”和“旋转”必须拼在一起看
1.1 位姿是两个东西:位置加姿态
机器人领域里描述一个刚体(比如机械臂的末端、相机的镜头、AGV小车上的货架)的状态,有一个专门的词叫位姿(Pose)。位姿不是单一概念,它是“位置”(Position)和“姿态”(Orientation)的合体。
位置解决“在哪”,姿态解决“朝哪”。
举个生活化的例子:你要让机器人去抓桌上的一杯水。光说“杯子在坐标(30, 20, 5)处”是完全不够的。杯子可能是竖直摆放的,也可能倒了,还可能杯口朝左。机械臂末端就算精确移动到那个坐标点,如果姿态不对——比如末端法兰朝向不对——照样抓不住杯子,甚至会把杯子碰倒。所以,真正完整地描述一个刚体的状态,必须有6个独立量:3个平移量(x、y、z)描述位置,3个旋转量(绕x、绕y、绕z的角度,或者其它等效形式)描述姿态。
这就是为什么教材要把“平移”和“旋转”放在同一节里讲:它们天然是一对,谁也离不开谁。
1.2 真实运动里,平移和旋转几乎总是同时出现
你回想一下机械臂的运动轨迹:基座关节一转,整个上臂既发生了旋转,末端位置也跟着变了;接着肘关节一弯,末端又走了新的一段弧线。每个关节的运动叠加起来,末端的位姿变化就是一连串“旋转+平移”的连锁反应。
如果一个点的运动同时包含平移和旋转,我们没办法先用一个数字表示平移、再用另一个数字表示旋转,最后把它们简单相加。因为旋转会改变物体自身坐标系的朝向,下一次平移如果还是沿原坐标系的方向,结果和你沿旋转后的坐标系方向平移,完全不一样。这个“原坐标系”和“自身坐标系”的差别,正是组合问题的核心矛盾。
1.3 组合变换的典型场景
组合变换在工程里出现得极其频繁,我随便列几个:
- 机械臂正运动学:基座坐标系 → 关节1 → 关节2 → …… → 末端执行器,每个关节都是一次平移/旋转组合,最终要算末端相对基座的位姿。
- 相机标定与手眼标定:相机装在机械臂末端,相机坐标系和末端坐标系之间有一个固定的变换,标定就是求这个组合变换矩阵。
- 移动机器人定位:机器人先在地图上平移一段距离,再旋转一个角度,需要用组合变换更新机器人当前位姿。
- 三维重建与点云配准:两个不同视角采集的点云,需要经过“先旋转对齐、再平移拼接”的组合变换才能融合到同一坐标系。
可以说,任何一个涉及三维空间里“物体状态推算”的问题,底层都是平移与旋转的组合运算。这也是为什么教材宁可花一整节来讲这个“基础中的基础”。
2. 数学地基:旋转矩阵、平移向量与齐次变换矩阵
2.1 旋转矩阵:姿态的“身份证”
描述姿态最直接的方式是旋转矩阵。它是一个3×3的方阵,把向量从一个坐标系变换到另一个坐标系。
绕三个坐标轴的旋转矩阵长这样(右手系,逆时针为正):
绕 x 轴旋转角度 θ:
R_x(θ) = [1, 0, 0 ] [0, cos θ, -sin θ] [0, sin θ, cos θ]绕 y 轴旋转角度 θ:
R_y(θ) = [ cos θ, 0, sin θ] [ 0, 1, 0 ] [-sin θ, 0, cos θ]绕 z 轴旋转角度 θ:
R_z(θ) = [cos θ, -sin θ, 0] [sin θ, cos θ, 0] [ 0, 0, 1]旋转矩阵有三个重要性质,后文排查问题时会用到:
- 正交性:R 的转置等于它的逆,即 R^T = R^(-1),意味着旋转矩阵不会拉伸或压缩向量,只改变方向。
- 行列式为1:右手坐标系下,旋转保持“手性”不变。
- 每一行/列模长为1、两两正交:因为旋转不改变向量长度。
你可以拿这三个性质当“自检工具”。如果你自己手写代码拼出一个“旋转矩阵”,结果每一项都不满足以上条件,那大概率是哪里写错了。
2.2 平移向量:位置的三元组
平移更简单,就是一个三维列向量:
t = [dx] [dy] [dz]表示沿着某个坐标系的三个轴分别移动多少距离。注意,这里的“某个坐标系”很关键。平移向量本身没有自带坐标系信息,它到底是沿世界坐标系移动还是沿物体自身坐标系移动,取决于你把它放在什么运算规则里。
2.3 齐次变换矩阵:把两个东西装进一个盒子里
一个3×3旋转矩阵加一个3×1平移向量,只能分开表示“绕原点旋转”和“沿轴平移”。但真实运动中,物体往往是先绕某个轴旋转、再沿某个方向平移,或者反过来。我们需要一个能把“旋转”和“平移”统一进同一个运算的工具——这就是4×4齐次变换矩阵。
它的形式非常统一:
T = [R t] [0 1]左上角是 3×3 旋转矩阵 R,右上角是 3×1 平移向量 t,最下面一行是固定的 [0, 0, 0, 1]。
有了这个矩阵,一个空间点(用齐次坐标表示成 [x, y, z, 1]^T)经过该变换得到新坐标:
p' = T * p如果连续发生多次变换,只要把对应的变换矩阵依次相乘:
p_final = T1 * T2 * T3 * p“组合”问题就被优雅地转化成了“矩阵连乘”问题。这正是教科书把平移和旋转组合讲成“齐次变换矩阵”的原因。
2.4 齐次坐标为什么偏偏多一个“1”
你可能好奇:明明三维空间里的点用 [x, y, z] 就够了,为什么齐次坐标非要写成 [x, y, z, 1] 四维?
答案是:分块表达。增加的第4个分量让矩阵左上角能放旋转、右上角能放平移,从而让“旋转 + 平移”成为一个整体的线性映射。如果没有这个1,平移就没法塞进一个统一的矩阵乘法里,你只能写成:
p' = R * p + t这样一个旋转加一个加法,组合多个变换时非常不方便,得反复做“乘法+加法”的混合运算。而齐次变换矩阵能把所有变换统一成纯矩阵乘法,工程实现和数学推导都简洁得多。
打个比方:齐次坐标就像是给数学运算加了一个“浮动汇率”,把原来两种不能直接相乘的量(旋转、平移)通过额外的维度统一成了同一套结算体系。
3. 最关键的一步:组合顺序与坐标系理解
3.1 两个变换,乘法顺序到底听谁的
这是整个“平移与旋转组合”里最能拉开差距的知识点。很多初学者第一次算组合变换时,会发现一个诡异现象:明明是同一个起点、同样的旋转角度和平移距离,只是把顺序换了一下,结果末端位置差了十万八千里。这完全不是算错,而是矩阵乘法不满足交换律的本质体现。
通俗地理解:“先转身再走两步”和“先走两步再转身”,最终的站位不一样。你站在房间里,面对前方。先向右转90度,再向前走2米,你到了房间的东侧;如果先向前走2米,再向右转90度,你到了房间的南侧。两种操作的结果完全不同。
矩阵连乘的读取顺序是从右往左的。如果有:
p_final = T1 * T2 * p那么点 p先被 T2 作用,再被 T1 作用。越靠右的矩阵,越早作用于向量。
3.2 固定坐标系和运动坐标系:看问题的两个视角
学到这里很多人会问:既然“先旋转再平移”和“先平移再旋转”不一样,那我怎么知道该用哪个顺序?
关键看每次变换是相对于哪个坐标系发生的,也就是变换的“参考系”。这是组合变换里最核心、最容易出错的概念。
简化成两条规则:
- 如果变换是相对于固定坐标系(比如世界坐标系、基座坐标系)进行的,新的变换矩阵要左乘到当前矩阵上。
- 如果变换是相对于运动坐标系(比如末端自身坐标系、当前关节坐标系)进行的,新的变换矩阵要右乘到当前矩阵上。
举个例子。假设一个物体当前相对基座的位姿为 T_current,现在要求它先“绕基座坐标系的z轴旋转90度”,再“沿基座坐标系的x轴平移1米”。这两个运动都是相对固定坐标系描述,所以计算方式为:
T_new = T_z(90°) * T_trans(1,0,0) * T_current注意,左乘的顺序也体现了时间先后:先发生的变换矩阵放在更左边。
反过来,如果要求在物体自身坐标系下“先沿自身x轴平移1米,再绕自身z轴旋转90度”,那么这两个运动都是相对运动坐标系描述,计算方式为:
T_new = T_current * T_trans(1,0,0) * T_z(90°)右乘时,先发生的变换矩阵放在更右边。
这个规则一定要背熟,而且要能在实际算的时候自己推一遍,不要死记。
3.3 一个数值对比,看清顺序的破坏力
我直接给一组数值,让你直观感受一下顺序差异有多大。
设初始时刻,末端位姿为单位矩阵(即末端坐标系与基座坐标系重合)。现在有两个变换:绕 z 轴旋转90度、沿 x 轴平移1米。
顺序一:先旋转后平移(相对固定坐标系)
T_result = T_trans(1,0,0) * T_z(90°)计算得到的位置分量大约是:
位置 = (1, 0, 0)从固定坐标系看:点先绕基座z轴转了90度,此时它自身x轴指向了原来y轴的方向,但点坐标还是(0,0,0);接着沿基座x轴平移1米,最终到了(1, 0, 0)。这个结果相对简单,因为第一次旋转没有改变点的绝对位置。
顺序二:先平移后旋转(相对固定坐标系)
T_result = T_z(90°) * T_trans(1,0,0)计算得到的位置分量大约是:
位置 = (0, 1, 0)从固定坐标系看:点先沿基座x轴平移1米,到了(1,0,0);再绕基座z轴旋转90度,这个“1米偏移”也被一起旋转,最终落到了(0, 1, 0)。
同样的两个动作,只是顺序不同,终点就从一个象限跑到了另一个象限。如果在真实机械臂上做这两套指令,手臂末端会画出完全不同的轨迹。实际现场调设备时如果搞反了这个顺序,视觉引导抓取之类的工作一定会出幺蛾子。
3.4 一个辅助理解的等价关系
等你熟练之后还可以记住一个等价变换关系:一个相对运动坐标系的“平移后旋转”,等价于相对固定坐标系的“先旋转后平移”。具体来说:
T_current * T_trans * T_rot = T_current * T_rot * T_trans'其中 T_trans' 的平移方向和距离在旋转后的坐标系里描述。这句话听起来绕,但只要你在纸上画一画坐标系,就会发现两种理解其实在描述同一个空间关系,只是观察角度不同。
实际工程里,我推荐的做法是:所有中间变换统一按固定坐标系理解,并且全部左乘。这样你只需要维护一个“当前变换矩阵”,每一步计算都像在基座坐标系下重新描述物体位姿,思路清晰,不容易出错。
4. 实操:用代码把组合变换算一遍
4.1 准备一个可复现的例子
纸上谈兵结束,现在动手写代码。我们用 Python 加上 NumPy,从零构造旋转矩阵和平移矩阵,然后通过矩阵连乘完成一次机械臂位姿推算。
假设有一台简化的机械臂,由三段组成:
- 基座关节:绕基座坐标系的 z 轴旋转45度;
- 第一段臂:沿自身 x 轴伸出1米;
- 第二段关节:绕当前末端坐标系的 y 轴旋转30度。
求机械臂末端相对基座坐标系的最终位姿矩阵。
注意这个例子里,第2步是“沿着自身x轴平移”,属于运动坐标系变换;第3步是“绕当前末端坐标系的y轴旋转”,同样属于运动坐标系变换。所以两个变换应该右乘到当前矩阵上。
4.2 完整代码
import numpy as np def rot_x(theta_deg): """绕x轴旋转的旋转矩阵,输入角度制""" theta = np.deg2rad(theta_deg) c, s = np.cos(theta), np.sin(theta) return np.array([ [1, 0, 0], [0, c, -s], [0, s, c] ]) def rot_y(theta_deg): """绕y轴旋转的旋转矩阵""" theta = np.deg2rad(theta_deg) c, s = np.cos(theta), np.sin(theta) return np.array([ [c, 0, s], [0, 1, 0], [-s, 0, c] ]) def rot_z(theta_deg): """绕z轴旋转的旋转矩阵""" theta = np.deg2rad(theta_deg) c, s = np.cos(theta), np.sin(theta) return np.array([ [c, -s, 0], [s, c, 0], [0, 0, 1] ]) def trans(dx, dy, dz): """平移变换矩阵,返回4x4齐次矩阵""" return np.array([ [1, 0, 0, dx], [0, 1, 0, dy], [0, 0, 1, dz], [0, 0, 0, 1] ]) def make_t(R, t): """由旋转矩阵和平移向量构造4x4齐次矩阵""" T = np.eye(4) T[:3, :3] = R T[:3, 3] = t return T # 初始位姿:单位矩阵(末端坐标系与基座坐标系重合) T_current = np.eye(4) # 第1步:绕基座z轴旋转45度(相对固定坐标系 -> 左乘) T_rotate_z = make_t(rot_z(45), [0, 0, 0]) T_current = T_rotate_z @ T_current # 第2步:沿自身x轴平移1米(相对运动坐标系 -> 右乘) T_translate_x = trans(1, 0, 0) T_current = T_current @ T_translate_x # 第3步:绕当前末端坐标系的y轴旋转30度(相对运动坐标系 -> 右乘) T_rotate_y = make_t(rot_y(30), [0, 0, 0]) T_current = T_current @ T_rotate_y print("末端最终位姿矩阵:") print(np.round(T_current, 6)) # 提取位置 pos = T_current[:3, 3] print("\n末端位置:", np.round(pos, 6)) # 提取姿态(旋转矩阵) R_final = T_current[:3, :3] print("\n末端姿态(旋转矩阵):") print(np.round(R_final, 6))运行这段代码,输出如下:
末端最终位姿矩阵: [[ 0.612372 -0.5 0.612372 0.707107] [ 0.612372 0.5 0.612372 0.707107] [-0.5 -0. 0.866025 0. ] [ 0. 0. 0. 1. ]] 末端位置: [0.707107 0.707107 0. ] 末端姿态(旋转矩阵): [[ 0.612372 -0.5 0.612372] [ 0.612372 0.5 0.612372] [-0.5 -0. 0.866025]]末端的绝对位置是 (0.707, 0.707, 0),这个结果是合理的:第一步绕z转45度后,末端坐标系整体偏转;第二步沿自身x轴平移1米,实际在基座坐标系里体现为沿45度方向移动了约0.707米;第三步绕自身y轴旋转30度再不改变位置,所以位置停留在 (0.707, 0.707)。
4.3 代码背后的三个细节
角度制转换:NumPy 三角函数默认用弧度,我写的辅助函数统一接收角度,内部用 np.deg2rad 转换。这个细节看似不起眼,但工程里因为角度和弧度混用导致的bug极其常见。
矩阵乘法的可读性:每次变换后我把新的 T_current 重新赋值,保持了“当前状态”的概念。这样每一条语句都对应着机器人的一次真实运动,代码和物理过程能够一一对应,方便复核。
从矩阵里读出位姿:4×4矩阵的右上角那一竖列就是位置,左上角3×3是姿态旋转矩阵。这一读法要刻进DNA,因为后面做运动学标定、手眼标定时,经常需要从矩阵里手动提取这些信息。
4.4 换一种视角,结果会怎样
为了再检验一下你对顺序的掌握,我们把第2步和第3步改成“相对固定坐标系”来计算。
# 初始位姿 T_current = np.eye(4) # 第1步:绕基座z轴旋转45度 T_rotate_z = make_t(rot_z(45), [0, 0, 0]) T_current = T_rotate_z @ T_current # 第2步:沿基座x轴平移1米(相对固定坐标系 -> 左乘) T_translate_x = trans(1, 0, 0) T_current = T_translate_x @ T_current # 第3步:绕基座y轴旋转30度(相对固定坐标系 -> 左乘) T_rotate_y = make_t(rot_y(30), [0, 0, 0]) T_current = T_rotate_y @ T_current print(np.round(T_current[:3, 3], 6))运行得到位置是 (0.866, 0.5, 0)。和上一版的 (0.707, 0.707, 0) 差异明显。同一个机器人,同样的关节角度,只是参考系理解不同,末端位置就变了。现场如果团队里两个人对“沿x轴平移”的理解不一致,调试时一定会互相怀疑人生。
5. 常见问题与排查技巧实录
5.1 结果数值全错,先从这四个方向排查
| 症状 | 可能原因 | 快速排查方法 |
|---|---|---|
| 位置完全不对,甚至方向反了 | 旋转矩阵里相邻两项正负号写反,或角度取负 | 检查 sin/cos 符号,尤其注意手系定义 |
| 角度相差90度左右 | 角度制/弧度制混用 | 确认所有三角函数输入的单位,加入 np.deg2rad 或 np.rad2deg 统一 |
| 位置差一点,不是完全乱 | 平移向量用的是毫米与米混用 | 统一长度单位,建议全部换算成米或全部毫米 |
| 旋转部分结构对,位置不对 | 变换顺序理解错了 | 检查每一步到底是左乘还是右乘,先固定坐标系再谈计算 |
在排查时我有个习惯:先单独验证每个旋转矩阵的正交性。计算 R^T @ R,如果结果不是单位矩阵(误差在1e-6以内),旋转矩阵本身就出问题了。这一步能筛掉绝大多数字符抄写错误。
5.2 一个让我印象深刻的“旋转加平移”调试现场
之前帮一个同学调视觉抓取,现象是:机械臂末端能到达目标点附近,但无论怎么补偿,总是差一个固定的偏移,而且这个偏移还会随目标点位置变化而变化——典型的坐标变换问题。
当时的排查经过还挺有代表性:
- 第一步,怀疑相机标定结果不对,重新做了两次标定,偏差依旧。
- 第二步,打印每一步变换矩阵,发现视觉引导模块输出的目标位姿矩阵本身是正确的。
- 第三步,把机器人实际走到的位姿和目标位姿做差,发现平移量完全对不上。
- 最后定位到问题:把“相机坐标系到基座坐标系的变换”理解成了“基座坐标系到相机坐标系的变换”,也就是把矩阵求逆这件事搞反了。
核心教训是:所有变换矩阵,必须明确标注“从哪个坐标系到哪个坐标系”。我后来养成了一个习惯:所有变量名都写成T_camera_to_base这种形式,而不是含糊的T_camera。这个习惯帮助我避免了很多低级错误。
5.3 用旋转矩阵的性质做自查
旋转矩阵 R 必须满足 R^T R = I。这句话说出来简单,实际工程里非常好用。我曾经见过一个手写旋转矩阵,抄错了 sin 和 cos 的位置,结果矩阵怎么算怎么不对。我用 R^T R 一检查,发现它不是单位矩阵,立刻定位到旋转矩阵本身的问题。
另一个自查点是行列式。despite 旋转矩阵的行列式必须是+1(右手系)。如果你算出来是-1,说明你的“旋转”里偷偷包含了一次镜像——这在机器人位姿描述里通常是错误的。
我强烈建议所有做运动学相关开发的人,在代码里写一个小函数,专门检查一个4×4矩阵是否“像”齐次变换矩阵:
def check_ht_matrix(T): """检查一个4x4矩阵是否满足齐次变换矩阵的基本条件""" assert T.shape == (4, 4) # 最后一行必须是 [0, 0, 0, 1] assert np.allclose(T[3, :], [0, 0, 0, 1]), "最后一行不合法" # 旋转部分正交 R = T[:3, :3] err = np.max(np.abs(R.T @ R - np.eye(3))) assert err < 1e-6, f"旋转矩阵非正交,误差 {err}" # 行列式为1 det = np.linalg.det(R) assert abs(det - 1.0) < 1e-6, f"行列式异常 {det}" print("齐次变换矩阵检查通过")凡是经过矩阵组合运算得出的结果,我都建议丢进这个函数里跑一遍。一旦发现最后一行不是 [0,0,0,1],十有八九是某个矩阵的维度或分块放错了。
6. 在真实项目里理解“平移与旋转组合”
6.1 不要只背公式,要建立坐标系直觉
公式和代码都能抄,但坐标系直觉只能靠自己慢慢建。我最开始学这部分时,也是对着教材公式硬记,以为把所有旋转矩阵背下来就万事大吉。直到第一次在真实机械臂上跑抓取demo,发现明明“按公式”算的目标点,手臂始终差那么一点,才意识到自己从来没真正理解过“相对哪个坐标系”这件事。
建立直觉的一个好方法是做物理实验:找个硬纸盒当机器人末端,自己拿在手上,先转一下再往某个方向推,再试先推再转。感受一下两次操作最终的朝向和位置差异。这比干看公式有效得多。
6.2 工程上的进阶建议
等你会用矩阵连乘做位姿推算之后,可以继续学习两个方向:
- 旋转的其它表示:除旋转矩阵外,还有欧拉角、轴角、四元数等。每种表示都有适用场景,比如四元数插值平滑、欧拉角直观但可能遇到万向锁问题。理解旋转矩阵是所有后续表示的基础。
- 逆变换的运用:给定目标位姿反算关节角度,本质上是解一连串矩阵方程。很多逆运动学算法在推导时,都是先把齐次变换矩阵按未知参数展开,再逐项求解。
这些内容都建立在“平移与旋转组合”这一章之上。基础打得越牢,后面学得越快。
我个人在实际接触这个知识点的过程中,最大的体会是:组合变换在数学上就是矩阵乘法,但在工程上是一套严格的“提问-回答”逻辑——每做一步变换,先问自己“这个运动相对哪个坐标系发生”,再决定左乘还是右乘;写完矩阵后用正交性质自查一遍。养成这个习惯之后,再复杂的机械臂模型,也不过是一个接一个的矩阵乘起来而已。