OpenCV 4.13.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 4.13.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
カメラキャリブレーション

目的

このセクションでは、次のことを学ぶ

  • カメラによって生じる歪みの種類
  • カメラの内部 (intrinsic) および外部 (extrinsic) の特性を求める方法
  • これらの特性に基づいて画像の歪みを補正する方法

基礎

ピンホールカメラの中には、画像に大きな歪みをもたらすものがある。主な歪みには2種類あり、放射状歪み (radial distortion) と接線方向歪み (tangential distortion) である。

放射状歪みは、直線が曲がって見える原因となる。放射状歪みは、点が画像の中心から離れるほど大きくなる。例えば、下に示す画像ではチェスボードの2つの辺が赤い線で示されている。しかし、チェスボードの境界が直線になっておらず、赤い線と一致していないことがわかる。本来は直線であるべき線がすべて外側に膨らんでいる。詳細は Distortion (optics) を参照のこと。

image

以降のセクションでは、いくつかの新しい引数を導入する。詳細は カメラキャリブレーションと3次元再構成 を参照のこと。

放射状歪みは次のように表せる。

\[x_{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \\ y_{distorted} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6)\]

同様に、接線方向歪みは、撮影レンズが結像面に対して完全に平行に配置されていないために生じる。そのため、画像内の一部の領域が予想より近くに見えることがある。接線方向歪みの量は次のように表せる。

\[x_{distorted} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy]\]

要するに、歪み係数 (distortion coefficients) と呼ばれる次の5つの引数を求める必要がある。

\[Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3)\]

これに加えて、カメラの内部引数や外部引数といった他の情報も必要となる。内部引数 (Intrinsic parameters) はカメラ固有のものである。これには焦点距離 ( \(f_x,f_y\)) や光学中心 ( \(c_x, c_y\)) などの情報が含まれる。焦点距離と光学中心を使ってカメラ行列を作成でき、これは特定のカメラのレンズによる歪みを除去するために使える。カメラ行列は特定のカメラに固有のものなので、いったん計算すれば、同じカメラで撮影した他の画像にも再利用できる。これは3x3行列として表される。

\[camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ]\]

外部引数 (Extrinsic parameters) は、3D点の座標を座標系へ変換する回転ベクトルおよび並進ベクトルに対応する。

ステレオの用途では、まずこれらの歪みを補正する必要がある。これらの引数を求めるには、よく定義されたパターン (例えばチェスボード) のサンプル画像をいくつか用意しなければならない。相対位置が既知の特定の点 (例えばチェスボードのマスの角) を見つける。これらの点の実世界空間における座標も画像中の座標もわかっているので、歪み係数を解くことができる。より良い結果を得るには、少なくとも10枚のテストパターンが必要である。

コード

前述のとおり、カメラキャリブレーションには少なくとも10枚のテストパターンが必要である。OpenCV にはチェスボードの画像がいくつか付属しているので (samples/data/left01.jpg – left14.jpg を参照)、これらを利用する。チェスボードの画像を考える。カメラのキャリブレーションに必要な重要な入力データは、3Dの実世界点の集合と、それらの点に対応する画像中の2D座標である。2D画像点は画像から容易に求められるので問題ない。(これらの画像点は、チェスボードで2つの黒いマスが互いに接する位置である)

では実世界空間の3D点はどうだろうか。これらの画像は固定されたカメラから撮影され、チェスボードはさまざまな位置と向きに置かれている。したがって \((X,Y,Z)\) の値を知る必要がある。しかし単純化のために、チェスボードはXY平面上に静止していた (つまり常に Z=0) とし、カメラがそれに応じて移動したと考えることができる。この考え方により、X, Y の値だけを求めればよくなる。さて X, Y の値については、点の位置を表す (0,0), (1,0), (2,0), ... として単純に点を渡せばよい。この場合、得られる結果はチェスボードのマスのサイズを単位としたスケールになる。しかしマスのサイズ (例えば30 mm) がわかっていれば、値を (0,0), (30,0), (60,0), ... として渡せる。こうすると結果が mm 単位で得られる。(ここではこれらの画像を自分で撮影したわけではないのでマスのサイズがわからないため、マスのサイズを単位として渡す)

3D点は オブジェクト点 (object points) と呼ばれ、2D画像点は 画像点 (image points) と呼ばれる。

セットアップ

チェスボード中のパターンを見つけるには、関数 cv.findChessboardCorners() を使える。また、8x8グリッドや5x5グリッドなど、どのような種類のパターンを探すのかも渡す必要がある。この例では7x6グリッドを使う。(通常チェスボードは8x8のマスを持ち、内部の角は7x7である)。この関数は角の点と retval を返し、retval はパターンが得られた場合に True となる。これらの角は (左から右へ、上から下へ) という順序で配置される

覚え書き
この関数は、すべての画像で必要なパターンを見つけられるとは限らない。そこで良い方法の一つは、カメラを起動して各フレームについて必要なパターンを確認するようにコードを書くことである。パターンが得られたら、角を見つけてリストに格納する。また、次のフレームを読み込む前にある程度の間隔を設け、チェスボードを別の向きに調整できるようにする。必要な数の良好なパターンが得られるまでこの処理を続ける。ここで示した例でも、与えられた14枚のうち何枚が良好なのかはわからない。したがって、すべての画像を読み込み、良好なものだけを採用しなければならない。
チェスボードの代わりに、円形グリッドを使うこともできる。この場合、パターンを見つけるには関数 cv.findCirclesGrid() を使わなければならない。円形グリッドを使うと、より少ない画像でカメラキャリブレーションを行える。

