Warning: Undefined variable $position in /home/pystyles/pystyle.info/public_html/wp/wp-content/themes/lionblog/functions.php on line 4897

OpenCV – 特徴点マッチングを行う方法について

OpenCV – 特徴点マッチングを行う方法について

概要

OpenCV で特徴点マッチングを行う方法について、解説します。
特徴点マッチングを利用することで、物体の位置の検出などに応用できます。

Advertisement

特徴点検出

画像のうち、追跡、比較に利用することができる部分を特徴 (feature / keypoint) といい、画像から特徴を検出することを特徴検出 (feature detection) といいます。

OpenCV での特徴検出を行う方法

In [1]:
import cv2
from IPython.display import Image, display


def imshow(img):
    """ndarray 配列をインラインで Notebook 上に表示する。
    """
    ret, encoded = cv2.imencode(".jpg", img)
    display(Image(encoded))

特徴点検出器を作成する

まず、画像から特徴を検出を行う特徴検出器を作成します。 OpenCV では、以下の関数で特徴検出器を作成できます。

In [2]:
# OBR 特徴検出器を作成する。
detector = cv2.ORB_create()
Advertisement

特徴点及び特徴量記述子を計算する

作成した特徴検出器の detect() で画像から特徴点 (feature keypoints) を検出できます。

keypoints = cv2.Feature2D.detect(image[, mask])
  • 引数
    • image: 画像
    • mask: マスク画像
  • 返り値
    • keypoints: 特徴点を表す KeyPoint オブジェクトのリスト

検出した特徴点のリストは cv2.drawKeypoints() で画像に描画して確認できます。

sample1.jpg

In [3]:
# 画像を読み込む。
img = cv2.imread("sample1.jpg")

# 特徴点を検出する。
kp = detector.detect(img)

# 特徴点を描画する。
dst = cv2.drawKeypoints(img, kp, None)
imshow(dst)

特徴点が検出できたら、compute() でその特徴点の特徴記述子 (feature descriptor) を計算します。

keypoints, descriptors = cv2.Feature2D.compute(images, keypoints)
  • 引数
    • images: 画像
    • keypoints: 特徴点を表す KeyPoint オブジェクトのリスト。
  • 返り値
    • keypoints: 特徴点を表す KeyPoint オブジェクトのリスト。引数に渡した特徴点のリストからいくつかの特徴点が削除または追加されている場合があります。
    • descriptors: 形状が (特徴点の数, 特徴量記述子の次元数) の numpy 配列。各特徴点に対応する特徴記述子。
In [4]:
# 各特徴点の特徴量記述子を計算する。
kp, desc = detector.compute(img, kp)
print(len(kp), desc.shape)
481 (481, 32)

detectAndCompute() を使うと、特徴点の検出と特徴量記述子の計算を一度に行えます。 返り値は (特徴点の一覧, 各特徴点の特徴量記述子) のタプルになります。

keypoints, descriptors = cv2.Feature2D.detectAndCompute(image, mask)
  • 引数
    • images: 画像。
    • mask: マスク画像。
  • 返り値
    • keypoints: KeyPoint オブジェクトのリスト。特徴点の一覧。
    • descriptors: 形状が (特徴点の数, 特徴量記述子の次元数) の numpy 配列。各特徴点に対応する特徴記述子。
In [5]:
# 特徴点を検出する。
kp, desc = detector.detectAndCompute(img, None)
print(len(kp), desc.shape)
481 (481, 32)

OpenCV での特徴検出を行う方法

2つの画像に対して、特徴点の検出及び特徴量記述子の計算を行い、類似度が高い特徴点同士をマッチングすることを特徴点マッチング (feature matching) といいます。

OpenCV では、次の2種類のマッチング器が提供されています。

  • cv2.BFMatcher: 総当りによるマッチング (Brute Force Matching) を行う。
  • cv2.FlannBasedMatcher 近似近傍探索手法 Flann によるマッチングを行う。

今回は、総当りによるマッチングが行える cv2.BFMatcher を紹介します。

sample1.jpg

