特徴点マッチング(SURF,SIFT,AKAZE)(OpenCV,Python を使用)
AI を用いて元記事全体を大幅に推敲の上公開
1. はじめに(特徴量の選び方と最新の状況)
OpenCV で利用できる主な特徴量と,その利用条件は次の通りである.
- SIFT: 特許は2020年3月に失効した.OpenCV 4.4 以降では標準機能となり,
cv2.SIFT_create()で追加設定なしに利用できる. - AKAZE,ORB: もともと特許の制約がなく,OpenCV の標準機能として利用できる.高速であり,実用上よく使われる.
- SURF: opencv_contrib の nonfree モジュールに含まれる.pip でインストールできる opencv-python,opencv-contrib-python には含まれていない.利用するには,OPENCV_ENABLE_NONFREE オプションを有効にして 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)を計算し,その平均や外れ値を調べると,カメラの動きの向きの推定につながる.