読者です 読者をやめる 読者になる 読者になる

catalinaの備忘録

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

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購入したいと思います。

WindowsストアアプリでOpenCVを使う

Hololensが楽しそうなのでアプリ作れないか試行錯誤しています。かたりぃなです。 開発者向けの販売とはいえ購入には踏ん切りがつかないので、何かつくれるようになったら購入したいと思っています。 というわけでWindowsストアアプリ向けに画像処理のアプリを作る基盤づくりです。 今回はOpenCVWindowsストアアプリ向け(UWP)にポーティングしてみます。

とりあえず動かしたい

手っ取り早くHololensエミュレータOpenCV動かすにはこちらがお勧め。 HololensのCPU向けにビルドされたパッケージ一式がすでにNuGetにありました。 OpenCV for Hololens (UWP/C++) - NuGet Must Haves

c++/cxのUWPプロジェクトを作って適当なopencvAPIを叩いてみたところ動作はしました。 ストアアプリ認定キットも通るので問題なく使えそうです。 このNuGetパッケージのcv::getBuildInformation()を参考にすればOpenCVソースコードからビルドするのに使えるかもしれませんね。 ただ配布されているパッケージはRelease版のライブラリのみなので、デバッグに難ありです。

公式で何かやってるんじゃない?

MSもOpenCVをforkしていました。 Easily build OpenCV-powered apps for Windows Store! - MS Open Tech ブラウザからパっと見た限り特別何か手を入れてる様子は見えません。 git cloneして中身を調べてみた内容をメモします。

既に試した人がいるので、参考にさせてもらいました。

Windows 10ユニバーサルアプリ(Universal Windows Application)でOpenCVを使う(その2) - embeddedなブログ

とりあえず中身みてみる

windowsでもpowershellが入ってコマンドライン操作も昔より楽になりました。 コマンド体系が独特ですが、まあ慣れの問題でしょう。 とりあえず中身を見てみます。

git clone https://github.com/Microsoft/opencv
git branch -a
  master
  remotes/origin/HEAD -> origin/master
  remotes/origin/contrib-enable
  remotes/origin/highgui
  remotes/origin/master
  remotes/origin/test
  remotes/origin/test-attach
  remotes/origin/test-vs2015
  remotes/origin/test_gtest_upd
  remotes/origin/update_py_script
  remotes/origin/videoio-old-sample
  remotes/origin/vs2015-samples
  remotes/origin/vs2015-samples-ARM
  remotes/origin/winrt

vs-2015-samplesってのが気になりますね。

git checkout -b vs2015-samples remotes/origin/vs2015-samples

このブランチのREADME.mdを見ると、OpenCVをUWP向けにビルドする手順が記載されています。 Short wayとLong wayがありますが、まずはShort wayを試してみます。 準備済みのソリューションファイルを開いてビルドするだけです。 何が違うのかちょっと気になったので、簡単にですがOpenCV本家と比較してみました。

  1. UWP環境でビルドするコンパイルオプションの追加(/ZWなど)
  2. vcxprojファイルにWindowsストアアプリ用の設定を追記

まず1ですが、OpenCVをデフォルト設定でビルドすると、従来のWindowsアプリ向けのDLLとインポートライブラリが生成されます。 これはWindowsストアアプリで配布できないらしく、Windowsストアアプリ用にはUWPとしてDLLを生成する必要があります。 このためのコンパイルオプションが/ZWらしいです。 方法: ユニバーサル Windows アプリで既存の C++ コードを使用する

次に2ですが、単純にOpenCV本家のビルドオプションに/ZWを付けるだけではダメでした。 UWPのvcxprojファイルをテキストエディタで開いてみるとに次の内容が記載されています。

  • ApplicationType
  • WindowsTargetPlatformVersion
  • WindowsTargetPlatformMinVersion

など。 アプリ配布のターゲットを示すもののようで、単純にOpenCVのビルドプロジェクトを/ZWを付けてcmakeした場合、このあたりの記述が行われません。 このためコンパイルエラーになってしまいます。 MS曰く適切に記述しなさいとのこと。 ユニバーサル Windows プラットフォームへの移植 (C++)

