☰
双相机坐标系标定原理与工程实践:从外参求解到立体校正
2026/10/4 6:24:55 网站建设 项目流程

双相机坐标系标定这活儿,听着像是个纯学术概念,但只要你碰过双目测距、机械臂抓取、AGV导航、或者任何需要“两个眼睛”配合感知的视觉项目,早晚要跟它正面交锋。说白了,单相机标定解决的是“这一个相机看到的图像怎么换算成空间坐标”,而双相机坐标系标定要解决的,是两个相机各自看到的世界怎么拼成一个统一的世界——左眼看到的点和右眼看到的点,到底怎么对应,左相机坐标系和右相机坐标系之间隔了多远、转了多少度。这篇文章就从实际工程角度,把双相机坐标系标定的原理、操作流程、以及我自己踩过的坑一起理一遍,适合正在做双目视觉、手眼标定、或者相机与雷达联合标定的朋友参考。

1. 双相机坐标系标定到底在标什么

1.1 从一个具体的应用场景说起

我最早认真研究这个,是做一个双目测距的活儿。两个工业相机固定在一个横梁上,需要测量前方目标物体的三维坐标和距离。刚开始以为只要把两个相机的内参标好就行,结果发现左右两幅图像里的同一个特征点根本对不上——同一个螺丝孔在左图里位于中心偏右,在右图里却偏左更多,而且不同距离下偏差还不一样。折腾几天后终于明白:内参解决的是单个相机的“成像畸变”,而要把左右相机拉进同一个坐标系,必须把两个相机之间的相对位置和角度关系测出来。这个相对关系,就是双相机坐标系标定的核心产物——旋转矩阵R和平移向量T。

类似场景其实远不止双目测距。手眼标定里,机械臂末端坐标系和相机坐标系之间需要确定R和T;自动驾驶里,摄像头和激光雷达联合标定,本质也是在求多个传感器坐标系之间的变换关系;甚至产线上常见的九点标定,也是在二维平面上建立像素坐标和机械坐标的映射。这些任务形态各异,但底层的数学含义是相通的:把一个坐标系下的点,变换到另一个坐标系下,需要的无非就是一个旋转加一个平移。

1.2 坐标系之间的“桥梁”:内参、外参、畸变

聊到坐标系变换,必须先把相机模型捋清楚。一个三维空间点要变成图像上的像素坐标,要经历三次变换:世界坐标系到相机坐标系(外参决定)、相机坐标系到图像物理坐标系(针孔投影决定)、图像物理坐标系到像素坐标系(内参中的焦距和主点决定)。

内参矩阵长这样:

fx 0 cx 0 fy cy 0 0 1

fx、fy是焦距(单位是像素),cx、cy是光轴中心(主点)在像素坐标系下的位置。畸变系数一般是五个:径向畸变k1、k2、k3和切向畸变p1、p2。

外参则是相机坐标系相对世界坐标系的旋转和平移。双相机标定里,通常把左相机坐标系当作基准,右相机相对左相机的变换矩阵就是双目的外参。这个外参一旦标定出来,左右相机就算是“绑定”到一个统一的坐标系里了。以后不管相机装在哪里,只要相对位置不变,这个外参就能一直用。

1.3 为什么不能只看单目标定结果

这是很多新手栽跟头的地方。单目标定做完,每个相机都能单独把像素点还原成该相机坐标系下的射线方向,但两个相机各自的世界坐标系根本不是同一个——除非两个相机摆放位置完全相同(这不现实)。双相机系统里,更合理的做法是:把两个相机的内参、畸变一起参与优化,同时求出一个能最小化左右重投影误差的R和T。单独标定两个相机再分别求世界坐标,误差会叠得很难看。

举一个直观的类比:你左眼看世界,右眼看世界,看到的都是同一个场景,但你的左眼和右眼之间有大约6厘米的眼距。大脑知道这个眼距信息,所以能把两幅图像融合出深度感。双相机外参R和T,就是给机器“大脑”提供的眼距和角度信息。

