誇大タイトルです笑。 2019年最初の記事ということで気合い入れたら、結構長くなってしまいました。
この記事では、カメラのキャリブレーションでよく使われるOpenCVのcalibrateCamera
関数の原理や実用上のコツを解説します。
「とりあえず関数は呼べたけど、結果が合っているのかイマイチわからない」「全く見当違いな値が出てきた」というときの参考になればと思います。
※ 2020/08/26:3.2.項の初期解の求め方の部分で「内部パラメータ」と「外部パラメータ」を誤植していたのを修正しました
スポンサーリンク
もくじ
- 0. キャリブレーションアプリの例
- 1. cv::calibrateCamera関数とは
- 2. OpenCVのカメラモデル
- 3. calibrateCamera関数の内部実装
- 4. calibrateCamera関数の使い方
- 5. 結果の見方
- 6. 良い結果が出なかったときの試行錯誤
0. キャリブレーションアプリの例
【2019/06/01 追記】
チェッカーボード/ドットグリッドを使ったカメラキャリブレーションのためのアプリを書いてみました。
カメラ画像とグリッド検出結果を確認しながら、インタラクティブに操作できるようになっています。
言語はPythonです。
カメラアクセスにOpenCVのVideoCapture
を利用しているので、非対応のカメラを使う場合は、カメラ画像取得部分の書き直しが必要です。
1. cv::calibrateCamera関数とは
calibrateCamera
関数は、その名の通りカメラのキャリブレーション(較正)を行うための関数です。
カメラ画像から物体の位置や姿勢を認識するタスクは、古くからコンピュータビジョンの分野で研究されている話題です。 カメラは三次元の情報を二次元の画像に写し込むので、奥行方向の情報は欠けてしまうのですが、左右上下のどの方向に物体があるのかという情報は読み取ることができます。 この情報をうまく活用してやることで、自動運転のための物体認識やAR/MRで使うリアルタイムなCG合成など、様々なアプリケーションが実現できます。
こうした物体の位置検出をするためには、「カメラから見てこの方向にある物体はカメラ画像上のここに映り込む」という対応関係を事前に知っておく必要があります。
calibrateCamera
関数は、この対応関係を較正するために利用されます。
2. OpenCVのカメラモデル
カメラを較正するための準備として、まず物体がカメラに映り込む様子を数式にモデル化する必要があります。 このモデルは、カメラモデルと呼ばれます。
2.1. 定義
OpenCVでは、次のようなカメラモデルを採用しています(執筆時のバージョンは4.1.0)。
- カメラをピンホールカメラとして考える(ピントは、見たい範囲全体をまんべんなくボケずに撮影できるような位置に固定されていると仮定する)
- カメラの光学中心(ピンホールカメラの穴の位置・画角の基点)を原点とし、右をX、下をY、前をZとする右手座標系を考える
- カメラ座標系で見て
にあるオブジェクトは、以下の式で求められる画素
に映り込むものとする
式の詳細は次の節で説明するとして、まずは各パラメータの呼び名を確認します。
はカメラの内部パラメータ(intrinsic parameters)と呼ばれます。
特に
は歪みパラメータ(distortion parameters)と呼ばれます。
また、伝統的に
をカメラ行列(camera matrix)と呼びます(歪みを考慮しない場合の計算式を整理するとこの行列が出てくるため)。 上のモデルで計算する場合、実際にこの行列を使うことはないですが、呼び方として覚えておきましょう。
補足
魚眼レンズと呼ばれる広角レンズを搭載したカメラの場合、上のモデルでは画像の輪郭に近い部分の歪みは正確に表現できません。
広角カメラを使う場合は、cv::fisheye
という別のモジュールが用意されているので、そちらを利用するほうがよいです。
また、上の図で と定義していた画像平面をあえて傾かせる、ティルト機能付きカメラというものも存在します。
詳細は「シャインプルーフの原理」で検索してみてください。
calibrateCamera
関数はこうしたティルト機能付きカメラのキャリブレーションにも対応しており、ティルトの影響を歪みパラメータ として算出できます。
ただ、このあたりの式を書くと紙面が長くなるので今回は省略します。
2.2. 解説
それでは、先ほどのカメラモデルの式を順に紐解いていきましょう。
透視投影変換
まず、ピンホールカメラモデルに従って、透視投影(perspective projection)という遠近法を考慮した変換を行います。 これを簡単に可視化したのが先ほどの図です。
図中の の位置に画像平面があると仮定します。
すると、オブジェクトがある点
とカメラ原点を結んだときに交差する点
にオブジェクトが写り込む、という風に考えることができます。
この考え方に従い、三次元のオブジェクトを二次元の画像平面に写し込んでいるのが最初の2つの式です。
OpenGLの透視投影ではオクルージョンを考慮するために2×2×2の正規座標系に変換しますが、ここでは奥行き情報を捨ててしまって構わないのでシンプルな変換式となっています。
ビューポート変換
次に、 の画像平面上の二次元座標
と画像に切り出したときのピクセル座標
の対応関係を考えます。
平たく言えば、二次元平面の座標値をピクセル値に直す処理をするのです。
これは二次元から二次元への射影なので、Y軸が同じ方向を向いていると考えれば
という風に定式化できます。
実際の画像に写り込むのは、変換後の が0〜画像の幅・高さの範囲に収まる領域です。
ここで出てきた はスキュー(skew)と呼ばれるパラメータです。
この値が大きくなることは「四角形を撮影したとき平行四辺形に歪んだものが撮影される」という状態を意味するのですが、最近のカメラ素子はこの種の歪みをほとんど生じないため、OpenCVでは0で固定されています。
はカメラの画角と画像の解像度(サイズ)を反映したパラメータで、焦点距離と呼ばれます。
数式的には
f_x(pix) = レンズの焦点距離(m) × 画像の幅(pix) / 撮像素子の幅(m) f_y(pix) = レンズの焦点距離(m) × 画像の高さ(pix) / 撮像素子の高さ(m)
で表され、単位はピクセルです。 なぜこのパラメータを焦点距離と呼ぶのかについては以前の記事で解説しているので、興味があれば読んでみてください。
はカメラの光軸を通ってきた光が写り込むピクセルの座標です。
おおよそ画像の中心あたりの座標になります。
以上をまとめたのが最後の2つの式です。
各種画像歪みの考慮
さて、ここまでの式で理想的なピンホールカメラをモデル化することはできたのですが、現実のカメラはレンズ収差や組み付け精度が原因で画像に歪み(distortion)が生じます。
この歪みを考慮するため、画像平面上で理想的な座標 から歪んだ座標
への射影を考えます。
OpenCVでは、この射影を以下の式でモデル化しています。
式を見ての通り、歪みパラメータが全て0のときが歪みなし、絶対値が大きくなるほど歪みが大きくなるようなモデルです。
第一項は放射状歪み(radial distortion)、第二項・第三項は接線歪み(tangential distortion)、第四項・第五項は薄プリズム歪み(thin prism distortion)と呼ばれます。
ちょっと自信のない情報ですが、放射状歪みはレンズの特性上必ず生じるほか、組み付け精度にも影響を受けるらしいです。 接線歪みと薄プリズム歪みは、どちらもレンズと撮像素子が完全に並行でなかったり、光軸がズレていたりといった組み付け誤差が原因で生じる歪みだそうです。
試しに歪みパラメータを可視化するWebアプリを作ってみたので、こちらで歪みのイメージを掴んでもらえればと思います(Camera Distortion Simulator)。
以上を全てまとめると、先ほどのカメラモデルの式となります。
3. calibrateCamera関数の内部実装
3.1. 問題の定式化
カメラをどのようにモデル化するのかが決まったので、次はカメラパラメータをどのように求めていくかを考えます。
OpenCVでは、以下のZhangの手法を拡張したものが実装されています。
- Zhengyou Zhang. A flexible new technique for camera calibration. Pattern Analysis and Machine Intelligence, IEEE Transactions on, 22(11):1330–1334, 2000.
- 被引用数が5桁を数える大ヒット論文です
- 実は上で紹介した放射状歪みのモデル化もこの論文で提案されたものです
Zhangの手法の基本的な考え方は、回帰などで利用する最小二乗法による関数フィッティングと同じです。
すなわち、三次元座標 とピクセル座標
のペアをたくさん集めて、「
から計算されるピクセル座標」と「実際に計測されたピクセル座標
」の誤差(再投影誤差)がなるべく小さくなるようなパラメータを探します。
ちなみに、こうした再投影誤差の最小化によって知りたいパラメータを求める手法はBundle Adjustmentと呼ばれており、コンピュータビジョンの分野ではメジャーな計算手法の1つになっています。
カメラモデルに従ってピクセル座標を計算する関数を 、カメラパラメータをまとめて
として、Bundle Adjustmentを数式で表すと以下のようになります。
つまり座標 が既知であるマーカーを複数用意し、カメラで撮影したときのピクセル座標
を計算し、この最適化問題を解くことでカメラパラメータを求めることができます。
しかし、ここで問題になるのは、どうやってカメラ座標系における三次元座標 を計算するかという点です。
いちいちカメラからの距離を定規で測っていては手間がかかりますし、誤差も大きくなります。
そこでZhangの方法では、相対座標(モデル座標)が既知であるマーカー群を用意し、それをカメラの前で姿勢を変えながら何度か撮影する、という手法を取ります。 そして、最適化問題で求めるべきパラメータの中に「各フレームにおけるモデル座標とカメラ座標系の幾何関係(外部パラメータ : extrinsic parameters)」を加えてしまいます。
モデル座標系におけるマーカー座標を 、
番目のフレームにおけるモデル座標系とカメラ座標系の間の回転・並進成分を
として、数式で表すと以下のようになります。
1フレーム増やすごとにパラメータが6個(回転の3自由度と並進の3自由度)増えてしまいますが、マーカー1つのデータにつき2自由度の制約を加えられるので、各フレームで最低でも4つのマーカーを検出できればこの最適化問題を解くことができます。
カメラキャリブレーションでよく使うチェッカーボードは「モデル座標におけるマーカー座標が既知なオブジェクト」の代表例です。 必ずしもチェッカーボードである必要はなく、ドットパターンやグレイコードパターンを利用してもよいです。
また原理上、マーカーの数が多ければ1回だけの撮影で内部パラメータを求めることも可能です(精度は悪化しやすいですが…)。
3.2. 問題の解法
次に、先ほど導出した最適化問題をどのように解くのかを考えます。
上の式は非線形最適化問題となるので、一発で解を求める有効な方法は知られていません。 そこで、初期解を徐々にそれらしい解に近付けていく反復解法を利用します。
二乗和の最小化(Least-Squares Problems)はよく利用される最適化問題で、過去に効率的なアルゴリズムが提案されています。 OpenCVではその一つであるLevenberg-Marquardt法を利用しています。 LM法の詳細はこちらの記事で解説しているので、興味があれば読んでみてください。
ただし、LM法は非常に有効な手法なのですが、一般に数値計算の反復解法は局所最適解のみを探索するので、正しくないパラメータに収束することがあります。 正しいパラメータに収束させるためには、それなりに正解に近い初期解を与えてやる必要があります。
calibrateCamera
関数の内部でも、最初に初期解を推定してからLM法を解いています。
外部パラメータの初期解は、モデル座標から画素平面へのホモグラフィーを考えることで計算しています。
ただし、ホモグラフィーを計算するためにはチェッカーボードのようにマーカーが平面上に配置されていることが条件になります。
非平面に配置されたマーカーを使うときはこの方法が使えないので、calibrateCamera
関数を呼ぶ際に内部パラメータの初期解を与える必要があります。
この場合、外部パラメータの初期解はいったん内部パラメータを初期解で固定した上でLM法を解いて求められます。
4. calibrateCamera関数の使い方
いよいよ関数の使い方です。
OpenCV 4.1.0 現在、戻り値・引数リストは以下のようになっています。
C/C++版
double cv::calibrateCamera( InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints, Size imageSize, InputOutputArray cameraMatrix, InputOutputArray distCoeffs, OutputArrayOfArrays rvecs, OutputArrayOfArrays tvecs, OutputArray stdDeviationsIntrinsics, OutputArray stdDeviationsExtrinsics, OutputArray perViewErrors, int flags = 0, TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, DBL_EPSILON) )
Python版
retval, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera( objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs[, rvecs[, tvecs[, flags[, criteria ]]]]) retval, cameraMatrix, distCoeffs, rvecs, tvecs, stdDeviationsIntrinsics, stdDeviationsExtrinsics, perViewErrors = cv2.calibrateCameraExtended( objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs[, rvecs[, tvecs[, stdDeviationsIntrinsics[, stdDeviationsExtrinsics[, perViewErrors[, flags[, criteria ]]]]]]])
細かい話は公式ドキュメントを見ていただくことにして、ここでは主要な変数についてのみ解説していきます。
- C/C++版の戻り値・Python版の戻り値
retval
objectPoints
imagePoints
imageSize
- カメラの解像度(ピクセル単位)
の初期解を求めるときに使うだけなので
cameraMatrix
の初期解を与えるときは指定しなくてもよい
cameraMatrix
を含むカメラ行列
- 非平面に配置されたマーカーを使うときは初期解の指定が必須
- \begin{bmatrix} f _ x & 0 & c _ x \\ 0 & f _ y & c _ y \\ 0 & 0 & 1 \end{bmatrix}
distCoeffs
- 歪みパラメータのリスト
flags
をどう指定するかによって長さが変わる(デフォルトは5)
rvecs
tvecs
- モデル座標系の姿勢ごとに求めた、モデル座標系からカメラ座標系への回転ベクトルと並進ベクトル
- 回転ベクトルは
cv::Rodrigues()
で回転行列と相互変換できる - 回転行列
と並進ベクトル
を使うと、モデル座標系の座標
をカメラ座標系の座標
に変換できる
- 長さの単位は、モデル座標系におけるマーカー座標を記述したときと同じ単位
- 三次元座標の回転表現についてはこちらの過去記事にまとめています(回転ベクトル・回転行列・クォータニオン・オイラー角についてまとめてみた - かみのメモ)
criteria
- LM法の収束判定に使うパラメータ
- 明らかに反復回数が足りていないなどのケースを除いてデフォルトのままでよい
flags
- 列挙体は
cv::CALIB_*
の形で定義されている - 例:高次の放射状歪みを推定するとき
CALIB_RATIONAL_MODEL
- 例:すべての歪みパラメータを固定するとき
CALIB_ZERO_TANGENT_DIST | CALIB_FIX_K1 | CALIB_FIX_K2 | CALIB_FIX_K3
distCoeffs
に何も指定しないと0のまま固定される
- 例:カメラ行列の初期解をこちらから与えるとき
CALIB_USE_INTRINSIC_GUESS
cameraMatrix
に3×3行列を渡す
- 列挙体は
5. 結果の見方
さて、LM法が無事に収束すればカメラパラメータが推定できて「めでたしめでたし」なのですが、ちゃんとした値に収束しているのかが気になるところです。
ここからは、私がいつもやっているチェック方法を紹介します。 必ずしも正しい方法とは限りませんのでご了承ください。
5.1. キャリブレーション結果の例
参考までに、とあるカメラのキャリブレーション結果を添付しておきます。
撮影画像は5枚で、 で固定しています。
Image Size : (1024, 1280) RMS : 0.3162601179834329 Intrinsic parameters : [[3.45238543e+03 0.00000000e+00 5.87300567e+02] [0.00000000e+00 3.44991446e+03 5.21409899e+02] [0.00000000e+00 0.00000000e+00 1.00000000e+00]] Distortion parameters : [[-2.23845891e-01 -7.32573873e-01 -7.30350307e-04 -1.68303803e-03 0.00000000e+00]] Rotation vector / Translation vector 0 [[-0.14270636 -0.05149731 -1.55891318]] [[-3.85846485 2.3775021 48.44265457]] Rotation vector / Translation vector 1 [[0.65924161 0.87410419 1.48192946]] [[ 2.98064258 -3.67776322 39.63023961]] Rotation vector / Translation vector 2 [[ 0.37713801 -0.56964512 -1.53005519]] [[-4.8628964 2.10324681 39.70077357]] Rotation vector / Translation vector 3 [[-0.30036312 -0.22436363 -1.52070959]] [[-2.19763672 2.38379239 46.23177521]] Rotation vector / Translation vector 4 [[-0.13302378 0.47900357 0.03471266]] [[-0.27348193 -5.69902391 47.07153337]]
5.2. RMSを確認する
一番わかりやすいのはRMSです。 先ほども説明したように、RMSは最適化問題の最終的な誤差をピクセル単位で表現したものです。 これが数ピクセル以下ならとりあえずLM法自体は正常に収束していると言っていいです(ただし正しい解に収束したとは限らない)。
これがあまりに大きい値を取っているときは、LM法の反復回数が足りていないか、入力データにミスがあるか、撮影の際にモーションブラーなどのひどいノイズが乗ってしまっているかのいずれかだと思います。
また、RMSは画角やピクセルに対してスケールされるので、画素数が多くなるほどもしくは画角が狭くなるほど大きい値を取りやすいという点に注意してください。 異なるカメラセットアップのキャリブレーション結果をRMSで比較しても意味はありません。
5.3. カメラ行列を確認する
次にわかりやすいのは焦点距離 と中心座標
です。
が大体同じ値を取っていて、
が画像の中心座標に近ければOKです(光軸が斜めを向いているようなカメラを除く)。
これが大きくズレている場合は、データ数(撮影回数)が足りていないか、ノイズが乗ってしまっているかのどちらかの可能性が高いです。
5.4. 歪みパラメータを確認する
歪みパラメータの確認は難しいのですが、目安として絶対値が10を超えているときは疑ってかかったほうがよいでしょう。
とはいえ、 が負で
が正であるために互いの歪みが相殺されて最終的な見た目はそれっぽくなっているというケースもあります。
先ほども紹介しましたが、歪みパラメータを可視化するWebアプリを作ったので目安として使ってみてください(Camera Distortion Simulator)。
5.5. 回転・並進ベクトルを確認する
回転・並進ベクトルも検証が難しいですが、慣れてくると何となくそれらしい値になっているかを確認できるようになります。 わかりやすい部分としては、並進ベクトルのz成分がカメラからオブジェクトまでのおおよその距離を表しているので、それっぽい値になっているか確認してみましょう。 長さの単位は、モデル座標系におけるマーカー座標を記述したときと同じ単位になります。
6. 良い結果が出なかったときの試行錯誤
- チェッカーボードが曲がっていないか/正しいものを使っているか確認する
- チェッカーボードを使う場合、厳密に平面であるという条件がかなり重要になってきます。アクリル板など丈夫な板に貼り付けましょう。PC画面に表示させたものを使ってもいいです。
- また気付きにくい仕様ですが、OpenCVの
findChessboardCorners()
はチェッカーパターンの周りに白色の余白があることを前提としています。余白があるか確認しましょう。
- 画像がきれいに撮影できているか確認する
- 手ブレや蛍光灯の反射光の映り込みがあるとパターンをうまく検出できないことがあります。
- 焦点ボケのせいでチェッカーパターンがボケてしまっている場合は、カメラの絞り(F値)を絞ってみるか、ボケに強いドットパターンの使用を検討してみましょう。
- データ数(撮影枚数)を増やす
- Zhangの論文では5枚だけですが、OpenCVの実装では推定するパラメータが増えているので、最低でも10枚、余裕があれば15枚以上は撮影しておくのがよいと思います。
- 同じ姿勢ばかり撮影してもあまり意味がないので色々な姿勢を試しましょう。
- 初期解を与える
- 歪みパラメータを減らす
- 多くの歪みパラメータを使用する設定にしていると、うまく収束しないことがあります。一度思い切って使用する歪みパラメータを減らしてみましょう。
以上、calibrateCamera
関数周りの話をまとめました。