文章目录
- Camera Calibration 相机校准
- 目标
- 基础知识
- 代码
- 设置
- 校准
- 去失真
- 1. 使用 **cv.undistort()**
- 2. 使用 **remapping**
- 重新投影误差
Camera Calibration 相机校准
目标
在本节中,我们将学习
- 相机造成的失真类型
- 如何找到相机的内在和外在属性
- 如何根据这些属性消除图像失真
基础知识
一些针孔相机会给图像带来严重的失真。两种主要的失真是径向失真和切向失真。
径向失真会导致直线出现弯曲。点距离图像中心越远,径向失真就越大。例如,下面显示了一个图像,其中棋盘的两个边缘用红线标记。但是,您可以看到棋盘的边框不是直线,与红线不匹配。所有预期的直线都凸出。请访问 失真(光学) 了解更多详细信息。
径向畸变可以表示如下:
x d i s t o r t e d = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) y d i s t o r t e d = 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) xdistorted=x(1+k1r2+k2r4+k3r6)ydistorted=y(1+k1r2+k2r4+k3r6)
同样,切向畸变的发生是因为拍摄镜头与成像平面没有完全平行。因此,图像中的某些区域可能看起来比预期的更近。切向畸变量可以表示如下:
x d i s t o r t e d = x + [ 2 p 1 x y + p 2 ( r 2 + 2 x 2 ) ] y d i s t o r t e d = y + [ p 1 ( r 2 + 2 y 2 ) + 2 p 2 x y ] x_{distorted} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy] xdistorted=x+[2p1xy+p2(r2+2x2)]ydistorted=y+[p1(r2+2y2)+2p2xy]
简而言之,我们需要找到五个参数,称为畸变系数,如下所示:
D i s t o r t i o n c o e f f i c i e n t s = ( k 1 k 2 p 1 p 2 k 3 ) Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3) Distortioncoefficients=(k1k2p1p2k3)
除此之外,我们还需要一些其他信息,例如相机的内在参数和外在参数。内在参数特定于相机。它们包括焦距 ( f x , f y f_x,f_y fx,fy)和光学中心( c x , c y c_x, c_y cx,cy)等信息。焦距和光学中心可用于创建相机矩阵,该矩阵可用于消除由于特定相机的镜头而导致的失真。相机矩阵对于特定相机是唯一的,因此一旦计算出来,就可以在同一相机拍摄的其他图像上重复使用。它表示为 3x3 矩阵:
[ c a m e r a m a t r i x = [ f x 0 c x 0 f y c y 0 0 1 ] ] [camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ]] [cameramatrix= fx000fy0cxcy1 ]
外部参数对应于将 3D 点的坐标转换为坐标系的旋转和平移向量。
对于立体应用,需要首先纠正这些失真。为了找到这些参数,我们必须提供一些定义明确的图案(例如棋盘)的样本图像。我们找到一些我们已经知道相对位置的特定点(例如棋盘上的方角)。我们知道这些点在现实世界空间中的坐标,也知道它们在图像中的坐标,因此我们可以求解失真系数。为了获得更好的结果,我们至少需要 10 个测试图案。
代码
如上所述,我们需要至少 10 个测试模式来进行相机校准。OpenCV 附带了一些棋盘图像(参见 samples/data/left01.jpg – left14.jpg),因此我们将利用这些图像。考虑一张棋盘图像。相机校准所需的重要输入数据是一组 3D 真实世界点和这些点在图像中对应的 2D 坐标。2D 图像点是可以的,我们可以从图像中轻松找到它们。(这些图像点是棋盘中两个黑色方块相互接触的位置)
那么来自真实世界空间的 3D 点呢?这些图像是从静态相机拍摄的,棋盘放置在不同的位置和方向。所以我们需要知道 ( X , Y , Z ) (X,Y,Z) (X,Y,Z) 值。但为了简单起见,我们可以说棋盘在 XY 平面上保持静止(因此 Z=0 始终),相机相应地移动。这种考虑有助于我们仅找到 X、Y 值。现在对于 X、Y 值,我们可以简单地将点传递为 (0,0)、(1,0)、(2,0) …,这表示点的位置。在这种情况下,我们得到的结果将以棋盘正方形的大小为单位。但如果我们知道正方形的大小(例如 30 毫米),我们可以将值传递为 (0,0)、(30,0)、(60,0) …。因此,我们得到的结果以毫米为单位。(在这种情况下,我们不知道正方形的大小,因为我们没有拍摄这些图像,所以我们以正方形大小的形式传递)。
3D 点称为对象点,2D 图像点称为图像点。
设置
因此,要查找棋盘中的图案,我们可以使用函数 cv.findChessboardCorners()。我们还需要传递我们正在寻找的图案类型,例如 8x8 网格、5x5 网格等。在此示例中,我们使用 7x6 网格。(通常棋盘有 8x8 个方格和 7x7 个内角)。它返回角点和 retval,如果获得图案,则为 True。这些角将按顺序排列(从左到右、从上到下)
@note 此函数可能无法在所有图像中找到所需的图案。因此,一个不错的选择是编写代码,使其启动相机并检查每一帧是否需要图案。获得图案后,找到角并将其存储在列表中。此外,在读取下一帧之前提供一些间隔,以便我们可以向不同方向调整棋盘。继续此过程,直到获得所需数量的良好图案。即使在这里提供的示例中,我们也不确定给出的 14 张图像中有多少张是好的。因此,我们必须读取所有图像并仅选取好的图像。
@note 除了棋盘,我们还可以使用圆形网格。在这种情况下,我们必须使用函数 cv.findCirclesGrid() 来查找模式。使用圆形网格执行相机校准所需的图像较少。
一旦我们找到角点,我们就可以使用 cv.cornerSubPix() 来提高其准确性。我们还可以使用 cv.drawChessboardCorners() 绘制图案。所有这些步骤都包含在以下代码中:
import numpy as np
import cv2 as cv
import glob
# 终止标准
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 准备对象点,如 (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)
# 用于存储所有图像中的对象点和图像点的数组。
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,则保留所有像素,并添加一些额外的黑色图像。此函数还返回一个图像 ROI,可用于裁剪结果。
因此,我们拍摄一张新图像(本例中为 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 来裁剪结果。
dst = cv.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪图像
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)
2. 使用 remapping
这种方式稍微有点困难。首先,找到一个从失真图像到未失真图像的映射函数。然后使用重映射函数。
mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
# 裁剪图像
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)
尽管如此,两种方法的结果相同。请参见下面的结果:
您可以在结果中看到所有边缘都是直的。
现在,您可以使用 NumPy 中的写入函数(np.savez、np.savetxt 等)存储相机矩阵和失真系数,以备将来使用。
重新投影误差
重新投影误差可以很好地估计找到的参数的精确度。重新投影误差越接近零,我们找到的参数就越准确。给定固有矩阵、失真矩阵、旋转矩阵和平移矩阵,我们必须首先使用 cv.projectPoints() 将对象点转换为图像点。然后,我们可以计算通过变换得到的值与角点查找算法得到的值的绝对范数。为了找到平均误差,我们计算所有校准图像计算出的误差的算术平均值。
mean_error = 0
for i in range(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)) )