一次實踐:給自己的手機攝像頭進行相機標定

charlee44發表於2024-09-27

目錄
  • 1. 問題引入
  • 2. 準備工作
    • 2.1 標定場
    • 2.2 相機拍攝
  • 3. 基本原理
    • 3.1 成像原理
    • 3.2 畸變校正
  • 4. 標定解算
    • 4.1 程式碼實現
    • 4.2 詳細解析
      • 4.2.1 解算實現
      • 4.2.2 提取點位
    • 4.3 解算結果
  • 5. 問題補充

1. 問題引入

不得不說,現在的計算機視覺技術已經發展到足夠成熟的階段了,還記得筆者剛工作的時候,相機標定還是個很神秘的技術,只有少數專業人員能夠做,網上也找不到什麼相關的資料。但是現在相機標定已經是一個非常普遍的技術了,也有不少的資料的可以參考,因此筆者突發奇想,既然那些大部頭的相機可以標定,那麼我們使用的手機攝像頭一定也可以標定。因此,筆者就記錄一下給自己手機攝像頭的具體實踐,算是彌補下當年沒有學習到該技術的遺憾,畢竟要學習一項技術最好的辦法就是親自實踐一下。

2. 準備工作

2.1 標定場

筆者見過不少正規的標定場,有的標定場很大,有很多帶有標誌物的豎條,還帶有載動相機裝置的軌道。不過目前比較流行且成本最低的辦法就是使用棋盤格標定板了,也就是所謂的張正友標定法。

那麼棋盤格標定板哪裡來呢?列印到紙上倒是一個辦法,不過可能有兩個問題,一個是列印後每個格子的尺寸需要換算一下,由畫素換成米制單位,這可能不是一個整數;另一個就是得找一面牆來貼上去,要貼的光滑平整還是挺難的。因此筆者沒有選擇這個辦法,最後還是透過網上購物找的標定板。由於是給手機攝像頭感測器尺寸都不是太大,標定板也不用選擇太大,筆者最終選用的標定板尺寸如下所示:

圖1:標定板尺寸

每個格子是5毫米,一共12X9個格子,整體尺寸還是比較小巧的,大概就一個手掌心大小。材質是玻璃基板,成本大概是50元左右。這個尺寸筆者實際體驗還是有點偏小的,不過再大成本就上來了,建議有財力的同學可以適當選擇大一點。

2.2 相機拍攝

接下來就是用手機攝像頭對棋盤格標定板進行拍攝了。理論上進行標定解算只需要6組控制點就可以了,但是因為識別的控制點都是有誤差的,需要多組點位來進行求解以提升精度。只拍攝一張照片獲得的控制點也不太夠,通常還需要獲取多張照片上的控制點,避免區域性最優的問題,提高解算過程的可靠性。如果可以的話,要使用多個視角、多個不同距離的標定板照片,同時最好保證標定板覆蓋整個影像平面的不同區域,這樣可以更好地估計畸變和其他引數。

在這裡筆者拍攝了6張棋盤格標定板的圖片,分別是前、後、左、右、上、下6個不同的位置和視角,如下所示:

圖2:拍攝的棋盤格圖片

可以看到拍攝的標定板區域都太靠中間了,不過也是沒辦法,使用的標定板尺寸確實有點偏小。拍攝的時候一旦靠的很近,手機拍照程式就會自動切換成近景拍攝。筆者不太確定切換成近景拍攝之後會不會修改相機的引數,所以都沒有靠的很近。但是太遠了拍照又有點糊,只能使用目前這樣的效果。

現在很多手機拍照的功能會自動修正照片,比如濾鏡,廣角矯正等等,這些功能都儘量關了或者不使用。另外,拍照過程不要進行調焦,具體來說相機上會有0.6x、1x、2x、3x這樣的引數,這代表變焦倍率,使用原始倍率(1x)進行標定即可。自動對焦功能當然也要關閉,保持鏡頭和焦平面的位置不變。