2. 核心原理:从针孔模型到对极几何

2.1 针孔模型与张正友标定法回顾

张正友标定法是目前最普及的相机标定方案,它用一个平面棋盘格(或圆点阵列板)在不同姿态下成像,通过单应性矩阵约束来求解内参和外参。核心思想是:平面标定板上的特征点在世界坐标系下坐标已知(一般把Z=0),它们在图像上的投影与像素坐标之间构成一个单应矩阵H,而H = K * [r1, r2, t]。通过多张不同姿态的图像,利用旋转向量正交性约束,就能解出内参K。

这个方法最妙的地方在于:它不需要昂贵的三维标定块,一张打印出来的棋盘格用在不同角度拍20张图就能用。我在实际中用的就是A4纸打印的棋盘格(贴平板上),配合OpenCV的findChessboardCorners和calibrateCamera,单目标定的重投影误差都能控制在0.1像素以内。不过打印的标定板有个前提——纸必须贴得很平,否则标定板本身的褶皱会直接变成误差源。

2.2 双相机外参标定的数学本质

双目标定在单目基础上多了一步:同时观察同一个标定板,分别得到左右相机各自的外参(左相机相对标定板的位姿、右相机相对标定板的位姿)。因为同一个标定板在两个相机看来有各自的变换矩阵,而标定板自身是一个刚体,所以左右相机之间可以通过“等式联立”求出相对变换:

假设标定板坐标系为W,左相机坐标系为L,右相机坐标系为R。左相机相对标定板的外参是T_W->L,右相机相对标定板的外参是T_W->R,那么左相机和右相机之间的变换就是:

T_L->R = T_W->R * (T_W->L)^(-1)

用每张图片都能算出一个R和T,但单张图片总有噪声,所以实际用多张图像联立优化,最终得到使重投影误差最小的R和T。这个优化过程OpenCV的stereoCalibrate已经封装好了,但它内置的优化策略对初始值、标志位的选择仍然敏感,后面我会讲到参数怎么配。

另外要提一下本质矩阵E和基础矩阵F。在没有标定板的场景下,我们可以通过左右图像上的匹配点对来估计这两个矩阵,再分解出R和T。E = t^ * R(本质矩阵,只和内参无关的归一化坐标有关),F = K^(-T) * E * K^(-1)(基础矩阵,直接作用在像素坐标上)。8点法或5点法就是干这个的。不过工程实践里,用标定板做双目标定比纯靠特征点匹配更稳定,因为标定板提供的点对分布更可控、精度更高。

2.3 立体校正:让两幅图像“水平对齐”

双目标定完成后,还有一个非常关键的后续步骤——立体校正。校正的目的之一,是把左右相机图像重新投影到一个共同的平面上,使得同一特征点在两幅图像上的纵坐标一致(行对齐),这样后续的立体匹配和视差计算就能在一维搜索空间内完成,大幅降低计算量。

这一步用的是Bouguet算法:把右相机相对左相机的旋转矩阵R分解成两半,左相机转一半、右相机转一半,使两个光轴方向平行;然后再构造一个使极点移到无穷远的矩阵,让极线水平。OpenCV里通过stereoRectify和initUndistortRectifyMap生成左右两个映射表,再用remap完成图像变换。

我提醒一句:立体校正后的图像“看起来”像是一个平移相机拍摄的,但前提是原始标定结果足够准。如果标定外参偏了,校正后左右图像的行对齐会失败,视差图上会出现明显的横向条纹或空洞,这时候别急着调匹配算法的参数,回头重新检查标定结果更高效。

3. 实操流程:OpenCV双目标定全记录

3.1 标定板选择与图像采集要点

先聊标定板。我日常工作里用过棋盘格、圆点阵列板、以及Charuco板。

  • 棋盘格:OpenCV检测稳定,角点定位精度高。打印方便。但对遮挡敏感,边缘不完整时容易失败。
  • 圆点阵列板:对运动模糊更鲁棒,工业场景常用。但圆心提取在透视变形明显时会有系统偏差。
  • Charuco板:结合了棋盘格和ArUco码的优点,允许部分遮挡,适合大视野和标定板不能完整入镜的场景。