OpenCVをUWP(Windowsストアアプリ向け)にビルドする

ソリューションファイルのパスはMicrosoft/OpenCVのルートディレクトリから見て

  • vs2015\WS\10.0\ARM
  • vs2015\WS\10.0\x64
  • vs2015\WS\10.0\x86

の3つが各CPUアーキテクチャ向けのものです。 いずれのディレクトリにもOpenCV.slnがあるのでこれをビルドすればOKです。 READMEに書いてあるように環境変数OCV2015_ROOTにopencvのルートディレクトリを設定してからビルドです。 この環境変数がないとOpenCVが参照しているライブラリ(zlibとか)を探せないのでエラーになります。 Windowsストアアプリとして組み込むにはRelease版が必要ですが、開発中はデバッグ版も欲しいので、Debug,Release両方をビルドするのを忘れずに。

ビルドで生成されるもの

ビルドが終わるとソリューションファイルの配置されているディレクトリに次のものが生成されます。

  • /include
  • /lib/Debug
  • /lib/Release
  • /bin/Debug
  • /bin/Release

それぞれの役割は名前から想像できるとおり、 includeはインクルードファイル群の置き場です。 libはDLLをアプリケーションが利用するためのインポートライブラリ一式です。 binはDLL本体です。

生成されたOpenCVWindowsストアアプリで使う

ここからはストアアプリのVisualStudioプロジェクトで設定をしていきます。 ソリューションエクスプローラからプロジェクトのプロパティを開き、「C/C++ -> 追加のインクルードディレクトリ」で設定します。 libは同様に「リンカー -> 追加のライブラリディレクトリ」で設定します。 リンクするライブラリファイルはリンカの入力で指定しても良いのですが、一個一個記載していくとミスの元になるのでスクリプト化しました。 プロジェクト作るたびに設定するのも面倒ですし。 PowerShell書くのは初めてですがなかなか楽しめました。

gist.github.com

こいつを実行すると、生成されたインポートライブラリを取り込むためのC++ソースコードが生成できます。 各アーキテクチャ(x86/x64/ARM)と各ビルド(Debug/Release)libディレクトリにあるライブラリを全てリンクするようなC++ソースコードが生成できます。 こんな感じ。

// genereted powershell script from gen_opencl_linkheader.ps1
#include "pch.h"
#ifdef _DEBUG
#ifdef _M_X64
#pragma comment(lib, "C:\\myproject\\ms-opencv\\opencv\\vs2015\\WS\\10.0\\x64\\lib\\Debug\\opencv_calib3d300d.lib" )
#pragma comment(lib, "C:\\myproject\\ms-opencv\\opencv\\vs2015\\WS\\10.0\\x64\\lib\\Debug\\opencv_core300d.lib" )
#pragma comment(lib, "C:\\myproject\\ms-opencv\\opencv\\vs2015\\WS\\10.0\\x64\\lib\\Debug\\opencv_features2d300d.lib" )
// 略

これでdebug版/release版の切り替え、ターゲットCPUごとにリンクライブラリを設定しなおさずに済みます。

最後にOpenCVのDLLをVisualStudioプロジェクトに組み込んで、コンテンツとしてファイルを含めるよう設定します。 (ソリューションエクスプローラにDLLを追加してからそのDLLを右クリック、プロパティ->全般->コンテンツ->YES)

Windowsアプリ認定キットを通す

VisualStudioのプロジェクトを右クリックして、「ストア -> アプリパッケージの作成」でパッケージを作成します。 配布者名・予約済みアプリ名をプロジェクトに関連付けて、アプリパッケージのターゲットアーキテクチャを指定してパッケージ化すれば完了です。 アプリ認定キットで少しトラブったので忘れないようにメモします。

OpenCVのDLLがストアアプリとして利用不可のAPIを呼び出している

Debugビルドではストアアプリの要件を満たせないのが原因です。パッケージに含めたDLLがリリース版ビルドされているか確認しましょう。

