勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる-19. 特徴点のマッチング

今回は特徴点のマッチングです。
今までにやった特徴点検出、特徴量生成を元に、2つの画像を比較、同じものが写っている点を探す、 というような感じです。
2つの画像というのは、具体的にはステレオカメラで撮影した画像だったり、ウォーリーを探せのウォーリーとウォーリーが隠れている絵のようなものになるかと。

OpenCV: Feature Matching

特徴点のマッチング — OpenCV-Python Tutorials 1 documentation

概要

今回のチュートリアルでは、2種類のマッチング手法が紹介されています。

  • 総当たりマッチング
    • 2画像の特徴点の組み合わせ全てを調べて、最も類似性の高いものを一致した点、とする
    • データ量が大きくなると計算量が増大する
  • FLANN(Fast Library for Approximate Nearest Neighbor)ベースのマッチング
    • 近似的に(特徴量空間での)最近傍点を探索する

あとはratio testというのが紹介されています。以下のページに少し詳細が書かれていますが、 最近傍点との距離、その次の近傍点との距離の比を取って、これが一定の値以下であれば良いマッチング結果として採択する、というような感じです。

OpenCV: Feature Matching with FLANN

実際に使ってみながら説明していきます。

使用画像

今まで使っていた春のパン祭り画像だと、似たようなパターンが複数あって1対1のマッチングは難しそう。ということで、別の画像を使います。

使うのは、フランスのシャルトル大聖堂の写真です。南西側の正面から見ると、左右に塔が立っていますが、向かって右側は12世紀ごろ建造でロマネスク様式、左側は16世紀ごろの後期ゴシック様式になっています。

f:id:nokixa:20210824004145j:plain:w400

フランスの観光名所「シャルトル大聖堂」の見どころ紹介 | 地球の歩き方 ニュース&レポート

今回は、シャルトル大聖堂の両側の塔を撮影した画像、片側ずつ撮影した画像を使って、異なる画角、カメラの向きで撮影した画像のマッチングができるかやってみます。

img_both = cv2.imread('Chartres_both.JPG')
img_right = cv2.imread('Chartres_right.JPG')
img_left = cv2.imread('Chartres_left.JPG')
img_both = cv2.resize(img_both, None, fx=0.25, fy=0.25, interpolation=cv2.INTER_AREA)
img_right = cv2.resize(img_right, None, fx=0.25, fy=0.25, interpolation=cv2.INTER_AREA)
img_left = cv2.resize(img_left, None, fx=0.25, fy=0.25, interpolation=cv2.INTER_AREA)
cv2.imshow('Image both', img_both)
cv2.imshow('Image right', img_right)
cv2.imshow('Image left', img_left)

f:id:nokixa:20210822102202p:plain:w400
シャルトル大聖堂 正面 両サイド

f:id:nokixa:20210822102317p:plain:w400
シャルトル大聖堂 向かって右側

f:id:nokixa:20210822102445p:plain:w400
シャルトル大聖堂 向かって左側

各画像の撮影条件は以下の通りです。

  • 解像度はいずれも3000x4000 → 1/4に縮小してから使っています。
  • 両サイド画像: シャッタ1/500, f3.2, 6.169mm
  • 向かって左側画像: シャッタ1/200, f3.5, 13.926mm
  • 向かって右側画像: シャッタ1/400, f3.5, 13.926mm

片側ずつの画像の方はズームして撮影しています。これで拡大縮小に対しての頑健性を見ることができます。

SIFTでの特徴点検出、特徴量計算、マッチング

まずはSIFTを使います。
特徴点検出して、画像に表示してみます。