還有個問題是保持手機不動移動標定板來拍攝照片,還是保持標定板不動移動手機來拍攝照片?應該來說,兩者原理上都可以實現,但是標定板不動,相機移動更常見一點,因為實現起來更見簡單。筆者就是將棋盤格標定板透過雙面膠粘在牆上實現的,也算是組成了一個成本最低的微型標定場了。

其實筆者也試過將標定板放在桌面上來拍攝,不過在室內拍攝很容易在照片上有影子,還是固定在牆上比較好一點。而且最好放採光比較好的牆面上,在白天日照充足的時候進行拍攝,以便獲得最好的拍攝效果。

3. 基本原理

3.1 成像原理

相機標定雖然解算的是內參,但是其實連外參也解算了,因為相機標定解算使用的相機成像原理,這個過程中內參和外參會一起參與解算。在不考慮畸變的情況下,相機的成像原理可用下式(1)來表示:

\[s \begin{bmatrix} u\\ v\\ 1\\ \end{bmatrix} = K \begin{bmatrix} R|t\\ \end{bmatrix} \begin{bmatrix} X_w\\ Y_w\\ Z_w\\ 1\\ \end{bmatrix} \tag{1} \]

在這個式子中:

  • \({\begin{bmatrix}X_w & Y_w & Z_w\\\end{bmatrix}}^T\)表示世界空間中的三維點,也稱為物方點。
  • \({\begin{bmatrix}u & v\\\end{bmatrix}}^T\)表示影像平面上的畫素座標,也稱為像點。
  • \(\begin{bmatrix}R|t\\\end{bmatrix}\)是相機的外參矩陣。具體來說,就是旋轉變換和平移變換的組合,\(R\)就是3X3的旋轉矩陣,\(t\)則是一個3列維向量。由於旋轉變換可以用尤拉角來表示,因而也可以表示成3維向量。3個旋轉量,3個平移量,這就是相機的6個外參的由來。
  • \(K\)是相機的內參矩陣,通常表示為下式(2):

\[K = \begin{bmatrix} f_x & 0 & c_x\\ 0 & f_y & c_y\\ 0 & 0 & 1\\ \end{bmatrix} \tag{2} \]

  • \(f_x\)\(f_y\)分別是水平方向和垂直方向的焦距,單位為畫素。
  • \(c_x\)\(c_y\)是像主點(即成像平面的光軸交點)座標,單位為畫素。
  • \(s\)是比例因子,這個引數是為了實現齊次座標的轉換,將其次三維座標需要轉為二維座標。

以筆者的見識來說,上述相機成像原理其實與其他學科的一些知識有類似的地方:

  1. 計算機圖形學。圖形渲染中的幾何變換,包含模型(model)變換、檢視(view)變換和投影(projection)變換 ,合起來就是通常所說的MVP矩陣。模型變換包括旋轉變換和平移變換,檢視變換又是模型變換的逆變換,對應的就是式(1)的外參矩陣\(\begin{bmatrix}R|t\\\end{bmatrix}\)。不過投影矩陣有所不同,式(1)的內參矩陣\(K\)是將點從相機座標系轉換為影像座標系,圖形渲染中的投影矩陣則是將點從將點從相機座標系轉換為裁剪座標系。

  2. 攝影測量學。在攝影測量學中,這一套成像原理的公式被總結為共線方程,除了表示的形式不同,最顯著的不同是內參只有三個:焦距和像主點二維座標。這個公式個人認為並不太直觀,但是比較容易進行平差計算。

如果有以上兩者經驗的讀者,可以對照著進行理解,雖然它們看起來有點差異,但是筆者確定它們的原理都是一樣的,都是基於空間的幾何變換,只不過是應對於不同情況有不同的描述。

3.2 畸變校正

以上成像原理沒有考慮到畸變的影響。為什麼會產生畸變呢?很簡單,相機鏡頭不是完美的平面光學系統,光線在傳輸時發生複雜的彎曲,這會導致影像中的直線在影像邊緣發生扭曲。常見的畸變有徑向畸變和切向畸變。

