OpenCV 4.13.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 4.13.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
OpenCVによるカメラキャリブレーション

前のチュートリアル: 正方形チェスボードを用いたカメラキャリブレーション
次のチュートリアル: テクスチャ付き物体のリアルタイム姿勢推定

原著者Bernát Gábor
互換性OpenCV >= 4.0

カメラは長い間存在してきた。しかし、20世紀後半に安価なピンホールカメラが登場したことで、それらは日常生活でありふれた存在となった。残念ながら、この安さには代償が伴う。すなわち、大きな歪みである。幸い、これらは定数であり、キャリブレーションといくらかのリマッピングによって補正できる。さらに、キャリブレーションによって、カメラの自然な単位(ピクセル)と実世界の単位(例えばミリメートル)との関係を求めることもできる。

理論

歪みについて、OpenCV は半径方向(radial)成分と接線方向(tangential)成分を考慮する。半径方向成分については、次の式を用いる:

\[x_{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \\ y_{distorted} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)\]

したがって、\((x,y)\) 座標にある歪みのないピクセル点について、歪み画像上でのその位置は \((x_{distorted} y_{distorted})\) となる。半径方向の歪みの存在は、「樽型」または「魚眼」効果の形で現れる。

接線方向の歪みは、画像を撮影するレンズが結像面と完全に平行でないために生じる。これは次の式で表すことができる:

\[x_{distorted} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy]\]

したがって、5つの歪み引数があり、OpenCV では5列の1行行列として表される:

\[distortion\_coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3)\]

次に、単位変換のために以下の式を用いる:

\[\left [ \begin{matrix} x \\ y \\ w \end{matrix} \right ] = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] \left [ \begin{matrix} X \\ Y \\ Z \end{matrix} \right ]\]

ここで \(w\) の存在は、ホモグラフィ座標系の使用によって説明される(また \(w=Z\))。未知の引数は \(f_x\) と \(f_y\)(カメラの焦点距離)、および \((c_x, c_y)\)(ピクセル座標で表される光学中心)である。両方の軸に対して、与えられたアスペクト比 \(a\)(通常は1)を持つ共通の焦点距離が使われる場合、\(f_y=f_x*a\) となり、上記の式では単一の焦点距離 \(f\) を持つことになる。これら4つの引数を含む行列はカメラ行列と呼ばれる。歪み係数は使用するカメラの解像度によらず同じであるが、これらはキャリブレーション時の解像度から現在の解像度に合わせてスケーリングする必要がある。

これら2つの行列を求める処理がキャリブレーションである。これらの引数の計算は、基本的な幾何方程式を通じて行われる。使用する方程式は、選択したキャリブレーション対象物に依存する。現在、OpenCV はキャリブレーション用に3種類の対象物をサポートしている:

  • 古典的な白黒チェスボード
  • ChArUco ボードパターン
  • 対称な円パターン
  • 非対称な円パターン

基本的には、これらのパターンのスナップショットをカメラで撮影し、OpenCV にそれらを見つけさせる必要がある。見つかった各パターンが新しい方程式を生み出す。方程式を解くには、適切に定式化された方程式系を形成するために、少なくともあらかじめ決められた数のパターンのスナップショットが必要である。この数はチェスボードパターンでは多く、円パターンでは少ない。例えば、理論上はチェスボードパターンには少なくとも2枚のスナップショットが必要である。しかし、実際には入力画像にはかなりの量のノイズが含まれているため、良好な結果を得るには、おそらく異なる位置での入力パターンの良好なスナップショットが少なくとも10枚必要になるだろう。

目標

このサンプルアプリケーションは次のことを行う:

  • 歪み行列を求める
  • カメラ行列を求める
  • カメラ、ビデオ、画像ファイルリストから入力を受け取る
  • XML/YAML ファイルから設定を読み込む
  • 結果を XML/YAML ファイルに保存する
  • 再投影誤差を計算する

ソースコード

