catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

HololensでAR(vuforiaとunity)

HoloLens上でVuforiaとUnityを使ったARを試してみました。かたりぃなです。

Vuforiaとは

VuforiaはARフレームワークです。 マーカーを登録することで、それを認識するためのライブラリが生成されます。

対応プラットフォームは

ライブラリは各種プラットフォーム用のものを出力でき、Android, iOS, UWP, Unityなどがあります。

マーカーの種類

Vuforiaで扱えるマーカーはいくつかの種類があって、次の中から選択することができます。

  1. 平面画像マーカー
  2. 直方体マーカー
  3. 円柱マーカー
  4. 立体マーカー

今回試したのは1と4です。

平面画像マーカー

平面画像マーカーは、写真などの二次元画像をマーカーとして使用できます。 写真をアップロードすると、特徴点を算出して、そのマーカーの認識しやすさを表示してくれます。 こんな感じに。 f:id:Catalina1344:20180810164557j:plain f:id:Catalina1344:20180810164602p:plain

ちょっと特徴点が少なすぎて認識できないですね。 そもそも三次元物体に対する認識を二次元でやろうというのは少々無理があるかと。

立体マーカー

今回のメインは、この立体マーカーです。立体マーカーを作る方法は2つあります。

  1. Androidの立体スキャンアプリを使って立体スキャンする
  2. CADデータをVuforia_Model_Target_Generaterで処理する

どちらも以下のURLからダウンロードしたツールを使って実現できます。 https://developer.vuforia.com/downloads/tool

今回は都合よくCADデータと、それを3Dプリントして塗装仕上げしたものがあるので2の方法でいきます。

CADデータはこんな感じです。 f:id:Catalina1344:20170913003738p:plain

3Dプリントして塗装したのはこんな感じです f:id:Catalina1344:20180810164905j:plain

技術系ブログなのに方向性がおかしくなってきてる気がしますが、突き進みます。

CADデータってどんなの?

厳密な定義はおいといて、Vuforiaが3Dマーカーとして使えるCADデータのフォーマットとしてはFBX, STLなどがあります。

ここではSTLを使います。STLデータとは3DプリンタやCNC切削機で利用できるデータ形式です。

STLデータ形式Wikipediaにある詳細説明のとおりです。

  • 頂点3つで構成されるポリゴン
  • そのポリゴンの表裏を表現する法線

これを1つの要素として、その集合として表現されるものです。

3DCGとの最大の違いは、次のような情報が含められないということです。 原理的に当然といえば当然なのですが。

  1. マテリアル
  2. テクスチャマップ
  3. 法線マップ

なので、3Dプリントするデータは生ポリゴンで全部表現する必要があります。

CADデータを取り込んでマーカーをつくる

ModelTargetGeneratorを起動するとライセンス入力を求められるので、Web上で登録して、そのライセンスコードを入力します。

使い方はこのあたりを参考にします。 https://library.vuforia.com/articles/Solution/model-target-generator-user-guide.html

そして新規プロジェクト作成でCADデータを読み込ませると、設定画面が開きます。

先にこたえを書くと、出力可能な状態にまで設定するとこんな画面になります。 f:id:Catalina1344:20180810165243p:plain

Model Target Generatorの基本設定

起動時に設定したモデルが表示されるので、次のものを設定します

  • 単位系
  • モデルの上方向

ここで単位系をあわせておかないと、Unityにもっていったときに認識サイズが大きすぎる/小さすぎるなんてことになります。 まあUnity側でスケールかければいいんですが。

マーカーの検出位置を設定

DetectionPositionというボタンを押すと、画面右側にシルエットが表示されます。

よくわかってませんが、どうやらこのシルエットが認識するオブジェクトの情報のようです。

なので、現実世界のオブジェクトのシルエット(実際に3Dプリントしたデータ)と一致しないようなシルエットが表示されるときは、CAD側で修正もしくは他オブジェクトを使うなどの対応が必要になります。

上のシルエットも、同様の理由から3Dオブジェクトの一部しか使用していません。

シルエットが一致しないケースとして

  • 組み立て時の誤差。3Dプリントして塗装、接着して組み立てるとどうしても誤差が出ます。
  • 光沢が大きな素材。マニュアルにはこう書かれてるのですが、実際どうなのかはよくわからないです。背景が白だと光沢出たときに穴になるというのはわかるのですが。
  • 遮蔽物による影響。手で持つ部分などは基本的に遮蔽されるので、認識には向かないです

などがありますので、適宜工夫する必要がありそうです。

エクスポート

ModelTargetGeneratorのメニュー右端の「Generate Target」ボタンを押すと、マーカーとして認識するライブラリを生成できます。(Unity)

無料ライセンスでは10回までという制限があるので、その点だけ気を付けましょう。

Unity側でARの設定をする

エフェクト出したいので、Unityで作業していきます。こういうのはC++でやるの辛すぎるので。

unity 2018.2.2です。

Vuforiaの設定をする

事前にHololens用の設定を済ませておきます。

そのうえで、このページを参考にVuforiaの設定をします。 https://library.vuforia.com/articles/Training/Developing-Vuforia-Apps-for-HoloLens

ModelTargetを設定する

先ほどModelTargetGeneratorで出力したパッケージをインポートします。

インポートしたパッケージをシ、ーンヒエラルキに追加してから、必要な項目を設定します。

f:id:Catalina1344:20180810165557p:plain

UnityのPlayerモードで動作確認

ここまでできると、Unity上からマーカーを認識するかなど、確認できるようになります。 シーンに追加したModelTargetの子オブジェクトとして任意の3Dオブジェクトを配置して、正しく認識するか確認します。

もし認識できていれば配置した3Dオブジェクトが表示されます。 認識できていない場合は次の点を見直すとよいです。今回はこの2つが原因でした。

  • マーカーの質が悪い。光沢がありすぎたりすると、マーカーとして認識しづらいようです
  • 3Dオブジェクトが大きすぎる マーカーの大きさより小さくなるようスケール設定しましょう。画面からはみ出していると何も表示されません。

エフェクトを出す

大した事してないので、簡単に説明します。

3Dモデル(天球儀)の周りに回転するタロットカードを出したいので、その一枚ポリゴンを配置します。 とりあえず4枚くらいあればいいでしょう。

タロットカードはニコニコモンズにあがってる無料素材を使わせていただきました。

あとは雰囲気だしたいので適当なパーティクルを置きます。

オクルージョンの設定

現実世界では、視点からみて奥にある物体は、手前にある物体にさえぎられると見えなくなります。あたりまえですね。

仮にコレが無い場合、今回のようなマーカーの周りを周回するエフェクトは、すべてマーカーより手前にあるように見えてしまいます。

このあたりまえをUnityに設定してあげる必要があります。

即ち - Hololensの空間マッピング機能でえられたマップを取得する - そのマップをZバッファに反映するよう指示する

まあ、このドキュメントのVisualizationあたりまでやっておけばその通りになります。 https://docs.microsoft.com/en-us/windows/mixed-reality/holograms-230

(実際に可視化の段階でZバッファにも書き込まれるので。)

空間マップの詳細説明はまだ理解が追い付いてないのでまたの機会にします。

できた!

感想と今後の展望