畸變校正看起來很玄乎,其實說穿了也非常簡單,我們只需要理解一點,畸變校採用的有理函式的模型。所謂有理函式的模型,就是將校正前的位置x與校正後的位置y使用一個高階多項式(形如\(y=ax^3+bx^2+cx+d\))來進行表示,沒有什麼物理上的原理,就是純採用數學方式進行擬合,最後得到了每個高階項的係數(a,b,c,d)。

鑑於畸變校正會增加對標定解算的複雜度,這裡就不進行進一步論述了。對於初學者來說,理解成像原理的公式(1)更為關鍵一點。

4. 標定解算

4.1 程式碼實現

使用上述介紹的基本原理就可以進行標定解算了,不過解算方法比較複雜,我們還是結合具體的實現來解釋,程式碼如下所示,這裡主要使用了OpenCV庫:

#include <filesystem>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <vector>

#ifdef _WIN32
#include <Windows.h>
#endif

using namespace cv;
using namespace std;

int main() {
#ifdef _WIN32
  SetConsoleOutputCP(65001);
#endif

  vector<std::filesystem::path> imgPaths = {
      "C:/Work/CalibrateCamera/Data/front.jpg",
      "C:/Work/CalibrateCamera/Data/left.jpg",
      "C:/Work/CalibrateCamera/Data/right.jpg",
      "C:/Work/CalibrateCamera/Data/up.jpg",
      "C:/Work/CalibrateCamera/Data/down.jpg",
      "C:/Work/CalibrateCamera/Data/back.jpg"};
  size_t imageNum = imgPaths.size();

  // 定義棋盤格尺寸 (內角點數)
  int boardWidth = 11;  // 列數
  int boardHeight = 8;  // 行數
  cv::Size boardSize(boardWidth, boardHeight);

  double cellSize = 0.005;

  Size imageSize(3072, 4096);  // 影像尺寸

  // 準備標定所需的物方點和像方點
  vector<vector<Point3f>> objectPoints(imageNum);  // 多張影像的3D物方點
  vector<vector<Point2f>> imagePoints(imageNum);   // 多張影像的2D像方點

  for (size_t ii = 0; ii < imageNum; ++ii) {
    // 載入棋盤格影像
    cv::Mat image = cv::imread(imgPaths[ii].string().c_str());
    if (image.empty()) {
      std::cerr << "Error: Could not load image!" << std::endl;
      return -1;
    }

    // 儲存角點座標
    std::vector<cv::Point2f> corners;

    // 轉換影像為灰度
    cv::Mat grayImage;
    cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);

    // 尋找棋盤格角點
    // cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE
    bool found = cv::findChessboardCorners(grayImage, boardSize, corners,
                                           cv::CALIB_CB_FAST_CHECK);

    // 如果找到角點,進行進一步處理
    if (found) {
      std::cout << "Chessboard corners found!" << std::endl;

      // 增加角點的精度
      cv::cornerSubPix(
          grayImage, corners, cv::Size(11, 11), cv::Size(-1, -1),
          cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER,
                           30, 0.001));

      // 繪製角點
      std::string cornerImgPath = imgPaths[ii].parent_path().generic_string() +
                                  "/corner/" + imgPaths[ii].stem().string() +
                                  "_corner" + imgPaths[ii].extension().string();
      cv::drawChessboardCorners(image, boardSize, corners, found);
      cv::imwrite(cornerImgPath.c_str(), image);

      cout << corners.size() << endl;
      imagePoints[ii].resize(corners.size());

      for (size_t ci = 0; ci < corners.size(); ++ci) {
        imagePoints[ii][ci] = corners[ci];
      }

      objectPoints[ii].resize(corners.size());
      for (int hi = 0; hi < boardHeight; ++hi) {
        for (int wi = 0; wi < boardWidth; ++wi) {
          int ci = hi * boardWidth + wi;
          objectPoints[ii][ci].x = cellSize * wi;
          objectPoints[ii][ci].y = cellSize * hi;
          objectPoints[ii][ci].z = 0;
        }
      }
    } else {
      std::cerr << "Chessboard corners not found!" << std::endl;
    }
  }

  // 內參矩陣和畸變係數
  Mat cameraMatrix = Mat::eye(3, 3, CV_64F);  // 初始化為單位矩陣
  Mat distCoeffs = Mat::zeros(8, 1, CV_64F);  // 初始化為零

  // 外參的旋轉和位移向量
  vector<Mat> rvecs, tvecs;

  // 執行標定
  double reprojectionError =
      calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix,
                      distCoeffs, rvecs, tvecs);

  cout << u8"重投影誤差:" << reprojectionError << endl;
  cout << u8"內參矩陣:" << cameraMatrix << endl;
  cout << u8"畸變係數:" << distCoeffs << endl;

  return 0;
}

