OpenCV を用いて、複数の画像から一枚のパノラマ画像を作成します。内部パラメータが分かっているカメラを位置を変えずに回転させて画像を取得していき、各画像を取得した時点でのカメラの向きをもとに画像を重ね合わせる方法と、各画像における特徴点が一致するように画像を重ね合わせる方法の二つについて記載します。
あるワールド座標系における点 $(x, y, z)$ は、カメラ座標系において、カメラの外部パラメータである変換行列 $T$ を用いて
$$X = T \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} $$
となります。カメラで取得した画像内において $X$ に対応する点のピクセル座標 $x$ は、カメラの内部パラメータ $K$ を用いて以下のようになります。
$$x = K X $$
同じカメラを定位置で回転させて、異なるカメラの向きで同じ点 $(x, y, z)$ について二枚の画像を取得することを考えます。
a rotating camera around its axis of projection
二つの向きにおけるカメラの外部パラメータは異なります。
$$x_1 = K X_1 = K T_{WorldToCamera1} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} $$
$$x_2 = K X_2 = K T_{WorldToCamera2} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} $$
カメラ座標系2 からカメラ座標系1 への変換行列 $T_{Camera2ToCamera1}$ について、以下のような式が成り立ちます。
$$\begin{eqnarray} T_{Camera2ToCamera1} &=& \begin{pmatrix} R_{Camera2ToCamera1} & t_{Camera2ToCamera1} \\ 0 & 1 \end{pmatrix} \\ &=& T_{WorldToCamera1} T_{Camera2ToWorld} \\ &=& T_{WorldToCamera1} T_{WorldToCamera2}^{-1} \end{eqnarray} $$
カメラは定位置で回転させているため $t_{Camera2ToCamera1}$ は 0 です。$x_2$ から $x_1$ への変換式であるホモグラフィ行列 $H$ が得られます。
$$\begin{eqnarray} x_1 &=& K X_1 \\ &=& K R_{Camera2ToCamera1} X_2 \\ &=& K R_{Camera2ToCamera1} K^{-1} x_2 \\ &=& H x_2 \\ \\ H &=& K R_{Camera2ToCamera1} K^{-1} \end{eqnarray} $$
#!/usr/bin/python
# -*- coding: utf-8 -*-
import numpy as np
import cv2 as cv
# 二枚の画像を読み込みます。
img1 = cv.imread('Blender_Suzanne1.jpg')
img2 = cv.imread('Blender_Suzanne2.jpg')
# カメラの内部パラメータ
cameraMatrix = np.array([[700.0, 0.0, 320.0],
[0.0, 700.0, 240.0],
[0, 0, 1]], dtype=np.float32)
# 画像が取得された時点でのカメラの座標系への、ワールド座標からの変換行列
tWorldToCamera1 = np.array([[0.9659258723258972, 0.2588190734386444, 0.0, 1.5529145002365112],
[ 0.08852133899927139, -0.3303661346435547, -0.9396926164627075, -0.10281121730804443],
[-0.24321036040782928, 0.9076734185218811, -0.342020183801651, 6.130080699920654],
[0, 0, 0, 1]], dtype=np.float64)
tWorldToCamera2 = np.array([[0.9659258723258972, -0.2588190734386444, 0.0, -1.5529145002365112],
[-0.08852133899927139, -0.3303661346435547, -0.9396926164627075, -0.10281121730804443],
[0.24321036040782928, 0.9076734185218811, -0.342020183801651, 6.130080699920654],
[0, 0, 0, 1]], dtype=np.float64)
# カメラ座標間の変換行列
tCamera2ToCamera1 = tWorldToCamera1.dot(np.linalg.inv(tWorldToCamera2))
rCamera2ToCamera1 = tCamera2ToCamera1[:3, :3]
# 今回、カメラは定位置であるため、以下のように回転行列だけを抜き出して考えることもできます。
rWorldToCamera1 = tWorldToCamera1[0:3, 0:3]
rWorldToCamera2 = tWorldToCamera2[0:3, 0:3]
_rCamera2ToCamera1 = rWorldToCamera1.dot(rWorldToCamera2.T)
print(np.sum(tCamera2ToCamera1[:3, 3])) #=> 2.1533593896894132e-07
print(np.sum(rCamera2ToCamera1 - _rCamera2ToCamera1)) #=> -1.7321051634655582e-07
# ホモグラフィ行列の計算
H = cameraMatrix.dot(rCamera2ToCamera1).dot(np.linalg.inv(cameraMatrix))
H = H / H[2, 2]
# img2 を、img1 を取得した時点でのカメラから見た画像に変換
# 座標がずれるため、もと画像よりも大きいサイズを指定
img2_warped = cv.warpPerspective(img2, H, (img2.shape[1]*2, img2.shape[0]))
# img1 と重ね合わせます。
img_stitched = img2_warped.copy()
img_stitched[:img1.shape[0], :img1.shape[1]] = img1
# 検証用の画像
img_space = np.zeros((img1.shape[0], 50, 3), dtype=np.uint8)
img_compare = cv.hconcat([img1, img_space, img2])
cv.imshow('img_stitched', img_stitched)
cv.imshow('img_compare', img_compare)
cv.imshow('img2_warped', img2_warped)
cv.waitKey(0)
回転行列の転置行列は逆行列と一致します。サンプルとして用いた画像はこちらからダウンロードできます。
画像内の特徴点の検出のために、ここでは SIFT (Scale Invariant Feature Transform) アルゴリズムを利用します。SIFT は特許を取得しており、最新の OpenCV には含まれていません。ここでは古いバージョンの OpenCV を利用することにします。
python -m pip install opencv-python==3.4.1.15
python -m pip install opencv-contrib-python==3.4.1.15
二枚の画像内で検出した特徴点の特徴量を cv.BFMatcher
で比較して、特徴量がより良く一致する特徴点の組を複数計算します。特徴点の組からホモグラフィ行列を計算します。片方の画像をホモグラフィ行列で変換することで、二枚の画像を一枚に重ね合わせることができます。
original_image_left.jpg
original_image_right.jpg
パノラマ画像
以下では、右側の画像から左側の画像へのホモグラフィ行列を計算しています。関連ページ
#!/usr/bin/python
# -*- coding: utf-8 -*-
import cv2 as cv
import numpy as np
img1 = cv.imread('original_image_left.jpg')
img2 = cv.imread('original_image_right.jpg')
# img1 = cv.imread('Blender_Suzanne1.jpg')
# img2 = cv.imread('Blender_Suzanne2.jpg')
img1_gray = cv.cvtColor(img1, cv.COLOR_BGR2GRAY)
img2_gray = cv.cvtColor(img2, cv.COLOR_BGR2GRAY)
# 特徴点 Key Points kp1, kp2
# 特徴量記述子 Feature Description des1, des2
sift = cv.xfeatures2d.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1_gray, None)
kp2, des2 = sift.detectAndCompute(img2_gray, None)
# 特徴量を総当たりでマッチングします。
# マッチング度合いが高い順に二つ (k=2) 取得します。
match = cv.BFMatcher()
matches = match.knnMatch(des2, des1, k=2)
# マッチング結果に閾値を設定します。
# 取得した結果二つのうち、一つをもう一つの閾値として利用しています。
good = []
for m, n in matches:
if m.distance < 0.03 * n.distance:
# if m.distance < 0.75 * n.distance:
good.append(m)
# ホモグラフィの計算には理論上 4 つの点が必要です。実際にはノイズの影響もあるため更に必要です。
MIN_MATCH_COUNT = 10
if len(good) > MIN_MATCH_COUNT:
src_pts = np.float32([ kp2[m.queryIdx].pt for m in good ])
dst_pts = np.float32([ kp1[m.trainIdx].pt for m in good ])
H = cv.findHomography(src_pts, dst_pts, cv.RANSAC, 5.0)[0]
else:
print('Not enought matches are found - {}/{}'.format(len(good), MIN_MATCH_COUNT))
exit(1)
# ホモグラフィ行列で img2 を変換します。
img2_warped = cv.warpPerspective(img2, H, (img1.shape[1] + img2.shape[1], img1.shape[0]))
# img1 と結合します。
img_stitched = img2_warped.copy()
img_stitched[:img1.shape[0], :img1.shape[1]] = img1
# 余分な 0 領域を削除します。
def trim(frame):
if np.sum(frame[0]) == 0:
return trim(frame[1:])
if np.sum(frame[-1]) == 0:
return trim(frame[:-2])
if np.sum(frame[:,0]) == 0:
return trim(frame[:, 1:])
if np.sum(frame[:,-1]) == 0:
return trim(frame[:, :-2])
return frame
img_stitched_trimmed = trim(img_stitched)
# 特徴点を可視化して確認します。
cv.imshow('drawKeypoints', cv.drawKeypoints(img2, kp2, None))
# マッチング結果を可視化して確認します。
draw_params = dict(matchColor=(0,255,0),
singlePointColor=None,
flags=2)
cv.imshow('drawMatches', cv.drawMatches(img2, kp2, img1, kp1, good, None, **draw_params))
cv.imshow('img2_warped', img2_warped)
cv.imshow('img_stitched', img_stitched)
cv.imshow('img_stitched_trimmed', img_stitched_trimmed)
cv.waitKey(0)
「キャリブレーションされたカメラを定位置で回転させる方法」で扱った画像についても以下のように結合できます。
OpenCV は上記のような結合処理を簡単に行うための stitching API を提供しています。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import cv2 as cv
imgs = []
imgs.append(cv.imread('Blender_Suzanne1.jpg'))
imgs.append(cv.imread('Blender_Suzanne2.jpg'))
# imgs.append(cv.imread('original_image_left.jpg'))
# imgs.append(cv.imread('original_image_right.jpg'))
stitcher = cv.Stitcher_create() # opencv4
# stitcher = cv.createStitcher(True) # opencv3
stitched = stitcher.stitch(imgs)[1]
cv.imshow('stitched', stitched)
cv.waitKey(0)