技術ブログのはずが、段々サブカル要素が混ざってカオスになってきましたが気にしないことにします。

これからの時代、一つのカテゴリだけで生き残るのではなく複数カテゴリのコンビネーションができたほうが有利だと思います。

というわけで今回はこれくらいで。

C++のHTTPサーバ(boost::beast)を試す

毎年恒例の夏バテの時期がやってきました。かたりぃなです。 今回はC++でhttpやってみたいと思います。

C++でhttpサーバ

今はC++単体でラクにHTTPサーバが実装できるようになりました。

使うライブラリはboost::beastです。

https://www.boost.org/doc/libs/1_67_0/libs/beast/doc/html/index.html

boost1.66で入ったので、結構最近です。

exampleをビルドして動かす

とりあえず適当な仮想マシン上でサンプルコードをビルドして、実行してみます。

example/http/server/sync/http_server_sync.cpp あたりが単純で分かりやすいです。

これを実行したディレクトリのファイルをHTTP-GETできます。

まるでPythonのSimpleHTTPServerみたいですね。

足りない

私がやりたいことに少々足りません。

Hololensから画像を投げて、それを受け取って処理するWebサーバが欲しいのです。そう、PUTとPOSTの実装がサンプルコードにはないのです。

ないなら作りましょう。

サンプルコードの改造でサクッと。

POSTを受け付ける

サンプルコードではhandle_requestという関数があります。 第二引数がまさにHTTPリクエストで、一通りの情報が入ってます。

http::request<Body, http::basic_fields<Allocator>>&& req

サンプルコードではreq.method() == http::verb::getとかやってるので真似しましょう。

    if( req.method() == http::verb::post){
        std::cerr << "POST request" << std::endl;
    }

実際のHTTPメソッドの定義はこうなってます。 https://www.boost.org/doc/libs/develop/boost/beast/http/verb.hpp

データを投げるテストをするならこんな感じで。

curl -X POST localhost:port --data-binary @/home/catalina/imgfile.jpg --verbose

これでサーバを起動したマシン側でPOST requestという文字列が出力されます。

POSTのbodyでバイナリを受け付ける用意をする

サンプルコードでは、HTTP-Requestには文字列が入ってくる前提になっています。画像を受けたいのでここの型を変更します。

まだこのライブラリ使い慣れてないので、bodyにpngファイルがバイナリで1つだけ入っている という前提でいきます。

実現方法としては型を変更してあげればよいです。 大本の呼び出し元の型を変えてあげれば、関連するテンプレート関数が一通り置き換わっていくことになります。

いいですね。テンプレート。(コンパイルエラーになるとメッセージが長すぎて怒られてる気がしてくるけど。)

sessionクラス内で定義されているreq_の型を変更します。

    http::request<http::vector_body<unsigned char> > req_;

これでHTTP-bodyはstd::vectorとしてアクセスできます。

HTTP-Bodyを取り出す

さきほど作ったPOSTの条件分岐のところでBodyを取り出します。

こんな感じで

    // デバッグ用。サイズが一致しているか目視確認したい
    auto size = req.body().size();
    std::cerr << "bodysize = " << size << std::endl;

    // 受信した画像ファイルをOpenCVのmatにする
    cv::Mat rawdata(req.body() );
    auto img = cv::imdecode(rawdata, 1);
    cv::imwrite("received_image.png", img);

今回は直接cv::Matを作りましたが、当然std::vectorで取り出すこともできます。

できた?

思ったより簡単にできてしまいました。

せっかくなので、レスポンスも作っちゃいましょう。

サンプルコードから適当なところをコピって貼れば動く気がします。雑に見えるのは飽きてきたからです。

適当なJSONレスポンスを返す

サーバで画像を受け取りたい理由は、計算資源が潤沢なサーバで画像を解析して、結果をHololensに返したいからでした。

というわけで、「返すべきデータを何か準備できる」という想定のもとJSONを返してみます。

        // レスポンスを返す
        http::string_body::value_type   body;
        body = "{'result':'ok'}";               // TODO : ここでbodyを設定する
        http::response<http::string_body> res{
            std::piecewise_construct,
            std::make_tuple(std::move(body)),
            std::make_tuple(http::status::ok, req.version())};
        res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
        res.set(http::field::content_type, "application/json");
        res.prepare_payload();                  // これ呼ばないとBodyがカラになる
        res.keep_alive(req.keep_alive());
        return send(std::move(res));

なんかあっさりいけました。

感想と今後の展望

最近ご無沙汰していたBoostですが、やっぱり楽しいですね。

エラーメッセージが長すぎて泣きそうになりますが、追っていくとメタな実装になっていて「そのテがあったか」と思わされることが多々あります。

またコンパイル時に型チェックが入るので、仕様から実装した段階で抜けがあると気づけるのもいいところだと思っています。

(たとえばrequestのbodyをfile_body型にすると、std::vectorみたいな使い方はできなくなるので、実装時点でマズイということに気づくきっかけになったりとか。)

今後の展望としては、もはやC++だけでなんでもできるので、実験がはかどります。

クライアント側もサーバ側も同じ言語(C++)で作れるので、性能面はプロトタイプ実装が終わってからでも大丈夫だろうと思っています。モジュールの配置場所を変えるだけなので。 (ただし、モジュール間の結合が疎であるという大前提が必要)

Hololensもバージョンアップきてましたし、いろいろ試してみたいですね。それでは今回はこれくらいで。

chainerUIを試す

以前私がやろうとしていたことを公式がスマートにカッコよくやってのけてくれたので、試してみました。かたりぃなです。

その昔、こんなこと実現しようと考えていました。

catalina1344.hatenablog.jp

そしていつの間にやらPFN公式からこんなの出てました。

https://research.preferred.jp/2017/12/chainerui-release/

すごいですね。さすがPFN。早速試してみます。

実験環境

環境はいつものです。今回はdockerを試してみます

  • Windows10 Pro
  • Docker for Windows
  • python3.6
  • chainer 0.1.0
  • flask
  • nginx

どうしてdocker?

今は実験段階なので、もっと単純に仮想化なしでWindows上で直接chainerを動かすほうが便利だったりします。 ただ、今後クラウド上にサービスを乗せることを考えると、やっぱりそういう移行をしやすい状況を作っておきたいと思うわけです。

ansibleとかやってもいいんですが、クラウドサービスを提供する各社ともdockerサポートを進めている状況を見ると、今後はdockerがいいのかなと考えます。 まあ実験の再現をしやすくなるというメリットが大きいと個人的に考えています。

dockerでchainerを動かす場合のGPUについて

色々と調べてみましたが、Docker for Windows上からだと、GPUを利用する方法がわかりませんでした。

NVidiaが出してるnvidia-dockerだと、まだWindowsはサポートしてないようです。 https://github.com/NVIDIA/nvidia-docker/issues/429

というわけで、今回は面倒なのでGPUまでやらなくてもいいやという結論にたどり着きました。本気でやるときはクラウド課金でnvidoa-docker使えばいいです。(まだ試してはいない)

dockerfileを書く

仮想環境構築って、毎回手間がかかりますよね。どんなパッケージを入れるかとか、設定どうするのがいいかとか。

dockerfileはそのあたりの手順をまとめておくためのものです。

