OpenCVで画像マッチングをする

OpenCVというのは、コンピュータビジョン用のライブラリです。
カメラキャリブレーションもできたり、(多分)すごい機能がたくさん入っているんだと思います。

その道の人には怒られるかもしれないですが、普通に画像処理をするにもなかなか便利なライブラリがたくさん入っていたり、画像の入出力も分かりやすかったりで、Pythonで画像を扱うとき、私はだいたいOpenCV経由でやっています。

さて、OpenCVを使って画像マッチングというものを行うことができます。
どんなものかというと、同じ対象が写っている2枚の画像から、対応点を見つけ出すという処理です。

何のために行うのかというと、例えば以下のような用途でしょうか。
・パノラマ画像の作成
 重複するエリアの同じ地点を見つけ出せるので、ずれることなく重ねられます。
・3次元形状の復元
 視点の異なる2点間の対応点が取れれば、カメラの相対姿勢が分かり、
 対象の形状を推定できます

実際にやってみた例が下の画像です。

これはORBによるもので、若干、マッチングポイントに偏りがあるような気がしますが、写真の視点と範囲が異なっていても、対応が取れていることがわかります。
ロープウェーに乗っているときに撮った写真ですので、撮影したときの高さに差異があって、写真の範囲も重なる部分はありますが、異なっています。

OpenCVのインストール

OpenCVは完全フリーなものと権利的な制約のあるものとでパッケージが明示的に分かれています。制約付きのものはcontribというパッケージに入っています。
contribに含まれているライブラリには、例えば、SIFTやSURFなどの特徴抽出・記述アルゴリズムがあります。

インストールは 以下のコマンドで 簡単にできます。1行目のopencv-pythonだけでも動くのですが、私はSIFTも使いたいのでcontribも入れました。
なお、contribも入れる場合、バージョン指定は必須です。

★2019/9/8時点でバージョン3.4.2.16がなくなっていたようでしたので、指定バージョンを修正しました。

pip install opencv-python==3.4.2.17
pip install opencv-contrib-python==3.4.2.17

そうでないと、エラーが出ることがあります(下はSIFTを動かしたときに発生)。多分、デフォルトだとバージョンの不一致が生じるのでしょう。

# -----出力-----
cv2.error: OpenCV(4.1.0) /io/opencv_contrib/modules/xfeatures2d/src/sift.cpp:1207: error: (-213:The function/feature is not implemented) This algorithm is patented and is excluded in this configuration; Set OPENCV_ENABLE_NONFREE CMake option and rebuild the library in function 'create'

特徴点抽出

早速動かしてみます。

マッチングは二段階の処理で実施しています。
①画像中から特徴点を見つけて、その特徴量を記述する
②2画像間の特徴量を比較して、似ている特徴点同士をマッチングする。

この特徴量抽出/記述に各アルゴリズムの工夫があります。
英語論文なので、そのうち読んでまとめたいなと思います。

基本的なコードは、ここを参考にしています。ただし、仕様の変更(?)により、そのままでは動かない箇所がありますので、手を加えてはいます。

img1 = cv2.imread('IMG001.JPG')
img2 = cv2.imread('IMG002.JPG')

h,w,_ = img1.shape
rate = int(max(h,w)/512)

# 画像サイズを最大512に変更
img1 = cv2.resize(img1, (int(w/rate), int(h/rate)))
img2 = cv2.resize(img2, (int(w/rate), int(h/rate)))

detector = cv2.ORB_create()

# 特徴点抽出
kp1, des1 = detector.detectAndCompute(img1, None)
kp2, des2 = detector.detectAndCompute(img2, None)
    
bf = cv2.BFMatcher()

# マッチング
matches = bf.match(des1, des2)
matches = sorted(matches, key = lambda x:x.distance)

# 画像書き出し、出力
out = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None, flags=2)
cv2.imwrite('outimg.png', out)

