特徴点マッチング(SURF,SIFT,AKAZE)(OpenCV,Python を使用)

【概要】 OpenCV(コンピュータビジョンのためのライブラリ)を用いて,2枚の画像の間で特徴点マッチングを行う体験教材である.特徴点マッチングとは,画像中の特徴的な点(キーポイント)を検出し,その周囲の情報(記述子)を用いて,2枚の画像の間で対応する点の組を求める処理である.画像検索,物体検出,パノラマ合成,動画解析などの基礎になる.本教材では,SIFT,AKAZE,SURF の3種類の特徴量を扱う.マッチング結果は CSV ファイルに保存し,あとで確認や分析ができるようにする.手順の後半では,動画から取り出した連続フレームに特徴点マッチングを適用し,対応点数の変化を観察する体験を行う.

AI を用いて元記事全体を大幅に推敲の上公開

1. はじめに(特徴量の選び方と最新の状況)

OpenCV で利用できる主な特徴量と,その利用条件は次の通りである.

本教材では,まず SIFT と AKAZE を用いて体験を行い,SURF を使いたい場合の手順を別に示す.旧バージョンの OpenCV(2.x 系)の C 言語 API(cvExtractSURF など)は廃止済みであるため,本教材では現行の Python API を用いる.

2. 前準備

◆ インストール手順例(Windows の場合はコマンドプロンプト,Ubuntu の場合は端末で実行)

python -m pip install -U opencv-python opencv-contrib-python numpy matplotlib

◆ 動作確認手順例

python -c "import cv2; print(cv2.__version__)"

バージョン番号(4.4 以上であること)が表示されれば準備完了である.

【ヒント】エラーが出た場合は,エラーメッセージをよく読み,コマンドやプログラムの綴りを確認すること.

3. SIFT による特徴点マッチング

2枚の画像から SIFT 特徴点を検出し,FLANN(高速近似最近傍探索)でマッチングを行う.誤対応を減らすため,比率テスト(最近傍距離が第2近傍距離の 0.6 倍未満の組のみ採用)を行う.

【プログラム説明】 以下の Python プログラムでは,2枚の画像をグレースケールで読み込み,cv2.SIFT_create() で特徴点と記述子を求めている.マッチングには cv2.FlannBasedMatcher の knnMatch(k=2)を用い,比率テストで対応点を絞り込む.採用された各対応点について,番号,両画像でのキーポイント番号,座標,半径(キーポイントのサイズから換算)を CSV ファイル result.csv に書き出す.実行結果は CSV ファイルに保存されるので,あとで確認できる.

ソースコード(ファイル名: feature_match.py

import sys
import csv
import cv2
import numpy as np

RATIO = 0.6  # 比率テストのしきい値

def main():
    if len(sys.argv) != 4:
        print("使い方: python feature_match.py 画像1 画像2 出力CSV")
        sys.exit(1)
    img1 = cv2.imread(sys.argv[1], cv2.IMREAD_GRAYSCALE)
    img2 = cv2.imread(sys.argv[2], cv2.IMREAD_GRAYSCALE)
    if img1 is None or img2 is None:
        print("画像が読み込めない:", sys.argv[1], sys.argv[2])
        sys.exit(1)

    detector = cv2.SIFT_create()
    kp1, des1 = detector.detectAndCompute(img1, None)
    kp2, des2 = detector.detectAndCompute(img2, None)

    flann = cv2.FlannBasedMatcher(
        dict(algorithm=1, trees=4),   # KDTree を 4 本
        dict(checks=64))              # 探索する葉の最大数
    matches = flann.knnMatch(des1, des2, k=2)

    good = [m for m, n in matches if m.distance < RATIO * n.distance]

    with open(sys.argv[3], "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "objkeypoint", "imagekeypoint",
                    "p1x", "p1y", "radius1", "p2x", "p2y", "radius2", "dist"])
        for j, m in enumerate(good):
            r1 = kp1[m.queryIdx]
            r2 = kp2[m.trainIdx]
            w.writerow([j, m.queryIdx, m.trainIdx,
                        r1.pt[0], r1.pt[1], round(r1.size / 2),
                        r2.pt[0], r2.pt[1], round(r2.size / 2),
                        m.distance])
    print("特徴点数:", len(kp1), len(kp2), " 対応点数:", len(good))