ソースコードは、OpenCV ソースライブラリの samples/cpp/tutorial_code/calib3d/camera_calibration/ フォルダでも見つけることができ、あるいは こちらからダウンロード できる。プログラムの使い方については、-h 引数を付けて実行する。このプログラムには必須の引数が1つある。設定ファイルの名前である。何も与えられない場合は、"default.xml" という名前のファイルを開こうとする。XML 形式のサンプル設定ファイルはこちら である。設定ファイルでは、入力としてカメラ、ビデオファイル、または画像リストを使用するかを選択できる。最後の選択肢を選ぶ場合は、使用する画像を列挙した設定ファイルを作成する必要がある。その例はこちら である。覚えておくべき重要な点は、画像は絶対パス、またはアプリケーションの作業ディレクトリからの相対パスを使って指定する必要があるということである。これらすべては、上記で言及したサンプルディレクトリで見つけることができる。

アプリケーションは、設定ファイルから設定を読み込むことから始まる。これは重要な部分ではあるが、このチュートリアルの主題であるカメラキャリブレーションとは関係がない。そのため、その部分のコードはここには掲載しないことにした。これを行う方法に関する技術的背景は、XML / YAML / JSON ファイルを使用したファイル入出力 のチュートリアルで見つけることができる。

説明

  1. 設定を読み込む

    Settings s;
    const string inputSettingsFile = parser.get<string>(0);
    FileStorage fs(inputSettingsFile, FileStorage::READ); // Read the settings
    if (!fs.isOpened())
    {
    cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;
    parser.printMessage();
    return -1;
    }
    fs["Settings"] >> s;
    fs.release(); // close Settings file

    このために、シンプルな OpenCV クラスの入力操作を使用した。ファイルを読み込んだ後、入力の妥当性をチェックする追加の後処理関数がある。すべての入力が良好な場合にのみ、goodInput 変数が true になる。

  2. 次の入力を取得し、失敗するか十分な数が集まったらキャリブレーションする

    この後、次の操作を行う大きなループがある。すなわち、画像リスト、カメラ、またはビデオファイルから次の画像を取得する。これが失敗するか、十分な数の画像が集まったら、キャリブレーション処理を実行する。画像の場合はループから抜け出し、そうでない場合は、残りのフレームが(オプションが設定されていれば)DETECTION モードから CALIBRATED モードに切り替わることで歪み補正される。

    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, grid_width,
    release_object))
    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, grid_width,
    release_object);
    break;
    }

    一部のカメラでは、入力画像を反転させる必要があるかもしれない。ここではそれも行う。

  3. 現在の入力からパターンを見つける

    上で述べた方程式の形成は、入力中の主要なパターンを見つけることを目的としている。チェスボードの場合はこれらは正方形のコーナーであり、円の場合は、まさに円そのものである。ChArUco ボードはチェスボードと等価だが、コーナーは ArUco マーカーによってマッチングされる。これらの位置が結果を形成し、それが pointBuf ベクトルに書き込まれる。

    vector<Point2f> pointBuf;
    bool found;
    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::CHARUCOBOARD:
    ch_detector.detectBoard( view, pointBuf, markerIds);
    found = pointBuf.size() == (size_t)((s.boardSize.height - 1)*(s.boardSize.width - 1));
    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;
    }

    使用する入力パターンの種類に応じて、cv::findChessboardCorners 関数、cv::findCirclesGrid 関数、または cv::aruco::CharucoDetector::detectBoard メソッドのいずれかを使用する。これらすべてに対して、現在の画像とボードのサイズを渡すと、パターンの位置が得られる。cv::findChessboardCornerscv::findCirclesGrid は、入力中にパターンが見つかったかどうかを示す真偽値の変数を返す(これが true である画像だけを考慮すればよい!)。CharucoDetector::detectBoard は部分的に見えているパターンを検出することができ、見えている内側コーナーの座標と id を返す。

    覚え書き
    ボードのサイズとマッチした点の数は、チェスボード、円グリッド、ChArUco で異なる。チェスボードに関連するすべてのアルゴリズムは、ボードの幅と高さとして内側コーナーの数を期待する。円グリッドのボードサイズは、グリッドの両次元方向の円の数そのものである。ChArUco ボードのサイズは正方形の数で定義されるが、検出結果は内側コーナーのリストであり、そのため両次元方向で1だけ小さくなる。

    次に、カメラの場合は再び、入力遅延時間が経過したときにのみカメラ画像を取得する。これは、ユーザーがチェスボードを動かしながら異なる画像を取得できるようにするために行われる。似た画像は似た方程式を生み、キャリブレーション段階での似た方程式は不良設定問題を形成するため、キャリブレーションは失敗する。正方形の画像では、コーナーの位置は近似的なものにすぎない。cv::cornerSubPix 関数を呼び出すことで、これを改善できる。(winSize は探索ウィンドウの一辺の長さを制御するために使われる。デフォルト値は11である。winSize はコマンドラインパラメータ --winSize=<number> で変更できる。)これにより、より良いキャリブレーション結果が得られる。この後、すべての方程式を1つのコンテナに集めるために、有効な入力の結果を imagePoints ベクトルに追加する。最後に、可視化によるフィードバックのために、cv::findChessboardCorners 関数を使用して、見つかった点を入力画像上に描画する。

    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(winSize,winSize),
    Size(-1,-1), TermCriteria( TermCriteria::EPS+TermCriteria::COUNT, 30, 0.0001 ));
    }
    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.
    if(s.calibrationPattern == Settings::CHARUCOBOARD)
    drawChessboardCorners( view, cv::Size(s.boardSize.width-1, s.boardSize.height-1), Mat(pointBuf), found );
    else
    drawChessboardCorners( view, s.boardSize, Mat(pointBuf), found );
    }
  4. 状態と結果をユーザーに表示し、加えてアプリケーションのコマンドライン制御を行う

    この部分は、画像上にテキスト出力を表示する。

    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.showUndistorted)
    msg = cv::format( "%d/%d Undist", (int)imagePoints.size(), s.nrFrames );
    else
    msg = cv::format( "%d/%d", (int)imagePoints.size(), s.nrFrames );
    }
    putText( view, msg, textOrigin, 1, 1, mode == CALIBRATED ? GREEN : RED);
    if( blinkOutput )
    bitwise_not(view, view);

    キャリブレーションを実行し、歪み係数を伴うカメラの行列が得られた場合、cv::undistort 関数を使用して画像を補正したいことがあるだろう:

    if( mode == CALIBRATED && s.showUndistorted )
    {
    Mat temp = view.clone();
    if (s.useFisheye)
    {
    Mat newCamMat;
    fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
    Matx33d::eye(), newCamMat, 1);
    cv::fisheye::undistortImage(temp, view, cameraMatrix, distCoeffs, newCamMat);
    }
    else
    undistort(temp, view, cameraMatrix, distCoeffs);
    }

    次に、画像を表示し、入力キーを待つ。それが u であれば歪み除去を切り替え、g であれば検出処理を再び開始し、そして最後に ESC キーであればアプリケーションを終了する:

    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.showUndistorted = !s.showUndistorted;
    if( s.inputCapture.isOpened() && key == 'g' )
    {
    mode = CAPTURING;
    imagePoints.clear();
    }
  5. 画像についても歪み除去を表示する

    画像リストを扱う場合、ループ内で歪みを除去することはできない。したがって、ループの後でこれを行う必要がある。これを機に、ここでは cv::undistort 関数を展開する。この関数は実際には、まず cv::initUndistortRectifyMap を呼び出して変換行列を求め、次に cv::remap 関数を使用して変換を実行する。キャリブレーションが成功した後はマップの計算を一度だけ行えばよいため、この展開された形式を使うことでアプリケーションを高速化できる:

    if( s.inputType == Settings::IMAGE_LIST && s.showUndistorted && !cameraMatrix.empty())
    {
    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
    {
    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;
    }
    }

