catalinaの備忘録

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

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

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

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

目的

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

DirectXPixelFormat::R16G16B16A16IntNormalized

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    m_meshRenderer->HideInactiveMeshes(surfaceCollection);
}

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

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

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

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

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

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

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

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

    return processMeshTask;
}

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

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

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

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

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

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

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

opencv-contribのedge-boxesを試す

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

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

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

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

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

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

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

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

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

ObjectProposalとは

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

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

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

edge-boxesとは

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

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

動かしてみる

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

ビルドする

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

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

サンプルプログラムは

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

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

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

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

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

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

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

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

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

いざ、実行

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

edgeboxes_demo model.yml.gz test.jpg

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

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

結果

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

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

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

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

感想と今後の展望

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

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

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

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

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

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

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

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

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

などなど。

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

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

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

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

CMakeを使ってmakefileを作る

はじめに

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

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

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

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

総括

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

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

CMakeの前にautotoolsについて

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

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

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

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

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

makefileをラクして書くとは?

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

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

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

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

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

CMakeの基本

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

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

各パラメータの意味は

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

まずはmakeしてみる

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

int main()
{
  return 0;
}

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

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

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

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

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

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

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

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

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

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

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

いけました。

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

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

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

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

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

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

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

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

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

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

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

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

makeしてみます。

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

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

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

よさそうです。

トラブルシューティング

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

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

感想と今後の展望

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

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

背景

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

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

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

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

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

SSDの何を知りたいのか

SSDの論文はこちら

https://arxiv.org/abs/1512.02325

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

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

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

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

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

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

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

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

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

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

import cv2
from chainercv.datasets import voc_detection_label_names

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

コードの説明

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

入力画像からの特徴抽出

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

ネットワークの後半部分

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

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

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

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

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

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

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

ImageSets\Main\train.txt

訓練データの概要

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

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

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

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

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

というわけですね。

おまけ

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

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

感想と今後の展望

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

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

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

MTGwikiのAPIを使ってみるテスト

本業が忙しいため趣味プログラミングが滞っていました。かたりぃなです。

自分が書いた実験コードを久しぶりに読むと辛いものがあるので、リハビリを兼ねて簡単なことからやってみます。

今回のお題はカードゲームMTGをもっと楽しく遊べないかというお題でいきます。

タイトルはMTGwikiにしてますが、ほかのREST-APIも試してみます。

その前に、ここではRESt-APIで該当サーバから情報を取得する例を示しますが、もし利用する場合は自己責任でお願いします。

当然のことですがスクレイピングなどの行為は相手サイトに負荷をかけることになるので、その影響がどうなるのかをしっかり考えましょう。

エンジニアとしての良心というかマナーを守りましょうってやつですね。

さて、MTGを楽しく遊びたい。

具体的にはMTGWikiを見ながらデッキを組んでいると困ることがあります。 今回はこれをソフトウェアで解決できないかという検討です。

まずカードゲームではデッキを作るときに色々と戦略を練るわけですが、「どのようなカードを入れると良いか」がアイデアとしてあっても、それを探すにはDBにクエリを投げるような操作が必要です。

上級者が強いと言われるのは、頭の中にあるDBのレコード数が非常に大きく、レスポンスも良いためと考えています。

初心者はその逆ですね。

さて、私自身それほど強いわけでないので、頭の中のDBはそんなに大きくありません。

なのでMTGwikiを見ながらになるのですが、このとき具体的に困る例として次のようなケースがあります。

  • どういう能力をもったカードが欲しいのかは解る。でも正式カード名忘れた
  • 使いたいカードのイラストはなんとなく覚えてる。でもカード名も能力も忘れた

MTGwikiをゆっくり眺めてればそのうち答えにたどり着くこともありますが、手間がかかりすぎます。

他の解決策として、だいたいのデッキレシピが固まっているのであればショップに行って店員さんにクエリ投げれば解決できそうです。

しかし試行錯誤している中で購入すると、後で「やっぱり違った」みたいになることもあって悲しいです。

というわけで、「少しでもデッキを組みやすくするアプリを作る」のを目標に色々と試します。