4.2 詳細解析

4.2.1 解算實現

程式碼實現的步驟很簡單,就是透過函式findChessboardCorners提取棋盤格圖片的角點,將其傳入calibrateCamera函式中,就得到了最終的解算成果,也就是內參矩陣。這其中的關鍵就在於calibrateCamera這個函式,我們可以看一下它的函式原型:

CV_EXPORTS_W double calibrateCamera( InputArrayOfArrays objectPoints,
                                     InputArrayOfArrays imagePoints, Size imageSize,
                                     InputOutputArray cameraMatrix, InputOutputArray distCoeffs,
                                     OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs,
                                     int flags = 0, TermCriteria criteria = TermCriteria(
                                        TermCriteria::COUNT + TermCriteria::EPS, 30, DBL_EPSILON) );

其引數詳解如下:

  • objectPoints:3D空間中的物方點座標集合,也就是公式(1)中的\({\begin{bmatrix}X_w & Y_w & Z_w\\\end{bmatrix}}^T\)。由於是多張圖片的多組點的集合,所以它的型別實際是std::vector<std::vector<cv::Point3f>>
  • imagePoints:影像中的畫素座標集合,對應公式(1)中的\({\begin{bmatrix}u & v\\\end{bmatrix}}^T\),型別同樣應該也是雙重陣列std::vector<std::vector<cv::Point2f>>
  • imageSize:輸入影像的尺寸(寬度和高度),單位為畫素。
  • cameraMatrix:輸出的攝像機內參矩陣,也就是公式(1)中的\(K\),為3X3矩陣。
  • distCoeffs:輸出的攝像機的畸變係數,通常為1X5或1X8的向量,包含徑向和切向畸變係數。
  • rvecs:輸出的旋轉向量集合,可以轉換成公式(1)中的\(R\)。每個旋轉向量對應一個影像,所以型別是std::vector<cv::Mat>
  • tvecs:輸出的平移向量集合,對應公式(1)中的\(t\)。每個平移向量對應一個影像,型別也是std::vector<cv::Mat>
  • 返回值:標定的重投影誤差,用於衡量標定結果的精確度。誤差越小,標定結果越準確。

透過對calibrateCamera函式的解析,相信讀者就很容易明白為什麼筆者要先講公式(1)的成像原理。這個解算引數的輸入輸出都是根據公式(1)來的,不過另一個問題來了,輸入的物方點和像方點是怎麼來的呢?

4.2.2 提取點位

答案很簡單,就是棋盤格上的角點。棋盤格由黑白相間的格子組成,所以它的角點是很容易提取的;另外一方面,棋盤格也是規整的,只要每個格子的尺寸都是一樣,就很容易知道物方座標。理論上,只要對影像提取角點,然後剔除掉非棋盤的角點就可以作為相機標定的像點了。不過,OpenCV提供了更進一步的介面findChessboardCorners,直接輸入棋盤格的內角點個數,就可以自動檢測出像點。如下圖所示在一張圖片上筆者提取的像點:

圖3: 提取棋盤格的角點作為像點

正如上圖所示,findChessboardCorners提取的是內角點,例如12X9的棋盤格,提取的內角點是11X8個,並且結果是按照從左到右,從上往下進行排序的。為什麼要這麼排序呢?因為很容易幫我們算出物方點。在相機標定這個應用中,相機的外參是不重要的,因此我們可以就以棋盤格標定板的左上角作為世界座標系的原點,第1個點的座標是(0,0,0),第2個點的座標是(0.005,0,0),第3個點座標是(0.010,0,0)...第12個點座標是(0,0.005,0),第13個點座標是(0.005,0.005,0)...就這麼依次類推得到所有角點對應的世界空間座標系座標。

