OpenCV開發筆記(七十七):相機標定(二):透過棋盤標定計算相機內參矩陣矯正畸變攝像頭影像

红胖子(红模仿)發表於2024-03-11

前言

  透過相機圖片可以識別出棋盤角點了,這時候我們需要透過角點去計算相機內參矩陣,透過上篇得知畸變的原理,所以我們儘可能要全方位都能獲取標定圖片,全方位意思是提供的多張圖綜合起來基本覆蓋了相機所有的畫素,同時還要注意遠近和斜著
  本篇透過一張圖片來識別計算得到相機內參矩陣,並矯正相機畸形。

補充

  做專案一定要多張且基本覆蓋相機所有區域,要保證每一張擷取的圖片也要被識別,可以做成個軟體,識別出棋盤都在一個預先指定的區域內則截圖,然後下一個區域,實現半自動半人工化標定。

Demo

  在這裡插入圖片描述

  這裡只用了一張圖校準,所以可能內參矩陣經度不那麼高:
   在這裡插入圖片描述

一張圖校準的例項

   注意:這裡demo只使用了可識別的一張圖作為計算,可能沒覆蓋的區域則出現不可預期的影像問題。

步驟一:世界座標系初始化

   這裡是直接填充行列的座標,第三個是z座標直接設定為0,為視口處:

// 步驟八:角點對應的三維座標(一張圖一組)
std::vector<std::vector<cv::Point3f>> vectorObjectPoint;
std::vector<cv::Point3f> objectPoints;  // 三維世界座標系
for(int i = 0; i < chessboardRowCornerCount; i++)
{
    for(int j = 0; j < chessboardColCornerCount; j++)
    {
        objectPoints.push_back(cv::Point3f(j, i, 0));
    }
}
vectorObjectPoint.push_back(objectPoints);

步驟二:識別的角點放入列表

   多張圖放入多次,這裡只有一張圖:

// 步驟九:影像識別出來的角點(一張圖一組)
std::vector<std::vector<cv::Point2f>> vectorImagePoint;
vectorImagePoint.push_back(vectorPoint2fCorners);

步驟三:計算內參和畸變係數

   輸出的引數有點多,輸入的引數卻不多:

// 步驟十:計算內參和畸變係數
cv::Mat cameraMatrix;                   // 相機矩陣(接收輸出)
cv::Mat distCoeffs;                     // 畸變係數(接收輸出)
cv::Mat Rotate;                         // 旋轉量(接收輸出)
cv::Mat Translate;                      // 偏移量(接收輸出)
cv::calibrateCamera(vectorObjectPoint,
                  vectorImagePoint,
                  grayMat.size(),
                  cameraMatrix,
                  distCoeffs,
                  Rotate,
                  Translate);
std::cout << "cameraMatrix:" << std::endl;
std::cout << cameraMatrix << std::endl;

std::cout << "distCoeffs:" << std::endl;
std::cout << distCoeffs << std::endl;

std::cout << "Rotate:" << std::endl;
std::cout << Rotate << std::endl;

std::cout << "Translate:" << std::endl;
std::cout << Translate << std::endl;

步驟四:畸變函式校準

   這裡校準相對容易,所以難點在於標定校準,做專案肯定要自己寫一個標定軟體了,每次這麼手動檢視校準肯定不行的。

// 步驟十一:畸變影像校準
cv::Mat dstMat;
cv::undistort(srcMat, dstMat, cameraMatrix, distCoeffs);
cv::imshow("6", dstMat);

函式原型

calibrateCamera:相機標定求解函式

   OpenCV中的一個函式,用於相機標定。相機標定是估計相機內參(如焦距、主點座標等)和畸變係數的過程,這些引數對於後續的影像處理任務(如三維重建、目標跟蹤等)至關重要。