ぶっちゃけMTGwikiのカード個別評価ページにカード画像貼りつけできれば解決しそうな気がしますが、画像引用とかしてしまうと権利的にどうなのとか色々あるのでしょう。多分。

今回はプログラムは書きません。APIの仕様をざっと調べるだけに留めます。

情報源として使えそうなAPIとかサイト

公式サイトの検索ページ

http://gatherer.wizards.com/Pages/Default.aspx

API 日本語検索

MTGwiki

http://www.mtgwiki.com/wiki/

wisdom-guild

http://www.wisdom-guild.net/

API 英語検索(他言語でも検索可能)

Magic: The Gathering Developers

https://docs.magicthegathering.io/

とりあえずはこれらを使ってアプリが作れないか考えてみます。

MTG-wikiはルールや評価が細かく載っているので、デッキを組むには最高の情報源です。

ただ、このサイトには不足している情報があります。

他のサイトも同様で、相互に補完関係です。各サイトに掲載されている情報の有無を整理します。

サイト名称 カード情報の詳細 カード評価 ショップ価格 イラスト レガシーカード
MTGwiki o o x x o
wisdom-guild o x o x o
公式 o x x o x

この表からいえることは、欲しい情報を統一的に扱うにはすべてのサイトから横断的に情報を集めてくる必要がある。ということですね。

とりあえずAPIでアクセスしてみる

上記の各サイトのAPI仕様を調べてみます。

公式

公式のサイトで適当なカード名で検索かけるとこんなのになります。

たとえば「霊体の先達」で検索するとURLはこうなります。

http://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=414057

少々使いにくいですね。パラメータのidが何を表しているものなのかさっぱりわかりません。

そのうえ古いカードを検索しても出てきません。(後述のAPIから検索かけるとひっかかるのですが。。。)

色々と不便なので諦めます。

ただし、このサイトから得られる情報には重要な情報が含まれています。

他のサイトでも同様に取れるのですが、せっかくなのでここで述べておきます。

MTGのオラクル

上記URLを開くと、カードのテキストとは別に「オラクル」というのがあります。 書いてある内容は(基本的に)カードのテキストと同じなのですが、食い違いがあることがあります。

食い違いが発生する原因は、ルールの変更です。

カードゲームなので、あまりに「強すぎる」とか「製作者がそのヤバさに気づかなかった」といった場合にゲームバランスを崩壊させてしまうため、その対応として変更が加えられることがあります。

単純に強すぎるとかの場合は「使用禁止」と御触れが出ることがありますが、状況によってはこのオラクルが変更されることもあります。

この霊体の先達の例では10年くらい前まで「戦場に出たとき」という文言から「手札から戦場に出たとき」という文言に変更されていました。

詳細はこちら

http://mtgwiki.com/wiki/%E9%9C%8A%E4%BD%93%E3%81%AE%E5%85%88%E9%81%94/Karmic_Guide

私の大好きなコンボです。

MTG-wiki

日本語の情報源としては最高です。 このwiki自体はmedia-wiki(wikipediaと同じ)なので、クエリを投げてあげれば結果が返ってきます。

APIのエンドポイントはここ

http://www.mtgwiki.com/api.php?

ページタイトルを指定して情報を取得するにはこんな感じです。

http://www.mtgwiki.com/api.php?format=xml&action=query&prop=revisions&rvprop=content&pllimit=500&&titles=<ページタイトル>

というわけでカード名をtitleパラメータに指定すれば直接そのカードの詳細情報が記載されたページに飛べるのですが、少々使いにくいポイントがあるようです。

たとえば「世界を目覚めさせる者、ニッサ」のページのURLはこうなっています。

http://mtgwiki.com/wiki/%E4%B8%96%E7%95%8C%E3%82%92%E7%9B%AE%E8%A6%9A%E3%82%81%E3%81%95%E3%81%9B%E3%82%8B%E8%80%85%E3%80%81%E3%83%8B%E3%83%83%E3%82%B5/Nissa,_Worldwaker

“~/wiki/"以降がページタイトルに相当するのですが、エンコードを解くと「世界を目覚めさせる者、ニッサ/Nissa, Worldwaker」という文字列が得られます。これがページタイトルです。 つまりどういうことかというと

title=<日本語カード名>/<英語カード名>のように指定します。