if __name__ == "__main__":
    main()

◆ 実行手順例

python feature_match.py a.jpg b.jpg result.csv

【実行結果の確認】画面には特徴点数と対応点数が表示され,対応点の一覧は result.csv に保存される.result.csv を表計算ソフトなどで開き,座標や距離を確認すること.

【ヒント】画像が大きすぎると処理に時間がかかる.次のように ImageMagick で横幅 2880 画素などに縮小してから実行するとよい.

magick a.jpg -resize 2880x a_s.jpg
magick b.jpg -resize 2880x b_s.jpg

4. AKAZE による特徴点マッチング

【プログラム説明】 AKAZE を用いる場合は,前節のプログラムのうち検出器の生成とマッチャーの2行を書き換えるだけでよい.AKAZE の記述子は2値であるため,マッチングにはハミング距離を用いる cv2.BFMatcher を使用する.

    detector = cv2.AKAZE_create()
    # (中略:kp1, des1, kp2, des2 の計算は同じ)
    flann = cv2.BFMatcher(cv2.NORM_HAMMING)
    matches = flann.knnMatch(des1, des2, k=2)

【考察のポイント】同じ2枚の画像に対して SIFT と AKAZE を実行し,特徴点数,対応点数,処理時間を比較すること.どちらが多く対応点を得られたか,画像の内容(模様の多さ,ぼけ,明るさの違い)との関係を考えること.

5. SURF を使いたい場合

SURF は opencv_contrib の nonfree モジュールに含まれるため,OPENCV_ENABLE_NONFREE を有効にしてソースからビルドする必要がある.

◆ ビルド手順例(Ubuntu の場合)

sudo apt -y install build-essential cmake git python3-dev python3-numpy
git clone --depth 1 https://github.com/opencv/opencv.git
git clone --depth 1 https://github.com/opencv/opencv_contrib.git
mkdir build
cd build
cmake ../opencv \
  -DOPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules \
  -DOPENCV_ENABLE_NONFREE=ON \
  -DCMAKE_BUILD_TYPE=Release
make -j4
sudo make install

【プログラム説明】 ビルド後は,前節までのプログラムのうち検出器の生成の1行を次のように書き換えるだけで SURF を利用できる.引数の 500 はヘシアンしきい値であり,値を小さくすると特徴点数が増える.

    detector = cv2.xfeatures2d.SURF_create(hessianThreshold=500)

6. 体験:動画の連続フレームへの適用

動画から一定間隔でフレーム画像を取り出し,隣り合うフレームの組ごとに特徴点マッチングを行う.対応点数の時間変化を観察すると,カメラの動き(直進,旋回,停止)の変化点が読み取れることを体験する.

◆ 実行手順例(1)動画からのフレーム取り出し(ffmpeg を使用.5フレームごとに1枚を w に保存)

mkdir w
ffmpeg -i input.avi -vf "select=not(mod(n\,5))" -vsync vfr w/frame-%07d.png

◆ 実行手順例(2)隣り合うフレームの組ごとのマッチングと,対応点数の集計

cd w
prev=""
for i in frame-*.png; do
  if [ -n "$prev" ]; then
    python ../feature_match.py "$prev" "$i" "$prev.csv"
  fi
  prev="$i"
done

# 各 CSV の行数(=対応点数+ヘッダ1行)を集計
rm -f lines.csv
for i in *.csv; do
  wc -l "$i" >> lines.csv
done

◆ 実行手順例(3)対応点数の時間変化のプロット(Python を使用)

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

n = []
with open("lines.csv") as f:
    for line in f:
        n.append(int(line.split()[0]) - 1)  # ヘッダ1行分を引く
plt.plot(n)
plt.xlabel("frame pair")
plt.ylabel("number of matched keypoints")
plt.savefig("matches.png")
print("matches.png に保存した")

【実行結果の確認】実行結果は CSV ファイルに保存されるので,あとで確認できる.グラフは matches.png に保存される.

【考察のポイント】対応点数が急に減る区間はどこか.そのフレーム画像を実際に開いて,カメラの旋回,被写体の変化,ぼけなどとの関係を考えること.さらに,CSV に保存した座標から対応点の移動量(p1x−p2x,p1y−p2y)を計算し,その平均や外れ値を調べると,カメラの動きの向きの推定につながる.