カメラキャリブレーションについて簡単なまとめ
[History] [Last Modified] (2020/02/14 15:39:17)
ここは
趣味のプログラミングを楽しむための情報共有サービス。記事の一部は有料設定にして公開できます。 詳しくはこちらをクリック📝
Recent posts
Popular pages

概要

カメラキャリブレーション (Camera Calibration, Camera Resectioning) を行うと、レンズの歪みを表現するパラメータや、カメラのワールド座標系での位置姿勢を推定できます。

チェスボードのようなキャリブレーション専用のボードが利用されます。

Uploaded Image

キャリブレーションで得られたパラメータを用いると、例えば歪みを補正することができます。

Uploaded Image

カメラキャリブレーションのおおまかな流れは以下のようになります。

キャリブレーションボードの画像をカメラで撮影

キャリブレーションで利用するのは距離センサで取得した情報ではなく、カメラで取得した平面画像です。カメラの位置を固定したまま、キャリブレーションボードを様々な位置に移動させて画像を撮影します。

キャリブレーションボードの各点の座標を取得

画像処理を行い、キャリブレーションボードの各点の、平面画像内での座標 $(u, v)$ を取得します。

Uploaded Image

連立方程式を立てる

画像処理で取得したキャリブレーションボードの各点の座標 $(u, v)$ と、ある座標系におけるキャリブレーションボードの各点の座標 $(x, y, z)$ は既知です。例えばローカル座標系におけるキャリブレーションボードの各点の座標は (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0) のようになります。

ローカル座標系からカメラの視点座標系への変換行列 T と、カメラの視点座標系から平面画像に投影するための、カメラモデルを表現する行列 K を用いると以下のような連立方程式が立てられます。キャリブレーションにおいて、KT をそれぞれ内部パラメータ (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

Uploaded Image

理想的なピンホールカメラであれば、ピンホールの光軸 $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 が返す歪みパラメータの一つです。

必要となるキャリブレーション画像の枚数

カメラキャリブレーションで推定する必要のあるパラメータは三種類です。

  • 内部パラメータ $c_x, c_y, f_x, f_y$ 4つ (キャリブレーションボードの位置姿勢によらず一定)
  • 外部パラメータ $T$ 回転 3つ と平行移動 3つ で合計 6つ (キャリブレーションボードの位置姿勢一つに対して 6 つ)
  • 歪みパラメータ $k_1, k_2, p_1, p_2, k_3, k_4, ...$

透視変換を考えると、キャリブレーション画像一枚で利用可能な点は、例えば一番右上、右下、左上、左下の4つだけです。得られるパラメータは透視変換行列の 8 つです

  • 歪みがない場合、キャリブレーション画像一枚で推定する必要のあるパラメータの個数は 10 です。8 つ求まりますが足りません。
  • 歪みがない場合、キャリブレーション画像二枚で推定する必要のあるパラメータの個数は 4 + 6 + 6 で 16 です。透視変換行列二つで 16 となるため理想的には最低二枚のキャリブレーション画像が必要ということになります。

しかしながら、実際には歪みだけでなくノイズの影響があるため 10 枚程度は必要になります。

OpenCV で連立方程式を解く

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]])

歪み補正 rectification

歪み補正マップを計算する方法

得られたパラメータで歪み補正を行います。補正したい画像を読み込みます。

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()

Uploaded Image

より簡単な cv.undistort の利用

歪みを補正する画像が一枚だけの場合は 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

Uploaded Image

Related pages
    概要 ワールド座標に固定された単一カメラが存在するとします。RGB-D カメラではなく RGB カメラです。この単一カメラからはカラーまたはグレースケール画像が取得できます。カメラキャリブレーションの考え方を利用すると、カメラで取得した画像に写っている既知の物体のワールド座標における位置姿勢を推定できます。 カメラキャリブレーションによる内部パラメータ推定結果の保存
    概要 OpenCV を用いて、複数の画像から一枚のパノラマ画像を作成します。内部パラメータが分かっているカメラを位置を変えずに回転させて画像を取得していき、各画像を取得した時点でのカメラの向きをもとに画像を重ね合わせる方法と、各画像における特徴点が一致するように画像を重ね合わせる方法の二つについて記載します。 キャリブレーションされたカメラを定位置で回転させる方法