内部的にはRUNごとにファイルシステムの状態がコミットされて~とかあるんですが、まあ、そのあたりは実際に触ってみないとイメージしづらいと思うので割愛。

とりあえずchainerとか必要なものを一式インストールしたイメージを生成するdockerfileです。

FROM centos

RUN yum -y install initscripts MAKEDEV

# SSHサーバ
RUN yum -y install openssh-server
RUN sed -ri 's/^#PermitRootLogin yes/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN echo 'root:root' | chpasswd
RUN /usr/bin/ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -C '' -N ''
RUN /usr/bin/ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -C '' -N ''
RUN /usr/bin/ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -C '' -N ''

# 開発ツール
RUN yum groupinstall -y 'Development tools'
RUN yum install -y git wget cmake
RUN yum install -y sudo

# 文字コード
#RUN yum -y reinstall glibc-common
RUN localedef -v -c -i ja_JP -f UTF-8 ja_JP.UTF-8; echo "";
env LANG=ja_JP.UTF-8
RUN rm -f /etc/localtime
RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# python 3
RUN yum install -y https://centos7.iuscommunity.org/ius-release.rpm
RUN yum install -y python36u python36u-libs python36u-devel python36u-pip
RUN pip3.6 install --upgrade pip
RUN pip3.6 install mtgsdk
#RUN pip3.6 install flask   chaineruiとバージョンかみ合わないので
RUN pip3.6 install chainer
RUN pip3.6 install chainercv
RUN pip3.6 install chainerui==0.1.0

# 共有ディレクトリ
RUN mkdir /mnt/data/

# テスト用のファイル
ADD example.py /root/

EXPOSE 22
CMD bash -c "/usr/sbin/sshd -D"

flaskのところのコメントが悲しいのですが、バージョンの整合性とるのが面倒になったのでこうしました。 chaineruiの現時点の最新版である0.2.0を入れると、flaskの内部で使っているパッケージの最新版とうまくかみ合いませんでした。

なので、flaskのパッケージとうまくかみ合うchainerui-0.1.0を使うことにしました。

つかいかた

上記dockerfileのあるディレクトリに移動して

PS> docker build -t <任意のタグ名> .

です。

ログが流れてsuccessになっていればOKです。 念のため生成されたイメージを確認したいときは

PS> docker images

です。

あきらめた部分

boostとC++opencvあたりも入れたかったのですが、python側との整合性とるのが面倒なので辞めました。 作業で使うことはあるので、別のdockerfileにまとめてあります。

dockerコンテナを起動させる

これで一通りのパッケージが入ったイメージが生成できたので、コンテナとして起動させます。 とはいっても、起動パラメータが多くて覚えるのが大変です。

たとえば、

  • コンテナ名
  • ポートマッピング
  • ホストとのファイル共有
  • ネットワーク設定。。。

等々。

実際にdockerコマンドから起動する場合は

docker run -it --name chainer_test --rm -v /c/Users:/tmp/data -p 30001:22 -d <buildで指定したタグ名>

みたいになるんですが、毎回コレ打ち込むのは面倒ですよね。 テキストでパラメータをメモしておいてコピペして使うとか、シェルスクリプトとして置いておくなども考えられますが、もう少しスマートに解決したいところです。

docker-composeを使う

docker-composeとは、ざっくりいうとvagrant upみたいになのをdockerで実現してくれるものです。

docker-composeはYAMLでパラメータを記述して、upが叩かれるとそのパラメータに従ってコンテナを起動してくれるというものです。 マニュアル見ながら設定したパラメータをファイルにできれば、毎回調べる手間も減りますし、もしパラメータ忘れても安心です。

本来の利用用途はオーケストレーションと呼ばれるもので、複数台のコンテナを楽に管理するときに使います。 今回は単一コンテナですが、記憶力がない私にとっては十分楽になるものでした。

version: "3"
services:
  web:
    image: <任意のタグ名>
    ports: 
      - "30001:22"
      - "30000:5000"
    volumes:
      - "C:/Users/<windowsユーザー名>:/mnt/data/"

このYAMLで定義しているのは

  • <任意のタグ名>のイメージをコンテナとして動かす
  • ホスト:ゲストのポートマッピングは2つ
  • ホスト:ゲストのファイル共有ボリューム

こうしておくと、ポートマッピングと、ボリュームの設定が行われます。 ボリュームはVirtualboxとかでいうホストとの共有ディレクトリみたいなもので、この例では/mnt/dataのディレクトリにホストOSのWindows側のユーザーディレクトリがマウントされます。

sshするときはlocalhost:30001で、flaskが公開するデフォルトポート(5000)へhttpでいくときはlocalhost:30000でいけます。

つかいかた

PS> docker-compose up -d

これでdocker-compose.ymlの指定を読み込んで、コンテナが起動されます。 docker psしてコンテナが起動していることが確認できます。

dockerにssh接続して、chaineruiを起動する

本当はdockerにsshできる環境というのはよろしくないらしいです。 最終的にクラウドとかにデプロイする前提なので、余計な穴は無いほうがいいでしょう。

ただし今の私は実験段階なので、sshできたほうが何かと便利です。既存の仮想マシンと同じように扱えるので。

というわけで、ssh接続してchaineruiを起動します。 Windows10のWSL(旧 bash on ubuntu on Windows)でいきます。

PS1> bash
$ ssh root@127.0.0.1:30001
$ cd /mnt/data/<ユーザー名>

コードを編集しやすいようにWindows側からも見える場所で作業することにします。 以降の作業はwindowsのユーザーディレクトリでやります。

とはいっても、今回はchaineruiのquickstartに従ってexampleを動かしてみるだけですが。

chaineruiのquickstartを試す

こんな感じになってるので、順番に実行していけば動きます。 最後の行だけ少しパラメータを追加しました。(--host=0.0.0.0)

# Initialize ChainerUI database.
$ chainerui db create
$ chainerui db upgrade

# Clone examples of train log and create a project.
$ git clone https://github.com/chainer/chainerui.git
$ cd chainerui

# create your first project
$ chainerui project create -d examples -n example-project

# run ChainerUI server
$ chainerui server --host=0.0.0.0

あとはブラウザからlocalhost:30000にアクセスすればOKです。

--hostパラメータについて

これを付けた理由は、コンテナ外部(ホストOS)のブラウザからアクセスするためです。

chaineruiが使っているWebサーバのflaskはデフォルトで127.0.0.1:5000で待ち受けますが、このIPでは外から見えません。 一旦はLAN内で実験する想定なので、IP=0.0.0.0で待ち受けることにします。

もうちょっとスマートにアクセスしたい

WebサーバにアクセスするためにIPとかポート番号を指定する方法では、実験で色々とマシンを増やしたときに管理が大変になってきます。 今立ててあるマシンは生かしておいて、少しパラメータ変更したマシンで試したいとか。

sshは~/.ssh/configに書けばいいんですが、問題はHTTPです。 hostsファイルに書けばホスト名からIPをひけるのですが、これはホストとIPの関係だけであって、ポートまでは書くことができません。

いろいろやりようはありそうですが、単純にHTTPだけの問題ならHTTPレイヤで解決するのがスマートだと思います。

というわけで、HTTPプロキシとしてNginxを立てます。 最終的にサービス作るときはリバースプロキシみたいに振舞わせることになるかもしれませんね。

