做机器视觉的工程师,几乎都要过“相机标定”这一关。单目标定、双目标定、手眼标定,这些词在实际项目里反复出现,很多人也跟着网上的教程跑通了整个流程,但真正被问到“外参里的旋转R和平移t,到底相对于谁”的时候,往往会卡壳。更常见的是这种情况:明明用OpenCV标定拿到了rvec和tvec,可是换个标定板位置,结果就变了,有人却说外参应该是固定不变的;还有人在多相机系统里,把两台相机各自标定出来的R/t拼到一起,怎么都对不上。
这些困惑的根源,都在于“外参”这个词没有说清楚“相对于哪个坐标系”。同样是[R|t],参考世界坐标系是一种理解,参考另一台相机或传感器是第二种理解,参考标定板坐标系是第三种理解。三种参考坐标,对应三种外参含义,数值、方向、用途完全不一样。这篇文章就把这个“相对于谁”的问题彻底讲透,顺便把坐标变换矩阵的正反方向、单位换算这些最容易出错的地方一起理清。适合正在做单目视觉测量、双目立体视觉、机械臂手眼标定,或者任何需要在系统里使用相机位姿的人。
1. 想清楚外参,先理顺“谁在谁里面”
1.1 外参的本质:一个带方向的坐标变换
外参的数学形式通常写成旋转矩阵R和平移向量t,组合起来是一个4x4的齐次变换矩阵T:
T = [ R t ] [ 0 1 ]它的作用是,把同一个三维点在一个坐标系中的坐标,换算到另一个坐标系中的坐标。比如世界坐标系里的点P_w,映射到相机坐标系里就是:
P_c = R * P_w + t这个公式看着简单,但里面藏着两个容易搞混的语义。第一,t是世界坐标系原点在相机坐标系中的坐标,不是相机原点在世界坐标系中的坐标。第二,R的列向量是世界坐标系的三个坐标轴在相机坐标系中的方向表示。换句话说,矩阵[R|t]描述的是“世界坐标系怎样摆放、放在哪里,才能在相机坐标系里被看到”。
反过来的变换同样常用。如果想把相机坐标系下的点换算回世界坐标系,直接用逆矩阵:
T_w_c = inv(T_c_w) = [ R^T -R^T * t ] [ 0 1 ]也就是说:
P_w = R^T * P_c - R^T * t这里的-R^T * t才是相机原点在世界坐标系中的坐标。这也是为什么有人说外参的平移是相机的坐标,有人说平移是世界原点的坐标——两个人说的其实是同一个矩阵的正反两个方向。
1.2 同一组R和t,为什么会有两种完全相反的读法
同一个变换矩阵,可以有两种读法。第一种叫坐标变换读法:点从A坐标系变换到B坐标系;第二种叫位姿读法:A坐标系在B坐标系中处于什么位置、什么朝向。两种读法写的公式方向相反,数值上互为逆矩阵。
打个比方,你在商场问路,保安说“电梯在你前方20米,右转5米就到了”,这是在描述电梯相对于你的位姿;如果换成“从电梯的位置来看,你朝那个方向走”,那就是反过来的坐标变换。外参也一样,如果文档写“外参是相机在世界坐标系中的位姿”,它给你的矩阵是T_w_c;如果文档写“外参把世界坐标变换到相机坐标”,那它是T_c_w。两者差一个逆变换。
实操中我有个习惯:永远不把R和t单独命名,而是封装成一个T_source_target矩阵,一眼就能看出“从哪个坐标到哪个坐标”。光是这一点,就能规避掉大量方向打反的bug。正式写代码时,应该刻意用类似T_board_cam这种变量名,而不是Tvec这种看起来很有道理、实际没方向的裸变量。
2. 第一种外参:以世界坐标系为参考
2.1 什么是“世界坐标系外参”
世界坐标系外参,是把相机坐标系和一个全局固定的世界坐标系关联起来的变换。这个外参描述的是:相机在世界里的位置和朝向。典型场景包括:自动驾驶里相机在车体坐标系中的安装位姿、工业检测中相机相对于测量平台坐标系的位姿、机器人视觉中相机相对于机械臂基座的位姿。
这类外参的特点是一个“全局位姿”。在ROS这类框架里,对应的就是TF树中从base_link到camera_link的坐标变换,是一棵树里两个固定节点之间的关系。只要相机的安装位置和方向没有动过,这个外参就是固定的;相机被撞了一下、支架松了,外参就必须重新标定。
获取方式通常有三种:
- 第一种,知道足够多的控制点在真实世界坐标系中的三维坐标,同时能在图像里找到这些点,直接用PnP求解;
- 第二种,用外部测量设备直接测出相机的物理位置和朝向;
- 第三种,通过标定板做一个中间转换,先求标定板相对相机的位姿,再把标定板放到已知世界坐标的位置,合成最终外参。
第三种方式最灵活,也是后面要重点展开的路径。
2.2 标定世界坐标系外参时的常见误区
这里最常见的误区,是直接把某个标定板图像的外参拿过来当世界坐标系外参用。实际项目中,标定板只是标定的工具,当标定完成、系统投入使用后,标定板早就拿走了。如果外参里的参考坐标是那块临时摆的标定板,显然没法用于正式测量。
正确的做法是先定义清楚“世界坐标系的原点和三个轴放在哪”。比如某检测项目的世界坐标系原点取在测量台面的左上角,X轴沿台面向右,Y轴沿台面向内,Z轴竖直向上。定义好之后,把标定板平放在台面上,或者把标定板固定在台面一角,让标定板坐标系与世界坐标系之间有一个已知的固定变换T_w_board。此时任意时刻的相机相对世界坐标系的外参就是:
T_w_cam = T_w_board * T_board_cam这个合成公式会在第5章详细展开。另外,单位问题也在这里爆发。标定板的角点坐标如果按毫米输入,solvePnP输出的tvec就是毫米;如果某段代码里标定板坐标用了米,输出tvec也随之变成米。同一个系统里两套单位混用,外参拼出来往往差三个数量级,特征非常明显:本该是几十毫米的平移量,变成了零点零几。
世界坐标系的定义还有一条建议:尽量和系统的主要结构对齐。比如测量台面是长方形的,原点就放在台面一角,轴和边平行;机器人系统里,尽量和机器人基座坐标系平行。这样后期调试、排查异常时,人脑可以直接想象坐标方向,而不是每次都拿着矩阵算半天才能猜到相机偏向哪边。
3. 第二种外参:以另一台相机/传感器为参考
3.1 双目视觉里的相对外参
第二种最常见的外参参考坐标,是另一台相机或传感器。双目相机标定时,stereoCalibrate返回的R和T,通常表示两个相机坐标系之间的相对位姿。典型约定是:把左相机坐标系中的点,通过:
P_right = R * P_left + t变换到右相机坐标系。也就是说,这个R和t描述的是“右相机相对于左相机的位置和朝向”,参考坐标是左相机。
真正用的时候,我一般不看文档里的文字,而是做一个极简单的物理验证:把棋盘格放在左右相机都能看到的公共区域,分别用solvePnP求出标定板相对左相机的位姿和相对右相机的位姿,再拼出相对外参,和stereoCalibrate的结果对比。如果符号对不上,说明方向约定反了,调整矩阵逆转换即可。这种用“同一块板做中间代理”的验证方法,能覆盖绝大多数方向问题,比背文档可靠得多。
双目标定之后,还会用重投影误差来评估精度:把标定板角点的三维坐标通过外参和内参投影到右相机图像上,和实际检测到的角点像素位置做差。误差通常应该在0.1到0.3个像素之间。如果重投影误差大了,先别急着怪外参,八成是内参或者畸变参数本身还有问题。
3.2 手眼标定中的“相对外参”
手眼标定也属于这一类,只是参考对象变成了机械臂末端或机械臂基座。Eye-in-Hand构型下,相机固定在机械臂末端,标定的外参X描述的是相机坐标系与末端坐标系之间的相对位姿;Eye-to-Hand构型下,相机固定在工作空间外,标定的外参X描述的是相机坐标系与机械臂基座坐标系之间的相对位姿。
手眼标定的核心方程是AX=XB,其中A来自机械臂自身的运动学数据,B来自标定板检测数据,X就是我们要找的外参。这里有一个非常大的坑:不同库对X的方向定义不同。OpenCV的calibrateHandEye输出的是cam2gripper,也就是从相机坐标系到末端坐标系的变换;而不少论文和开源代码里用的X是gripper2cam,也就是末端坐标系到相机坐标系的变换,恰好是前者的逆矩阵。如果不去确认方向,标定结果看起来误差很小,但真正换算抓取点位时,偏差会放大好几倍。
多传感器融合项目里,这个“以另一台传感器为参考”的外参同样常见。比如RGB相机和深度相机之间、激光雷达和相机之间,都需要标定一个相对变换矩阵,把传感器坐标系下的点云映射到主传感器坐标系里做数据对齐。这类外参统称为“相对外参”,本质上是两套坐标系的刚体变换。
4. 第三种外参:以标定板坐标系为参考
4.1 标定板坐标系是怎么来的
第三种外参的参考坐标,是标定板坐标系本身。在OpenCV的calibrateCamera里,我们构造一组objectPoints,通常把棋盘格左上角的角点设为原点,X轴沿棋盘格往右,Y轴沿棋盘格往下,Z轴垂直于棋盘格平面,所有角点的Z坐标都是0。这组objectPoints本身就是一个世界坐标系,只不过它的原点固定在标定板上。
每拍一张标定板图像,函数就会返回一组rvec和tvec,这组参数描述的就是这一帧图像里,标定板坐标系相对于相机坐标系的位姿。这块板向左挪了一下,tvec就跟着变;板转了个角度,rvec的旋转向量也变了。因为标定板在标定过程中通常都要移动来覆盖不同视角,所以多帧图像对应多组不同的外参,本身就很正常。
张正友标定法里,每帧标定板外参还是解算单应矩阵的核心。棋盘格平面上的点Z=0,单应矩阵H可以写成:
H = K * [r1 r2 t]对每一帧图像解出单应矩阵H之后,才能进一步分解出内参K和外参的r1、r2、t。所以标定板外参不是“副产品”,而是内参标定算法中绕不开的中间量。
4.2 为什么标定板外参总被误当成“固定外参”
我在不少项目里见过这类问题:用calibrateCamera标定完,直接拿第一组rvec/tvec当成相机固定外参,写进上位机就开始做定位。等到相机角度被人动一下、或者换了一块更大的标定板,系统就完全乱了。原因就是:第一组rvec/tvec的参考坐标是标定板,而标定板不在系统坐标系里,它不是那个“固定的世界坐标系”。
还有一个常见的混淆场景:用solvePnP做单帧位姿估计,比如给一块ArUco码拍照,得到marker相对相机的位置姿态,很多人也管这个叫“外参”。这个说法没错,但它是第三种外参,只在当前这一帧、当前这个板的位置上成立。只要板子一挪动,这个外参就要重新算。理解到这一层,很多坐标系混乱的代码就能理顺了。
那标定板外参有没有用呢?当然有。它是最重要的中间量:内参标定的每一帧都要用到标定板外参来做单应矩阵分解;双目相对外参可以通过两块板外参拼接;系统世界外参也可以通过标定板外参合成。它的角色是“桥梁”,而不是“终点”。
5. 三种外参的对比与互相换算
5.1 一张表看懂三者的区别
| 外参类型 | 参考坐标系 | 典型获取方式 | 是否固定 | 典型应用 |
|---|---|---|---|---|
| 世界坐标系外参 | 自定义的全局坐标系 | 已知控制点PnP、外部测量设备 | 固定(安装不动时) | 机器视觉定位、车体坐标系、测量平台 |
| 另一相机/传感器外参 | 另一台相机或传感器坐标系 | 双目立体标定、手眼标定、多传感器标定 | 固定(器件不动时) | 双目视觉、RGB-D融合、手眼抓取 |
| 标定板外参 | 标定板坐标系 | calibrateCamera/solvePnP逐帧获取 | 随板位变化 | 标定中间量、ArUco位姿估计 |
从这张表可以看出来,所谓的“外参”,本身并没有默认指向某一类。当你和外参打交道,第一件事永远是问自己:这个外参相对哪个坐标系?这个问题想清楚,后面怎么用都不会乱。
5.2 外参合成:如何从标定板外参推出其他外参
三种外参之间不是孤立的,它们可以通过矩阵乘法互相转换。最常用的一个公式是:如果标定板在世界坐标系中的位姿T_w_board已知,而某一帧标定板相对相机的位姿T_board_cam已经通过solvePnP求出,那么相机相对世界坐标系的外参就是:
T_w_cam = T_w_board * T_board_cam这个公式的实际意义很大。比如某项目里,标定板固定放置在检测平台的某个精确位置上,世界坐标系定义在平台上。只要做一次solvePnP,就相当于完成了一次外参标定,而且之后相机实时观测同一块板时,还能持续估计相机相对世界坐标系的位姿变化。这在相机安装在运动平台的场景里尤其好用。
同理,两台相机同时观测同一块标定板时,通过中间板坐标做桥梁,可以得到两台相机之间的相对外参:
T_camA_camB = T_board_camB * inv(T_board_camA)也就是说,把点从相机A坐标系变换到相机B坐标系,需要先把点从相机A坐标变换回标定板坐标系,再通过标定板坐标系变换到相机B坐标系。这个拼接公式正是第6章代码验证环节的核心。
6. 实操:用代码验证三种外参
6.1 用solvePnP获取标定板外参
有了前面这些概念,代码环节反而简单。下面这段Python演示的是最标准的用法:已知相机内参,输入标定板坐标系下的三维角点坐标和当前图像里的像素角点坐标,求解标定板相对相机的位姿。
import cv2 import numpy as np # 棋盘格尺寸:内角点9x6,方格边长30mm pattern_size = (9, 6) square_size = 30.0 # 标定板坐标系下的三维点,Z全部为0 obj_pts = np.zeros((pattern_size[0] * pattern_size[1], 3), dtype=np.float32) obj_pts[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) obj_pts *= square_size # img_points 是这一帧图里检测到的角点像素坐标 # K 和 dist 是已经标定好的相机内参和畸变系数 retval, rvec, tvec = cv2.solvePnP(obj_pts, img_points, K, dist) # 旋转向量转旋转矩阵,并组装成齐次变换矩阵 R_board_cam = cv2.Rodrigues(rvec)[0] T_board_cam = np.eye(4) T_board_cam[:3, :3] = R_board_cam T_board_cam[:3, 3] = tvec.flatten()这里的变量名是经过提醒的:T_board_cam表示从标定板坐标系到相机坐标系的变换。代码里如果出现裸的rvec和tvec,长期维护时很容易搞混,我习惯在这个位置直接封装成带方向的矩阵变量。
求解之后,可以用一个直观的物理规律自检:如果标定板摆在相机正前方,那么tvec的Z分量应该是正的,数值大约等于板到相机的距离(按毫米计)。如果Z分量是负的,说明方向约定理解错了,或者输入的物点顺序反了。
6.2 拼接两台相机的相对外参
现在模拟双目场景。左右两台相机同步拍摄同一块标定板,各自调用solvePnP,得到T_board_camL和T_board_camR。通过中间板做桥梁:
# 相机L和相机R分别求得的标定板外参 T_board_camL = ... T_board_camR = ... # 从相机L坐标系变换到相机R坐标系的相对外参 T_camL_camR = T_board_camR @ np.linalg.inv(T_board_camL)这行代码的背后逻辑很清楚:先把点从相机L坐标系逆变换回标定板坐标系,再用标定板坐标系变换到相机R坐标系。得到的结果理论上应该与stereoCalibrate返回的相对变换一致。
务必记住一点:如果stereoCalibrate文档里的约定是“右相机在左相机坐标系中的位姿”,而你的T_camL_camR是从L到R的坐标变换,那两者在矩阵数值上是同一个东西。如果写反了,结果就差一个逆矩阵。实际比对时,可以用一组左右相机同步采集的真实数据,分别求出T_camL_camR和stereoCalibrate的结果,再算一下两者之间的差值矩阵是否接近单位阵,就能快速确认方向有没有弄反。
6.3 一个低成本但好用的方向验证技巧
多相机系统标定完之后,我总会做一个最简单的现场验证:在公共视野里放一个形状尖锐的物件,比如一个锥桶尖点,先在左相机图像上点击该点的像素坐标,再通过外参转换计算它在右相机图像中应该出现的像素位置,最后和右相机图像里实际看到的点对比。如果误差只有几个像素,外参方向和数值基本可信;如果出现几十上百像素的偏差,通常不是标定精度问题,而是方向反了或者平移方向错了。
这个验证方法不需要任何额外标定板,也不需要复杂的数学,实际上就是一个小工具函数的事,但能非常快地把方向类错误拦截在项目早期。很多做视觉做了多年的师傅,依然在使用这套“像素点对点验证”来判断外参对不对,因为它直接基于最终使用场景,绕过了中间所有可能出错的环节。
7. 常见问题与避坑清单
7.1 高频问题速查表
| 问题 | 典型现象 | 根本原因 | 排查思路 |
|---|---|---|---|
| 外参方向反了 | 坐标变换后点乱飞、位置错乱 | R/t的定义方向理解错误 | 用已知点做正向和反向变换验证 |
| 标定板外参当固定外参 | 相机没动,外参却随图像变化 | 把单帧板位姿当成系统外参 | 明确世界坐标系定义,重新标定系统外参 |
| 平移量单位混乱 | 外参拼接后数值差几个数量级 | 标定板坐标单位不一致 | 统一单位,检查t值的量级是否与物理距离一致 |
| 手眼标定抓取偏差大 | 手眼标定误差很小但抓不准 | X的方向定义与库约定不同 | 确认文档中X是cam2gripper还是gripper2cam |
| rvec/tvec命名混乱 | 代码复用方向出错 | 裸变量名无方向语义 | 封装成T_source_target矩阵,配合注释 |
7.2 标定完成后,别忘了做“物理验算”
标定数据本身看起来再漂亮,都不如一次现场物理验算来得实在。我习惯在标定结束后做三件事:
- 第一,拍一张棋盘格,把标定板外参的tvec数值和实际尺子量出来的距离对照;
- 第二,如果有多相机,用上面的像素点对点验证方法检查各相机之间的外参方向;
- 第三,在系统进入正式工作前,用几组不参与标定的新图像做一次完整的坐标转换链路测试,确保从图像坐标到物理坐标的输出稳定可靠。
这三件事加起来用不了半小时,但确实能挡住绝大部分坐标系类错误。经历过几次“标定完看着挺好、一上产线全崩”的教训之后,我现在宁可多花这半小时,也不愿意后期在真实产线上排查坐标系问题。
最后说一点个人体会。相机标定这个领域,工具链已经很成熟,OpenCV和各种标定库都提供了一键式接口,真正难的不是算法本身,而是坐标系约定的一致性。一个外参矩阵,如果没有明确“从哪个坐标到哪个坐标”,再好用的库也会变成事故现场。我在代码里强制自己用T_source_target命名,在调试时先做正反变换验证方向,遇到手眼标定先查文档确认X的方向约定……这些都只是习惯,但正是这些习惯,帮我避免了大量定位错乱的深夜排查。希望这篇文章能把“外参相对于谁”这扇门推开一点,后续大家做标定、做融合、做抓取时,坐标系思路能一直保持清晰。