图像采集是整个标定流程中决定成败的一环。我的建议:

  1. 准备20~30对不同姿态的左右图像对,不要少于15对。
  2. 保证标定板在画面中占据1/4到1/2以上的面积,太小会导致角点亚像素定位不稳。
  3. 姿态要“全”:正对、左右倾斜、上下倾斜、远近交错都要有。正对图像可以有,但不能全是正对。
  4. 调整光源避免反光,棋盘格白格过曝会导致角点周围梯度失真。
  5. 两个相机必须同步采集(或者场景静止),否则左右图像不是同一时刻的位姿,外参解算必然失败。

如果两个相机是异步触发而场景中有人走动,那采集过程就得特别小心。我曾经在实验台上做标定,旁边同事走动,导致同一组图像里标定板在一张图中是清晰的、另一张图中带了残影,最后标出来的外参明显异常。后来改用同步触发信号(硬件线连或者同一个软件触发源),才把这个问题根治。

3.2 代码实现:检测角点、标定、校正

我用OpenCV C++和Python都跑过,下面以Python为例,把核心链路捋一遍。假设左右相机图像路径分别存在left/和right/目录下,图像名为1、2、3...共N对。

第一步,读取图像并检测棋盘格角点:

import cv2 import numpy as np # 棋盘格内角点数(不是格数!) pattern_size = (9, 6) # 例如9x6个内角点 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) obj_points = [] # 世界坐标系下的棋盘格角点 left_points = [] # 左图像角点 right_points = [] # 右图像角点 N = 25 for i in range(1, N + 1): left = cv2.imread(f'left/{i}.jpg') right = cv2.imread(f'right/{i}.jpg') gray_l = cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) ret_l, corners_l = cv2.findChessboardCorners(gray_l, pattern_size, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, pattern_size, None) if ret_l and ret_r: corners_l = cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r = cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) obj_points.append(objp) left_points.append(corners_l) right_points.append(corners_r)

注意棋盘格的“内角点数”不是格子的数量,比如一张8x7格的棋盘格,内角点是7x6个。这个搞错,后面的所有结果都作废。

第二步,分别对两个相机做单目标定,得到内参K1、K2和畸变系数dist1、dist2。这一步也可以直接用stereoCalibrate的初始值选项(CALIB_USE_INTRINSIC_GUESS),但更稳妥的做法是先单独标一遍,至少可以得到比较可靠的初始内参。

ret_l, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(obj_points, left_points, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(obj_points, right_points, gray_r.shape[::-1], None, None)

第三步,双目标定求外参:

flags = 0 flags |= cv2.CALIB_FIX_INTRINSIC # 如果你信任单目标定的结果,可以固定内参。否则可以用CALIB_USE_INTRINSIC_GUESS做联合优化。 ret_stereo, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( obj_points, left_points, right_points, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6), flags=flags )

R是右相机相对左相机的旋转矩阵,T是右相机相对左相机的平移向量。到这里,双相机坐标系之间的变换关系就出来了。

第四步,立体校正和映射表:

R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0 ) map1_l, map2_l = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) map1_r, map2_r = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1) rectified_left = cv2.remap(left, map1_l, map2_l, cv2.INTER_LINEAR) rectified_right = cv2.remap(right, map1_r, map2_r, cv2.INTER_LINEAR)

3.3 结果验证:重投影误差与极线约束

标定结束不等于完事,重要的是验证结果质量。两个关键指标要盯住:

第一,立体标定的重投影误差。stereoCalibrate返回的ret_stereo就是所有角点投影的平均误差,单位是像素。我的经验是,良好标定结果通常在0.1~0.3像素之间。如果超过0.5,说明图像质量或初值有问题,必须排查后重标。

