勉強しないとな~blog

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

OpenCVやってみる-21. 特徴点マッチングとHomography

今回もだいたいチュートリアル通りの内容です。
前々回前回で、違う視点から同じ対象を撮影した2つの画像での点の対応を見つけることができました。
この結果を使って、射影変換の行列を計算し、画像の視点変換をすることができます。これをやってみます。

OpenCV: Feature Matching + Homography to find Objects

特徴点のマッチングとHomographyによる物体検出 — OpenCV-Python Tutorials 1 documentation

使用画像

前々回、前回と同じシャルトル大聖堂の画像3つです。

f:id:nokixa:20210822102202p:plain:w200 f:id:nokixa:20210822102317p:plain:w200 f:id:nokixa:20210822102445p:plain:w200

特徴点マッチング

前々回にやったのと同じことをやります。
SIFTでのマッチングが結果が良かったので、SIFTで特徴点検出します。

import cv2
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)

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

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

射影変換行列計算

cv2.fincHomography()関数で射影変換行列を計算することができます。
チュートリアルを読むと、以下のようなことが書いてあります。

  • 射影変換行列を求めるには、最低4点のマッチング点が必要
  • 誤ったマッチングペアがあると結果に悪影響を与える
  • これに対処するため、cv2.fincHomography()関数ではRANSACかLEAST_MEDIANのアルゴリズムを使って、各入力のペアについて、良いマッチングかどうか判定します。その結果もこの関数の返り値として得られます。
    良いマッチングのことをinlier、それ以外をoutlierと言うようです。

チュートリアルでは、10点以上のマッチングペアを使用するようにしていました。

シャルトル大聖堂の画像でいうと、前々回やった感じでは、マッチングペアを一致度の高い(特徴量の距離の小さい)順に並べて20個取ると、ほぼ正しいマッチング結果となっていました。
本来はratio testでマッチングペアをふるいにかけてからcv2.findHomography()関数に投げたほうが良いようですが、今回はその必要なしかなと。
ということで、この20個を使って射影変換行列を探したいと思います。

src_pts_for_right = np.float32([kp_both[m.queryIdx].pt for m in matches_right[:20]]).reshape(-1,1,2)
dst_pts_right = np.float32([kp_right[m.trainIdx].pt for m in matches_right[:20]]).reshape(-1,1,2)
M_right, mask_right = cv2.findHomography(src_pts_for_right, dst_pts_right, cv2.RANSAC, 5.0)

マッチング結果のオブジェクト(DMatchオブジェクト)のqueryIdxtrainIdxBFMatcher.match()に与える2つの特徴量セットのうちそれぞれ1つ目、2つ目に対応します。
ちなみに、全ポイントinlierとなったようでした。

>>> print(mask_right)
[[1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]
 [1]]

画像の視点変換

チュートリアルでは、物体を探すというのを目標にしていますが、ここでは画像の視点変換をしようと思っているので、前に一度使ったcv2.warpPerspective()を使ってみたいと思います。

OpenCVやってみる-4. 射影変換 - 勉強しないとな~blog

元の右側画像と、両側画像から変換した画像を比較しましたが、いい感じになっているかと。

img_both_converted = cv2.warpPerspective(img_both, M_right, (750, 1000))
img_right_comp = np.hstack((img_right, img_both_converted))
cv2.imshow('Image compare', img_right_comp)

f:id:nokixa:20210905023426p:plain

以下を参考に画像を重畳してみましたが、一致度が高すぎてなんだかよく分からず。

Python OpenCV Overlaying or Blending Two Images

img_right_blended = cv2.addWeighted(img_right, 0.5, img_both_converted, 0.5, 0)
cv2.imshow('Image compare(Blended)', img_right_blended)

f:id:nokixa:20210905023804p:plain:w400

両側画像を変換して右側画像の視点で見てみようとしましたが、さらに右側に拡張されるだけだったので、拡張領域は真っ黒に。

img_right_extended = cv2.warpPerspective(img_both, M_right, (750*2, 1000))
cv2.imshow('Image right extended', img_right_extended)

f:id:nokixa:20210905022642p:plain

