9.1. 摄像机校准

9.1.1. 目标

在本节中,
-我们将学习相机中的失真、相机的内在和外在参数等-我们将学习寻找这些参数、不失真图像等。

9.1.2. 基础

今天的廉价针孔相机给图像带来了很多失真。两种主要的畸变是径向畸变和切向畸变。

由于径向变形,直线会出现弯曲。当我们离开图像中心时,它的效果更明显。例如,下面显示了一幅图像,其中棋盘的两个边缘用红线标记。但你可以看到边界不是一条直线,与红线不匹配。所有预期的直线都凸出了。访问 Distortion (optics) 了解更多详细信息。

该失真的解决方法如下:

\[开始{aligned}x{corrected}=x(1+k 1 r^2+k 2 r^4+k 3 r^6)\y{corrected}=y(1+k1 r^2+k2 r^4+k3 r^6)结束{aligned}\]

类似地,另一个失真是切向失真,这是因为摄像镜头没有与成像平面完全平行地对齐。所以图像中的某些区域看起来可能比预期的要近。解决方法如下:

\[\begin{split}\begin{aligned} x_{corrected} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{corrected} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy] \end{aligned}\end{split}\]

简言之,我们需要找到5个参数,即失真系数:

\[畸变系数=(kÓ1hspace{10pt}k 2hspace{10pt}p 1hspace{10pt}p2hspace{10pt}k 3)\]

除此之外,我们还需要找到一些更多的信息,比如相机的内部和外部参数。内在参数是相机特有的。包括焦距等信息 (\(f_x,f_y\) ),光学中心 (\(c_x, c_y\) )它也被称为相机矩阵。它只取决于相机,所以一旦计算出来,它就可以存储起来以备将来使用。它表示为3x3矩阵:

\[\begin{split}\begin{aligned} camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] \end{aligned}\end{split}\]

外部参数对应于将三维点的坐标转换为坐标系的旋转和平移向量。

对于立体声应用,这些失真需要首先纠正。要找到所有这些参数,我们必须提供一些定义良好的模式(如棋盘)的示例图像。我们在其中找到一些特定的点(棋盘的方角)。我们知道它在现实世界中的坐标,也知道它在图像中的坐标。利用这些数据,在背景中求解一些数学问题,得到畸变系数。这是整个故事的总结。为了获得更好的结果,我们需要至少10个测试模式。

9.1.3. 代码

如上所述,我们需要至少10个测试模式的相机校准。OpenCV附带了一些棋盘图像(参见 samples/cpp/left01.jpg -- left14.jpg ),所以我们会利用它。为了理解,只考虑棋盘的一个图像。摄像机标定所需的重要输入数据是一组三维真实世界点及其对应的二维图像点。二维图像点是可以的,我们可以很容易地从图像中找到。(这些图像点是棋盘上两个黑色方块相互接触的位置)

从现实世界的空间看三维点呢?这些图像是从一个静态相机和棋盘放置在不同的位置和方向。所以我们需要知道 \((X,Y,Z)\) 价值观。但为了简单起见,我们可以说棋盘在XY平面上保持静止(所以Z始终为0),相机也相应地移动。这种考虑有助于我们只找到X,Y值。现在对于X,Y值,我们可以简单地将点传递为(0,0),(1,0),(2,0),…这表示点的位置。在这种情况下,我们得到的结果将是棋盘正方形的大小。但是如果我们知道正方形的大小(比如30mm),我们可以把值传递为(0,0),(30,0),(60,0),…,我们得到的结果是mm。(在这种情况下,我们不知道正方形大小,因为我们没有拍摄这些图像,所以我们通过正方形大小)。

三维点称为 对象点 二维图像点被称为 图像点。

安装程序

为了找到棋盘上的图案,我们使用函数, cv2.findChessboardCorners()) . 我们还需要传递我们所看到的模式,如8x8网格、5x5网格等。在本例中,我们使用7x6网格。(通常棋盘有8x8个正方形和7x7个内角)。它返回角点和retval,如果获得模式则为True。这些角将按顺序排列(从左到右,从上到下)

此函数可能无法在所有图像中找到所需的图案。所以一个好的选择是编写代码,这样,它启动相机,检查每帧所需的模式。获得模式后,找到角落并将其存储在列表中。在阅读下一帧之前还提供一些间隔,以便我们可以在不同方向上调整棋盘。继续此过程,直到获得所需数量的良好模式。即使在这里提供的示例中,我们也不确定给出的14个图像中有多少是好的。所以我们把所有的图片都读了,拍了好的。

我们可以使用一些圆形网格代替棋盘,但随后使用函数 cv2.findCirclesGrid() 找到模式。在使用圆网格时,可以减少图像的数量。