第二,极线校正效果。把校正后的左右图像并排显示,用鼠标(或者代码)在左图选中一个点,找到它在右图对应行上的匹配点,观察两者纵坐标差。如果校正到位,任意匹配点的纵坐标差应该在1像素以内。我通常会批量取几十个点做统计,画出纵坐标差的直方图,比肉眼观察靠谱得多。

另外还可以用Q矩阵(stereoRectify输出的深度映射矩阵)把校正后的视差图转成三维点云,和标定板的实际几何尺寸做对比。比如标定板是30cm的,算出来的尺度对不对,基本能判断平移向量的单位是否正确。对,双目标定给的T没有绝对尺度信息——它是以标定板方格边长作为尺度基准的,所以拍标定板时,方格的实际物理边长必须测量准确,否则三维重建出来就是个“缩了水”或“放大了”的世界。

4. 衍生场景:双相机与其它传感器的坐标系关系

4.1 手眼标定:相机装在机械臂上

双相机坐标系标定的思想一旦吃透,扩展到其它传感器就顺了。最常见的扩展是手眼标定。机械臂末端装一个相机(眼在手上)或相机固定在某处看机械臂(眼在手外),需要求相机坐标系和机械臂末端坐标系(或基座坐标系)之间的变换。数学形式是AX = XB——A是机械臂末端在两次位姿之间的相对变换,B是相机在两次位姿之间观察到的标定板相对变换,X就是我们要求的手眼矩阵。

我接触过的方案里,opencv有calibrateHandEye接口,支持Tsai、Park等经典解法。此外还有easy_handeye这个ROS包,可以半自动完成数据采集和求解。但实际用起来有几点要格外注意:

  1. 机械臂位姿变化要“有旋转有平移”,如果只是纯平移或纯旋转,方程会退化——这一点和双目标定里“标定板姿态要全”是同一个道理。
  2. 标定板固定在机械臂工作空间内,不能动。相机每次变换位姿后,标定板要完整出现在视野里。
  3. 机械臂末端的位姿精度直接影响手眼标定结果,如果机器人绝对定位误差大,手眼标定结果也会很飘。

4.2 双相机与IMU、激光雷达联合标定

多传感器融合是现在智能设备的大趋势,相机和IMU、激光雷达的外参标定其实也是“坐标系变换”问题,只不过变换关系发生在异构传感器之间。比如视觉-激光雷达联合标定,常见的方案是Autoware里的calibration_camera_lidar工具,以及lidar_camera_calibration这类开源包。核心做法是:把棋盘格放在激光雷达和相机共同视野中,激光点云上拟合出标定板平面,同时相机图像上检测标定板角点,通过点-面对应关系或平面-平面对应关系,求解相机坐标系到雷达坐标系的R和T。

相机-IMU标定(如Kalibr、IMU-Camera Calibration)需要采集连续运动数据,利用视觉特征和IMU积分的一致性来估计相对位姿,以及IMU的零偏、尺度等。这种标定比纯视觉标定更复杂,但它解决的问题还是同一件事——把一个坐标系下的观测映射到另一个坐标系下。

我说得直白一点:你把这套坐标系变换的思想装进脑子里,以后遇到任何“A设备和B设备数据怎么对齐”的问题,第一反应就会是先查它们的外参标定流程,而不是一头扎进匹配算法里调参。

4.3 工程简化技巧:九点标定与旋转中心标定

在纯2D平面场景(比如桌面抓取、丝网印刷定位),不用搞复杂的双目标定,用九点标定就够了。本质上是建立像素坐标系到运动平台坐标系的二维仿射或透视变换关系。做法很简单:在运动平台的机械行程范围内均匀取九个点,机械手/平台移动到每个点时记录机械坐标,同时相机识别该点得到像素坐标,然后用cv2.estimateAffine2D或findHomography求解变换矩阵。

这里有一个很多人忽略的细节:如果相机固定不动、平台运动完成取点,那么相机和平台的相对位置必须始终保持平行,否则不同高度下变换矩阵不一样。而如果相机装在运动轴Z轴上,那还要先做旋转中心标定——在某些视觉引导的贴合设备上,相机随旋转轴转动,必须知道旋转中心在像素坐标系下的位置,否则角度补偿根本无从谈起。