なんとかできないかと考えてみましたが、変換行列を作るときに入力座標にオフセットをつければどうかな、と。

dst_pts_right_offset = dst_pts_right
for i in range(20):
    dst_pts_right_offset[i,0,:] += [750,0]

M_right_offset, mask_right_offset = cv2.findHomography(src_pts_for_right, dst_pts_right_offset, cv2.RANSAC, 5.0)
img_right_extended = cv2.warpPerspective(img_both, M_right_offset, (750*2, 1000))
cv2.imshow('Image right extended', img_right_extended)

f:id:nokixa:20210905025430p:plain

いけた!
左側は特に何もしなくてもよさそうかな。

dst_pts_left = np.float32([kp_left[m.queryIdx].pt for m in matches_left[:20]]).reshape(-1,1,2)
M_left, mask_left = cv2.findHomography(src_pts_for_left, dst_pts_left, cv2.RANSAC, 5.0)

f:id:nokixa:20210905030217p:plain

img_left_extended = cv2.warpPerspective(img_both, M_left, (750*2, 1000))
cv2.imshow('Image left extended', img_left_extended)

f:id:nokixa:20210905031506p:plain

以上

今回もいい感じの結果が得られました。
カメラ画像の視点変換は自由自在です。

次回の内容は検討中…

OpenCVやってみる-20. 特徴点のマッチング(FLANN)

前回のやり残しで、FLANNでの特徴点マッチングをやります。

OpenCV: Feature Matching

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

概要

前回のおさらいですが、FLANNはFast Library for Approximate Nearest Neighborの略で、近似的に特徴量空間での最近傍点を探索する手法です。総当たりマッチングより高速で実施することができます。

やること

前回はcv2.BFMatcher()でマッチングしましたが、今回はcv2.FlannBasedMatcher()を使います。

これは2つのdict型オブジェクトを引数に取ります。
dictが何なのか分かっていないのでググる

Pythonで辞書を作成するdict()と波括弧、辞書内包表記 | note.nkmk.me

dictは辞書オブジェクトで、キーと値をペアで保持するものとのこと。
キー、値の型はある程度制限はあるものの、文字列および整数でないといけない、ということはなさそう。

【Python】 辞書(dict)の使い方の基本 | Hbk project

【Python】辞書型変数(dictionary) - Qiita

なぜcv2.FlannBasedMatcher()の引数をdict型で与えないといけないかはわかりませんが、とりあえず使ってみます。

使用画像

前回と同じシャルトル大聖堂です。

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
シャルトル大聖堂 向かって左側

実施

SIFTで特徴点マッチングして、前回の総当たりマッチングと比較します。

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

FLANNのオブジェクトは、SIFTを使う場合は以下のように生成します。

FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)

index_paramsアルゴリズムの選択、および関連パラメータのよう。 使用する特徴点検出器によって変えるといいようです。今回のSIFTでは1を選択していますが、ORBだと6(FLANN_INDEX_LSH)が良いようです。

search_paramsは、「インデックス中の木構造再帰的にたどっていく回数」とのこと。 これを増やすと精度が上がりますが、実行時間が伸びます。

あとはマッチングを実施しますが、チュートリアルではratio testをやっていました。
今回はこれに倣います。

matches_right = flann.knnMatch(des_both, des_right, k=2)
good_matches_right = []
for m,n in matches_right:
    if m.distance < 0.7*n.distance:
        good_matches_right.append([m])

最後に結果を表示します。

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', img_matches_right)

f:id:nokixa:20210828014728p:plain

結果は前回より悪くなってしまった…
シャルトル大聖堂向かって左側は、

matches_left = flann.knnMatch(des_left, des_both, k=2)
good_matches_left = []
for m,n in matches_left:
    if m.distance < 0.7*n.distance:
        good_matches_left.append([m])