角が見つかったら、cv.cornerSubPix() を使ってその精度を高めることができる。また、cv.drawChessboardCorners() を使ってパターンを描画することもできる。これらの手順はすべて以下のコードに含まれている。

import numpy as np
import cv2 as cv
import glob
# termination criteria
criteria = (cv.TERM_CRITERIA_EPS + cv.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 = cv.imread(fname)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# Find the chess board corners
ret, corners = cv.findChessboardCorners(gray, (7,6), None)
# If found, add object points, image points (after refining them)
if ret == True:
objpoints.append(objp)
corners2 = cv.cornerSubPix(gray,corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# Draw and display the corners
cv.drawChessboardCorners(img, (7,6), corners2, ret)
cv.imshow('img', img)
void drawChessboardCorners(InputOutputArray image, Size patternSize, InputArray corners, bool patternWasFound)
Renders the detected chessboard corners.
bool findChessboardCorners(InputArray image, Size patternSize, OutputArray corners, int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE)
Finds the positions of internal corners of the chessboard.
void imshow(const String &winname, InputArray mat)
Displays an image in the specified window.
int waitKey(int delay=0)
Waits for a pressed key.
void destroyAllWindows()
Destroys all of the HighGUI windows.
Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
Loads an image from a file.
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0, AlgorithmHint hint=cv::ALGO_HINT_DEFAULT)
Converts an image from one color space to another.
void cornerSubPix(InputArray image, InputOutputArray corners, Size winSize, Size zeroZone, TermCriteria criteria)
Refines the corner locations.

パターンを描画した画像の一例を下に示す。

image

キャリブレーション

オブジェクト点と画像点が得られたので、キャリブレーションを行う準備が整った。関数 cv.calibrateCamera() を使える。これはカメラ行列、歪み係数、回転ベクトル、並進ベクトルなどを返す。

ret, mtx, dist, rvecs, tvecs = cv.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
double 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))
Finds the camera intrinsic and extrinsic parameters from several views of a calibration pattern.

歪み補正

これで画像を取得して歪みを補正できる。OpenCV にはこれを行うための2つの方法がある。ただしまず、free scaling parameter に基づいて cv.getOptimalNewCameraMatrix() を使ってカメラ行列を調整できる。スケーリング引数 alpha=0 の場合、不要なピクセルが最小限となった歪み補正画像を返す。そのため画像の隅のピクセルが除去されることさえある。alpha=1 の場合、すべてのピクセルが保持され、余分な黒い領域がいくらか含まれる。この関数は、結果をクロップするのに使える画像ROIも返す。

そこで新しい画像 (ここでは left12.jpg。これは本章の最初の画像である) を取得する。

img = cv.imread('left12.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
Mat getOptimalNewCameraMatrix(InputArray cameraMatrix, InputArray distCoeffs, Size imageSize, double alpha, Size newImgSize=Size(), Rect *validPixROI=0, bool centerPrincipalPoint=false)
Returns the new camera intrinsic matrix based on the free scaling parameter.

1. cv.undistort() を使う

これが最も簡単な方法である。関数を呼び出し、上で得られた ROI を使って結果をクロップするだけでよい。

# undistort
dst = cv.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)
void undistort(InputArray src, OutputArray dst, InputArray cameraMatrix, InputArray distCoeffs, InputArray newCameraMatrix=noArray())
Transforms an image to compensate for lens distortion.
bool imwrite(const String &filename, InputArray img, const std::vector< int > &params=std::vector< int >())
Saves an image to a specified file.

2. リマッピングを使う

この方法は少しだけ難しい。まず、歪んだ画像から歪み補正画像へのマッピング関数を求める。次に remap 関数を使う。

# undistort
mapx, mapy = cv.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv.remap(img, mapx, mapy, cv.INTER_LINEAR)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv.imwrite('calibresult.png', dst)
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R, InputArray newCameraMatrix, Size size, int m1type, OutputArray map1, OutputArray map2)
Computes the undistortion and rectification transformation map.
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2, int interpolation, int borderMode=BORDER_CONSTANT, const Scalar &borderValue=Scalar())
Applies a generic geometrical transformation to an image.

それでも、両方の方法で同じ結果が得られる。下の結果を参照のこと。

image

結果を見ると、すべてのエッジが直線になっていることがわかる。

これで、今後の利用のためにカメラ行列と歪み係数を NumPy の書き込み関数 (np.savez, np.savetxt など) を使って保存できる。

再投影誤差

再投影誤差は、求めた引数がどれだけ正確かを良く見積もる指標となる。再投影誤差がゼロに近いほど、求めた引数はより正確である。内部行列、歪み行列、回転行列、並進行列が与えられたとき、まず cv.projectPoints() を使ってオブジェクト点を画像点へ変換しなければならない。次に、その変換で得られたものと角検出アルゴリズムとの間の絶対ノルムを計算できる。平均誤差を求めるには、すべてのキャリブレーション画像について計算した誤差の算術平均を計算する。

mean_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv.norm(imgpoints[i], imgpoints2, cv.NORM_L2)/len(imgpoints2)
mean_error += error
print( "total error: {}".format(mean_error/len(objpoints)) )
void projectPoints(InputArray objectPoints, InputArray rvec, InputArray tvec, InputArray cameraMatrix, InputArray distCoeffs, OutputArray imagePoints, OutputArray jacobian=noArray(), double aspectRatio=0)
Projects 3D points to an image plane.
double norm(InputArray src1, int normType=NORM_L2, InputArray mask=noArray())
Calculates the absolute norm of an array.

演習

  1. 円形グリッドを使ったカメラキャリブレーションを試してみよう。