本文是对OpenCV文档中《Camera calibration With OpenCV》一章的翻译与学习。由于本人英语底子不是特别强,有误请见谅,谢谢!
该网址是:
http://docs.opencv.org/doc/tutorials/calib3d/camera_calibration/camera_calibration.html
基于OpenCV的摄像机标定
摄像机已在生活中随处可见,特别是20世界末的简易针孔相机模型的引入。然而,这种偏移的制作带来了相应的代价:严重的畸变。幸运的是,这些是相互联系的,我们可以通过标定和一定的重组建来矫正麻烦的问题。更远的考虑,你可以通过标定来观测摄像机的自然单元(像素)和物理世界单位(例如毫米)。
理论
对于OpenCV中畸变,我们引入切向因素和径向因素。对于径向畸变使用以下公式:
相对于输入照片中的在(x,y)坐标系中旧的像素点,它在正确的输出照片中将是(xcorrected,ycorrected)。径向畸变的主要表现是在制作中的的“筒形”或者“鱼眼”现象。
由于组装摄像机过程中,无法完全保证透镜与图像平面的绝对平行,从而产生了切向畸变。对于切向畸变使用以下公式:
由此,我们有了5个畸变参数,在OpenCV中表现为一个5×1的矩阵:
现在对于单位转换,我们可用以下公式:
这里w的解释是由于使用了单应性坐标系统(w=Z)。未知参数有:相机焦距fx和fy,和像素坐标系中的光心表达的(cx,cy)。如果对于两个坐标系,给一个普通的焦距和比例a(通常是1),然后fy=fx*a,以及对于上面的表达式我们将有一个焦距f。我们称含有这四个参数的矩阵为摄像机矩阵。无论摄像机结果是否被使用,畸变系数都是相同的,从标定结论的正确结果中可以计算出这些系数。
计算这两个矩阵的过程就是标定。这些参数是通过一系列的基本几何等式的计算得到的。等式的使用与选择的标定物有关。当前OpenCV支持以下三种标定物:
1. 经典的黑白棋盘
2. 对称的圆形图案
3. 非对称的圆形图案
基本上来说,你需要使用你的摄像机对这些标定物拍一系列照片,并让OpenCV去寻找到它们。每一个被发现的照片将会产生一个新的等式。为了解决这些等式你至少需要来自一个适当的等式系统中的一系列事先准备好的序列照片。事实中棋盘比圆形图案更适合标定。例如理论中棋盘标定至少需要两个图像。然而现实中我们所有的输入图片有很多噪音,所以为了准确的结果我们至少要准备10张以上的不同位置的输入图片。
目标
简单的应用将要:
1. 确定畸变矩阵
2. 确定摄像机矩阵
3. 从相机,摄像机和图片文件中的到输入
4. 从XML/YAML文件中读取确认
5. 将结果保存在XML/YAML文件中
6. 计算再投影误差
源代码
可以在OpenCV源代码库中的sample/cpp/tutorial_code/calib3d/camera_calibration目录中找到源代码,或者在此点击下载。程序只有一个内容提要:名称就是它自己的确认文件。如果没有它会打开一个“default.xml”文件。这有一个简易的XML格式的确认文件。在确认文件中你可以选择摄像机、摄影机或者序列图片作为输入。如果你选择最后一个,你需要创建一个确认文件来确保图片能够使用。这里有个例子。重要的一步是这些图片必须使用完全正确的路径或者与你的工作目录相关联。以上说明你在简例文件夹中都可以找到。
应用开启后就会从确认文件中读取设置。然而,尽管这是重要的一步,但是它没有对于我们的项目——摄像机标定——有任何作用。因此,我并没有选择将这部分代码在这里解释。如何操作的技术背景可以在以下辅导中找到:使用XML\YAML文件导入导出。
解释
1.读取设置
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;
}
fs["Settings"] >> s;
fs.release(); // close Settings file
if (!s.goodInput)
{
cout << "Invalid input detected. Application stopping. " << endl;
return -1;
}
这一段我使用简单的OpenCV的输入选项类。已填加的后处理功能能辨别输入量,读取后,只有输入量为好的,goodInput的值才能为真。
2.下一步,如果失败或者拥有足够的输入量,开始标定
随后我们将有个大的循环,在这循环中我们将完成以下操作:从图像列表,摄像机或者摄影机文件中获取下一个图像。如果失败或者拥有足够的输入量,我们将开始标定过程。如果跳出循环或者其他剩余的框架的图像,将会通过改变DETECTION为CALIBRATED校正(如果选项已被设置)
for(int i = 0;;++i)
{
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 );
}
对于一些摄像机,我们可能要跳过输入的图像。这里我们也这样做。
3.在当前输入中寻找模块
我们上面所提的公式旨在在输入量中寻找主要的模块:在棋盘中就是方形的角点,在圆中就是圆形。这些模块的位置将会被存储在结果中,即写入pointBuf向量。
vector<Point2f> pointBuf;
bool found;
switch( s.calibrationPattern ) // Find feature points on the input format
{
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;
case Settings::CIRCLES_GRID:
found = findCirclesGrid( view, s.boardSize, pointBuf );
break;
case Settings::ASYMMETRIC_CIRCLES_GRID:
found = findCirclesGrid( view, s.boardSize, pointBuf, CALIB_CB_ASYMMETRIC_GRID );
break;
}
依据输入值的类型,你可以使用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 );
}
4.对操作者显示状态与结果,另加命令行控制应用
这部分是显示输出图像的文本数据。
//----------------------------- Output Text ------------------------------------------------
string msg = (mode == CAPTURING) ? "100/100" :
mode == CALIBRATED ? "Calibrated" : "Press 'g' to start";
int baseLine = 0;
Size textSize = getTextSize(msg, 1, 1, 1, &baseLine);
Point textOrigin(view.cols - 2*textSize.width - 10, view.rows - 2*baseLine - 10);
if( mode == CAPTURING )
{
if(s.showUndistorsed)
msg = format( "%d/%d Undist", (int)imagePoints.size(), s.nrFrames );
else
msg = format( "%d/%d", (int)imagePoints.size(), s.nrFrames );
}
putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ? GREEN : RED);
if( blinkOutput )
bitwise_not(view, view);
如果我们运行标定,获得含有畸变系数的摄像机矩阵,我们需要用undistort函数来矫正图像。
//------------------------- 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);
随后我们等待输入一个值,如果输入u我们将会把畸变去除;如果是g我们将开始检测过程;如果是ESC我们将退出应用。
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();
}
5.同时显示畸变除去后的图像
如果当在循环内无法除去序列图像的畸变,则可以在循环后做这一步。利用这一点,我们可以扩展undistort函数,第一步使用initUndistortRectifyMap来寻找转换矩阵,然后用remap函数来展示转换。因为有时成功的标定计算需要一次完成,而通过扩展格式可以加速应用。
if( s.inputType == Settings::IMAGE_LIST && s.showUndistorsed )
{
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;
}
}
标定与保存
由于标定的完成只需要一个相机,则有必要在标定成功后将其保存。随后只需在程序中加载这些数据。根据这些我们首先进行标定,如果成功了我们根据在确定文件中的扩展来讲结果保存在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;
}
在calibration函数的帮助文档下,我们可以完成标定。它有以下的参数:
1.物体点。这是一个Point3f向量,用来描述标定物的形象。如果我们有一个平面物体(例如棋盘),然后我们可以设置所有的Z轴为0。这是所有表现出的重要点的一个集合。由于我们使用唯一一个标定物来标定所有的输入图像,我们可以标定一次就能运用到其他的输入图像。我们运用calcBoardCornerPositions函数来计算角点:
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;
}
2.图像点。这是一个Point2f向量,这里面包含重要点的关联(棋盘的角点、圆模块的圆心),我们已经可用findChessboardCorners或者findCirclesGrid函数来搜寻这些点。我们现在只需将其通过。
3.从摄像机、摄影机或者图片中获得图像的大小。
4.摄像机矩阵。如果我们要使用修补后的比例选项,需要将fx设置为0:
cameraMatrix = Mat::eye(3, 3, CV_64F);
if( s.flag & CV_CALIB_FIX_ASPECT_RATIO )
cameraMatrix.at<double>(0,0) = 1.0;
5.畸变系数矩阵。初始化设置为0。
distCoeffs = Mat::zeros(8, 1, CV_64F);
6.所有的函数旨在计算出旋转和移动向量,其用来讲三维点(在模型坐标系中给出的)转换为二维点(在世界坐标系中给出的)。第7个和第8个参数是含有输出向量的矩阵,其中包括在第i个位置的第i个三维点到第i个二维点的转动与移动向量。
7.最后一个语句是标记。你需要指定一些选项,例如修补焦距的比例,假设零切向畸变,或者修补光心。
double rms = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,
distCoeffs, rvecs, tvecs, s.flag|CV_CALIB_FIX_K4|CV_CALIB_FIX_K5);
8.函数返回一个平均重投影错误值。这个值给出了所发现参数精确值的好的评估,并且这个值越接近零越好。通过给出的内参数,畸变,旋转和移动矩阵,我们可以使用projectPoints函数来转换三维点到二维点,从而计算出一个图像所产生的误差。然后我们可以计算在我们得到的转换与角点-圆点之间的完整的范数来寻找算法。为了寻找平均的误差,我们需计算所有标定完的图像所产生的每一个误差。
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
}
结果
我们所用的输入棋盘模板的大小为9×6。我已经使用摄像机AXIS IP对棋盘进行了一系列的拍照,并将这些图片存入VID5文件夹中。并将其放入images/CameraCalibration文件夹中作为我的工作目录,并且创造了VID5.XML文件来描述图像的使用:
<?xml version="1.0"?>
<opencv_storage>
<images>
images/CameraCalibration/VID5/xx1.jpg
images/CameraCalibration/VID5/xx2.jpg
images/CameraCalibration/VID5/xx3.jpg
images/CameraCalibration/VID5/xx4.jpg
images/CameraCalibration/VID5/xx5.jpg
images/CameraCalibration/VID5/xx6.jpg
images/CameraCalibration/VID5/xx7.jpg
images/CameraCalibration/VID5/xx8.jpg
</images>
</opencv_storage>
随后在确认文件中将images/CameraCalibration/VID5/VID5.XML作为输入值。以下是应用运行过程中的棋盘模块:
在除去畸变后我们得到:
同样面对宽度为4高度为11的非对称圆形模块。这一次我们使用在线摄像机,通过对其进行标记为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>
将这些数据添加到你的程序中,运用initUndistortRectifyMap函数和remap函数来除去畸变,从而对于便宜和低质量的摄像机则没有了畸变。
整篇文章是边看边操作边翻译完成,感觉有些部分翻译很不好,即使自己能懂得大概意思。我将进一步编辑加修改。