dockerでnginx

dockerで構築したイメージを公式が公開してくれてたりします。 今回はnginx公式のイメージを使わせてもらいましょう。

PS> docker pull nginx

このイメージに設定ファイルを与えればいいんですが、dockerfileを書くまでもないようです。 設定ファイルと、ポートの設定さえあればいいので。

というわけでdocker-compose.ymlをこんな感じに作りました。

version: "3"
services:
  nginx_proxy:
    image: nginx
    ports: 
      - "80:80"
    volumes:
      - "./log:/var/log/nginx"
      - "./:/etc/nginx"

このdocker-compose.ymlと同じディレクトリに、次の2つを準備します。

user  nginx;
worker_processes  1;

pid    /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {
  sendfile  on;
  keepalive_timeout  60;

  # local.chainerui.jpでアクセスされたら、localhost:30000に飛ばす。
  server {
    server_name local.chainerui.jp;
    proxy_set_header Host $http_host;
    location / {
      proxy_pass http://localhost:30000;
    }
  }
}

これで、HTTPのHOSTヘッダがlocal.chainerui.jpのリクエストはlocalhost:30000にパスされます。 local.chainerui.jpは適当につけた名前です。ドメイン持っているわけではないです。

で、localhost:30000はdockerのネットワークを経由して先ほど立てたchaineruiコンテナの5000番にパスされます。 そしてコンテナ側のポート5000はflaskが口を開けて待っているhttpポートなので、無事にchaineruiの画面が見れるというわけです。

ちなみにwindows側のhostsファイルはこんな感じで追記します。

127.0.0.1   local.chainerui.jp

今後の展望

普段とは違うインフラ寄りなことをしたおかげで、いい気分転換になりました。 何かに詰まったり躓いたりしたときは気分転換でこういうことをやるのもいいですね。

chainerui動かして気づいたのは、DBがsqliteなので、コンテナ内にDBとして使うファイルが生成されてしまいます。 どういうことかというと、docker再起動のたびにデータが消えるということを意味します。

これは実際に使い始めたら改善したいですね。たとえばストレージ用ボリュームを作るとか。

それでは今回はこれくらいで。

hololensの空間マッピングに触れる

久しぶりのブログ記事です。かたりぃなです。 Hololensの空間マッピングC++からいじっていて、とりあえずだいたいどんなものか掴めたので整理します。

実際に動いているものはまだないので、インパクトは小さいですが、こういう小さな積み重ねが大事だと思ってます。

目的

まず空間マッピングを使う目的について。これは2つあって、

  1. カードゲームARでカードを検出しやすくなりそう
  2. ARの可能性の一つとしてHololens特有の機能(=空間マッピング)を試したい

少々話が長くなりますが、整理してみます。

平面マーカー(カード)を検出しやすくするアイデア

まず私がやりたいことは既存カードゲームのAR化で、技術的には画像からの特定物体検出+コンテンツのレンダリングです。

マーカー型ARで有名なvuforiaでは、たとえば二次元マーカーとして「特定の絵柄をもったカード」を検出できます。 しかし、カードゲームは絵柄が多いので特定の絵柄をマーカーとして検出しても意味がありません。 「カードらしきもの」を検出して、絵柄の分類は別タスクとして処理したいというわけです。

で、絵柄を除いた「カードらしきもの」とは何かというと、画像データでいうとカードの外枠だったり、輪郭だったりしか残らないわけです。 これを画像データから直接検出するには少々難易度が高いので、次のようなフローを考えています。

  1. 空間マッピングデータを解析して、ゲームが行われているテーブルを見つける
  2. 三次元空間上のテーブルの平面を、二次元平面に変換する方法fを求める
  3. 方法fをカメラの二次元画像に適用して、テーブル平面を画面に投影した画像を生成する
  4. 画像からカードを検出する

こうすると何がうれしいかというと、4の段階では「机を真上から撮影した画像」であるという前提が得られるので、処理が簡単かつ高速になると考えます。

Holensの空間マッピングを試す

Microsoftがサンプルコードを提供してくれているのでこれを使います。

https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/HolographicSpatialMapping

ライセンスはMITのようです。 https://github.com/Microsoft/Windows-universal-samples/blob/master/LICENSE

以下の文章はこのサンプルコードに関する説明になります。

そもそも空間マッピングとは?

Hololensでは装着者の周囲の環境を読み取ることができます。

乱暴に解釈するなら3Dスキャン的なもんです。装着して部屋の中歩き回れば、どんな空間なのかがわかります。

ここで注意しなくてはいけないのは、Hololensの空間マッピングで識別できるのは「特定の位置に何かがある」ということだけです。

それがテーブルなのか、本棚なのか、そこまではHololensのAPIは関与しません。理由は以下に説明します。

Hololensはどうやって空間マッピングをしているの?

Hololensの空間マッピングではおそらくKinnectと同じく赤外線照射による空間認識だろうといわれています。

赤外線を使った距離測定器みたいなものだといえば伝わりやすいかと思います、

ただ、別の手段を使えばHololens以外でも空間マッピングは(ある程度は)可能と考えています。

たとえば有名なアルゴリズムでPTAMとかDTAMなどがありますが、あれらはカメラの映像を解析して三次元推定をします。

最近ではDNNとかでやってる例もありますね。

実際にHololensの空間マッピングを使ってみる

実際にやってみました。 とはいえ、ほとんどMSが提供しているサンプルコードのままなので、あれをそのまま読んで意味を理解できる人なら、以降の情報は役に立たないかと思います。

Hololensで動かすアプリはUWPアプリとなるので、この形式にあわせて作ります。

開発用の言語は、C++でいきます。私はC++が好きなので。いわゆるC++/cxですね。

VisualStudio2017のテンプレートにあるHolographicのDirectX11プロジェクトをベースに作業していきます。

答えだけ欲しい人は、UWPのHolographicサンプルコード中のSpatialMapping周りのクラスとレンダリング用のシェーダーをコピペすれば動きます。

以下はサンプルコードを調べた内容のメモです。

アプリのパッケージマニフェストの設定

アプリがどんな機能を利用するかをマニフェストファイルに記載していきます。

VisualStudioが生成したプロジェクトにPackage.appmanifestというファイルが含まれているので、これを編集します。

注意点としては、このファイルを普通にVisualStudioで開くとGUIからの設定画面になってしまうので、右クリックとかで適当なテキストエディタで開きます。

Holographic Academyのチュートリアルどおり次のように編集します。

  • uap2の追加
  • spatialPerceptionの追加
<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2"
  IgnorableNamespaces="uap uap2 mp">
  <!--- 中略 --->
  <Capabilities>
    <uap2:Capability Name="spatialPerception"/>
  </Capabilities>
</Package>

これで空間マッピング系のAPIを呼び出せるようになりました。

空間マッピングが利用可能かどうか調べる

やっとコードの編集まできました。

まずは空間マッピングの機能を使えるかどうか、調べる必要があります。 こんな感じです。

    auto initSurfaceObserverTask = create_task(SpatialSurfaceObserver::RequestAccessAsync());
    initSurfaceObserverTask.then(
        [this, currentCoordinateSystem]
        (Windows::Perception::Spatial::SpatialPerceptionAccessStatus status)
    {
        switch (status)
        {
        case SpatialPerceptionAccessStatus::Allowed:
            return true;
            break;
        default:
            return false;
            break;
        }
    });