OpenCV3.1のデフォルト設定では、生成されたDLLのファイル名でdebug/releaseどちらかを確認できます。 名前付けのルールは"モジュール名"+"バージョン番号(310とか300とか)"+"d"+".dll"になっています。 デバッグ版の場合はdが付加されて、リリース版では付加されません。 つまりバージョン番号と拡張子の間に"d"が付いているDLLを含めたパッケージをアプリ認定キットに通すとこのエラーで引っかかります。

イメージが既定です

ちょっと日本語が不自由なエラーメッセージですが、アプリ提出するならアイコンとかをデフォルト(=既定)のままじゃダメだということです。 内容をよく読むと、アプリのリソース(画像とかアイコンとか)がデフォルト設定のままだからダメだと言っているようです。 試しにアイコンを差し替えるとエラーメッセージが減ったので一通り設定してあげればよさそうです。

感想

色々と詰まりましたが、これでアプリ開発の環境は整いました。 次からは色々と試してみたいと思います。 Windows上でもPowerShellのおかげで手順の自動化ができるので、もっといろいろ試してみたいところです。 では今回はこれくらいで。

ChainerのモデルデータをOpenCVで使うための準備

できれば一通りの目論見が達成してから投稿したかったのですが、まとまった時間をとれそうにないのでいったん投稿です。かたりぃなです。

何をしたいの?

ChainerなどのDeep-Learningフレームワークを使えば色々な学習モデルを構築できそうです。 次のステップとして、その学習したモデルを使ってアプリだったりサービスだったりに組み込むことを考えています。 このとき、サーバがあればPythonをインストールしてChainer走らせて完成になるわけですが、できればクライアント側で処理したいところです。 クライアント環境にPythonを導入してもいい(そういうアプリの配布方法もある)のですが、私自身Pythonはあんまり得意ではないので、妙なところで転びそうです。 できれば慣れたC++でやりたい。C++でchainerの学習済みモデルを取り込みたい

というわけで、今回のゴールは「C++でchainerの学習済みモデルを取り込む」です。 まずは「実現はできそう」というところは見えてきたので、ゴール手前までやります。

どうやるの?

方法ですが、まずOpenCVのdnnで学習済みモデルを取り込めないか考えました。 結論としては「CaffeとTorchはOpencv.dnnの関数を使えば済むけどchainerはインポータを書く必要がある」です。 インポータを書くためにはChainerの出力フォーマットを知る必要があります。chainerは現バージョンでは2種類提供されていて

でした。 numpyを使ってしまうとまたPythonの環境に戻されてしまうので、c++からOpenCVのhdfモジュールを使ってchainerが出力したhdf5を読み込む実験をしてみます。

使うライブラリ一覧

  • OpenCV3.1 dev
    • contrib
      • hdf5モジュール
      • dnnモジュール
  • libhdf

ビルドツール

ライブラリの簡単な説明

OpenCVは有名な画像処理ライブラリです。少し前まではltseezという会社がメンテナンスしていたのですが、Intelに買収されたらしい。 政治的な話はおいといて、構成です。 一般的な機能(枯れた技術)を含むものはopencv本家のリポジトリにあります。 これに対して最新技術であったり、特許なんかの理由で本家に含められないものはopencv-contribリポジトリに含められています。 というわけで、両方それぞれダウンロードしてきます。

libhdfは階層型のデータ構造を扱うためのフォーマットらしく、データサイエンスの分野でよく使われるらしいです。 このあたりにライブラリやツールがあるのでダウンロードしておきます。 The HDF Group - Information, Support, and Software

OpenCVはNuGetでインストールじゃだめなの?

NuGetだとうまくいきませんでした。 NuGetで提供されているものをためそうとしましたが、少し古いVisualStudioでビルドされているようで2015ではそのまま使うことはできなさそうです。(ディレクトリを覗いてみると2012,13用っぽいディレクトリができているのがわかります) このあたりのパッケージ定義をいじってツールセットもそれにあったものを選択してあげればいけそうな気はしますが、そうしてまで使うのは本末転倒(バージョン管理でラクできない)ので手元で全部バージョン管理します。 またNuGetでもOpenCV3.1まではリリースされているのですが、contribがどうなってるのかちょっと不安です。

cmakeの注意点

