カメラキャリブレーション (Camera Calibration, Camera Resectioning) を行うと、レンズの歪みを表現するパラメータや、カメラのワールド座標系での位置姿勢を推定できます。
チェスボードのようなキャリブレーション専用のボードが利用されます。
キャリブレーションで得られたパラメータを用いると、例えば歪みを補正することができます。
カメラキャリブレーションのおおまかな流れは以下のようになります。
キャリブレーションで利用するのは距離センサで取得した情報ではなく、カメラで取得した平面画像です。カメラの位置を固定したまま、キャリブレーションボードを様々な位置に移動させて画像を撮影します。
画像処理を行い、キャリブレーションボードの各点の、平面画像内での座標 $(u, v)$ を取得します。
画像処理で取得したキャリブレーションボードの各点の座標 $(u, v)$ と、ある座標系におけるキャリブレーションボードの各点の座標 $(x, y, z)$ は既知です。例えばローカル座標系におけるキャリブレーションボードの各点の座標は (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
のようになります。
ローカル座標系からカメラの視点座標系への変換行列 T
と、カメラの視点座標系から平面画像に投影するための、カメラモデルを表現する行列 K
を用いると以下のような連立方程式が立てられます。キャリブレーションにおいて、K
と T
をそれぞれ内部パラメータ (Intrinsics) および外部パラメータ (Extrinsics) とよびます。
$$s \begin{pmatrix} u \\ v \\ 1 \end{pmatrix} = K T \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} $$
$$T = \begin{pmatrix} r_{11} & r_{12} & r_{13} & t_1 \\ r_{21} & r_{22} & r_{23} & t_2 \\ r_{31} & r_{32} & r_{33} & t_3 \end{pmatrix} $$
一般性を失うことなく、$z$ 座標が 0 となるような、キャリブレーションボードのローカル座標系を考えることができます。その場合は、外部パラメータの行列は少しだけ簡単になります。
$$s \begin{pmatrix} u \\ v \\ 1 \end{pmatrix} = K T' \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} $$
$$T' = \begin{pmatrix} r_{11} & r_{12} & t_1 \\ r_{21} & r_{22} & t_2 \\ r_{31} & r_{32} & t_3 \end{pmatrix} $$
K
はカメラの種類に応じて異なりますが、ピンホールカメラの場合は以下のようになることが知られています。
$$K = \begin{pmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{pmatrix} $$
以下の図において、物体 $P$ の座標 $(X, Y, Z)$ はカメラの視点座標系における値です。ピンホール (真ん中に微小な穴が開いている架空の壁) は $X_c Y_c$ 平面の点 $F_c$ で表現されています。ピンホール $F_c$ から入った光は、撮像素子となる平面 $x y$ の点 $(u, v)$ に到達します。分かりやすさのため、平面 $x y$ を物体 $P$ と同じ側に仮想的に反転移動して考えています。
Camera Calibration and 3D Reconstruction
理想的なピンホールカメラであれば、ピンホールの光軸 $Z_c$ は平面 $x y$ の $(0, 0)$ を通るようにできますが、実際には製造の過程で多少のずれが発生します。光軸が通る点の平面 $x y$ における座標を $(c_x, c_y)$ とすることでずれを表現しています。このずれは、キャリブレーションで推定する、カメラの内部パラメータの一つです。
また、画像処理で得られる、画像平面内の座標 $(u, v)$ はピクセル座標です。撮像素子である平面 xy
におけるピクセルは正方形ではないことが一般的です。そのため、距離とピクセル座標の変換において、x 軸と y 軸で別々の値を用いる必要があります。三角形の相似を考えると、以下のような式が得られます。これを行列で表現すると $K$ のようになります。
$$u = f_x \frac{X}{Z} + c_x \\ v = f_y \frac{Y}{Z} + c_y \\ f_x = f \cdot s_x \\ f_y = f \cdot s_y $$
一点しか光を通す穴がない理想的なピンホールを用いる場合は歪みは発生しませんが、実際には一点しか穴がないと露光時間中に十分な光量を得られないためレンズを用いる必要があります。レンズを用いると十分な光量を得られる一方、その代償として歪みが発生します。
OpenCV は特に影響の大きい「半径方向歪み」と「円周方向歪み」の二つを考慮した実装がなされています。
半径方向歪みはレンズの形状に起因する歪みで、レンズの中心から離れた場所を通過する光は中心付近を通過する光よりも大きく曲げられることによります。魚眼のようになります。補正のためのパラメータはテイラー級数になります。実際には最初の 2 項だけ考えていれば十分な場合が多く、OpenCV の calibrateCamera
が返す歪みパラメータの個数も最初の数個です。
$$x_{distorted} = x \cdot (1 + k_1 r^2 + k_2 r^4 + ...) \\ y_{distorted} = y \cdot (1 + k_1 r^2 + k_2 r^4 + ...) $$
円周方向歪みは、撮像素子がピンホールの平面 $X_c Y_c$ に対して平行になっていない、組立て工程に起因します。パラメータ $p_1, p_2$ の二つで表現できます。OpenCV の calibrateCamera
が返す歪みパラメータの一つです。
カメラキャリブレーションで推定する必要のあるパラメータは三種類です。
透視変換を考えると、キャリブレーション画像一枚で利用可能な点は、例えば一番右上、右下、左上、左下の4つだけです。得られるパラメータは透視変換行列の 8 つです
しかしながら、実際には歪みだけでなくノイズの影響があるため 10 枚程度は必要になります。
OpenCV にはカメラキャリブレーションのための機能が実装されています。キャリブレーションで利用する画像のサンプルも OpenCV に含まれています。
$ ls samples/data/left*.jpg | head -2
samples/data/left01.jpg
samples/data/left02.jpg
cornerSubPix
は内部的に既に findChessboardCorners
で利用されてはいますが、更に精度を高めるために、事実上もう一度実行しておくとよいです。
sample.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
import numpy as np
import cv2 as cv
import glob
# ワールド座標系におけるキャリブレーションボードの各点の座標
# (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)
# キャリブレーションで利用する座標
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.
# cornerSubPix の閾値
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
images = glob.glob('*.jpg')
for fname in images:
img = cv.imread(fname)
# グレースケールで利用
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# キャリブレーションボード内の点の座標を取得
ret, corners = cv.findChessboardCorners(gray, (7,6), None)
# キャリブレーションのために結果を保存
if ret == True:
objpoints.append(objp)
# 座標の精度を上げる
corners2 = cv.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 確認のため画像を表示
cv.drawChessboardCorners(img, (7,6), corners2, ret)
cv.imshow('img', img)
cv.waitKey(0)
cv.destroyAllWindows()
imageSize = gray.shape[::-1]
import IPython
IPython.embed()
上記スクリプトで用意した imgpoints $(u, v)$ と objpoints $(x, y, z)$ による連立方程式を解くためには cv.calibrateCamera
を利用します。err
が小さい程よいキャリブレーション結果であると言えます。内部パラメータ (Intrinsics) KK
、$k_1, k_2, p_1, p_2, k_3$ が格納されてる歪み係数 distCoeffs
、外部パラメータ (Extrinsics) rvecs
および tvecs
が得られます。外部パラメータは環境におけるカメラの位置姿勢の推定結果と考えることもできます。rvecs
はロドリゲス形式のベクトルです。
err, KK, distCoeffs, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, imageSize, None, None)
In [2]: err
Out[2]: 0.15536900918594446
In [3]: KK
Out[3]:
array([[534.07088626, 0. , 341.53407091],
[ 0. , 534.11914802, 232.94565231],
[ 0. , 0. , 1. ]])
In [4]: distCoeffs
Out[4]:
array([[-2.92971621e-01, 1.07706888e-01, 1.31038490e-03,
-3.11023081e-05, 4.34799129e-02]])
In [5]: rvecs[0]
Out[5]:
array([[-0.3784336 ],
[-0.18064237],
[-3.11615995]])
In [6]: tvecs[0]
Out[6]:
array([[ 2.82321765],
[ 2.22374307],
[10.95762955]])
得られたパラメータで歪み補正を行います。補正したい画像を読み込みます。
targetImage = cv.imread('left12.jpg')
targetImageSize = targetImage.shape[:2][::-1]
歪み係数を用いてカメラ行列を計算しなおします。補正した後の画像と補正前の画像のサイズは異なります。alpha に 1 を指定すると、補正後の画像において、補正前にはないピクセルは 0 で埋められます。
alpha = 1
newKK, roiSize = cv.getOptimalNewCameraMatrix(KK, distCoeffs, targetImageSize, alpha, targetImageSize)
元画像のピクセルの、新しい画像における X 座標の情報を持つ mapX
と、Y 座標の情報を持つ mapY
を計算します。歪み補正マップとよばれます。
mapX, mapY = cv.initUndistortRectifyMap(KK, distCoeffs, None, newKK, targetImageSize, cv.CV_32FC1)
歪み補正を行います。
undistortedImage = cv.remap(targetImage, mapX, mapY, cv.INTER_LINEAR)
alpha を 1 にしたことによる 0 で埋められた箇所を取り除いてみます。
x, y, w, h = roiSize
undistortedImage2 = undistortedImage[y:y+h, x:x+w]
以下のようになります。
cv.imshow('undistortedImage', undistortedImage)
cv.imshow('undistortedImage2', undistortedImage2)
cv.waitKey()
歪みを補正する画像が一枚だけの場合は cv.undistort
を利用した方が簡単です。
targetImage = cv.imread('left12.jpg')
targetImageSize = targetImage.shape[:2][::-1]
alpha = 1
newKK, roiSize = cv.getOptimalNewCameraMatrix(KK, distCoeffs, targetImageSize, alpha, targetImageSize)
undistortedImage = cv.undistort(targetImage, KK, distCoeffs, None, newKK)
x, y, w, h = roiSize
undistortedImage2 = undistortedImage[y:y+h, x:x+w]
cv.imwrite('calibresult.png', undistortedImage2)
calibresult.png