catalinaの備忘録

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

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つければ、色々と楽しいかもしれません。

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

カードゲームARの実現可能性検証

なんとなくですが実現可能性は高い気がしてきました。かたりぃなです。 本題に入る前に問題領域を今一度整理します。

やりたいこと

MTG遊戯王みたいなカードゲームの遊びの付加価値として、AR化を考えています。 やりたいことはARと一言で済むのですが、もう少し問題領域を整理します。

  • 現実空間のカードを自動的に検出できる
  • 検出したカードが何なのかを言い当てられる
  • カード上に任意の3Dモデルをレンダリングできる

こんなところです。

このうち「現実空間のカードの検出」が今回のエントリでのお題です。

カードが何なのかを言い当てるのは、以前のエントリで実現可能性は見えてきているので今回は触れません。

3Dモデルのレンダリングは、あとでやります。モーションやエフェクトも考えるとUnity使ったほうがラクかなぁと思っています。

肝心の3Dモデルデータはありません。mod方式にしておいてユーザーが差し替えられる仕組みがあれば楽しめそうです。最初はコナンの犯人シルエットみたいなのでも出しておくつもりです。もしモデル作ってる人がいたら、そのとき考えることにします。

概念図

Hololensだけではマシンパワー不足なので、PCのパワーを借りることにします。 実際に実現できて採算がとれるならクラウドでやればいいですし。

気分転換に概念図書いてみました。赤文字が今回やるところです。 f:id:Catalina1344:20170720223327p:plain

既存のライブラリで不足していること

Hololens単体でも、Vuforia使っても、私のやりたいことには単体では届きません。

まずそれぞれでできることは

  • Hololensは空間認識ができる
  • Hololensは自分の位置を知っている
  • Vuforiaは検出対象物が既知でなければならない

です。 Vuforiaを試そうかと思いましたが、全部のカードを登録していくのは少々面倒です。ほかのARライブラリについても同様です。

じゃあHololensなら解決できるの?と問われると、そんなことは全然なくて、あくまで空間と自身の位置を認識することで、その空間に任意の3Dレンダリングを行えるだけです。

というわけで、検出対象物の検出をどう工夫するかといったところが今回のお題です。

R-CNN

領域抽出をいろいろと調べて回りました。

ざっと年代順にR-CNNの技術要素を並べると

  • R-CNN
  • fast R-CNN
  • faster R-CNN
  • YOLO
  • SSD

こんな感じのようです。 古いものは領域抽出自体はselective searchで、その領域の分類をCNNでやっているだけの雰囲気でした。(ざっと読んだだけなので違うかもしれません)

SSDは「物体の領域抽出と」「物体のカテゴリ分類」を一度のCNN計算で済ませることからついた名前らしいです。(Single Shot Detect)

実際にくっつけてみる

R-CNN自体は以前のエントリでchainercvを使って試したので、今回も同様にしてみます。

chainercvの実装が変わったのか、うまく検出できなくなっているのでスコアが低いオブジェクトも列挙できるようにuse_presetで閾値をさげておきます。

このモデルは自分の目的にあわせて学習させていないので、あとで少しずつ方法論を考えます。

rcnn_model.predict()で領域抽出してもらいます。 一度に複数枚の画像を入力できるらしく、引数はリストです。 戻り値も当然リストなので0番目の要素をとっておきます。 bboxが傾いていないバウンディングボックスで、labelがその領域のカテゴリです。

    rcnn_model = SSD300(
            n_fg_class=len(voc_detection_label_names),
            pretrained_model='voc0712')
    chainer.cuda.get_device(0).use()
    rcnn_model.to_gpu()
    rcnn_model.use_preset('evaluate')

    # R-CNNへ入力してobject-proposalをする
    bboxes, labels, scores = rcnn_model.predict([pixels])
    bbox, label, score = bboxes[0], labels[0], scores[0]

バウンディングボックスをもとにcropする

バウンディングボックス内にカード画像が含まれているとするなら、そのバウンディングボックスのROIに対してカード検出をかければいいと考えます。 というわけで、バウンディングボックスのROIを作ります。ここからopencvの世界です。

    for i, b in enumerate(bbox):
        top, left, bottom, right = b
        t = 0 if top < 0 else top
        l = 0 if left < 0 else left
        croped_img = base_img[int(t):int(bottom), int(l):int(right)]
        croped_img_clone = np.array(croped_img)

