目标
在本节中,我们将学习:
- 相机引起的各种畸变类型
- 如何找到相机的内参和外参
- 如何根据这些参数对图像进行去畸变
基础知识
一些针孔相机会在图像中引入明显的畸变。两种主要的畸变类型是径向畸变和切向畸变。
径向畸变会导致直线看起来弯曲。径向畸变随着点离图像中心越远而越大。例如,下图显示了一张图像,其中棋盘格的两个边缘用红线标记。但是,您可以看到棋盘格的边界不是直线,并且与红线不匹配。所有预期的直线都向外凸出。访问畸变(光学)了解更多详情。
图像
径向畸变可以表示如下:
\[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,如果获得图案,则 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_BGR)
从文件中加载图像。
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0, AlgorithmHint hint=cv::ALGO_HINT_DEFAULT)
将图像从一种颜色空间转换为另一种颜色空间。
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. 使用 **cv.undistort()**
这是最简单的方法。只需调用函数并使用上面获得的 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. 使用 **重新映射**
这种方法稍微复杂一些。首先,找到从畸变图像到去畸变图像的映射函数。然后使用重新映射函数。
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( "总误差: {}".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())
计算数组的绝对范数。
练习
- 尝试使用圆形网格进行相机标定。