sample2.jpg

まず、2つの画像から特徴点及び特徴記述子をそれぞれ抽出します。

In [6]:
# 画像を読み込む。
img1 = cv2.imread("sample1.jpg")
img2 = cv2.imread("sample2.jpg")

# 特徴点及び特徴量記述子を検出する。
kp1, desc1 = detector.detectAndCompute(img1, None)
kp2, desc2 = detector.detectAndCompute(img2, None)

最近傍マッチングを行なう

マッチング器 cv2.BFMatcher オブジェクトを作成し、2つの画像の特徴記述子を BFMatcher.match() に渡して、マッチングを行います。

matches = cv2.DescriptorMatcher.match(queryDescriptors, trainDescriptors[, mask])
  • 引数
    • queryDescriptors: クエリ用の特徴記述子
    • trainDescriptors: 学習用の特徴記述子
    • mask: マスク画像
  • matches: マッチング情報を表す DMatch オブジェクトのリスト。
In [7]:
# マッチング器を作成する。
bf = cv2.BFMatcher(cv2.NORM_HAMMING)

# マッチングを行う。
matches = bf.match(desc1, desc2)
print(len(matches))
481

DMatch オブジェクトは、次の属性を持っています。

  • distance: 特徴量記述子の距離
  • queryIdx: クエリ記述子 (match(desc1, desc2) と渡した場合、desc1 のインデックス)
  • trainIdx: 学習記述子 (match(desc1, desc2) と渡した場合、desc2 のインデックス)

matches の最初の DMatch オブジェクトを見てみましょう。

In [8]:
m = matches[0]

print(f"distance: {m.distance}, trainIdx: {m.trainIdx}, queryIdx: {m.queryIdx}")
distance: 3.0, trainIdx: 15, queryIdx: 0

これは img1 の特徴点 kp1[0] と最も距離が近い img2 の特徴点は kp2[15] であることを表しています。 また、その2つの特徴点の特徴量記述子同士の距離が3であることを表しており、この距離が近いほど、2つの特徴点は似ていると解釈できます。

In [9]:
# 特徴点の対応関係
query_pt, train_pt = kp1[m.queryIdx], kp2[m.trainIdx]
# 特徴量記述子の対応関係
query_desc, train_desc = desc1[m.queryIdx], desc2[m.trainIdx]

match() による特徴点マッチングの結果は、cv2.drawMatches() で可視化できます。

In [10]:
# マッチング結果を描画する。
dst = cv2.drawMatches(img1, kp1, img2, kp2, matches, None)
imshow(dst)
Advertisement

k 近傍法によるマッチング

BFMatcher.knnMatch()k近傍法によるマッチングも行えます。 この関数は、queryDescriptors と距離が近い上位 $k$ 個の特徴点を trainDescriptors から探します。 返り値は、各要素がマッチング情報 DMatch オブジェクトである形状が (特徴点の数, k) のリストとなっています。

In [11]:
import cv2

img1 = cv2.imread("sample1.jpg")
img2 = cv2.imread("sample2.jpg")

# OBR 特徴量検出器を作成する。
detector = cv2.ORB_create()

# 特徴点を検出する。
kp1, desc1 = detector.detectAndCompute(img1, None)
kp2, desc2 = detector.detectAndCompute(img2, None)

# マッチング器を作成する。
bf = cv2.BFMatcher(cv2.NORM_HAMMING)

# マッチングを行う。
matches = bf.knnMatch(desc1, desc2, k=2)

matches の最初の DMatch オブジェクトを見てみましょう。

In [12]:
m = matches[0]
print(m)

for n in m:
    # 特徴点の対応関係
    query_pt, train_pts = kp1[n.queryIdx], kp2[n.trainIdx]
    # 特徴量記述子の対応関係
    query_desc, train_desc = desc1[n.queryIdx], desc2[n.trainIdx]

    print(f"distance: {n.distance}, trainIdx: {n.trainIdx}, queryIdx: {n.queryIdx}")