日本語版が発売されていないカードの場合、title=<英語カード名>で指定します。

それぞれのケースで例示します。

「世界を目覚めさせる者、ニッサ/Nissa, Worldwaker」のページの情報を取得する

http://www.mtgwiki.com/api.php?format=xml&action=query&prop=revisions&rvprop=content&pllimit=500&&titles=%E4%B8%96%E7%95%8C%E3%82%92%E7%9B%AE%E8%A6%9A%E3%82%81%E3%81%95%E3%81%9B%E3%82%8B%E8%80%85%E3%80%81%E3%83%8B%E3%83%83%E3%82%B5/Nissa,_Worldwaker

「Black_Lotus」のページの情報を取得する

http://www.mtgwiki.com/api.php?format=xml&action=query&prop=revisions&rvprop=content&pllimit=500&&titles=Black_Lotus

このことから、「英語名称」と「日本語名称」の両方をうまく与えることができればページを開けそうです。

wisdom-guild

ここではお値段と、使用可否を取得したいです。ただ、私自身あまり価格を気にしない(高いカードは買わない)主義なので、簡単に調べ方だけメモします。

それになんか注意事項書いてありますし。「あんまり負荷かけるような使い方しないでね」と。

スクレイピングするつもりはないので、どんなインタフェースになっているかだけ軽く調べるだけに留めます。

とりあえず以下のページを開いて検索条件設定すればクエリ文字列が設定されます。

http://whisper.wisdom-guild.net/

「世界を目覚めさせる者、ニッサ」で検索したときのURL

http://whisper.wisdom-guild.net/search.php?name=%E4%B8%96%E7%95%8C%E3%82%92%E7%9B%AE%E8%A6%9A%E3%82%81%E3%81%95%E3%81%9B%E3%82%8B%E8%80%85%E3%80%81%E3%83%8B%E3%83%83%E3%82%B5&name_ope=and&mcost=&mcost_op=able&mcost_x=may&ccost_more=0&ccost_less=&msw_gt=0&msw_lt=&msu_gt=0&msu_lt=&msb_gt=0&msb_lt=&ms_ope=and&msr_gt=0&msr_lt=&msg_gt=0&msg_lt=&msc_gt=0&msc_lt=&msp_gt=0&msp_lt=&msh_gt=0&msh_lt=&color_multi=able&color_ope=and&rarity_ope=or&text=&text_ope=and&oracle=&oracle_ope=and&p_more=&p_less=&t_more=&t_less=&l_more=&l_less=&display=cardname&supertype_ope=or&cardtype_ope=or&subtype_ope=or&format=all&exclude=no&set_ope=or&illus_ope=or&illus_ope=or&flavor=&flavor_ope=and&sort=name_en&sort_op=&output=

大量のパラメータが見えますが、見た感じGUIでの設定項目とほぼ一致してそうです。 略称から推測するに、設定項目の上から順にパラメータに入っているのでしょう。

というか価格情報だけを取得したいならMTG-wikiのほうからリンク張ってくれてるので、そこから辿ったほうがラクそうです。

Magic: The Gathering Developers

単純なAPIなので使いやすいです。 https://docs.magicthegathering.io/

とりあえずAPIのエンドポイントは https://api.magicthegathering.io/v1/cards

カード名検索は少々工夫が必要ですが、 「世界を目覚めさせる者、ニッサ」で検索する場合 https://api.magicthegathering.io/v1/cards?name=%E4%B8%96%E7%95%8C%E3%82%92%E7%9B%AE%E8%A6%9A%E3%82%81%E3%81%95%E3%81%9B%E3%82%8B%E8%80%85%E3%80%81%E3%83%8B%E3%83%83%E3%82%B5&language=japanese

で取れます。 最後のlanguage=japaneseで「検索名称が日本語」であることを指定します。

取得できるJSONの仕様の細かい部分はマニュアル読むとして、重要ポイントだけ。

  • cardsリストには同一カードでもエキスパンションごとに格納される。
  • cards[n].nameがカード名称(英語版)
  • cards[n].textがオラクル
  • cards[n].foreignNamesが各国語訳されたカード名称
  • foreignNamesはlanguage要素で言語種別、name要素でその国の言語表現でのカード名が入る
  • cards[n].imageUrlにカード画像URL(英語)が入る
  • cards[n].foreignNames.imageUrlに各国語版の画像URLが入る

