用OpenCV进行摄像机标定
照相机已经存在很长时间了。然而,随着廉价针孔相机在20世纪末的引入,日常生活中变得司空见惯。不幸的是,这种廉价伴随着它的代价:显著的扭曲。幸运的是,这些常数,通过校准和一些重新映射,可以纠正这一点。此外,通过校准,还可以确定相机的自然单位(像素)和真实世界单位(例如毫米)之间的关系。
原理
对于畸变,OpenCV考虑了径向和切向因素。对于径向,使用以下公式:
So for an old pixel point at coordinate in the input image, for a corrected output image its position will be
.
径向畸变的存在表现为“桶”或“鱼眼”效应。
由于摄像镜头与成像平面不完全平行,因此会发生切向失真。通过以下公式进行修正:
有五个失真参数,在OpenCV中被组织在一个1行5列的矩阵中:
Now for the unit conversion, we use the following formula:
Here the presence of the is cause we use a homography coordinate system (and
). The unknown parameters are
and
(camera focal lengths) and
what are the optical centers expressed in pixels coordinates. If for both axes a common focal length is used with a given
aspect ratio (usually 1), then
and in the upper formula we will have a single focal length. 包含这四个参数的矩阵称为摄像机矩阵。尽管失真系数是相同的,不管使用何种摄像头分辨率,但这些失真系数应与当前校准分辨率一起缩放。
确定这两个矩阵的变换过程就是校准。这些参数的计算是通过一些基本的几何方程来完成的。使用的方程式,取决于使用的校准对象。目前OpenCV支持三种类型的对象进行校准:
经典黑白棋盘
对称圆模式
不对称圆形图案
基本上,需要用相机拍下这些图案的快照,然后让OpenCV找到它们。在一个新的方程中,每一个可见的模式都相等。为了解这个方程,至少需要预定数量的模式快照,来形成一个适配的方程组。这个数字具有较高的棋盘图案和较少的圆的。例如,在理论上,一个棋盘至少需要两个快照。然而,实际上,输入图像中存在大量的噪声,因此为了获得好的结果,可能需要至少10个不同位置的输入模式的良好快照。
目标
示例应用程序将:
确定失真矩阵
确定摄像机矩阵
摄像机、视频和图像文件列表的输入
从XML/YAML文件进行配置
将结果保存到XML/YAML文件中
计算重投影误差
Source code
You may also find the source code in the samples/cpp/tutorial_code/calib3d/camera_calibration/ folder of the OpenCV source library or download it from here.
可以在OpenCV源代码库的samples/cpp/tutorial\u code/calib3d/camera\u calibration/文件夹中找到源代码。程序只有一个参数。其配置文件的名称。如果没有,它将尝试打开一个名为“default.xml”. 下面是一个XML格式的示例配置文件。在配置文件中,可以选择使用相机、视频文件或图像列表作为输入。如果选择后者,则需要创建一个配置文件,在其中枚举要使用的映像。下面是一个例子。需要记住的重要一点是,需要使用应用程序工作目录中的绝对路径或相对路径来指定映像。可以在前面提到的目录中找到所有这些。
应用程序首先从配置文件中读取设置。尽管这是其中的一个重要部分,但与本文的主题无关:相机校准。因此,选择不在这里发布代码部分。关于如何做到这一点的技术背景可以在XML YAML中找到。
Explanation
Read the settings.
Settings s;
const string inputSettingsFile = argc > 1 ? argv[1] : "default.xml";
FileStorage fs(inputSettingsFile, FileStorage::READ); // Read the settings
if (!fs.isOpened())
{
cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;
return -1;
}
10.fs["Settings"] >> s;
11.fs.release(); // close Settings file
13.if (!s.goodInput)
14.{
17.}
使用简单的OpenCV类输入操作。在读取文件之后,有一个额外的后处理函数,来检查输入的有效性。只有当它们都正常,goodInput变量才是正常。
获取下一个输入,如果失败,或者有足够的输入。此后,有一个大的循环,在这里,做以下操作:从图像列表中获取下一个图像,相机或视频文件。如果这失败了,或者有足够的图像,运行校准过程。在图像的情况下,步出循环,否则其余的帧将不失真(如果选项设置),通过改变检测模式到校准一。
18.for(int i = 0;;++i)
19.{
For some cameras we may need to flip the input image. Here we do this too.
42.vector
44.bool found;
45.switch( s.calibrationPattern ) // Find feature points on the input format
46.{
47.case Settings::CHESSBOARD:
51.case Settings::CIRCLES_GRID:
54.case Settings::ASYMMETRIC_CIRCLES_GRID:
57.}
根据输入模式的类型,可以使用findChessboardCorners或findCirclesGrid函数。对于这两种情况,会传递当前的图像、电路板的大小,会得到图案的位置。此外,还返回一个布尔变量,说明在输入中是否可以找到模式(只需要考虑图像中的情况)。
同样,对于相机,只在输入延迟时间,过后才拍摄相机图像。用户移动棋盘,获得不同的图像。相同的图像意味着相同的方程,在标定时,相同的方程将形成不适定问题,因此标定将失败。对于正方形图像,角点的位置只是近似值。可以通过调用cornerSubPix函数来改进这一点。这样可以得到更好的标定结果。之后,将一个有效的输入结果添加到imagePoints向量中,将所有方程收集到一个容器中。最后,为了实现可视化反馈,将使用findChessboardCorners函数在输入图像上绘制找到的点。
if ( found) // If done with success,
{
// improve the found corners' coordinate accuracy for chessboard
if( s.calibrationPattern == Settings::CHESSBOARD)
{
Mat viewGray;
cvtColor(view, viewGray, CV_BGR2GRAY);
cornerSubPix( viewGray, pointBuf, Size(11,11),
Size(-1,-1), TermCriteria( CV_TERMCRIT_EPS+CV_TERMCRIT_ITER, 30, 0.1 ));
}
if( mode == CAPTURING && // For camera only take new samples after delay time
(!s.inputCapture.isOpened() || clock() - prevTimestamp > s.delay*1e-3*CLOCKS_PER_SEC) )
{
imagePoints.push_back(pointBuf);
prevTimestamp = clock();
blinkOutput = s.inputCapture.isOpened();
}
// Draw the corners.
drawChessboardCorners( view, s.boardSize, Mat(pointBuf), found );
}
为用户显示状态和结果,以及应用程序的命令行控制。显示部分由实时文本输出组成,对于要显示“捕获”帧的视频或相机输入,只需按位求反输入图像。
58.//----------------------------- Output Text ------------------------------------------------
59.string msg = (mode == CAPTURING) ? "100/100" :
61.int baseLine = 0;
62.Size textSize = getTextSize(msg, 1, 1, 1, &baseLine);
63.Point textOrigin(view.cols - 2*textSize.width - 10, view.rows - 2*baseLine - 10);
65.if( mode == CAPTURING )
66.{
71.}
73.putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ? GREEN : RED);
75.if( blinkOutput )
If we only ran the calibration and got the camera matrix plus the distortion coefficients we may just as correct the image with the undistort function:
//------------------------- Video capture output undistorted ------------------------------
if( mode == CALIBRATED && s.showUndistorsed )
{
Mat temp = view.clone();
undistort(temp, view, cameraMatrix, distCoeffs);
}
//------------------------------ Show image and check for input commands -------------------
imshow("Image View", view);
Then we wait for an input key and if this is u we toggle the distortion removal, if it is g we start all over the detection process (or simply start it), and finally for the ESC key quit the application:
char key = waitKey(s.inputCapture.isOpened() ? 50 : s.delay);
if( key == ESC_KEY )
break;
if( key == 'u' && mode == CALIBRATED )
s.showUndistorsed = !s.showUndistorsed;
if( s.inputCapture.isOpened() && key == 'g' )
{
mode = CAPTURING;
imagePoints.clear();
}
显示图像的失真消除。使用图像列表时,无法消除循环中的失真。因此,必须在循环之后附加。利将展开无失真函数,实际上是首先调用initundortrectitymap来找出变换矩阵,然后用remap函数进行变换。在成功校准后,地图计算只需进行一次,通过使用此扩展表格,可以加快应用程序的速度:
77.if( s.inputType == Settings::IMAGE_LIST && s.showUndistorsed )
78.{
95.}
The calibration and save
每个摄像机只需要校准一次,在成功校准后,保存它们是有意义的。这样以后就可以将这些值加载到程序中。因此,首先进行校准,如果校准成功,将结果保存到OpenCV样式的XML或YAML文件中,具体取决于在配置文件中给出的扩展名。
因此,在第一个函数中,只是将这两个过程分开。因为想保存许多校准变量,所以将在这里创建这些变量,并将它们传递给校准和保存函数。再次,将不显示保存部分,因为与校准几乎没有共同点。浏览源文件以了解如何和内容:
bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,vector
{
vector
vector
double totalAvgErr = 0;
bool ok = runCalibration(s,imageSize, cameraMatrix, distCoeffs, imagePoints, rvecs, tvecs,
reprojErrs, totalAvgErr);
cout << (ok ? "Calibration succeeded" : "Calibration failed")
<< ". avg re projection error = " << totalAvgErr ;
if( ok ) // save only if the calibration was done with success
saveCameraParams( s, imageSize, cameraMatrix, distCoeffs, rvecs ,tvecs, reprojErrs,
imagePoints, totalAvgErr);
return ok;
}
在calibleCamera函数进行校准。具有以下参数:
物体角点。这是点3f向量的向量,对于每个输入图像,它描述了模式的外观。如果有一个平面图案(如棋盘),可以简单地将所有Z坐标设置为零。这是这些要点所在的要点的集合。对所有的输入图像使用一个模式,只需计算一次,然后乘以所有其他的输入视图。使用CalcBoardCornePositions函数计算角点,如下所示:
And then multiply it as:
vector
calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);
objectPoints.resize(imagePoints.size(),objectPoints[0]);
图像点。这是点2f向量的向量,对于每个输入图像,该向量包含找到重要点(棋盘的角点和圆模式的圆心)的位置。已经从findChessboardCorners或findCirclesGrid函数返回的内容中,收集了这个。只需要把它传下去。
从相机、视频文件或图像中获取的图像大小。
摄像机矩阵。如果使用“固定纵横比”选项,则需要将设置
为零:
cameraMatrix = Mat::eye(3, 3, CV_64F);
if( s.flag & CV_CALIB_FIX_ASPECT_RATIO )
cameraMatrix.at
The distortion coefficient matrix. Initialize with zero.
该函数将为所有视图计算旋转和平移矢量,该矢量将对象点(在模型坐标空间中给出)转换为图像点(在世界坐标空间中给出)。第7和第8参数是矩阵的输出向量,该矩阵在第i个位置中,包含第i个目标角点到第i个图像点的旋转和平移向量。
最后一个参数是flag。需要在这里指定选项,如固定焦距的纵横比、假设零切向失真或固定主点。
double rms = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,
distCoeffs, rvecs, tvecs, s.flag|CV_CALIB_FIX_K4|CV_CALIB_FIX_K5);
函数返回平均重投影误差。这个数字很好地估计了,找到的参数到底有多精确。应该尽可能接近于零。在给定内禀矩阵、畸变矩阵、旋转矩阵和平移矩阵的情况下,可以利用投影点,将物体点变换为像点,来计算一个视图的误差。然后,计算得到的绝对范数之间的转换和角/圆发现算法。为了找出平均误差,计算了所有校准图像的误差算术平均值。
double computeReprojectionErrors( const vector
const vector
const vector
const Mat& cameraMatrix , const Mat& distCoeffs,
vector
{
vector
int i, totalPoints = 0;
double totalErr = 0, err;
perViewErrors.resize(objectPoints.size());
for( i = 0; i < (int)objectPoints.size(); ++i )
{
projectPoints( Mat(objectPoints[i]), rvecs[i], tvecs[i], cameraMatrix, // project
distCoeffs, imagePoints2);
err = norm(Mat(imagePoints[i]), Mat(imagePoints2), CV_L2); // difference
int n = (int)objectPoints[i].size();
perViewErrors[i] = (float) std::sqrt(err*err/n); // save for this view
totalErr += err*err; // sum it up
totalPoints += n;
}
return std::sqrt(totalErr/totalPoints); // calculate the arithmetical mean
}
Results
设置这个输入棋盘图案的大小为9 X 6。用一个AXIS IP摄像头,创建几个板的快照,并将其保存到VID5目录中。我已将其放入工作目录的images/cameracalibration文件夹中,并创建了以下VID5.XML文件,该文件描述了要使用的图像:
images/CameraCalibraation/VID5/xx1.jpg
images/CameraCalibraation/VID5/xx2.jpg
images/CameraCalibraation/VID5/xx3.jpg
images/CameraCalibraation/VID5/xx4.jpg
images/CameraCalibraation/VID5/xx5.jpg
images/CameraCalibraation/VID5/xx6.jpg
images/CameraCalibraation/VID5/xx7.jpg
images/CameraCalibraation/VID5/xx8.jpg
然后将images/cameracalibration/VID5/VID5.XML指定为配置文件中的输入。在应用程序运行时,可见棋盘模式:
应用失真消除后,得到:
通过将“输入宽度”(input width)设置为4,将“高度”(height)设置为11,同样的方法也适用于这种不对称的圆形图案。使用了一个实时摄像机feed,为输入指定了它的ID(“1”)。以下是检测到的模式的照片:
在这两种情况下,在指定的输出XML/YAML文件中,将发现摄影机和失真系数矩阵:
6.5746697944293521e+002 0. 3.1950000000000000e+002 0.
6.5746697944293521e+002 2.3950000000000000e+002 0. 0. 1.
-4.1802327176423804e-001 5.0715244063187526e-001 0. 0.
-5.7843597214487474e-001
将这些值作为常量添加到程序中,调用initundortrectitymap和remap函数以消除失真,并使用廉价和低质量的相机享受无失真输入。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章