空間マッピングのフォーマットを決める

空間マッピングで得られる情報は、PointCloud形式ではなくアプリケーションレイヤではポリゴンデータとなっています。 ここではその設定をしています。

PointCloudとポリゴンの違いは、乱暴に言うと点と面の違いです。 ポリゴンになっているとそのままDirectXレンダリングに渡せるので楽ですね。

さて、ここでのフォーマットですが、

  • VertexPositionFormatに頂点の表現方法を設定する
  • VertexNormalFormatに法線の表現形式を設定する

だけです。 浮動小数点形式はHololensでは受け付けてくれませんでした。

ここで注意が必要なのは次のフォーマットです。

DirectXPixelFormat::R16G16B16A16IntNormalized

一般的なフォーマットならfloat要素3つ(もしくはアクセス効率を考慮してfloat要素4つ)で一つの頂点が表現されますが、このフォーマットは「正規化された符号付き16bit整数」だと言っています。

念のため、、、3次元空間x,y,zの要素なのに4要素使っているのは、よくあるアクセス効率のためと考えます。 1つの頂点表現が32bitもしくは64bit境界を跨ぐとアクセス効率が極端に悪くなるので。

頂点表現の話に戻ります。

細かい部分は置いといて、概要だけいうと符号付き16bit値(C言語でいうsigned short型)を-1.0 ~ 1.0の範囲にマッピングする符号付き固定小数点です。

つまり整数として読んだときに32768が1.0, -32767が-1.0です。

ただし、このマッピングだけだと誤差があるので、正確な情報は上記フォーマットのマニュアルページを参照してください。

    m_surfaceMeshOptions = ref new SpatialSurfaceMeshOptions();
    IVectorView<DirectXPixelFormat>^ supportedVertexPositionFormats = m_surfaceMeshOptions->SupportedVertexPositionFormats;
    unsigned int formatIndex = 0;
    if (supportedVertexPositionFormats->IndexOf(DirectXPixelFormat::R16G16B16A16IntNormalized, &formatIndex))
    {
        m_surfaceMeshOptions->VertexPositionFormat = DirectXPixelFormat::R16G16B16A16IntNormalized;
    }
    IVectorView<DirectXPixelFormat>^ supportedVertexNormalFormats = m_surfaceMeshOptions->SupportedVertexNormalFormats;
    if (supportedVertexNormalFormats->IndexOf(DirectXPixelFormat::R8G8B8A8IntNormalized, &formatIndex))
    {
        m_surfaceMeshOptions->VertexNormalFormat = DirectXPixelFormat::R8G8B8A8IntNormalized;
    }

空間マッピングのデータを受け取るためのイベントハンドラを登録する

空間マッピングのデータをアプリケーションが受け取るためのイベントハンドラを登録し、ハンドラ内で受け取ったデータを好きなように料理しましょうという流れです。

複数回空間マッピングのデータを採取すると同じものが取れる(空間内のオブジェクトに変化がないということ)ので、サンプルコードでは上手に弾くように実装されてます。

    m_surfaceObserver = ref new SpatialSurfaceObserver();
    if (m_surfaceObserver)
    {
        m_surfaceObserver->SetBoundingVolume(bounds);

        // If the surface observer was successfully created, we can initialize our
        // collection by pulling the current data set.
        auto mapContainingSurfaceCollection = m_surfaceObserver->GetObservedSurfaces();
        for (auto const& pair : mapContainingSurfaceCollection)
        {
            // Store the ID and metadata for each surface.
            auto const& id = pair->Key;
            auto const& surfaceInfo = pair->Value;
            m_meshRenderer->AddSurface(id, surfaceInfo);
        }

        // We then subcribe to an event to receive up-to-date data.
        m_surfacesChangedToken = m_surfaceObserver->ObservedSurfacesChanged +=
            ref new TypedEventHandler<SpatialSurfaceObserver^, Platform::Object^>(
                bind(&spatial_plane_detectionMain::OnSurfacesChanged, this, _1, _2)
                );
    }

空間マッピングデータを受け取るイベントハンドラ本体

上記で登録したイベントハンドラはこうなっていました。 AddOrUpdateという名前のとおり、同一のメッシュに対する処理がしっかりされています。

void spatial_plane_detectionMain::OnSurfacesChanged(
    SpatialSurfaceObserver^ sender,
    Object^ args)
{
    IMapView<Platform::Guid, SpatialSurfaceInfo^>^ const& surfaceCollection = sender->GetObservedSurfaces();

    // Process surface adds and updates.
    for (const auto& pair : surfaceCollection)
    {
        auto id = pair->Key;
        auto surfaceInfo = pair->Value;

        if (m_meshRenderer->HasSurface(id))
        {
            if (m_meshRenderer->GetLastUpdateTime(id).UniversalTime < surfaceInfo->UpdateTime.UniversalTime)
            {
                // Update existing surface.
                m_meshRenderer->UpdateSurface(id, surfaceInfo);
            }
        }
        else
        {
            // New surface.
            m_meshRenderer->AddSurface(id, surfaceInfo);
        }
    }

    m_meshRenderer->HideInactiveMeshes(surfaceCollection);
}

メッシュデータを取り出す

ここでは頂点、法線、インデックスなど、レンダリングするために必要な情報を取り出して、surfaceMeshクラスに放り込んでいます。 このとき重要になるのが、メッシュの変換行列です。

上述のとおり、メッシュ表現に使われる頂点座標は上限が1.0で下限が-1.0です。Hololensの座標系はメートル単位なので、このままでは1メートル四方のものしか表現できないことになってしまいます。

んで、そんな不便なわけなくて、ただ単純に行列のスケール要素で処理すれば元の大きさになりますよという話です。

サンプルコードではレンダリングするだけなのでConstantBufferに入れて終わりです。

Concurrency::task<void> RealtimeSurfaceMeshRenderer::AddOrUpdateSurfaceAsync(Guid id, SpatialSurfaceInfo^ newSurface)
{
    auto options = ref new SpatialSurfaceMeshOptions();
    options->IncludeVertexNormals = true;

    // The level of detail setting is used to limit mesh complexity, by limiting the number
    // of triangles per cubic meter.
    auto createMeshTask = create_task(newSurface->TryComputeLatestMeshAsync(m_maxTrianglesPerCubicMeter, options));
    auto processMeshTask = createMeshTask.then([this, id](SpatialSurfaceMesh^ mesh)
    {
        if (mesh != nullptr)
        {
            std::lock_guard<std::mutex> guard(m_meshCollectionLock);

            auto& surfaceMesh = m_meshCollection[id];
            surfaceMesh.UpdateSurface(mesh);
            surfaceMesh.SetIsActive(true);
        }
    }, task_continuation_context::use_current());

    return processMeshTask;
}

メッシュデータをもとにDirectXレンダリングするためのリソースを作る

このあたりのコードはもはやお約束ですね。 サンプルコードを引用するの疲れたので省略します。

重要なポイントだけ列挙すると

  • メッシュデータのスケールは、シェーダー内でやっている
  • 頂点フォーマットの形式は、空間マッピングで取得したものをそのまま受け渡せるように設定する