キャリブレーションと保存

キャリブレーションはカメラごとに一度だけ行えばよいため、キャリブレーションが成功した後にそれを保存しておくのは理にかなっている。こうすれば、後でこれらの値をプログラムに読み込むだけで済む。このため、まずキャリブレーションを行い、成功した場合は、設定ファイルで指定した拡張子に応じて、結果を OpenCV スタイルの XML または YAML ファイルに保存する。

したがって、最初の関数ではこれら2つの処理を分割するだけである。多くのキャリブレーション変数を保存したいので、ここでこれらの変数を作成し、それらの両方をキャリブレーションおよび保存関数に渡す。繰り返しになるが、保存部分はキャリブレーションとほとんど共通点がないので示さない。どのように何を行うかを知るために、ソースファイルを調べてほしい:

bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,
vector<vector<Point2f> > imagePoints, float grid_width, bool release_object)
{
vector<Mat> rvecs, tvecs;
vector<float> reprojErrs;
double totalAvgErr = 0;
vector<Point3f> newObjPoints;
bool ok = runCalibration(s, imageSize, cameraMatrix, distCoeffs, imagePoints, rvecs, tvecs, reprojErrs,
totalAvgErr, newObjPoints, grid_width, release_object);
cout << (ok ? "Calibration succeeded" : "Calibration failed")
<< ". avg re projection error = " << totalAvgErr << endl;
if (ok)
saveCameraParams(s, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, imagePoints,
totalAvgErr, newObjPoints);
return ok;
}
@ READ
value, open the file for reading
Definition persistence.hpp:266
@ CALIB_USE_INTRINSIC_GUESS
Definition calib3d.hpp:4058
@ CALIB_FIX_K2
Definition calib3d.hpp:4063
@ CALIB_FIX_K4
Definition calib3d.hpp:4065
@ CALIB_FIX_K1
Definition calib3d.hpp:4062
@ CALIB_FIX_PRINCIPAL_POINT
Definition calib3d.hpp:4067
@ CALIB_FIX_K3
Definition calib3d.hpp:4064
#define INVALID
Definition multicalib.hpp:56
#define CV_32FC2
Definition interface.h:119
#define CV_16SC2
Definition interface.h:107

