け日記

最近はPythonでいろいろやってます

OpenCV: 特徴点抽出とマッチング

お仕事で初めて画像処理システムの開発に携わってます。
基本的なツールとしてOpenCVについて知っておいた方が良さそうですので、自分用のメモとしてトピックごとに整理していこうと思います。

OpenCV

言わずと知れたコンピュータビジョンのOSSライブラリですね。(そう言っておきながら、自分はほとんど使ったことがなかったのですが。)

opencv.org

インストールは多少ハマるかも知れません。こちら↓の記事などを参考にしてくださいませ。ちなみにGoogle Colaboratoryだとデフォルトでインストールされてます。

画像のロード

準備としてサンプル画像をロードします。

import numpy as np
import matplotlib.pyplot as plt
import cv2

# サンプル画像
input_file_path = "yagi.jpg"
output_file_path = "tmp.jpg"

# 画像のロード
img = cv2.imread(input_file_path)
print(img.shape)  # (400, 400, 3)

# 画像の表示
def display(img, output_file_path=output_file_path):
    cv2.imwrite(output_file_path, img)
    plt.imshow(plt.imread(output_file_path))
    plt.axis('off')
    plt.show()
    
display(img)

いつものアイコンですね。

f:id:ohke:20190803133350p:plain

特徴点抽出

特徴点抽出はその名の通り、画像の中から"特徴的な"ポイントを抽出するアルゴリズムです。使われる特徴としては角 (コーナー) が多いようですが、輝度の勾配なども使われるそうです。

コーナーを検出して特徴点とするアルゴリズムの1つに、SIFT (scale-invariant feature transformation, 提案論文) があります。
従来の手法 (HarrisやShi-Tomasiなど) と比較して、拡大・縮小しても1つのコーナーとして検出される頑健さを持ってます。チュートリアル (SIFT (Scale-Invariant Feature Transform)の導入 — OpenCV-Python Tutorials 1 documentation) に詳しい説明があります。

  1. M x Nの元画像をM/2 x N/2, M/4 x N/4, ... と低解像度にした画像 (オクターブ) を生成します (参考)
    • こうしてできたオクターブの組み合わせをガウシアンピラミッドと呼びます
  2. 各オクターブに複数のσ (σ, kσ, ...) のガウシアンフィルタ (≒ぼかし) をかけて差分を取ります (difference of Gaussians: DoG)
    • 輪郭抽出に近い働きをします
  3. 各差分画像の画素の極大値をキーポイント (=特徴点) 候補とします
    • 同一画像の近傍8画素 + オクターブの上下18画素で極大値となる画素を選択します
  4. エッジや低コントラストの候補を閾値で除外して最終的なキーポイントを決定します

OpenCVでの実装ですが、今回はSIFTの改良であるAKAZEを使います。

  • SIFTではエッジもぼかしてしまっていたものを、非線形フィルタで特徴抽出することで精度を改善したKAZE
  • KAZEを高速化したAKAZE (Accelarated KAZE)

AKAZEについてはこちら↓が参考になります。 - AKAZE特徴量の紹介と他特徴量との比較 - 遥かへのスピードランナー

実装ではグレースケール画像からdetectAndComputeメソッドでキーポイントと特徴量記述子を得ています。キーポイントの近傍画素のヒストグラムを持った特徴量となっており、この後のマッチングで使用します。

# グレースケール変換
from_img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

# 特徴点抽出 (AKAZE)
akaze = cv2.AKAZE_create()
from_key_points, from_descriptions = akaze.detectAndCompute(from_img, None)

# キーポイントの表示
extraceted_img = cv2.drawKeypoints(from_img, from_key_points, None, flags=4)
display(extraceted_img)

画像に検出された特徴点をマッピングしたのが下の図です。目の端や口元、角の根元などが特徴点として抽出されていることがわかります。(ちょっとエモい。)

f:id:ohke:20190803134137p:plain

マッチング

2枚の画像の特徴点を対応付けることをマッチングと言います。マッチングができると、同じ物体であることの認識や物体の動きの検出などが可能になります。

2枚の画像から得られた特徴量記述子の距離を計算し、最も近いものを同一のキーポイントとする方法があります。OpenCVではBFMatcherでそれができます。こちらもチュートリアル(特徴点のマッチング — OpenCV-Python Tutorials 1 documentation) に詳しく記載があります。
BFMatcher_createでは2つの引数を取ります。1つ目は距離の計算方法で、AKAZEの場合バイナリ値となるのでハミング距離を指定してます。2つ目の引数はcrossCheckで、Trueにすると互いの距離が最も近いキーポイント同士のみが紐付けられるようになります (デフォルトFalseでは、一方のキーポイントから見ると最も近いが、他方のキーポイントから見ると他のキーポイントの方が近いという非対称が起こります) 。

def match(from_img, to_img):
    # 各画像の特徴点を取る
    from_key_points, from_descriptions = akaze.detectAndCompute(from_img, None)
    to_key_points, to_descriptions = akaze.detectAndCompute(to_img, None)
    
    # 2つの特徴点をマッチさせる
    bf_matcher = cv2.BFMatcher_create(cv2.NORM_HAMMING, True)
    matches = bf_matcher.match(from_descriptions, to_descriptions)
    
    # 特徴点を同士をつなぐ
    match_img = cv2.drawMatches(
        from_img, from_key_points, to_img, to_key_points, 
        matches,  None, flags=2
    )
    
    return match_img, (from_key_points, from_descriptions, to_key_points, to_descriptions, matches)

元画像を変形してキーポイントをマッチングした結果を示します。

同じ画像

f:id:ohke:20190803235322p:plain

リサイズした画像

f:id:ohke:20190803235345p:plain

回転した画像

f:id:ohke:20190803235407p:plain

一部をクロッピングした画像

f:id:ohke:20190803235425p:plain

同じ画像であれば変形が入ったとしても概ねマッチングできているようです。

まとめ

今回はOpenCVを使って特徴点抽出とマッチングを行いました。