といったところでしょうか。

スケールの設定は、モデルの大きさをプログラムから変更するようなことをしている場合には注意ですね。行列の乗算順序を意識しておかないと、おかしくなります。

頂点フォーマットについては、普通のDirectXアプリでは頂点データは32bit浮動小数点形式で受け渡すことが多いと思いますが、今回は符号付き16bit形式なので注意が必要です。

opencv-contribのedge-boxesを試す

恒例となりつつあるホリデーシーズン前のOpenCV最新版がリリースされました。

https://opencv.org/opencv-3-4.html

個人的に興味があるのはこのあたりです。

  • 本家DNNモジュール
    • YOLOやfaster-rcnnなど、RCNNの実装
    • YOLOもRCNNもバックエンドでOpenCLアクセラレーションをサポート
    • JSからDNN叩けるよ!
  • contribのximgprocモジュール

今回はObjectProposalを試してみます。YOLOとかのDNNまわりも興味ありますが、こちらははDeepLearningマジ勢の方々がすでにいろいろ試してくれていますので。こういうのは私は後追いでいかせてもらいます。

というか、上記のリリースノート見る限り、「JSからDNN叩けば裏で勝手にOpenCLアクセラレーションが走ってGPGPU的なことやるよ」に読めるのですが、もうなんか言葉で表現できないですね。

ちなみにNVidiaGPUでも一応OpenCLインターフェース備えてるので、OpenCLカーネルを叩き込んであげれば動きはするようです。速度比較まではしてませんが。。。

さて、前置きが長くなりましたが、edge-boxesを試す理由は、カードゲームのAR化と汎用化を考えたとき、カードの領域は画像データ上ではエッジとなることが多いので、このアルゴリズムでラクできるんじゃないかなという考えからです。

一般的にはR-CNNの前段で使うことが多いようです。

ObjectProposalとは

おおざっぱに言うと、ある画像を与えたとき「このあたりがオブジェクトかもしれないよ」という候補領域を提案することをいいます。

問題領域としては画像のセグメンテーションになるようです。

objectProposalのアルゴリズムは色々ありますが、今回はedge-boxesを試します。

edge-boxesとは

Microsoftの研究部門であるMicrosoftResearchが考えてくれたアルゴリズムです。論文はこのあたりから。

https://www.microsoft.com/en-us/research/publication/edge-boxes-locating-object-proposals-from-edges/

動かしてみる

とりあえず動かしてみます。

ビルドする

まずopencvをビルドするときに、次の2つを設定しておきます。

  • contribモジュールの追加
  • サンプルプログラムの生成

サンプルプログラムは

https://github.com/opencv/opencv_contrib/blob/master/modules/ximgproc/samples/edgeboxes_demo.cpp

にあるので、ビルド時間を少しでも短縮したいときはこれだけをあとでビルドしてもいいです。

実行するためのデータを用意する

こういうデータはopencv_extraリポジトリに置かれているので、cloneしときます。

実際に使うデータはこいつです。 https://github.com/opencv/opencv_extra/blob/master/testdata/cv/ximgproc/model.yml.gz

どうしてモデルデータが必要なの?

モデルデータというと機械学習アルゴリズムで出てくることが多いので、edge-boxes自体も機械学習なのかと勘違いしてしまいますが、edge-boxes自体は機械学習を含みません。

edge-boxesアルゴリズムに入力するための構造化されたエッジ情報を生成するモジュール(StructuredEdgeDetection)がモデルデータを必要としているだけです。

StructuredEdgeDetectionがそういうものなので、それに従います。

いざ、実行

コマンドラインから次のように実行します。

edgeboxes_demo model.yml.gz test.jpg

これでオブジェクト領域候補を書き込んだ画像が表示されます。 仮想環境にsshで作業していてウインドウ表示できない人はコードを修正して画像ファイルに落とすようにすればOKです。

#if 0
  imshow("im", im);
  waitKey(0);
#else
  imwrite("output.jpg", im);
#endif

結果

f:id:Catalina1344:20170530225057j:plain
input
f:id:Catalina1344:20171230235431j:plain
output
不要な領域も抽出していますが、そういうものなので。おおむね期待通りといったところです。

パラメータチューニングしようと思いましたがマニュアル見てもよくわかんないので、論文と照らし合わせる必要があります。

https://docs.opencv.org/3.4.0/d4/d0d/groupximgprocedgeboxes.html

論文の数式中のα、β、ηとかをそのままパラメータにするのは優しくない。。。

感想と今後の展望

2017年は色々なことに挑戦しました。

特に私がやりたいARを実現するために、AIを活用するという目論見も可能性は見えてきた点はGoodです。

7月以降は少々別のことをやっていて、このブログの更新が滞っていた点はbadかなと。ちょうど本業のほうでも色々変化がありましたので、余裕がなかったという事情もありはするのですが。。。

本業はDeepLearningでもARでもないのですが、クラウドサービスに触れる機会を頂けたので、興味深く取り組んでいます。 ただ言われたことだけやってても眠いだけなので、自分でサービス立ち上げる時を想定して、たとえばDeepLearningをクラウドで実行するにはどう応用していけばいいのだろう?などをモチベーションとして取り組んでいます。

少し話題が変わりますが、別のブログ立ち上げて色々メモしつつ楽しむようになりました。 キーワードとしては「3Dモデリング」「3Dプリンタ」「コスプレ」あたりでしょうか。

http://catalina-cosplay.hatenablog.jp/

実際に3Dプリンタで色々と作ってみて改めて思ったことは、 「私が作りたいのは『仮想世界』とか『現実世界を便利にする』とかではなく、『仮想世界と現実世界の融合』なんだな」ってことです。

たとえば今のARとか3Dプリンタで作ったものを工夫すれば、こんなのができるだろうと思っています。

  • Hololensを装着して超電磁砲(レールガン)発射を体験できる
  • Hololensを装着して聖ジョージの聖域(セントじょーじのせいいき) を幻想破壊を体験できる
  • Hololensを装着して海馬コーポレーションのデュエルシステムを体験できる

などなど。

要は空想科学とか魔法とか使えると思っています。

今そこに自分が実現したいことを実現する手段があるなら、僕は全力でやっていきたいと思います。

理想だけ述べるなら独立すればいいんですが、算段がないのに独立はリスクが高すぎるんですよね。なので来年も本業をこなしつつ趣味でサービス開発をするという生活かなと思っています。

それではみなさん良いお年を。

CMakeを使ってmakefileを作る

はじめに

C/C++でプログラミングをしているとmakefileが必要になってきます。

VisualStudioみたいなIDE環境なら自動生成に任せてしまうのですが、Linux環境だとそういうものを使うこと自体が面倒だったりします。

小さなツール群を組み合わせて使っていったほうが後々幸せになれるケースが多いと思ってます。

というわけで、今回はmakefileを自動生成するツールとしてCMakeを試してみます。

総括

とりあえずこういう書き方すればc++11でboostをリンクできますよ的なの。 記事の上のほうに書いておくと私が忘れてしまったときにコピペしやすいので。

cmake_minimum_required(VERSION 3.0)
project(rest_test1)
set(CMAKE_CXX_FLAGS "-std=c++11")
add_executable(rest_test t.cpp)
target_link_libraries(rest_test boost_system)