カードを検出する

以前c++でやったことをpythonで実装します。 実装し終わって気づいたのですが、C++で作った部分をdll化してCTypes使えば済んだかもしれません。

夏バテなのか、疲れがたまっているのか、コード汚いです。動けばいいやというスタンスです。

def card_detect(img):
    # 平滑化、二値化、輪郭抽出
    g_img = cv2.GaussianBlur(img, (5,5), 8)
    r, bin_img = cv2.threshold(g_img, 85, 255, cv2.THRESH_BINARY_INV)
    canny_img = cv2.Canny(bin_img, 50, 200)

    # 確率的ハフ変換による線分検出
    h, w, c = img.shape
    minLineLength = min(w,h) // 4
    maxLineGap = 10
    lines = cv2.HoughLinesP(canny_img, 1, np.pi/180, 40, minLineLength, maxLineGap)
    if lines is not None:
        if len(lines) < 4:
            return None

        # 検出した線分のすべての頂点を列挙
        pts = np.array(lines)
        pts = pts.reshape(-1, 2)

        # 回転を考慮した外接矩形を求める
        rect = cv2.minAreaRect(pts)
        box = cv2.boxPoints(rect)
        box = np.int0(box)

        return box
    return None

この関数は、カードらしい領域が見つかった(=線分が4本以上ある)ならば、傾きを考慮したバウンディングボックスが返されます。見つからなかったらNoneです。

傾きを考慮したといっても、透視投影変換のような変換には耐えられないので、机に対して斜めから撮影した画像ではズレが大きくなります。あとでもうちょっとまともにしましょう。

画像分類するために透視投影変換をする

以前、chainerで画像分類のために学習させたモデルはカードの正位相(MTG用語)での画像です。 なので、正位相の画像を作ります。

(省略)

画像分類する

以前にchainerを使って実装したものそのままです。 このコード自体はいたって普通なのですが、NNから間違えた答えしか返ってきません。悲しい。

なんとなくですが、opencvとpillowで行列の形が違う気がします。特にカラーチャネルとか。 あとで見直しましょう。

def detect_cardtype(image):
    # NNへ入力可能な形式に変換する
    pixels = np.asarray(image).astype(np.float32)
    pixels = pixels.transpose(2, 0, 1)
    pixels = pixels.reshape((1,) + pixels.shape)

    print("NN input image shape = {}".format(pixels.shape) )
    # 識別
    y = model(pixels)
    prediction = F.softmax(y)
    m = np.argmax(prediction.data)
    return m

実行結果

カード検出と分類結果の取得まで1秒くらいで動いています。実測はしていませんが、これくらいの遅延なら実運用に耐えられるかなと思います。

Hololensと連携したい

実際のアプリと連携させるにあたって、この遅延時間を考慮する必要があります。

遅延時間の考慮とはどういうことかというと、ある画像からカードを検出して識別結果が返ってきたとして、それは過去の映像フレーム上での話であってHololensの今現在の状態から過去に-delta遡った時点での推定結果です。

ここでHololensが「自身の空間上の位置と姿勢」を把握していることを活用することになりそうです。

つまり識別したい映像フレームが採取されたタイミングでのHololensの空間上の位置・姿勢を保持しておけば、そこからの相対位置で本来あるべき位置へと補正できるというアイデアです。

DeepLearningで推定されたカードの位置・姿勢は、確かにDNNの計算時間による遅延はあります。 けれども、Hololensがフレームを採取した時点での位置姿勢とは必ず合致するはずです。 ということは、推定された位置と姿勢は現在時刻からみて-deltaの時刻の姿勢にあたるので、このdelta時間の間に変化したHololensの位置・姿勢の変化量を加味してあげれば辻褄はあうはずです。

カード種別間違えて識別しているのはそのうち調査するとして、先にこっちの理屈が正しいかどうかの確認をすすめたいところです。

今後の方針

