モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

工作HardwareHub ロゴ画像 (Laptop端末利用時)
工作HardwareHub ロゴ画像 (Mobile端末利用時)

複数の画像からパノラマを作成 (OpenCV、Python)

モーダルを閉じる

ステッカーを選択してください

モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

公開日公開日
2020/02/23
最終更新最終更新
2024/09/08
記事区分記事区分
一般公開

目次

    アカウント プロフィール画像 (サイドバー)

    どもです

    0
    ステッカーを贈るとは?

    OpenCV を用いて、複数の画像から一枚のパノラマ画像を作成します。内部パラメータが分かっているカメラを位置を変えずに回転させて画像を取得していき、各画像を取得した時点でのカメラの向きをもとに画像を重ね合わせる方法と、各画像における特徴点が一致するように画像を重ね合わせる方法の二つについて記載します。

    キャリブレーションされたカメラを定位置で回転させる方法

    あるワールド座標系における点 (x,y,z)(x, y, z) は、カメラ座標系において、カメラの外部パラメータである変換行列 TT を用いて

    X=T(xyz1)X = T \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}

    となります。カメラで取得した画像内において XX に対応する点のピクセル座標 xx は、カメラの内部パラメータ KK を用いて以下のようになります。

    x=KXx = K X

    同じカメラを定位置で回転させて、異なるカメラの向きで同じ点 (x,y,z)(x, y, z) について二枚の画像を取得することを考えます。

    a rotating camera around its axis of projection

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

    x1=KX1=KTWorldToCamera1(xyz1)x_1 = K X_1 = K T_{WorldToCamera1} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}
    x2=KX2=KTWorldToCamera2(xyz1)x_2 = K X_2 = K T_{WorldToCamera2} \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}

    カメラ座標系2 からカメラ座標系1 への変換行列 TCamera2ToCamera1T_{Camera2ToCamera1} について、以下のような式が成り立ちます。

    TCamera2ToCamera1=(RCamera2ToCamera1tCamera2ToCamera101)=TWorldToCamera1TCamera2ToWorld=TWorldToCamera1TWorldToCamera21\begin{aligned} T_{Camera2ToCamera1} &= \begin{pmatrix} R_{Camera2ToCamera1} & t_{Camera2ToCamera1} \\ 0 & 1 \end{pmatrix} \\ &= T_{WorldToCamera1} T_{Camera2ToWorld} \\ &= T_{WorldToCamera1} T_{WorldToCamera2}^{-1} \end{aligned}

    カメラは定位置で回転させているため tCamera2ToCamera1t_{Camera2ToCamera1} は 0 です。x2x_2 から x1x_1 への変換式であるホモグラフィ行列 HH が得られます。

    x1=KX1=KRCamera2ToCamera1X2=KRCamera2ToCamera1K1x2=Hx2H=KRCamera2ToCamera1K1\begin{aligned} 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{aligned}

    #!/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 モジュールを利用

    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)
    

    0
    詳細設定を開く/閉じる
    アカウント プロフィール画像 (本文下)

    どもです

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    さらに詳しく →
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      関連記事