OpenCV 4.12.0
开源计算机视觉
加载中...
搜索中...
无匹配项
相机标定

目标

在本节中,我们将学习:

  • 相机引起的畸变类型
  • 如何找到相机的内参和外参
  • 如何根据这些参数校正图像畸变

基础

一些针孔相机图像会引入明显的畸变。两种主要的畸变是径向畸变和切向畸变。

径向畸变会导致直线看起来弯曲。径向畸变的大小与点到图像中心的距离成正比。例如,下图显示了一张棋盘的图像,其中棋盘的两个边缘用红线标记。但是,你可以看到棋盘的边界不是一条直线,与红线不匹配。所有预期的直线都向外凸出。访问 畸变 (光学) 获取更多详细信息。

image

以下部分将介绍几个新参数。访问 相机标定和 3D 重建 获取更多详细信息。

径向畸变可以表示如下:

\[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]\]

简而言之,我们需要找到五个参数,称为畸变系数,由下式给出:

\[Distortion \; coefficients=(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 矩阵:

\[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),相机相应地移动。这种考虑有助于我们仅找到 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)
# 准备对象点,例如 (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 点在真实世界空间中
imgpoints = [] # 2d 点在图像平面中。
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_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)
精炼角点位置。

下面显示了一张绘制了图案的图像:

image

标定

现在我们有了对象点和图像点,我们准备好进行标定了。我们可以使用函数 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. 使用 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)
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. 使用 重映射

这种方式稍微困难一些。首先,找到从畸变图像到畸变校正图像的映射函数。然后使用 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())
对图像应用通用几何变换。

尽管如此,这两种方法都给出了相同的结果。请参见下面的结果:

image

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

现在您可以使用 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)
将三维点投影到图像平面。
double norm(InputArray src1, int normType=NORM_L2, InputArray mask=noArray())
计算数组的绝对范数。

练习

  1. 尝试使用圆形网格进行相机标定。