英語版の「世界を目覚めさせる者、ニッサ」の画像URLは http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=383328&type=card

日本語版「世界を目覚めさせる者、ニッサ」の画像URlは http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=385032&type=card

ですね。

画像は存在しない場合もあって、たとえば特殊カードの画像はここからは取れないみたいです。 上記例ではcards[0]にはimageUrl要素がありません。 set=pMEIになっているので、プロモーションカードか何かでしょう。

あと古いカードも検索できるみたいです。全部を試したわけではないので断言できませんが。

たとえば「Black Lotus」で検索

https://api.magicthegathering.io/v1/cards?name=Black Lotus”

この中のimageUrl要素から画像ページに飛ぶと。。。

http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=298&type=card

いけました。

まとめ

MTGwikiの情報+上記APIを使うことで、デッキ構築に必要な情報がすべて取れそうです。

具体的にどういうアプリ作るとか考えると楽しそうですね。

ただ、wikiを見るだけならビューワ使えばいいわけで、わざわざアプリ作る意味がないです。

もうちょっと目的に特化させてデッキ構築専用アプリ(多言語対応)とかしたほうが実用的な気がします。

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

WindowsのChainer環境を2.xにアップデートしたときのメモ

何週間か前に、Chainerをアップデートしようとしたときのメモを整理したので公開します。かたりぃなです。

Windows上のPython環境が壊れていたので修復していたら一日潰れました。

環境はいつものです。

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

結論

手元の環境でおかしかったのは次のとおりでした。

  • 自前でインストールしたAnacondaとVisualStudio2017 CommunityのPythonToolsのパスと重複してた
  • cuDNNが入っていない

原因1, 環境変数の競合(pythontools for vs と自前でインストールしたanaconda)

単純に環境の競合でした。

まず、自前インストールしたAnacondaは、たぶんこいつですね。 自前でやったって書いてますね。。。

windows10でchainer+CUDA+cuDNNをGPUで動かしてみた - catalinaの備忘録

次に、PythonTools for visual studioです。 これはVisualStudioでPython開発できるツール一式です。 インストール時にごちゃごちゃ聞かれますが、これを入れるとAnacondaが入りました。

この時点で何か環境壊してないか気づくべきでしたね。。。

何が競合するの?

Windows10には「システム環境変数」と「ユーザー環境変数」があります。 この両方に"pythonのパス"が入っているとpython環境がおかしくなります。

手元の環境では次の3つが重複していました。

システム環境変数

  • C:\Program Files\Anaconda3
  • C:\Program Files\Anaconda3\Scripts
  • C:\Program Files\Anaconda3\Library\bin

ユーザー環境変数

  • C:\Users\ユーザー名\Anaconda3
  • C:\Users\ユーザー名\Anaconda3\Scripts
  • C:\Users\ユーザー名\Anaconda3\Library\bin

chainer1.x時代に色々試していたのはユーザー環境変数側のpythonです。Anacondaをデフォルト設定でインストールすればこちらを向きます。

システム環境変数側はVisualStudio2017のpythontoolsが作ったやつだと思います。 visualstudioのpythontoolsのアンインストールでいなくなってくれましたし。

というわけで、この競合が起きた状態だと「耳から悪魔」みたいなよくわからないことになってしまいます。

ざっとこんな現象が起きてました。

  • chainercvのmultiprocessで死ぬことがある
  • pip失敗 - UTF8エンコードエラー
  • pip失敗 - パーミッションエラー
  • pip失敗 - でもパッケージのファイルは存在している

というわけで、こういうふうに動きが妖しいときはpythonの設定を見直しましょう。

原因2, CuDNNがインストールされていない

上記のPython環境の競合と相まって、正しくインストールできてませんでした。

これに気づかずにchainerの適当なexampleを–gpuで動かすとこうなりました。

RuntimeError: CUDA environment is not correctly set up

pipからcupyインストールに成功するときは、ダウンロードが終わってから少々時間かかるので、「ファイルダウンロードしただけ」みたいなインストールの終わり方のときは疑ったほうが良いかもしれません。

