坐标系与位姿
机器人身上有世界系、底盘系、相机系、夹爪系,还有被抓的物体系。同一个点在每个坐标系里的数字都不一样;一个位姿 \(T_{WC}\) 说清楚的,就是"相机系相对世界系摆在哪里"。把名字写规范,位姿连乘就变成消去相邻下标。
1为什么机器人需要它
相机看到杯子,说它在"前方 0.6 米、偏左 0.1 米"。机械臂却要知道杯子在底座的哪里,才能伸手去抓。这两句话描述的是同一个杯子,数字却完全不同,因为它们量的是不同的坐标系。从相机的读数到手臂的目标,中间隔着一串"坐标系 A 相对坐标系 B 在哪里"的信息:相机装在头上,头在底盘上,底盘在世界里,手臂又装在底盘上。
机器人软件里相当多的 bug,说穿了都是坐标系的 bug:手臂朝相反方向伸出去,地图和激光对不上,抓取总是偏一个固定的位移。这一节只做一件事:把"位姿"这个概念讲清楚,再立一条命名纪律,让你写下的每一个变换都能被检查。
2直觉:点只有一个,读数有很多
一个坐标系(frame)由一个原点和三根互相垂直的单位坐标轴组成。本课程一律用右手系,\(z\) 轴向上(地面是 \(xy\) 平面),机器人自己的坐标系里 \(x\) 朝前、\(y\) 朝左、\(z\) 朝上(ROS 的 REP-103 约定)。\(x\)、\(y\)、\(z\) 轴依次用红、绿、蓝画。本节先看平面(\(xy\) 平面,从上往下看),三维只是多一根轴。
桌上的杯子是物理上的一个点,它不属于任何坐标系。坐标是"把它投影到某个坐标系的各根轴上得到的读数":\(\mathbf p^{W}\) 是世界系 W 的读数,\(\mathbf p^{B}\) 是机器人系 B 的读数。换坐标系,读数就变,点不动。
反过来也成立:机器人动了,杯子在世界里没动,杯子在机器人系里的读数却变了。下面拖动机器人和杯子,对照两组读数。
- 让杯子正好在机器人正前方 1 米:\(\mathbf p^{B} \approx (1, 0)\)。
- 再让杯子在机器人正左方 1 米:\(\mathbf p^{B} \approx (0, 1)\)。这时车头指向世界系的哪个方向?
- 只拖动机器人,不碰杯子,把机器人挪到离出发点 2 米以外。注意 \(\mathbf p^{W}\) 一动不动,\(\mathbf p^{B}\) 却变了。
位姿就是"坐标系 B 相对坐标系 W 摆在哪里":B 的原点在 W 里的坐标 \(\mathbf t_{WB}\),加上 B 的三根轴在 W 里的方向 \(R_{WB}\)。有了这对 \((R_{WB}, \mathbf t_{WB})\),就能把任何点的 B 读数翻译成 W 读数:\(\mathbf p^{W} = R_{WB}\,\mathbf p^{B} + \mathbf t_{WB}\)。
3动手玩:连乘与顺序
命名纪律:T_WB 读作"B 在 W 里的位姿"
规则只有一条,写在变量名里:\(T_{WB}\) 是 B 系在 W 系里的位姿,它把 B 系的读数变成 W 系的读数,\(\mathbf p^{W} = T_{WB}\,\mathbf p^{B}\)。代码里写 T_world_base,读作"world ← base":右边的下标和被乘的点的上标相同,才能消掉。相机装在底盘上、底盘在世界里,则相机在世界里的位姿是
中间的 B 相邻,就消掉:\(W{\cdot}\!B \,\cdot\, B{\cdot}\!C \to W{\cdot}\!C\)。下面的实验里,机器人在世界中移动(\(T_{WB}\) 变),相机固定在车上(\(T_{BC}\) 不变,可以用滑块调安装位置)。杯子固定在世界里。
- 移动机器人,让杯子出现在相机的正前方(偏角小于 5°,距离 1 到 6 米)。读一读 \(\mathbf p^{C}\):它的 \(y\) 分量应该接近 0。
- 切换到"错误顺序",让红色的错误相机和真实相机相距超过 0.5 米。乘法顺序为什么不能随便换?
左乘还是右乘:在谁的坐标系里运动
机器人已经在位姿 \(A = T_{WB}\),现在要再做一个相对运动 \(\Delta\)(例如"前进 1 米")。有两种乘法,结果不同:
- 右乘:\(A\,\Delta\)。\(\Delta\) 的读数是相对机器人自己的坐标系 B:前进 1 米,就是沿车头方向走 1 米。
- 左乘:\(\Delta\,A\)。\(\Delta\) 的读数是相对世界系 W:先把整个机器人当成世界里的一个物体,再把它整体平移、转动。"前进 1 米"变成沿世界 \(x\) 轴走 1 米,转 90° 则是绕世界原点转。
- 先把机器人转到车头偏离世界 \(x\) 轴超过 30°,再设 \(\Delta = (1, 0, 0)\)。琥珀色和紫色各自往哪个方向走了?
- 设 \(\Delta\) 是纯旋转 90°(\(\Delta x = \Delta y = 0\)),把机器人放在离原点 1.5 米以外。一个是原地转弯,另一个是绕着世界原点画圆弧,哪个是哪个?
4再看数学
提示:把鼠标悬停在公式中带底色的符号上,FIG 1 中对应的量会被高亮。
位姿 = 旋转 + 平移
设 B 系的原点在 W 里是 \(\mathbf t_{WB}\),B 的三根轴在 W 里的坐标依次是 \(\mathbf r_1, \mathbf r_2, \mathbf r_3\)。把它们并排成矩阵 \(R_{WB} = [\mathbf r_1\ \mathbf r_2\ \mathbf r_3]\),它是一个旋转矩阵(1.10 节)。点的两种读数满足
读法:\(\mathbf p^{B}\) 是"沿 B 的各轴走多少",\(R_{WB}\mathbf p^{B} = p^B_x\mathbf r_1 + p^B_y\mathbf r_2 + p^B_z\mathbf r_3\) 把这些步子换成 W 里的方向,再从 B 的原点 \(\mathbf t_{WB}\) 出发走过去。这也是 1.3 节的"矩阵的列是新基向量"。
逆与连乘
把上式反解 \(\mathbf p^{B}\),用到 \(R^{-1} = R\T\)(1.10 节):
注意 \(\mathbf t_{BW} \neq -\mathbf t_{WB}\):W 的原点在 B 里的坐标,要把 \(-\mathbf t_{WB}\) 也换到 B 的方向上才对。再看两次变换的连乘:先 \(C \to B\),再 \(B \to A\),
于是有一套可以"目检"的记号规则,写代码时对着变量名核对:
- 相邻下标消去:\(T_{AB}\,T_{BC} = T_{AC}\),里面的 B 必须相同,且紧挨着。
- 求逆交换下标:\(T_{AB}^{-1} = T_{BA}\)。
- 作用在点上:\(T_{AB}\,\mathbf p^{B} = \mathbf p^{A}\),点的上标和 \(T\) 的右下标相同。
主动与被动:同一个 \(T\) 的两种读法
被动读法:\(T_{WB}\) 把同一个点的 B 读数换成 W 读数,点没动,只是换了尺子。主动读法:\(T_{WB}\) 是一个动作,把 W 系本身搬到 B 系的位置(先转 \(R_{WB}\),再平移 \(\mathbf t_{WB}\)),或者把 B 系里的点搬到 W 系里同样的位置。数字一样,说法不同。FIG 3 里的"左乘 / 右乘"也出自这里:\(T\Delta\)(右乘)是"在 \(T\) 的自身坐标系里再动 \(\Delta\)",\(\Delta T\)(左乘)是"在外面的坐标系里整体再动 \(\Delta\)"。
坐标系组成树
一个机器人系统里坐标系很多,但不需要存"每一对之间的位姿"。只要每个坐标系记录它相对父坐标系的位姿,整个系统就是一棵树。任意两个坐标系 A、B 之间的位姿,都是沿树上唯一的一条路径,把每一段边的位姿连乘(逆着边走就取逆):\(T_{AB} = T_{WA}^{-1}\,T_{WB}\)(W 是它们的共同祖先)。树里不能有环,否则同一对坐标系会有两个互相矛盾的答案。ROS 的 tf2 就是这样的一个带时间戳的位姿树。
5机器人中的它:移动操作机器人的 tf 树
下面是一台简化的移动操作机器人:底盘 B 在世界 W 里,头上装着相机 C,肩部 S、肘部 E、夹爪 G 组成一条手臂,桌上有一只杯子 O。树里每条边存一个相对父坐标系的位姿,其中底盘位姿来自里程计 / 定位,肩、肘位姿由关节角算出,杯子是(相机检测、世界系里固定的)一个静态位姿。
从下面选两个坐标系 A 和 B,页面会沿树找出路径,把边上的位姿连乘(或求逆)出 \(T_{AB}\)。抓取的核心是 \(T_{GO}\):杯子在夹爪系里的位姿。它的平移部分为 0 时,手就在杯子上了。
- 选 A = 相机 C,B = 杯子 O。这就是"相机看到杯子"的读数 \(T_{CO}\),它的平移部分就是相机坐标系里的杯子位置。
- 驱动底盘移动超过 0.8 米。哪些位姿变了,哪些没变?(提示:\(T_{WO}\) 是不是该变?\(T_{BC}\) 呢?)
- 选 A = 夹爪 G,B = 杯子 O,用底盘和三个关节把夹爪送到杯子上,使 \(\|\mathbf t_{GO}\| < 0.12\) 米。
ROS 的 tf2 正是这个结构:每个节点广播"子坐标系在父坐标系里的位姿(带时间戳)",lookupTransform 沿树查询任意两个坐标系之间的位姿。手眼标定求出的就是树里某一条固定的边,比如相机相对夹爪的 \(T_{GC}\),求出来之后写进 URDF,往后每一帧的 \(T_{BO} = T_{BG}\,T_{GC}\,T_{CO}\) 都靠它。SLAM / 定位负责维护 \(T_{\text{map},\text{base}}\) 这条边,里程计负责 \(T_{\text{odom},\text{base}}\),两者之差就是 map → odom 的修正。
6常见误区
两者互为逆矩阵,都是"合法"的位姿,所以弄反时程序不会报错,只会让机器人动错。习惯:变量名写全两个坐标系(T_world_cam,不要写 T_cam、pose),读的时候念"W ← C"。tf 的 API 也容易反:lookupTransform(target, source) 返回的是"source 在 target 里的位姿",把 source 系里的点变换到 target 系。
"向前走 1 米"是在机器人自己的坐标系里说的,要右乘;"把整个机器人在世界里向东平移 1 米"是在世界系里说的,要左乘。只有机器人的位姿恰好是单位位姿(在世界原点、车头朝世界 \(+x\)),或 \(\Delta\) 与 \(A\) 可交换时,两者才相等(FIG 3)。判断办法:问一句,这个增量是谁的坐标系里的读数?
机器人系(ROS 的 base_link、相机的 camera_link)是 \(x\) 朝前、\(y\) 朝左、\(z\) 朝上。计算机视觉的相机系(OpenCV、ROS 的 camera_optical_frame)是 \(x\) 朝右、\(y\) 朝下、\(z\) 朝前。两者之间差一个固定的旋转,需要显式写进 tf 里。把像素算出来的点直接当成 camera_link 里的点用,是新手最常见的"整个世界歪了 90°"的来源。
相机在 \(t_1\) 拍到杯子,机器人到 \(t_2\) 才开始动手,这期间底盘动过了。要用 \(t_1\) 时刻的 \(T_{WC}(t_1)\) 把杯子变到世界系里,再用 \(t_2\) 时刻的 \(T_{WG}(t_2)\) 去抓它。tf 树里每个位姿都带时间戳,混用不同时刻的位姿,会得到一个"既不是过去也不是现在"的机器人。
7小练习
8延伸阅读
- K. Lynch & F. Park, Modern Robotics, Ch. 3.3 — 刚体运动、坐标系的下标记法(\(T_{sb}\))与左右乘的含义
- T. Barfoot, State Estimation for Robotics, Ch. 6 — 三维几何、坐标系与位姿的记号
- B. Siciliano, L. Sciavicco, L. Villani, G. Oriolo, Robotics: Modelling, Planning and Control, Ch. 2 — 刚体位姿与坐标变换
- ROS 文档:REP-103(单位与坐标系约定)、REP-105(移动平台的坐标系:map / odom / base_link)与 tf2 教程