catalinaの備忘録

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

HololensでMediaCaptureを使ってカメラプレビューを取得する

HoloLensで入力画像を処理する前準備をしました。かたりぃなです。

今回やること

今回の記事でやることをざっと列挙します。

  • UWPでMediaCaptureを叩いてプレビューフレームを取得する
  • OpenCVのMat形式で扱えるようにする
  • とりあえず画像処理する

です。 画像処理の結果を使って3Dレンダリングまでやりたかったのですが、ちょっと詰まってしまっているのでいったんここまで記事にしておきます。 シミュレータではカメラが使えないのでほとんどが実機デバッグになります。買ってよかったHoloLens。

カメラから映像を取得する

HoloLensでMediaCaptureを使ってプレビューフレームを取得するコードはMicrosoft公式がサンプルコードを出してくれているのでそのまま使います。 中身ざっと眺めたところ、CreateAsyncしてGetLatestFrameするだけで使えるみたいです。

https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/HolographicFaceTracking/cpp/Content/VideoFrameProcessor.cpp

早速コード。 アプリのメインクラスの定義にメンバ変数を作っておきます。 mediacaptureを制御するクラスと、プレビューフレームを処理するワーカースレッドです。

    std::shared_ptr<HolographicFaceTracker::VideoFrameProcessor>    m_videoProcessor;
    std::thread                                                     m_worker_thread;

CreateAsyncでインスタンス化したものを保持しておき、std::threadでぐるぐる回します。 取得した映像フレームを処理するVideoProcessingという関数を作りました。ここで画像処理をします。

    task<void> videoInitTask = HolographicFaceTracker::VideoFrameProcessor::CreateAsync()
    .then([this](std::shared_ptr<HolographicFaceTracker::VideoFrameProcessor> videoProcessor)
    {
        m_videoProcessor = std::move(videoProcessor);
        m_worker_thread = std::thread([this]{
            while (1) {
                if (m_videoProcessor) {
                    auto frame = this->m_videoProcessor->GetLatestFrame();
                    if (frame) {
                        if (Windows::Media::Capture::Frames::VideoMediaFrame^ videoMediaFrame = frame->VideoMediaFrame) {
                            VideoProcessing(videoMediaFrame);
                        }
                    }
                }
            }
        });
    });

OpenCVに渡す(前準備)

映像フレームをOpenCVで処理します。 その前にOpenCVをUWP向けにビルドしておいたライブラリをプロジェクトに組み込みます。 MSがUWP向けに準備してくれているOpenCVはこちら。 GitHub - Microsoft/opencv: Open Source Computer Vision Library

試しに組み込んだときの記事はこちら。 WindowsストアアプリでOpenCVを使う - catalinaの備忘録

というわけで同様にして組み込みます。

先ほどのサンプルコード見るとそのまま画素にアクセスできるらしいので真似します。

 auto buffer = frame->SoftwareBitmap->LockBuffer(Windows::Graphics::Imaging::BitmapBufferAccessMode::Read);
    IMemoryBufferReference^ bufferRef = buffer->CreateReference();

    Microsoft::WRL::ComPtr<Windows::Foundation::IMemoryBufferByteAccess> memoryBufferByteAccess;
    if (SUCCEEDED(reinterpret_cast<IInspectable*>(bufferRef)->QueryInterface(IID_PPV_ARGS(&memoryBufferByteAccess))))
    {
        BYTE* pSourceBuffer = nullptr;
        UINT32 sourceCapacity = 0;
        if (SUCCEEDED(memoryBufferByteAccess->GetBuffer(&pSourceBuffer, &sourceCapacity)) && pSourceBuffer)
        {
            // pSourceBuffer使ってごにょごにょする
            // YUV420Pとして扱えばいい
        }
    }

ここで注意として、今回のコードではbitmapはNv12フォーマットです。 NV12とは。。。 YUV Video Subtypes (Windows) とのことで、いわゆるYUV420P形式です。

プレビューフレームの画素のバイト列をOpenCVのcv::Matにする。

簡単に。 OpenCVのMatとして取り扱います。YUV420Pなので最初のプレーンを参照すれば輝度のみ取れるので、グレースケール画像として取り扱えます。

auto cv_preview_image = cv::Mat(cv::Size(frame->SoftwareBitmap->PixelWidth, frame->SoftwareBitmap->PixelHeight), CV_8UC1, pSourceBuffer);

これでOKです。 cv::Matのコンストラクタは色々ありますが、この形式のものを使います。 第一引数はcv::Sizeであらわされる画像の幅と高さ。 第二引数はピクセルフォーマット(厳密にはmatの各要素のtype)で、pSourceBufferが画素列の先頭アドレスです。

デバッガで目視確認する

ピクセルの数列見ても楽しくないので、画を見たいです。 MS公式がVisualStudioのプラグインとしてcv::Matのイメージビューワを提供してくれてるので使います。 Image Watch - Visual Studio Marketplace

適当にフィルタ処理してみるとそれっぽくできてるのが確認できました。 というわけで画像処理の入り口まで辿り着きました。

課題・今後の展望

HoloLens実機でのデバッグはそれなりに手間がかかります。デバッガで少し見て解る問題ならいいんですが、ちょっと複雑なことをやろうとすると、厳しいです。

特に問題になるのは、リアルタイム処理とVisualStudioのデバッガが相性悪すぎといったところです。 映像をリアルタイムに処理した結果をデバッガのログに出していても、それを見るために頭を動かしてしまうと状況が変わってしまいます。 こればっかりはどうしようもないので、HoloLens側でデバッグ情報をレンダリングする仕組みを作っておいたほうが後々幸せになれそうな気がします。

とりあえずビルボードでテキストをレンダリングする仕組みくらい作っておいて損はなさそうです。

では今回はこれにて。

HoloLensでChainerの学習経過を覗いてみた

せっかくHoloLensがあるので機械学習の途中経過を覗いて遊んでみました。かたりぃなです。

学習中のデータの可視化する意義

機械学習についてですが、Chainerなどのフレームワークがどうやって学習を進めているかよくわかっていません。 具体的にはフレームワークによってネットワーク内パラメータがどう変動していくのかが分からないのです。 optimizerがいて順伝播と逆伝播の誤差を~というのは理屈ではそうなのでしょうけれども、実際に中を見ていないのでしっくりきていません。

Chainer自体はよくできたフレームワークなのでexampleの中身動かして「あ、こんな簡単に動くんだね」と感動するのですが、そこから先どうしようかという展望を持ちにくい印象です。 学習が進むことによって「ニューラルネットワークのパラメータにどういう"変化"が起きているのか」を見れたらもっと欲望というか展望が見える気がします。

可視化といえばchainerが出しているログをグラフにしたり、各層を可視化するなどといった手法はありますが、それって静的なものなので、「どういう過程で生み出されたものなのか」が見えにくいです。 少しずつ変化していっているとしても、やっぱりそこを見てみたいですよね。 可視化した画像をブラウザで開いてCtrl+F5連打という力業を試しましたが毎回連打するのは疲れます。

というわけで、機械学習の進行状況をリアルタイムで見れるようにアプリを作ってみます。 せっかくHoloLensあるのでこいつを使います。

実運用ではオーバーヘッドが大きすぎ(GPUからCPUにNNを取り出してからnpz形式でファイル出力)で役に立たないとは思いますが、まずは動いているものを見て興味を持つという点が重要かなと思っています。

結果

こんなの出ました。一定周期で更新されるワイヤーフレームはまるで昭和時代のCGです。 だんだんと山と谷の差が大きくなっていく様子が観察できました。

f:id:Catalina1344:20170218215123p:plain

実世界のテーブルの上とかに表示しておいて山の裏側を見たいってときは回り込めば見えます。 (実機でのキャプチャしようと思ったのですがDirectXレンダリング結果をオーバーレイしたキャプチャがうまくできないので保留です)

やったこと、やらなかったこと

今回やってみたことと、保留にしたものを列挙します。 大まかに考えると、「可視化すること」が目標で、そこから洗い出された課題などはいったん保留です。

やったこと

  • chainerのネットワークの状態を一定周期でファイルに出力する
  • ネットワークの種類はautoencoder(可視化して結果がわかりやすそう)
  • HoloLens側からファイルをポーリングして一定周期で読み出す
  • HoloLens上でDirectXを使ってレンダリング