ダウンロード後に時間かかるのは、裏でCUDAカーネルをビルドしてるっぽい?

cuDNNをインストールしてからcupyをインストールすれば解決です。 もちろんCUDA本体はインストールしたうえでcupyをインストールする必要があります。

https://developer.nvidia.com/cudnn からダウンロードしてcudaのディレクトリに置いてから

pip install cupy

です。

ついでに

chainercvの最新版では、ssdあたりにも学習サンプルがついてます。 動かすならcv2が必要なので、入れておくとよいです。 (opencv3なのに名前空間がcv2になってるのは気にしたら負けかなと思っている。) pipだとパッケージ見つけられなかったのでcondaでいきました。

conda install -c http://conda.binstar.org/menpo opencv3

chainercvの動作確認

以前試した時(http://catalina1344.hatenablog.jp/entry/2017/06/03/000314) はそれっぽく検出できたのですが、最新版(0.6.0)ではなぜか検出できなくなりました。 テスト用のデータが違うからうまくいかないのかも。

今回は「インストールが成功して動いていること」を確認したいだけなので、検出したオブジェクトのスコアが低いものも出すようにすればいいです。

やっつけですが、こんな感じで。

rcnn_model = SSD300(
            n_fg_class=len(voc_detection_label_names),
            pretrained_model='voc0712')
rcnn_model.use_preset('evaluate')

これでスコアが低いオブジェクトも全部列挙されます。出力画像ファイルがごちゃごちゃした感じになりますが、無事に最新版に移行できました。

感想

今回はくだらないところで躓いてしまいました。

ニュースでchainer開発元のPFNがMSと協業みたいなのを見たので、そのうち安定して環境構築できるパッケージとか提供されたりするのかなと期待していたりします。

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

外出先からDNNの学習状況を確認したい

今回はchainerでの学習進捗を見るのが目的ですが、あんまりchainer関係ないです。webフロントまわりですね。

やりたいこと・その経緯

DNNを試しておいて、自動で学習してる間にちょっと出かけたりとかしたいです。

そうすると出先で学習の経過が見えたほうが便利ですよね。

帰宅中の電車の中で結果を見ることができれば、次どうするかの策を移動中に考えることができます。

リモートデスクトップでいいじゃない

外出先でもVPN使えばPCにログインできるので、リモートデスクトップでなんとかなったりします。

ただWindowsリモートデスクトップって今回やりたいことに対して少々オーバースペックです。ちょっとjpegファイルみたりログ読みたいだけなのに。

そのうえ外出先だとスマホからのアクセスがメインになるので、リモートデスクトップが操作しづらいです。

というわけで、外出先から自宅のPCにアクセスしてchainerの学習状況を確認するための「使いやすい」構成を考えてみました。

  • Chainerが走ってるPCでWebサーバを走らせる
  • VPNはこれまで通り使う
  • WebサーバからChainerの状況を読み出す
  • スマホのブラウザから進捗が確認できる

こんなところでしょうか。 実験用のWebサーバをそのまま外部に公開するのはセキュリティ的に少々怖いので、VPNは維持しておきます。

使用するフレームワークの選定

DNNのフレームワークはchainerを使います。

Webサーバとしては候補は色々ありますが、ChainerがPythonなのでPythonで統一したいところです。

とりあえずWebから見たい情報は

  • 学習の進捗
  • グラフの形状
  • 各層の状況

などでしょうか。

グラフの形状を見れるなら、そこから画面遷移したいとか要望が増えることも考えられます。

画面遷移とか考えるとbottleでは少々力不足です。 せっかくなので、勉強も兼ねてDjangoを使ってみることにします。

今回使うDjangoは公式のチュートリアルをやっておけば最低限の扱いはできそうな感じでした。 https://docs.djangoproject.com/ja/1.11/intro/tutorial01/

ではやってみます。

環境

環境はいつものです。

Djangoを使う準備

Djangoとはpythonでwebアプリケーションを作るためのフレームワークです。 軽く触ってみた感触ではよくあるフレームワークと同じなのかなと思っています。

まずはインストールしてコマンド類を確認します。

pip install django

Webサーバを立てる

プロジェクトを作ります。 このときプロジェクトと同名のWebアプリケーションも自動で生成されます。

django-admin.exe startproject dnn_view
cd dnn_view

しばらくはこのディレクトリがカレントであったほうが作業しやすいです。 ほとんどのコマンドはこのディレクトリに生成されたmanage.pyで使うので。

どんなコマンドがあるのかは

python manage.py --help

で確認できます。

とりあえずサーバを立ち上げてみます。

python ./manage.py runserver

これでhttp://127.0.0.1:8000 にアクセスすればDjangoのページが開きます。

同様にして http://127.0.0.1:8000/admin にアクセスするとWebサイト管理者用の画面が開きますが、まだユーザー作ってないので入れません。

管理ユーザーを作る

今回使用するDjangoのバージョンでは、DBのマイグレーションしてからユーザーを作る手順になります。 DBはデフォルトでSQLite使ってくれるみたいなので、当面は気にしなくていいでしょう。

python ./manage.py migration
python ./manage.py createsuperuser

ユーザー名、メールアドレス、パスワード、再確認パスワードを入力して完成。 先ほどの管理者用のページでログインできるようになります。

とはいえ、今回は管理者ページを使ったことは何もしません。

Webページを作る

初めてのDjangoなので、簡単なページを作ることにします。 幸いにもChainerは進捗状況をpngに吐いてくれる機能があるので、このpng画像を張り付けただけのページWebサーバで見せることにします。

次のものを作成・変更します。

  • URLとビューを結びつける urls.py。djangoが作ったものに変更を加えます
  • ビューを表現するviews.py。これもdjnagoが作ってくれてます
  • ビューの具体的な表示内容を示すhtml。自前で書きます。

URLとメソッドを結びつける

ページのパス名とpythonメソッドを結びつける必要があります。 とりあえず適当なviewに結び付けます。

from django.conf.urls import url
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from . import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^dnn_view/', views.net),
]