CMakeの前にautotoolsについて

CMakeの前にmakefileの自動生成について。

まずmkaefileを作るツールというのは昔から存在しています。

歴史的にみると、古くはautotools(ソースコードからパッケージインストールするときにautofonfとかconfigureとかやるアレです)とかがあって、プロジェクトによりますが現役でも充分使えるものです。

https://ja.wikipedia.org/wiki/Autotools

じゃあautotolsでいいのでは?って思いますが、これは入力がm4とシェルの合わせ技になっていて、「ラクしてmakefile書きたい」ってのを満たせません。

makefileをラクして書くとは?

まずmakefileについて。 makefileに書く内容はいろいろありますが、大まかに列挙すると以下のとおりです。

これをラクして書きたいのです。

今のmakefileが不便な理由は、私が$とか<とかの記号や暗黙のルールを覚えられないからです。

記号やルールの意味を調べてmakefileを綺麗に書こうとすると、それだけで時間とられまくるので悲しくなります。

できることなら上記のものを列挙するだけで済ませたいわけです。そしてcmakeならこの目的を達成できます。

CMakeの基本

とりあえずビルドするだけのものです。

cmake_minimum_required(VERSION 3.0)
project(test_project)
add_executable(executable_file test.cpp)

各パラメータの意味は

  • cmake3.0以降
  • test_projectというプロジェクト名
  • executable_fileを生成するための依存関係としてtest.cppというファイルを追加

まずはmakeしてみる

適当にtest.cppを作って動かしてみます。makeするのが目的なので、mainさえあればいいです。

int main()
{
  return 0;
}

cmakeにmakefileを生成させます。centos7環境なのでver3のcmakeはcmake3コマンドになります。

$ cmake3 . 
-- Configuring done
-- Generating done
-- Build files have been written to: /test_project
$ ls Makefile
Makefile

Makefileができたのでmakeしてみます。

$ make
Scanning dependencies of target execfile
[ 50%] Building CXX object CMakeFiles/execfile.dir/t.cpp.o
[100%] Linking CXX executable execfile
[100%] Built target execfile
$ ls execfile -l
-rwxrwxr-x 1 user user  8504 <日付> execfile

実行ファイルができました。簡単ですね。便利ですね。

コンパイルオプションを指定したい

C++の新しい機能を使うにはコンパイルオプションを指定する必要があります。 というわけでオプションを指定します。 このやり方が正しいかどうかよくわかっていないので、もしコピペするときは自己責任でお願いします。

cmakeに次の行を追加します。位置はprojectの後くらいで。

set(CMAKE_CXX_FLAGS "-std=c++11")

これでC++11の記述ができるようになります。とりあえずラムダ、関数戻り値後置記法、auto型を使ってみます。

int main(){
    auto lambdatest = []() -> int {std::cout << "hello world" << std::endl; return 0;};
    return lambdatest();
}
$ cmake3 .
-- Configuring done
-- Generating done
-- Build files have been written to: /test_project
$ make
[ 50%] Linking CXX executable execfile
[100%] Built target execfile
$ ./execfile
hello world

いけました。

これでコンパイル時のオプション指定は何でもできそうです。

リンクオプションを指定したい

C++といえばboostですね。リンクしてみましょう。 ただしboost単体ではアプリっぽいことはできなくてつまらないので、MSのrestsdkをリンクしてみます。

boostは以下のサイトを参考にインストールしておきます。

https://boostjp.github.io/howtobuild.html

MSのrestsdkは次のサイトを参考にインストールしておきます。

https://github.com/Microsoft/cpprestsdk/wiki/How-to-build-for-Linux

現時点でのrestsdkはCMake3以上が必要でboostも必要になります。

手元の環境ではインストールが終わるとパスも通っていたので、ライブラリのファイル名を指定するだけでOKでした。

CMakeLists.txtに追記します。 また、以降は出力ファイル名はrest_testとなるよう変更しています。

restsdkと、その依存ライブラリをリンク指定します。

target_link_libraries(rest_test boost_system)
target_link_libraries(rest_test crypto)
target_link_libraries(rest_test ssl)
target_link_libraries(rest_test cpprest)

makeしてみます。

$ cmake3 .
-- Configuring done
-- Generating done
-- Build files have been written to: /test_project
$ make
[ 50%] Linking CXX executable execfile
[100%] Built target execfile

ライブラリが正しくリンクできているか見てみます。

$ ldd rest_test
        linux-vdso.so.1 =>  (0x00007ffe3fbdd000)
        libboost_system.so.1.55.0 => /usr/local/lib/libboost_system.so.1.55.0 (0x00007f9cffc02000)
        libcrypto.so.10 => /lib64/libcrypto.so.10 (0x00007f9cff773000)
        libssl.so.10 => /lib64/libssl.so.10 (0x00007f9cff501000)
        libcpprest.so.2.10 => /usr/local/lib/libcpprest.so.2.10 (0x00007f9cfe832000)
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f9cfe529000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f9cfe227000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f9cfe011000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f9cfdc4f000)
        librt.so.1 => /lib64/librt.so.1 (0x00007f9cfda47000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f9cfd82b000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f9cfd626000)
        libz.so.1 => /lib64/libz.so.1 (0x00007f9cfd410000)
        libgssapi_krb5.so.2 => /lib64/libgssapi_krb5.so.2 (0x00007f9cfd1c3000)
        libkrb5.so.3 => /lib64/libkrb5.so.3 (0x00007f9cfceda000)
        libcom_err.so.2 => /lib64/libcom_err.so.2 (0x00007f9cfccd6000)
        libk5crypto.so.3 => /lib64/libk5crypto.so.3 (0x00007f9cfcaa3000)
        libboost_random.so.1.55.0 => /usr/local/lib/libboost_random.so.1.55.0 (0x00007f9cfc89f000)
        libboost_thread.so.1.55.0 => /usr/local/lib/libboost_thread.so.1.55.0 (0x00007f9cfc687000)
        libboost_filesystem.so.1.55.0 => /usr/local/lib/libboost_filesystem.so.1.55.0 (0x00007f9cfc470000)
        libboost_chrono.so.1.55.0 => /usr/local/lib/libboost_chrono.so.1.55.0 (0x00007f9cfc268000)
        libboost_atomic.so.1.55.0 => /usr/local/lib/libboost_atomic.so.1.55.0 (0x00007f9cfc066000)
        libboost_date_time.so.1.55.0 => /usr/local/lib/libboost_date_time.so.1.55.0 (0x00007f9cfbe55000)
        libboost_regex.so.1.55.0 => /usr/local/lib/libboost_regex.so.1.55.0 (0x00007f9cfbb46000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9cffe07000)
        libkrb5support.so.0 => /lib64/libkrb5support.so.0 (0x00007f9cfb938000)
        libkeyutils.so.1 => /lib64/libkeyutils.so.1 (0x00007f9cfb733000)
        libresolv.so.2 => /lib64/libresolv.so.2 (0x00007f9cfb519000)
        libicuuc.so.50 => /lib64/libicuuc.so.50 (0x00007f9cfb19f000)
        libicui18n.so.50 => /lib64/libicui18n.so.50 (0x00007f9cfada0000)
        libicudata.so.50 => /lib64/libicudata.so.50 (0x00007f9cf97cc000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f9cf95a4000)
        libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f9cf9342000)

