用OpenCV进行摄像机标定
用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.{
- cout << "Invalid input detected. Application stopping. " << endl;
- return -1;
17.}
使用简单的OpenCV类输入操作。在读取文件之后,有一个额外的后处理函数,来检查输入的有效性。只有当它们都正常,goodInput变量才是正常。
获取下一个输入,如果失败,或者有足够的输入。此后,有一个大的循环,在这里,做以下操作:从图像列表中获取下一个图像,相机或视频文件。如果这失败了,或者有足够的图像,运行校准过程。在图像的情况下,步出循环,否则其余的帧将不失真(如果选项设置),通过改变检测模式到校准一。
18.for(int i = 0;;++i)
19.{
- Mat view;
- bool blinkOutput = false;
- view = s.nextImage();
- //----- If no more image, or got enough, then stop calibration and show result -------------
- if( mode == CAPTURING && imagePoints.size() >= (unsigned)s.nrFrames )
- {
- if( runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints))
- mode = CALIBRATED;
- else
- mode = DETECTION;
- }
- if(view.empty()) // If no more images then run calibration, save and stop loop.
- {
- if( imagePoints.size() > 0 )
- runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints);
- break;
- imageSize = view.size(); // Format input image.
- if( s.flipVertical ) flip( view, view, 0 );
- }
For some cameras we may need to flip the input image. Here we do this too.
- Find the pattern in the current input. The formation of the equations I mentioned above consists of finding the major patterns in the input: in case of the chessboard this is their corners of the squares and for the circles, well, the circles itself. The position of these will form the result and is collected into the pointBuf vector.
42.vector<Point2f> pointBuf;
44.bool found;
45.switch( s.calibrationPattern ) // Find feature points on the input format
46.{
47.case Settings::CHESSBOARD:
- found = findChessboardCorners( view, s.boardSize, pointBuf,
- CV_CALIB_CB_ADAPTIVE_THRESH | CV_CALIB_CB_FAST_CHECK | CV_CALIB_CB_NORMALIZE_IMAGE);
- break;
51.case Settings::CIRCLES_GRID:
- found = findCirclesGrid( view, s.boardSize, pointBuf );
- break;
54.case Settings::ASYMMETRIC_CIRCLES_GRID:
- found = findCirclesGrid( view, s.boardSize, pointBuf, CALIB_CB_ASYMMETRIC_GRID );
- break;
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" :
- mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";
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.{
- if(s.showUndistorsed)
- msg = format( "%d/%d Undist", (int)imagePoints.size(), s.nrFrames );
- else
- msg = format( "%d/%d", (int)imagePoints.size(), s.nrFrames );
71.}
73.putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ? GREEN : RED);
75.if( blinkOutput )
- bitwise_not(view, view);
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.{
- Mat view, rview, map1, map2;
- initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),
- getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0),
- imageSize, CV_16SC2, map1, map2);
- for(int i = 0; i < (int)s.imageList.size(); i++ )
- {
- view = imread(s.imageList[i], 1);
- if(view.empty())
- continue;
- remap(view, rview, map1, map2, INTER_LINEAR);
- imshow("Image View", rview);
- char c = waitKey();
- if( c == ESC_KEY || c == 'q' || c == 'Q' )
- break;
- }
95.}
The calibration and save
每个摄像机只需要校准一次,在成功校准后,保存它们是有意义的。这样以后就可以将这些值加载到程序中。因此,首先进行校准,如果校准成功,将结果保存到OpenCV样式的XML或YAML文件中,具体取决于在配置文件中给出的扩展名。
因此,在第一个函数中,只是将这两个过程分开。因为想保存许多校准变量,所以将在这里创建这些变量,并将它们传递给校准和保存函数。再次,将不显示保存部分,因为与校准几乎没有共同点。浏览源文件以了解如何和内容:
bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,vector<vector<Point2f> > imagePoints )
{
vector<Mat> rvecs, tvecs;
vector<float> reprojErrs;
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函数计算角点,如下所示:
- void calcBoardCornerPositions(Size boardSize, float squareSize, vector<Point3f>& corners,
- Settings::Pattern patternType /*= Settings::CHESSBOARD*/)
- {
- corners.clear();
- switch(patternType)
- {
- case Settings::CHESSBOARD:
- case Settings::CIRCLES_GRID:
- for( int i = 0; i < boardSize.height; ++i )
- for( int j = 0; j < boardSize.width; ++j )
- corners.push_back(Point3f(float( j*squareSize ), float( i*squareSize ), 0));
- break;
- case Settings::ASYMMETRIC_CIRCLES_GRID:
- for( int i = 0; i < boardSize.height; i++ )
- for( int j = 0; j < boardSize.width; j++ )
- corners.push_back(Point3f(float((2*j + i % 2)*squareSize), float(i*squareSize), 0));
- break;
- }
- }
And then multiply it as:
vector<vector<Point3f> > objectPoints(1);
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<double>(0,0) = 1.0;
- The distortion coefficient matrix. Initialize with zero.
- distCoeffs = Mat::zeros(8, 1, CV_64F);
- 该函数将为所有视图计算旋转和平移矢量,该矢量将对象点(在模型坐标空间中给出)转换为图像点(在世界坐标空间中给出)。第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<vector<Point3f> >& objectPoints,
- const vector<vector<Point2f> >& imagePoints,
- const vector<Mat>& rvecs, const vector<Mat>& tvecs,
- const Mat& cameraMatrix , const Mat& distCoeffs,
- vector<float>& perViewErrors)
- {
- vector<Point2f> imagePoints2;
- 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文件,该文件描述了要使用的图像:
<?xml version="1.0"?>
<opencv_storage>
<images>
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>
</opencv_storage>
然后将images/cameracalibration/VID5/VID5.XML指定为配置文件中的输入。在应用程序运行时,可见棋盘模式:
应用失真消除后,得到:
通过将“输入宽度”(input width)设置为4,将“高度”(height)设置为11,同样的方法也适用于这种不对称的圆形图案。使用了一个实时摄像机feed,为输入指定了它的ID(“1”)。以下是检测到的模式的照片:
在这两种情况下,在指定的输出XML/YAML文件中,将发现摄影机和失真系数矩阵:
<Camera_Matrix type_id="opencv-matrix">
<rows>3</rows>
<cols>3</cols>
<dt>d</dt>
<data>
6.5746697944293521e+002 0. 3.1950000000000000e+002 0.
6.5746697944293521e+002 2.3950000000000000e+002 0. 0. 1.</data></Camera_Matrix>
<Distortion_Coefficients type_id="opencv-matrix">
<rows>5</rows>
<cols>1</cols>
<dt>d</dt>
<data>
-4.1802327176423804e-001 5.0715244063187526e-001 0. 0.
-5.7843597214487474e-001</data></Distortion_Coefficients>
将这些值作为常量添加到程序中,调用initundortrectitymap和remap函数以消除失真,并使用廉价和低质量的相机享受无失真输入。
用OpenCV进行摄像机标定的更多相关文章
- [OpenCV-Python] OpenCV 中摄像机标定和 3D 重构 部分 VII
部分 VII摄像机标定和 3D 重构 OpenCV-Python 中文教程(搬运)目录 42 摄像机标定 目标 • 学习摄像机畸变以及摄像机的内部参数和外部参数 • 学习找到这些参数,对畸变图像进行修 ...
- 【OpenCV】摄像机标定+畸变校正
摄像机标定 本文目的在于记录如何使用MATLAB做摄像机标定,并通过OpenCV进行校正后的显示. 首先关于校正的基本知识通过OpenCV官网的介绍即可简单了解: http://docs.open ...
- 机器视觉学习笔记(5)——基于OpenCV的单目摄像机标定
本文CameraCalibrator类源代码来自于OpenCV2 计算机视觉编程手册(Robert Laganiere 著 张静 译) 强烈建议阅读机器视觉学习笔记(4)--单目摄像机标定参数说明之后 ...
- [zt]摄像机标定(Camera calibration)笔记
http://www.cnblogs.com/mfryf/archive/2012/03/31/2426324.html 一 作用建立3D到2D的映射关系,一旦标定后,对于一个摄像机内部参数K(光心焦 ...
- 使用OpenCV进行相机标定
1. 使用OpenCV进行标定 相机已经有很长一段历史了.但是,伴随着20世纪后期的廉价针孔照相机的问世,它们已经变成我们日常生活的一种常见的存在.不幸的是,这种廉价是由代价的:显著的变形.幸运的是, ...
- Matlab 摄像机标定+畸变校正
博客转载自:http://blog.csdn.net/Loser__Wang/article/details/51811347 本文目的在于记录如何使用MATLAB做摄像机标定,并通过opencv进行 ...
- 基于EmguCV的摄像机标定及矫正
标签: EmguCV摄像头标定C# 2015-05-03 14:55 501人阅读 评论(6) 收藏 举报 分类: C# 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] ...
- 张正友相机标定Opencv实现以及标定流程&&标定结果评价&&图像矫正流程解析(附标定程序和棋盘图)
使用Opencv实现张正友法相机标定之前,有几个问题事先要确认一下,那就是相机为什么需要标定,标定需要的输入和输出分别是哪些? 相机标定的目的:获取摄像机的内参和外参矩阵(同时也会得到每一幅标定图像的 ...
- halcon摄像机标定
摄像机标定程序: 注意:E:/calibration_image :为标定图像文件路径 'E:/calibration_description/caltab_123mm.descr:为标定 ...
随机推荐
- PAT 乙级 -- 1001 -- 害死人不偿命的(3n+1)猜想
题目: 卡拉兹(Callatz)猜想: 对任何一个自然数n,如果它是偶数,那么把它砍掉一半:如果它是奇数,那么把(3n+1)砍掉一半.这样一直反复砍下去,最后一定在某一步得到n=1.卡拉兹在1950年 ...
- unresolved external symbol _WinMain@16
vc下,新建一个win32项目,就写了个main函数,打印hello ,出现了如标题所述的错误 原因: 你建立了一个WINDOWS应用程序,可是你却在入口函数的时候使用main而不是WinMain 解 ...
- Spring的安装
Spring的安装 Spring框架包 spring-framework-4.3.6RELEASE-dist.zip http://repo.spring.io/simple/libs-release ...
- 脱壳入门----脱ASPack壳保护的DLL
前言 结合脱dll壳的基本思路,对看雪加密解密里的一个ASPack壳保护的dll进行脱壳分析. 脱壳详细过程 寻找程序的OEP 先将目标DLL拖入OD,来到壳的入口处. 然后利用堆栈平衡原理在push ...
- c#log4net简单好用的配置
新建文件log4net.config 编辑文件log4net.config <configuration> <configSections> <!--日志记录--> ...
- Pytorch_Part2_数据模块
VisualPytorch beta发布了! 功能概述:通过可视化拖拽网络层方式搭建模型,可选择不同数据集.损失函数.优化器生成可运行pytorch代码 扩展功能:1. 模型搭建支持模块的嵌套:2. ...
- KMP板子(其实还没完全懂...)
KMP模板 1.next数组的实际含义 next数组从-1开始,主串a,子串b,next[j]=k,满足b[0,k-1]==b[j-k,j-1],k同时也为b子串前缀的下标,j为b子串后缀的下标 ge ...
- JMM——Java内存模型抽象|八种同步操作|操作规则
JMM 调用栈&本地变量在线程栈上 对象整体在堆上(包括其本地变量,不论类型),栈有其引用即可访问, 线程调用同一个对象时,是访问该对象的私有拷贝 每个CPU有自己的高速缓存 高速缓存存在意义 ...
- [Java]数据分析--聚类
距离度量 需求:计算两点间的欧几里得距离.曼哈顿距离.切比雪夫距离.堪培拉距离 实现:利用commons.math3库相应函数 1 import org.apache.commons.math3.ml ...
- 【转载】CentOS下查看电脑硬件设备属性命令
CentOS下查看电脑硬件设备属性命令2018年09月13日 17:48:31 乔烨 阅读数 510如何在linux下查看电脑硬件设备属性 # uname -a # 查看内核/操作系统/CPU信息 # ...