Boy Meets ML

機械工出身者が機械学習やその界隈の知識を考える

SfMについて #3:OpenSfMの中身をみる(detect_features)

はじめに

前回の記事の続きです。 mlengineer.hatenablog.com mlengineer.hatenablog.com

初回記事では、OpenSfMを使ってイタリアで撮影したダンテのデスマスクを3次元復元するサンプルスクリプトを実行し、 2回目以降の記事ではこのサンプルスクリプトが何をやっているのかを確認していき、 最終的には精度向上や手法改善をめざしています。 今回は3回目の記事で、ようやくSfMの中身に近づいていくことができそうです。

OpenSfMの流れをおさらい

OpenSfM 1のサンプルで紹介されている以下のコマンドを実行すると、主に次のような処理が行われるのでした。

bin/opensfm_run_all data/berlin
  1. extract_metadata
  2. detect_features(●本記事の対象)
    • 対象データの各画像から特徴量を取得する処理
  3. match_features
    • 取得した特徴量を画像間でマッチングする処理
  4. create_tracks
    • マッチング結果からカメラ位置推定を行う処理
  5. reconstruct
    • カメラ位置推定の結果から3次元点群を生成する処理
  6. mesh
    • 3次元点群から可視化を行うためにメッシュを生成する処理
  7. undistort
    • カメラパラメータを利用して歪みを軽減する処理
  8. compute_depthmaps
    • カメラパラメータから対象データの各画像の深度マップを生成する処理

本記事では、2. detect_featuresについて見ていきたいと思います。

detect_features

本家の記事では1行しか言及されていなく、非常に心もとないです。

detect_features
This command detects feature points in the images and stores them in the feature folder.

一応、翻訳すると、「このコマンドは画像列から特徴点を見つけて、featureフォルダに格納する」ということです。 特徴点を見つけると言われても、特徴点検出方法はたくさんあるわけで、更には特徴量記述はどうなっているのか?と疑問を持ちます。

そこで以下のファイルを見てみるとpythonスクリプトは3つの重要そうなモジュールをimportしていることがわかります。

from opensfm import bow
from opensfm import dataset
from opensfm import features
  • /OpenSfM/opensfm/bow.py
    • Bag-of-wordsを行うモジュールでしょう
  • /OpenSfM/opensfm/dataset.py
    • データセットや設定ファイルを扱うための関数が入っている
    • OpenSfM/opensfm/config.pyという重要なモジュールをインポートしている
  • /OpenSfM/opensfm/features.py
    • 各画像から特徴点抽出および特徴量記述を行う
    • 結果を保存/読込を行う関数をもつ

実は2番目にあげたdataset.pyがimportしているconfig.pyの中には、default_config_yamlという変数に重要な手がかりが散りばめられていました。 以下にyamlの一部を貼り付けます。

...
# Params for features
feature_type: HAHOG           # Feature type (AKAZE, SURF, SIFT, HAHOG, ORB)
feature_root: 1               # If 1, apply square root mapping to features
feature_min_frames: 4000      # If fewer frames are detected, sift_peak_threshold/surf_hessian_threshold is reduced.
feature_process_size: 2048    # Resize the image if its size is larger than specified. Set to -1 for original size
feature_use_adaptive_suppression: no
# Params for SIFT
sift_peak_threshold: 0.1     # Smaller value -> more features
sift_edge_threshold: 10       # See OpenCV doc
# Params for SURF
surf_hessian_threshold: 3000  # Smaller value -> more features
surf_n_octaves: 4             # See OpenCV doc
surf_n_octavelayers: 2        # See OpenCV doc
surf_upright: 0               # See OpenCV doc
# Params for AKAZE (See details in lib/src/third_party/akaze/AKAZEConfig.h)
akaze_omax: 4                      # Maximum octave evolution of the image 2^sigma (coarsest scale sigma units)
akaze_dthreshold: 0.001            # Detector response threshold to accept point
akaze_descriptor: MSURF            # Feature type
akaze_descriptor_size: 0           # Size of the descriptor in bits. 0->Full size
akaze_descriptor_channels: 3       # Number of feature channels (1,2,3)
akaze_kcontrast_percentile: 0.7
akaze_use_isotropic_diffusion: no
# Params for HAHOG
hahog_peak_threshold: 0.00001
hahog_edge_threshold: 10
hahog_normalize_to_uchar: yes
# Params for general matching
lowes_ratio: 0.8              # Ratio test for matches
matcher_type: FLANN           # FLANN(symmetric) or BRUTEFORCE(symmetric), WORDS(one-way), WORDS_SYMMETRIC(symmetric)
# Params for FLANN matching
flann_branching: 8           # See OpenCV doc
flann_iterations: 10          # See OpenCV doc
flann_checks: 20             # Smaller -> Faster (but might lose good matches)
# Params for BoW matching
bow_file: bow_hahog_root_uchar_10000.npz
bow_words_to_match: 50        # Number of words to explore per feature.
bow_num_checks: 20            # Number of matching features to check.
bow_matcher_type: FLANN       # Matcher type to assign words to features
# Params for VLAD matching
vlad_file: bow_hahog_root_uchar_64.npz
# Params for matching
...

yamlにはmatchingについての記載もありましたが、次回考えるために省略しました。 このyamlから次の手法が使えそうです。 ちなみに、特徴点検出(Key points Extraction)と特徴量記述(Descriptor)の処理をまとめて特徴量抽出(Feature Extraction)とよぶことが多いかなと思います。

