ちまたで話題のRPA(Robotic Process Automation)をpythonで自作することで、業務効率化はもちろん、必要に応じて内製でカスタマイズできるシステム作成を目指します。
今回は使えそうなライブラリ OpenCV の検証その(2)特徴点の検証をしていきます。
-- 実行環境 --
Windows 10 64ビット版(1903)、python 3.7.4
特徴点の検出とマッチング
OpenCV では画像の特徴点と呼ばれるポイントを検出し、その特徴点を比較/マッチングすることでテンプレートマッチング同様キャプチャ画像からターゲット画像の検出が可能です。
まずは下記のコントロールパネルの画像を使って特徴点の検出について検討してみます。

特徴点の抽出
まずは特徴点検出にA-KAZEを使ったサンプルです。
import cv2
# 画像ファイルの読み込み
img_cap = cv2.imread(r'D:\Sample-Source\control.png')
# 検出器生成/特徴点検出
detector = cv2.AKAZE_create()
keypoints = detector.detect(img_cap)
# 画像に特徴点書き込み/保存
img_result = cv2.drawKeypoints(img_cap, keypoints, None)
cv2.imwrite(r"D:\Sample-Source\result.png", img_result)
実行結果


おお、いっぱいマークついてますね!この〇ついてるところが特徴点ですか?

そうだよ。この点をマッチングに使用するんだ。ただこのままだと上下左右の外郭あたりに特徴点が出てないね。ちょっとカスタマイズが必要そうだよ。

ほんとだ、気づきませんでした。

画像の外周に白い枠を付加してみようか。
修正版
import cv2
# 外枠の幅
border_space = 30
# 画像ファイルの読み込み/外枠生成
img_cap = cv2.imread(r'D:\Sample-Source\control.png')
img_cap = cv2.copyMakeBorder(img_cap,border_space,border_space,border_space,border_space,cv2.BORDER_CONSTANT,value=[255,255,255])
# 検出器生成/特徴点検出
detector = cv2.AKAZE_create()
keypoints = detector.detect(img_cap)
# 画像に特徴点書き込み/保存
img_result = cv2.drawKeypoints(img_cap, keypoints, None)
cv2.imwrite(r"D:\Sample-Source\result.png", img_result)


画像の角まで特徴点が検出されていますね~^^
特徴点の検出方式と特徴点数
上のサンプルではA-KAZEを使用しましたが他の方式でどのぐらい特徴点が検出できるか試してみます。上記のサンプルコードの11行目「検出器生成」部分を変更するだけで変更でき、特徴点数はkeypoints の数を調べればわかります。
検出器 | コード(11行目) | 特徴点検出数 |
A-KAZE | cv2.AKAZE_create() | 1178 |
KAZE | cv2.KAZE_create() | 1241 |
AgastFeatureDetector | cv2.AgastFeatureDetector_create() | 2639 |
FAST | cv2.FastFeatureDetector_create() | 2601 |
MSER | cv2.MSER_create() | 37 |
BRISK | cv2.BRISK_create() | 4103 |
ORB | cv2.ORB_create() | 500 |
SimpleBlobDetector | cv2.SimpleBlobDetector_create() | 9 |
結果

別の画像で検証してみます。

検出器 | コード(11行目) | 特徴点検出数 |
A-KAZE | cv2.AKAZE_create() | 3343 |
KAZE | cv2.KAZE_create() | 3823 |
AgastFeatureDetector | cv2.AgastFeatureDetector_create() | 5607 |
FAST | cv2.FastFeatureDetector_create() | 5735 |
MSER | cv2.MSER_create() | 280 |
BRISK | cv2.BRISK_create() | 8248 |
ORB | cv2.ORB_create() | 501 |
SimpleBlobDetector | cv2.SimpleBlobDetector_create() | 31 |

検出数に結構差が出ましたね。

そうだね。方式によって得意な画像や処理速度など差があるんだ。今回ターゲットとしているデスクトップでの業務効率化で使用される平面の画像ではBRISK、A-KAZE、AgastFeatureDetector、FASTあたりがよさそうだね。とりあえず当面はサンプルをよく見かけるA-KAZEを使用することにしよう。
対象の画像サイズの影響を確認
特徴点を検出する場合、画像サイズをより大きくした場合の特徴点の変化を見てみたいと思います。対象は前回使用したアイコン画像を使用、外周の余白ありで検出しています。

画像の倍率 | 特徴点検出数 |
×1倍 | 28 |
×5倍 | 228 |
×10倍 | 444 |
×50倍 | 631 |
×100倍 | 456 |

最後検出数減ってますね。