今回はver3.7を使います。 起動後にopencvやhdfのディレクトリを指定してconfigureしたときにコンパイラのバージョン問い合わせダイアログが表示されます。 ここでターゲット(x86/x64, visualstudioのバージョン)が固定されてしまいます。変更方法はわかりませんでした。 x86/64の切り替えだけなら以前はのcmakeで生成されたソリューションをvisual studioで開いてから切り替えられるようになっていた気がしたのですが。 まあ実験用としてはどちらかに固定しておけば問題ないです。 名前がわかりにくく表示されているので間違えないようにしましょう。visual studio 2015の64bitなら「VisualStudio 14 2015 Win64」です。

hdfライブラリのビルド

hdf5はさきほどのライブラリをビルドします。 ビルドツールはCmakeで。 cmakeの基本的な使い方はほかの親切なサイトに譲るとして、ここではトラブった内容と解決方法を書き留めます。

hdfライブラリのビルド後のイベントが失敗する

setlocalうんたらとかいうスクリプトがビルド後のイベントに設定されているのですが、インストールプロジェクトでインストールを試みるとここで失敗します。 原因としてはデフォルト設定ではインストール先が"c:\programfiles\HDF~"になっているためで、ここにアクセスするには管理者権限が必要です。

解決策をスマートな順に列挙します。

  1. cmakeのinstallprefix変数に適当なディレクトリを設定する
  2. visual studioを管理者モードで起動する

最後のはやっつけ仕事的ですね。 installprefix指定したところにincludeとlib,binと一通り吐き出してくれるので、これが正攻法だと思います。 configureしたらgenerateを忘れずに。

OpenCV

最後にOpenCVのビルドです。

  • opencv-contribを取りんでconfigure
  • hdfのパスを指定する
  • with-msmfをenable
  • with-python(2,3ともに外す)

msmfはMicrosoft Media Foundationの略です。レガシーになりつつあるDirect Showではなく今後はこちらが推奨とのこと。

with-pythonを外した理由は、chainerをインストールしたときにhdfライブラリがくっついていたらしく、有効にしたままだとこっちを参照してしまいました。 依存ライブラリのパスを変えてあげればいいのかもしれませんが、依存関係で面倒ごとにならないように最初から切っておきます。

OpenCVPythonから叩くことは当面は無いので。2.7系3.5系それぞれ外すのを忘れずに。

OpenCV-contrib

windows環境だとopencvのビルドで多数の警告が出ることがあります。 警告はW4819(文字コードunicode形式で保存してください)ですが、とりあえずvisual studioの警告抑制で4819を指定しておきます。

opencv,hdfを使うプロジェクトを作る

visual studioを起動して適当なプロジェクトを作ります。 win32コンソールでいいんじゃないでしょうか。

ライブラリの参照パスの設定

参照プロジェクトとしてopencvを指定すればいいんですが、もういい加減面倒になってきました。dllをコピーして済ませます。 ライブラリディレクトリとインクルードディレクトリの指定をして、インポートライブラリもすぐ使うものだけ指定します。 libhdfとopencv_coreがあればいいかと。

hdfを読む実験(途中)

libhdfは私が欲しい機能と違う気がしてきたので、コードの途中までです。(投げやり) ちなみにこのコードのままではcv::Matの型やサイズを指定していないので実行時エラーになります。

 const std::string  hdf5_file_path("testmodel.hdf");
    auto hdf5_inst = cv::hdf::open(hdf5_file_path);

    const std::string layer_name = "predictor/conv1";
    if (hdf5_inst->hlexists(layer_name) ) {
        auto mat = cv::Mat();
        hdf5_inst->dsread(mat, "predictor/conv1");
    }

何が違う?

このhdfライブラリはhlexistsで目的のノードが存在していることを確認してからdsreadすれば、目的のデータは読めそうです。 しかしこういったインターフェースは私が欲しいものではありません。

私が欲しい機能は 「データ構造が変わってもプログラムを変更せずに動作確認したい」 です。

対するhdfの機能は「データの階層構造は固定されたものである」という大前提があります。 これは「chainerで層を増やして学習したものをOpenCVで試したい」といったときにプログラムを書き換える手間が発生します。