另外一點要提醒讀者的是,findChessboardCorners這裡我配置的是引數是cv::CALIB_CB_FAST_CHECK,是一種快速演算法,cv::CALIB_CB_ADAPTIVE_THRESH和cv::CALIB_CB_NORMALIZE_IMAGE會對影像作預處理,能夠增加提取棋盤格角點的穩健性。但是我實際使用發現程式卡住了,不知道是效率很低還是OpenCV的問題,就沒有使用這兩個選項。

4.3 解算結果

最終,筆者的結算結果如下所示:

重投影誤差:0.166339
內參矩陣:[2885.695162446343, 0, 1535.720945173723;
 0, 2885.371543143629, 2053.122840953737;
 0, 0, 1]
畸變係數:[0.181362004467736;
 -3.970106972775221;
 0.0005157812878172198;
 0.0004644406171824815;
 23.559069196518]

解算的結果重投影誤差是0.166339,表示每個物體點在重新投影到影像上時與實際檢測到的角點位置的誤差為0.166339畫素。通常來說,這樣的誤差已經算是非常小,表明標定結果較為精確。

不過筆者還考慮一個問題,誤差為0.166339畫素,那麼具體是多少米呢?以前做測繪軟體的時候,平差的結果也是以畫素為單位,總會有客戶對我發起靈魂拷問:那具體是多少米呢?這次筆者也關注了一下這個問題,個人認為在相機標定這樣的應用場景,確實無法直接使用物理單位表示精度,因為這個演算法的結果就在於重投影到影像上的畫素差為量度,這一點與相機外參的定向的誤差量度有所不同。

對照內參矩陣,可得解算的焦距是\(f_x=2885.695\)\(f_y=2885.372\),單位也是畫素。那麼這個焦距換算成物理單位是多少米呢?根據筆者查詢的資料顯示,焦距在畫素和毫米之間的轉換公式如下所示:

\[焦距(毫米)= \frac{焦距(畫素)×感測器尺寸(毫米)}{影像解析度(畫素)} \]

也就是說與相機感測器尺寸有關,不過關於感測器尺寸的描述有點蛋疼,比如網上顯示我手機攝像頭的感測器是1/1.49英寸,這通常表示感測器的對角線長度。可以根據對角線長度加上寬高比(例如4:3還是16:9)算出相機感測器的物理尺寸,進而知道具體物理單位的焦距值大小。不過感測器的對角線長度標稱值和真實物理尺寸之間,會因為行業慣例和歷史標準有所差異,所以算出來的也不一定正確,最好還是聯絡官方來確定。不過,標定出畫素單位的焦距已經足夠後續滿足後續的使用場景了,筆者這裡也就是尋根究底一下。

5. 問題補充

最後,補充一些沒搞定或者暫時沒理解的問題吧:

  1. 關於成像原理列出的公式(1)的內參矩陣部分,其實筆者也沒弄清楚為什麼將焦距分成X方向上的\(f_x\)和y方向上的\(f_y\),有些資料上的內參矩陣並不是這麼列的,《攝影測量學》教材上列出的共線方程更是隻有一個焦距值\(f\)
  2. 筆者記得似乎有個操作“相機重標定”,可以將使用固定焦距\(f\),調整像主點到影像中心,以及消除畸變的重投影,可以簡化後續的空間計算,使得計算更為便捷。時間關係就留待後續研究了。
  3. 本文筆者並沒有具體解釋解算的演算法原理,因為這不是一兩句話就能說清楚的,在測繪學中有個過程有個專門的名詞叫做平差;或者叫做狀態估計、最大似然估計、非線性最佳化等等,至少我們需要知道最小二乘法原理才能繼續論述這個,就留待後續的文章中進行討論吧。

列出一些文章以供參考:

  1. 相機標定:從入門到實戰
  2. 相機系列——相機標定簡述
  3. 相機標定之張正友標定法數學原理詳解
  4. 計算機視覺----相機標定

本文原始碼和資料地址

相關文章