用OpenCV进行摄像机标定
用OpenCV進行攝像機標定
照相機已經(jīng)存在很長時間了。然而,隨著廉價針孔相機在20世紀末的引入,日常生活中變得司空見慣。不幸的是,這種廉價伴隨著它的代價:顯著的扭曲。幸運的是,這些常數(shù),通過校準和一些重新映射,可以糾正這一點。此外,通過校準,還可以確定相機的自然單位(像素)和真實世界單位(例如毫米)之間的關系。
原理
對于畸變,OpenCV考慮了徑向和切向因素。對于徑向,使用以下公式:
So for an old pixel point at
coordinate in the input image, for a corrected output image its position will be
.
徑向畸變的存在表現(xiàn)為“桶”或“魚眼”效應。
由于攝像鏡頭與成像平面不完全平行,因此會發(fā)生切向失真。通過以下公式進行修正:
有五個失真參數(shù),在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. 包含這四個參數(shù)的矩陣稱為攝像機矩陣。盡管失真系數(shù)是相同的,不管使用何種攝像頭分辨率,但這些失真系數(shù)應與當前校準分辨率一起縮放。
確定這兩個矩陣的變換過程就是校準。這些參數(shù)的計算是通過一些基本的幾何方程來完成的。使用的方程式,取決于使用的校準對象。目前OpenCV支持三種類型的對象進行校準:
?經(jīng)典黑白棋盤
?對稱圓模式
?不對稱圓形圖案
基本上,需要用相機拍下這些圖案的快照,然后讓OpenCV找到它們。在一個新的方程中,每一個可見的模式都相等。為了解這個方程,至少需要預定數(shù)量的模式快照,來形成一個適配的方程組。這個數(shù)字具有較高的棋盤圖案和較少的圓的。例如,在理論上,一個棋盤至少需要兩個快照。然而,實際上,輸入圖像中存在大量的噪聲,因此為了獲得好的結(jié)果,可能需要至少10個不同位置的輸入模式的良好快照。
目標
示例應用程序?qū)?#xff1a;
?確定失真矩陣
?確定攝像機矩陣
?攝像機、視頻和圖像文件列表的輸入
?從XML/YAML文件進行配置
?將結(jié)果保存到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/文件夾中找到源代碼。程序只有一個參數(shù)。其配置文件的名稱。如果沒有,它將嘗試打開一個名為“default.xml”. 下面是一個XML格式的示例配置文件。在配置文件中,可以選擇使用相機、視頻文件或圖像列表作為輸入。如果選擇后者,則需要創(chuàng)建一個配置文件,在其中枚舉要使用的映像。下面是一個例子。需要記住的重要一點是,需要使用應用程序工作目錄中的絕對路徑或相對路徑來指定映像??梢栽谇懊嫣岬降哪夸浿姓业剿羞@些。
應用程序首先從配置文件中讀取設置。盡管這是其中的一個重要部分,但與本文的主題無關:相機校準。因此,選擇不在這里發(fā)布代碼部分。關于如何做到這一點的技術(shù)背景可以在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; -
}
-
fs[“Settings”] >> s;
-
fs.release(); // close Settings file
-
if (!s.goodInput)
-
{
-
cout << "Invalid input detected. Application stopping. " << endl; -
return -1; -
}
使用簡單的OpenCV類輸入操作。在讀取文件之后,有一個額外的后處理函數(shù),來檢查輸入的有效性。只有當它們都正常,goodInput變量才是正常。
獲取下一個輸入,如果失敗,或者有足夠的輸入。此后,有一個大的循環(huán),在這里,做以下操作:從圖像列表中獲取下一個圖像,相機或視頻文件。如果這失敗了,或者有足夠的圖像,運行校準過程。在圖像的情況下,步出循環(huán),否則其余的幀將不失真(如果選項設置),通過改變檢測模式到校準一。 -
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 );
-
}
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.
-
vector 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;
-
}
根據(jù)輸入模式的類型,可以使用findChessboardCorners或findCirclesGrid函數(shù)。對于這兩種情況,會傳遞當前的圖像、電路板的大小,會得到圖案的位置。此外,還返回一個布爾變量,說明在輸入中是否可以找到模式(只需要考慮圖像中的情況)。
同樣,對于相機,只在輸入延遲時間,過后才拍攝相機圖像。用戶移動棋盤,獲得不同的圖像。相同的圖像意味著相同的方程,在標定時,相同的方程將形成不適定問題,因此標定將失敗。對于正方形圖像,角點的位置只是近似值。可以通過調(diào)用cornerSubPix函數(shù)來改進這一點。這樣可以得到更好的標定結(jié)果。之后,將一個有效的輸入結(jié)果添加到imagePoints向量中,將所有方程收集到一個容器中。最后,為了實現(xiàn)可視化反饋,將使用findChessboardCorners函數(shù)在輸入圖像上繪制找到的點。
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 );
}
為用戶顯示狀態(tài)和結(jié)果,以及應用程序的命令行控制。顯示部分由實時文本輸出組成,對于要顯示“捕獲”幀的視頻或相機輸入,只需按位求反輸入圖像。
58. //----------------------------- Output Text ------------------------------------------------
59. string msg = (mode == CAPTURING) ? “100/100” :
60. 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 - 2textSize.width - 10, view.rows - 2baseLine - 10);
64.
65. if( mode == CAPTURING )
66. {
67. if(s.showUndistorsed)
68. msg = format( “%d/%d Undist”, (int)imagePoints.size(), s.nrFrames );
69. else
70. msg = format( “%d/%d”, (int)imagePoints.size(), s.nrFrames );
71. }
72.
73. putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ? GREEN : RED);
74.
75. if( blinkOutput )
76. 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();
}
顯示圖像的失真消除。使用圖像列表時,無法消除循環(huán)中的失真。因此,必須在循環(huán)之后附加。利將展開無失真函數(shù),實際上是首先調(diào)用initundortrectitymap來找出變換矩陣,然后用remap函數(shù)進行變換。在成功校準后,地圖計算只需進行一次,通過使用此擴展表格,可以加快應用程序的速度:
77. if( s.inputType == Settings::IMAGE_LIST && s.showUndistorsed )
78. {
79. Mat view, rview, map1, map2;
80. initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(),
81. getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0),
82. imageSize, CV_16SC2, map1, map2);
83.
84. for(int i = 0; i < (int)s.imageList.size(); i++ )
85. {
86. view = imread(s.imageList[i], 1);
87. if(view.empty())
88. continue;
89. remap(view, rview, map1, map2, INTER_LINEAR);
90. imshow(“Image View”, rview);
91. char c = waitKey();
92. if( c == ESC_KEY || c == ‘q’ || c == ‘Q’ )
93. break;
94. }
95. }
The calibration and save
每個攝像機只需要校準一次,在成功校準后,保存它們是有意義的。這樣以后就可以將這些值加載到程序中。因此,首先進行校準,如果校準成功,將結(jié)果保存到OpenCV樣式的XML或YAML文件中,具體取決于在配置文件中給出的擴展名。
因此,在第一個函數(shù)中,只是將這兩個過程分開。因為想保存許多校準變量,所以將在這里創(chuàng)建這些變量,并將它們傳遞給校準和保存函數(shù)。再次,將不顯示保存部分,因為與校準幾乎沒有共同點。瀏覽源文件以了解如何和內(nèi)容:
bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,vector<vector > imagePoints )
{
vector rvecs, tvecs;
vector 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函數(shù)進行校準。具有以下參數(shù):
?物體角點。這是點3f向量的向量,對于每個輸入圖像,它描述了模式的外觀。如果有一個平面圖案(如棋盤),可以簡單地將所有Z坐標設置為零。這是這些要點所在的要點的集合。對所有的輸入圖像使用一個模式,只需計算一次,然后乘以所有其他的輸入視圖。使用CalcBoardCornePositions函數(shù)計算角點,如下所示:
?
? void calcBoardCornerPositions(Size boardSize, float squareSize, vector& 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( jsquareSize ), float( isquareSize ), 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((2j + i % 2)squareSize), float(isquareSize), 0));
? break;
? }
? }
And then multiply it as:
vector<vector > objectPoints(1);
calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);
objectPoints.resize(imagePoints.size(),objectPoints[0]);
? ?圖像點。這是點2f向量的向量,對于每個輸入圖像,該向量包含找到重要點(棋盤的角點和圓模式的圓心)的位置。已經(jīng)從findChessboardCorners或findCirclesGrid函數(shù)返回的內(nèi)容中,收集了這個。只需要把它傳下去。
? ?從相機、視頻文件或圖像中獲取的圖像大小。
? ?攝像機矩陣。如果使用“固定縱橫比”選項,則需要將設置
為零:
? cameraMatrix = Mat::eye(3, 3, CV_64F);
? if( s.flag & CV_CALIB_FIX_ASPECT_RATIO )
? cameraMatrix.at(0,0) = 1.0;
? The distortion coefficient matrix. Initialize with zero.
? distCoeffs = Mat::zeros(8, 1, CV_64F);
? ?該函數(shù)將為所有視圖計算旋轉(zhuǎn)和平移矢量,該矢量將對象點(在模型坐標空間中給出)轉(zhuǎn)換為圖像點(在世界坐標空間中給出)。第7和第8參數(shù)是矩陣的輸出向量,該矩陣在第i個位置中,包含第i個目標角點到第i個圖像點的旋轉(zhuǎn)和平移向量。
? ?最后一個參數(shù)是flag。需要在這里指定選項,如固定焦距的縱橫比、假設零切向失真或固定主點。
?
double rms = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,
distCoeffs, rvecs, tvecs, s.flag|CV_CALIB_FIX_K4|CV_CALIB_FIX_K5);
? ?函數(shù)返回平均重投影誤差。這個數(shù)字很好地估計了,找到的參數(shù)到底有多精確。應該盡可能接近于零。在給定內(nèi)稟矩陣、畸變矩陣、旋轉(zhuǎn)矩陣和平移矩陣的情況下,可以利用投影點,將物體點變換為像點,來計算一個視圖的誤差。然后,計算得到的絕對范數(shù)之間的轉(zhuǎn)換和角/圓發(fā)現(xiàn)算法。為了找出平均誤差,計算了所有校準圖像的誤差算術(shù)平均值。
? double computeReprojectionErrors( const vector<vector >& objectPoints,
? const vector<vector >& imagePoints,
? const vector& rvecs, const vector& tvecs,
? const Mat& cameraMatrix , const Mat& distCoeffs,
? vector& perViewErrors)
? {
? vector 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(errerr/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攝像頭,創(chuàng)建幾個板的快照,并將其保存到VID5目錄中。我已將其放入工作目錄的images/cameracalibration文件夾中,并創(chuàng)建了以下VID5.XML文件,該文件描述了要使用的圖像:
<opencv_storage>
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
</opencv_storage>
然后將images/cameracalibration/VID5/VID5.XML指定為配置文件中的輸入。在應用程序運行時,可見棋盤模式:
應用失真消除后,得到:
通過將“輸入寬度”(input width)設置為4,將“高度”(height)設置為11,同樣的方法也適用于這種不對稱的圓形圖案。使用了一個實時攝像機feed,為輸入指定了它的ID(“1”)。以下是檢測到的模式的照片:
在這兩種情況下,在指定的輸出XML/YAML文件中,將發(fā)現(xiàn)攝影機和失真系數(shù)矩陣:
<Camera_Matrix type_id=“opencv-matrix”>
3
3
總結(jié)
以上是生活随笔為你收集整理的用OpenCV进行摄像机标定的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 摄像头标定GML Camera Cali
- 下一篇: 向量算子优化Vector Operati