.. _calibration: カメラキャリブレーション **************************** 目的 ======= このセクションでは * カメラの歪み(内部パラメータや外部パラメータなど)を学びます. * これらのパラメータの推定方法や画像の歪みを取り除く方法などを学びます. 基礎 ======== 最近の安いピンホールカメラは画像上に多くの歪みを生じさせてしまいます.主な歪みとして放射状歪み(radial distortion)と接線歪み(tangential distortion)が挙げられる. 放射状歪みは直線が曲線になるような歪みです.画像中心から離れれば離れるほど歪みは大きくなります.以下に示す画像を見てください.チェスボードのエッジに沿って赤い線を書きましたが,チェスボードの境界線が直線ではなく,さきほど描いた赤い線と厳密には一致していないのが分かるかと思います.直線として見えるはずのものが全て膨れてしまっています.詳細については `歪み(光学) (英語) `_ を参照してください. .. image:: images/calib_radial.jpg :alt: Radial Distortion :align: center 放射状歪みは以下のようにして補正できます: .. math:: x_{corrected} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \\ y_{corrected} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) 同様に,もう一つの歪みである接線歪みは,レンズと画像平面が完璧に平行になっていないことが原因で生じる歪みです.そのため,ある領域が期待しているよりも近くにあるように見えてしまいます.接線歪みの補正方法は以下のようになります: .. math:: x_{corrected} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{corrected} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy] まとめると,以下の式に示す5個のレンズ歪みパラメータを推定しなければいけません: .. math:: Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3) 更に,カメラの内部パラメータや外部パラメータといった情報も必要になります. **内部パラメータ(Intrinsic parameters)** とはカメラ固有のパラメータを指し,焦点距離 (:math:`f_x,f_y`),光学中心, (:math:`c_x, c_y`) などです.内部パラメータはカメラ行列とも呼ばれます.このパラメータはカメラ固有のものであるため,一度計算すればそれ以降保存した値を使い続けられます.内部パラメータは3x3の行列として以下のように表されます: .. math:: camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] 外部パラメータはある座標系における3次元点の座標を別の座標系での座標に変換するための回転と並進のパラメータを指します. ステレオアプリケーションを作るためには,まず初めにこれらの歪みを解消(補正)する必要があります.これらのパラメータを推定するためにはチェスボードのように,(歪みが無かったとしたら)画像中でどのように見えるか想像できるパターンを何枚か撮影します.そのような特殊パターン上の制御点(チェスボードでは四角形の角)を見つけます.制御点の実空間中での位置はあらかじめ計測でき,画像上の位置はコーナー検出などによって検出できます.これらのデータを基に,歪みパラメータを推定するために幾つかの数学的な問題を解きます.これがカメラキャリブレーションの概要になります.良い結果を得るには,少なくとも10枚程度の画像が必要になります. 実装(コード) ================ 上述したように,カメラキャリブレーションを(精度よく)行うには少なくとも10枚程度の画像が必要です.OpenCVはチェスボードを撮影した画像をデフォルトで用意しているので(OpenCVをインストールしたフォルダの ``samples/cpp/left01.jpg -- left14.jpg`` です), このサンプル画像を使いましょう.理解のために,今は1枚の画像を考えましょう.カメラキャリブレーションに必要な重要なデータは3次元空間中での点の集合と,各点に対応する画像中での2次元点の位置です.2次元点は画像中から容易に検出できます(チェスボード上の二つの黒い四角形の交点を指します). では,3次元空間中での3次元点の位置はどうすればいいでしょうか?これらの画像は,固定してあるカメラの前でチェスボードの位置や姿勢を色々と変えて撮影された画像です.そのため,我々が知る必要がある情報は3次元点の位置 :math:`(X,Y,Z)` になります.これらの画像を,XY平面上に固定されたチェスボード(すなわち全ての点に対してZ=0)をカメラの位置・姿勢を変えて撮影した画像だとみなして考えてみましょう.このように考えると,知らなければいけない3次元点の位置は実際にはXとYの位置さえわかれば良いことになります.チェスボードは白黒の格子パターンが交互に並んでいるパターンなので,XとYの位置は(0,0), (1,0), (2,0), ... と簡単に計算できます.この時,格子パターンのサイズが結果に含まれますが,今我々が使用しているサンプル画像の場合そのサイズは30mmだとしましょう.そうすると,制御点の3次元点位置のX,Y座標は (0,0),(30,0),(60,0),..., となり,結果を㎜単位で取得する事が出来ます(In this case, we don't know square size since we didn't take those images, so we pass in terms of square size). 3次元点は **object points** 2次元画像上の点は **image points** と呼ばれます. 設定 --------- 画像中からチェスボード上の格子点を検出するには **cv2.findChessboardCorners()** 関数を使います.どのようなパターン(例えば8x8の格子とか7x5の格子とか)を見つけたいか指定する必要があります.この例では7x6の格子パターンです.(普通,チェスボードは8x8の四角形,7x7の制御点を持ちます).この関数は二つの出力を返します.一つ目の出力は二値のフラグで,指定したパターンが画像中から検出されればTrueとなります.二つ目の出力は検出された制御点の座標になります.検出された制御点は,画像中の左から右,上から下にソートされています. .. seealso:: この関数は与えられた全ての画像に対して検出したいパターンを検出できるとは限りません.そのため,カメラを起動させたら各フレームに対してパターンが検出できるか確認すると良いでしょう.パターンを見つけられたら検出した制御点の情報をリストに登録します.また,次のフレームを読み込む前に間隔を空けるとチェスボードの位置や向きを調整できます.この処理を十分な枚数の画像を取得できるまで繰り返します.このサンプル画像でさえ14枚の画像の内,何枚が良い画像なのか確証はありません.そのため,全画像を入力し,その中で良いものだけを使います. .. seealso:: チェスボードではなく格子状に並んだ円のパターンを使う事も出来ます.その場合は, **cv2.findCirclesGrid()** 関数を使ってパターンを検出します.円パターンを使うと,より少ない画像枚数でも十分な精度が得られると言われています. コーナー検出をした後に **cv2.cornerSubPix()** 関数を使うと,検出精度を向上できます.また, **cv2.drawChessboardCorners()** 関数を使えば,検出した制御点の位置を画像上に描画できます.上記の全てを実装すると,コードは以下のようになります: :: import numpy as np import cv2 import glob # termination criteria criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0) objp = np.zeros((6*7,3), np.float32) objp[:,:2] = np.mgrid[0:7,0:6].T.reshape(-1,2) # Arrays to store object points and image points from all the images. objpoints = [] # 3d point in real world space imgpoints = [] # 2d points in image plane. images = glob.glob('*.jpg') for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) # Find the chess board corners ret, corners = cv2.findChessboardCorners(gray, (7,6),None) # If found, add object points, image points (after refining them) if ret == True: objpoints.append(objp) corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria) imgpoints.append(corners2) # Draw and display the corners img = cv2.drawChessboardCorners(img, (7,6), corners2,ret) cv2.imshow('img',img) cv2.waitKey(500) cv2.destroyAllWindows() サンプル画像の1枚に検出した制御点を描画した結果が,以下になります: .. image:: images/calib_pattern.jpg :alt: Calibration Pattern :align: center キャリブレーション(Calibration) --------------------------------- 制御点について,3次元空間中での位置(object points)と2次元画像上での位置(image points)が分かったので,キャリブレーションができるようになりました.関数は **cv2.calibrateCamera()** を使います.返戻値はカメラ行列(内部パラメータ),レンズ歪みパラメータ,回転・並進ベクトルなどです. :: ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1],None,None) 歪み補正(Undistortion) ------------------------------ キャリブレーションをすれば,画像中からレンズ歪みの影響を取り除くことができます.OpenCvは歪み補正のために二つの関数を用意しています.それぞれの関数を紹介する前に, **cv2.getOptimalNewCameraMatrix()** 関数を使ってカメラ行列(内部パラメータ)を改善します.スケーリングパラメータ ``alpha=0`` であれば, この関数は期待しない画素の数を最小にする補正画像を返します.画像の角にある画素まで消してしまいます.一方で, ``alpha=1`` を与えると黒い画素が外挿されます.更に結果を切り出すためのROIも返します. それでは新しい画像(今回は ``left12.jpg`` です.このチュートリアルでは最初の画像です.)を使いましょう. :: img = cv2.imread('left12.jpg') h, w = img.shape[:2] newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h)) 1. **cv2.undistort()** 関数を使う ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 最短で歪み補正を行う方法です.この関数を呼び,結果の切り出しには関数が出力するROIを使います. :: # undistort dst = cv2.undistort(img, mtx, dist, None, newcameramtx) # crop the image x,y,w,h = roi dst = dst[y:y+h, x:x+w] cv2.imwrite('calibresult.png',dst) 2. **remapping** 関数を使う ^^^^^^^^^^^^^^^^^^^^^^^^^^^ **cv2.undistort()** 関数に比べ,手間のかかる方法です.まず初めに,歪んだ映像を補正するためのマッピング関数を用意し,次にremappingによって補正します. :: # undistort mapx,mapy = cv2.initUndistortRectifyMap(mtx,dist,None,newcameramtx,(w,h),5) dst = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR) # crop the image x,y,w,h = roi dst = dst[y:y+h, x:x+w] cv2.imwrite('calibresult.png',dst) どちらの方法を使っても同じ結果が返ってきます.以下の結果を見比べてください : .. image:: images/calib_result.jpg :alt: Calibration Result :align: center 結果を見ると,歪み補正によって全てのエッジが直線になっていることが分かります. Calibrationの結果を今後も再利用したければ,Numpyの関数(np.savez, np.savetxt 等)を使ってカメラ行列とレンズ歪みパラメータを保存できます. 再投映誤差(Re-projection Error) ======================================== 再投映誤差はキャリブレーションによって推定したパラメータを評価する指標の一つです.この評価値は0に近ければ近いほど良いことを意味します.カメラの内部パラメータ,レンズ歪みパラメータ,外部パラメータを与えると,まず初めに制御点の3次元座標(object point)を **cv2.projectPoints()** 関数を使って画像空間に写像(再投映)します.次に,画像上で検出した点の座標と再投映した点の座標の絶対ノルムを計算します.誤差の平均を出すにはキャリブレーション用の全画像の誤差の算術平均を計算します. :: mean_error = 0 for i in xrange(len(objpoints)): imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2) tot_error += error print "total error: ", mean_error/len(objpoints) 補足資料 ====================== 課題 ============ #. 円形パターンを使ったカメラキャリブレーションを試してみてください.