1. 双目相机标定与图像校正

任务目标

本案例使用棋盘格标定板对双目相机进行标定,获取左右目相机的内参矩阵、畸变系数,以及双目立体校正所需的旋转矩阵、平移向量和重投影矩阵,最终输出校正后的双目图像对。校正后的图像对应满足极线平行且行对齐,即左右图像中同一物点位于同一像素行。

环境与依赖

需要 OpenCV (4.x) 和 NumPy。若使用真实双目相机,需准备10~20张不同位姿的棋盘格图像对;若仅验证流程,可使用下方代码生成的合成棋盘格图像对。

关键完整代码

import cv2
import numpy as np

# ========== 1. 合成棋盘格图像对(用于无相机时的流程验证) ==========
def make_synth_pair(rows=6, cols=9, square=0.03, img_size=(640, 480)):
    """生成一对带轻微旋转/平移差异的棋盘格图像,模拟双目视图"""
    pattern = np.zeros((rows*square, cols*square), np.float32)
    pattern[::2, ::2] = 1.0
    pattern[1::2, 1::2] = 1.0
    pattern_img = (pattern * 255).astype(np.uint8)
    pattern_img = cv2.resize(pattern_img, img_size)
    pattern_img = cv2.cvtColor(pattern_img, cv2.COLOR_GRAY2BGR)

    # 左图:原图;右图:轻微透视变换模拟右目视角
    h, w = img_size
    src_pts = np.float32([[0,0],[w-1,0],[0,h-1],[w-1,h-1]])
    dst_pts = np.float32([[20,10],[w-30,5],[10,h-20],[w-15,h-25]])
    M = cv2.getPerspectiveTransform(src_pts, dst_pts)
    right_img = cv2.warpPerspective(pattern_img, M, (w,h))
    return pattern_img, right_img

# ========== 2. 角点提取与标定 ==========
def calibrate_stereo(img_pairs, pattern_size=(9,6), square_size=0.03):
    """img_pairs: [(left_img, right_img), ...]"""
    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) * square_size

    obj_points = []  # 3D点
    l_img_points = []  # 左图2D点
    r_img_points = []  # 右图2D点

    for l_img, r_img in img_pairs:
        gray_l = cv2.cvtColor(l_img, cv2.COLOR_BGR2GRAY)
        gray_r = cv2.cvtColor(r_img, 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:
            obj_points.append(objp)
            criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
            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)
            l_img_points.append(corners_l)
            r_img_points.append(corners_r)

    if len(obj_points) < 3:
        raise ValueError("有效图像对不足,至少需要3对")

    # 单目标定
    ret_l, mtx_l, dist_l, _, _ = cv2.calibrateCamera(obj_points, l_img_points, gray_l.shape[::-1], None, None)
    ret_r, mtx_r, dist_r, _, _ = cv2.calibrateCamera(obj_points, r_img_points, gray_r.shape[::-1], None, None)

    # 双目标定(获取相对位姿)
    ret_s, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate(
        obj_points, l_img_points, r_img_points,
        mtx_l, dist_l, mtx_r, dist_r,
        gray_l.shape[::-1], flags=cv2.CALIB_FIX_INTRINSIC)

    return mtx_l, dist_l, mtx_r, dist_r, R, T

# ========== 3. 立体校正与输出 ==========
def rectify_and_save(l_img, r_img, mtx_l, dist_l, mtx_r, dist_r, R, T, out_prefix="rectified"):
    h, w = l_img.shape[:2]
    R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
        mtx_l, dist_l, mtx_r, dist_r, (w,h), R, T, alpha=0)

    map1_l, map2_l = cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, (w,h), cv2.CV_32FC1)
    map1_r, map2_r = cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, (w,h), cv2.CV_32FC1)

    rect_l = cv2.remap(l_img, map1_l, map2_l, cv2.INTER_LINEAR)
    rect_r = cv2.remap(r_img, map1_r, map2_r, cv2.INTER_LINEAR)

    # 水平拼接并画辅助线验证行对齐
    vis = np.hstack([rect_l, rect_r])
    for y in range(0, h, 30):
        cv2.line(vis, (0, y), (2*w, y), (0, 255, 0), 1)

    cv2.imwrite(f"{out_prefix}_left.jpg", rect_l)
    cv2.imwrite(f"{out_prefix}_right.jpg", rect_r)
    cv2.imwrite(f"{out_prefix}_compare.jpg", vis)
    return Q  # 重投影矩阵,用于后续深度计算

# ========== 4. 主流程 ==========
if __name__ == "__main__":
    # 生成8对合成图像用于标定
    pairs = [make_synth_pair() for _ in range(8)]
    mtx_l, dist_l, mtx_r, dist_r, R, T = calibrate_stereo(pairs)

    # 用其中一对做校正演示
    l_img, r_img = pairs[0]
    Q = rectify_and_save(l_img, r_img, mtx_l, dist_l, mtx_r, dist_r, R, T)

    print("左目内参:\n", mtx_l)
    print("右目内参:\n", mtx_r)
    print("旋转矩阵 R:\n", R)
    print("平移向量 T:\n", T.ravel())
    print("校正对比图已保存为 rectified_compare.jpg")

运行与验证

运行方式:直接执行 python stereo_calib.py(将上述代码保存为该文件)。若使用真实相机,将 make_synth_pair() 替换为读取真实左右图像对的代码,并确保图像中棋盘格角点完整可见。

验证方法:

  1. 程序正常结束且打印出左右目内参矩阵、旋转矩阵 R 和平移向量 T;
  2. 打开 rectified_compare.jpg,观察绿色水平辅助线:校正后左右图像中的棋盘格角点应落在同一条水平线上(行对齐);
  3. 若使用真实图像,可进一步用鼠标点击左图某个角点,检查右图同一行是否出现对应角点。

通过标准:校正对比图中棋盘格横线无明显倾斜,左右图对应角点行坐标差不超过2个像素。

常见问题

  • 角点检测失败:确保棋盘格为奇数×偶数(如9×6),且图像中棋盘完整无遮挡;
  • 标定结果跳动大:增加图像对数量至15~20对,并覆盖不同角度和距离;
  • 校正后图像畸变严重:检查 alpha=0 参数,若希望保留全部像素可改为 alpha=1。