複数の画像からパノラマを作成 (OpenCV、Python)
[履歴] [最終更新] (2020/02/24 00:39:41)

概要

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

Uploaded Image

二つの向きにおけるカメラの外部パラメータは異なります。

$$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} $$

Uploaded Image

Uploaded Image

Uploaded Image

#!/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

Uploaded Image

original_image_right.jpg

Uploaded Image

パノラマ画像

Uploaded Image

以下では、右側の画像から左側の画像へのホモグラフィ行列を計算しています。関連ページ

#!/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)

Uploaded Image

Uploaded Image

Uploaded Image

Uploaded Image

Uploaded Image

「キャリブレーションされたカメラを定位置で回転させる方法」で扱った画像についても以下のように結合できます。

Uploaded Image

Uploaded Image

Uploaded Image

OpenCV が提供する stitching モジュールを利用

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)

Uploaded Image

関連ページ
    特徴点の検出 Feature Detection 特徴点として利用できるものの一つに、物体の角があります。角を検出するアルゴリズムの一つに Harris Corner Detection があります。分かりやすさのため、モルフォロジー変換で「角」を膨張させています。 #!/usr/bin/python # -*- coding: utf-8 -*- import numpy as np i
    概要 特徴点が分かっている既知の物体については、単一カメラを用いて位置姿勢が推定できます。物体や全体のシーンが未知である場合は、例えばステレオ形式のカメラ二つを用いることで同様の結果を得ます。具体的には、距離情報を濃淡として保存した距離画像 (Depth Map; 奥行きマップ) を作成します。 エピポーラ幾何について