使用OpenCV相機(jī)校準(zhǔn)

2018-10-08 09:17 更新

相機(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ì)于徑向因子,使用以下公式:

使用OpenCV相機(jī)校準(zhǔn)

因此,對(duì)于坐標(biāo)處的未失真像素點(diǎn) (x,y),其在失真圖像上的位置將為。徑向變形的存在表現(xiàn)為“barrel”或“fish-eye”效應(yīng)的形式

由于攝像鏡頭不完全平行于成像平面,因此會(huì)發(fā)生切向畸變。它可以通過公式表示:

使用OpenCV相機(jī)校準(zhǔn)

所以我們有五個(gè)失真參數(shù),它們?cè)贠penCV中呈現(xiàn)為具有5列的一行矩陣:

使用OpenCV相機(jī)校準(zhǔn)

現(xiàn)在對(duì)于單位轉(zhuǎn)換,我們使用以下公式:

使用OpenCV相機(jī)校準(zhǔ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ì)象類型:

  • 古典黑白棋盤
  • 對(duì)稱圓形圖案
  • 不對(duì)稱圓形圖案

基本上,您需要使用相機(jī)拍攝這些圖案的快照,并讓OpenCV找到它們。每個(gè)發(fā)現(xiàn)的模式產(chǎn)生一個(gè)新的方程。要解決方程式,您需要至少預(yù)定數(shù)量的模式快照來形成一個(gè)精心設(shè)計(jì)的方程式。這個(gè)數(shù)字對(duì)于棋盤圖案更高,而對(duì)于圓形圖案則更小。例如,理論上棋盤圖案至少需要兩個(gè)快照。然而,實(shí)際上我們的輸入圖像中存在大量的噪音,所以為了獲得良好的效果,您可能需要至少10個(gè)不同位置的輸入圖形快照。

目標(biāo)

示例應(yīng)用程序?qū)ⅲ?/p>

  • 確定失真矩陣
  • 確定相機(jī)矩陣
  • 從相機(jī),視頻和圖像文件列表中輸入
  • 從XML / YAML文件讀取配置
  • 將結(jié)果保存到XML / YAML文件中
  • 計(jì)算重新投影誤差

源代碼

您也可以在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文件的教程。

說明

  • 閱讀設(shè)置
    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è)輸入,如果它失敗或我們有足夠的 - 校準(zhǔn)

之后,我們有一個(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)輸入圖像。這里我們也這樣做。

  • 查找當(dāng)前輸入中的模式

上面提到的方程式的形成旨在找到輸入中的主要模式:在棋盤的情況下,這是方格和圓圈的角落,以及圓圈本身。這些的位置將形成將被寫入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 :: findChessboardCornerscv :: 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 );
        }
  • 向用戶顯示狀態(tài)和結(jié)果,加上應(yīng)用程序的命令行控制

該部分顯示圖像上的文本輸出。

        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;
        }
    }

校準(zhǔn)并保存

因?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ù):

  • 對(duì)象點(diǎn)。這是Point3f矢量的矢量,每個(gè)輸入圖像描述圖案的外觀。如果我們有平面圖案(如棋盤),那么我們可以簡(jiǎn)單地將所有Z坐標(biāo)設(shè)置為零。這是收集這些重要的要點(diǎn)。因?yàn)槲覀兪褂靡粋€(gè)單一的圖案,所有的輸入圖像,我們可以計(jì)算這一次,并將其乘以所有其他輸入視圖。我們用calcBoardCornerPositions函數(shù)計(jì)算角點(diǎn):
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]);
  • 圖像點(diǎn)。這是一個(gè)Point2f向量的向量,對(duì)于每個(gè)輸入圖像,它包含重要點(diǎn)的坐標(biāo)(棋盤的角和圓形圖案的圓的中心)。我們已經(jīng)從cv :: findChessboardCornerscv :: findCirclesGrid函數(shù)收集了這個(gè)。我們只需要通過它。
  • 從相機(jī),視頻文件或圖像獲取的圖像的大小。
  • 相機(jī)矩陣。如果我們使用固定長(zhǎng)寬比選項(xiàng),我們需要設(shè)置fx:
    cameraMatrix = Mat :: eye(3,3,CV_64F);
    if(s.flag&CALIB_FIX_ASPECT_RATIO)
        cameraMatrix.at < double >(0,0)= s.aspectRatio;
  • 失真系數(shù)矩陣。初始化為零。
distCoeffs = Mat::zeros(8, 1, CV_64F);
  • 對(duì)于所有視圖,函數(shù)將計(jì)算將對(duì)象點(diǎn)(在模型坐標(biāo)空間中給出)轉(zhuǎn)換為圖像點(diǎn)(在世界坐標(biāo)空間中給出)的旋轉(zhuǎn)和平移向量。第7和第8參數(shù)是在第i個(gè)位置包含第i個(gè)對(duì)象點(diǎn)到第i個(gè)圖像點(diǎn)的旋轉(zhuǎn)和平移向量的矩陣的輸出向量。
  • 最后的爭(zhēng)論就是旗幟。您需要在此處指定選項(xiàng),例如修復(fù)焦距的寬高比,假定零切向失真或固定主點(diǎn)。
double rms = calibrateCamera(objectPoints,imagePoints,imageSize,cameraMatrix,
                            distCoeffs,rvecs,tvecs,s.flag | CV_CALIB_FIX_K4 | CV_CALIB_FIX_K5);
  • 該函數(shù)返回平均重投影誤差。這個(gè)數(shù)字給出了發(fā)現(xiàn)參數(shù)精度的良好估計(jì)。這應(yīng)盡可能接近零。給定內(nèi)在,失真,旋轉(zhuǎn)和平移矩陣,我們可以通過使用cv :: projectPoints將對(duì)象點(diǎn)轉(zhuǎn)換為圖像點(diǎn)來計(jì)算一個(gè)視圖的錯(cuò)誤。然后我們計(jì)算出我們與轉(zhuǎn)換和角/圓搜索算法之間的絕對(duì)范數(shù)。為了找到平均誤差,我們計(jì)算出所有校準(zhǔn)圖像計(jì)算出的誤差的算術(shù)平均值。
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);
}

結(jié)果

這個(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)的棋盤模式:

使用OpenCV相機(jī)校準(zhǔn)

應(yīng)用失真去除后,我們得到:

使用OpenCV相機(jī)校準(zhǔn)

通過將輸入寬度設(shè)置為4并將高度設(shè)置為11 ,可以對(duì)該不對(duì)稱圓形圖案進(jìn)行相同的工作。此時(shí),我通過為輸入指定其ID(“1”),使用了實(shí)時(shí)相機(jī)饋送。以下是檢測(cè)到的模式應(yīng)該如何:

使用OpenCV相機(jī)校準(zhǔn)

在這兩種情況下,在指定的輸出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 :: initUndistortRectifyMapcv :: remap函數(shù)來消除失真,并享受無瑕疵的便宜和低質(zhì)量相機(jī)輸入。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)