double calibrateCamera(InputArrayOfArrays objectPoints,  
                       InputArrayOfArrays imagePoints,  
                       Size imageSize,  
                       OutputArray cameraMatrix,  
                       OutputArray distCoeffs,  
                       OutputArray rvecs,  
                       OutputArray tvecs,  
                       int flags=0,  
                       TermCriteria criteria=TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6));

   引數說明:

  • objectPoints:世界座標系中的三維點。通常,這些點是透過在標定板上定義的一系列點來獲取的,這些點的座標是已知的。對於每個影像,它應該是一個 Nx3 的陣列(或陣列列表),其中 N 是點的數量,而 3 表示每個點的 (X, Y, Z) 座標。
  • imagePoints:影像座標系中的二維點,即對應於 objectPoints 中的三維點在影像中的投影。對於每個影像,它應該是一個 Nx2 的陣列(或陣列列表),其中 N 是點的數量,而 2 表示每個點的 (x, y) 座標。
  • imageSize:影像的大小,表示為 Size 型別的物件,包含影像的寬度和高度。
  • cameraMatrix:輸出引數,儲存 3x3 的相機內參矩陣。
  • distCoeffs:輸出引數,儲存畸變係數。通常有 5 個係數(k1, k2, p1, p2, k3)對於徑向和切向畸變,或 8 個係數(k1, k2, k3, k4, k5, k6, p1, p2)對於魚眼相機模型。
  • rvecs:輸出引數,對於每個影像,儲存旋轉向量的陣列。
  • tvecs:輸出引數,對於每個影像,儲存平移向量的陣列。
  • flags:不同標誌的組合,用於指定標定過程中使用的演算法。
    CV_CALIB_USE_INTRINSIC_GUESS:使用該引數時,將包含有效的fx,fy,cx,cy的估計值的內參矩陣cameraMatrix,作為初始值輸入,然後函式對其做進一步最佳化。如果不使用這個引數,用影像的中心點初始化光軸點座標(cx, cy),使用最小二乘估算出fx,fy(這種求法好像和張正友的論文不一樣,不知道為何要這樣處理)。注意,如果已知內部引數(內參矩陣和畸變係數),就不需要使用這個函式來估計外參,可以使用solvepnp()函式計算外引數矩陣。
    CV_CALIB_FIX_PRINCIPAL_POINT:在進行最佳化時會固定光軸點,光軸點將保持為影像的中心點。當CV_CALIB_USE_INTRINSIC_GUESS引數被設定,保持為輸入的值。
    CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只將fy作為可變數,進行最佳化計算。當 CV_CALIB_USE_INTRINSIC_GUESS沒有被設定,fx和fy的實際輸入值將會被忽略,只有fx/fy的比值被計算和使用。
    CV_CALIB_ZERO_TANGENT_DIST:切向畸變係數(P1,P2)被設定為零並保持為零。
    CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:對應的徑向畸變係數在最佳化中保持不變。如果設定了CV_CALIB_USE_INTRINSIC_GUESS引數,就從提供的畸變係數矩陣中得到。否則,設定為0。
    CV_CALIB_RATIONAL_MODEL(理想模型):啟用畸變k4,k5,k6三個畸變引數。使標定函式使用有理模型,返回8個係數。如果沒有設定,則只計算其它5個畸變引數。
    CALIB_THIN_PRISM_MODEL (薄稜鏡畸變模型):啟用畸變係數S1、S2、S3和S4。使標定函式使用薄稜柱模型並返回12個係數。如果不設定標誌,則函式計算並返回只有5個失真係數。
    CALIB_FIX_S1_S2_S3_S4 :最佳化過程中不改變薄稜鏡畸變係數S1、S2、S3、S4。如果cv_calib_use_intrinsic_guess設定,使用提供的畸變係數矩陣中的值。否則,設定為0。
    CALIB_TILTED_MODEL (傾斜模型):啟用畸變係數tauX and tauY。標定函式使用傾斜感測器模型並返回14個係數。如果不設定標誌,則函式計算並返回只有5個失真係數。
    CALIB_FIX_TAUX_TAUY :在最佳化過程中,傾斜感測器模型的係數不被改變。如果cv_calib_use_intrinsic_guess設定,從提供的畸變係數矩陣中得到。否則,設定為0。
  • criteria:迭代最佳化的終止條件。通常包含最大迭代次數和收斂的精度。

   這個函式返回一個雙精度浮點數,表示重投影誤差的估計值,即實際影像點與透過相機引數和畸變係數計算出的影像點之間的平均誤差。
   為了獲得準確的相機標定結果,通常需要多個檢視(即多張不同角度和姿態拍攝的標定板影像),**並確保標定板在不同影像中佔據足夠的視場。**此外,影像應該清晰,且標定板上的特徵點(如棋盤格的角點)應準確檢測。

initUndistortRectifyMap:計算畸變引數

   OpenCV中用於初始化用於影像去畸變和校正的對映表的函式。這個函式的目的是生成兩個對映,一個用於x座標,另一個用於y座標,它們可以被用於 remap函式來校正影像的畸變。

