目标
在本部分,我们将学习
- 相机引起的不同类型的失真
- 如何查找相机的内在和外在属性
- 如何根据这些属性对图像去失真
基础
有些针孔相机引入的图像失真非常大。两种主要失真类型是径向失真和切向失真。
径向失真使得直线看起来弯曲。离图像中心越远,径向失真就越大。例如,下面显示了一张图像,棋盘的两个边缘用红色线条标记。但是,你可以看到,棋盘的边框不是直线,与红线不匹配。所有预期的直线都被向外凸出。访问畸变(光学)以获取更多详细信息。
图像
径向失真可表示如下
\[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 + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy]\]
简而言之,我们需要找到五个参数,称为畸变系数,由下式给出
\[畸变系数=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3)\]
除此之外,我们还需要其他一些信息,例如相机的内在和外在参数。内在参数特定于相机。它们包括焦距(\(f_x,f_y\))和光学中心(\(c_x, c_y\))。焦距和光学中心可用于创建相机矩阵,该矩阵可用于去除特定相机镜头引起的失真。相机矩阵特定于特定相机,因此一旦计算出来,就可以在同一相机拍摄的其他图像上重复使用。它表示为 3x3 矩阵
\[相机矩阵 = \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 一直是固定),相机相应移动。考虑到这一点有助于我们只找到 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。这些角点将按照顺序放置(从左到右,从上到下)
- 注意
- 在所有图像中,此函数可能无法找到所需的模式。因此,一个不错的选择是编写代码,然后启动摄像头并检查每一帧的所需模式。一旦获得模式,找到角点并存储在一个列表中。此外,在读取下一帧之前提供一些时间间隔,以便可以将棋盘调整到不同的方向。继续此过程,直到获得所需数量的良好模式为止。即使在本文提供的示例中,我们也无法确定 14 幅图像中有多少图像可用。因此,我们必须读取所有图像并仅采用良好的图像。
- 除了棋盘外,我们还可以使用圆形网格。在这种情况下,我们必须使用函数 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)
objp = np.zeros((6*7,3), np.float32)
objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2)
objpoints = []
imgpoints = []
images = glob.glob('*.jpg')
for fname in images
if ret == True
objpoints.append(objp)
imgpoints.append(corners2)
void drawChessboardCorners(InputOutputArray image, Size patternSize, InputArray corners, bool patternWasFound)
绘制检测到的棋盘角点。
bool findChessboardCorners(InputArray image, Size patternSize, OutputArray corners, int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE)
查找棋盘的内部角点位置。
void imshow(const String &winname, InputArray mat)
显示指定窗口的图像。
int waitKey(int delay=0)
等待按下的键。
void destroyAllWindows()
销毁所有 HighGUI 窗口。
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR)
从文件中加载图像。
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)
将图像从一个色彩空间转换到另一个色彩空间。
void cornerSubPix(InputArray image, InputOutputArray corners, Size winSize, Size zeroZone, TermCriteria criteria)
优化角点位置。
下面显示了一幅绘制了图案的图像
图像
校正
现在,我们有了我们的目标点和图像点,我们就可以进行校正了。我们可以使用函数 cv.calibrateCamera(),该函数返回相机矩阵、畸变系数、旋转和平移向量等。
ret, mtx, dist, rvecs, tvecs =
cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],
None,
None)
double calibrateCamera(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints, Size imageSize, InputOutputArray cameraMatrix, InputOutputArray distCoeffs, OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs, OutputArray stdDeviationsIntrinsics, OutputArray stdDeviationsExtrinsics, OutputArray perViewErrors, int flags=0, TermCriteria criteria=TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON))
从校准图案的多个视图中找到相机内参和外参。
畸变校正
现在,我们可以拍摄图像并对其进行畸变校正。OpenCV 带有两种执行此操作的方法。但首先,我们可以使用cv.getOptimalNewCameraMatrix()基于自由缩放参数优化相机矩阵。如果缩放参数 alpha = 0,它将返回具有最少不需要像素的未畸变图像。因此,它甚至可能会移除图像角部的一些像素。如果 alpha = 1,则将保留所有像素,并带有一些额外的黑色图像。此函数还返回可用于裁剪结果的图像 ROI。
因此,我们拍摄了一张新图像(此情况下为 left12.jpg。这是本章中的第一张图像)
h, w = img.shape[:2]
Mat getOptimalNewCameraMatrix(InputArray cameraMatrix, InputArray distCoeffs, Size imageSize, double alpha, Size newImgSize=Size(), Rect *validPixROI=0, bool centerPrincipalPoint=false)
根据自由缩放参数返回新的相机内参矩阵。
1. 使用 <strong>cv.undistort()</strong>
这是最简单的方式。只需调用函数并使用上述获得的 ROI 裁剪结果。
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
void undistort(InputArray src, OutputArray dst, InputArray cameraMatrix, InputArray distCoeffs, InputArray newCameraMatrix=noArray())
根据透镜畸变转换图像。
CV_EXPORTS_W bool imwrite(const String &filename, InputArray img, const std::vector< int > ¶ms=std::vector< int >())
将图像保存到指定的文件中。
2. 使用 <strong>重新映射</strong>
此方法稍微复杂一些。首先,从畸变的图像查找映射到未畸变图像的函数。然后使用 remap 函数。
dst =
cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R, InputArray newCameraMatrix, Size size, int m1type, OutputArray map1, OutputArray map2)
计算畸变校正和立体校正变换映射。
尽管如此,该方法均会给出相同的结果。请查看以下结果
图像
您可以在结果中看到,所有边缘都是直的。
现在,您可以使用 numpy(np.savez、np.savetxt 等)中的写入函数存储这些摄像机矩阵和畸变系数,以供将来使用。
重投影误差
重投影误差可以较好地估计找到的参数精确到什么程度。重投影误差越是接近于零,我们找到的参数就越准确。给定本征、畸变、旋转和平移矩阵,我们首先必须使用cv.projectPoints()将目标点转变为图像点。然后,我们可以计算我们通过变换得到的和角点检测算法之间的绝对范数。要找出平均误差,我们可以计算针对所有校准图像计算出的平均误差。
mean_error = 0
for i in range(len(objpoints))
error =
cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2)/len(imgpoints2)
mean_error += error
print( "total error: {}".format(mean_error/len(objpoints)) )
void projectPoints(InputArray objectPoints, InputArray rvec, InputArray tvec, InputArray cameraMatrix, InputArray distCoeffs, OutputArray imagePoints, OutputArray jacobian=noArray(), double aspectRatio=0)
将 3D 点投影到图像平面上。
double norm(InputArray src1, int normType=NORM_L2, InputArray mask=noArray())
计算一个数组的绝对范数。
其他资源
练习
- 尝试使用圆形网格对摄像机进行校准。