相机校准

目标

在这一部分中,我们将了解关于:

  • 相机畸变的类型
  • 如何找出相机的固有属性和可变属性
  • 如何利用这些属性校准相机图像

基础

一些针孔摄像机会有严重的图像畸变的问题。其中径向畸变和切向畸变是两种主要的畸变现象。

径向畸变使得直线变得弯曲。切向畸变使得离图像中心点越远的点看上去更远。举个例子,如下图像展示了一个两条边界被红线标记的棋盘。但是,你可以看到棋盘的边缘不仅不是直线,而且与红线偏差很大。所有预期的直线都弯曲了。访问畸变(光学)来获取更多信息。

calib_radial

calib radial image

径向畸变可表示为如下公式: {\notag} x{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \ y{distorted} = y( 1 + k1 r^2 + k_2 r^4 + k_3 r^6) 相似的,发生切向畸变是因为摄像镜头未完全平行于图像平面。所以,图像中的某些区域可能看起来比预期的更近。切向畸变可表示如下公式: {\notag} x{distorted} = x + [ 2p1xy + p_2(r^2+2x^2)] \ y{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy] 简而言之,我们需要找到上面的五个参数,其被称为畸变系数,由下式给出: {\notag} Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3) 除此之外,我们还需要一些其他信息,像是相机的固有属性和可变属性。固有属性是每个相机的特有属性。其中包括像是焦距(f_x,f_y)​和光心(c_x, c_y)​。焦距和光心可以被用于创建相机矩阵,用于消除相机镜头特有属性造成的畸变。每个相机的相机矩阵都是独一无二的,所以一旦我们计算出来,便可以在同一相机所拍摄的其他图像上重复使用。其表示为 3x3 的矩阵: {\notag} camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \ 0 & f_y & c_y \ 0 & 0 & 1 \end{matrix} \right ] 外部参数对应于旋转和平移矢量,其将 3D 点的坐标平移到坐标系。

对于立体应用方面,这些畸变现象首先需要解决。为了寻找到这些参数,我们必须提供一些被明确定义的图像(比如棋盘)。我们可以寻找到一些我们早就知道相对位置的点(比如棋盘的方格的角点)。我们也知道这些点在真实世界中的坐标以及图像中的坐标,由此我们便可以解出畸变系数。如果想要获取更好的结果,我们需要至少 10 个测试图像。

代码

正如上面所言,我们至少需要 10 个图像用以相机校准。OpenCV 提供了一些棋盘的图片(参见 samples/data/left01.jpg – left14.jpg),所以我们将利用这些图像。思考一个棋盘的图像。相机校准所需要的重要输入数据便是 3D 真实世界点的集合以及在图像中这些点所对应的 2D 坐标。我们可以轻易从这些图像中寻找到 2D 图像点。(这些图像点是棋盘中两个黑色块相交的位置)。

那真实世界中的 3D 点又如何呢?这些图像于同一相机静止拍摄,其中的棋盘放置于不同的位置与方向。所以我们需要知道(X,Y,Z)​的值。但是为了简单起见,我们可以说棋盘在 XY 平面保持静止,(所以 Z 恒等于 0 )而相机是移动物件。这个考量帮助我们可以仅找出 X,Y 的值。现在对于 X,Y 的值,我们可以简单地传递像是(0,0), (1,0), (2,0), … 之类的点用于表示点的位置。在此之下,我们得到的结果将是棋盘方块相对的大小。但是如果我们知道棋盘方块的大小,(大约 30mm),我们便可以传递像(0,0), (30,0), (60,0), …这样的值。因此,我们的到的结果也是 mm 为单位的。(在这种情况下,我们不知道方块尺寸,因为我们没有拍摄这些图像,所以我们将方块尺寸作为参数传入)。

3D 点被称作对象点,2D 点被称作图像点

标定

所以为了寻找到棋盘上的图案,我们可以使用一个函数,cv.findChessboardCorners()。我们同样需要传递我们寻找的图案类型,像是 8x8 网格,5x5 网格之类的。在这个例子中,我们使用 7x6 的网格(一般的棋盘规格是 8x8 网格和 7x7 的内角)。它返回角点和阈值,如果成功找到所有角点,则返回 True。这些角落将按顺序放置(从左到右,从上到下)