私は技術的な課題(遅延)を追及することよりも、実用性・コストパフォーマンスのほうが重要だと思っています。 ただし技術を無視していいとは思いません。あくまで優先順位の話です。

なぜなら「技術的・理論的に美しい」ことと、「面白い・実用的なアプリである」というのは別の概念です。

なので「実現可能性があること」と「実現に向けた課題の洗い出し」のためにプロトタイプを作ります。

応用分野

物体の検出と分類がほぼすべてDeepLearningで実現できそうなので、あとはゲームが変わっても学習モデルを更新するだけで済むんじゃないかなと思っています。

汚いコードですがgistにあげておきました。 https://gist.github.com/javoren/2fa2fc6ebcd8582d72b86f7017e742eb

今後の展望

古いPCがお亡くなりになって、夜な夜な貯めていたアレな動画とか重要参考資料がなくなってしまいました。 バックアップ大事。

gistが正しい意味でバックアップといえるかどうかはさておき。

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

UWPで非同期taskを書くときに気を付けること

まだまだtaskの操作に慣れていません。かたりぃなです。

今日はUWPのコードをC++/cxで書くにあたって、詰まったポイントと解決策を書いてみます。

UWPでの非同期taskとは

ムーアの法則の限界が叫ばれてからCPUはマルチコア時代に入っています。モバイル端末でさえも。

CPU資源の有効活用を考えたとき、マルチスレッド・マルチプロセスなコードにすればCPU資源を有効活用できるよねと概念レベルで言うだけならタダですが、実際にやってみると難しいことがあります。

難しいポイントとしては、例えば従来型の手続き型プログラミングの延長でマルチスレッド・マルチプロセスをやろうとすると、次のような問題に直面します。

  • スレッドやプロセス間のデータ受け渡し
  • スレッドの同期
  • スレッド立ち上げのコストと、並列実行のトレードオフ

特にC/C++のような手続き型・オブジェクト指向な言語でこれらを乗り越えようとすると相当キツイです。

設計上はうまく作ったつもりでも、マルチスレッド・マルチプロセスではテスト難しくなってきて「動くこともあるけど、動かないこともある」なんてことが簡単に起きます。

こういった類の問題はテストでは再現が難しい問題になりがちなので、相当タチが悪いバグになってプログラマを悩ませます。

Windowsアプリではこの問題に対する多くのアプローチがあります。

今回はその中でもtaskをどうやってうまく扱うかという問題に焦点をあてて分析してみます。

いわゆるMicrosoft-PPLです。

公式ドキュメントはこのあたりです。

https://msdn.microsoft.com/ja-jp/library/dd492418.aspx

https://msdn.microsoft.com/ja-jp/library/dd492418.aspx

taskを返す関数=高階関数としてとらえてコードを書く

これは私なりの結論です。

taskとは何かということを考えてコードを書くとき、それは関数であり、taskを生成する関数とは関数を返す関数、いわゆる高階関数だという解釈です。

※あくまで概念レベルで「ああ、私が欲しかったの、こういうやつだ」と感じただけの話なので、その世界で本気でやってる人からは異論あるとは思います。

高階関数の概要はwikipediaで一行で簡単に述べられています。 https://ja.wikipedia.org/wiki/%E9%AB%98%E9%9A%8E%E9%96%A2%E6%95%B0

要は

  • 関数を引数にとる関数
  • 関数を戻り値にできる関数

です。

というわけで 以下にコードと概念を整理します。

PPLでは、従来の関数と呼ばれてきたものを「タスク」として定義できる

タスクとはppl::concurrency::taskテンプレートクラスによってラップされた関数です。

ここではラムダをtaskで包むことに焦点をあてます。実際そういう使い方がほとんどですし。

PPLでタスクを作るには2つの方法がありました。

taskクラスのコンストラクタを使う方法

たとえば整数のリストを受け取って合計を出す関数を考えたとき

auto sum_lambda = [](std::vector<int> nums) -> int {
    int s = 0;
    for(auto val : nums){s += val;}
    return s;
};
auto sum_task = task(sum_lambda(arg_list));

こんな感じになります。 ここでは分けて書きましたが、以下のようにtaskクラスのコンストラクタに直接ラムダを渡してしまうほうが便利かつ安全です。