よさそうです。

トラブルシューティング

期待したオプションが入っているかどうかは、CMakeFiles以下にmakeに関するものがごっそり入っているので、grepかけてあげればだいたい出てきます。

ちょっとだけいじって試したいってときはここを直接変更すれば試せたりしますが推奨できません。 cmakeで上書きされるので、変更した結果を正しく反映するにはCmakeLists.txtを変更する必要があるためです。

感想と今後の展望

CMake便利です。今後もどんどん使っていこうと思います。

chainercv0.6付属のSSDサンプルコードを読んでいく

背景

chainercvが0.6になって、rcnnの実装のひとつであるSSDの訓練をするためのサンプルコードが提供されるようになりました。

論文読んでも少々わからない部分があったので、実際にネットワークの訓練コードを動かしながら追っていきたいと思います。

まだ理解しきれていない部分もありますので誤解・間違いなどあるかもしれません。

環境はいつものです。chainerとchainercvのバージョンだけあがってます。

  • Windows 10 Pro
  • Python 3.5.2 : Anaconda 4.2.0 (64-bit)
  • chainer 2.1.0
  • chainercv 0.6.0

SSDの何を知りたいのか

SSDの論文はこちら

https://arxiv.org/abs/1512.02325

今回知りたかったのは「一枚の画像からN個のオブジェクトを検出することを考えたとき、その個数Nとそれに付随する領域はネットワークの出力ではどのような表現になっているのだろう?」という内容です。

まだ私の中での答えはでていませんが、少しずつやっていったほうがモチベーション維持に繋がるので、ここで一旦ブログに残そうといった次第です。

ちなみにopencv本家でもYOLOという物体検出アルゴリズムがマージされたようなので、あちらも安定板がリリースされ次第試してみたいですね。

ssd学習データの入力を確認する

まずSSDの訓練で使われているデータがどんなものか見てみることにします。

/examples/ssd/train.pyにあるTransformクラスのcall関数に次のコードを入れてみます。

このクラスは不変性を高めるためにデータを加工するのが目的のようです。 訓練データをネットワークに入力する前に変換を行うことで、それらの変換に対する不変性を得られるという話ですね。

というわけで変換をする直前に入力データと教師データの組を出してしまえば実際の訓練データが見れるわけです。

コード末尾exitしてるのは、この関数は訓練データの入力ごとに呼び出されるためです。 まあ一個だけでもサンプル見れればいいかなという乱暴な実験です。

これでSSDを使った識別とと同じような出力結果を得ることができます。

import cv2
from chainercv.datasets import voc_detection_label_names

        oimg = img.transpose(1,2,0) # channel,width,heightの順に並んでるので、width,height,channelにする
        for b,l in zip(bbox,label):
            # 正解ラベル名を得る
            object_name = voc_detection_label_names[l]
            # 正解の矩形領域を表示
            red = (255,0,0)
            cv2.rectangle(oimg, (b[1], b[0]), (b[3], b[2]), red, 2 )
            # 正解のラベルを表示
            cv2.putText(oimg, object_name, (b[1], b[0]), cv2.FONT_HERSHEY_SIMPLEX, 1, red)
        # 保存して終了
        cv2.imwrite('transformed.jpg', oimg)
        exit()

コードの説明

画像に変換をかけているコードを見れば、だいたいどんな感じのことやってるのかわかるので、そのあたりは省略します。

読むときのポイントとして、正解ラベルと領域は1つではないことに気を付ければよいかと思います。

あと上記コードで画像として出力するために私が慣れているopencv関数を使っています。x,yの順序, カラーチャネルの順序に要注意です。

ちなみにこのコードではカラーチャネルの順序を意識していないので、色味がおかしくなります。(デフォルトでpillowはRGB, opencvはBGR。)

最後に、ラベル名を表すvoc_detection_label_namesという変数は chainercv/datasets/voc/voc_utils.py に定義されています。

VOCデータセットでの正解ラベルってことですね。

ネットワークグラフを見てみる

trainerがあるので、グラフをダンプさせれば見れます。

trainer.extend(extensions.dump_graph('main/loss'))

ダンプしたファイルはdot形式なので、例によってgraphvizのdotコマンドで画像化します。

dot -Tpng cg.dot -o network-graph.png
# もしくは
dot -Tsvg cg.dot -o network-graph.svg

pngだとネットワークが大きすぎて超巨大な解像度の画像になってしまうので、ベクター形式(svg)のファイルのほうが幸せになれそうです。

こんなのできました。 svg形式張れなかったのでpngです。 f:id:Catalina1344:20171014000310p:plain

論文と照らし合わせて確認

まだ論文全部を理解しきれてないので、概要だけざっと照らし合わせます。

入力画像からの特徴抽出

ネットワークの前半(画像での上のほう)はVGG16と呼ばれるネットワーク構造で、特徴抽出を目的としています。 これはリンク先の文書中ではbase networkと呼ばれているものです。必ずしもVGG16でなければならないというものではないようです。

ネットワークの後半部分

スケールを変化させつつ、それぞれの解像度で特徴抽出と分類をしているように見えますが、よくわかりません。 分類結果をconcatしているようにも見えるのは何なのだろう。。。 私にはまだ早すぎたのかもしれません。一度に全部理解するのは大変なので、少しずつマイペースで調べていきましょう。

とりあえず利用することを目的にする

この分野は進歩が速いので、せっかく中身を把握してもまた新しいネットワークが提案されるということは充分にあり得ます。

というわけで、それらを利用する観点からも分析をしてみます。 ネットワークを利用する簡単な方法は、訓練データを自分がやりたいように改変してあげればよいかと思います。

自分の環境では訓練データは次の場所でした。

C:\Users\ <ユーザー名>.chainer\dataset\pfnet\chainercv\voc\VOCdevkit\VOC2012\

また、SSDの訓練サンプルで使用されている学習データのパスは次のファイルを指しています。

ImageSets\Main\train.txt

訓練データの概要

SSDのサンプルコードを例に動作概要を示します。 まず上記train.txtファイルから適当なレコードを取り出します。

たとえば一行目は "2008_000008" です。

これはAnnocationsとJPEGImagesディレクトリのファイル名(拡張子を除く)を示していて、 画像ファイルと、その正解データの組を表現しています。

上記ファイル名を例にとると、

  • Annotations/2008_000008.xmlに画像内の情報(どこに何があるか)
  • JPEGImages/2008_000008.jpegが実際の画像

というわけですね。

おまけ

SSDでは検出したオブジェクトのバウンディングボックスを正解として使うため、詳細な輪郭情報とそれに付随するラベルは使われていません。

データとしては SegmentationClassディレクトリとSegmentationObjectディレクトリがあって領域とラベル情報が入っているようなのでそのうち詳しく見てみたいところです。

感想と今後の展望

R-CNNのSSDの中身までは理解できませんでしたが、使い方はおおよそ目星がつきました。

私自身はカードゲームのAR化,その後のARアプリの拡張を目標としているので、この訓練データの一部を自前の訓練データで置き換えてやればいい結果が出せるかもしれません。

とりあえず今回はこれくらいで。