hdfそのままだと扱いづらい。どうしよう?

hdfの構造なんてそういうものと言われればそれまでかもしれませんが、ちょっと納得いきません。 私が欲しいのはboost::ptreeみたいなパーサです。 libhdfのツールにdumphdfというのがあってxml出力できるようなので試してみましたが、xmlにするとファイルサイズが大変なことになってしまったのでちょっと工夫が必要そうです。 (以前作った3層のCNNで10MByte超)

感想と今後の展望

xmlとhdfのハイブリッド構成ができればいいのかもしれません。 xml側にネットの構造だけ記述されていて、実際のwとbはhdfから読んでくるとか。 色々工夫する余地はありそうです。 では今回はこれくらいで。

Chainerでcifar-10画像分類を試してみる

やっとchainerでCNNを動作させるところまで辿り着きました。かたりぃなです。

何がやりたいのか?

ARというかHololensのMRで現実世界のオブジェクトを識別して追加の情報をユーザーに提示できれば楽しいだろうなと思っています。 HololensのAPIを軽く見たところ、深度マップなどの面倒は見てくれるようですが、画像からの物体検出などは見当たりませんでした。 画像処理といえばOpenCVなんかが有名で、最近はdnnモジュールが追加されているのでこいつを使って公開されている学習済みモデルを取り込めば一般物体認識はできそうですが、学習済みモデルを持ってくるだけでは私のやりたいことに届かなさそうです。 手作業で前処理や特徴抽出を記述していくのもアリかもしれませんが、そういう試行錯誤に時間を費やすのは勿体ないと思います。 なら自前でモデルを作ってDeepLearningのフレームワークを使って学習させよう、そしてAR/MRでなんか面白いことやりたいなといったところです。 過去にChainer動かしてみたりもしてますし、ゲーム目的とはいえGPUもよさげなのが手に入りました。 さっそくやってみます。 今回のゴールはcifarデータセットをCNNで処理して一般物体認識を行うことです。

cifarデータセットとは

10種類のラベル付けされた画像のデータセットです。一般物体認識における分類問題と呼ばれるものです。 画像の情報として

  • RGBの3チャンネル
  • 32x32 pixel です。 これができれば私がやりたいことの実現に向けた足掛かりになりそうです。

データセットの中身を確認する

cifarデータセットpython-typeでダウンロードしてそこからデータを取り出します。 pythonはあまり慣れていないので調べることが多いです。 ネットの情報ではcPickleを使おうという情報が散見されますが、どうやらインストールしたpythonのバージョンにはcPickleが無いらしく、仕方ないのでpickleを使います。 シリアライズとデシリアライズを面倒見てくれるモジュールらしいです。 手元の環境ではエンコードタイプの問題でややこしいことになってしまいました。

# python3ではcPickleがなくなったらしいので、pickleを使う
import pickle;

# metadataがラベルデータ
# それ以外がデータセット
# データは32x32,3ch(RGB)の形式

# ただのバイト列としてエンコーディングされたものとして扱う
f = open('data_batch_1', 'rb')
train_data = pickle.load(f, encoding="bytes")

len(train_data)
# result : 4
train_data.keys()
# result : dict_keys([b'filenames', b'data', b'labels', b'batch_label'])
type(train_data[b'data'])
# result : <class 'numpy.ndarray'>
train_data[b'data'].shape
# result : (10000, 3072)
type(train_data[b'labels'])
# result : <class 'list'>
train_data[b'labels'][:10]
# result : [6, 9, 9, 4, 1, 1, 2, 7, 8, 3]
sample_image = train_data[b'data'][:100].reshape((10, 10, 3, 32, 32)).transpose((0, 3, 1, 4, 2)).reshape((320, 320, 3)) # 先頭100個をタイル状に並べ替える
Image.fromarray(sample_image).save('sample.png')
# result : outputfile "sample.png"

これでデータ構造はわかりました。何か目に見えるものがあると達成感ありますね。

CNNとは

畳み込み(2次元カーネル)を使ったニューラルネットワークです。 今回は動かすことが目的なので、細かいパラメータ調整はしていません。

