Boy Meets ML

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

SfMについて #1:OpenSfMを実行する

はじめに

今回は機械学習とは少し異なる話題ですが、SfM(Structure from Motion)についての記事となります。 Google先生に聞けばたくさん記事が出てくる話題ですが、 まずはOpenSfMを利用する方法をメモしておきます。 SfMの詳細については時間ができたときに話題にしたいと思います。 最近では深層学習を利用して、単眼画像から深度(デプス)を推定する手法も多く提案されており、 車の自動運転からVR/ARに至るまで応用先が広く、魅力的な技術といえます。

SfMとは?

詳細はまたの機会にしますが、SfMとは被写体に対して視差をもたせたたくさんの画像を撮影しまくり、 各画像 の撮影した位置を推定しつつ、被写体の3次元点群を生成する手法を指します。 1つの手法名ではなく一連の処理方法をまとめたもの・概念の名前です。 カメラパラメータを利用できれば実際に撮影した物体の実寸を推定することもできたりと、活用先が多岐に渡ります。 自動運転や自律移動ロボットの分野では自己位置推定と地図構築に関わるVisual SLAMのような技術分野でも活用される技術となります。

Docker環境でOpenSfMを利用する準備

OpenSfM1はpython2系でもpython3系でも動かすことが可能なライブラリです。 私はDockerで構築したUbuntu 18.04.1でpython3系を利用しているので、 こちらのDockerfile2を参考にして既に立ち上げているコンテナに必要なモジュールを追加していきました。 独立した環境をDockerで構築したい場合は、このDockerfileをそのまま利用しdocker buildによるイメージを作成、 コンテナを作成していけばOKだと思います3

私の既存コンテナの環境では、anacondaが入っていて、どのpythonを利用しているかというと、

which python

/opt/conda/bin/python

python -V

Python 3.7.3

となり、 /opt/conda/lib/python3.7/site-packages/にconda installを行ったモジュールたちがいます。 anacondaを利用せずに、最近はpipで十分なのでこの場合は公式サイトの手順に従えばOKだと思います。

さて、anacondaを利用している場合はDockerfile内の以下のコマンドにおける DPYBIND11_PYTHON_VERSIONオプションとDPYTHON_INSTALL_DIRオプションを変えてあげます。

# opengv
RUN \
    mkdir -p /source && cd /source && \
    git clone https://github.com/paulinus/opengv.git && \
    cd /source/opengv && \
    git submodule update --init --recursive && \
    mkdir -p build && cd build && \
    cmake .. -DBUILD_TESTS=OFF \
             -DBUILD_PYTHON=ON \
             -DPYBIND11_PYTHON_VERSION=3.6 \
             -DPYTHON_INSTALL_DIR=/usr/local/lib/python3.6/dist-packages/ \
             && \
    make install && \
    cd / && rm -rf /source/opengv

まず、DPYBIND11_PYTHON_VERSION=3.7として、DPYTHON_INSTALL_DIR=/opt/conda/lib/python3.7/site-packages/と変更してあげることで、無事opengvが使えました。

OpenSfMを使う

無事、OpenSfMと依存環境がインストールできれば、公式サイトに書いてある次のコマンドを実行できます。

bin/opensfm_run_all data/berlin

このopensfm_run_allの中身をcatで覗いてみると、OpenSfM/bin/opensfmに対してオプションを渡して順番に実行してくれるだけだとわかります。 下の$1はdata/berlinを渡しています。

#!/usr/bin/env bash

set -e

DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
PYTHON=${2:-python}

echo "Running using Python command: $PYTHON"

$PYTHON $DIR/opensfm extract_metadata $1
$PYTHON $DIR/opensfm detect_features $1
$PYTHON $DIR/opensfm match_features $1
$PYTHON $DIR/opensfm create_tracks $1
$PYTHON $DIR/opensfm reconstruct $1
$PYTHON $DIR/opensfm mesh $1
$PYTHON $DIR/opensfm undistort $1
$PYTHON $DIR/opensfm compute_depthmaps $1

OpenSfM/bin/opensfmの中身は結局引数を渡して、OpenSfM/opensfm/commands以下にあるpythonスクリプトたちを実行しています。

