欢迎光临
我们一直在努力

【OpenCV实战】双目相机标定:从左右相机标定到立体校正

前言

在双目测距、三维重建、机器人视觉、深度估计等任务中,双目相机标定是非常关键的一步。

单目相机标定主要求的是每个相机自身的内参和畸变参数,而双目相机标定除了要标定左右两个相机,还需要求出两个相机之间的相对位置关系,也就是旋转矩阵 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

要求:

  • 左右图片必须一一对应
  • 棋盘格必须同时出现在左右图像中
  • 图片数量建议 15 到 30 组
  • 棋盘格姿态要丰富,不能全部正对相机
  • 棋盘格尽量覆盖图像中心、边缘、角落区域
  • 左右相机采集时不要移动相机结构
  • 假设棋盘格内角点数量为:

    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 双目相机标定的完整流程。整体步骤可以概括为:

  • 采集左右相机同步棋盘格图像
  • 分别检测左右图像棋盘格角点
  • 对左右相机分别进行单目标定
  • 使用 stereoCalibrate() 求解双目外参
  • 使用 stereoRectify() 完成立体校正
  • 使用 initUndistortRectifyMap() 和 remap() 校正图像
  • 保存参数,供后续双目测距或三维重建使用
  • 双目标定比单目标定更容易受到采集质量影响。实际项目中,最重要的是保证左右图像严格对应、棋盘格姿态丰富、相机结构固定不变。只要数据采集质量足够好,OpenCV 的双目标定流程就可以得到比较稳定的结果。

    赞(0)
    未经允许不得转载:171主机测评 » 【OpenCV实战】双目相机标定:从左右相机标定到立体校正
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址