detector=cv2.ORB_create()の箇所で、ORBの検出器のインスタンスを作成しています。
ここでは、ORBを使用しています。

detector.detectAndCompute()で、入力画像に対するキーポイント(kp)と特徴量(des)を出力します。なお、detector.detect()だけにすると、キーポイントだけを出力します。
マッチングには特徴量を使用しますので、マッチングしたいときは、detectAndCompute()を選びます。

マッチングポイントを確認する

Query画像とTrain画像で抽出したキーポイントのマッチング結果を確認するとき、drawMatches()を使うと、キーポイント同士を結んだ画像を出力できます。

では、具体的に座標値はいくつになるのでしょうか。
意外と情報が見つからなかったりしますので、以下、メモです。

Matchした点を単純にprintで出力してみます。

print(matches[:10])

DMatch型オブジェクトとして返されて中が見えません。
マッチングした点の情報を知るには、属性にアクセスしなければいけないのです。

# -----出力-----
[<DMatch 0x7fe03e2ac7b0>, <DMatch 0x7fe03e2ad3b0>, <DMatch 0x7fe03e2ac790>, <DMatch 0x7fe03e2ad6f0>, <DMatch 0x7fe03e2ab8d0>, <DMatch 0x7fe03e2ad310>, <DMatch 0x7fe03e2ad530>, <DMatch 0x7fe03e2adf10>, <DMatch 0x7fe03e2ae0f0>, <DMatch 0x7fe03e2adad0>]

DMatch型オブジェクトの持っている属性は、以下の4つです。
・distance:特徴量記述子間の距離
・trainIdx:学習記述子(参照データ)の記述子のインデックス
・queryIdx:クエリ記述子(検索データ)の記述子のインデックス
・imgIdx:学習画像のインデックス

ちなみに、上のコード「bf.match(des1, des2)」で、des1がクエリ、des2が学習画像に該当します。des1と近い特徴をdes2に問い合わせる(クエリ)というイメージですね。

では、1番目のマッチングの情報を見てみます。

print(matches[0].distance)
print(matches[0].trainIdx)
print(matches[0].queryIdx)
print(matches[0].imgIdx)
# -----出力-----
121.76616668701172
214
207
0

ここで、でてきたtrainIdxとqueryIdxはdetectAndComputeで出力されたキーポイントのインデックスです。
したがって、次に 画像の特徴抽出の際に計算したキーポイント情報にアクセス して、具体的に画像座標のどこの点が該当するかを調べなくてはなりません。

このキーポイント情報もKeypoint型のオブジェクトですので、同じように属性にアクセスします。

keypointオブジェクトは以下の属性を持っています。
・pt:キーポイントの座標
・size:キーポイント周辺の重要領域の直径
・angle:計算されたキーポイントの方向(計算できない場合は -1 )
・response:最も強いキーポイントが選択されたときの応答
・octave:キーポイントが抽出されるオクターブ(ピラミッドの階層)
・class_id:オブジェクトクラス

print(kp1[0].pt)
print(kp1[0].size)
print(kp1[0].angle)
print(kp1[0].response)
print(kp1[0].octave)
print(kp1[0].class_id)

クエリ側の最初(0番目)のキーポイントの情報はこんな感じです。

# -----出力-----
(60.0, 129.0)
31.0
276.7517395019531
0.001297724898904562
0
-1

とりあえず、キーポイントの情報で最も距離の近い(似ている)キーポイントを見てみます。
matchesに並べ替えを行っていましたので、一番先頭に来ているインデックスの座標を見てみます。

print('query: ', kp1[matches[0].queryIdx].pt)
print('train: ', kp2[matches[0].trainIdx].pt)

以下の数値が出力されました。
整数にすると、query: (108, 171) , train: (292, 266)です。

# -----出力------
query:  (108.00000762939453, 171.36000061035156)
train:  (292.32000732421875, 266.4000244140625)

画像に重ねるとこのようになりました。
建物の角の部分を最も似ていると判断したようです。