こちらのページを参考にさせていただきました。

Chainerのtrainerを使ってCIFAR-10の分類に挑戦したかった - Qiita

詰まったポイント

ネットで調べてみましたが、少し前のバージョンのchainerのコードが多く見受けられました。 functionsetとforwardで順方向の計算グラフを定義されていたりしましたが、functionsetは非推奨となったようで、NN全体をchainとして定義するのが今の主流のようです。 手作業で書こうとしましたが、ゼロから書くのはまだ無理でした。 上記ページのコードを引用して動作確認しました。 pickleのところとか、学習結果の出力を少し試してみた程度の変更です。

全体のコード。

giste82cb557c1939de2f1bb45089637ea59

cpuのみで動かすと重くて全然進みませんが、GPUなら数十分で終わりました。これなら色々と試行錯誤できそうです。

今後の展望

以前に試したARの原理の延長で、物体ごとに異なるオブジェクトを表示してみたいですね。

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

ただ、カードゲームなんかで全ての種類のカードを分類するのはちょっときつそうなので、適当な分類ラベルを選択して限定的に実装してみたいと思います。 またDeepLearningを使えば検出精度の向上も期待できるかもしれません。 DeepLearningで何ができるか、色々と試行錯誤してみましょう。 週末のまとまった時間でキリのいいところまでできたのでひと段落です。

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

UWPで音声認識APIを試してみる

画像の分類問題に少し飽きてきました。かたりぃなです。
機械学習を使った画像の分類方法を色々と試していますが、どれも私のやりたいことと少しずれている感じがしていて少し煮詰まった感があります。

音声認識に挑戦(ライブラリを叩くだけ)

画像や映像よりも先行しているイメージのある音声の識別ってどういう理論なのだろうと思いつつPRMLの後ろのほうの章をじっくり読んでいますが、まだまだ修行が足りないらしくよくわかりません。
時系列のデータ扱うのって色々面倒だなーと思い始めています。
そういえばWindowsの左下で「何でも聞いてください」と言ってる人がいますね。マイクアイコン付いてますし、試してみましょう。
Hololensでこれが使えれば遊びの幅が広がりそうなので購入するための言い訳に使えます。

Microsoft音声認識APIを試す

まずは自分のプログラムで試す前にCortanaさんに話しかけてみます。
思った以上にお利口さんで、動画サイト見ながらマイクに音声を入れてもしっかり識別してくれます。
ユーザー見えのふるまいとしてCortanaさんがやってくれることは次のようなことでした。

  • 音声を認識して文字列にする
  • 文字列をもとにアプリやブラウザを起動する

試した単語は次の4つです。最後のは意地悪テストの域に入るので、まあ充分実用的な精度で識別できています。
発音の識別だけでなく形態素解析までやってくれてるんでしょうか。なんかすごい。

  • 「ぺいんと」

  候補「ペイント」のアプリを表示後にペイントを起動

  • 「はいぱーぶいまねーじゃー」

  候補「Hyper v マネージャ」を表示後にブラウザで検索結果を表示

  • 「とらんぷし」

  候補「トランプ氏」を表示後にブラウザで「トランプ氏」の検索結果を表示

  • 「じぇんきんすし」

  候補「jenkinsし」を表示後にアプリ候補「天気」を表示

HyperVマネージャを起動してくれなかったのはちょっと残念です。Cortanaさんのすぐ隣にいるのに。
ジェンキン寿司は古いネタなのかCortanaさんには通じませんでした。

公式ドキュメントはこちら
https://msdn.microsoft.com/en-us/library/windows/apps/windows.media.speechrecognition.aspx

簡単に試せそうなものを先人が示してくれているのでコピペして動かします。
UWPで音声認識 - かずきのBlog@hatena

まとめ

10行未満のコードで簡単に音声認識ができました。
これなら気軽にアプリに組み込むことができそうです。
Hololensのような端末で何か作ることを考えたときに、こういったユーザーインターフェイスは従来のそれと比較して重要度が相対的に高くなると思います。
どこまで細かいことができるのかはまた別途試してみたいところです。
では今回はこれくらいで。