やらなかったこと

  • 機械学習の分野の詳細に立ち入ること(学習が収束するとか、効率の良いネットワークだとか)
  • 可視化した結果の正確性
  • 過度な高速化(HoloLensでとりあえず表示できればいい)

以降、実装の詳細です。

chainerのサンプルコードを編集

chainerはPC上で実行します。 まずchainerのmnistサンプルコードでスナップショットを出力する部分がありますが、ここを変更します。

trainer.extend(extensions.snapshot(filename='work.npz'), trigger=(1, 'epoch'))

デフォルトでは連番ファイルを出力するようになっているところを変更します。同一ファイルをwrite/readし続けたほうが実験としてはやりやすいので。 また、トリガは毎epoch終了ごととします。 ネットワークが大きいくなると毎epochやっていると重そうですが、今は気にしないことにします。

次にネットワークをautoencoderにします。 autoencoderを選んだ理由は実装が簡単かつ「なんかそれっぽい気がする」ものが見えそうだからです。 データセットにmnistを使うので入力/出力ともに784次元のデータです。

class MLP(chainer.Chain):
    def __init__(self, n_units):
        super(MLP, self).__init__(
            # the size of the inputs to each layer will be inferred
            encoder = L.Linear(None, n_units),  # n_in -> n_units
            decoder = L.Linear(None, 784)
        )

    def __call__(self, x):
        h = F.relu(self.encoder(x))
        return self.decoder(h)

ここでencoder/decoderと名前を付けましたが、この名前がそのままスナップショット内のネットワークのレイヤ名に使われるので、わかりやすい名前をつけておいたほうが幸せになれます。

最後にデータセットを入出力ともに同じものを使うようにします。 withlabelをfalseにすれば教師ラベルなしの入力データがそのまま取得できます。 ただし、学習のイテレーションでは教師データをTupleDatasetとして与える必要があるので、 入力=出力となるDatasetを生成します。testも同様です。

    train, test = chainer.datasets.get_mnist(withlabel=False)
    train = tuple_dataset.TupleDataset(train, train)
    test = tuple_dataset.TupleDataset(test, test)

ちょっと寄り道して動作確認。 それっぽく動いているかどうかpythonから画像を出力して確認してみます。 plot関係のライブラリを使って画像に出します。

なんか見れた。 f:id:Catalina1344:20170218151616p:plain

寄り道の詳細

ClassifierしたmodelからだとNNを参照するのが大変そう(簡単にできるのだろうか?)なので、networkをとっておいてそこから重み係数を参照することにしました。 また画像として出力するにはユニット数が少ないほうが出力画像に収めやすいので10x10枚の可視化=100ユニットとしています。

import matplotlib.pyplot as plt
import numpy as np

#~~~
network = MLP(unit)
model = L.Classifier(network)

#~~~
save_images(network.encoder.data, "plot_test.png")

#~~~
# 画像で保存(隠れ層=100という前提)
def save_images(x, filename):
    fig, ax = plt.subplots(10, 10, figsize=(10, 10), dpi=100)
    for ai, xi in zip(ax.flatten(), x):
        ai.imshow(xi.reshape(28, 28))
    fig.savefig(filename)

本当にこれでいいのか不安ですが、これでchainer側で可視化の準備は整いました。 スナップショットはresultフォルダに出力されます。 ついでにdotファイル吐かせてgraphvizで見れることも確認しました。

chainerのスナップショットをhttpで取得する

まずHTTPサーバをpythonで簡単に立ち上げます。3.5系ではこうなります。

python -m http.server

これで8000番ポートが開くので、httpアクセスすると上記スクリプトを実行したディレクトリに置かれたファイルを参照できます。

次はHoloLensアプリ側の実装です。 と、その前にHoloLensの設定とリモートデバッガの設定をします。

HoloLensの設定

HoloLensのデバイスポータルに接続してデバイスの情報を拾えるようにしておきます。

これをやっておけばPCのブラウザからHoloLensにアクセスできるようになります。 証明書のインストールとかあるので、手順は公式を参照。

HoloLens 用 Device Portal | Microsoft Docs

リモートデバッガを構成する

VisualStudioのデバッグ構成を変更してリモートデバッグの設定をします。 とはいえ毎回IP調べるのは面倒なので固定IPにしたほうが便利です。 f:id:Catalina1344:20170214214457p:plain

HTTPサーバからファイルを取得する

ここからHoloLens側の実装です。 VisualStudio2015のテンプレートからHoloLens DirectX11を使って作業していきます。

httpアクセスしてchainerの学習中スナップショットを取得します。 UWPのhttpclientクラスはキャッシュが効いてしまって実験中は不便なのでHttpCacheReadBehavior::MostRecentを設定します。 あとは普通にresponseを見てdatareadすれば済みます。 エラーチェックしていませんが、ストアに提出するようなアプリのときはもうちょっと真面目にやりましょう。

     auto uri = ref new Windows::Foundation::Uri(L"http://192.168.0.14:8000/result/work.npz";);
        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;
        create_task(client->GetAsync(uri)).then([cb](Windows::Web::Http::HttpResponseMessage ^ response) {
            response->EnsureSuccessStatusCode();
            return response->Content->ReadAsBufferAsync();
        }).then([cb](Windows::Storage::Streams::IBuffer ^ input) {
            auto reader = Windows::Storage::Streams::DataReader::FromBuffer(input);
            auto loaded_buffer = reader->ReadBuffer(input->Length);
            parse_npz(loaded_buffer, cb);
        });

この処理を一定周期(1秒間隔とか)で呼び出してあげたうえでzip形式を解釈してzlibで伸長してあげれば完成です。

Direct3Dレンダリングする

取得したNNのencoderレイヤの各ユニットの重み係数は28x28のシングルチャンネル画像として解釈できます。 今回は単純にW係数を高さ(Y)にとることにします。(つまり、X,Zは0~27の間のいずれかの値で、X,Zが与えられるとYが一意に決まる,Yは重み係数) ポリゴンレンダリングすると色々と大変なのでワイヤーフレームでいきます。ワイヤーフレームのほうが味がありますし。

ラスタライズステージでワイヤーフレームにします。

 D3D11_RASTERIZER_DESC rasterizerDesc = {
        D3D11_FILL_WIREFRAME,   // ワイヤフレーム
        D3D11_CULL_NONE,        // カリングなし
        FALSE,
        0,
        0.0f,
        FALSE,
        FALSE,
        FALSE,
        FALSE,
        FALSE
    };
    ID3D11RasterizerState* rasterizerState = NULL;
    HRESULT hr = m_deviceResources->GetD3DDevice()->CreateRasterizerState(&rasterizerDesc, &rasterizerState);
    if (FAILED(hr)) {
        // TODO : エラー処理
    }
    context->RSSetState(rasterizerState);

しかしこれではやりたいことに対して少し不足しています。 VisualStudioのDirectXテンプレートではTriangleListをシェーダーに入力していますが単純にLineListを入力したいところです。

GeometryShaderを書く

LineListをシェーダに入力するには、まずCPU側のコードは単純に

context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);

すれば良いです。

これに合わせてGeometryShadeerのコードを変更します。 3頂点を受け取ってに三角形を吐き出すのではなく、2頂点を受け取って1つのLine(始点,終点)を出力するよう書き換えます。

-[maxvertexcount(3)]
-void main(triangle GeometryShaderInput input[3], inout TriangleStream<GeometryShaderOutput> outStream)
+[maxvertexcount(2)]
+void main(line GeometryShaderInput input[2], inout LineStream<GeometryShaderOutput> outStream)

シェーダー内でやっているジオメトリ生成も同様にして2頂点ずつ生成するようにします。

    [unroll(2)]
    for (int i = 0; i < 2; ++i)
    {
        // 略
    }

あとはLineListの規則に従って頂点バッファとインデックスバッファを定義して格子状のメッシュを生成すれば完成です。

頂点バッファを動的に更新する

表示するワイヤーフレームができたので、Y軸を動的に更新します。 乱暴ですが新しい頂点情報を受け取るたびにVertexBufferを作り直すことにしました。 UpdateSubResourceのほうが適任な気がしますが、HoloLensのサンプルコードのspatialmappingもこういう実装になっていたりするのでいったん良しとします。 コードは自分自身納得いっていないので省略です。

参考にさせていただいたサイト

chainerのモデルをC++から読み込むのはこちらのサイトが参考になりました。そのままnpzを読み込めました。 ChainerのモデルをC++で読み込む - TadaoYamaokaの日記

感想

chainerのモデルをC++から読みだすことができるようになりました。

「なんとなくdeepLearningやってる」感じが見えたので良しとします。 できれば1つのレイヤ内のすべてのユニットを可視化したかったのですが、視界からはみ出して残念な見栄えになるので諦めました。

とはいえChainerに慣れていない段階だと便利な気がします。HoloLens装着したまま別の作業進めつつ、進捗状況をチラ見できます。 PCの画面を占有しない+進捗確認の作業が単純(頭の向きを変えるだけ)っていうのは正義ですね。

しかしながらワイヤーフレーム表示では学習が進んでデータのレンジが大きくなってきたときに辛いです。実世界に表示したモデルをぐるっと回って見るのであれば光源処理を入れたポリゴンのほうがいい気がします。

あと今回の簡易的な実装ではHoloLens側の処理がやたら重いです。 どれくらい重いかというとHoloLensのジェスチャ操作が効きにくくなるくらい重いです。

原因は詳しく調べてはいませんが、Chainerのsnapshotのnpzを展開するとき何も考えずに全部展開してしまっているのが原因かもしれないと思っています。というのも、snapshotで保存されるnpzはすべての情報が含まれていて、今回使わないdecoderやoptimizerなんかも展開してしまっているので。。。 気が向いたときに調べてみます。

今後の展望

読みだした学習済みモデルを食べさせるC++実装があれば色々できそうです。 あとはdotファイルをうまく解析すれば「c++からchainerのモデルなんでも読めるよ」という夢も叶うかもしれませんね。 書いてて気づきましたがboost::proptreeとcv::dnn使えばできそうな気がします。

アプリづくりの練習としてChainerの出力したdotファイルを読んで、どの層を見るかを選択するUIつけてあげれば面白そうです。

それでは今回はこれにて。

Hololensでカメラを扱う[c++/UWP]

HoloLensにはカメラが搭載されているので、どんなもんが載ってるか調べてみようと思います。かたりぃなです。

(2017/02/26更新) Hardware detailsのURLを追記。

HoloLensのカメラって?

公式サイト

https://developer.microsoft.com/en-us/windows/holographic/hardware_details#pagenotfound

ここからUnderstanding HoloLens->Hardware detailsで詳細を見れますが現時点ではリンク切れになってしまっています。(2017/02/09 17:00現在) まあ、無いものねだりしても仕方ないので、そのうち復旧することを祈りつつソフトウェア側から攻めることにします。

(2017/02/26更新) URL更新されているとのコメントいただきました。現在はHoloLens hardware detailsでアクセスできました。

結論

手元のHoloLens上でデバイスを列挙してプレビューの開始をしてみたところ、"MN34150"という型番のデバイスが1個だけ列挙されました。

型番調べてみたところ、これっぽいです。 MN34150 | 製品 | 半導体 | Panasonic

列挙されたデバイスの物理的な場所はHololens正面の鼻あての上あたりでした。(カメラの前に手をかざしてプレビュー見ながら確認)

UWPでのカメラ操作

まずUWPでのカメラAPIは2種類存在しています。

  1. CameraCaptureUI
  2. MediaCapture

1はカメラを操作するUIも含めて提供されている高レベルAPIです。 インスタンス作って呼びだせば写真撮影のUIが起動します。

2は1と比べると低レベルなAPIで、デバイスの列挙、カメラの準備、プレビュー開始、撮影など細かく制御できます。 中身はMediaFoundationらしく、MediaTransferのインターフェイスをもったエフェクトの挿入などもできるようです。 今回はこれを使ってHoloLensのデバイスを列挙してみます。

詳細なドキュメントはそれぞれ以下になります。

MediaCapture class - UWP app developer | Microsoft Docs

CameraCaptureUI Class (Windows)

DeviceInformationを使ってカメラデバイスを列挙する

まずMainPageのクラス定義に以下を追加します。列挙されたデバイスを保持しておくためのものです。

DeviceInformationCollection^ m_devInfoCollection;

そして初期化コードに以下のものを埋め込みます。ほとんどサンプルコードのままです。

いつもどおりcreate_taskで生成したタスクの実行が完了すると、then節のラムダが起動します。ラムダ内で列挙されたデバイスをごにょごにょします。 このAPIは列挙されたデバイス個別にコールバックするのではなくまとめてコレクション返してくる仕様のようです。

 m_devInfoCollection = nullptr;

    EnumedDeviceList2->Items->Clear();

    create_task(DeviceInformation::FindAllAsync(DeviceClass::VideoCapture)).then([this](task<DeviceInformationCollection^> findTask)
    {
        m_devInfoCollection = findTask.get();
        for (unsigned int i = 0; i < m_devInfoCollection->Size; i++){
            auto devInfo = m_devInfoCollection->GetAt(i);
            auto location = devInfo->EnclosureLocation;

            if (location != nullptr){

                if (location->Panel == Windows::Devices::Enumeration::Panel::Front){
                    EnumedDeviceList2->Items->Append(devInfo->Name + "-Front");
                }
                else if (location->Panel == Windows::Devices::Enumeration::Panel::Back){
                    EnumedDeviceList2->Items->Append(devInfo->Name + "-Back");
                }
                else {
                    EnumedDeviceList2->Items->Append(devInfo->Name);
                }
            }else {
                EnumedDeviceList2->Items->Append(devInfo->Name);
            }
        }
        EnumedDeviceList2->SelectedIndex = 0;
    });

プレビュー機能を実装する

プレビューにメディアキャプチャクラスを使うので、MainPageの定義に以下の変数を追加します。

 Platform::Agile<Windows::Media::Capture::MediaCapture> m_mediaCaptureMgr;

プレビュー対象CaptureElementというものを使えばいいらしいのでXAMLに適当に定義します。 これとは別に適当にボタン貼っておいて、そのイベントハンドラをプレビューの起動トリガに使います。

        <Canvas x:Name="previewCanvas2" Width="320"  Height="240" Background="Gray" Margin="110,220,423,20">
            <CaptureElement x:Name="previewElement2" Width="320" Height="240" />
        </Canvas>

ボタンのイベントハンドラに以下のようなコードを入れます。 エラーチェックはしていません。

 m_mediaCaptureMgr = ref new Windows::Media::Capture::MediaCapture();

    auto settings = ref new Windows::Media::Capture::MediaCaptureInitializationSettings();
    auto chosenDevInfo = m_devInfoCollection->GetAt(EnumedDeviceList2->SelectedIndex);
    settings->VideoDeviceId = chosenDevInfo->Id;
    settings->StreamingCaptureMode = Capture::StreamingCaptureMode::AudioAndVideo;

    create_task(m_mediaCaptureMgr->InitializeAsync(settings))
    .then([this](task<void> initTask)
    {
        auto mediaCapture = m_mediaCaptureMgr.Get();
        previewCanvas2->Visibility = Windows::UI::Xaml::Visibility::Visible;
        previewElement2->Source = mediaCapture;
        create_task(mediaCapture->StartPreviewAsync());
    });

最後にPackage.appmanifestのWebカメラとマイクをONにしてビルド。エミュレータでやっても意味がないので実機でやります。

実行結果

  • 列挙されたデバイス : MN34150-back
  • プレビュー:表示された

というわけで、デバイス名とカメラの向き(front/back)まで調べてプレビュー表示できました。 もっといっぱいカメラを列挙できるかと思いましたが、1個しかなくて残念です。 (スペック表には4つの環境カメラや空間認識カメラなど載っていた気がしたのですが、今回のAPIでは取れないようです。)

どうしてback?

カメラのプロパティにbackというのがついてきます。これはカメラの向きの定義が「使用者のほうを向いているほうがfront」「反対方向がback」だからのようです。 ノートパソコンで画面と同じ向きに取り付けられているカメラはfrontで、画面と反対方向(メーカーのロゴとか入ってるほう)はbackだと考えると、まあ納得できます。 ちなみにUSBカメラはどっちでもないようです(そりゃ向きを自由に変えられるのだからfrontもbackもありませんよね。)

感想と今後の展望

やっとHoloLensでMR的なことをやる道具が揃い始めました。 MediaFoundationの近くまで進んだので、次回はMediaFoundationで画像処理を行って、その結果を取り出したいと思います。

HololensでMMDモデルをレンダリングしてみる[c++/UWP]

3Dモデルが出せたのでモチベーション上がり始めています。かたりぃなです。

環境 * HoloLens * Visual Studio 2015 * DirectX11 * C++/cx * UWP

とりあえずスクリーンショット f:id:Catalina1344:20170208220522p:plain やっぱりこうやって何かが出ると「アプリ作ってる」感でてきますね。 UWPでのDirectXの扱いに慣れていなかったので、詰まったポイントを整理していきます。 DirectX11そのものの扱いはほかにもっと詳しいサイトがありますし。

頂点シェーダが2種類存在している

vertex shaderですが、VisualStudio2015でHoloLens向けのテンプレートを生成すると、なぜかこれが2つ生成されます。

  • VertexShader.hlsl
  • VPRTVertexShader.hlsl

何者かというと、"VertexShader.hlsl"のほうがシミュレータで走らせるためのもののようです。"VPRTVertexShader.hlsl"はHoloLensで実行するものです。 しかしVPRTって何なのかよくわかっていません。あとで調べます。

なので、シミュレータで動くけど、実機で動かない(レンダリング周りの初期化でエラーになるとか)って時はこの2つのファイルの整合性を確認すると幸せになれそうです。

入力アセンブリの初期化で失敗する(CreateInputLayout)

先ほどのVertexShaderへの入力レイアウトの定義をもとにCreateInputLayoutするとシミュレータではうまくいくけど実機ではうまくいかないという罠にハマりました。 これもVertexShaderの入力が上記2種類で違いがあるために起きていました。 戻り値だけみても具体的に何が悪いのかわかりません(E_INVALIDARG)が、デバッグモードで起動していれ出力ウインドウに詳細を出してくれるので、よく読めばわかる問題でした。

実機でのレンダリング結果が壊れる(像がブレるなど)

HololensではDrawIndexedではなくDrawIndexedInstancedという関数を使います。 これは複数のインスタンスレンダリングするときに使うものです。

シェーダーでどちらのインスタンスを参照してレンダリングするかを決めています。(ここではCPUから渡された変換行列のどちらを参照するかを決めているだけ)

    int idx = input.instId % 2;
    pos = mul(pos, model);
    pos = mul(pos, viewProjection[idx]);
    output.pos = (min16float4)pos;

推測になりますが複数インスタンスとして左レンズと右レンズ用のインスタンスを作っていてそれを使い分けているのでしょう。

このあたりのインスタンスの参照先をうっかり書き換えてしまうと、像がブレる(というか、壊れて見える)ので要注意です。

あとは引数がやたら多いので、間違えないように注意です。

感想と今後の展望

とりあえず3Dモデルのレンダリングという基礎的な部分は作れるようになりました。モーションはシェーダの勉強と平行して実装していきたいところです。

あとHoloLensのSpatialMapping(空間認識)はちょっと独特(実際の物体よりも大きく空間を認識している)なので、アプリ側でもうちょっと改善できないか試してみたいところです。

最後にどうでもいいこと

HoloLensの空間認識は赤外線によるもの(Kinnectのような仕組み)だということなので調べてみました。 本当に赤外線出してるのかどうかスマホのカメラで見てみました。 アプリを起動していない状態では何も出ていないので、空間認識のサンプルアプリを起動した状態で撮影。 f:id:Catalina1344:20170207161543j:plain

写真のように光っています。これは一定のインターバルで点滅していました。やっぱり赤外線なんでしょうね。ほかのセンサがどう動いているかはこの写真ではわかりませんが。

Hololensで3Dモデルをレンダリングする[C++/UWP]

HoloLens実機で入出力回りをいじって色々と体感しようと試行錯誤です。かたりぃなです。 2017/02/06 追記:HoloLensからファイルピッカーを開くときの注意事項

この記事の意義

まずHoloLensを使ってプログラミングをしていく前にDirectXC++/cxを使う意義について整理したいと思います。

まず一番の目的は将来私自身が振り返った時、どこまで試したかを明確化しておいたほうが効率的だというのがあります。

次に現時点ではhololensでDirectXを扱った情報原が非常に少ないということです。 DirectX11でのWindowsプログラミングの情報は書籍でもWebでも充実していて、Windows上で3Dプログラミングをやりたいって人はそれで充分だと思います。 しかしC++/cxでのDirectX開発(UWP)となってくると情報が非常に少なくて、厳しいです。 というわけで、もし今後C++/cxでDirectX使ってHoloLensアプリを作りたいという人がいたとき、私の試行錯誤の記録が何かの参考になればと思っています。

では開発に入ります。 今回も開発ツールはVisual Studio 2015です。

2017RCが出ているので試してはみたいのですが、NuGetに2017対応のBoostが入ってないように見えたのでしばらく放置します。

言語はC++/cxです。

ジェスチャーを受け取る

Visual Studio2015でHoloLens用DirectXプロジェクトを生成すると、空間ジェスチャーの入力ハンドラが生成されます。 これは普通のUWPとかでは生成されないものです。実装はContent/SpatialInputHandler(.cpp/.h)です。 このクラス自身はジェスチャーをラッチするという単純な実装です。 具体的には

  • ジェスチャーを受け取って状態を保持する
  • 状態を取り出されたらクリアする

ラッチしたジェスチャーを取り出すのはアプリのMain(プロジェクト名にMain.cpp/Main.hサフィックスをつけたファイル)でやっていました。 ここのupdate関数(フレームごとに入力拾って立方体回してレンダリングする関数)でこんなのやってました。

#ifdef DRAW_SAMPLE_CONTENT
    // Check for new input state since the last frame.

    SpatialInteractionSourceState^ pointerState = m_spatialInputHandler->CheckForInput();
    if (pointerState != nullptr)
    {
        // When a Pressed gesture is detected, the sample hologram will be repositioned
        // two meters in front of the user.
        m_spinningCubeRenderer->PositionHologram(
            pointerState->TryGetPointerPose(currentCoordinateSystem)
            );
    }
#endif

単純に入力があったら、その視線の先に立方体を移動してあげるみたいです。 HoloLensエミュレータDirectXプロジェクトテンプレートをビルドしたものを放り込んで右クリックしたときに立方体がついてきていたのはこのルーチンがあったからなんですね。 今すぐにはジェスチャーによって何かするみたいな処理はやらない(というかやり方がよくわからない)ので、とりあえずここを間借りすることにしましょう。

UWPアプリでファイルを開く

3Dモデルのロード実験の方法としてはいくつか考えられます。

  • アプリにアセットとして組み込んでしまう
  • 都度ユーザーに選択してもらう

実験だけならどちらでもいい気がしますが、表示する3Dモデルの切り替えくらいできたほうが楽しいと思うので、後者でいきます。 前者も必要に迫られたらやると思います。 とりあえずユーザーがエアタップしたら先のコードが走ることはわかったので、書き換えます。

    SpatialInteractionSourceState^ pointerState = m_spatialInputHandler->CheckForInput();
    if (pointerState != nullptr)
    {
        auto openPicker = ref new FileOpenPicker();

        std::wstring cpath;
        openPicker->ViewMode = PickerViewMode::List;

        // ピクチャーライブラリーが起動時の位置
        // その他候補はPickerLocationIdを参照
        // http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.pickers.pickerlocationid
        openPicker->SuggestedStartLocation = PickerLocationId::PicturesLibrary;
        openPicker->FileTypeFilter->Append(".pmx");

        create_task(openPicker->PickSingleFileAsync()).then([&cpath](StorageFile^ file) {
            // 選択されたファイルに対してごにょごにょする
        });
    }