void initUndistortRectifyMap(InputArray cameraMatrix, 
                         InputArray distCoeffs, 
                         InputArray R, 
                         InputArray newCameraMatrix, 
                         Size size, 
                         int m1type, 
                         OutputArray map1, 
                         OutputArray map2)

   引數說明

  • cameraMatrix:相機的內參矩陣,一個3x3的浮點數矩陣。
  • distCoeffs:畸變係數,一個1x5或1x8的向量,包含徑向和切向畸變係數。
  • R:可選的旋轉矩陣,一個3x3的浮點數矩陣,表示從原相機座標系到新的相機座標系的旋轉。如果這個引數是空的,那麼newCameraMatrix必須是cameraMatrix。
  • newCameraMatrix:新的相機內參矩陣,一個3x3的浮點數矩陣。這個矩陣可以是原始相機矩陣,或者經過getOptimalNewCameraMatrix調整後的矩陣,以考慮影像的有效視場。
  • size:輸出對映的尺寸,表示為Size型別的物件,包含影像的寬度和高度。
  • m1type:輸出對映的型別,可以是CV_32FC1或CV_16SC2。
  • map1:輸出的第一個對映,用於x座標,可以被傳遞給remap函式。
  • map2:輸出的第二個對映,用於y座標,可以被傳遞給remap函式。

   這兩個對映map1和map2可以被傳遞給remap函式,以對影像進行去畸變和校正。
   如果有一個畸變的影像distortedImage和想要得到校正後的影像undistortedImage,可以這樣使用這兩個函式:

Mat map1,map2;
initUndistortRectifyMap(cameraMatrix, distCoeffs, R, newCameraMatrix, size, CV_32FC1, map1, map2);  
remap(distortedImage, undistortedImage, map1, map2, INTER_LINEAR);

   在這個例子中,INTER_LINEAR是插值方法的型別,用於remap函式。其他的插值方法,如INTER_NEAREST、INTER_CUBIC等也可以被使用,具體取決於應用需求。

Demo原始碼