auto sum_task = task([](std::vector<int> nums) -> int {
    int s = 0;
    for(auto val : nums){s += val;}
    return s;
});

この例を手続き型orオブジェクト指向の考え方のまま読むと「sum_taskはtaskクラスのインスタンス」になります。

しかし「合計を求める"関数"をインスタンス化した」と考えたほうが後々スッキリします。

create_task関数を使う

関数をcreate_task()に渡すことでコンストラクタと同じようにtaskクラスを作ることができます。

UWPのAPI呼び出しなんかは、この関数を使って書かれているサンプルが多かった印象です。 APIの戻り値型はAPIごとに異なっていますが、taskクラステンプレートに戻り値型が適用されるため、あんまり気にせずautoで受ければいいかなと思っています。

ただし、普通は後続の処理(後述のthen)で型を明示的に指定するので、taskクラスインスタンスを直接どうこうするということは意識せずとも良さそうです。

たとえばファイルを開くタスクを作るコードはこうなります。

auto file_get_task = create_task(StorageFile::GetFileFromApplicationUriAsync(uri));

GetFileFromApplicationUriAsyncが返してきたタスクが生成されます。

タスクを実行する

上記の方法で、タスクを作ることはできました。 次は作ったタスクを実行する必要があります。

taskがラップしている関数であっても、C/C++のふつうの関数と同じです。 関数定義だけ書いてもどこかから実行してもらわなければ意味がありません。

wait, when_all, when_anyなどでタスクの実行完了を待つことができます。 結果を拾いたいときはget()で。

上記の方法で生成したタスクに対してそれぞれ呼び出すだけです。

waitとgetは単一のタスクの終了を待つものです。

たとえばこんな風に。

