前のチュートリアル: 正方形チェスボードを用いたカメラキャリブレーション 次のチュートリアル: テクスチャ付き物体のリアルタイム姿勢推定
原著者 Bernát Gábor
互換性 OpenCV >= 4.0
カメラは非常に古くから存在している。しかし、20世紀後半に安価な ピンホール カメラが登場したことで、日常生活でありふれた存在になった。残念ながら、この低コストには引き換えがある。すなわち、無視できない歪みである。幸い、これらは定数であり、キャリブレーションと少々のリマッピングによって補正できる。さらにキャリブレーションによって、カメラの自然な単位(ピクセル)と実世界の単位(例えばミリメートル)の関係も求めることができる。
理論
歪みについて、OpenCV は半径方向と接線方向の両方の要因を考慮する。半径方向の要因には、次の式を用いる。
\[r^2 = x^2 + y^2\]
\[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 形式のサンプル設定ファイル である。設定ファイルでは、入力としてカメラ、ビデオファイル、または画像リストを選べる。最後のものを選んだ場合は、使用する画像を列挙した設定ファイルを作成する必要がある。こちらがその例 である。覚えておくべき重要な点は、画像は絶対パスか、アプリケーションの作業ディレクトリからの相対パスで指定する必要があるということである。これらすべては上記の samples ディレクトリにある。
アプリケーションは、設定ファイルから設定を読み込むことから始まる。これは重要な部分ではあるが、このチュートリアルの主題であるカメラキャリブレーション とは関係がない。そのため、その部分のコードはここには掲載しないことにした。これを行う方法に関する技術的背景は、XML / YAML / JSON ファイルを使用したファイル入出力 のチュートリアルで見つけることができる。
解説
設定を読み込む
Settings s;
const string inputSettingsFile = parser.get<string >(0);
if (!fs.isOpened())
{
cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;
parser.printMessage();
return -1;
}
fs["Settings" ] >> s;
fs.release();
このために、シンプルな OpenCV クラスの入力操作を使用した。ファイルを読み込んだ後、入力の妥当性をチェックする追加の後処理関数がある。すべての入力が良好な場合にのみ、goodInput 変数が true になる。
次の入力を取得し、失敗するか十分な数が集まったらキャリブレーションする
この後、次の操作を行う大きなループがある。すなわち、画像リスト、カメラ、またはビデオファイルから次の画像を取得する。これが失敗するか、十分な数の画像が集まったら、キャリブレーション処理を実行する。画像の場合はループから抜け出し、そうでない場合は、残りのフレームが(オプションが設定されていれば)DETECTION モードから CALIBRATED モードに切り替わることで歪み補正される。
for (;;)
{
bool blinkOutput = false ;
view = s.nextImage();
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 ( mode != CALIBRATED && !imagePoints.empty() )
runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints, grid_width,
release_object);
break ;
}
一部のカメラでは、入力画像を反転させる必要があるかもしれない。ここではそれも行う。
現在の入力からパターンを見つける
上で述べた方程式の形成は、入力中の主要なパターンを見つけることを目的としている。チェスボードの場合はこれらは正方形のコーナーであり、円の場合は、まさに円そのものである。ChArUco ボードはチェスボードと等価だが、コーナーは ArUco マーカーによってマッチングされる。これらの位置が結果を形成し、それが pointBuf ベクトルに書き込まれる。
vector<Point2f> pointBuf;
bool found;
if (!s.useFisheye) {
}
switch ( s.calibrationPattern )
{
case Settings::CHESSBOARD:
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:
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::findChessboardCorners と cv::findCirclesGrid は、入力中にパターンが見つかったかどうかを示すブール変数を返す(この値が true である画像のみを考慮すればよい)。CharucoDetector::detectBoard は部分的にしか見えないパターンも検出でき、見える内側コーナーの座標と ID を返す。
覚え書き ボードのサイズとマッチした点の数は、チェスボード、円グリッド、ChArUco で異なる。チェスボードに関連するすべてのアルゴリズムは、ボードの幅と高さとして内側コーナーの数を期待する。円グリッドのボードサイズは、グリッドの両次元方向の円の数そのものである。ChArUco ボードのサイズは正方形の数で定義されるが、検出結果は内側コーナーのリストであり、そのため両次元方向で1だけ小さくなる。
ライブカメラの場合、入力の遅延時間が経過したときにのみ画像を取り込む。これは、ユーザーがチェスボードを動かして異なる画像を取得できるようにするためである。似た画像は似た方程式を生み、キャリブレーションのステップで似た方程式は不良設定問題を構成するため、キャリブレーションが失敗してしまう。正方形の画像ではコーナーの位置は近似的なものでしかない。cv::cornerSubPix 関数を呼ぶことでこれを改善できる。(winSize は探索ウィンドウの一辺の長さを制御するために使われる。デフォルト値は 11 である。winSize はコマンドラインパラメータ --winSize=<number> で変更できる。)これによりキャリブレーション結果が良くなる。この後、有効な入力結果を imagePoints ベクトルに追加して、すべての方程式を1つのコンテナにまとめる。最後に、可視化のフィードバックを目的として、cv::findChessboardCorners 関数を使って入力画像上に見つかった点を描画する。
if (found)
{
if ( s.calibrationPattern == Settings::CHESSBOARD)
{
cvtColor (view, viewGray, COLOR_BGR2GRAY);
Size (-1,-1),
TermCriteria ( TermCriteria::EPS+TermCriteria::COUNT, 30, 0.0001 ));
}
if ( mode == CAPTURING &&
(!s.inputCapture.isOpened() || clock() - prevTimestamp > s.delay*1e-3*CLOCKS_PER_SEC) )
{
imagePoints.push_back(pointBuf);
prevTimestamp = clock();
blinkOutput = s.inputCapture.isOpened();
}
if (s.calibrationPattern == Settings::CHARUCOBOARD)
else
}
状態と結果をユーザーに表示し、加えてアプリケーションのコマンドライン制御を行う
この部分は、画像上にテキスト出力を表示する。
string msg = (mode == CAPTURING) ? "100/100" :
mode == CALIBRATED ? "Calibrated" : "Press 'g' to start" ;
int baseLine = 0;
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 )
キャリブレーションを実行してカメラ行列と歪み係数が得られたら、cv::undistort 関数を使って画像を補正できる。
if ( mode == CALIBRATED && s.showUndistorted )
{
if (s.useFisheye)
{
fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
Matx33d::eye(), newCamMat, 1);
}
else
undistort (temp, view, cameraMatrix, distCoeffs);
}
次に、画像を表示し、入力キーを待つ。それが u であれば歪み除去を切り替え、g であれば検出処理を再び開始し、そして最後に ESC キーであればアプリケーションを終了する:
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();
}
画像についても歪み除去を表示する
画像リストを扱う場合、ループ内で歪みを除去することはできない。したがって、これはループの後で行う必要がある。これを機に、ここでは cv::undistort 関数を展開する。これは実際には、まず cv::initUndistortRectifyMap を呼んで変換行列を求め、次に cv::remap 関数を使って変換を実行する。キャリブレーションが成功すれば、マップの計算は一度だけ行えばよいので、この展開した形式を使うことでアプリケーションを高速化できる。
if ( s.inputType == Settings::IMAGE_LIST && s.showUndistorted && !cameraMatrix.
empty ())
{
Mat view, rview, map1, map2;
if (s.useFisheye)
{
fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
Matx33d::eye(), newCamMat, 1);
fisheye::initUndistortRectifyMap(cameraMatrix, distCoeffs, Matx33d::eye(), newCamMat, imageSize,
}
else
{
cameraMatrix, distCoeffs,
Mat (),
}
for (size_t i = 0; i < s.imageList.size(); i++ )
{
view =
imread (s.imageList[i], IMREAD_COLOR);
continue ;
remap (view, rview, map1, map2, INTER_LINEAR);
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
#define INVALID
Definition multicalib.hpp:56
#define CV_32FC2
Definition interface.h:108
#define CV_16SC2
Definition interface.h:96
キャリブレーションは 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 )
{
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]);
覚え書き キャリブレーションボードが不正確で、未計測で、おおよそ平面のターゲットである場合(市販のプリンタを使って紙に印刷したチェッカーボードパターンは最も手軽なキャリブレーションターゲットだが、そのほとんどは十分な精度を持たない)、[264] の手法を利用して、推定されるカメラ内部パラメータの精度を劇的に向上させることができる。この新しいキャリブレーション手法は、コマンドラインパラメータ -d=<number> が与えられた場合に呼び出される。上記のコードスニペットでは、grid_width は実際には -d=<number> で設定される値である。これは、パターングリッド点の左上 (0, 0, 0) と右上 (s.squareSize*(s.boardSize.width-1), 0, 0) のコーナー間の計測距離である。定規やノギスで正確に計測する必要がある。キャリブレーションの後、newObjPoints は物体点の洗練された3次元座標で更新される。
画像点。これは Point2f ベクトルのベクトルであり、各入力画像について重要な点(チェスボードではコーナー、円パターンでは円の中心)の座標を保持する。これらはすでに cv::findChessboardCorners または cv::findCirclesGrid 関数から収集している。それをそのまま渡すだけでよい。
カメラ、ビデオファイル、または画像から取得した画像のサイズ。
固定する物体点のインデックス。標準のキャリブレーション手法を要求するには -1 に設定する。新しい object-releasing 手法を使う場合は、キャリブレーションボードグリッドの右上コーナー点のインデックスに設定する。詳しい説明は 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:60
すべてのビューについて、この関数は(モデル座標空間で与えられた)オブジェクト点を(世界座標空間で与えられた)画像点へ変換する回転ベクトルと並進ベクトルを計算する。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 で見ることができる。
VIDEO