一旦我们找到角落,我们就可以使用 cv2.角点组件() . 我们也可以用 cv2.棋盘转角() . 所有这些步骤都包含在下面的代码中:

>>> %matplotlib inline
>>> import matplotlib.pyplot as plt
>>>
>>> import os
>>> import numpy as np
>>> import cv2
>>> import glob
>>>
>>> # termination criteria
>>> criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
>>>
>>> # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
>>> objp = np.zeros((6*7,3), np.float32)
>>> objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)
>>>
>>> # Arrays to store object points and image points from all the images.
>>> objpoints = [] # 3d point in real world space
>>> imgpoints = [] # 2d points in image plane.
>>>
>>> os.chdir('/cvdata/')
>>> images = glob.glob('*.jpg')
>>>
>>> for fname in images:
>>>     img = cv2.imread(fname)
>>>     gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
>>>
>>>     # Find the chess board corners
>>>     ret, corners = cv2.findChessboardCorners(gray, (7,6),None)
>>>
>>>     # If found, add object points, image points (after refining them)
>>>     if ret == True:
>>>         objpoints.append(objp)
>>>
>>>         corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
>>>         imgpoints.append(corners2)
>>>
>>>         # Draw and display the corners
>>>         img = cv2.drawChessboardCorners(img, (7,6), corners2,ret)
>>>         plt.imshow(img)
>>>
>>>
>>>         # cv2.imshow('img',img)
>>>         # cv2.waitKey(500)
>>>
>>> # cv2.destroyAllWindows()
../_images/sec01-calibration_1_0.png

一张画有图案的图片如下所示:

校准

所以现在我们有了目标点和图像点,我们可以进行校准了。为此我们使用函数, 校准照相机() . 它返回摄像机矩阵、畸变系数、旋转和平移向量等。

>>> ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)
>>> np.savez('/tmp/x_B.npz', ret = ret, mtx= mtx, dist = dist, rvecs = rvecs, tvecs = tvecs)

不失真

我们已经得到了我们想要的。现在我们可以拍摄一个图像,不受干扰。OpenCV提供了两种方法,我们将看到这两种方法。但在此之前,我们可以使用 cv2.getoptimenewcameramatrix() . 如果缩放参数 alpha=0 ,它返回未失真的图像,其中包含最少不需要的像素。所以它甚至可以去除图像角的一些像素。如果 alpha=1 ,所有像素都保留有一些额外的黑色图像。它还返回一个图像ROI,可以用来裁剪结果。

所以我们换个新形象 (left12.jpg 在这种情况下。这是本章的第一幅图片)

>>> img = cv2.imread('/cvdata/left12.jpg')
>>> h,  w = img.shape[:2]
>>> newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))

一。使用 cv2.不中断()

这是最短的路。只需调用函数并使用上面获得的ROI来裁剪结果。

>>> # undistort
>>> dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
>>>
>>> # crop the image
>>> x,y,w,h = roi
>>> dst = dst[y:y+h, x:x+w]
>>> cv2.imwrite('calibresult.png',dst)
True

2。使用 重新映射

这是弯道。首先找到一个从畸变图像到未畸变图像的映射函数。然后使用remap函数。

>>> # undistort
>>> mapx,mapy = cv2.initUndistortRectifyMap(mtx,dist,None,newcameramtx,(w,h),5)
>>> dst = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)
>>>
>>> # crop the image
>>> x,y,w,h = roi
>>> dst = dst[y:y+h, x:x+w]
>>> cv2.imwrite('calibresult.png',dst)
True
>>> plt.imshow(dst)
<matplotlib.image.AxesImage at 0x7f4805ee0d68>
../_images/sec01-calibration_11_1.png

两种方法得出的结果相同。结果如下:

从结果中可以看出所有的边都是直的。

现在您可以使用Numpy(np.savez,np.savetxt等)中的写函数来存储相机矩阵和失真系数,以备将来使用。

9.1.4. 重投影误差

重投影误差能很好地估计出所找到的参数的精确程度。这应该尽可能接近零。给定内在矩阵、失真矩阵、旋转矩阵和平移矩阵,我们首先使用 项目点() . 然后我们计算了我们得到的与角点搜索算法之间的绝对范数。为了找出平均误差,我们计算了所有校准图像的误差算术平均值。

>>> mean_error = 0
>>> for i in range(len(objpoints)):
>>>     imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
>>>     error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2)/len(imgpoints2)
>>>     mean_error += error
>>> print( "total error: {}".format(mean_error/len(objpoints)) )
total error: 0.02831772349383625

9.1.5. 额外资源

9.1.6. 练习

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