摄像头矫正

Goal

在这部分

  • 我们将学习相机的失真,相机的内在和外在参数等。
  • 我们将学会找到这些参数,不失真的图像等。

Basics

如今,便宜的单孔摄像头会引起图像的失真。主要有两种失真:径向失真和切向失真。

由于径向失真,直线会出现弯曲。随着我们从图像中心移动到边缘,它的影响越来越严重。比如下面的图像,棋盘的两条边用红线标记出来了。但是你可以看到边界不是直线,和红线不匹配。所有预期的直线都凸出来了。

img

失真可以如下解决:

img

同样地,切向失真发生是因为图像采集镜头没有完全平行于成像平面对齐。因此,图像中的某些区域可能看起来比预期更近。它的解决如下:

img

总之,我们需要找到五个参数,称为失真系数,由下式给出:

img

除此之外,我们还需要找到其他一些信息,如摄像头的内在参数和外在参数。内在参数是摄像头特有的。它包括像焦距 $(f_x, f_y)$,光学中心 $(c_x,c_y)$ 等信息。它也叫做摄像头矩阵。它只依赖摄像头本身,一旦被计算出来,就可以永久保存。可以用如下 3x3 矩阵表示:

img

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

对立体声应用,这些失真首先需要被矫正。为了找到所有这些参数,我们必须做的是提供一些样本图像。我们在其中找到一些具体点(棋盘上的方角)。我们知道它在现实世界空间中的坐标,并且知道它在图像中的坐标。有了这些数据,我们可以解决一些数学问题以获得失真系数。为了获得更好的结果,我们需要至少10个测试模式。

Code

正如上面提到的,我们需要至少10张测试图片来进行摄像头矫正。OpenCV 。为了易于理解,我们只考虑一张棋盘图像。摄像头校准需要的重要的输入数据是真实世界的3D点集合,和它对应的2D图像点。2D图像点是可以从图像中轻松找到的。(这些图像点是两个黑色方块在棋盘上相互接触的位置)

那么真实世界空间中的3D点呢?这些图像是从静态相机拍摄的,棋盘放置在不同的位置和方向。所以我们需要知道 $(X,Y,Z)$ 值。但为了简单起见,我们可以说棋盘在XY平面上保持静止,(因此总是Z=0)并且相应地移动相机。这种考虑有助于我们找到只有X,Y值。现在对于X,Y值,我们可以简单地将点传递为(0,0),(1,0),(2,0),…这表示点的位置。在这种情况下,我们得到的结果将会是棋盘格的大小。但是如果我们知道方形尺寸(例如30毫米),并且我们可以将值作为(0,0),(30,0),(60,0),…,我们得到的结果是毫米。(在这种情况下,我们不知道平方尺寸,因为我们没有拍这些图像,所以我们通过平方尺寸)。

一旦找到角落,我们可以使用cv2.cornerSubPix()来提高它们的准确性。我们也可以使用cv2.drawChessboardCorners()来绘制模式。所有这些步骤都包含在下面的代码中:

Setup

为了找到棋盘中的pattern,我们使用函数cv2.findChessboardCorners()。我们也需要传递我们寻找的是哪种pattern,比如 8x8 的网格,5x5的网格。在这个例子中,我们使用7x6的网格。(通常一个棋盘有8x8个网格和7x7个内角)。它返回角点和retval,如果获得pattern,retval将为True。这些角落将按顺序排列(从左到右,从上到下)。

Calibration

所以现在我们有了准备进行校准的物体点和图像点。进行校准所使用的函数是cv2.calibrateCamera()。它返回摄像头矩阵,失真系数,旋转和平移向量等。

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

Undistortion

现在我们取一张图片,然后使它去失真。OpenCV提供了两种方法,我们来看看。但在此之前,我们可以使用cv2.getOptimalNewCameraMatrix() 基于自由缩放参数来优化摄像头矩阵。如果缩放参数alpha=0,它会返回不失真的图像,并将不需要的像素减至最少。所以它甚至可以去除图像角落处的一些像素。如果alpha=1,所有像素都会保留,并且有一些额外的黑色图像。它还会返回可用于裁剪结果的图像ROI。

所以我们取一张新的图片:

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)

两种方法都得到了相同的结果:
img
你可以看到在这个结果中,所有的边都是直的。

Re-projection Error

再投影误差可以很好地估计找到的参数的精确程度。这应尽可能接近于零。考虑到内在参数,失真,旋转和平移矩阵,我们首先使用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)