双目视觉(二)相机标定与图像校正

1,870 阅读5分钟

畸变校正

上篇文章说了双目视觉的数学原理,但整篇都是基于针孔相机模型来讲解的,但实际上为了更好的成像效果我们使用的都是透镜相机,二者之间的原理类似,但透镜相机会产生透视失真,也叫透视畸变。

image.png

上图为双目相机的拍摄结果,拍摄目标为相机标定使用的标定板,可以明显的观察到标定板的边缘在透视畸变的作用下是弯曲的。

极线校正

除了畸变的问题外其实还有点问题,我们通过画一些平行线可以看的更清楚,你会发现同名点(被摄点在左右图像的点对)在各自图像(或投影)坐标系中 YY 轴方向上的坐标是不同的。

image.png

也就是说,对于左图中的一个点,想去右图中寻找同名点,需要遍历整个图像,但如果我们能够让同名点的 YY 轴方向坐标相同,我们就只需要在右图中遍历一条线就行了,不仅时间复杂度更低,而且需要匹配的位置更少因此结果也更精准。

所谓遍历匹配同名点,就是用如 (5×5)(5\times5) 或其他尺寸的矩阵,以每一个像素为该矩阵的中心去遍历,通过某种公式计算两个 (5×5)(5\times5) 矩阵的相关性,来判断它们是不是同名点。

之所以同名点 YY 轴方向坐标不同,是因为我们无法保证左右相机坐标系的 ZZ 轴平行。

这里明确下相机坐标系,以我们的双眼类比,视觉右方为 XX 轴正向,视觉下方为 YY 轴正向,视觉前方为 ZZ 轴正向。

image.png

如上图所示,极线为被摄点、左相机坐标系原点、右相机坐标系原点组成的极平面与成像平面的相交直线,校正方式就是为分别为左右相机虚拟出 ZZ 轴平行的相机坐标系,成像平面上的点通过某种数学公式映射到虚拟像平面上。

代码实现

畸变校正和极线校正都是用 stereoRectify 这一个函数来实现,先来看一下校正后的效果。不难发现,标定板的边缘已经不再是曲线了,并且同名点 YY 轴坐标也保持一致。

image.png

在校正前,我们需要对双目相机进行标定来获取相机的参数。这里使用 OpenCV Python 来演示,如果你习惯 OpenCV C++ 也是一样的。

标定

先来看双目标定的函数签名。

def stereoCalibrate(
    objectPoints,
    imagePoints1,
    imagePoints2,
    cameraMatrix1,
    distCoeffs1,
    cameraMatrix2,
    distCoeffs2,
    imageSize
    [, R[, T[, E[, F[, flags[, criteria]]]]]]
) -> (
    retval,
    cameraMatrix1,
    distCoeffs1,
    cameraMatrix2,
    distCoeffs2,
    R, T, E, F
)

接下来我们一个一个的说明这些参数和返回值的意义:

参数

  1. objectPoints:棋盘格角点的世界坐标,这里规定每个格子边长为 1 个单位。
w, h = 9, 6 # 棋盘格两个方向的角点数量
def obj_points():
    points = []
    for x in range(w):
        for y in range(h):
            points.append([x, y, 0])
    return [points]*len(imgs)

# 还有一种简洁的写法,与上面等价
def obj_ponits():
    points = np.zeros((np.prod((w, h)), 3), np.float32)
    points[:, :2] = np.indices((w, h)).T.reshape(-1, 2)
    return [points]*len(imgs)
  1. imagePoints:棋盘格角点的图像坐标,需要通过角点检测来得到。
def detect_corners(img):
    ok, corners = cv2.findChessboardCorners(img, self.board_size)
    assert ok
    cv2.cornerSubPix(img, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 0.01))
    return corners
img_points = [detect_corners(img).reshape(-1, 2) for img in imgs]
  1. cameraMatrix:相机内参矩阵,也就是上篇文章提到的 KK 矩阵,需要通过 objectPoints 和 imagePoints 来初始化,这里得到的 KK 只是一个初始值,是不精确的还需要多次迭代计算。