これでhttp://127.0.0.1:8000/dnn/ へブラウザでアクセスするとviews.pyに定義されたnetという関数が呼び出されます。

ビューをつくる

ビューをつくります。 アプリケーションのディレクトリ(urls.pyとかと同じ階層)にtemplateディレクトリを作ってて、その中にdnn_view/index.htmlを置いたので、このファイルを読み出して使うという単純なものです。

from django.template import loader
from django.http import HttpResponse

def net(request):
    t = loader.get_template('dnn_view/index.html')
    return HttpResponse(t.render())

テンプレートをつくる

お行儀よくテンプレートで定義することにします。パスがちょっとアレなのでお行儀良くないかもしれませんが。

これはUWPでいうXAMLに相当するものかなと思っています。

Microsoft-Edgeさんで実験していると、DocTypeなしのHTMLだと警告がうるさいので、つけておきます。

{% load static %}
<!DOCTYPE html>
<img src="{% static "loss.png" %}" alt="My image"/>

どうしてEdge?

私のスマホはWindowsPhoneです。なのでEdge。

あとはPC版EdgeだとUAのエミュレーション試験機能もついてるので。こいつですね。 f:id:Catalina1344:20170805012100p:plain

モバイル用UAに設定してから適当なサイト見るとモバイル用のボタンとか増えたりして楽しかったりします。

テンプレートと画像のパスを登録する

Djangoが静的ファイル(画像とかcssとか)を探すときに使うパスを登録しておきます。 本番だとこういうやりかたはオススメできないみたいな話もありますが、所詮は実験用なので良しとします。

どうやらDjangoにはデプロイ用に静的ファイルまとめてくれる機能があるらしいです。今回は目的からそれるので試しません。

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
    'C:/myproject/deep_learning/sdks/result', # chainerの学習結果が吐かれるパス
]

できた!

これでブラウザから http://127.0.0.1:8000/dnn_view/ にアクセスするとこんなの出ました。よさげですね。

f:id:Catalina1344:20170805012147p:plain

感想と今後

これで学習の経過は見れるようになりました。 出かける前に仕掛けておいて、昼休みとかに進捗どうですかと確認したりとかできますね。 あとはスナップショットとグラフ構造も見せるようにしてUIつければ、色々と楽しいかもしれません。

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