[<DMatch 0x7f57a43ad710>, <DMatch 0x7f576871be30>]
distance: 3.0, trainIdx: 15, queryIdx: 0
distance: 17.0, trainIdx: 9, queryIdx: 0

これは img1 の特徴点 kp1[0] と1番目、2番目に距離が近い img2 の特徴点はそれぞれ kp2[15], kp2[9] であることを表しています。

knnMatch() による特徴点マッチングの結果は、cv2.drawMatchesKnn() で可視化できます。

In [13]:
# マッチング結果を描画する。
dst = cv2.drawMatchesKnn(img1, kp1, img2, kp2, matches, None)
imshow(dst)

信頼性の低いマッチング結果を除く

マッチング結果から信頼性の低いものを除く方法を2つ紹介します。

クロスチェック

crossCheck=True とした場合、マッチングの際にクロスチェックを行います。 queryDescriptors と最も距離が近い特徴点を trainDescriptors から探すのと同様、 trainDescriptors と最も距離が近い特徴点を queryDescriptors から探して、結果が両者で一致した場合のみマッチングしたと判定します。

同じ距離の特徴量が複数ある場合は、訓練集合とテスト集合を入れ替えた際のマッチング結果が変わってくるため、クロスチェックを有効にした場合、このようなマッチングは信頼性に欠けるので除外します。

この方法は、最近傍探索を行う場合 (BFMatcher.match() または BFMatcher.knnMatch(k=1)) のみ有効です。 $k >= 2$ で実行した場合、error: (-215:Assertion failed) K == 1 && update == 0 && mask.empty() in function 'batchDistance' とエラーになります。

In [14]:
import cv2

img1 = cv2.imread("sample1.jpg")
img2 = cv2.imread("sample2.jpg")

# OBR 特徴量検出器を作成する。
detector = cv2.ORB_create()

# 特徴点を検出する。
kp1, desc1 = detector.detectAndCompute(img1, None)
kp2, desc2 = detector.detectAndCompute(img2, None)

# マッチング器を作成する。
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)

# マッチングを行う。
matches = bf.match(desc1, desc2)
        
# マッチング結果を描画する。
dst = cv2.drawMatches(img1, kp1, img2, kp2, matches, None)
imshow(dst)
Advertisement

レシオテスト

レシオテスト (ratio test)SIFT 特徴量の論文 で紹介されている方法です。 この方法は BFMatcher.knnMatch(k=2) で2近傍探索によるマッチングを行なう場合に最も近い距離と2番目に近い距離の比率が閾値以上のマッチング結果のみを残す方法です。 1番目に近い距離と2番目に近い距離の差があまりない場合、信頼性に欠けるので除外します。

$$ \frac{\text{1番目に近い距離}}{\text{2番目に近い距離}} < \text{threshold} $$
In [15]:
import cv2

img1 = cv2.imread("sample1.jpg")
img2 = cv2.imread("sample2.jpg")

# OBR 特徴量検出器を作成する。
detector = cv2.ORB_create()

# 特徴点を検出する。
kp1, desc1 = detector.detectAndCompute(img1, None)
kp2, desc2 = detector.detectAndCompute(img2, None)

# マッチング器を作成する。
bf = cv2.BFMatcher(cv2.NORM_HAMMING)

# マッチングを行う。
matches = bf.knnMatch(desc1, desc2, k=2)

# レシオテストを行う。
good_matches = []
thresh = 0.7
for first, second in matches:
    if first.distance < second.distance * thresh:
        good_matches.append(first)
        
# マッチング結果を描画する。
dst = cv2.drawMatches(img1, kp1, img2, kp2, good_matches, None)
imshow(dst)

距離関数

特徴量記述子の種類によって、適した距離関数が変わってきます。 リファレンスによると、特徴量記述子によって以下のように使い分けるとよいそうです。

  • cv2.NORM_L1, cv2.NORM_L2: SIFT、SURF
  • cv2.NORM_HAMMING: ORB, BRISK, BRIEF, AKAZE
  • cv2.NORM_HAMMING2: パラメータ WTA_K を3または4に設定した ORB