相機(jī)已經(jīng)存在了很長(zhǎng)時(shí)間。然而,隨著20世紀(jì)后期引入便宜的針孔相機(jī),它們?cè)谌粘I钪谐蔀槌R姷氖录?。不幸的是,這種廉價(jià)的價(jià)格是:顯著的扭曲。幸運(yùn)的是,這些是常數(shù),校準(zhǔn)和一些重新映射,我們可以糾正這一點(diǎn)。此外,通過校準(zhǔn),您還可以確定相機(jī)的自然單位(像素)與實(shí)際單位之間的關(guān)系(例如毫米)。
對(duì)于失真,OpenCV考慮到徑向和切向因素。對(duì)于徑向因子,使用以下公式:
因此,對(duì)于坐標(biāo)處的未失真像素點(diǎn) (x,y),其在失真圖像上的位置將為。徑向變形的存在表現(xiàn)為“barrel”或“fish-eye”效應(yīng)的形式
由于攝像鏡頭不完全平行于成像平面,因此會(huì)發(fā)生切向畸變。它可以通過公式表示:
所以我們有五個(gè)失真參數(shù),它們?cè)贠penCV中呈現(xiàn)為具有5列的一行矩陣:
現(xiàn)在對(duì)于單位轉(zhuǎn)換,我們使用以下公式:
這里通過使用單應(yīng)性坐標(biāo)系(w = Z)來解釋w的存在。未知參數(shù)是fx和fy(攝像機(jī)焦距)和(cx,cy),它們是以像素坐標(biāo)表示的光學(xué)中心。如果對(duì)于兩個(gè)軸,使用給定的a縱橫比(通常為1)的公共焦距,則fy=fx?a a,并且在上面的公式中,我們將具有單個(gè)焦距f。包含這四個(gè)參數(shù)的矩陣稱為相機(jī)矩陣。雖然失真系數(shù)是相同的,無論使用的相機(jī)分辨率,這些應(yīng)該與校準(zhǔn)分辨率的當(dāng)前分辨率一起縮放。
確定這兩個(gè)矩陣的過程是校準(zhǔn)。這些參數(shù)的計(jì)算是通過基本幾何方程來完成的。所使用的方程取決于所選擇的校準(zhǔn)對(duì)象。目前OpenCV支持三種校準(zhǔn)對(duì)象類型:
基本上,您需要使用相機(jī)拍攝這些圖案的快照,并讓OpenCV找到它們。每個(gè)發(fā)現(xiàn)的模式產(chǎn)生一個(gè)新的方程。要解決方程式,您需要至少預(yù)定數(shù)量的模式快照來形成一個(gè)精心設(shè)計(jì)的方程式。這個(gè)數(shù)字對(duì)于棋盤圖案更高,而對(duì)于圓形圖案則更小。例如,理論上棋盤圖案至少需要兩個(gè)快照。然而,實(shí)際上我們的輸入圖像中存在大量的噪音,所以為了獲得良好的效果,您可能需要至少10個(gè)不同位置的輸入圖形快照。
示例應(yīng)用程序?qū)ⅲ?/p>
您也可以在samples/cpp/tutorial_code/calib3d/camera_calibration/OpenCV源庫的文件夾中找到源代碼,或者從這里下載。該程序有一個(gè)參數(shù):其配置文件的名稱。如果沒有給出,那么它將嘗試打開一個(gè)名為“default.xml”的文件。以下是 XML格式的示例配置文件。在配置文件中,您可以選擇將相機(jī)用作輸入,視頻文件或圖像列表。如果您選擇最后一個(gè),您將需要?jiǎng)?chuàng)建一個(gè)配置文件,您可以枚舉要使用的圖像。這是一個(gè)例子。要記住的重要部分是圖像需要使用絕對(duì)路徑或應(yīng)用程序工作目錄中的相對(duì)路徑進(jìn)行指定。
該應(yīng)用程序從配置文件讀取設(shè)置啟動(dòng)。雖然這是它的重要組成部分,但與本教程的主題無關(guān):攝像機(jī)校準(zhǔn)。因此,我選擇不在這里發(fā)布該部分的代碼。技術(shù)背景如何做到這一點(diǎn),你可以在文件輸入和輸出中找到使用XML和YAML文件的教程。
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
為此我使用簡(jiǎn)單的OpenCV類輸入操作。閱讀文件后,我有一個(gè)額外的后處理功能來檢查輸入的有效性。只有所有的輸入都是好的,那么goodInput變量才是真的。
之后,我們有一個(gè)很大的循環(huán),我們進(jìn)行以下操作:從圖像列表,相機(jī)或視頻文件中獲取下一個(gè)圖像。如果這樣做失敗或者我們有足夠的圖像,那么我們運(yùn)行校準(zhǔn)過程。在圖像的情況下,我們退出循環(huán),否則剩余的幀將通過從DETECTION模式切換到CALIBRATED模式而不會(huì)失真(如果選項(xiàng)被設(shè)置)。
for(;;)
{
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() >= (size_t)s.nrFrames )
{
if( runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints))
mode = CALIBRATED;
else
mode = DETECTION;
}
if(view.empty()) // If there are no more images stop the loop
{
// if calibration threshold was not reached yet, calibrate now
if( mode != CALIBRATED && !imagePoints.empty() )
runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints);
break;
}
對(duì)于某些相機(jī),我們可能需要翻轉(zhuǎn)輸入圖像。這里我們也這樣做。
上面提到的方程式的形成旨在找到輸入中的主要模式:在棋盤的情況下,這是方格和圓圈的角落,以及圓圈本身。這些的位置將形成將被寫入pointBuf向量的結(jié)果。
vector<Point2f> pointBuf;
bool found;
int chessBoardFlags = CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE;
if(!s.useFisheye) {
// fast check erroneously fails with high distortions like fisheye
chessBoardFlags |= CALIB_CB_FAST_CHECK;
}
switch( s.calibrationPattern ) // Find feature points on the input format
{
case Settings::CHESSBOARD:
found = findChessboardCorners( view, s.boardSize, pointBuf, chessBoardFlags);
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;
default:
found = false;
break;
}
根據(jù)輸入模式的類型,您可以使用cv :: findChessboardCorners或cv :: findCirclesGrid函數(shù)。對(duì)于他們來說,您傳遞當(dāng)前的圖像和板的大小,您將獲得圖案的位置。此外,它們返回一個(gè)布爾變量,它指出在輸入中是否找到了模式(我們只需要考慮那些是真實(shí)的圖像!)。
然后在相機(jī)的情況下,只有當(dāng)輸入延遲時(shí)間過去時(shí),才能拍攝相機(jī)圖像。這樣做是為了讓用戶移動(dòng)棋盤并獲得不同的圖像。類似的圖像產(chǎn)生類似的等式,并且在校準(zhǔn)步驟中的類似方程將形成不適當(dāng)?shù)膯栴},因此校準(zhǔn)將失敗。對(duì)于方形圖像,角落的位置只是近似值。我們可以通過調(diào)用cv :: cornerSubPix函數(shù)來改進(jìn)這一點(diǎn)。它將產(chǎn)生更好的校準(zhǔn)結(jié)果。之后,我們向imagePoints向量添加一個(gè)有效的輸入結(jié)果,將所有方程集合到一個(gè)容器中。最后,為了可視化反饋的目的,我們將使用cv :: findChessboardCorners在輸入圖像上繪制找到的點(diǎn) 功能。
if ( found) // If done with success,
{
// improve the found corners' coordinate accuracy for chessboard
if( s.calibrationPattern == Settings::CHESSBOARD)
{
Mat viewGray;
cvtColor(view, viewGray, COLOR_BGR2GRAY);
cornerSubPix( viewGray, pointBuf, Size(11,11),
Size(-1,-1), TermCriteria( TermCriteria::EPS+TermCriteria::COUNT, 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 );
}
該部分顯示圖像上的文本輸出。
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);
如果我們運(yùn)行校準(zhǔn),并使用失真系數(shù)得到相機(jī)的矩陣,我們可能需要使用cv :: undistort函數(shù)來校正圖像:
if( mode == CALIBRATED && s.showUndistorsed )
{
Mat temp = view.clone();
if (s.useFisheye)
cv::fisheye::undistortImage(temp, view, cameraMatrix, distCoeffs);
else
undistort(temp, view, cameraMatrix, distCoeffs);
}
然后我們顯示圖像并等待一個(gè)輸入鍵,如果這是你,我們切換失真消除,如果是g,我們?cè)俅螁?dòng)檢測(cè)過程,最后為ESC鍵退出應(yīng)用程序:
imshow("Image View", view);
char key = (char)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();
}
當(dāng)您使用圖像列表時(shí),無法刪除循環(huán)中的失真。因此,您必須在循環(huán)后執(zhí)行此操作。利用這一點(diǎn)現(xiàn)在我將擴(kuò)展cv :: undistort函數(shù),其實(shí)際上首先調(diào)用cv :: initUndistortRectifyMap來查找轉(zhuǎn)換矩陣,然后使用cv :: remap函數(shù)執(zhí)行轉(zhuǎn)換。因?yàn)槌晒Φ男?zhǔn)映射計(jì)算需要做一次,使用這種擴(kuò)展形式可以加快你的應(yīng)用程序:
if( s.inputType == Settings::IMAGE_LIST && s.showUndistorsed )
{
Mat view, rview, map1, map2;
if (s.useFisheye)
{
Mat newCamMat;
fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
Matx33d::eye(), newCamMat, 1);
fisheye::initUndistortRectifyMap(cameraMatrix, distCoeffs, Matx33d::eye(), newCamMat, imageSize,
CV_16SC2, map1, map2);
}
else
{
initUndistortRectifyMap(
cameraMatrix, distCoeffs, Mat(),
getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0), imageSize,
CV_16SC2, map1, map2);
}
for(size_t i = 0; i < s.imageList.size(); i++ )
{
view = imread(s.imageList[i], IMREAD_COLOR);
if(view.empty())
continue;
remap(view, rview, map1, map2, INTER_LINEAR);
imshow("Image View", rview);
char c = (char)waitKey();
if( c == ESC_KEY || c == 'q' || c == 'Q' )
break;
}
}
因?yàn)樾?zhǔn)只需要對(duì)每個(gè)攝像機(jī)進(jìn)行一次,所以在成功校準(zhǔn)后保存它是有意義的。這樣一來,您可以將這些值加載到程序中。因此,我們首先進(jìn)行校準(zhǔn),如果成功,將結(jié)果保存到OpenCV樣式的XML或YAML文件中,這取決于在配置文件中給出的擴(kuò)展名。
因此,在第一個(gè)功能中,我們只是分開這兩個(gè)進(jìn)程。因?yàn)槲覀円4嬖S多校準(zhǔn)變量,所以我們將在這里創(chuàng)建這些變量,并將它們傳遞給校準(zhǔn)和保存功能。再次,我不會(huì)顯示保存部分,因?yàn)樗c校準(zhǔn)幾乎沒有共同之處。瀏覽源文件,以了解如何和什么:
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 << endl;
if (ok)
saveCameraParams(s, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, imagePoints,
totalAvgErr);
return ok;
}
我們?cè)?a rel="external nofollow" target="_blank" rel="external nofollow" target="_blank" target="_blank">cv :: calibrateCamera功能的幫助下進(jìn)行校準(zhǔn)。它具有以下參數(shù):
static 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(j*squareSize, 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((2*j + i % 2)*squareSize, i*squareSize, 0));
break;
default:
break;
}
}
然后將其乘以:
vector <vector <Point3f>> objectPoints(1);
calcBoardCornerPositions(s.boardSize,s.squareSize,objectPoints [0],s.calibrationPattern);
objectPoints.resize(imagePoints.size(),objectPoints [0]);
cameraMatrix = Mat :: eye(3,3,CV_64F);
if(s.flag&CALIB_FIX_ASPECT_RATIO)
cameraMatrix.at < double >(0,0)= s.aspectRatio;
distCoeffs = Mat::zeros(8, 1, CV_64F);
double rms = calibrateCamera(objectPoints,imagePoints,imageSize,cameraMatrix,
distCoeffs,rvecs,tvecs,s.flag | CV_CALIB_FIX_K4 | CV_CALIB_FIX_K5);
static 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, bool fisheye)
{
vector<Point2f> imagePoints2;
size_t totalPoints = 0;
double totalErr = 0, err;
perViewErrors.resize(objectPoints.size());
for(size_t i = 0; i < objectPoints.size(); ++i )
{
if (fisheye)
{
fisheye::projectPoints(objectPoints[i], imagePoints2, rvecs[i], tvecs[i], cameraMatrix,
distCoeffs);
}
else
{
projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, imagePoints2);
}
err = norm(imagePoints[i], imagePoints2, NORM_L2);
size_t n = objectPoints[i].size();
perViewErrors[i] = (float) std::sqrt(err*err/n);
totalErr += err*err;
totalPoints += n;
}
return std::sqrt(totalErr/totalPoints);
}
讓這個(gè)輸入棋盤模式的大小為9 X 6.我已經(jīng)使用了一個(gè)AXIS網(wǎng)絡(luò)攝像機(jī)來創(chuàng)建幾個(gè)板卡的快照并將其保存到VID5目錄中。我將其放在images/CameraCalibration我的工作目錄的文件夾中,并創(chuàng)建了以下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
在配置文件中作為輸入傳遞。這是在應(yīng)用程序運(yùn)行時(shí)發(fā)現(xiàn)的棋盤模式:
應(yīng)用失真去除后,我們得到:
通過將輸入寬度設(shè)置為4并將高度設(shè)置為11 ,可以對(duì)該不對(duì)稱圓形圖案進(jìn)行相同的工作。此時(shí),我通過為輸入指定其ID(“1”),使用了實(shí)時(shí)相機(jī)饋送。以下是檢測(cè)到的模式應(yīng)該如何:
在這兩種情況下,在指定的輸出XML / YAML文件中,您將找到相機(jī)和失真系數(shù)矩陣:
< camera_matrix type_id = “opencv-matrix” >
< rows > 3 </ rows >
< cols > 3 </ cols >
< dt > d </ dt >
< data >
6.5746697944293521 e +002 0. 3.1950000000000000 e +002 0。
6.5746697944293521 e +002 2.3950000000000000 e +002 0. 0. 1。</ data > </ camera_matrix >
< distortion_coefficients type_id = “opencv-matrix” >
< rows > 5 </ rows >
< cols > 1 </ cols >
< dt > d </ dt >
< data >
-4.1802327176423804 e-001 5.0715244063187526 e-001 0. 0。
-5.7843597214487474 e-001 </ data > </ distortion_coefficients >
將這些值作為常量添加到程序中,調(diào)用cv :: initUndistortRectifyMap和cv :: remap函數(shù)來消除失真,并享受無瑕疵的便宜和低質(zhì)量相機(jī)輸入。
更多建議: