catalinaの備忘録

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

2018年振り返り

年の瀬も迫ってきたので、今年一年を振り返ってみようと思います。かたりぃなです。

よかったこと

ちょっと残念

  • DeepLearning

  • Unity

  • C++オンリーでHololens開発の限界

よかったこと詳細

インフラまわり色々できた

なんやかんやで、今年はインフラをいじってることが多かったと思います。 インフラの具体的な技術要素としては

こんなところでしょうか

2018年に身に着けた技術で特に大きかったのはAWSとansibleですね。

Hololensなどのアプリを作る場合のアーキテクチャ設計を考えたとき、クラウドとの連携は必要だと考えています。 これはHololensに限らずモバイルアプリ全般に言えることです。

そのための土台作りができたということで、かなり大きな前進です。

これらをうまく活用して開発を進めていきたいですね。

開発環境のコンテナ化

ローカル開発環境はdockerを本格的に使うようになりました。

くーべねてぃす?とやらはまだ試していませんが、dockerfile, docker-composeあたりを駆使して色々な環境を試しています。

軽く試したコンテナとしては

などです。

dockerでやってみて個人的にいいなと思ったのが次のものです

  • 環境の固定, 実験の再現のしやすさ
  • マイクロサービス

前者はこのブログでも時々書いてますが、docker-compose up すれば状況を再現できるので、すごく楽です。

後者のマイクロサービスはコンテナ化と合わせて考慮していきたい事項です。

マイクロサービスを目指すモチベーションは、これまでの開発で個人的に悲しい案件が多々あって、それを回避する手段として使えそうだなというところです。

これまでの開発で個人的に経験した悲しい事案として

  • chainerとflaskを組み合わせたい
    • flask最新版では動かない
  • rubyのバージョン上げた
    • 同居してるfluentdが死んだ
  • PHPのバージョンを上げた

こんなのがありあす。

そもそも上記悲しい事案は複数サービスが1つの環境に同居しているから起きるのであって、分離してしまえば何も問題はないはずです。

分離の方法としては言語ごとにあるenvで回避できそうな気もします(pythonだとpyenvとか)が、言語ごと/モジュールごとにそれをやっていくのはどうなの?的な思いがあります。

各言語のプロフェッショナルならenv使って上手に回避できるのかもしれませんが、私の母国語はC,C++ということもあってライブラリの管理が少々面倒というところもあって、個人的にはマイクロサービス化とコンテナ化推しです。

そのうちAWSのほうもdockerで動かしてみたいですね。ECS(elastic-container-service)というサービスらしいです。

通信プロトコル

クラウドとHololensの連携との話をさらに掘り下げると、具体的にWebフロントどうするかとか、プロトコルどうするとかあります。 webサーバはnginxで色々できるようになってきたので、これでいけそうと思ってます。apacheでもいいですけど。

通信プロトコルの現状の実装はHTTPSのRESTですが、セッション張り直しのコストとか考えるとWebSocketかなと思ってます。

というわけで、カードゲームのARのための最低限の土台が整ったと思います。 性能面とか考慮しつつ、今後どうするか色々試していきたいですね。

WebとHololensの連携

まだ完成していないので記事にしていないのですが、HololensとWebで少しずつ連携できるようになってきました。

通信部分はただのRESTではありますが、重要なのはWebリクエストを出した時点でのHololensの姿勢を使うというところです。

Webと連携すると多少なりとも遅延は発生するので、そのあたりをどうやって解決していくかという実験をするための土台が整ったといえます。

実際に動かした感じでは秒単位の遅延が起きることが分かっています。 これは通信レイテンシだけでなく物体検出・カテゴリ分類などの、処理が重いことも影響しています。

リアルタイムのARアプリでWebと連携する場合、いかにユーザーにストレスや違和感を与えずに演出を組み込んでいくかなど、そういった部分を今後色々と試していきたいですね。

開発環境について

少々蛇足になりますが、貴重な時間を有効活用するために、通勤時間や昼休みにSurfaceBook2で作業しています。

初期投資に多額の資金を投入するリスクを負うのは怖かったので、RAMは8GBのモデルです。

動作は快適なのですが、8Gだと少々つらくて、それぞれの開発環境を個別に動かすことはできますが、同時実行ができないです。

こういう状況なので、Webとの連携が進み始めた段階でAWSも使い始めることができたのは幸運だったなぁと思う次第です。

技術の融合

個人的には私の好きな分野で技術を活用できた一年だったと考えています。

技術の融合の話に入る前に、まず「技術」もしくは「テクノロジー」について私の思想について少し書きます。

私は「テクノロジーはエンドユーザーに価値を提供してこそ意味があるもの」だと思っています。

エンドユーザーに提供する価値というと、具体的には「製品」や「体験」といったものです。 店で売っているような商品や、同人作品などは製品そのものですし、アプリで遊んでもらう(=楽しむ)なんてのは体験ですね。

今年は「技術を活用する、価値を提供する」領域を広げるために「コスプレでのAR利用」を試し始めました。

「コスプレでのAR利用」は、イベント会場で興味を持ってくれた人もいたみたいで、今後も表現方法の1つとして色々試したいと思います。

コスプレでAR, 3Dプリンタの利用

ます「コスプレ」は仮想世界(ゲーム, アニメなど)にあるものを現実世界に再現して、キャラになりきる行為です。

言うなれば「ごっこあそび」ですね。子供の頃やっていたであろう「ドラゴンボールごっこ」とかの進化形です。

こういった分野で個人的にやりたいこと(製品そのもの+アプリ)の実現に一歩近づいた年だと思います。

具体的には「ARとコスプレの融合ができた」というのが大きいと思います。イベント会場ではほとんど使えませんでしたが。(猫耳が引っかかってHololens装備できなかった。。。)

技術要素はこんな感じでしょうか

仮想世界にあるものを現実世界で再現する(たとえば3Dプリンタ)という行為は、ARとは異なる技術で現実世界を楽しくしてくれます。 というわけで、コスプレとARは相性が良いだろうと考えています。

特に3Dプリンタは一昔前は「家も建てることができる夢の装置」みたいにもてはやされましたが、プリント時間やコスト・耐久性などを考えると、そんなことは全然ありません。家庭用なら尚のことです。

3Dプリンタだけでは大して面白いものは作れませんが、ARデバイスと組み合わせることでとても楽しいものができるということは見えてきました。

特に私の場合はギミック(光る、動く、変形、合体)とかが大好きですから、そういうものの再現にも使えそうです。

他の技術でもいいのでは?

VRでいいのでは?と思うこともありますが、個人的には実物のほうが好きです。

たとえば、コスプレの武器って実際に持ってみると「すごいもの持ってる」感があって、VRのコントローラとは全然違った感覚です。

(VR自体は家電量販店でやってるデモしか体験したことないので、私の中での誤解があるかもしれませんが。)

現行のVRでは再現できないけれども、コスプレ用の武器で再現できる(優れている)点について考えてみました。

具体的には次の点がコスプレの武器が優れていると思っています。

  • 持ち手部分の触った感じ
  • 振ると重さを感じる

前者は武器製作者のセンスにも依存しますが、金属的な冷たさとか、革のようなじっとりした感じは、実物ならではです。

後者の「重さ」というのは、ただの重量だけに留まらず、実世界での動きに関係してきます。というのも、コスプレ用の武器というのは装飾がついていたりして、重心が不安定です。

  • 杖の先端のほうが重い
  • 斧は刃がついてる側のほうが重い

とかですね。

こういうものを振ったり構えたりすると遠心力でさらに不安定になるため、独特の「持ってる感じ」を受けます。これは現状のVRコントローラでは再現できてなくて、少々物足りない感じになります。

もちろん人によって受け止め方は違っていて、

  • そういうのは要らない派(たとえばゲームコントローラの振動もいらない派)
  • VRでもそういうのを再現できればよい派(研究をしている方々)
  • もっと実物の再現度上げたい派(ARすら不要派)

みたいに色々な考え方がありそうです。

私は実物+AR派でいこうと思っていますし、各個人が目的にあったテクノロジーを使えば幸せになれそうだなと思います。

色々と堅苦しいことも書きましたが、自分が楽しめることが一番大事だったりします。 継続できるのは楽しいからこそです。

年齢を重ねても夢は忘れないようにしたいですね。

ちょっと残念だったこと

やってみたけど上手くいかなかったり、やる予定だったけど実現できなかったことを書いてみます。

DeepLearning

まだ記事にしていませんが、chainerを使ってVGG16からのMTGのカード分類のファインチューニングはじめました。 試しに自宅のゲーミングPCで一週間くらいかけて40Epoc回したところ、50~60%の正答率でした。 学習・教師データともに少ないので過学習してる気はします。

Unity

2018年内にネイティブプラグイン試しに作るくらいやりたかったのですが、手付かずでした。。。

C++の限界

これも記事にしてない案件ですね。 Unityでやればすぐ終わることがC++だとめっちゃ手間かかるので、あきらめました。

これはC++がダメというわけではなくて、プログラムコードだけでは3D空間を把握できないという事実です。

文字列だけでのプログラミングの限界なのかなぁと。

具体的には3D空間での衝突判定とかで、bulletとeigenを使おうとビルドして組み込んではみたものの、うまく動いてくれなくてデバッグに手間かかりすぎです。

ベクトルの値なんかをグラフィカルに表示出来ればいのですが、結局ノートに鉛筆で書いたりとかしてるうちに「Unityでいいじゃない」って結論に辿り着きました。

C++のみの実装自体は断念しましたが、線形代数を復習する良い機会だったと思います。

やったこと自体は来年の目標であるネイティブプラグイン開発にも引き継げると思います。

2019年やりたいこと

来年の目標というか、やってみたい願望を書いてみます。

こういうところは現実主義なので、目標の段階で「無理やん?」みたいなのは自然に除外しているみたいです。

目標を達成していくことで次の2つが実現できると考えています。

  • カードゲームのAR化
  • コスプレでのAR活用

Unityネイティブプラグイン (継続)

とりあえず趣味レベルとはいえ2年近くC++/CXとUnityを触ってみてわかったことは、どちらも一長一短ですね。 Unityだとエンジンがほぼ全部面倒見てくれて、C++だと細かいところまで手が届く。

なので、これらの美味しいところ取りをしたいと思っています。

どういった場合にどちらが優れているかという点は見えてきたので、Unityをフロントとして、C++プラグインとして使っていく方向で2019年は動いてみたいと思います。

納得できなければまたC++からDirectX叩けばいいですし。

DeepLearning (継続)

ご無沙汰してる間にDeepLearning界隈も進んだようです。 カードゲームのAR化での画像分類やカード検出で使おうと試しているものをさらに進めてみたいと思います。

やりたいこととしては2つあって、1つ目は学習用のデータ収集です。 まずはアプリ側のプロトタイプ実装を終わらせて、それを使って学習用データを収集するところからですね。 現状のWebAPIから拾ってきたカード画像だけでは限界があります。

2つ目はOpenCVのONNIXローダを試したいですね。

ONNIXというのはDeepLearningのデータ交換用フォーマットです。、乱暴に表現するなら「Web技術でいうJSON/XMLのDeepLearning版」といった感じです。

何ができるかというと、Chainerで学習させたネットワークをONNIXで出力し、そのONNIXをOpenCV(C++)で読み込めるようになります。

これはどういうことかというと、

  • 学習はクラウドの強いマシンでChainerを使う
  • 識別はモバイル端末でOpenCVのネイティブ実装を使う

といったことができそうです。

Hololensだと、識別フェーズの処理ですら厳しいかもしれませんが、試す価値はあると思っています。

次期Hololensに搭載されるという噂のAIチップでONIIXを使えるAPIを公開してくれることを祈りましょう。(そういう用途ではなさそうですし、公開されないでしょうけれど)

電子工作魔法陣 (新規)

ずっとご無沙汰してた電子工作を復活させたいと思います。