ちなみにこのコードを実行しようとすると、OneDrive入れてねと表示されるので、ストアから入れておきます。 理由は謎ですが今は目的のファイルさえ読めれば何でもいいです。 それにOneDrive経由のほうがテスト用のデータを投げ込むのラクになるので。 ファイルピッカーについての注意はFAQに載っていました。 同様の質問はStackOverFlowで出ていました(C#の場合)。 c# - Filepicker for Hololens: List available filepickers? - Stack Overflow

要約すると

  • HoloLensのファイルピッカーは「最初にインストールしたファイルストレージを扱うためのアプリ」を使って実現されている
  • ファイルピッカーとして使うアプリの切り替えはできない
  • つまり後から別の使いやすいエクスプローラを入れても、それを使うように切り替えたりはできない
  • OneDriveお勧め

だそうです。 今はこういう仕様とのことです。まあ開発者版だから仕方ないですね。

ファイルから読む

UWPでのファイル読み込みはMSが公開しているサンプルから適当なものを拝借しました。

https://github.com/Microsoft/Windows-universal-samples.git

さっきのファイルピッカーで選択されたファイルを全部バッファに読み込むまでのコードです。

     create_task(openPicker->PickSingleFileAsync()).then([this](StorageFile^ file) {
            this->path = file->Path;
            return FileIO::ReadBufferAsync(file);
        }).then([this](task<IBuffer^> task) {
            auto buffer = task.get();
            auto reader = DataReader::FromBuffer(buffer);
            auto length = buffer->Length;
            auto loaded_buffer = reader->ReadBuffer(length);
            parse_(this->path->Data(), loaded_buffer);
        });

ファイルのパスをthisに保持しているのはデバッグ用です。 また今回は魅力的なモデルが多数公開されているMMDを使うのですが、どうもテクスチャのイメージファイルのパスが相対パスで記述されているものもあるみたいなので、いったんここでとっておくようにしています。 ちなみにここで拾ったpathをC/C++の標準関数で開けないかと試してみましたが、「アクセス権がない」といわれて開かせてくれません。 OneDrive経由のファイルだからなのか、このあたりはよくわかりません。

boostをインストール

Nuget経由でboostをインストールします。わざわざビルドしなくて済むのいいですね。 boostは簡易な機能であればヘッダだけで使えるのですが、今後のことも考えてライブラリまとめて取り込んでおきます。 スクリーンショットで選択されているものをインストールすれば依存関係の解決でヘッダもついてきます。 boostを使う理由は後述。 f:id:Catalina1344:20170205130208p:plain

どうしてboost?

ライブラリのインターフェースとして「ストリームやファイルから読み込んで処理する」みたいなのが多々あります。 今回使おうとしているパーサなどもこういったインターフェースです。 ファイルの読み取りはUWPの機能を使って実装したので、あとはインターフェースの橋渡しをしてあげれば話は簡単になります。 (ライブラリ側には手を入れなくていいし、ファイルアクセスはUWPの枠組みで行える) この橋渡しをするために、basic_ivectorstreamというクラスを使うことにしました。 このクラスを使えばc++コンテナをistreamとして扱えるようになります。便利ですね。 というわけで、プログラムの構造はこうなりました。

  1. UWPのランタイムで提供される機能を使ってファイルの内容を読み取る
  2. UWPのバッファのアドレスをCOM経由で取り出す
  3. std::vectorとしてバッファを準備して、そこにデータをコピー
  4. Boost::basic_ivectorstreamでstd::vectorをラップする
  5. ラップしたデータをistreamとしてライブラリに渡す(ここではMMDパーサ)

すごく回りくどいことになりましたが、いったん何かを作りたいのでtodoコメントだけ入れておきます。 ちなみにComPtrを使うにはwrl/client.hが必要です。

 // IBufferオブジェクトからC++標準のistreamを作りたい
    Object^ obj = buffer;
    Microsoft::WRL::ComPtr<IInspectable> insp(reinterpret_cast<IInspectable*>(obj));
    Microsoft::WRL::ComPtr<IBufferByteAccess> bufferByteAccess;
    DX::ThrowIfFailed(insp.As(&bufferByteAccess));
    byte* pmx_raw_data_ptr = nullptr;
    DX::ThrowIfFailed(bufferByteAccess->Buffer(&pmx_raw_data_ptr));

    // COMインターフェイス経由で得たアドレスを使ってvectorを生成したい
    // いい方法が思いつかないのでコピーで済ませる : todo改善ポイント
    std::vector<char>    pmx_raw_data;
    pmx_raw_data.resize(buffer->Length);
    CopyMemory(&pmx_raw_data[0], pmx_raw_data_ptr, buffer->Length);

    boost::interprocess::basic_ivectorstream<std::vector<char> >  input_vector_stream(pmx_raw_data);

これで、以降はinput_vector_streamをistreamとして読めるようになります。 中身にはもちろんファイルから読み込んだデータが入っているので、istreamを扱うパーサライブラリさんとboostさんが連携して適切に処理してくれます。 改善ポイントは多々ありますが、それは追々やっていきます。 mmdのファイル中にテクスチャ画像ファイルへの参照パスが含まれていますが、今はおいておきます。

DirectXを叩く準備をする

やっと最低限の下地が整いました。 DirectX11に対してパースした3Dモデルのデータを投げ込んでいきます。 VisualStudioテンプレートで生成したコードではSpinningCubeRendererクラスがDirectXでのレンダリングを担当しています。 #if DRAW_SAMPLE_CONTENTで切り替えられているので、レンダリングするコンテンツにあわせた最適なクラスを作れという意思表示でしょう。 しかし今回は「どういった感じなのか」という感触をつかみつつ、何かをレンダリングするのが目的なので、このクラスを直接変更していくことにします。

DirectXで3Dダリングするために必要なもの

初期化周りはややこしいので放置します。幸いなことに別クラスで面倒見てくれていますし。 CreateDeviceDependentResourcesに記述されているものを順に整理します。

  1. プログラマブルシェーダ
  2. シェーダーへの入力レイアウト
  3. 3Dモデルの姿勢を表す変換行列
  4. 立方体の頂点情報(xyz座標,rgbカラー)
  5. 頂点情報からポリゴンを作るためのインデックス指標

mmdファイルをパースして取り出した情報を設定する対象は4と5です。 生ポリゴンをレンダリングするだけならこれで充分です。

できればテクスチャ貼りたいところですが、VisualStudioのDirectXテンプレートではシェーダーが受け取る頂点フォーマットにテクスチャ座標が含まれていないうえ、そもそもテクスチャサンプラとかの初期化も入っていないので、大工事になってしまいます。

年度末の道路工事をやるとしても部分的にやっていったほうが全面通行止め箇所も少なくて通りやすい=安心できるので、テクスチャはシェーダー含めて今後やっていきたいと思います。

ファイルから頂点とインデックスを取り出す

作業を細分化して話は簡単になりました。3DオブジェクトのジオメトリのみDirectXに与えればいいわけですね。 やってみましょう。

まずファイルから読み込んだ頂点とインデックスを適当にクラスメンバに保存しておきます。変数名がなんかアレですがMSのサンプルの名前付け規則に合わせました。

型ですがDirectXテンプレートのデフォルトの頂点シェーダに合わせるようにしています。 頂点型がfloatなのは良いとしても、インデックス型がunsinged shortって不安ですね。 しかし幸いなことに手元の3Dモデルではindexの上限が65535(=0xFFFF)なので助かりました。(古いフォーマットからコンバートされたものなのだろうか。。。)

// SpinningCubeRenderer.h
namespace HolographicApp1
{
    // This sample renderer instantiates a basic rendering pipeline.
    class SpinningCubeRenderer
    {
    private:
        std::vector<float>           m_vertics;
        std::vector<unsigned short> m_indics;
// SpinningCubeRenderer.cpp
    const int elements_of_vertex = 6;
    auto vertex_num = reader.total_vertex_num();
    m_vertics.resize(vertex_num * elements_of_vertex);

    float scale = 0.01f;
    for (size_t i = 0; i < reader.total_vertex_num(); i++) {
        auto r = reader.get_vertex(i);
        m_vertics[i*elements_of_vertex + 0] = r.position[0] * scale;
        m_vertics[i*elements_of_vertex + 1] = r.position[1] * scale;
        m_vertics[i*elements_of_vertex + 2] = r.position[2] * scale;

        m_vertics[i*elements_of_vertex + 3] = 1.0f;
        m_vertics[i*elements_of_vertex + 4] = 1.0f;
        m_vertics[i*elements_of_vertex + 5] = 1.0f;
    }

まずscaleについてですが、そのままのスケールで表示してしまうと画面(実機では視界)からはみ出す巨人が出てきてしまいました。 xyz全体にとりあえず適当なスケールかけておきます。

次にフォーマットです。 このデータ列の塊をDirectX経由でGPUに転送するわけですから、頂点シェーダーの読み取るフォーマットに合わせておかないと大変です。 VisualStudioが生成したDirectXのテンプレートに含まれているシェーダー入力では頂点フォーマットはxyz,rgbの6要素からなります。 xyzにファイルから取り出した頂点の座標を入れていきます。カラー情報今は無視したいので1.0でも設定しておきます。 0.0にしてしまうと何も表示されないことになってしまいます。 これはHoloLensが光を投影するという原理であるための制約ですね。たぶん。(黒い光は出せませんよね。。。) PC向けUWPアプリなら背景色を黒以外に設定しておけば0.0でも見えるとは思います。

次にインデックス

 auto index_num = reader.total_indics_num();
    m_indics.resize(index_num*3);

    auto face_num = reader.total_face_num();
    for (size_t i = 0;i < face_num; i++) {
        auto face = reader.get_face(i);
        auto* face_buf = &m_indics[i * 3];
        face_buf[0] = face[0];
        face_buf[1] = face[1];
        face_buf[2] = face[2];
    }

今回使用したファイルパーサのインデックスの出力は「3つのインデックスで1つの三角形ポリゴンを表現する(TRIANGLE_LIST)」ので、こうなります。 どちらの例も本来なら適当な構造体を定義してそれの配列とするべきでしょうけれど、まあこれで一旦やってみましょう。 構造体作るとアライメント考えないといけませんし。

つくった頂点とインデックス情報をDirectXのバッファに書き込みます。いわゆるGPUのVRAMに転送ですね。 まず頂点バッファ

     D3D11_SUBRESOURCE_DATA vertexBufferData = { 0 };
        vertexBufferData.pSysMem = &this->m_vertics[0];
        vertexBufferData.SysMemPitch = 0;
        vertexBufferData.SysMemSlicePitch = 0;
        const CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(float) * this->m_vertics.size(), D3D11_BIND_VERTEX_BUFFER);
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateBuffer(
                &vertexBufferDesc,
                &vertexBufferData,
                &m_vertexBuffer
            )
        );

次にインデックスバッファ

     m_indexCount = m_indics.size();

        D3D11_SUBRESOURCE_DATA indexBufferData = { 0 };
        indexBufferData.pSysMem = &m_indics[0];
        indexBufferData.SysMemPitch = 0;
        indexBufferData.SysMemSlicePitch = 0;
        CD3D11_BUFFER_DESC indexBufferDesc(sizeof(unsigned short) * m_indics.size(), D3D11_BIND_INDEX_BUFFER);
        DX::ThrowIfFailed(
            m_deviceResources->GetD3DDevice()->CreateBuffer(
                &indexBufferDesc,
                &indexBufferData,
                &m_indexBuffer
            )
        );

m_indexCountはクラスのメンバ変数として最初から定義されていたのですが、実際のレンダリング時にこれを使っていました。

    // Draw the objects.
    context->DrawIndexedInstanced(
        m_indexCount,   // Index count per instance.
        2,              // Instance count.
        0,              // Start index location.
        0,              // Base vertex location.
        0               // Start instance location.
        );

というわけでこれで生ポリゴンを出す準備は整いました。

動かしてみる

最低限のポリゴンレンダリングのコードを書いたので動かしてみました。 実機で動かしても同様の結果が得られました。

実機で動画とってアップしようかと思いましたが、机の上どころか部屋が散らかりすぎていてアップロードするのはキツイです。 エミュレータスクリーンショットです。

f:id:Catalina1344:20170205185659p:plain

シルエットはきちんと表示されていますね。まずはOKですね。

課題

実機で動かしてみて、とりあえず机の上に置いてみました。フィギュアみたいです。 VRもそうですがとりあえずお約束としてまずは覗き込みますよね。みえ。 見えたけど、内側から見るとスカートが見えなくなってしまいました。どうやらこのモデルは外側からしかポリゴン貼ってないようです。 AR/VRでアプリを作るときはカリングモードの設定とモデルの作り方に要注意ですね。

また、Hololensのデモやアプリの実装を見ていると、現状のソフトウェアではまだまだ機能が足りないなと思う部分があります。 たとえば表示されているホログラムと実世界のつながり。

Hololensは現実世界の空間マッピングを行ってマッピング情報をもとに適切な位置にホログラムを表示できるのが基本です。 いろいろ触ってみて感じたのは「表示されているホログラムを掴みたい」です。 ホログラムを掴むためには表示されているホログラムの情報を単純に空間マッピングに書き戻せばできそうですが、それってHololensのマシンスペックで解決できるレベルなのかどうか少々不安です。 「掴む」ことができないならマーカー型ARみたくマーカー置けば似たようなことはできそうな気はします。

今後の展望

週末2日間でそれっぽいところまでできました。 あとはテクスチャを張ってモーション再生してあげれば楽しそうです。(やっとUnity組に追いつける!)

いくつか気になる点があるので次はこのあたりから手を付ける予定です。 1. MMDパーサがテクスチャ読み込もうとしてエラー吐いている 2. モデルの配置位置が固定になってしまう

まず1。今の仕組みだとエラー吐いて当然だと思います。OneDriveのファイルをダウンロードしたパスからテクスチャを読もうとするので、ファイルのダウンロードが終わっている保障はどこにもありません。というかダウンロード命令出していません。 ファイル操作まわりをもっとラクに扱えるように整理したいですね。

次に2。今回の実装でジェスチャの機能を殺している(エアタップされたら読み込むモデルを選択する)ので、このあたりも使い方を調べて適切なUIを作りたいですね。

Hololensが届いた

やっとhololensが届いたので開封の儀式です。かたりぃなです。

 

開封

f:id:Catalina1344:20170131202531j:plain

 とりあえず開封した直後の写真です。この写真中央部の奥にマニュアルが入っていました。

どうせマニュアル全部英語だろと決めつけて冊子を半分読んだところで、残り半分が日本語マニュアルという事実。ショックです。

 

初期化

マニュアルに従って装着して電源オン。

音声ガイドに従って初期設定していきます。音声ガイドは英語です。KIAIで解読しましょう。画面表示内容みれば何言ってるか推測はつきます。

まずキャリブレーションから。表示されるカーソル位置に指をあわせてキャリブレーションしていきます。

指紋認証か何かかと勝手に深読みしてしまいましたが、そんなことはなかった。

 

操作方法ガイドを一通り試す

キャリブレーションが終わると操作方法のガイダンスがはじまります。

指をすぼめて上に向けた状態から開くのがブルーム。

人差し指を立てた状態から倒すエアタップ。

長押しするとスクロールバーとかつまめるよみたいなのも。

クリッカーの設定もここでやります。クリッカー側のペアリングボタンは細いものがないと押せないので、事前に準備しておいたほうがいいです。とりあえず手元に転がってたボールペンの先端で押しました。

 

アカウント設定

ガイダンスを終えるとアカウント設定です。

ここから先はHololens Emulaterと同じ手順です。

ちなみにこの時点ではBlueToothキーボードは未設定(設定するタイミングが無い)なので、クリッカーと視線で頑張って入力します。(使えるキーボードがまだ手元にないから、どっちみち使えないのですが。)hololensの視線入力はこういう細かい操作には向きませんね。慣れの問題かもしれませんが。

 

とりあえず起動

アカウント設定が終わばあとは好きな事ができます。

3Dオブジェクトを好きな位置に配置したり、適当なゲームを試したりと。

部屋の壁に穴が開いて、虫が湧いてくるのを退治するゲームを試してみましたが結構面白いです。

ただ、視野角が狭いので視線だけで対象を追おうとするとホログラフィックが消失してしまいます。視野角については今後の改善に期待ですね。まあ暫く使っていけば「こんなもんだろ」と慣れてくると思いますが。

 

アプリを書き込む

早速アプリを書き込みたいところですが、まだ何も作っていません。

でもせっかくだから何か書き込みたいですよね。というわけでDirectXのUWP(holographic)のサンプルコードを投入しましょう。

ビルドしてターゲットデバイスとUSB接続。デバッグ開始を押すと始まります。

ただし初めての書き込み時はPINコードの設定をしろと言われるのでこのあたりを参考に設定します。設定のセキュリティのとこにあります。

表示されるコードをVisualStudioに入力すればOKです。

初めてのHoloLens開発(2D/Holographicアプリ)[環境準備~実行、HoloToolkit-Unity活用] - Build Insider

 

とりあず動きました。スクリーンショットの撮り方がわからないのでまた別の機会に。

 

今後の展望

もうすでに何かやってる人いないかなと思って色々と調べましたが、UnityとVuforiaの人が多かった印象です。手軽に作って試すにはよさそうです。

イノベーションを起こすならフットワークの軽さは重要だと思います。

ただ、そうして先端を走り続けるには限界があると思っている(どこかでフレームワークの限界が来る)ので、マイペースでいきます。下から攻めたほうが有利というか、人の上を行くなら下から行けみたいな格言もありますし。

本質的な部分を押さえておけばHololensが普及しなくても応用が利きます。まあ結局ライブラリ叩くんですけどね。

というわけで今年一年くらいのペースでやりたいことを技術要素に落とし込んだものをリストアップしてみました。

  1. ソフトウェアで映像入力を受ける(Microsoft MediaFoundation)
  2. 映像を処理して、ある共通する特徴を持った物体(カードとか)の特定物体認識を行う(OpenCV-imgproc etc.)
  3. 特定物体認識で検出した物体をトラッキングする(OpenCV-opticalflow etc.)
  4. 検出した物体の詳細を(カードの絵柄とか文字列とかを使って)分類する(chainer - machine learning)
  5. 検出結果をもとにWebから追加情報を拾ってくる(UWP-HTTPClient)
  6. 3Dオブジェクトのレンダリングを行う(Microsoft DirectX11)

なんとなくですが2と4が一番ネックになりそうな気がしています。

だいたい以前やったことのブラッシュアップなので、結構なんとかなる気がしていますし、ブラッシュアップの段階でHololensの機能を借りれば色々と良くなりそうです。

例えば

ARの原理実験 - catalinaの備忘録

Chainerでcifar-10画像分類を試してみる - catalinaの備忘録

Windowsストアアプリを作ってみる - catalinaの備忘録

openGLでMMDモデルを表示する - catalinaの備忘録

だけでも最低限のプロトタイプくらいはできそうです。

あとは機械学習をどう活用するか考えながら色々実験していきたいですね。

Hololensのスペック的にDNNを載せるのはきつそうなので、そこも工夫が必要そうかなと思います。

 

あ、DirectX版のレンダリングの記事書いていないのに気づきました。

UWP版を作るときに整理します。とりあえず今回は写真だけ。適当なパーサ落としてきてDirectX11でレンダリングしてみたところです。

色合いがおかしいのは適当に書いたHLSLシェーダーのせいです。確かMMD本家はトゥーンですよね。。。

f:id:Catalina1344:20170129221225p:plain

UWP版への移植はこれからですが、とりあえず表示まで行ってるので、あとはなんとかなる気がしています(というかMMDはライセンス面倒だから他のフォーマットがいいな。。。)。

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

Windowsストアアプリを作ってみる

Windowsストアアプリを作ってみました。かたりぃなです。 HoloLensが目標ではあるのですが、Windowsストアアプリの作り方もわからないまま買うのはただのギャンブルなので、まずはかんたんなアプリを作ってみて、慣れてからHololens用に移る算段です。

アプリを作る動機

MagicTheGatheringというカードゲームの遊び方の一つにプレインチェイスというのがあります。 これは次元カードというものを使うのですが、悲しいことに英語版しか発売されていません。 頑張れば読めなくはないですが、わからない単語が出てきたりするとスマホでネットを調べることになり、ゲームが中断されてしまいます。というわけで ゲームが中断されてしまうと興ざめです。

何を作ったの?

できることは次のとおりです。

  • MTGのプレインチェイスの次元カードを一覧で見れる(日本語/英語ともに)
  • カードの詳細をすぐに見れる

アプリとしてはとてもシンプルです。

技術的には

  • アプリが起動されたら自動的にMTG-WikiAPIを叩いて、次元カードの一覧を拾ってくる
  • 次元カード一覧をリストで表示できる
  • ユーザーはカード一覧から詳細を見たいものを選択できる
  • 選択されたカードの詳細情報をMTG-WikiAPIでとってくる
  • カードの詳細を表示できる
  • 詳細ページを見終わったらリスト画面に戻れる
  • リストアップされたカード中から検索できる
  • 一度見たMTG-Wikiの情報はアプリのローカルストレージにキャッシュする

です。 とても簡単そうですが、「まずは作ってみる」目標としては丁度いいレベルだと思います。 言語はc++です(HoloLensでDirectX叩きたいので、その準備)。 では開発者アカウントの取得から順にポイントを順に整理していきます。

Microsoft開発者アカウントを取得してVisualStudioに設定する

既にとってあるので省略します。Microsoftの開発者向けサイトから適当に登録します。 登録するためにはMicrosoftアカウントが必要です。クレジットカードを登録して千円くらい支払えば完了です。 この登録でMicrosoftアカウントが開発者アカウントとして登録されるみたいです。

登録後にVisualStudioを起動するとMicrosoftアカウント設定しましょうとか出るので、開発者登録したMicrosoftアカウントを設定するだけです。

Windows Phoneの開発者向け機能のロック解除

まず開発用マシンとWindows PhoneをUSB接続しておきます。今回使用するのはFreetelのKATANA2です。

ドライバインストールが終わってエクスプローラ起動してストレージが見える状態になってればOKでした。 WindowsPhoneが認識されたら「Windows Phone Developer Registration」というツールを起動します。 Windows10SDKに含まれているので、インストール済みならCortanaさんに聞くだけで場所を教えてくれます。 ロック解除が終わると「Windows Phone Developer Registration」はこんな画面になります。 f:id:Catalina1344:20170124211021p:plain

Windows Phone側での設定

これはWindows Phone側の作業です。

開発者向け機能がアンロックされたので、次のぺージを参考にしつつ設定していきます。 Enable your device for development 某Androナントカのときも似たようなことしてた気がします

適当なプロジェクトをビルド・デプロイする

再びPC側での作業です。Visual StudioのテンプレートでUWPの適当なプロジェクトを選択してARM向けビルドするだけです。 Release設定じゃなきゃダメかもなーと思っていましたが、Debugプロジェクトもデプロイできました。

VisualStudioの画面としてはここを設定します。 f:id:Catalina1344:20170124211332p:plain

ちなみに、デプロイ時にデバイスがアクティブである必要があります。(ロック画面や画面が消灯している状態ではダメ) まあ失敗したらエラーメッセージ出るので、その都度直せばいいかと。

デバッガの起動が重い

シンボル情報の読み込みに時間かかりまくります。単純なUWPアプリをデバイスに書き込んでデバッガ起動するまで数分くらいです。 初回だけなので、我慢しましょう。我慢できないときはCtrl+F5でデバッガ接続なしでデプロイです。

ここまで一日かからずにできました。簡単ですね。 やっとプログラミングです。 プログラミングは年明けから始めたので、一か月弱でここまでできたぞと。

プログラミングで躓いたポイント

たくさんあってもう忘れかけていますが、せっかく新鮮な体験をしたので思い出せる限り記録につづりたいと思います。

アプリのマニフェスト

なぜかカメラが起動できないとか、なぜかWebアクセスできないといった時に真っ先に疑ったほうがよいところです。 プロジェクト中のPackage.appmanifestを開いて適切なアクセス権限を設定しましょう。

C++/cxのハット(^)記号

Windowsランタイム側で寿命管理してくれる(マネージド)なポインタのようです。 感覚的にはstd::shared_ptrみたいなもんかと思ってます。インスタンス化するときはnewではなくref newで。std::make_sharedと同じノリですね。 自前のc++クラスをマネージドにしたいとき(データバインディング対象にしたいとか)はref class class_name sealedでクラス定義を書くようです。

文字列処理

Platform::Stringは基本的な機能しか提供していないので、状況によっては自前で編集する必要があります。 とっても面倒ですね。 新しいAPI覚えるの面倒なのでC++の世界に持ってきて解決することにします。 UWP(Platform::String^)の文字列はstd::wstringにそのまま変換できます。

ただ、C++の世界そのままとはいえ文字列がwstringです。 単純な文字列リテラルとの比較をする場合でもwstring同志でやる必要あります。 Lつければこのあたりの面倒みてくれるらしいです。ラクですね。

// テキストボックスの文字列をc++の文字列にする
std::wstring    str(textbox->Text->Data() );

// C++の世界でやりたいことをやる
// たとえば検索
auto pos = str.find(L"Search"); // Lつけるのを忘れずに

// uwpに戻す
this->textBox->Text = ref new Platform::String(str.c_str() );

create_tasks

バイル端末もマルチコア時代なので、非同期処理を書きましょう。ユーザースレッドを止めないのが基本です。 毎回スレッド作るのってコスト高い気がしますが、ランタイムがスレッドプールを持っていて必要に応じて割り当ててくれるらしいので、気にせずcreate_taskしましょう。 ちなみにこのライブラリはMicrosoft PPL(並列パターンライブラリ)というらしいです。

非同期の例といえばWebAPIですね。とりあえず適当なURIを叩いてみましょう。

 create_task(client->GetAsync(request_uri)).then([](HttpResponseMessage ^ response) {
        response->EnsureSuccessStatusCode();
        return response->Content->ReadAsStringAsync();
    }).then([title, this](Platform::String ^ response_string) {
    });

サンプルコードコピペすれば動くのですが、ここが一番手こずったポイントです。一週間くらい悩みました。 細かく分割して整理します。分けて考えるは基本ですね。

create_taskによってタスク生成する

文法としてはこの部分です

create_task(client->GetAsync(request_uri))

この例ではhttpclientのGetASyncが返す関数オブジェクトを渡すことになりますが、ここには関数ポインタなら何でも渡せます。ラムダでもいいわけです。 作ったタスクは即時実行されるわけではなく、スレッドプールから割り当てられたスレッドを後で実行されます。(UIスレッドを止めない)

ただ、実行が終ったあとで何かしたいですよね?UIの更新だったり、ファイル保存だったり。 このままでは後続の処理に困ります。そこで登場するのがthenです。

.then

新しい言語仕様か?と思ってしまいますが、そんなことはありませんでした。create_taskで生成されたtaskクラスのメソッドthenです。 thenにも関数オブジェクトを渡すことができます。 こいつに渡す関数が先ほど登場した「後から実行したい何か」です。 thenが返すのもtaskなので、こうやって非同期操作を数珠繋ぎにしていくのがスタイルのようです。

ラムダ

c++にもラムダが実装されたので、ここではラムダをthenに渡しています。 昔ながらの非同期操作だとイベントハンドラごとに関数をわけて記述するので全体が見通せなくなりがちでした。 こうやってラムダで記述すれば全体が見通せるのでスッキリしますね

ちなみにMicrosoftのサンプルコードではこういう記述を見かけることがあります。

[](HttpResponseMessage ^ response) -> typename {}

ポインタからの何かを参照しているのかと勘違いしていましたが、どうやらこれはc++の機能の戻り値の型を後置する記法らしいです。

戻り値の型を後置する関数宣言構文 - cpprefjp C++日本語リファレンス

今回は直接関係ないので省略しますが、テンプレートを使うと戻り値の型を調べるのが大変になってくるので、auto宣言しておいてこういう記述を使うと幸せになれそうです。

非同期操作中に変数の寿命が尽きることがある

そもそも設計が間違っているのかもしれませんが、これに悩まされました。 非同期操作の関数にマネージドポインタを渡すと、変数の参照カウントが増えるので非同期操作が終わるまで安心して使えて、解放も自動的に行えるのですが、 taskを数珠繋ぎにしてアダプティブなクラスをかぶせていくようなスタイルだと、途中で変数の寿命が尽きてしまうことがあります。 実際に問題があったコードで絆創膏的な対処しかしていませんが、こういうのです。

         readtask.then([this](Streams::IRandomAccessStream ^ stream)
            {
                return BitmapDecoder::CreateAsync(stream);

            }).then([this](BitmapDecoder ^ decoder) -> IAsyncOperation<SoftwareBitmap^>^
            {
                return decoder->GetSoftwareBitmapAsync();

            }).then([this](SoftwareBitmap^ bitmap) -> IAsyncOperation<OcrResult^>^
            {
                this->tmp_bmp = bitmap;    // ここで保持しておかないと、OCR実行中にbitmapが消失してエラーになってしまう。
                                        // どうしてこうなるか理由は不明。
                return this->ocr->RecognizeAsync(bitmap);

            }).then([this](OcrResult^ result)
            {
                // 空白文字を除去する
                std::wstring    str(result->Text->Data() );
                std::wstring::size_type pos;
                do {
                    std::wstring target(L" ");
                    pos = str.find(target);
                    if (pos != std::wstring::npos) {
                        str.erase(pos, target.length());
                    }
                } while (pos != std::wstring::npos);

                // 文字認識結果を取り出す
                this->textBox->Text = ref new Platform::String(str.c_str() );
            });

寿命が尽きるということまでは解っているので、また気が向いたときに追ってみます。

コンパイル時データバインディング

まずデータバインディングとはUIとアプリケーションロジックの分離(コードビハインドというらしい)をスマートに行うものです。 WPFの頃からデータバインディング自体はありましたが、いつの頃からかコンパイル時データバインディングが実装されたようです。 リストビューを作る参考にさせてもらいました。

UWPアプリでコンパイル時バインド(x:Bind)を使ってListViewに値を表示する - Qiita

上記はC#での例なので、C++/cxで同じことをやろうとしたところで少し手こずりました。 ポイントは

  • ネイティブ型はデータバインディングバインディングソースに(というかプロパティとして見せること自体が)できない
  • MVVMのVMとして見せるクラスも同様
  • プロパティのgetter/setterはUWPランタイムのクラスなら書かなくてもいい

というわけで単純に文字列をlistviewで表示するコードはこうなりました。

まずXAML

<ListView x:Name="listView" ItemsSource="{x:Bind Path=card_list}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="local:data_element">
            <StackPanel>
                <TextBlock Text="{x:Bind card_name}"></TextBlock>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

次にプロパティを宣言するクラス

 // viewクラスから見せるプロパティ
    public:
        property Windows::Foundation::Collections::IVector< data_element^> ^ card_list;

最後にプロパティをインスタンス化するところ

        // OnNavigatedToあたりでリストをインスタンス化する
    card_list = ref new Platform::Collections::Vector< data_element^>();

あとはcard_listに対してよくある操作(要素の追加/削除/更新)をすれば、自動的にUIに反映されます。

プラットフォーム固有の型を覚えるのが面倒

型情報はC++使いとしてはとても大事ですが、プラットフォーム固有の型とか覚えるの(というかマニュアル調べるの)面倒です。 できるだけautoで済ませましょう。 forはどうするかと考えてしまいますが、コレクションに対する操作なら範囲指定forで充分です。 状況によっては型情報(Platform::Object型を返してくるとか)が必要になりますが、そういうとき以外はautoでいきます。

たとえばアプリのローカルストレージに保存してあるcompositeを読む処理はこうなります。 型がわからなくなりそうですが、このくらいならVisualStudioのIntelisenceも動きますし、マウスカーソルでポイントすると型情報を見せてくれます。autoさん(というかVisualStudio)すごい。

 auto localSettings = ApplicationData::Current->LocalSettings;
    auto composite = safe_cast<ApplicationDataCompositeValue^>(localSettings->Values->Lookup("cards"));

    if (composite) {
        // 二回目以降の起動の場合、既にあるカードリストを取り込む
        for (auto value : composite) {
            auto cardname = safe_cast<String ^>(value->Value);
            auto element = ref new data_element(cardname);
            card_list->Append( element );
        }
    }

まとめ

長くなりましたが、書初めプログラムとしては非常に楽しいものでしたし勉強になりました。 次はDirectX11を叩いてみて、納得がいくところまでできたらHoloLens購入したいと思います。