auto sum_task = task([](std::vector<int> nums) -> int {/* 略*/}
sum_task.wait();

when_allは複数のタスクが完了するのを待つ関数です。when_anyは複数のタスクの完了を待つという点では同様ですが、「いずれかが完了するのを待つ」関数です。 以下のコードではテクスチャの読み込み/デコードを並列実行可能なタスクにしたものです。

すべてが並列に実行される保証はありませんが、一例として。

 std::vector< task<void> >  texture_read_tasks;
    for (int i = 0; i < texture_num; i++) {
        auto readtask = task([](int num){/*ごにょごにょ*/});
        texture_read_tasks.push_back(readtask);
    }
    when_all(texture_read_tasks.begin(), texture_read_tasks.end()).then([]() {
        OutputDebugString(L"texture load/decode success\n");
        return;
    });

タスクとタスクをくっつけるthen

UWPのC++/cxでも従来の手続き型のように記述していきたいです。

従来の手続き型のように記述したいというのは、入出力の依存関係があって並列化できないケースなどがわかりやすいです。 たとえばファイル操作のopen/read-write/closeなんかが該当します。

こういうときに役立つのがtaskクラスのメソッドthenです。 タスクの後続タスクを定義するものです。

taskのthenの説明の前にコードを書くときの論理レベルで考えると

  • ファイルを開くタスク
  • ファイルハンドルを使って読みだすタスク
  • ファイルハンドルを閉じるタスク

とタスクを定義できます。これらのタスクの実行は、順序が大切です。

thenはあるタスクの後続タスクを定義するものなので、こういった場面で必須になります。

thenによって数珠つなぎにされたタスクをタスクチェーンと呼ぶらしいです。

タスクチェーンを実行する

thenが返してくるのもタスクです。 どのようなタスクでも実行してあげる必要があります。作ったまま放置ではいけません。

まだ完全に把握できていませんが、私が書いたコードについていえば、task関連の実行時エラーの原因の大半は作ったまま放置でした。

というわけでタスクチェーンの実行です。 これは末尾タスクの終了を待つだけで良いです。

タスクチェーンで「末尾のタスク完了を待つ」ということは、タスクチェーン全体が実行されるのを待つことに相当します。

ここで勘違いしていてすごく詰まったのですが、thenは「後続タスクを定義する」だけであって、「実行する」わけではないです。

なので、作ったタスクチェーンは誰かが実行してあげなければいけません。 (もしくはフレームワークのどこかで一括して実行する機構があるなど)

ラムダを使う理由

タスクを作るのにどうしてラムダを多用するのだろうと自分なりに考えてみました。

私なりの結論としては「task間のデータ受け渡しが安全である」ためと考えました。

C++のラムダは、定義した位置にクロージャオブジェクトが生成されます。 コンストラクタも生成されるので、それを使ってデータ受け渡しが行われます。

すなわち、ラムダの引数がtaskへ受け渡すデータになります。イメージとしてはプロセスへメッセージを送るというほうがしっくりきます。

ここで「安全」といっているのは「taskに渡すデータそのものが競合していない」という前提があったうえでの話です。

その前提を守ったうえでの安全です。

なお「ラムダ使ううえで、これは避けましょう」というのはMS公式からも提示されていました。

https://msdn.microsoft.com/ja-jp/library/dd492427.aspx

要は「taskに渡したラムダの実行完了前に寿命が尽きるオブジェクト(スタック上の変数とか)をキャプチャしないでね」ということですね。

これらの情報をもとに色々コード書いて試行錯誤した結果、スマートポインタ系をラムダの引数に渡すのが一番よさそうだと考えています。

ただし、スマートポインタとはいえ、もしstd::shared_ptrをtaskに受け渡す必要に迫られた場合、「それの寿命が尽きないこと」は言語側で保証できますが「競合をしていないこと」はプログラマが保証しなければいけないので注意が必要です。

理想的な設計

ここまででtaskの基本的な扱いができるようになりました。 次に一歩進んでキレイな設計とは何だろうということについて考えます。

宗教観とか時代の流れとかあるので、ここでの答えはあくまで現時点での私なりの答えです。

関数(をラップしたタスク)を返す関数

急に関数型言語っぽくなりましたね。関数型言語の世界でいう高階関数っぽいものです。

従来の手続き型プログラミングのように値やオブジェクトを返すのではなく関数を返そうみたいなアプローチです。

どうしてこれが良さそうと考えたかというと、

  • 色々試行錯誤した結果から
  • MSのサンプルコードでもこの方式が多い
  • タスクチェーンは呼び出し元が組み立てたいから

最後の以外あまり説得力ないので、最後のだけちょっと書きます。

タスクチェーンを呼び出し元で組み立てたい理由は、従来の手続き型プログラミングでの「ある関数の実行結果を使って、後続の関数を実行する」ように記述していたプログラミング方法の"関数"を"タスク"に置き換えたいからです。

混ぜるな危険

もし「順次処理の関数」と「PPLタスク」を混ぜると次のような面倒な事になってしまいます。

  • 手続き実行結果をもとに後続タスクを実行する
  • タスクの実行結果をもとに手続きを実行する

こういうコードを作ってしまうと「どこがタスクとして実行されて、どこが手続きとしてメインスレッドがら実行されるのか」が見えにくい・わかりにくいものができました。

順次処理とtaskを混ぜたコードは、一応完成しましたが、ちょっと機能拡張しようとかやりはじめたときに手も足もでなくなりました。

taskを返さずに、手続き関数の中でwaitしてはどうかと試みましたが、そうしてしまうと今度は「どの関数がブロッキングで、どの関数がノンブロッキングなのか」がわからなくなりました。

つまりデバッグできないのです。こういうコードは廃棄処分です。

というわけで、UWPの枠組みでやるならできるだけtaskにしたほうがスッキリします。UWPのC++API自体もtask返してくるのが多いですし。

感想

今まで何となくサンプルコードを真似して書いていたtaskですが、高階関数の概念のおかげでスッキリしました。

taskを使って競合を避けるコードを書こうとすると、どことなく関数型言語っぽくなってきた気がするので、また別途記事を書く予定です。

長くなりましたが今回はこれくらいで。

Hololensでカメラの解像度を変更する

前回つくったカード検出器をHololensで使ってみました。かたりぃなです。 処理がやっぱり重いみたいで表示がカクつくくらいに気持ち悪いです。

本当に負荷が原因なのか知るために、画像処理全体の負荷を下げて試すことにします。

簡単に負荷を下げる方法として、カメラの設定(解像度、フレームレートなど)を下げてしまうことにしました。

もちろんアルゴリズムによっては低解像度では使えないなど弊害はありうるので注意が必要です。

簡単に試したい理由は、アルゴリズムの最適化をかける前に「そもそも負荷が低ければアプリとして成立するのか?」を確認したいからです。

個人開発での限りある時間を無駄にはしたくないので。

カメラ(MediaCapture)の初期化

いつもどおりマニフェストファイルにカメラを使うよう設定してから、mediaCaptureクラスを使います。 Platform::AgileはいわゆるスレッドセーフなCLRらしいです。

初期化コードはこんな感じです。

        Platform::Agile<MediaCapture> mediaCapture( ref new MediaCapture() );
        return create_task(mediaCapture->InitializeAsync(settings))
            .then([=]
        {
                // ここでmediacaptureのカメラを起動する
        });        

カメラの解像度を変更

カメラ選択のUIとか作ろうかと思いましたが、面倒なのでやめました。

実験だけのつもりなのでデバッガで出力して、パラメータを書き換えるだけのほうが手っ取り早いです。

コードだけ簡単に。

         // ここまででmediaCaptureはインスタンス化されていること。開始はしてなくてもよい。

            // 負荷が低くなるよう、小さめのサイズのカメラ入力画像を設定する
            auto FindPreviewResolutions = [](Platform::Agile<MediaCapture> cap) -> Windows::Media::MediaProperties::VideoEncodingProperties ^
            {
                auto prop_list = cap->VideoDeviceController->GetAvailableMediaStreamProperties(MediaStreamType::VideoPreview);
                if (prop_list->Size == 0) {
                    // todo
                }

                Windows::Media::MediaProperties::VideoEncodingProperties ^ vp;
                for (auto prop : prop_list) {
                    auto name = prop->GetType()->FullName;
                    if (name == ref new String(L"Windows.Media.MediaProperties.VideoEncodingProperties")) {
                        auto video_prop = static_cast<Windows::Media::MediaProperties::VideoEncodingProperties ^>(prop);
                        char buf[1024];
                        sprintf_s(buf, 1024, "w=%d, h=%d\n", video_prop->Width, video_prop->Height);
                        OutputDebugStringA(buf);
                        if (video_prop->Width == 896) {
                            vp = video_prop;
                        }
                    }
                }
                return vp;
            };
            auto vp = FindPreviewResolutions(mediaCapture);
            // 低解像度のプロファイルがとれたので、設定する

簡単に説明。 cap->VideoDeviceController->GetAvailableMediaStreamProperties(type)で、メディアのプロパティリストが取れます。

このリストの要素はIMediaEncodingPropertiesというインターフェースなので、prop->GetType()->FullNameに目的のプロパティが入っていることを確認します。これはStringなので文字列比較です。

目的の型が入っていることが確認できたら、その型にキャストしてからプロパティを読み出します。

こういうキャストって個人的にはちょっと嫌です。ダウンキャストっぽく見えて。 たぶんただの宗教観なので気にしないことにします。MSのサンプルコードもこういう形になっていたので。

最後に取得したプロファイルをmediaCaptureに設定してあげれば完了です。 こんな感じに。 asyncなのでtaskでラップしてあげるのを忘れずに。

            mediaCapture->VideoDeviceController->SetMediaStreamPropertiesAsync(MediaStreamType::VideoPreview, vp);

というわけでこのコードをHololensの実機に放り込むと、サクサクとカメラ映像を処理できるようになりました。

つまり、カード検出まわりのアルゴリズムをもうちょっと工夫すればなんとかなるレベルというわけですね。 もしくは低解像度で検出できる仕組みを考えるか。

感想

C++らしくテンプレートで分岐できないかなと考えましたが、すぐには無理でした。

というのも、Stringの中身は実行時に確定するので、テンプレートを展開するコンパイル時にそれを判断する処理を入れるのにはちょっと工夫が必要になってしまいます。

ここまで書いて気づきましたが、いわゆるvisitorパターンを実装してあげればキャストなしでいけそうな気がします。 つまり任意のメディア型をacceptするクラスを作る。

しかしvisitorまで書き始めてしまうと今回の目的と比べると大がかりすぎるからやめておきます。

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