K = cv2.initCameraMatrix2D(obj_ponits, img_points, img_size, 0)
  1. distCoeffs:畸变参数,需要传入初始值用于后续迭代计算,也可以直接传空。

  2. imageSize:图像的大小 (w,h)(w, h),例如 (640,480)(640, 480)

  3. R、T、E、F:传出参数,不需要传。

  4. flags:

    • CALIB_FIX_INTRINSIC:固定传入的 cameraMatrix 和 distCoeffs,只计算 R、T、E、F。
    • CALIB_USE_INTRINSIC_GUESS:以传入的 cameraMatrix 和 distCoeffs 为初始值开始迭代。
    • CALIB_FIX_PRINCIPAL_POINT:迭代时固定主点。
    • CALIB_FIX_FOCAL_LENGTH:迭代时固定焦距。
    • CALIB_FIX_ASPECT_RATIO:固定 fxf_xfyf_y 比值。
    • CALIB_SAME_FOCAL_LENGTH:强制保持两个相机焦距相同。
    • CALIB_ZERO_TANGENT_DIST:切向畸变保持为零。
    • CALIB_RATIONAL_MODEL:使用更精确的畸变模型。
    • CALIB_FIX_K*:迭代时不改变相应的畸变参数。
  5. criteria:迭代的终止条件。

criteria = (
    cv2.TERM_CRITERIA_EPS |    # 达到精度
    cv2.TERM_CRITERIA_COUNT,   # 达到迭代次数
    100,                       # 迭代次数
    1e-5                       # 精度
)

返回值

  1. retval:误差(root mean square 本文用不到)。
  2. cameraMatrix:计算后的 KK 矩阵。
  3. distCoeffs:计算后的畸变参数。
  4. R:相机间的旋转矩阵。
  5. T:相机间的平移矩阵。
  6. E:本质矩阵(essential matrix 本文用不到)。
  7. F:基础矩阵(fundamental matrix 本文用不到)。

这里明确一下相机间的旋转和平移矩阵,这里假设 cameraMatrix1 描述左相机,cameraMatrix2 描述右相机,则函数返回的 R、T 是左相机坐标系到右相机坐标系的变换,也就是说左相机坐标进行R、T变换后得到的结果,为该点在右相机坐标系中的坐标。

校正

先来看立体校正的函数签名。

def stereoRectify(
    cameraMatrix1,
    distCoeffs1,
    cameraMatrix2,
    distCoeffs2,
    imageSize,
    R,
    T
    [, R1[, R2[, P1[, P2[, Q[, flags[, alpha[, newImageSize]]]]]]]]
) -> (
    R1,
    R2,
    P1,
    P2,
    Q,
    validPixROI1,
    validPixROI2
)

还是一个一个的说明这些参数和返回值,上面说过的就不再重复了。

参数

  1. flags:
    • CALIB_ZERO_DISPARITY:让两个像主点在校正后有相同的图像坐标。
  2. alpha:
    • 0:校正后的图像被改变,没有黑色区域。
    • 1:保留拍摄的所有像素,会有黑边。

返回值

  1. R:旋转矩阵。
  2. P:投影矩阵。
  3. Q:视差深度映射矩阵(disparity-to-depth mapping matrix 本文用不到)。
  4. validPixROI:有效区域,如果alpha = 0,有效区域即全部像素。

这里明确一下 RPR、P 矩阵的涵义。

旋转矩阵 RR 不是世界坐标系到相机坐标系的旋转矩阵,而是真实相机坐标系到虚拟相机坐标系的旋转矩阵。

投影矩阵 PP 则是虚拟相机坐标系到虚拟投影坐标系的投影矩阵。

End

上面的两个函数,计算出了 KDRPK、D、R、P 这四个矩阵,其中 KDK、D 用于畸变校正,RPR、P 用于极线校正。

本文只是说了说函数的用法(具体算法是怎么运行的我其实一概不知),关于函数的参数的说明其实官方文档都有,如果本文有不专业或不准确的地方万望指出。

本文代码地址:github.com/suqinglee/C…