前言
在双目测距、三维重建、机器人视觉、深度估计等任务中,双目相机标定是非常关键的一步。
单目相机标定主要求的是每个相机自身的内参和畸变参数,而双目相机标定除了要标定左右两个相机,还需要求出两个相机之间的相对位置关系,也就是旋转矩阵 R 和平移向量 T。
本文使用 OpenCV 完成一次完整的双目相机标定流程,包括:
- 左右相机图片采集
- 棋盘格角点检测
- 左右相机单目标定
- 双目外参标定
- 双目立体校正
- 参数保存与使用
- Python 和 C++ 示例代码
一、双目相机标定是什么?
双目相机标定的目标主要有三类参数。
第一类是左相机参数:
左相机内参矩阵 cameraMatrixL
左相机畸变系数 distCoeffL
第二类是右相机参数:
右相机内参矩阵 cameraMatrixR
右相机畸变系数 distCoeffR
第三类是左右相机之间的相对关系:
旋转矩阵 R
平移向量 T
本质矩阵 E
基础矩阵 F
其中最重要的是 R 和 T。
- R:右相机坐标系相对于左相机坐标系的旋转关系
- T:右相机坐标系相对于左相机坐标系的平移关系
- T 的模长通常可以理解为双目相机的基线长度
二、准备标定图片
双目相机标定需要同步采集左右相机图像。
假设图片路径如下:
left/left_01.jpg
left/left_02.jpg
right/right_01.jpg
right/right_02.jpg
要求:
假设棋盘格内角点数量为:
9 x 6
每个格子的实际尺寸为:
25 mm
注意:这里的 9 x 6 指的是棋盘格内角点数量,不是方格数量。
三、Python 双目相机标定代码
安装依赖:
pip install opencv-python numpy
完整代码如下:
import cv2
import numpy as np
import glob
CHECKERBOARD = (9, 6)
square_size = 25.0
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objp *= square_size
objpoints = []
imgpoints_left = []
imgpoints_right = []
left_images = sorted(glob.glob("left/*.jpg"))
right_images = sorted(glob.glob("right/*.jpg"))
criteria = (
cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER,
30,
0.001
)
image_size = None
for left_path, right_path in zip(left_images, right_images):
img_left = cv2.imread(left_path)
img_right = cv2.imread(right_path)
gray_left = cv2.cvtColor(img_left, cv2.COLOR_BGR2GRAY)
gray_right = cv2.cvtColor(img_right, cv2.COLOR_BGR2GRAY)
image_size = gray_left.shape[::-1]
ret_left, corners_left = cv2.findChessboardCorners(gray_left, CHECKERBOARD, None)
ret_right, corners_right = cv2.findChessboardCorners(gray_right, CHECKERBOARD, None)
if ret_left and ret_right:
corners_left = cv2.cornerSubPix(
gray_left, corners_left, (11, 11), (-1, -1), criteria
)
corners_right = cv2.cornerSubPix(
gray_right, corners_right, (11, 11), (-1, -1), criteria
)
objpoints.append(objp)
imgpoints_left.append(corners_left)
imgpoints_right.append(corners_right)
cv2.drawChessboardCorners(img_left, CHECKERBOARD, corners_left, ret_left)
cv2.drawChessboardCorners(img_right, CHECKERBOARD, corners_right, ret_right)
cv2.imshow("left", img_left)
cv2.imshow("right", img_right)
cv2.waitKey(300)
cv2.destroyAllWindows()
ret_l, camera_matrix_l, dist_coeff_l, rvecs_l, tvecs_l = cv2.calibrateCamera(
objpoints,
imgpoints_left,
image_size,
None,
None
)
ret_r, camera_matrix_r, dist_coeff_r, rvecs_r, tvecs_r = cv2.calibrateCamera(
objpoints,
imgpoints_right,
image_size,
None,
None
)
flags = cv2.CALIB_FIX_INTRINSIC
ret_s, camera_matrix_l, dist_coeff_l, camera_matrix_r, dist_coeff_r, R, T, E, F = cv2.stereoCalibrate(
objpoints,
imgpoints_left,
imgpoints_right,
camera_matrix_l,
dist_coeff_l,
camera_matrix_r,
dist_coeff_r,
image_size,
criteria=criteria,
flags=flags
)
print("左相机 RMS:", ret_l)
print("右相机 RMS:", ret_r)
print("双目标定 RMS:", ret_s)
print("左相机内参:")
print(camera_matrix_l)
print("右相机内参:")
print(camera_matrix_r)
print("旋转矩阵 R:")
print(R)
print("平移向量 T:")
print(T)
四、双目立体校正
双目标定完成后,还需要进行立体校正。
立体校正的目标是让左右图像的对应点尽量位于同一水平线上,这样后续计算视差会更方便。
R1, R2, P1, P2, Q, roi_l, roi_r = cv2.stereoRectify(
camera_matrix_l,
dist_coeff_l,
camera_matrix_r,
dist_coeff_r,
image_size,
R,
T,
alpha=0
)
map_lx, map_ly = cv2.initUndistortRectifyMap(
camera_matrix_l,
dist_coeff_l,
R1,
P1,
image_size,
cv2.CV_32FC1
)
map_rx, map_ry = cv2.initUndistortRectifyMap(
camera_matrix_r,
dist_coeff_r,
R2,
P2,
image_size,
cv2.CV_32FC1
)
left = cv2.imread("left/test_left.jpg")
right = cv2.imread("right/test_right.jpg")
rect_left = cv2.remap(left, map_lx, map_ly, cv2.INTER_LINEAR)
rect_right = cv2.remap(right, map_rx, map_ry, cv2.INTER_LINEAR)
cv2.imwrite("rect_left.jpg", rect_left)
cv2.imwrite("rect_right.jpg", rect_right)
其中:
- R1、R2:左右相机校正旋转矩阵
- P1、P2:左右相机校正后的投影矩阵
- Q:视差转三维坐标时使用的重投影矩阵
- map_lx、map_ly、map_rx、map_ry:后续图像校正映射表
五、保存标定参数
实际项目中,标定参数一般只需要计算一次,然后保存为文件。
fs = cv2.FileStorage("stereo_calib.yml", cv2.FILE_STORAGE_WRITE)
fs.write("camera_matrix_l", camera_matrix_l)
fs.write("dist_coeff_l", dist_coeff_l)
fs.write("camera_matrix_r", camera_matrix_r)
fs.write("dist_coeff_r", dist_coeff_r)
fs.write("R", R)
fs.write("T", T)
fs.write("E", E)
fs.write("F", F)
fs.write("R1", R1)
fs.write("R2", R2)
fs.write("P1", P1)
fs.write("P2", P2)
fs.write("Q", Q)
fs.release()
后续做双目测距或三维重建时,直接读取这个参数文件即可。
六、C++ 版本双目标定代码
下面给出 C++ 版本的核心代码。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
int main()
{
cv::Size boardSize(9, 6);
float squareSize = 25.0f;
std::vector<std::vector<cv::Point3f>> objectPoints;
std::vector<std::vector<cv::Point2f>> imagePointsLeft;
std::vector<std::vector<cv::Point2f>> imagePointsRight;
std::vector<cv::Point3f> objp;
for (int i = 0; i < boardSize.height; i++)
{
for (int j = 0; j < boardSize.width; j++)
{
objp.emplace_back(j * squareSize, i * squareSize, 0.0f);
}
}
std::vector<cv::String> leftImages;
std::vector<cv::String> rightImages;
cv::glob("left/*.jpg", leftImages);
cv::glob("right/*.jpg", rightImages);
std::sort(leftImages.begin(), leftImages.end());
std::sort(rightImages.begin(), rightImages.end());
cv::Size imageSize;
cv::TermCriteria criteria(
cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER,
30,
0.001
);
for (size_t i = 0; i < leftImages.size(); i++)
{
cv::Mat imgLeft = cv::imread(leftImages[i]);
cv::Mat imgRight = cv::imread(rightImages[i]);
if (imgLeft.empty() || imgRight.empty())
{
continue;
}
imageSize = imgLeft.size();
cv::Mat grayLeft, grayRight;
cv::cvtColor(imgLeft, grayLeft, cv::COLOR_BGR2GRAY);
cv::cvtColor(imgRight, grayRight, cv::COLOR_BGR2GRAY);
std::vector<cv::Point2f> cornersLeft;
std::vector<cv::Point2f> cornersRight;
bool foundLeft = cv::findChessboardCorners(grayLeft, boardSize, cornersLeft);
bool foundRight = cv::findChessboardCorners(grayRight, boardSize, cornersRight);
if (foundLeft && foundRight)
{
cv::cornerSubPix(
grayLeft,
cornersLeft,
cv::Size(11, 11),
cv::Size(-1, -1),
criteria
);
cv::cornerSubPix(
grayRight,
cornersRight,
cv::Size(11, 11),
cv::Size(-1, -1),
criteria
);
objectPoints.push_back(objp);
imagePointsLeft.push_back(cornersLeft);
imagePointsRight.push_back(cornersRight);
}
}
cv::Mat cameraMatrixL, distCoeffL;
cv::Mat cameraMatrixR, distCoeffR;
std::vector<cv::Mat> rvecsL, tvecsL;
std::vector<cv::Mat> rvecsR, tvecsR;
double rmsL = cv::calibrateCamera(
objectPoints,
imagePointsLeft,
imageSize,
cameraMatrixL,
distCoeffL,
rvecsL,
tvecsL
);
double rmsR = cv::calibrateCamera(
objectPoints,
imagePointsRight,
imageSize,
cameraMatrixR,
distCoeffR,
rvecsR,
tvecsR
);
cv::Mat R, T, E, F;
double rmsStereo = cv::stereoCalibrate(
objectPoints,
imagePointsLeft,
imagePointsRight,
cameraMatrixL,
distCoeffL,
cameraMatrixR,
distCoeffR,
imageSize,
R,
T,
E,
F,
cv::CALIB_FIX_INTRINSIC,
criteria
);
std::cout << "左相机 RMS: " << rmsL << std::endl;
std::cout << "右相机 RMS: " << rmsR << std::endl;
std::cout << "双目标定 RMS: " << rmsStereo << std::endl;
std::cout << "左相机内参:\\n" << cameraMatrixL << std::endl;
std::cout << "右相机内参:\\n" << cameraMatrixR << std::endl;
std::cout << "R:\\n" << R << std::endl;
std::cout << "T:\\n" << T << std::endl;
return 0;
}
七、C++ 双目立体校正
cv::Mat R1, R2, P1, P2, Q;
cv::Rect roiL, roiR;
cv::stereoRectify(
cameraMatrixL,
distCoeffL,
cameraMatrixR,
distCoeffR,
imageSize,
R,
T,
R1,
R2,
P1,
P2,
Q,
cv::CALIB_ZERO_DISPARITY,
0,
imageSize,
&roiL,
&roiR
);
cv::Mat mapLx, mapLy, mapRx, mapRy;
cv::initUndistortRectifyMap(
cameraMatrixL,
distCoeffL,
R1,
P1,
imageSize,
CV_32FC1,
mapLx,
mapLy
);
cv::initUndistortRectifyMap(
cameraMatrixR,
distCoeffR,
R2,
P2,
imageSize,
CV_32FC1,
mapRx,
mapRy
);
cv::Mat left = cv::imread("left/test_left.jpg");
cv::Mat right = cv::imread("right/test_right.jpg");
cv::Mat rectLeft, rectRight;
cv::remap(left, rectLeft, mapLx, mapLy, cv::INTER_LINEAR);
cv::remap(right, rectRight, mapRx, mapRy, cv::INTER_LINEAR);
cv::imwrite("rect_left.jpg", rectLeft);
cv::imwrite("rect_right.jpg", rectRight);
八、双目标定结果怎么看?
标定完成后,主要关注几个点。
1. RMS 误差
calibrateCamera() 和 stereoCalibrate() 都会返回 RMS 误差。
一般来说,误差越小越好。
如果误差明显偏大,通常说明图片质量、棋盘格检测、同步采集或角点数量存在问题。
2. 平移向量 T
T 表示右相机相对于左相机的位置。
例如输出:
T = [-120.3, 0.8, 2.1]
如果标定板单位是 mm,那么这里可以理解为左右相机基线大约是 120 mm。
3. 校正后图像是否水平对齐
最直观的检查方式是把左右校正图拼接在一起,然后画水平线。
如果同一个物体在左右图像中基本位于同一水平线,说明立体校正效果比较正常。
九、常见问题
1. 左右图片数量不一致
双目标定要求左右图像一一对应。
如果左图和右图错位匹配,会导致外参标定结果完全错误。
2. 棋盘格角点检测失败
常见原因包括:
- 棋盘格内角点数量设置错误
- 图片模糊
- 光照太暗
- 棋盘格反光
- 棋盘格没有完整出现在图像中
3. 双目标定误差很大
建议检查:
- 左右相机是否同步采集
- 左右图片是否正确配对
- 棋盘格尺寸 square_size 是否正确
- 标定过程中相机结构是否发生移动
- 标定图片角度是否过于单一
4. 校正后图像黑边很多
这是正常现象。
可以调整 stereoRectify() 中的 alpha 参数:
- alpha = 0:裁剪黑边,保留有效区域
- alpha = 1:保留更多视野,但黑边更多
总结
本文介绍了 OpenCV 双目相机标定的完整流程。整体步骤可以概括为:
双目标定比单目标定更容易受到采集质量影响。实际项目中,最重要的是保证左右图像严格对应、棋盘格姿态丰富、相机结构固定不变。只要数据采集质量足够好,OpenCV 的双目标定流程就可以得到比较稳定的结果。