そうだね。それに上には書いてないけど元があの小さいアイコン画像でも処理時間が結構かかっているんだ。単純に画像サイズを拡大すればいいというものではないってことだね。まだ試している途中だけど大体2~3倍程度で十分じゃないかな。
マッチング
特徴点の検出が出来たら次はマッチングです。マッチングを行うそれぞれの画像の特徴点を検出し、それを比較器にかけます。下記にDocumentを参考に作成したソースを記載します。
import cv2
border_space = 30
mag_rate = 3
# 画像ファイルの読み込み/外枠生成
img_cap = cv2.imread(r'D:\Sample-Source\capture.png')
img_cap = cv2.resize(img_cap, None, fx=mag_rate, fy=mag_rate)
img_cap = cv2.copyMakeBorder(img_cap,border_space,border_space,border_space,border_space,cv2.BORDER_CONSTANT,value=[255,255,255])
img_tmp = cv2.imread(r'D:\Sample-Source\target.png')
img_tmp = cv2.resize(img_tmp, None, fx=mag_rate, fy=mag_rate)
img_tmp = cv2.copyMakeBorder(img_tmp,border_space,border_space,border_space,border_space,cv2.BORDER_CONSTANT,value=[255,255,255])
# 検出器生成/特徴点検出
detector = cv2.AKAZE_create()
kp_cap, des_cap = detector.detectAndCompute(img_cap, None)
kp_tmp, des_tmp = detector.detectAndCompute(img_tmp, None)
# マッチング(Brute-force)
bf = cv2.BFMatcher()
matches = bf.knnMatch(des_tmp, des_cap, k=2)
# 精度の高い結果を抽出
ratio = 0.5
good = []
for m, n in matches:
if m.distance < ratio * n.distance:
good.append([m])
# マッチング結果を表示
cv2.namedWindow("Result", cv2.WINDOW_KEEPRATIO | cv2.WINDOW_NORMAL)
img_result = cv2.drawMatchesKnn(img_tmp, kp_tmp, img_cap, kp_cap, good, None, flags=0)
# 保存
cv2.imwrite(r"D:\Sample-Source\result.png", img_result)
実行結果


精度の計算がよくわかりませんが 、ちゃんと対象と一致しているようですね!

実は私もよくわかってない。公式のチュートリアル記載のサンプルをカスタマイズしてるんだ。公式のどこかに書いてあるんだとは思うんだけど色々情報が足らないんだよね。mが何でnが何かとか。とりあえずratioの値を調整すると結果が変わって、今回の画像だとratio=0.65ぐらいから違う場所とマッチングしてくるからこの値をどうするかがポイントだね。

マッチングの方式も色々あるんですよね。色々試してみる必要がありそうですね。
座標の取得
全体画像のキーポイントは「kp_cap」に格納されています。マッチングの結果 matches には、kp_cap のうち、マッチしたキーポイントのインデックスが格納されているので、それを使用し、kp_capのうちマッチしたキーポイント情報を特定→特定したキーポイントから座標情報を抽出といった流れになります。
サンプルを下記に記載します。
import cv2
border_space = 30
mag_rate = 3
# 画像ファイルの読み込み/外枠生成
img_cap = cv2.imread(r'D:\Sample-Source\capture.png')
img_cap = cv2.resize(img_cap, None, fx=mag_rate, fy=mag_rate)
img_cap = cv2.copyMakeBorder(img_cap,border_space,border_space,border_space,border_space,cv2.BORDER_CONSTANT,value=[255,255,255])
img_tmp = cv2.imread(r'D:\Sample-Source\target.png')
img_tmp = cv2.resize(img_tmp, None, fx=mag_rate, fy=mag_rate)
img_tmp = cv2.copyMakeBorder(img_tmp,border_space,border_space,border_space,border_space,cv2.BORDER_CONSTANT,value=[255,255,255])
# 検出器生成/特徴点検出
detector = cv2.AKAZE_create()
kp_cap, des_cap = detector.detectAndCompute(img_cap, None)
kp_tmp, des_tmp = detector.detectAndCompute(img_tmp, None)
# マッチング
bf = cv2.BFMatcher()
matches = bf.knnMatch(des_tmp, des_cap, k=2)
# 精度の高い結果を抽出
ratio = 0.5
xy = []
for m, n in matches:
if m.distance < ratio * n.distance:
xy.append( kp_cap[m.trainIdx].pt)
# 座標抽出
x = 0
y = 0
for pt in xy :
x += pt[0]
y += pt[1]
x /= len( xy)*mag_rate
y /= len( xy)*mag_rate
# 結果描画
img_result = cv2.imread(r'D:\Sample-Source\capture.png')
img_result = cv2.circle(img_result, (int( x), int( y)), 10, [0,0,255], thickness=-1)
# 保存
cv2.imwrite(r"D:\Sample-Source\result.png", img_result)
実行結果


少し位置が気になりますけどちゃんと検出対象アイコンの座標に赤丸ついてますね!成功です!

マッチした特徴点の真ん中を取っているんだけどもっといい方法がある気がするね。もっと調べてみるよ。
まとめ
OpenCVを使った特徴点の検出とマッチングから座標の取得までをやってみました。OpenCVは少し触った程度で分かるものではなかったのでまだ試行錯誤中ではありますが、まず触ってみるという点ではいいのではないでしょうか。
今回の結果をベースにマッチングや各種パラメーターを調整して、より良いロジックに改修していく予定です。
最後までご覧いただいてありがとうございます。
次回はPyWin32を試してみたいと思います。
コメント