目标
- 我们将了解导致相机失真、扭曲的内因与外因
- 我们将试着找到这些畸变参数,并消除畸变
基础
如今大量廉价的摄像机导致了很多照片畸变。两个主要的畸变是径向畸变和切向畸变。
由于径向畸变,直线会变弯。距离图片中心越远,它的影响越大。如下面这张图片,棋盘格中被红线标记的边缘。你会发现棋盘格的边缘并不与直红线重合,而是变弯了。可以到维基百科查看更多细节Distortion (optics) 。
这种畸变可以用如下公式消除:
同样的,另一种畸变-切向畸变是由于相机镜头没有与被拍摄物体平行造成的。因为有些区域会看起来更近。这可以通过下面公式消除:
总的来说,我们需要找到5个参数,即畸变系数:
除此之外,我们还需要找到一些另外的信息,如相机的内在和外部参数。内在参数是由相机内部决定的,它取决于特定的相机。它包括焦距(),光心()等。这些也称作相机矩阵。它们只取决于相机本身,因此只需要计算一次。
外部参数对应于旋转和平移向量,它将三维点的坐标转换为坐标系统。
对于三维应用程序,上面这些畸变需要先被计算。要找到所有这些参数,我们需要做的是提供一些定义好的模式的示例图像(如棋盘格)。我们找到模式中特定的点(格子的交点)。这样,我们知道这些点在真实世界中的坐标,也知道它们在图片中的坐标。有了这些数据,就可以解决一些数学问题,从而得到畸变系数。这就是解决整个问题的思路。为了得到更好的结果,我们至少需要10张测试模式图片。
代码
如上所述,我们至少需要10张测试模板图片来进行相机标定。OpenCV提供了一些棋盘格图片(samples/cpp/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点称作图片点。
设置
为了找到棋盘格的样式,我们使用cv2.findChessboardCorners()这个函数。我们同样需要传递我们要找什么样式,比如你这个棋盘格是8*8的,或是5*5的。在这个例子中,我们使用7*6的棋盘格。(通常棋盘格有8*8个方格,有7*7个内格点)。它返回每一一个角点,如果匹配到了模式,它将返回是True。这些角点将按一定顺序标注出来(从左到右,从上到下)
注意
这个函数并不能识别每张图片的样式。因此一个更好的选择是按照如下的思路写代码,打开摄像头检查每一帧图片是否包含样式。如果包含了需要的样式,找到角点并把他们存到一个列表中。同时提供一些间隔的时间,以便于我们调整棋盘格到不同的方向。重复这些步骤,直到足够多的图片被采集。即使在我们提供的例子中,我们也不确定14张图片中有多少合格的图片。因此我们要使用所有的图片,然后在其中找合适的一些。
注意
除了使用棋盘格,我们也可以使用圆形格子,这就需要使用cv2.findCirclesGrid() 来寻找样式。据说,使用圆形网格的时候可以减少图片的采样数。
当我们找到角点之后,我们可以使用cv2.cornerSubPix()这个函数来增加坐标精度。也可以使用cv2.drawChessboardCorners()将角点标注出来。所有这些步骤都包含在以下代码中:
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. 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) cv2.imshow('img',img) cv2.waitKey(500) cv2.destroyAllWindows()
一张标记了角点的图片如下所示:
标定
现在我们有了物体坐标和图片坐标,是时候开始标定相机了。我们使用cv2.calibrateCamera()这个函数。它返回相机矩阵、畸变系数、旋转和平移向量等。
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)
消除畸变
我们已经得到了所有数据。现在我们可以拿一张图片来消除它的畸变。OpenCV中有两种方法,我们一一来看。在那之前,我们可以根据一个自由尺度参数来改进相机矩阵。cv2.getOptimalNewCameraMatrix() 变换参数alpha=0,它使用最小不需要的像素返回校正的图像。如果alpha=1 所有的像素都保留下来,并且包括一些额外的黑色图像。它还返回一个图像ROI,可以用来裁剪结果。
我们选取一张新图片(left2.jpg)
img = cv2.imread('left12.jpg') h, w = img.shape[:2] newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))
1. Using cv2.undistort()
这是最简单的办法,通过调用函数,传递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)
2. Using remapping
这种方法麻烦一点。首先找到原图片与校正图片之间映射函数。然后使用重映射函数。
# 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)
两种方法都可以得到同样的结果,如下图:
你会看到结果中,所有的边都是直的。
现在你可以使用Numpy的函数(np.savez,np.savetxt etc)将相机矩阵和畸变系数存起来,以便以后使用。
重投影误差
重投影误差给出了一个很好的判别方式来检测找到的畸变参数的准确度。它尽可能的趋近于0. 考虑到固有的,扭曲的,旋转和平移矩阵,我们首先将物体点坐标变换到图片点坐标,使用cv2.projectPoints()函数。然后我们计算我们变换后得到的图片点和我们通过算法得到的角点坐标间的绝对范数。为了找到平均误差,我们计算了所有校准图像的误差的算术平均值。
mean_error = 0 for i in xrange(len(objpoints)): imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2) tot_error += error print "total error: ", mean_error/len(objpoints)