catalina1344.hatenablog.jp

今更感ありますが、現状のAR/VRには致命的な問題があると思っていて、それを解決するのが目的です。

AR/VR最大の課題は「デバイスを持っていない人は楽しくない」というところです。

どちらも外野から見ると「何やってんだ?」状態ですし、「装着者が見ている映像はコレです」と見せても、面白さは伝わりにくいです。

Hololensでは映像体験を共有する仕組みがありますが「デバイスorそれに準ずるものを持っていること」という前提条件ありきです。

私としてはコスプレとかのサブカルをテクノロジーで盛り上げたいと思っているので、外野のギャラリー(=デバイスを持っていない人)にも「面白い」「楽しい」と感じてもらいたいです。

通りがかりに「ちょっと写真一枚撮らせてほしい」という時に、「Hololensでぜひ」というやり取りはムリがあります。

なので、「ARデバイスがなくても楽しい」「傍から見てても楽しい」という物理魔法陣をまた頑張ってみようかなと思った次第です。

魔法(物理)ですね。

ちなみに「デバイスを持っていない人が楽しくない」のであれば「全員がデバイスを持っている環境を作ればいい」という発想で進めているのがHadoとか、VRエンタテインメントなんかの施設だと考えます。

個人活動ではハコモノ作れるわけもなく、そもそも貸し出し用デバイスを購入する資金もありませんので、別のアプローチでいくのが正解だと思っています。

まとめ

今年も色々と試行錯誤して勉強になりました。

自分で手を動かして失敗しないと覚えないタイプなので、順調に進められた一年だったかなと思います。

このブログも更新が少ないのに読者になってくださる方もいてありがたい限りです。

スターとかブクマとかもらえたときにはめっちゃやる気UPしてたりします。

ARそのものをお仕事にできれば楽しそうだとは思っています。

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

皆様よいお年を。

NginxでHTTPS(SSL)対応する

暗号/復号関係は苦手です。かたりぃなです。

苦手な理由は、これ系は期待動作していない場合のデバッグが非常にやりにくいからなんですよね。

アルゴリズムそのものは実績がある前提だとすると、うまく動かないときはモジュール外から与えるパラメータが間違えているわけです。

鍵、IV、証明書、暗号アルゴリズム、暗号利用モード、暗号開始アドレスetc...こういうパラメータを確認して一つづつ見直しをするしかないわけです。

で、パラメータを変更して動作を追っても「あってる or 間違えている」しかわからないので、本当につらい。

数学が得意な人はもっと効率的に作業できたりするんでしょうか?

さて、今回はHTTPSとその周辺技術を触ってみます。 個人的にサーバ立ててサービス化するときも役立つと思いますので。

HTTPSとは

一言でいうとHTTPをセキュアにしたものです。

まずプロトコルの観点から整理します。

プロトコルスタック

だいたいこんな感じです。 TCPレイヤとHTTPレイヤの間にTLSレイヤを設けて、ここで暗号/復号などが行われます。

レイヤ プロトコル
アプリケーション HTTP
アプリケーション TLS/SSL
トランスポート TCP
ネットワーク IP

Python-FlaskでHTTPS対応してみる(断念)

ちょっと面倒なので断念です。

HTTPS化するだけのはずが、本題以外のことを考えないといけなくて、手間かかりすぎでした。

どういうところが手間だったかというと

  • アプリケーションとインフラとの結合が密になってしまう
    • 当然、コードが入り乱れてきてしまう
    • コード混乱の結果、リポジトリの管理どうするかとか、本題と関係ないところで悩む

アプリケーションはビジネスロジックとかに注力すべきであって、インフラ周りと密に結合した実装にするのはよくないですよね。

アプリケーションのモジュール構成をしっかり考えるという対策もありますが、今回は一般的なWebサーバを活用する方向で考えたいと思います。

というわけで、apacheとnginxを試していきます。

apacheでやってみる

apacheの設定こんな感じにすれば、とりあえずHTTPS対応できます。

証明書と鍵は後で説明します。

apacheの設定ファイル(httpd.conf)