キャリブレーションは cv::calibrateCameraRO 関数の助けを借りて行う。これには以下の引数がある:

  • The object points. This is a vector of Point3f vector that for each input image describes how should the pattern look. If we have a planar pattern (like a chessboard) then we can simply set all Z coordinates to zero. This is a collection of the points where these important points are present. Because, we use a single pattern for all the input images we can calculate this just once and multiply it for all the other input views. We calculate the corner points with the calcBoardCornerPositions function as:
    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::CHARUCOBOARD:
    for (int i = 0; i < boardSize.height - 1; ++i) {
    for (int j = 0; j < boardSize.width - 1; ++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;
    }
    }
    And then multiply it as:
    vector<vector<Point3f> > objectPoints(1);
    calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);
    objectPoints[0][s.boardSize.width - 1].x = objectPoints[0][0].x + grid_width;
    newObjPoints = objectPoints[0];
    objectPoints.resize(imagePoints.size(),objectPoints[0]);
    覚え書き
    キャリブレーションボードが不正確、未計測、おおよそ平面の対象物である場合(市販のプリンタで紙に印刷したチェッカーボードパターンは最も便利なキャリブレーション対象物だが、それらのほとんどは十分に正確ではない)、[261] の手法を利用して、推定されるカメラ内部引数の精度を劇的に向上させることができる。この新しいキャリブレーション手法は、コマンドラインパラメータ -d=<number> が指定された場合に呼び出される。上記のコードスニペットでは、grid_width は実際には -d=<number> によって設定される値である。これは、パターングリッド点の左上 (0, 0, 0) と右上 (s.squareSize*(s.boardSize.width-1), 0, 0) のコーナー間の計測距離である。定規やノギスで正確に計測する必要がある。キャリブレーション後、newObjPoints はオブジェクト点の精密化された3D座標で更新される。
  • 画像点。これは Point2f ベクトルのベクトルであり、各入力画像について重要な点(チェスボードの場合はコーナー、円パターンの場合は円の中心)の座標を含む。これはすでに cv::findChessboardCorners または cv::findCirclesGrid 関数から集めてある。あとはそれを渡すだけでよい。
  • カメラ、ビデオファイル、または画像から取得した画像のサイズ。
  • 固定するオブジェクト点のインデックス。標準のキャリブレーション手法を要求するには -1 に設定する。新しいオブジェクト解放法を使用する場合は、キャリブレーションボードグリッドの右上コーナー点のインデックスに設定する。詳しい説明は cv::calibrateCameraRO を参照。
    int iFixedPoint = -1;
    if (release_object)
    iFixedPoint = s.boardSize.width - 1;
  • カメラ行列。固定アスペクト比オプションを使用した場合は \(f_x\) を設定する必要がある:
    cameraMatrix = Mat::eye(3, 3, CV_64F);
    if( !s.useFisheye && s.flag & CALIB_FIX_ASPECT_RATIO )
    cameraMatrix.at<double>(0,0) = s.aspectRatio;
  • 歪み係数の行列。ゼロで初期化する。
    distCoeffs = Mat::zeros(8, 1, CV_64F);
    #define CV_64F
    Definition interface.h:79
  • すべてのビューについて、この関数は(モデル座標空間で与えられた)オブジェクト点を(世界座標空間で与えられた)画像点へ変換する回転ベクトルと並進ベクトルを計算する。7番目と8番目の引数は、i番目のオブジェクト点をi番目の画像点へ変換するための回転ベクトルと並進ベクトルをi番目の位置に格納した、行列の出力ベクトルである。
  • 更新されたキャリブレーションパターン点の出力ベクトル。この引数は標準のキャリブレーション手法では無視される。
  • 最後の引数はフラグである。ここでは、焦点距離のアスペクト比を固定する、円周方向の歪みをゼロと仮定する、主点を固定する、といったオプションを指定する必要がある。ここでは、より高速なキャリブレーションを得るために CALIB_USE_LU を使用している。
    rms = calibrateCameraRO(objectPoints, imagePoints, imageSize, iFixedPoint,
    cameraMatrix, distCoeffs, rvecs, tvecs, newObjPoints,
    s.flag | CALIB_USE_LU);
  • この関数は平均再投影誤差を返す。この値は求めた引数の精度をよく見積もるものであり、できる限りゼロに近いほうがよい。内部パラメータ、歪み、回転、並進の各行列が与えられれば、まず cv::projectPoints を用いてオブジェクト点を画像点へ変換することで、1つのビューについての誤差を計算できる。次に、その変換で得た結果と、コーナー/円の検出アルゴリズムで得た結果との間の絶対ノルムを計算する。平均誤差を求めるには、すべてのキャリブレーション画像について計算した誤差の算術平均を計算する。
    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);
    }