参考

  • 这个函数可能不能够找到所有图像中需求的图案。所以,有一个很好的选项便是编写代码,以便它启动相机并检查每一帧是否需要获取图案。一旦图案确定了,便寻找角点并存入一个列表当中。同样的,在阅读下一帧之前提供一些间隔,以便我们可以在不同的方向上调整我们的棋盘。持续此过程,直至获得所需要的优良图案。甚至在我们在这里给出的示例当中,我们也无法确定在我们给出的 14 张图像中有多少是优良的。由此,我们必须读入所有的图像并仅选取好的图像。

  • 除了棋盘,我们也可使用圆形网格。在此之下,我们必须使用cv.cornerSubPix()函数用以寻找图案。使用圆形网格使得相机需更少的图像便可足以校准。

一旦我们找到了角点,我们可以使用cv.cornerSubPix()函数来提高它们的准确性。我们同样可以使用 cv.drawChessboardCorners()函数绘制图案。所有步骤均包含在下面代码当中:

  1. import numpy as np
  2. import cv2 as cv
  3. import glob
  4. # 终止标准
  5. criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
  6. # 准备对象点, 如 (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
  7. objp = np.zeros((6*7,3), np.float32)
  8. objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)
  9. # 用于存储所有图像对象点与图像点的矩阵
  10. objpoints = [] # 在真实世界中的 3d 点
  11. imgpoints = [] # 在图像平面中的 2d 点
  12. images = glob.glob('*.jpg')
  13. for fname in images:
  14. img = cv.imread(fname)
  15. gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
  16. # 找到棋盘上所有的角点
  17. ret, corners = cv.findChessboardCorners(gray, (7,6), None)
  18. # 如果找到了,便添加对象点和图像点(在细化后)
  19. if ret == True:
  20. objpoints.append(objp)
  21. corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
  22. imgpoints.append(corners)
  23. # 绘制角点
  24. cv.drawChessboardCorners(img, (7,6), corners2, ret)
  25. cv.imshow('img', img)
  26. cv.waitKey(500)
  27. cv.destroyAllWindows()

绘制完成的图像如下所示:

calib_pattern

calib pattern image

校准

现在我们拥有了对象点与图像点,我们便可以准备开始校准了。我们使用函数返回相机矩阵,畸变系数,旋转和平移向量等等。

  1. ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

矫正

现在,我们可以选取图像并矫正它们了。OpenCV 包含有两种方案来实现这件事。然而首先,我们需要使用 cv.getOptimalNewCameraMatrix()函数根据自由缩放系数细化相机矩阵。如果缩放参数 alpha = 0,这个函数将返回最小不必要像素的校正图像。所以它甚至可能会移除图像角落的一些像素。如果 alpha = 1,则所有像素都会保留一些额外的黑色图像。此函数还返回图像 ROI,可用于裁剪结果。

所以我们需要一张新的图像。(在此选用 left12.jpg,这是本章的第一张图片)

  1. img = cv.imread('left12.jpg')
  2. h, w = img.shape[:2]
  3. newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))

1. 使用cv.undistort()函数

这是最简单的方法。只需要调用这个函数并使用使用上面获得的 ROI 来裁剪结果。

  1. # 矫正
  2. dst = cv.undistort(img, mtx, dist, None, newcameramtx)
  3. # 裁切图像
  4. x, y, w, h = roi
  5. dst = dst[y:y+h, x:x+w]
  6. cv.imwrite('calibresult.png', dst)

2. 使用重映射

这个方法稍微困难一点,首先,找到一个从畸变的图像到矫正过的图像的映射函数。然后使用重映射函数。

  1. # 矫正
  2. mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
  3. dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
  4. # 裁切图像
  5. x, y, w, h = roi
  6. dst = dst[y:y+h, x:x+w]
  7. cv.imwrite('calibresult.png', dst)

尽管如此,这两种方法都给出了相同的结果。如下:

calib_result.jpg

calib result image

可以看到现在所有边都是直的。

现在你可以利用 NumPy 中的写入函数(np.savez, np.savetxt 等)用来保存你的相机矩阵和畸变系数用以备用。

重投影误差

重投影误差可以很好的估计我们计算出的参数的精确程度。重投影误差越接近于零,我们计算出的参数便越准确。给出固有,畸变,旋转和平移矩阵,我们首先必须使用cv.projectPoints()函数将对象点转换为图像点。然后,我们便可以计算出我们变换结果和发现角点的算法之间的绝对范数。为了找到平均误差,我们将计算所有图像计算误差的算术平均值。

  1. mean_error = 0
  2. for i in xrange(len(objpoints)):
  3. imgpoints2, _ = cv.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
  4. error = cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2)/len(imgpoints2)
  5. mean_error += error
  6. print( "total error: {}".format(mean_error/len(objpoints)) )

其他资源

练习

  1. 尝试用圆形网格进行相机校准