# vhost
NameVirtualHost *:80
NameVirtualHost *:443
Include pass/apache/conf.d/*.conf

virtualhost(pass/apache/conf.d/httpd_vhosts_site.conf)

<VirtualHost *:80>
    ServerName local.my.api.mtg_card_detect
    # ~~ 略 ~~
<VirtualHost>

<VirtualHost *:443>
    ServerName local.my.api.mtg_card_detect:443
    SSLEngine On
    SSLCertificateFile pass/apache/server.crt
    SSLCertificateKeyFile pass/apache/server.key
<VirtualHost>

で、この後段にお好きなアプリケーションサーバを置いておけばよさそうです。

PHPなんかだとこの方法でちゃちゃっとやっつけてもいいかもしれませんね。

アプリケーションとwebサーバを分離したい

ここから、今回のエントリの本題です。一般的にSSLオフローダ、リバースプロキシと呼ばれる方式です。

クライアントからのアクセスを一旦Webサーバで受けて、WebサーバはSSLを解きます。

SSLを解いたら、Webサーバからアプリケーションサーバへ(HTTPSではなく)HTTPでリクエストを出します。

これで次のことが実現できます

いい感じの役割分担になってきました。

nginxリバースプロキシでSSLオフロードをする検討

以前やったnginxのリバースプロキシにSSLを解かせます。

一点だけ懸念がありますが、今回は気にしないことにします。まだ使わない機能なので。

何が懸念かというと、アプリケーションに必ずHTTPでリクエストが来るということは、リクエスURIが書き換わるということを意味します。

リクエスURIが書き換わると、アプリケーションが実装している盗聴対策, 認証, 認可などのセキュリティでエラーとして弾かれる可能性があります。

具体的にはOAuthとか。うろ覚えですがOAuthはリクエスURIを含めて何か作ってた気がします。またの機会に調査します。

nginxでSSLを解く

まず証明書と鍵が必要です。実験用なので自分で鍵を作って、自分で署名しましょう(通称:オレオレ証明書)。

$cd /usr/local/nginx/conf.d
$openssl genrsa 2048 > server.key
$openssl req -new -key server.key > server.csr
# ※以下のような質問が出るが、オレオレ証明書なので全てEnterでスキップでOK。
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
 
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
 
# 署名する
# 実験中に期限くると困るので10年(365 x 10=3650)くらいの有効期限で
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

次にnginxの設定ファイルを書きましょう。 以前やったリバースプロキシの設定を書き換えています。

user  nginx;
worker_processes  1;

pid    /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http{
  server {
    listen       443 ssl ;
    server_name  local.my.proxy.server;

    ### SSL有効化と証明書の保存場所指定
    ssl           on;
    ssl_certificate     /etc/nginx/cert/server.crt;
    ssl_certificate_key /etc/nginx/cert/server.key;

    ### プロキシ先の指定とApacheに渡すリクエストヘッダーの指定
    location / {
        proxy_pass http://192.168.xx.xx:port/;
        proxy_redirect                         off;
        proxy_set_header Host                  local.my.api.mtg_card_detect;
        proxy_set_header X-Real-IP             $remote_addr;
        proxy_set_header X-Forwarded-Host      $host;
        proxy_set_header X-Forwarded-For       $proxy_add_x_forwarded_for;
    }
  }
}

これでOKです。 nginxにHTTPSでリクエストを出すとmtg_card_detectというアプリケーションサーバにHTTPリクエストが出るようになりました。

感想と今後の展望

SSL対応もできたので、今後カードゲームARアプリのゲームサーバ(カード検出、分類)に対するリクエストもHTTPSで実現できる目途は立ちました。

本物の証明書はCAに発行してもらわないといけないので、実際にどうするのかは必要になってから調べることにします。

サーバ構成も少しづついじれるようになってきたので、今後のアプリづくりで有効活用したいですね。

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

AWSを試してみる

AWSを契約してVPCとか仮想サーバとか試してみました。かたりぃなです。

AWSのアカウント作成

登録はクレジットカードが必要になります。

ユーザー登録自体は無料なのですが、無料登録枠からはみ出して課金となった場合、この登録しているクレジットカードから引き落としされるようなので注意が必要です。

(まだ実際に課金は発生していないので、具体的にどうなるのかは見たことありません。)

無料利用枠

新規登録から一年間は、無料利用枠というものがあります。

たとえば基本的なクラウドサーバであるEC2の最小構成であれば、無料利用枠で利用できたりします。

しかし残念ながらDockerコンテナをデプロイできるサービスは無料利用枠には含まれていないようです。 (Elastic Container Service = ECS)

なので、今回はまずEC2を利用します。

AWSの初期設定

セキュリティを考慮して構成を考えると、次のような構成になるかと思います。

  1. 管理用EC2インスタンス
  2. 実際のユーザーアクセスを受け付けるEC2インスタンス(複数あり)

どうしてこうなるかというと、どこからでもSSHでログインできるインスタンスというのはなかなか怖いものがあります。(AWSの管理コンソールでも警告が出ています。)

1でやりたいことは、管理用インスタンスは専用の固定IPからのみアクセス可能としておいて、他からのアクセスは禁止します。

2はパブリックIPを割り当てずにVPC(AWS上の自分のLANとでも思ってください)内からアクセスするものとします。 これはSSHの経路です。 HTTPで外から来る人向けにロードバランサ経由でプライベートIPへと転送させます。

これでなかなかよさげな構成(一般的なのかな)になりますが、複数インスタンスが必要だったりして、それなりにお金がかかってきてしまいます。 なので、簡易的な対処でいったんEC2を立てて、実験してみます。

  • EC2インスタンスは1個のみ
  • SSH接続のポート番号は変えておく
  • HTTPポートは自分が作業するときのみ開ける

SSHのポート番号を変えておくというのは、なかなか原始的な手法ですが、何もないよりは安心といったところですね。

自分がアタックする立場なら、22番とりあえず叩いてみるとかするでしょうし、それなりに効果はあるらしいです。

AWS-EC2の設定

AWSの操作はもっと有用な情報源があるので、ここでは省略します。 無料利用枠のデフォルト値から変更した箇所のみ記載します。

セキュリティグループ - HTTPあけた, SSHポート番号を変えた ディスクサイズ 30GiB。無料利用枠いっぱい

Linuxマシンの設定

まずSSHポートの設定を変更します。

/etc/ssh/sshd_config の上のほうに Port 22 という表記があるので、これを変えてsshdを再起動します。

sudo service sshd reload

ローカルマシンの設定(Windows)

今回はWSLのsshを使ってAWSに接続します。 AWSインスタンスを作成するときにキーペアがないとアクセスできなくなるよ?みたいに言われたかと思います。 この鍵をローカルマシンに保存します。

必要に応じて.ssh/configとか書いてアクセスできればOKです。 今回はこんな感じで書きました

Host aws-ec2
        HostName パブリックIPアドレス
        IdentityFile    ~/.ssh/aws-first-keypair.pem
        User    ec2-user
        Port    ポート番号

なお、ここで指定した鍵はパーミッション設定間違えやすいです。(Windows側からコピーして持ってくると、実行権限がついてたりとか)

chmox 600 ですかね。とりあえずは。

ansible

ansibleはプロビジョニングツールと呼ばれるもので、コマンドを実行することで対象のマシンに必要な設定をしてまわる便利なやつです。 今回はこれを使ってみます。

ansibleの追加パッケージを使うとEC2をたてたりする機能も使えるようになるらしいですが、はまだ試していないのであしからず。

さて、EC2でこれを使う目的は、いくつかありますが、代表的なものは以下の2つかと思います。

  1. マシンの環境を再現しやすくすること
  2. 複数台マシンにスケールアウトする場合のプロビジョニングの手間の削減

1はよくある「ライブラリ依存の問題」の解消などがわかりやすいと思います。 昔のWindowsだとよくあったトラブルで「なんかよくわからないけどアップデート走ったら治った」みたいなやつです。

趣味で適当にやるだけならいいですが、お客さんに納品するシステムなんかで「よくわかりませんが治りました」って嫌ですよね。(少なくとも私はそういう業者は信用しません)

ここではライブラリの組み合わせなど、環境を固定できるのが大きなメリットです。

手作業でやってたりオレオレ.shを自作したりすると後で見たとき泣きそうになりますが、ansibleで一定のフォーマットが担保されるなら少しは安心かなと思います。

2はスケールアウトを考えたとき、複数台に同じ処理を流していくのって手間かかりますし、ミスが紛れ込みます。 たとえばhost1にコマンド打ち込んだつもりが、host2に打ち込んでいたとか。 そういうミスを減らすためにもこういうツールはどんどん使っていくべきだと思います。

Ansible実行環境

WindowsではAnsibleが走ってくれません。Linux由来のパッケージではよくあることなので、あきらめて「ansibleを実行するためのdockerコンテナ」をたてます。

FROM centos

RUN yum -y install initscripts MAKEDEV

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

# 文字コード
#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 boto

# dockerコンテナとホスト間での共有ディレクトリ
RUN mkdir /tmp/data -p

# 共有ディレクトリの準備
RUN mkdir -p /tmp/data

# 起動オプション。CMDに指定できるのは一つだけ。
EXPOSE 22
CMD bash -c "/usr/sbin/sshd -D"

sshを許している理由は、docker内に鍵を置きたくないからです。事故らせる自信あります。(ぇー)

Windows側からsshするとき-Aでフォワードかけて、その鍵を使ってAWSに接続する。という使い道を想定しています。

playbookをかく

playbookの詳細はもっと有用なサイトに譲るとして、やってみた内容だけ書いていきます。

まずplaybook本体です。

開発ツール、C++用のライブラリ(boost, opencv)、Python用ライブラリ(chainer, chainerUI)をインストールすることを記載します。

---
- name: 
  hosts: all
  user: root
  become: yes
  become_user: root
  roles:
    - devtools
    - cpp-libs
    - chainer

cpp-libsロールのmain.ymlでは、opencvとboostをインストールしたいので、それぞれわけて書きました。

---
- include: tasks/opencv.yml
- include: tasks/boost.yml

opencvのインストールはこんな感じで書いてます。 git cloneしてcmakeしてビルドですね。

ちょっと古いdockerfileからコピってきたのでバージョン古いままです。

- name: Install opencv
  git:
    repo: 'https://github.com/opencv/opencv.git'
    dest: '/root/opencv/'
    version: 3.3.1

- name: Install opencv_contrib
  git:
    repo: 'https://github.com/opencv/opencv_contrib.git'
    dest: '/root/opencv_contrib'
    version: 3.3.1

- name: make opencv build dir
  file:
    path: /root/opencv/build/
    state: directory
    mode: 0755

- name: check cmake configure
  stat:
    path: /root/opencv/build/CMakeCache.txt
  register: cmake_cache

- name: configure opencv
  shell: >
    cd /root/opencv/build;
    cmake -DOPENCV_EXTRA_MODULES_PATH=/root/opencv_contrib/modules ..;
  when: not cmake_cache.stat.exists

boost側は似たようなもんなので省略します。

ansible-playbookの実行

ここまでで一通りのplaybookができました。 こんな感じで実行します。

ansible-playbook -i inventory/hosts playbook.yml

playbookのトラブルシューティング

Opencv, boostとも大きなライブラリなので、結構容量食います。 この大きさ故にEC2のT2-microのデフォルト設定では容量不足で終了します。

T2-microインスタンスののデフォルトストレージ容量は8GByteしかありません。 AWS無料利用枠の上限として合計30GiByteまでいけるようなので、おとなしくインスタンス作り直し、大きめの容量をとってリトライすればうまくいきました。

ちなみに、AWSのストレージ容量表記はSI単位系ではなく二進接頭辞(「キビバイト(KiB)」とか)になっていました。 まあサーバ系でギリギリの容量で運用することは普通やらない(組み込みソフトならいざ知らず…)ので、あまり気にしなくてもいいかもしれませんが、頭の片隅に置いておきましょう。

起動、動作確認

以前のブログ記事でやったソースコードをアップロードして、ビルド、実行です。

つまり、PNGファイルをHTTP-POSTしてあげれば、画像解析をして結果をJSONで返してくれるわけです。

実験用クライアントアプリ

最終的にHololensからアクセスする予定のRESTなので、Hololensと同様のUWPで作ります。 少しずつですが慣れてきたC++/CXです。

XAMLにボタンとか適当において、イベントハンドラに次のコードを記述します。

 FileOpenPicker^ openPicker = ref new FileOpenPicker();
    openPicker->ViewMode = PickerViewMode::Thumbnail;
    openPicker->SuggestedStartLocation = PickerLocationId::PicturesLibrary;
    openPicker->FileTypeFilter->Append(".jpg");
    openPicker->FileTypeFilter->Append(".jpeg");
    openPicker->FileTypeFilter->Append(".png");

    create_task(openPicker->PickSingleFileAsync()).then([this](StorageFile^ file){
        if (file) {
            // ファイルが選択された
            // todo : キャンセル処理が必要。
        }
        return file->OpenReadAsync();
    }).then([](Windows::Storage::Streams::IRandomAccessStream ^ stream) {
        // こうすればPOSTするためのHTTPリクエスト作れるらしい
        auto uri = ref new Windows::Foundation::Uri(L"http://EC2のパブリックIP/");

        auto streamContent = ref new Windows::Web::Http::HttpStreamContent(stream);
        auto request = ref new Windows::Web::Http::HttpRequestMessage(Windows::Web::Http::HttpMethod::Post, uri);
        request->Content = streamContent;

        auto filter = ref new Windows::Web::Http::Filters::HttpBaseProtocolFilter();
        filter->CacheControl->ReadBehavior = Windows::Web::Http::Filters::HttpCacheReadBehavior::MostRecent;
        auto client = ref new Windows::Web::Http::HttpClient(filter);
        auto headers = client->DefaultRequestHeaders;
        return create_task(client->SendRequestAsync(request));
    }).then([this](task<Windows::Web::Http::HttpResponseMessage ^ > previousTask) {
        try
        {
            auto response =  previousTask.get();

            // ここではHTTPリクエストの動作確認が目的なので、ログに出して終わる。
            auto body = response->Content->ToString()->Data();
            auto header = response->Headers->ToString()->Data();
            OutputDebugString(header);
            OutputDebugString(body);
        }
        catch (const task_canceled&)
        {
            // HTTPリクエストがキャンセルされた場合はここに来る
            OutputDebugString(L"http request task chanceld\n");
        }
        catch (Exception^ ex)
        {
            // HTTPリクエストで何らかのエラーが発生した場合はここ
            OutputDebugString(L"http request task error\n");
        }

    });

やっていることは次のとおりです。

  1. イメージピッカーを表示して、画像ファイル選択のUIを表示します
  2. 選択された画像ファイルを読みだし用に開き、ストリームを得ます。
  3. HTTPクライアントのリクエストにストリームを関連付け、リクエストを開始します
  4. HTTPレスポンスをログに出力します。

この手順の3番目以降はHololensに移植してもそのまま使える部分ですね。

つまり、リクエストに載せるストリームをカメラ映像由来のものにすると。

できた!

AWSのサーバにリクエストを投げて、レスポンスを受け取るという基本的な部分ができました。

動作結果は以前に Hololens - デスクトップPCでやったのと同じです。

まだサーバ側に認証・認可はつけてないので、不用意にアクセスされないよう、使わないときは落としておく運用とします。

感想と今後の展望

プロトタイプとはいえ段々とシステムを作っている感じが出てきました。

まだまだやることあ多いですが、少しずつ自分のペースでやっていきたいと思います。

次はHololens側との連携になりますが、サーバ側の画像検出器が少々不安定で落ちることがあるので、修正していくことになるかなと思っています。

こういうときに今回作ったようなダミーモジュールがあると便利ですね。

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

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形式なので注意が必要です。