目的
この章では
- ある画像の特徴を他の画像の特徴とマッチングする方法を見ていく。
- OpenCV の Brute-Force マッチャーと FLANN マッチャーを使う
総当たりマッチャ(Brute-Force Matcher)の基礎
Brute-Force マッチャーは単純である。最初の集合にある1つの特徴の記述子を取り、何らかの距離計算を使って2番目の集合にある他のすべての特徴とマッチングする。そして最も近いものが返される。
BFマッチャを使うには、まず cv.BFMatcher() を使って BFMatcher オブジェクトを作成する必要がある。これは省略可能な引数を2つ取る。1つ目は normType で、使用する距離尺度を指定する。デフォルトでは cv.NORM_L2 である。これは SIFT や SURF などに適している(cv.NORM_L1 もある)。ORB、BRIEF、BRISK などのバイナリ文字列ベースの記述子には、ハミング距離を尺度として用いる cv.NORM_HAMMING を使うべきである。ORB が WTA_K == 3 または 4 を使用している場合は、cv.NORM_HAMMING2 を使うべきである。
2つ目の引数はブール変数 crossCheck で、デフォルトでは false である。これが true の場合、マッチャは集合 A の i 番目の記述子にとって集合 B の j 番目の記述子が最良のマッチであり、その逆もまた成り立つような (i,j) の値を持つマッチのみを返す。つまり、両方の集合の2つの特徴が互いにマッチしている必要がある。これは一貫した結果をもたらし、D.Lowe が SIFT の論文で提案した比率テスト (ratio test) の良い代替手段となる。
作成したら、重要なメソッドが2つある。BFMatcher.match() と BFMatcher.knnMatch() である。1つ目は最良のマッチを返す。2つ目のメソッドは、ユーザが指定した k 個の最良マッチを返す。これは、そのマッチに対して追加の処理が必要な場合に役立つことがある。
キーポイントを描画するために cv.drawKeypoints() を使ったように、cv.drawMatches() はマッチを描画するのに役立つ。この関数は2つの画像を水平に並べ、最良のマッチを示す線を1つ目の画像から2つ目の画像へ引く。また cv.drawMatchesKnn もあり、これは k 個の最良マッチをすべて描画する。k=2 の場合、各キーポイントについて2本のマッチ線を描画する。そのため、選択的に描画したい場合はマスクを渡さなければならない。
SIFT と ORB のそれぞれについて1つずつ例を見てみよう(両者は異なる距離尺度を用いる)。
ORB記述子を用いた総当たりマッチング
ここでは、2つの画像間で特徴をマッチングする簡単な例を見る。この例では、queryImage と trainImage がある。特徴マッチングを使って trainImage の中から queryImage を探し出してみる。(画像は /samples/data/box.png と /samples/data/box_in_scene.png である)
特徴のマッチングには ORB 記述子を使用する。それでは、画像の読み込み、記述子の検出などから始めよう。
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
img1 =
cv.imread(
'box.png',cv.IMREAD_GRAYSCALE)
img2 =
cv.imread(
'box_in_scene.png',cv.IMREAD_GRAYSCALE)
orb = cv.ORB_create()
kp1, des1 = orb.detectAndCompute(img1,None)
kp2, des2 = orb.detectAndCompute(img2,None)
Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
Loads an image from a file.
次に、距離尺度 cv.NORM_HAMMING を指定して(ORB を使っているため)BFMatcher オブジェクトを作成し、より良い結果を得るために crossCheck を有効にする。次に Matcher.match() メソッドを使って2つの画像間の最良のマッチを取得する。最良のマッチ(距離が小さいもの)が先頭に来るように、距離の昇順で並べ替える。そして、最初の10個のマッチのみを描画する(見やすさのためだけである。好きなだけ増やしてよい)。
matches = bf.match(des1,des2)
matches = sorted(matches, key = lambda x:x.distance)
img3 =
cv.drawMatches(img1,kp1,img2,kp2,matches[:10],
None,flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
plt.imshow(img3),plt.show()
Brute-force descriptor matcher.
Definition features.hpp:1213
void drawMatches(InputArray img1, const std::vector< KeyPoint > &keypoints1, InputArray img2, const std::vector< KeyPoint > &keypoints2, const std::vector< DMatch > &matches1to2, InputOutputArray outImg, const Scalar &matchColor=Scalar::all(-1), const Scalar &singlePointColor=Scalar::all(-1), const std::vector< char > &matchesMask=std::vector< char >(), DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT)
Draws the found matches of keypoints from two images.
以下が得られた結果である。
image
このMatcherオブジェクトとは何か?
matches = bf.match(des1,des2) の行の結果は DMatch オブジェクトのリストである。この DMatch オブジェクトは以下の属性を持つ。
- DMatch.distance - 記述子間の距離。小さいほど良い。
- DMatch.trainIdx - train 記述子における記述子のインデックス
- DMatch.queryIdx - query 記述子における記述子のインデックス
- DMatch.imgIdx - train 画像のインデックス。
SIFT記述子と比率テストを用いた総当たりマッチング
今回は BFMatcher.knnMatch() を使って k 個の最良マッチを取得する。この例では k=2 とし、D.Lowe が論文で説明した比率テストを適用できるようにする。
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
img1 =
cv.imread(
'box.png',cv.IMREAD_GRAYSCALE)
img2 =
cv.imread(
'box_in_scene.png',cv.IMREAD_GRAYSCALE)
sift = cv.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
matches = bf.knnMatch(des1,des2,k=2)
good = []
for m,n in matches:
if m.distance < 0.75*n.distance:
good.append([m])
img3 = cv.drawMatchesKnn(img1,kp1,img2,kp2,good,None,flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
plt.imshow(img3),plt.show()
以下の結果を参照のこと。
image
FLANNベースのMatcher
FLANN は Fast Library for Approximate Nearest Neighbors(近似最近傍探索のための高速ライブラリ)の略である。大規模なデータセットや高次元の特徴に対する高速な最近傍探索に最適化されたアルゴリズム群を含んでいる。大規模なデータセットでは BFMatcher よりも高速に動作する。FLANN ベースのマッチャを使った2番目の例を見てみよう。
FLANN ベースのマッチャでは、使用するアルゴリズムやその関連パラメータなどを指定する2つの辞書を渡す必要がある。1つ目は IndexParams である。さまざまなアルゴリズムについて、渡すべき情報は FLANN のドキュメントで説明されている。要約すると、SIFT や SURF などのアルゴリズムでは次のように渡せる。
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
ORB を使う場合は次のように渡せる。コメントアウトされた値はドキュメントで推奨されているものだが、場合によっては必要な結果が得られなかった。その他の値ではうまく動作した。
FLANN_INDEX_LSH = 6
index_params= dict(algorithm = FLANN_INDEX_LSH,
table_number = 6,
key_size = 12,
multi_probe_level = 1)
2つ目の辞書は SearchParams である。これはインデックス内のツリーを再帰的に走査する回数を指定する。値が大きいほど精度は良くなるが、その分時間もかかる。値を変更したい場合は search_params = dict(checks=100) を渡す。
この情報があれば、準備は完了である。
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
img1 =
cv.imread(
'box.png',cv.IMREAD_GRAYSCALE)
img2 =
cv.imread(
'box_in_scene.png',cv.IMREAD_GRAYSCALE)
sift = cv.SIFT_create()
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)
matches = flann.knnMatch(des1,des2,k=2)
matchesMask = [[0,0] for i in range(len(matches))]
for i,(m,n) in enumerate(matches):
if m.distance < 0.7*n.distance:
matchesMask[i]=[1,0]
draw_params = dict(matchColor = (0,255,0),
singlePointColor = (255,0,0),
matchesMask = matchesMask,
flags = cv.DrawMatchesFlags_DEFAULT)
img3 = cv.drawMatchesKnn(img1,kp1,img2,kp2,matches,None,**draw_params)
plt.imshow(img3,),plt.show()
Flann-based descriptor matcher.
Definition features.hpp:1260
以下の結果を参照のこと。
image