img_matches_left = cv2.drawMatchesKnn(img_left, kp_left, img_both, kp_both, good_matches_left[:20], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow('Matches left', img_matches_left)

f:id:nokixa:20210828015435p:plain

こちらはまだ上の結果よりましですが、1つ明らかな誤検出があるので、前回と比べれば精度は落ちています。

扱う画像ごとにパラメータを調整したりしないといけないのか?

以上

前回総当たりマッチングをやっていましたが、その際は特に体感的に処理が遅い、ということはなく、実行してすぐに終わっていました。なのでFLANNのメリットを得ることができず。

次はチュートリアル通りで、特徴点マッチングとHomographyでの物体検出にします。

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を使ったマッチングは次に回します。

OpenCVやってみる-18. ORBによる特徴点検出、特徴量記述

前回の予告通り、ORBをやってみます。

OpenCV: ORB (Oriented FAST and Rotated BRIEF)

ORB (Oriented FAST and Rotated BRIEF) — OpenCV-Python Tutorials 1 documentation

ざっくり紹介、理論

  • SIFTやSURFは特許を取られていて自由に使えない
  • その代替としてOpenCV Labsで考案された
  • 基本的にはFASTによる特徴点検出と、BRIEFによる特徴量記述の組み合わせ、ただし以下の改良がある
    • FAST自体は回転を考慮しない、ORBでは特徴点の回転角を計算する
    • BRIEFでの特徴量は回転不変性を持たない、ORBでは上記で計算した回転角に合わせて特徴量を回転させる
      • BRIEF特徴量に回転を加えると、以下のデメリットが出てしまう
        • 各ビットの平均、分散が望ましい形(大きい分散、平均0.5)にならない
        • 各画素ペアの比較(binary test)結果に相関が生まれる
      • ORBでは可能なbinary testの組み合わせをすべて試して、最も良い性質を持つものを選択する(rBRIEF)
      • 特徴量の比較にはmulti-probe LSHが使用される

実践

流れはSIFT、SURF等と同じです。

画像としてはSIFTで使ったのと同じものを使います。

f:id:nokixa:20210706090007p:plain

ORBでもカラー画像の入力でいいよう。
まずは1つ目の画像だけで。

orb = cv2.ORB_create()
img1_kp = orb.detect(img1, None)
img1_kp, img1_des = orb.compute(img1, img1_kp)
img1_draw = cv2.drawKeypoints(img1, img1_kp, None, color=(0,255,0), flags=0)
cv2.imshow('Keypoints 1', img1_draw)

f:id:nokixa:20210817014250p:plain:w400

  • シールの点数の数字に特徴点が集中している、SIFTのときと比べると規則性は見える
  • 台紙のコーナーは1つしか特徴点とされていない…

残りの2画像でもやってみます。

img2_kp = orb.detect(img2, None)
img2_kp, img2_des = orb.compute(img2, img2_kp)
img2_draw = cv2.drawKeypoints(img2, img2_kp, None, color=(0,255,0), flags=0)
img3_kp = orb.detect(img3, None)
img3_draw = cv2.drawKeypoints(img3, img3_kp, None, color=(0,255,0), flags=0)

import numpy as np
img123_draw = np.hstack((img1_draw, img2_draw, img3_draw))
cv2.imshow('Keypoints 1-3', img123_draw)

f:id:nokixa:20210817014732p:plain

  • シールの点数数字に特徴点が集中するのはどれも同じ
  • 追加の2画像では、台紙コーナーが4つすべて検出されています。

参考に、SIFTでの結果を再掲します。

f:id:nokixa:20210713225333p:plain
SIFTでの結果

これと比べるとだいぶ見やすいですね。
このあたりの違いは実アプリケーションでどのアルゴリズムを使うか、の判断の基準になりそうです。

今回はデフォルトのパラメータ設定にしましたが、この調整の余地もあるかも。

以上

アルゴリズムごとの違いを見ることができました。

次回は、チュートリアル通りに「特徴点のマッチング」をやりたいと思います。

OpenCVやってみる-17. BRIEF

今回はBRIEFをやってみます。

OpenCV: BRIEF (Binary Robust Independent Elementary Features)

ざっくり理論

BRIEFは画像中の特徴点の特徴量を生成する方式のひとつです。

SIFTやSURFでは、特徴点の特徴量を表すのにそれなりに大きなデータ量(1点あたりSIFTで512byte、SURFで256byte)が必要になりますが、メモリサイズが大きくなるし、特徴点同士の比較演算も重くなる。

チュートリアルサイトの説明を読むと、「平滑化した画像パッチ」というのが書かれていますが、 注目する特徴点近傍の画素の集まり、という解釈でいいのかな。
特徴点近傍画素の中のn_d個の画素ペアについて、ペア内で輝度値を比較、比較結果に0、1を割り当ててn_dビットのビット列を作り、これを特徴量とするようです。

CPUのビットXORとビットカウントの命令を使用すると、この特徴量の比較は高速でできると。

n_dの値はOpenCVでは128、256、512がサポートされていて、1特徴点あたりの特徴量データはそれぞれ16byte、32byte、64byteとなります。

BRIEF自体は特徴点を検出するものではないので、SIFT、SURFなど何らかのアルゴリズムで特徴点を検出したのち、BRIEFで特徴量データを生成する、という流れになります。
ただ、BRIEFでは特徴点検出器としてはCenSurE(およびその派生版のSTAR)というのが推奨されているようです。

実践

今回はどんな特徴量データができるか確認するだけで、特に画像に重畳して表示したりすることはありません。なので、画像1つだけ使います。

import cv2
img1 = cv2.imread('harupan_200317_1.jpg')
img1 = cv2.resize(img1, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)

チュートリアルでは検出器としてSTARを使っています。
SIFTのときはグレースケール化してから検出器に入れていましたが、STARではカラー画像のままでいいよう。

なお、BRIEFを使うにはopencv contribがインストールされている必要があります。

star = cv2.xfeatures2d.StarDetector_create()
brief = cv2.xfeatures2d.BriefDescriptorExtractor_create()
img1_kp = star.detect(img1, None)
img1_kp, img1_des = brief.compute(img1, img1_kp)

結果を確認してみます。

>>> type(img1_kp)
<class 'list'>
>>> len(img1_kp)
382
>>> type(img1_kp[0])
<class 'cv2.KeyPoint'>
>>> type(img1_des)
<class 'numpy.ndarray'>
>>> img1_des.shape
(382, 32)
>>> img1_des[0,:]
array([ 27,   6, 248, 207, 179, 146,  86, 189, 164, 209, 146,  20, 219,
       106, 219,  41, 167, 228,  70, 140, 253, 240, 112,  60,  68, 148,
       229, 195, 126,  80,  65, 250], dtype=uint8)
  • BRIEFでの特徴量データはndarrayとして出てくる
  • 特徴量データの形状は(特徴点数, 32)、データ型はuint8となる
    • デフォルトではn_dは256なので、1特徴量あたり32byte

以上

特徴点のマッチングをまだやっていないので、今回の結果がどうなのかまだちゃんと評価できず。

次回はチュートリアル通りORBでの特徴点検出、特徴量記述で。

OpenCVやってみる-16. FASTアルゴリズムによる特徴点検出

前回の続き、FASTアルゴリズムを試します。

コーナー検出のためのFASTアルゴリズム — OpenCV-Python Tutorials 1 documentation

ざっくり理論

画像の各画素について、これを中心として周長16ピクセルになる円周(半径およそ3ピクセル) 上の画素を調べ、決まった数以上の連続した画素で中心輝度を上回っている、または下回っている、という状況であれば、そこをコーナーと判定するようです。

限られた計算資源でも高速にコーナー検出ができる、というのがメリットのよう。

高速テストというのがあり、まず円周上の4画素を調べて、コーナーの可能性がなければ 他の画素のチェックはしない、ということもやるようです。

機械学習(決定木)も使うように書かれていますが、OpenCVのライブラリでもやっているのか?
学習済みのものを使っているのか?

実践

前回までと同じ画像で試します。 f:id:nokixa:20210706090007p:plain

やってみたところ、SIFTのときと同様にcv2.FastFeatureDetector()ではなくcv2.FastFeatureDetector_create()を使わないといけませんでした。
ドキュメントのOpenCVのバージョンとは違いがあるのかと。 今見ているドキュメント自体も古くなっているようなので、次回からは更新されているチュートリアルも見ながら進めていこうかと思います。

OpenCV: OpenCV Tutorials

※8/15追記
上のチュートリアルはちょっと違う内容ですが、同じ内容のチュートリアルが別でちゃんとありました。

OpenCV: OpenCV-Python Tutorials

>>> fast = cv2.FastFeatureDetector()
>>> kp1 = fast.detect(img1, None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
cv2.error: Unknown C++ exception from OpenCV code
>>> fast = cv2.FastFeatureDetector_create()
>>> kp1 = fast.detect(img1, None)
>>> 

一応SIFTのときと同様に結果を一部確認。

>>> type(kp1)
<class 'list'>
>>> len(kp1)
3381
>>> type(kp1[0])
<class 'cv2.KeyPoint'>
>>> type(kp1[0].pt)
<class 'tuple'>
>>> kp1[0].pt
(75.0, 3.0)
>>> kp1[0].size
7.0
>>> kp1[0].angle
-1.0

特徴点について角度の情報はないということのよう。

特徴点を描画します。

img1_kp = cv2.drawKeypoints(img1, kp1, None, color=(255,0,0))
cv2.imshow('FAST Keypoints1', img1_kp)

f:id:nokixa:20210813082922p:plain

シールの境界が特徴点として認識されています。コーナー検出をするのが目的のアルゴリズムのようですが、この緩いカーブもコーナーとして認識されたということでいいのかな。

一応他の画像でもやっておきます。

kp2 = fast.detect(img2, None)
kp3 = fast.detect(img3, None)
img2_kp = cv2.drawKeypoints(img2, kp2, None, color=(255,0,0))
img3_kp = cv2.drawKeypoints(img3, kp3, None, color=(255,0,0))
import numpy as np
img123_kp = np.hstack((img1_kp, img2_kp, img3_kp))
cv2.imshow('FAST Keypoints', img123_kp)

f:id:nokixa:20210814005005p:plain

およそ同じような傾向。

以上

次はどうしようかな。

OpenCVやってみる-15. SURFによる特徴点検出

前回のSIFTに続いて、今度はSURFでの特徴点検出です。

SURF (Speeded-Up Robust Features)の導入 — OpenCV-Python Tutorials 1 documentation

SURFはSIFTに比べて高速化を目指したアルゴリズムとのこと。
発表は2006年になります。特許が取られていて、まだ終了していないようです。

画像処理の数式を見て石になった時のための、金の針 - Qiita

またもやざっくりまとめをすると、

  • SIFTでのLaplacian of Gaussianの代わりにBox filterを使ってのアルゴリズム積分画像を使って高速に計算ができる
  • 特徴点の回転角も計算する、6sx6sの領域の横、縦方向のwavelet係数を使うと書かれていますが、詳細はよくわからず。Wikipediaにはもう少し説明がありました。
    近傍(半径6s: sは特徴点を検出したスケール)の各点について縦、横方向のHaar waveletへの応答を計算、この応答を2次元空間にマッピングして、\pi/3の角度ごとに、その中に含まれる応答の総和を計算、最も大きいベクトルとなった角度が主要な回転角となる、
    という感じか?

Speeded up robust features - Wikipedia

実施

SIFTと同じ感じになります。と思いきや、エラーになりました。

>>> surf = cv2.SURF(400)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'cv2.cv2' has no attribute 'SURF'

今入れているOpenCVのままではだめなよう。opencv-contrib-pythonを入れる必要があります。

SIFT, SURFが利用できるPython用OpenCVをインストールする - はしくれエンジニアもどきのメモ

これを入れましたが、結局だめなようでした。

>>> surf = cv2.SURF(400)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'cv2.cv2' has no attribute 'SURF'
>>> surf = cv2.xfeatures2d.SURF_create(400)
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_contrib\modules\xfeatures2d\src\surf.cpp:1029: 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 'cv::xfeatures2d::SURF::create'

>>>

cv2.SURF()ではなくcv2.xfeatures2d.SURF_create()を使わないといけませんでした。

Python+OpenCV|SURF特徴量 | βshort Lab

ただ、それを使ってもライセンスの問題で使わせてくれないよう。
ビルドし直せばいいらしいですが、面倒なのでパスで。

以上

SURFはまたライセンスが切れたら試してみるということで。
残念。

次はFASTアルゴリズムでのコーナー検出をやります。