OpenCV  4.10.0
开源计算机视觉
加载...
搜索...
无匹配项
相机校准

目标

在本部分,我们将学习

  • 相机引起的不同类型的失真
  • 如何查找相机的内在和外在属性
  • 如何根据这些属性对图像去失真

基础

有些针孔相机引入的图像失真非常大。两种主要失真类型是径向失真和切向失真。

径向失真使得直线看起来弯曲。离图像中心越远,径向失真就越大。例如,下面显示了一张图像,棋盘的两个边缘用红色线条标记。但是,你可以看到,棋盘的边框不是直线,与红线不匹配。所有预期的直线都被向外凸出。访问畸变(光学)以获取更多详细信息。

图像

径向失真可表示如下

\[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)
# 准备目标点,如 (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 = [] # 真实世界空间中的三维点
imgpoints = [] # 图像平面中的二维点。
images = glob.glob('*.jpg')
for fname in images
img = cv.imread(fname)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# 找到棋盘角点
ret, corners = cv.findChessboardCorners(gray, (7,6), None)
# 如果找到,添加目标点、图像点(对其进行精炼后)
if ret == True
objpoints.append(objp)
corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 绘制并显示角点
cv.drawChessboardCorners(img, (7,6), corners2, ret)
cv.imshow('img', img)
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。这是本章中的第一张图像)

img = cv.imread('left12.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
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 裁剪结果。

# 畸变校正
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)
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 > &params=std::vector< int >())
将图像保存到指定的文件中。

2. 使用 <strong>重新映射</strong>

此方法稍微复杂一些。首先,从畸变的图像查找映射到未畸变图像的函数。然后使用 remap 函数。

# 畸变校正
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)
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R, InputArray newCameraMatrix, Size size, int m1type, OutputArray map1, OutputArray map2)
计算畸变校正和立体校正变换映射。
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2, int interpolation, int borderMode=BORDER_CONSTANT, const Scalar &borderValue=Scalar())
将通用几何变换应用于图像。

尽管如此,该方法均会给出相同的结果。请查看以下结果

图像

您可以在结果中看到,所有边缘都是直的。

现在,您可以使用 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)) )
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())
计算一个数组的绝对范数。

其他资源

练习

  1. 尝试使用圆形网格对摄像机进行校准。