SfMというと建築物の画像を題材にしたものをよく見ますので、少し異なる画像に対して適用して遊んでみます。 夏休みにイタリアを旅行し、フィレンツェにあるヴェッキオ宮殿に行き、 ここぞとばかりにダンテのデスマスクを大量に撮影してきました。 この画像に対してOpenSfMのサンプルスクリプト(bin/opensfm_run_all)を適用し、3次元点群を復元することで、 思い出をより深く楽しんでみます。

まず、私が撮影してきたダンテのデスマスクの画像は以下のようなもので、 角度を変えてiPhone Xで撮影しまくり、全19枚の画像からなります。 再構成した3次元点群と画像を重畳した結果も載せてしまいますが、結構きれいに復元できました。

Overview of SfM
Overview of SfM

蘇る、ダンテ

念の為に記載しておきますが、bin/opensfm_run_allにわたすターゲットとする自身のデータセット(複数の画像)は、 data/italy_dante/images/xx.jpgのようにimagesというフォルダにまとめておく必要があります。 一部省略しましたが、treeコマンドでみると次のような階層構造でデータが生成されます。 自分で用意したものはdata/italy_dante/imaegs/xx.jpgのみで、その他のフォルダやファイルはすべて自動で生成されます。

tree data/italy_date

data/italy_dante/
├── camera_models.json
├── depthmaps
│   ├── 01.jpg.clean.npz
│   ├── 01.jpg.pruned.npz
│   ├── 01.jpg.raw.npz
│   ├── ...
│   ├── 19.jpg.clean.npz
│   ├── 19.jpg.pruned.npz
│   ├── 19.jpg.raw.npz
│   └── merged.ply
├── exif
│   ├── 01.jpg.exif
│   ├── ...
│   └── 19.jpg.exif
├── features
│   ├── 01.jpg.features.npz
│   ├── ...
│   └── 19.jpg.features.npz
├── images
│   ├── 01.jpg
│   ├── ...
│   └── 19.jpg
├── matches
│   ├── 01.jpg_matches.pkl.gz
│   ├── ...
│   └── 19.jpg_matches.pkl.gz
├── profile.log
├── reconstruction.json
├── reconstruction.meshed.json
├── reference_lla.json
├── reports
│   ├── features
│   │   ├── 01.jpg.json
│   │   ├── ...
│   │   └── 19.jpg.json
│   ├── features.json
│   ├── matches.json
│   ├── reconstruction.json
│   └── tracks.json
├── tracks.csv
├── undistorted
│   ├── 01.jpg.jpg
│   ├── ...
│   └── 19.jpg.jpg
├── undistorted_reconstruction.json
└── undistorted_tracks.csv

あとは、次のコマンドで簡易的なhttpサーバ(ポート指定しなければポートは8000)をローカルに立ててウェブブラウザでSfMの結果ファイル(http://localhost:8000/viewer/reconstruction.html#file=/data/italy_dante/reconstruction.meshed.json)にアクセスしてあげれば、viewerによって描画することが可能です。

python -m http.server [port number]

A result of SfM
A result of SfM

3D Reconstruction result of Dante at center position3D Reconstruction result of Dante at left position3D Reconstruction result of Dante at right position
3D Point Clouds and Camera Positions

おでこのあたりの点群が少し欠けている結果となりますが、比較的うまく再構成されています。 おでこ付近は凹凸がすくないため特徴点がうまく抽出されなかった可能性がありますね。 よくあるサンプルとして被写体が建物のように大きな物体である場合、視差を発生させるためには多くの位置から撮影する必要があり、色々な場所に人が移動して撮影するために撮影に手間がかかります。 特に上斜め上空といった位置から大きな建物を撮影することは難しいと思いますので、SfMで遊んでみると思ったよりも綺麗に点群が得られなかったりします。 一方で今回のダンテのデスマスクの場合は、被写体に対してあらゆる角度から簡単に写真撮影ができたため、 いい具合の再構成ができたのだと考えられます。

まとめ

今回はSfMを適用して、かの有名なダンテのデスマスクの3次元復元を行いました。 被写体が小さいために、あらゆる角度から視差を生じさせた画像を容易に撮影でき、 再構成結果もいい感じとなりました。 しかし、おでこ付近においては特徴点不足が原因と疑われる欠損が発生しました。 身近な画像によってかんたんに3次元復元を行うことができ、旅の思い出がより鮮明に蘇ります! 次回以降は、SfMの原理や、精度向上を目指していこうかと考えています。

文献