結果

サイズが 9 X 6 のこのチェスボードパターンがあるとする。AXIS の IP カメラを使ってボードのスナップショットを数枚作成し、VID5 ディレクトリに保存した。これを作業ディレクトリの images/CameraCalibration フォルダ内に置き、どの画像を使うかを記述した以下の 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 を設定ファイルの入力として渡した。アプリケーションの実行中に検出されたチェスボードパターンは次のとおりである。

歪み除去を適用すると次の結果が得られる。

同じことが、入力の幅を 4、高さを 11 に設定することで、この非対称な円パターンでも機能する。今回は入力としてその ID("1")を指定し、ライブカメラ映像を使用した。検出されたパターンは次のように表示されるはずである。

いずれの場合も、指定した出力 XML/YAML ファイル内にカメラ行列と歪み係数の行列が見つかる。

<camera_matrix type_id="opencv-matrix">
<rows>3</rows>
<cols>3</cols>
<dt>d</dt>
<data>
6.5746697944293521e+002 0. 3.1950000000000000e+002 0.
6.5746697944293521e+002 2.3950000000000000e+002 0. 0. 1.</data></camera_matrix>
<distortion_coefficients type_id="opencv-matrix">
<rows>5</rows>
<cols>1</cols>
<dt>d</dt>
<data>
-4.1802327176423804e-001 5.0715244063187526e-001 0. 0.
-5.7843597214487474e-001</data></distortion_coefficients>

これらの値を定数としてプログラムに追加し、cv::initUndistortRectifyMap 関数と cv::remap 関数を呼び出して歪みを除去すれば、安価で低品質なカメラでも歪みのない入力を楽しめる。

これが実行されている様子は こちらの YouTube で見ることができる。