sift = cv2.SIFT_create()
kp_both = sift.detect(img_both, None)
kp_right = sift.detect(img_right, None)
kp_left = sift.detect(img_left, None)
img_kp_both = cv2.drawKeypoints(img_both, kp_both, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
img_kp_right = cv2.drawKeypoints(img_right, kp_right, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
img_kp_left = cv2.drawKeypoints(img_left, kp_left, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)

cv2.imshow('KeyPoints both', img_kp_both)
cv2.imshow('KeyPoints right', img_kp_right)
cv2.imshow('KeyPoints left', img_kp_left)

f:id:nokixa:20210822134331p:plain

f:id:nokixa:20210822134551p:plain

f:id:nokixa:20210822171054p:plain

やっぱりロマネスク様式の塔とゴシック様式の塔を比べると、ゴシック様式のほうが複雑で特徴点が多い。

cv2.BFMatcher()で総当たりマッチングをやってみます。 まずは 右側塔のマッチングで様子見。

kp_both, des_both = sift.compute(img_both, kp_both)
kp_right, des_right = sift.compute(img_right, kp_right)
kp_left, des_left = sift.compute(img_left, kp_left)

bf = cv2.BFMatcher()
matches_right = bf.match(des_both, des_right)
matches_right = sorted(matches_right, key = lambda x:x.distance)

img_matches_right = cv2.drawMatches(img_both, kp_both, img_right, kp_right, matches_right[:20], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches right', img_matches_right)

f:id:nokixa:20210822164950p:plain

思ったよりかなりいい感じでマッチングしています。
間違っているのは1ペアぐらいでは?

左側塔でもマッチングします。

matches_left = bf.match(des_both, des_left)
matches_left = sorted(matches_left, key = lambda x:x.distance)
img_matches_left = cv2.drawMatches(img_both, kp_both, img_left, kp_left, matches_left[:20], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches left', img_matches_left)

f:id:nokixa:20210822170824p:plain

こちらもいい感じのマッチング。
塔の上のほうのイボイボなんかはちゃんとマッチしています。

ORBで特徴点検出、マッチング

ORBでもやってみます。

cv2.drawKeypoints()関数でflags引数をDRAW_RICH_KEYPOINTSに設定すると訳が分からない感じになってしまったので、デフォルトのflagsにします。

img_kp_both = cv2.drawKeypoints(img_both, kp_both, None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv2.imshow('KeyPoints both', img_kp_both)

f:id:nokixa:20210822235144p:plain:w300

以下はORBでの特徴点検出、特徴量計算を行うコードです。

orb = cv2.ORB_create()
kp_both, des_both = orb.detectAndCompute(img_both, None)
kp_right, des_right = orb.detectAndCompute(img_right, None)
kp_left, des_left = orb.detectAndCompute(img_left, None)

img_kp_left = cv2.drawKeypoints(img_left, kp_left, None, flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT)
img_kp_right = cv2.drawKeypoints(img_right, kp_right, None, flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT)
img_kp_both = cv2.drawKeypoints(img_both, kp_both, None, flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT)

cv2.imshow('KeyPoints both', img_kp_both)
cv2.imshow('KeyPoints right', img_kp_right)
cv2.imshow('KeyPoints left', img_kp_left)

f:id:nokixa:20210822235513p:plain

f:id:nokixa:20210822235646p:plain

f:id:nokixa:20210822235825p:plain

やっぱりSIFTのときと同様に、左側塔にはたくさんの特徴点が検出されますが、右側塔では少ない… 特に屋根の直線部分なんかはつるっとしてます。

総当たりマッチングをやってみます。
ORBでは、特徴量の距離はハミング距離を使います。また、チュートリアルに従うと、クロスチェックも使っていました。

また右側塔から試します。

bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)

matches_right = bf.match(des_both, des_right)
matches_right = sorted(matches_right, key = lambda x:x.distance)
img_matches_right = cv2.drawMatches(img_both, kp_both, img_right, kp_right, matches_right[:20], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches right', img_matches_right)

f:id:nokixa:20210823000933p:plain

目立つ誤認識が一つありますが、ORBもまあまあいい結果が出てるかな。

とりあえず左側もマッチングを。

matches_left = bf.match(des_left, des_both)
matches_left = sorted(matches_left, key = lambda x:x.distance)
img_matches_left = cv2.drawMatches(img_left, kp_left, img_both, kp_both, matches_left[:20], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches left', img_matches_left)

f:id:nokixa:20210823001524p:plain

こちらも誤認識がありますが、まあまあな結果かと。

ORBでratio testを使ってみる

ORBでの結果改善を目指してratio testをやってみようとしましたが、 元のBFMatcherオブジェクトではエラーが出ました。

>>> matches_right = bf.knnMatch(des_both, des_right, k=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
cv2.error: OpenCV(4.5.3) C:\Users\runneradmin\AppData\Local\Temp\pip-req-build-_xlv4eex\opencv\modules\core\src\batch_distance.cpp:303: error: (-215:Assertion failed) K == 1 && update == 0 && mask.empty() in function 'cv::batchDistance'

crosscheckknnMatchは併用できないようです。
crosscheckをするということはお互いにベストマッチとなっているペアのみマッチ結果として採用するということで、これとknnMatchを同時に使うということは矛盾するため。

Python 3.x - python 特徴量マッチングde|teratail

crosscheckをオフにして続行します。

bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
matches_right = bf.knnMatch(des_both, des_right, k=2)
good_matches_right = []
for m,n in matches_right:
    if m.distance < 0.75*n.distance:
        good_matches_right.append([m])

ちなみに、knnMatchでは2つのDMatchオブジェクトのリストを要素とするリストが返ってきました。

>>> print(type(matches_right))
<class 'list'>
>>> print(len(matches_right))
500
>>> print(type(matches_right[0]))
<class 'list'>
>>> print(len(matches_right[0]))
2
>>> print(type(matches_right[0][0]))
<class 'cv2.DMatch'>

結果を表示します。

img_matches_right = cv2.drawMatchesKnn(img_both, kp_both, img_right, kp_right, good_matches_right[:20], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches right (ratio test)', img_matches_right)

f:id:nokixa:20210823083904p:plain

結果が良くなったような感じがしますが、誤認識がまだ一つ存在します。
左側もやります。

matches_left = bf.knnMatch(des_left, des_both, k=2)
good_matches_left = []
for m,n in matches_left:
    if m.distance < 0.75*n.distance:
        good_matches_left.append([m])
img_matches_left = cv2.drawMatchesKnn(img_left, kp_left, img_both, kp_both, good_matches_left, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches left (ratio test)', img_matches_left)

f:id:nokixa:20210823084031p:plain

こちらも誤認識があって目立ちますが、まあまあな結果かな。

ちょっと気になる点

気になる点があるので確かめておきます。

  • SIFT、ORBの入力画像はカラー、白黒で変わるのか?
  • drawMatchesでのflags引数を変えるとどうなるのか?

まずはSIFTで。

img_both_gray = cv2.cvtColor(img_both, cv2.COLOR_BGR2GRAY)
img_right_gray = cv2.cvtColor(img_right, cv2.COLOR_BGR2GRAY)
kp_both, des_both = sift.detectAndCompute(img_both_gray, None)
kp_right, des_right = sift.detectAndCompute(img_right_gray, None)
bf = cv2.BFMatcher()
matches_right = bf.match(des_both, des_right)
matches_right = sorted(matches_right, key = lambda x:x.distance)
img_matches_right = cv2.drawMatches(img_both_gray, kp_both, img_right_gray, kp_right, matches_right[:20], None, flags=cv2.DrawMatchesFlags_DEFAULT)
cv2.imshow('Matches right (gray)', img_matches_right)

f:id:nokixa:20210824234614p:plain

  • SIFTではグレースケール化してもマッチング結果は特に変わっていない感じです。
  • flags引数をデフォルトのものにすると、特徴点がすべて表示されました。
kp_both, des_both = orb.detectAndCompute(img_both_gray, None)
kp_right, des_right = orb.detectAndCompute(img_right_gray, None)
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
matches_right = bf.match(des_both, des_right)
matches_right = sorted(matches_right, key = lambda x:x.distance)
img_matches_right = cv2.drawMatches(img_both_gray, kp_both, img_right_gray, kp_right, matches_right[:20], None, flags=cv2.DrawMatchesFlags_DEFAULT)
cv2.imshow('Matches right (gray)', img_matches_right)

f:id:nokixa:20210825001022p:plain

  • ORBでも、カラー画像と白黒画像で同じ結果が得られています。

以上

ちゃんとマッチングできるものか半信半疑でしたが、思ったよりきっちりマッチングしてくれました。

長くなってしまったので、FLANNを使ったマッチングは次に回します。