void OpenCVManager::testCorrectingChessboard()
{
#define TestCorrectingChessboardUseCamera 0
#if !TestCorrectingChessboardUseCamera
    // 使用圖片
//    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/chessboard.png";
//    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/24.jpg";
//    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/27.png";
//    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/28.png";
    std::string srcFilePath = "D:/qtProject/openCVDemo/openCVDemo/modules/openCVManager/images/28.jpg";
    cv::Mat srcMat = cv::imread(srcFilePath);
#else
    // 使用攝像頭
    cv::VideoCapture capture;
    // 插入USB攝像頭預設為0
    if(!capture.open(0))
    {
        qDebug() << __FILE__ << __LINE__  << "Failed to open camera: 0";
    }else{
        qDebug() << __FILE__ << __LINE__  << "Succeed to open camera: 0";
    }
    while(true)
    {
        cv::Mat srcMat;
        capture >> srcMat;
#endif
    int chessboardColCornerCount = 6;
    int chessboardRowCornerCount = 9;
//    int chessboardColCornerCount = 7;
//    int chessboardRowCornerCount = 7;
    // 步驟一:讀取檔案
//    cv::imshow("1", srcMat);
//    cv::waitKey(0);
//     步驟二:縮放,太大了縮放下(可省略)
    cv::resize(srcMat, srcMat, cv::Size(srcMat.cols / 2, srcMat.rows / 2));
    cv::Mat srcMat2 = srcMat.clone();
    cv::Mat srcMat3 = srcMat.clone();
    cv::imshow("2", srcMat);
//    cv::waitKey(0);
    // 步驟三:灰度化
    cv::Mat grayMat;
    cv::cvtColor(srcMat, grayMat, cv::COLOR_BGR2GRAY);
    cv::imshow("3", grayMat);
//    cv::waitKey(0);
    // 步驟四:檢測角點
    std::vector<cv::Point2f> vectorPoint2fCorners;
    bool patternWasFound = false;
    patternWasFound = cv::findChessboardCorners(grayMat,
                                                cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                                                vectorPoint2fCorners,
                                                cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_FAST_CHECK | cv::CALIB_CB_NORMALIZE_IMAGE);
    /*
    enum { CALIB_CB_ADAPTIVE_THRESH = 1,    // 使用自適應閾值將影像轉化成二值影像
           CALIB_CB_NORMALIZE_IMAGE = 2,    // 歸一化影像灰度係數(用直方圖均衡化或者自適應閾值)
           CALIB_CB_FILTER_QUADS    = 4,    // 在輪廓提取階段,使用附加條件排除錯誤的假設
           CALIB_CB_FAST_CHECK      = 8     // 快速檢測
         };
    */
    cvui::printf(srcMat, 0, 0, 1.0, 0xFF0000, "found = %s", patternWasFound ? "true" : "false");
    cvui::printf(srcMat, 0, 24, 1.0, 0xFF0000, "count = %d", vectorPoint2fCorners.size());
    qDebug() << __FILE__ << __LINE__ << vectorPoint2fCorners.size();
    // 步驟五:繪製棋盤點
    cv::drawChessboardCorners(srcMat2,
                              cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                              vectorPoint2fCorners,
                              patternWasFound);
#if TestCorrectingChessboardUseCamera
    cv::imshow("0", srcMat);
    cv::imshow("4", srcMat2);
    if(!patternWasFound)
    {
        cv::imshow("5", srcMat3);
        cv::waitKey(1);
        continue;
    }
#endif
    // 步驟六:進一步提取亞畫素角點
    cv::TermCriteria criteria(CV_TERMCRIT_EPS | CV_TERMCRIT_ITER,   // 型別
                              30,                                   // 引數二: 最大次數
                              0.001);                               // 引數三:迭代終止閾值
    /*
    #define CV_TERMCRIT_ITER    1                   // 終止條件為: 達到最大迭代次數終止
    #define CV_TERMCRIT_NUMBER  CV_TERMCRIT_ITER    //
    #define CV_TERMCRIT_EPS     2                   // 終止條件為: 迭代到閾值終止
    */
    qDebug() << __FILE__ << __LINE__ << vectorPoint2fCorners.size();
    cv::cornerSubPix(grayMat,
                     vectorPoint2fCorners,
                     cv::Size(5, 5),
                     cv::Size(-1, -1),
                     criteria);
    // 步驟七:繪製棋盤點
    cv::drawChessboardCorners(srcMat3,
                              cv::Size(chessboardColCornerCount, chessboardRowCornerCount),
                              vectorPoint2fCorners,
                              patternWasFound);
    cv::imshow("5", srcMat3);
//    cv::waitKey(0);


    // 步驟八:角點對應的三維座標(一張圖一組)
    std::vector<std::vector<cv::Point3f>> vectorObjectPoint;
    std::vector<cv::Point3f> objectPoints;  // 三維世界座標系
    for(int i = 0; i < chessboardRowCornerCount; i++)
    {
        for(int j = 0; j < chessboardColCornerCount; j++)
        {
            objectPoints.push_back(cv::Point3f(j, i, 0));
        }
    }
    vectorObjectPoint.push_back(objectPoints);

    // 步驟九:影像識別出來的角點(一張圖一組)
    std::vector<std::vector<cv::Point2f>> vectorImagePoint;
    vectorImagePoint.push_back(vectorPoint2fCorners);

    // 步驟十:計算內參和畸變係數

    cv::Mat cameraMatrix;                   // 相機矩陣(接收輸出)
    cv::Mat distCoeffs;                     // 畸變係數(接收輸出)
    cv::Mat Rotate;                         // 旋轉量(接收輸出)
    cv::Mat Translate;                      // 偏移量(接收輸出)
    cv::calibrateCamera(vectorObjectPoint,
                        vectorImagePoint,
                        grayMat.size(),
                        cameraMatrix,
                        distCoeffs,
                        Rotate,
                        Translate);
    std::cout << "cameraMatrix:" << std::endl;
    std::cout << cameraMatrix << std::endl;

    std::cout << "distCoeffs:" << std::endl;
    std::cout << distCoeffs << std::endl;

    std::cout << "Rotate:" << std::endl;
    std::cout << Rotate << std::endl;

    std::cout << "Translate:" << std::endl;
    std::cout << Translate << std::endl;

    // 步驟十一:畸變影像校準
    cv::Mat dstMat;
    cv::undistort(srcMat, dstMat, cameraMatrix, distCoeffs);
    cv::imshow("6", dstMat);

#if TestCorrectingChessboardUseCamera
    cv::waitKey(1);
    }
//    cv::imshow(_windowTitle.toStdString(), dstMat);
#else
    cv::waitKey(0);
#endif
}

對應工程模板v1.68.0

  在這裡插入圖片描述

入坑

入坑一:無法識別影像

問題

  無法識別。
  在這裡插入圖片描述

原理

  要全部棋盤視野內,且可以識別,這個確實識別不了。

解決

  換圖重新來過(這是筆者隨便找的圖)。

入坑二:校準之後四角不準

問題

  四角明顯不對。
  在這裡插入圖片描述

原理

  這裡需要多張圖在能識別的情況下覆蓋所有區域。

解決

  先這樣,下次實際標定的時候再多張圖看是否還存在該問題。

相關文章