我建议,凡是涉及旋转轴、平台运动、机械臂抓取的视觉项目,先想清楚到底有几个自由度需要标定,再选择对应的标定方案。双目标定不是万能的,但坐标系变换的思想是贯通的。

5. 常见问题与避坑实录

5.1 重投影误差大:先查图像,再查初值

标定结果误差大,最常犯的错是把锅甩给算法,其实问题往往出在数据上。我把排查顺序固定为:

  1. 图像是否清晰?运动模糊、失焦、压缩失真都会导致角点亚像素定位偏。
  2. 标定板是否平整?贴在不平的板子上,棋盘的直线都弯了,标定自然不行。
  3. 图像数量够不够、姿态全不全?20张几乎相同角度的图,等价于只有1张有效姿态。
  4. 角点检测是否全部准确?把每个检测到的角点画出来人工扫一遍,比看任何数值都直观。
  5. 初值是否合理?双目标定如果选择联合优化内参,初值乱给会收敛到局部极小。

5.2 立体校正后图像扭曲严重

校正后图像如果边缘扭曲得很厉害,通常是alpha参数设得不对。stereoRectify的alpha参数控制校正后图像保留的像素范围:alpha=0只保留有效区域(图像被裁剪得多),alpha=1保留所有像素(黑边较多)。如果你后续还要用校正图做特征提取,alpha=0更合适;如果你要可视化或确保不丢失边缘信息,alpha可以设到0.5~1。

另外还要检查是不是内参标定不准导致的。如果畸变校正不彻底,校正后图像里的直线还是弯的,那问题根源在单目标定阶段,而不是立体校正阶段。

5.3 一张速查表:我常用的标定参数和判断标准

检查项合格参考不合格时优先排查
单目重投影误差< 0.1像素图像模糊、标定板不平、角点检测错
双目标定重投影误差< 0.3像素初值、图像同步性、内参是否可靠
立体校正行对齐误差< 1像素外参R和T偏差、畸变参数不准
深度尺度与真实尺度误差< 2%标定板方格尺寸测量不准
左右图像亮度一致性差异不可过大曝光时间、光圈不一致(采集参数,不影响外参,但影响匹配)

这张表不是教科书标准,是我长期实践下来的工程经验值。不同项目精度要求不一样,比如高精度测量可能要求重投影误差低于0.05像素,而普通的视觉定位系统0.3像素也能用。关键是建立“标定结果需要量化评估”的意识,而不是拿起来就跑。

5.4 关于标定的几条经验心得

标定做得多了,自然有几个体会。第一条,标定板方格尺寸一定要测准。很多项目里用的是打印棋盘格,打印完直接拿尺子量方格边长,量出来的值差个1毫米,重建出来的尺度就跟着差1毫米。如果你需要高精度尺度,建议用工业级玻璃陶瓷标定板,或者用激光测距仪精确测量。

第二条,两个相机的视野重叠区域决定了可用范围。双目标定的区域只在两相机共同可视的区域内可靠。超出重叠区,就算有外参,两个相机各自看到的点也无法形成有效匹配。所以安装相机时,要根据工作距离算好基线长度和视场角。

第三条,环境温湿度会影响标定稳定性。工业相机装在金属架上,温度变化会导致机械结构微小变形,标定参数就会偏移。我见过一个项目,白天标完,晚上工艺现场温度降了十几度,系统精度直接掉了。这种场景下要么选择热稳定系数好的安装支架,要么定期重新标定。

结尾:一个小技巧

最后分享一个我后来一直沿用的习惯:标定完成后,把R和T保存成配置文件,同时把当时的环境信息(相机型号、镜头焦距、安装距离、标定板方格尺寸)一起记录。这样现场如果出了精度问题,我能很快判断是环境变了、镜头动了、还是参数文件被误改。标定这件事,一次搞定不难,难的是让标定结果在真实产线上持续可靠——多留一份记录,就能少一次返工。

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

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

立即咨询