MotoJapan's Tech-Memo

技術めも

【opencv 基礎知識 #4】動画の手ぶれ補正をpython実装 (AKAZE, KNN, RANSAC)

行動認識が多かったので、半日くらいで動画の手ぶれ補正を作ってみた。

実装は数多あるので、そのうちコードをリファクタリングしたらGithubに載せようかと思う。
(すぐほしい人がいたら、コメントください)

すぐ忘れることをメモ。

結果

動画の通り、チューニングしなくても結構いい感じになっている。

上が補正前/下が補正後。
www.youtube.com

今年GWに山登りした時に撮った動画ですが、手ぶれを気にせず雫が落ちていくのが見れます。(最高です)
これもっと精度良くして無限ループすると、湯水のように時間が費やせそうです。

アルゴリズム

これを実装すれば基本的には動く。

  1. 動画を読み込み、フレームを読み出す
  2. 特徴量抽出する(AKAZE)
  3. キーポイントマッチングする(Brute-Force, KNN)
  4. 透視投影変換行列を求める(RANSAC)
  5. 画像を回転する

メモは以下。

1. 動画を読み込み、フレームを読み出す
cap = cv2.VideoCapture(mov_path)
ret, frame1 = cap.read()
2. 特徴量抽出する(AKAZE)

今回は権利が緩やかなAKAZEを用いる。

  • その他の選択肢
    • SHIFT, ORB, FAST, BRISK, KAZE, etc...
detector = cv2.AKAZE_create()
gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
keypoints1, descriptors1 = detector.detectAndCompute(gray1, None)
#frame2, keypoints2, descriptors2も同様に生成

frame1をquery画像、frame2をtrain画像とする。
query、trainの説明は、次に記載。

keypointsはkeypoint型である。
詳細は下記が分かりやすい。
Common Interfaces of Feature Detectors — OpenCV 2.4.13.3 documentation

3. キーポイントマッチングする(Brute-Force, KNN)

ここでハマったのは、queryとtrainの概念
ウェブの記事を見てもなかなか説明無かったが、一般的にこういうことらしい。

説明
query 探したいターゲット画像 犬のクロップ画像
train queryを探し出したいシーン画像 犬を散歩している風景画像

今回はqueryを基準フレーム(初期フレームなど)、trainは毎フレームとしてみた。
ここからBrute-Force(総当たり)で最近傍の特徴量を検索し、キーポイントマッチングする。

  • その他の選択肢
    • FLANN: Fast Library for Approximate Nearest Neighbors (高速近似近傍探索法)
#Brute-Force
bf = cv2.BFMatcher()
#matchesの要素数は、queryDescriptorsの要素数に一致
#上位k個の特徴点を返す
matches = bf.knnMatch(queryDescriptors = descriptors1, trainDescriptors = descriptors2, k=2)

matchesはDMatch型オブジェクトのリスト。
DMatch型の詳細は下記が分かりやすい。
特徴点のマッチング — OpenCV-Python Tutorials 1 documentation

(箸休め)

どんなマッチングをしているかは、下記コードでvisualizeして確認できる。

#good_matches = matchesから選び出したもの
frame3 = cv2.drawMatchesKnn(frame1, keypoints1, frame2, keypoints2, good_matches, None, flags=2)

たとえばこんなの(左画像がquery、右画像がtrain)
f:id:motojapan:20170816235844p:plain

4. 透視投影変換行列を求める(RANSAC)

マッチングが分かったので次のどういう一致になるか具体的に透視投影変換行列 Mを求める。
Mが分かればそれでtrain画像を回したり、query画像を回せば、互いに一致する画像が得られる。

具体的には、cv2.findHomographyで行列を得られるが、外れ値をいい感じに無視して変数推定をするRANSACを用いる。

#src_points/dst_pointsは、good_matchesからindexでアクセスしたquery/trainのkeypoint座標である
#(求めたいMの順にsrc_points/dst_pointsの入力順を決める)
M, mask = cv2.findHomography(src_points, dst_points, cv2.RANSAC, 5.0)
5. 画像を回転する

上記Mは、query画像座標系→train画像座標系とする行列である。
手ぶれ補正をするなら、Mの逆行列が必要であるので、linalg.inv(M)で得ると楽。

frame2_trans = cv2.warpPerspective(frame2, np.linalg.inv(M), size)
#frame1_trans = cv2.warpPerspective(frame1, M, size)

これで、frame2_transは、「train画像座標系がquery画像座標系にいい感じでキーポイントに一致する形で透視投影された画像」となる。


あとは各フレームごりごりやればうまくいく。

めでたしめでたし。