相机作为视觉传感器,是机器人、监视、太空探索、社交媒体、工业自动化、乃至娱乐业等多个领域的组成部分。在许多运用中,必须知道相机的参数才能将其有效地用作视觉传感器。在这篇文章中,你将理解相机标定所涉及的步骤及其意义。我们还共享C++和Python中的代码以及棋盘模式的示例图像。
1 什么是相机标定?相机参数的估计过程称为相机标定。这意味着我们拥有关于相机的所有信息(参数或系数),这些信息用于确定真实天下中的3D点与其在该标定相机捕获的图像中的相应2D投影(像素)之间的精确关系。常日这意味着规复两种参数。
内部参数相机/镜头系统。例如透镜的焦距、光学中央和径向畸变系数。外部参数这是指相机相对付某些天下坐标系的方位(旋转和平移)。不才图中,利用几何标定估计的透镜参数来肃清图像失落真。2 图像形成几何学要理解标定的过程,我们首先须要理解成像几何。我们将从几何的角度来阐明图像的形成。详细来说,我们将谈论三维点如何在图像平面上投影的数学问题。也便是说,你所须要知道的便是矩阵乘法。
2.1 设定为了随意马虎理解这个问题,假设你在房间里安装了一台摄影机。给定三维点P在这个房间里,我们想要找到这个3D点的像素坐标(u,v)在相机拍摄的图像中。在这个设置中有三个坐标系在起浸染。我们来阐明一下(阐明涉及光学知识看不懂不影响可跳过)。
由上图可以看到天下坐标系和相机坐标系是通过旋转和平移联系起来的。这六个参数(3个用于旋转,3个用于平移)称为相机的外部参数。
要定义房间中点的位置,我们首先须要为这个房间定义一个坐标系。它须要做两件事:
原点:我们可以随意地把房间的一角作为原点。(0,0,0).X,Y,Z轴:我们还可以沿着地板上的二维定义房间的X轴和Y轴,沿着垂直墙定义房间的Z轴。利用上面的方法,我们可以通过丈量空间内任意点沿X、Y和Z轴与原点的间隔来找到它的三维坐标。这个与房间相连的坐标系称为天下坐标系。在图1中,它利用橙色轴显示。我们将利用粗体字体表示轴,用普通字体表示点的坐标。让我们考虑一下这个房间的P点。在世界坐标系中,P的坐标只需沿三个轴丈量该点距原点的间隔,就可以找到该点的X、Y和Z坐标。
2.1.2 相机坐标系现在,让我们把相机放在这个房间里。这个房间的图像将用这个相机拍摄,因此,我们感兴趣的是连接到这个相机上的三维坐标系。如果我们将相机放在房间的原点,并使其X、Y和Z轴与房间的xyz轴对齐,则两个坐标系将是等同的。
然而,这是一个荒谬的限定。我们想把相机放在房间里的任何地方,它该当可以在任何地方看到。在这种情形下,我们须要找出三维房间(即天下)坐标和三维相机坐标之间的关系。
假设我们的相机位于房间中的任意位置(
,
,
)。用技能术语来说,我们可以用((
,
,
)相对付天下坐标来转换相机坐标。相机也可能朝着任意的方向看。换句话说,我们可以说相机是相对付天下坐标系旋转的。
3D中的旋转是用三个参数捕捉的——你可以把这三个参数看作yaw, pitch, roll。也可以将其视为三维中的轴(两个参数)和环绕该轴的角度旋转(一个参数)。
然而,将旋转编码为3×3矩阵每每是便于数学操作的。现在,您可能会认为,3×3矩阵有9个元素,因此有9个参数,但是旋转只有3个参数。这便是为什么任意3×3矩阵都不是旋转矩阵的缘故原由。不谈细节,让我们现在只知道,一个旋转矩阵只有三个自由度,纵然它有9个元素。
回到我们原来的问题。天下坐标和相机坐标由旋转矩阵 R 和一个三元平移矢量 t 关联。
那是什么意思?
这意味着在世界坐标系中具有坐标值(
,
,
)的点P在相机坐标系中将具有不同的坐标值(
,
,
)。我们用赤色表示相机坐标系。这两个坐标值与下面的方程有关。
请把稳,将旋转表示为一个矩阵可以让我们用大略的矩阵乘法来进行旋转,而不是像yaw, pitch, roll等其他表示中所需的繁琐的符号操作。我希望这能帮助你理解为什么我们把旋转表示为矩阵。有时,上面的表达式因此更紧凑的形式写成的。将3×1平移向量作为一列附加在3×3旋转矩阵的末端,得到一个3×4矩阵,称为外参矩阵。
个中,外参矩阵 P 是由下式给出:
在射影几何学中,我们常常用一个有趣的坐标即齐次坐标表示,在坐标上附加一个额外的维度。笛卡尔坐标系中的三维点(X,Y,Z)可以在齐次坐标系中写成(X,Y,Z,1)。更广泛地说,齐次坐标中(X, Y, Z, W)点与笛卡尔坐标中的点(
,
,
)相同。齐次坐标许可我们用有限的数字来表示无限量。例如,无穷远处的点可以在齐次坐标系中表示为(1,1,1,0)。你可能会把稳到我们在外参矩阵中利用了齐次坐标来表示天下坐标
2.1.3 图像坐标系点P在图像平面上的投影如上图所示。一旦我们通过对点天下坐标运用旋转和平移来得到相机三维坐标系中的点,我们就可以将该点投影到图像平面上以得到该点在图像中的位置。
在上面的图像中,我们看到的是一个点P,在相机坐标系中有坐标(
,
,
)。只是提醒一下,如果我们不知道这个点在相机坐标系中的坐标,我们可以利用外参矩阵变换它的天下坐标,从而利用外参矩阵得到相机坐标系中的坐标。上图显示了大略针孔相机的相机投影。
光学中央(针孔)用
表示,实际上在像面上形成点的倒像。为了数学上的方便,我们大略地做所有的打算,就彷佛图像平面在光学中央的前面一样,由于从传感器读出的图像可以轻微地旋转180度来补偿反转。实际上,这是不须要的。它乃至更大略:一个真正的相机传感器只是按照相反的顺序(从右到左)从最下面一行读出,然后从下到上读取每一行。通过这种方法,图像自动垂直形成,旁边顺序精确。因此在实践中,不再须要旋转图像。
图像平面放置在间隔光学中央f(焦距)的位置。
利用高中几何(相似三角形),可以显示出三维点(
,
,
)的投影图像(x,y)由下式得到:
这两个方程可以用矩阵形式重写如下:
矩阵K如下所示,称为内参矩阵并包含相机的内在参数。
上述大略矩阵只显示焦距。然而,图像传感器中的像素可能不是方形的,因此我们可能有两个不同的焦距。f_x和f_y。光学中央(c_x, c_y)相机的中央可能与图像坐标系的中央不重合。
此外,相机传感器的x轴和y轴之间可能有一个小的倾斜
。考虑到以上所怀孕分,相机矩阵可以重新编写为:
下图显示了当图像像素坐标系的原点位于左上角时更真实的场景。内参相机矩阵须要考虑主点的位置、轴的倾斜以及沿不同轴的潜在不同焦距。
然而,在上述等式中,x和y像素坐标相对付图像的中央。但是,在处理图像时,原点位于图像的左上角。
我们用(u,v)表示图像坐标。则有下式:
个中:
2.2 图像形成方法总结
将天下坐标系中的三维点投影到相机像素坐标上,有以下步骤:
利用由两个坐标系之间的旋转和平移组成的外部矩阵,将三维点从天下坐标转换为相机坐标。在相机坐标系中,利用相机内部焦距、光心等参数构成的内部矩阵将新的三维点投影到图像平面上。3 基于OpenCV的相机标定事理3.1 相机标定干系参数正如上章中所阐明的,要找到三维点在图像平面上的投影,我们首先须要利用外部参数(R和t)将点从天下坐标系转换为相机坐标系。接下来,利用相机的内部参数,我们将点投影到图像平面上。
将天下坐标系中的三维点(
,
,
)与其在图像坐标系中的投影(u,v)干系的方程式如下所示:
个中如下图所示,P是一个由两部分组成的3×4投影矩阵。包含内在参数的内参矩阵(K)和由3×3旋转矩阵R和3×1平移向量t组合而成的外参矩阵)。
如前文所述,内参矩阵K是上三角矩阵
个中:
,是x和y焦距(是的,它们常日是相同的)。,是图像平面上光学中央的x和y坐标。利用图像的中央常日是一个足够好的近似。是轴之间的倾斜度。常日是0。3.2 相机标定的目标标定过程的目标是利用一组已知的三维点(
,
,
)及其对应的图像坐标(u、v),找到3×3矩阵K、3×3旋转矩阵R、3×1平移向量T。当我们得到相机的内部和外部参数值时,相机就被称为标定相机。总之,相机标定算法具有以下输入和输出:
输入:具有已知二维图像坐标和三维天下坐标的点的图像凑集。输出:3×3相机内参矩阵,每幅图像的旋转和平移。把稳OpenCV中,相机内部矩阵不包含倾斜参数。以是矩阵的形式是:
3.3 不同类型的相机标定方法
以下是紧张的相机标定方法:
校正:当我们完备掌握成像过程时,实行校准的最佳方法是从不同的视角捕获一个物体或已知尺寸模式的多个图像。我们将在这篇文章中学习的基于棋盘的方法属于这一类。我们也可以利用已知尺寸的圆形图案,而不是棋盘格图案。几何线索:有时我们在场景中有其他的几何线索,如直线和消逝点,可以用来标定。基于深度学习的:当我们对成像设置的掌握非常小(例如,我们有场景的单个图像)时,仍旧可以利用基于深度学习的方法获取相机的校准信息。4 相机标定示例步骤标定示例过程用下面给出的流程图来阐明。
利用棋盘格模式定义真实天下坐标;从不同的角度捕获棋盘的多个图像;查找棋盘的2D坐标;校准相机
我们来看看这些步骤。
4.1 利用棋盘格模式定义真实天下坐标天下坐标系:我们的天下坐标是由以下这个棋盘格图案固定的,这个棋盘格图案附着在房间的墙上。我们的三维点是棋盘中正方形的角。上面的任何一角都可以选择到天下坐标系的原点。
和
轴沿墙,并且
垂直于墙。因此,棋盘上的所有点都在XY平面上(即
=0)。
在标定过程中,我们通过一组已知的3D点(
,
,
)和它们在图像中对应的像素位置(u,v)来打算相机参数。
对付三维点,我们在许多不同的方向拍摄具有已知尺寸的棋盘格图案。天下坐标被附加到棋盘上,由于所有的角点都在一个平面上,以是我们可以任意选择
。由于每个点的
都是0。
由于点在棋盘中的间距相等,(
,
)每个3D点的坐标很随意马虎定义,方法是将一个点作为参考点(0,0),并定义相对付该参考点的剩余坐标。
绘制检测到的棋盘角后的结果如下图所示:
为什么棋盘格模式在校准中运用如此广泛?棋盘图案是独特的,易于检测的图像。不仅如此,棋盘格上正方形的角点非常适宜定位它们,由于它们在两个方向上都有尖锐的梯度。此外,这些角也与它们位于棋盘格线的交点有关。所有这些事实都被用来在棋盘格模式中可靠地定位正方形的角点。
4.2 从不同的角度捕获棋盘的多个图像接下来,我们保持棋盘格静止,通过移动相机拍摄棋盘格的多个图像。或者,我们也可以保持相机恒定,在不同方向拍摄棋盘格图案。这两种情形在数学上是相似的。拍摄效果如下图所示:
4.3 查找棋盘的2D坐标
我们现在有多个棋盘的图像。我们还知道棋盘上的点在世界坐标系中的三维位置。末了一件事是图像中这些棋盘格角点的二维像素位置。
4.3.1 查找棋盘角点OpenCV供应了一个名为findChessboardCorners的内置函数,该函数查找棋盘并返回角点的坐标。让我们看看下面代码块中的用法。C++
bool findChessboardCorners(InputArray image, Size patternSize, OutputArray corners, int flags = CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE )
Python
retval, corners = cv2.findChessboardCorners(image, patternSize, flags)
紧张参数如下:
参数
含义
image
棋盘源图像。它必须是8位灰度或彩色图像
patternSize
每个棋盘行和列的内角点数 ( patternSize = cvSize (points_per_row, points_per_colum) = cvSize(columns,rows))
corners
检测到的角点的输出数组
flags
各种操作标志。只有当事情不顺利的时候你才须要担心这些。利用默认值
输出是真是假取决于是否检测到角点。
4.3.2 优化棋盘角点好的校准都是为了精确。为了得到良好的效果,得到亚像素级精度的角点位置非常主要。OpenCV的cornersubix函数吸收原始图像和角点的位置,并在原始位置的一个小邻域内探求最佳角点位置。算法实质上是迭代的,因此我们须要指定终止条件(例如迭代次数和/或精度)。C++
void cornerSubPix(InputArray image, InputOutputArray corners, Size winSize, Size zeroZone, TermCriteria criteria)
Python
cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria)
紧张参数如下:
参数
含义
image
输入图像
corners
输入角的初始坐标和为输出供应的精确坐标
WinSize
搜索窗口边长的一半
zeroZone
搜索区域中间零区大小的一半,在该零区上不进行下式求和。它有时用于避免自干系矩阵的可能奇点。(-1,-1)的值表示没有这样的大小
criteria
角点精化迭代过程的终止准则。也便是说,在criteria.maxCount迭代之后或在某些迭代中角位置移动小于criteria.epsilon时,角位置求精过程停滞
4.4 校准相机校准的末了一步是将天下坐标系中的3D点及其在所有图像中的2D位置通报给OpenCV的caliberecamera方法。该实现基于Zhang Zhengyou的一篇论文。数学有点繁芜,须要有线性代数背景。让我们看一下calibrateCamera:C++
double calibrateCamera(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints, Size imageSize, InputOutputArray cameraMatrix, InputOutputArray distCoeffs, OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs)
Python
retval, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(objectPoints, imagePoints, imageSize)
紧张参数如下:
参数
含义
objectPoints
三维图像点的矢量
imagePoints
二维图像点的矢量
imageSize
图像大小
cameraMatrix
内参矩阵
distCoeffs
透镜畸变系数
rvecs
用于表达旋转的3×1矢量。矢量的方向指定旋转轴,矢量的大小指定旋转角度
tvecs
用于表达位移的3×1矢量,与rvecs类似
5 结果与代码实际上便是输出内参矩阵和一系列系数。所有代码见:
https://github.com/luohenyueji/OpenCV-Practical-Exercise
C++
#include <opencv2/opencv.hpp>#include <stdio.h>#include <iostream>using namespace std;using namespace cv;// Defining the dimensions of checkerboard// 定义棋盘格的尺寸int CHECKERBOARD[2]{ 6,9 };int main(){ // Creating vector to store vectors of 3D points for each checkerboard image // 创建矢量以存储每个棋盘图像的三维点矢量 std::vector<std::vector<cv::Point3f> > objpoints; // Creating vector to store vectors of 2D points for each checkerboard image // 创建矢量以存储每个棋盘图像的二维点矢量 std::vector<std::vector<cv::Point2f> > imgpoints; // Defining the world coordinates for 3D points // 为三维点定义天下坐标系 std::vector<cv::Point3f> objp; for (int i{ 0 }; i < CHECKERBOARD[1]; i++) { for (int j{ 0 }; j < CHECKERBOARD[0]; j++) { objp.push_back(cv::Point3f(j, i, 0)); } } // Extracting path of individual image stored in a given directory // 提取存储在给定目录中的单个图像的路径 std::vector<cv::String> images; // Path of the folder containing checkerboard images // 包含棋盘图像的文件夹的路径 std::string path = "./images/.jpg"; // 利用glob函数读取所有图像的路径 cv::glob(path, images); cv::Mat frame, gray; // vector to store the pixel coordinates of detected checker board corners // 存储检测到的棋盘转角像素坐标的矢量 std::vector<cv::Point2f> corner_pts; bool success; // Looping over all the images in the directory // 循环读取图像 for (int i{ 0 }; i < images.size(); i++) { frame = cv::imread(images[i]); if (frame.empty()) { continue; } if (i == 40) { int b = 1; } cout << "the current image is " << i << "th" << endl; cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); // Finding checker board corners // 探求角点 // If desired number of corners are found in the image then success = true // 如果在图像中找到所需数量的角,则success = true // opencv4以下版本,flag参数为CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE success = cv::findChessboardCorners(gray, cv::Size(CHECKERBOARD[0], CHECKERBOARD[1]), corner_pts, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_FAST_CHECK | CALIB_CB_NORMALIZE_IMAGE); / If desired number of corner are detected, we refine the pixel coordinates and display them on the images of checker board / // 如果检测到所需数量的角点,我们将细化像素坐标并将其显示在棋盘图像上 if (success) { // 如果是OpenCV4以下版本,第一个参数为CV_TERMCRIT_EPS | CV_TERMCRIT_ITER cv::TermCriteria criteria(TermCriteria::EPS | TermCriteria::Type::MAX_ITER, 30, 0.001); // refining pixel coordinates for given 2d points. // 为给定的二维点细化像素坐标 cv::cornerSubPix(gray, corner_pts, cv::Size(11, 11), cv::Size(-1, -1), criteria); // Displaying the detected corner points on the checker board // 在棋盘上显示检测到的角点 cv::drawChessboardCorners(frame, cv::Size(CHECKERBOARD[0], CHECKERBOARD[1]), corner_pts, success); objpoints.push_back(objp); imgpoints.push_back(corner_pts); } //cv::imshow("Image", frame); //cv::waitKey(0); } cv::destroyAllWindows(); cv::Mat cameraMatrix, distCoeffs, R, T; / Performing camera calibration by passing the value of known 3D points (objpoints) and corresponding pixel coordinates of the detected corners (imgpoints) / // 通过通报已知3D点(objpoints)的值和检测到的角点(imgpoints)的相应像素坐标来实行相机校准 cv::calibrateCamera(objpoints, imgpoints, cv::Size(gray.rows, gray.cols), cameraMatrix, distCoeffs, R, T); // 内参矩阵 std::cout << "cameraMatrix : " << cameraMatrix << std::endl; // 透镜畸变系数 std::cout << "distCoeffs : " << distCoeffs << std::endl; // rvecs std::cout << "Rotation vector : " << R << std::endl; // tvecs std::cout << "Translation vector : " << T << std::endl; return 0;}
Python
#!/usr/bin/env pythonimport cv2import numpy as npimport glob# Defining the dimensions of checkerboardCHECKERBOARD = (6,9)criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)# Creating vector to store vectors of 3D points for each checkerboard imageobjpoints = []# Creating vector to store vectors of 2D points for each checkerboard imageimgpoints = [] # Defining the world coordinates for 3D pointsobjp = np.zeros((1, CHECKERBOARD[0]CHECKERBOARD[1], 3), np.float32)objp[0,:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)prev_img_shape = None# Extracting path of individual image stored in a given directoryimages = glob.glob('./images/.jpg')for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # Find the chess board corners # If desired number of corners are found in the image then ret = true ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, cv2.CALIB_CB_ADAPTIVE_THRESH+ cv2.CALIB_CB_FAST_CHECK+cv2.CALIB_CB_NORMALIZE_IMAGE) """ If desired number of corner are detected, we refine the pixel coordinates and display them on the images of checker board """ if ret == True: objpoints.append(objp) # refining pixel coordinates for given 2d points. corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria) imgpoints.append(corners2) # Draw and display the corners img = cv2.drawChessboardCorners(img, CHECKERBOARD, corners2,ret) #cv2.imshow('img',img) #cv2.waitKey(0)cv2.destroyAllWindows()h,w = img.shape[:2]"""Performing camera calibration by passing the value of known 3D points (objpoints)and corresponding pixel coordinates of the detected corners (imgpoints)"""ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None)print("Camera matrix : \n")print(mtx)print("dist : \n")print(dist)print("rvecs : \n")print(rvecs)print("tvecs : \n")print(tvecs)
6 参考
https://www.learnopencv.com/geometry-of-image-formation/https://www.learnopencv.com/camera-calibration-using-opencv/