概要
OpenCV でカメラパラメータを推定するためのキャリブレーション方法について説明します。
関連記事
カメラモデルについては、以下の記事を参照してください。
OpenCV – カメラモデルとカメラパラメータについて解説 | pystyle
カメラキャリブレーション
3 次元上のある点から画像平面上の点への変換を記述するモデルをカメラモデルといいます。 カメラモデルが持つパラメータをカメラパラメータといい、内部パラメータと外部パラメータに分かれます。 このカメラパラメータを測定する作業をカメラキャリブレーションといいます。
手順
1. キャリブレーション器具用意して撮影する
チェスボード、またはサークルグリッドが描かれた画像を紙に印刷します。これをキャリブレーション器具といいます。
画像は OpenCV: Create calibration pattern を参考に自分で作ることもできますが、以下の 2 種類の画像が OpenCV のサンプルページにあるので、こちらの画像を印刷して用意できます。
用意できたら、紙を平らな場所に配置し、キャリブレーション器具を様々な距離、角度から撮影します。 撮影する際のポイントとして、キャリブレーション器具全体が映るようにしてください。
2. 撮影した画像から目印を検出する
今回はサンプルとして こちら の画像を使用しました。キャリブレーション器具を撮影した画像が samples
ディレクトリにあるものとします。
画像処理により、画像に写っているキャリブレーション器具のマーカーを検出し、その画像上の位置を求めます。
チェスボードの場合
cv2.findChessBoardCorners()
を使用してチェスボードの交点を検出します。retval, corners = cv2.findChessboardCorners(image, patternSize[, corners[, flags]])
patternSize
には(列方向の交点数, 行方向の交点数)
を指定します。マス目の数ではなく、交点の数なので注意してください。 返り値は、すべての交点の検出に成功した場合はretval
が True になり、corners
にその交点の画像座標が格納されます。サークルグリッドの場合
cv2.findCirclesGrid()
を使用して円を検出します。retval, centers = cv2.findCirclesGrid(image, patternSize[, centers[, flags[, blobDetector]]])
patternSize
には(列方向の円の数, 行方向の円の数)
を指定します。 返り値は、すべての円の検出に成功した場合はretval
が True になり、corners
にその円の中心の画像座標が格納されます。
サークルグリッドの場合
キャリブレーション器具がサークルグリッドの場合のコードも記載しました。
img = cv2.imread("circles_pattern.jpg")
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Blob 検出器を作成する。
params = cv2.SimpleBlobDetector_Params()
detector = cv2.SimpleBlobDetector_create(params)
# 検出器で円が検出できるか確認する。
# keypoints = detector.detect(img_gray)
# dst = cv2.drawKeypoints(img.copy(), keypoints, None, color=(0, 0, 255))
# imshow(detect_frame)
# サークルグリッドを検出する。
cols, rows = 12, 8
ret, centers = cv2.findCirclesGrid(
img_gray,
(cols, rows),
flags=cv2.CALIB_CB_SYMMETRIC_GRID + cv2.CALIB_CB_CLUSTERING,
blobDetector=detector,
)
# 検出結果を描画する。
if ret:
dst = cv2.drawChessboardCorners(img.copy(), (cols, rows), centers, ret)
imshow(dst)
from pathlib import Path
import cv2
import matplotlib.pyplot as plt
import numpy as np
# チェスボードの設定
cols = 9 # 列方向の交点数
rows = 6 # 行方向の交点数
# 画像を読み込む。
img_points = []
imgs = []
for path in img_dir.glob("*.jpg"):
img = cv2.imread(str(path))
imgs.append(img)
# 画像をグレースケールに変換する。
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# チェスボードの交点を検出する。
ret, corners = cv2.findChessboardCorners(img, (cols, rows))
if ret: # すべての交点の検出に成功
img_points.append(corners)
else:
print(f"チェスボードの検出に失敗しました。(パス: {path})")
返り値 corners
の形状を見てみると、(54, 1, 2) となっています。
交点数は $6 \times 9 = 54$ であり、この配列は [[[1個目の交点の xy 座標], [2個目の交点の xy 座標], ...]
を表しています。
(448, 625, 3)
3. 検出した画像座標系の座標に対応する世界座標系の座標を作成する
検出した画像座標系の座標に対応する世界座標系の座標を作成します。 世界座標系の原点や向き、スケールなどは自由に決めてよいですが、通常はキャリブレーションボード上を $Z = 0$ とし、横方向を $X$ 軸、縦方向を $Y$ 軸と一致させます。 このように座標軸を決めることで、チェスボードの行、列方向の交点数は既知なので、検出した画像座標系の点に対応する 3 次元上の点を作成できます。
def create_world_point(rows, cols, scale=1):
# 検出した画像座標系の点に対応する 3 次元上の点を作成する。
world_point = np.zeros((rows * cols, 3), np.float32)
world_point[:, :2] = np.mgrid[:cols, :rows].T.reshape(-1, 2)
return world_point
# image_points に対応する世界座標系の点の一覧を作成する。
world_point = create_world_point(rows, cols)
world_points = [world_point] * len(img_points)
4. カメラパラメータを推定する
画像座標系の点と世界座標系の対応する点の一覧ができたので、cv2.calibrateCamera()
でカメラパラメータを推定します。
retval, cameraMatrix, distCoeffs, rvecs, tvecs = \
cv2.calibrateCamera(objectPoints, imagePoints, imageSize,
cameraMatrix, distCoeffs[, rvecs[, tvecs[, flags[, criteria]]]
- 引数
- objectPoints: 世界座標系の点一覧
- imagePoints: 画像座標系の点一覧
- imageSize: 画像の大きさ
- flags: フラグ
- criteria: 最適化の反復を終了する基準
- 返り値
- retval: 交点検出に成功した場合は True、そうでない場合は False
- cameraMatrix: カメラ行列
- distCoeffs: 歪み係数
- rvecs: 回転ベクトル
- tvecs: 平行移動成分
一般に使用されるカメラでは、skew は 0 であるため、OpenCV の関数ではカメラ行列で $s = 0$ としたカメラモデルを使用します。
$$ h \begin{pmatrix} x_I \\ y_I \\ 1 \end{pmatrix} = \begin{pmatrix} f_x & 0 & c_x & 0 \\ 0 & f_y & c_y & 0 \\ 0 & 0 & 1 & 0 \\ \end{pmatrix} [R|t] \begin{pmatrix} X_w \\ Y_w \\ Z_w \\ 1 \\ \end{pmatrix} $$cameraMatrix
: $s=0$ とした 3×3 のカメラ行列 (内部パラメータ)distCoeffs
: 歪み係数 (内部パラメータ)rvecs
:rvecs[i]
はworld_points[i]
からimage_points[i]
に変換する際の 3 次元上の回転。回転行列ではなく、回転ベクトルで表現。 (外部パラメータ)tvecs
:tvecs[i]
はworld_points[i]
からimage_points[i]
に変換する際の 3 次元上の平行移動。 (外部パラメータ)
img_h, img_w = imgs[0].shape[:2]
ret, camera_matrix, distortion, rvecs, tvecs = cv2.calibrateCamera(
world_points, img_points, (img_w, img_h), None, None
)
print("reprojection error:", ret, sep="\n")
print("camera matrix:", camera_matrix, sep="\n")
print("distortion:", distortion, sep="\n")
print("rvecs[0]:", rvecs[0], sep="\n")
print("tvecs[0]:", tvecs[0], sep="\n")
reprojection error: 0.29143269257189314 camera matrix: [[465.37442759 0. 338.20216266] [ 0. 466.22907443 218.85083528] [ 0. 0. 1. ]] distortion: [[-0.01356546 0.00182163 -0.00132746 0.00026079 0.03298632]] rvecs[0]: [[-0.24060521] [ 0.34735712] [ 1.53049246]] tvecs[0]: [[ 2.02114037] [-4.08028483] [12.7999798 ]]
5. カメラパラメータを保存する
カメラキャリブレーションで得られたカメラ行列、歪み係数はカメラ固有の内部パラメータであり、一度推定すれば使い回すことができます。
np.savez()
を使用して、ファイルに保存しておきましょう。
読み込む場合は、np.load()
を使用します。
np.savez("camera_params.npz", camera_matrix=camera_matrix, distortion=distortion)
camera_matrix, distortion = np.load("camera_params.npz").values()
print("camera matrix:", camera_matrix, sep="\n")
print("distortion:", distortion, sep="\n")
camera matrix: [[465.37442759 0. 338.20216266] [ 0. 466.22907443 218.85083528] [ 0. 0. 1. ]] distortion: [[-0.01356546 0.00182163 -0.00132746 0.00026079 0.03298632]]
コメント