【翻译:OpenCV-Python教程】摄像头校准

⚠️这篇是按4.1.0翻译的,你懂得。

⚠️除了版本之外,其他还是照旧,Camera Calibration,原文

目标

在本节,我们会学到:

  • 由摄像头引起的失真的类型
  • 如何找到相机的内在和外在特性
  • 如何基于这些属性还原图像

基础

一些针孔相机会对图像造成严重失真。两种主要的畸变是径向畸变和切向畸变。

径向畸变使直线显得弯曲。距离图像中心越远,径向畸变越大。比如,如下的这一张图像,用两条红线标出了国际象棋棋盘边缘。同时你也可以发现,棋盘的边线并不是沿着红线的一条直线。所有本该是直线的线条都凸出来了。查看 Distortion (optics) 了解更多信息。

径向畸变可以表示为:

x_{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)

y_{distorted} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)

同样的,切向畸变的发生是由于成像透镜没有与成像平面完全平行。所以,图像的某些区域看起来好像比预期的位置更近。切向畸变量可表示为:

x_{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)

y_{distorted} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)

简单的说,我们得找出五个参数,也就是失真洗漱,如下给出:

Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3)

除此之外,我们还需要点其他的信息,比如摄像头固有参数和外在参数。固有参数是由摄像头决定的,它们包括焦距(fx,fy)还有光学中心点(cx,cy)。焦距和光学中心点可以被用于创建一个摄像头矩阵。这个矩阵可用于消除特定相机镜头造成的失真。摄像头矩阵对于特定摄像机是唯一的,因此一旦计算出来,它就可以在同一摄像机拍摄的其他图像上重复使用。它表示为3x3矩阵:

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),并且摄像头也相应的移动了。这样的考虑方式帮助我们可以只算出XY值。现在对于X,Y值,我们可以简单的传入点,比如(0,0), (1,0), (2,0), ... 用于表示点的位置。在这种情况下,我们得到的结果将是棋盘方格的放缩后的大小。但如果我们知道方格尺寸(比方说 30 mm),我们可以传入这样的值(0,0), (30,0), (60,0), ... . 于是我们得到的结果就是mm为单位的。(当前情况下,我们不知道方格的尺寸,因为我们没有取那些图像,所以我们按照方格尺寸放缩的模式传参)。

3D 点被称为是object points 对象点,而2D 图像上的点被称为 image points 图象点。

体系

所以想要找出棋盘上的,我们可以使用这个函数,cv.findChessboardCorners()。我们还需要传入我们在寻找的形态。比如说8x8 网格,5x5网格等等。在这个例子里,我们使用7x6网格。(通常,国际象棋棋盘是8x8正方形和7x7内角)。它返回一个角点并且它的返回值将会为真,如果确实包含了这个形态的话,这些角点会被按顺序排列(从左到右,由上自下)。

也可以看看

此功能可能无法找到所需的图案中的所有图像。所以,一个好建议是这样写代码,它以被要求的模式启动摄像头然后检查每一帧。一旦获取到了这个模式,找出角点并且存储在一个list中。此外,读取下一帧,使我们可以在不同的方向上调整我们的国际象棋棋盘之前提供一些间隔。继续这个过程,直到获得所需的良好模式的数量。即使在这里提供的例子中,我们不知道多出来的超过14的图像有多少是好的。因此,我们必须读取所有图像和只拿好的。

我们或许可以使用圆形网格来代替棋盘。这种情况下,我们必须使用方法 cv.findCirclesGrid() 来算出模式。使用圆形的网格来校准摄像头需要的图像较少。

一旦我们找出了角点,我们可以使用函数 cv.drawChessboardCorners() 来增加他们的准确率,所有的步骤都包含在以下代码中:

import numpy as np
import cv2 as cv
import glob
# termination criteria
criteria = (cv.TERM_CRITERIA_EPS + cv.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 = cv.imread(fname)
    gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # Find the chess board corners
    ret, corners = cv.findChessboardCorners(gray, (7,6), None)
    # If found, add object points, image points (after refining them)
    if ret == True:
        objpoints.append(objp)
        corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
        imgpoints.append(corners)
        # Draw and display the corners
        cv.drawChessboardCorners(img, (7,6), corners2, ret)
        cv.imshow('img', img)
        cv.waitKey(500)
cv.destroyAllWindows()

以下显示的是一张图像以及在其上画出的模式:

校准

现在,我们有我们的对象点和图像点,我们已经准备好去校准。我们可以用函数 cv.calibrateCamera() 返回摄像头矩阵,畸变系数,旋转和平移矢量等。

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

反失真

现在我们拿到了一张图像来对它做反失真,OpenCV为此自带了一个函数。但是首先,我们可以使用 cv.getOptimalNewCameraMatrix() 函数来修正摄像头矩阵,基于任意的放缩参数。假设初始的放缩参数 alpha=0,它在“最小不必要像素”的前提下返回反失真后的图像。所以它甚至有可能在图像角点上去掉一些像素点。如果alpha=1,所有的像素都会被保留,还带着一些额外的黑色图像。这个函数同时也返回一个图像的感兴趣区域,可以被用作裁剪图像结果。

所以,我们去一个新的图像(以left12.jpg 为例,这是本章的第一个图像)

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

1. 使用 cv.undistort()

这是最简单的方式。只要调用函数,并且使用获取到的ROI感兴趣区裁剪图像即可。

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

2. 使用 remapping

这种方式更复杂,首先找出失真图像到反失真图像的映射函数。然后使用反映射函数。

# undistort
mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)

不过,两种方式都会返回相同的结果。看以下的结果:

你可以看一下结果,所有的边都是直的。

现在你可以使用Numpy的写函数NumPy(np.savez, np.savetxt etc)来缓存摄像头矩阵以及失真系数为未来的使用做准备。

重新投影误差

重投影误差对我们找出的参数有多精准给出了一个非常好的估计。它越接近0,我们找出的参数就越准确。由于固有的,失真,旋转,平移矩阵,我们必须使用 cv.projectPoints() 函数转变对象点到图像点。然后,我们可以计算通过转换我们所得到的和算法找出的角点之间的绝对范数。为了找出平均误差,我们为所有的校准图像计算所有误差的算术平均值。

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

额外资源

练习

  1. 尝试用圆形网格来校准摄像头

上篇:【翻译:OpenCV-Python教程】背景减法

下篇:【翻译:OpenCV-Python教程】图像金字塔

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值