Flow of Feature Extraction
Flow of Feature Extraction

OepnSfMで選択できる特徴量抽出手法を並べました。

  1. AKAZE(Accelerated-KAZE)
  2. SURF(Speeded Up Robust Features)
  3. SIFT(Scale-Invariant Feature Transform)
  4. HAHOG(Hessian Affine feature point detector + HOG)
  5. ORB(Oriented FAST and Rotated BRIEF)

このうち、変数default_config_yamlにあるfeature_typeを使うようにできています。 サンプルではHAHOGが使われるということです。 HAHOGは聞いたことがなかったので調べてみると、公式フォーラム2で回答されていて、 実態はHessian Affine特徴点をHOG(Histogram of Oriented Gradients)で記述する実装となっています。 HAHOGの実装は、/OpenSfM/opensfm/src/hahog.ccというpythonバインドするためにC++で書かれています。

HAHOGの性能について考えてみます。 まず特徴点検出であるHessian Affineの効果は、ある物体Aから抽出した特徴点aと、 その物体Aに対してアフィン変換を施した物体A'から抽出した特徴点a'とすると、 aとa'が一致するという利点があります。 これをアフィン不変であるといいます。 正確にはHessian AffineはMSERのようにアフィン不変な領域を抽出する方法です。

HOGは非常に簡単で、SIFTでも使われている特徴点の記述方法です。 特徴点の周りに適当なサイズの領域セルを考えてあげ、このセルの輝度勾配を求めます。 特徴の方向とは\piのうち、どっちを向いてますか?という話になるので、 \piを適当な数のビンで分割してあげて輝度勾配によるヒストグラムを作ります。 このようにつくったヒストグラムを更に大きな領域であるブロックで束ねてあげて新たなヒストグラムをつくり、 ベクトルとして扱う、という流れです。 文献3は図がたくさんあってわかりやすいです。 HOGだけを画像に適用する場合は、画像全体をセルに分割して、そのセルを複数個束ねてブロックにして、最終的には画像全体からブロックごとに特徴量を記述することができます。

HAHOGでは安定的な特徴点の周りだけにHOGを適用することで、軽量化しつつも特徴量として扱えるようベクトル化にしたものであると思います。 SURF/SIFTに関しては特許が取られているので、mapillaryとしては簡便な手法方法としてHAHOGを実装したのでは、と感じました。 これがデフォルト設定になっていることが微妙だと思いますが、 1~5の選択肢を考えるとまずAKAZEに変更することで3次元復元したときの精度向上が期待できそうです。 オリジナルはこちら4ですが、AKAZEの性能についてはこちら5がわかりやすいです。 AKAZEの詳細についてはまた今度論文を読んで解説したいと思います。

Params for features

AKAZEが良さそうだと思いますが、configで設定されたパラメータで他の特徴量にも影響しているものは、 yamlの上部にかかれているParams for featuresです。

  1. feature_root
    • 特徴量によって若干動作が異なるが、特徴量記述した結果に対して平方根をとる処理を行いその結果を使う
  2. feature_min_frames
    • 特徴点の検出数がこの数より小さい場合は、ハイパーパラメータを少し調整してくれる
  3. feature_process_size
    • オリジナル画像のサイズを使う場合は-1にする必要がある
  4. feature_use_adaptive_suppression
    • OpenSfM/opensfm/src/csfm.cc内を見る限り、AKAZEのみに影響があるパラメータのようです

feature_rootを実行することで極端な特徴量ベクトルの影響を抑えることができるのかな?と思います。 あまりいい処理だと思いませんが、デフォルトでは使うことになっていました。

feature_min_framesは各画像から取得した特徴点数を評価して、思ったよりも少ない場合は各手法のハイパーパラメータを調整して、 特徴点数を増やしてあげる処理を行います。

feature_process_size はデフォルト設定では画像の幅/高さのうち大きいほうを2048ピクセルにしてしまいます。 常に入力した画像と同じ大きさを使いたい場合は-1にセットすべきで、 ダンテのデスマスクは4K画像を入力しましたが、いつの間にか小さくされていたようです。 画像サイズは大きいほうが特徴点が取りやすくなるのでここは直す必要があります。

feature_use_adaptive_suppressionは、HAHOGの場合は強制的にFALSEになり、 AKAZEを使ったときのみ利用されるようです。 論文を読まないと詳しい処理のイメージがつきませんが、OpenSfM/opensfm/src/third_party/akaze/lib/AKAZE.cppに処理が書かれています。 今回はAKAZEについては詳しく触れるつもりはありませんので、また今度の宿題にしたいと思います。

まとめ

本家サイトでは1行でさらっと書かれており、どのような手法を適用することができるのかわかりにくいですが、 config.pyをみるだけでほぼほぼ十分でした。

特徴量抽出のデフォルト設定がHAHOGであり、初回投稿時のダンテのデスマスクの復元にはこれを利用しています。 単純にAKAZEに変更するだけでも性能が上がりそうです。

更に、てっきり4K画像を入力すればそのまま利用されると思っていましたが、デフォルトconfigはリサイズしてしまうことが判明しました。

AKAZEを利用する上では、更にハイパーパラメータを知る必要がありますが、また今度の宿題とします。

特徴量抽出はそのあとのマッチングをどのように行うかということにも依存します。 次回はmatch_featuresの処理について確認することで、3次元点群を復元する上で改良できる点を探っていきます。

文献