catalinaの備忘録

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

2020年の抱負と昨年の振り返り

2019年のふり返り、2020年の抱負

新年あけましておめでとうございます。例年やっているふり返り記事になります。かたりぃなです。

まず最初に。気が付けばこのブログの読者も30人を超えていました。まだまだ未熟者のブログではありますが読んでくださってありがとうございます。

今後も活動を続けていきますのでご愛顧のほどよろしくお願いします。

最初に2020年の目標を一言で表現すると。。。

ダイの大冒険のアニメ化の合わせて、ARで魔法や必殺技を再現する!」

です。20年以上前の夢、メドローアやアバンストラッシュ撃ちたいとかミナカトールで魔法陣を発動させたいという夢を叶える絶好のチャンスです。

(いつも物体検出やトラッキングで詰まっているように見えるのは気のせいということにしておきましょう)

それでは2019年のふり返りと2020年の抱負をやってみたいと思います。

2019年やったこと

このブログで2019年に記事にしたものをふり返ってみます。

記事にしたものは3つあります。それとは別に、記事にしてないものも軽く触れてみます。

1. フォトグラメトリ

OpenMVGとOpenMVSですね。

目的は「コスプレ用のアイテムを3Dプリンタで作りたい。3Dモデリングが手間かかるので、3Dモデルも自動で作りたい」というものでした。

試行錯誤しましたが、目的を完全に果たすことはできませんでした。

目標達成には至りませんでしたがOpenMVGのコードを追うことで技術的な理解が深まった点は良かった点かなと思います。今までARで追ってきていた技術の基礎の延長上の話だったので、やっぱり基礎は大事だなと思った次第です。

最終的にはMeshRoomというOSSを使用することで、使えそうな3Dデータが得られました。このMeshRoomで生成した3Dモデルも完全なものではないので、3Dモデリングの下書きとして利用しています。

結局は単体技術で全ての課題を解決するだけではなくて、運用まで含めて妥当な落としどころを見つけるのが大事ですね。もちろん単体技術で解決できるならそれは理想ではあるのですが。

2. インフラ

私の中で利用できる技術が2つ増えました。

  • ELKスタック(ElasticSearch, LogStash, Kibana)
  • Ansible-Playbook + ELKスタック

どちらもdocker上で動かしています。

EKL

ログ分析はシステムを運用するうえで今後も必要な技術だと考えているので、少しずつでもこれらを使えるようになっておいて損はないと思います。

実際の運用ではELKを素のままで使うことはおそらくないだろうなと思っていて、何らかのデータストレージのフロントとして使うことが多いだろうと想定しています。

また、ELKに文字列を突っ込む際に形態素解析を試したりしたのも楽しかったですね。PythonからMecabを叩いてたりしました。

Ansible-Playbook + EKL

まだ記事にしていないお話しです。「Ansible-Playbook + ELKスタック」というものを試していました。

どういうものかというと、Ansible-Playbookのログ(changedとかokとか出るアレ)をELKに渡しておくことでログの視認性を良くするのが目的です。

Playbookのログって見づらいので、色々と試行錯誤していてこの手法に辿り着きました。

これができると何がいいかというと、よくあるPlaybookの問題「changedが出てるけど、これ何?常に出る(=実は無視していい)やつでは?」を簡単に切り分けできるようになるだとうと思っています。

もちろんログを蓄積してあるという前提ですが。

CI/CDの監視で使えば幸せになれるかもしれませんね。本年の早い段階で記事にしておくつもりです。

Ansible-Docker

これもまだ記事にできてないお話しです。Dockerのデプロイで外部のコンテナリポジトリを使わずにデプロイするものです。

AnsibleのモジュールでDockerコンテナを操作できるものがあるので、利用したものです。 やりたいことと完全にマッチしているわけではありませんが、工夫すれば色々とできそうです

Ansible-Playbookの枠組みで次のことが実現できるので、迅速に開始させたいサービスでコンテナを使う場合には選択肢としてアリなのではと思っています。

  • dockerイメージを作れる
  • dockerイメージをエクスポートできる
  • dockerイメージをインポートできる
  • コンテナ操作できる(調査中)

3. MixedReality(Hololens)

自分はHololensでC++を使う奇特な人種なので、UWP用の新しいC++/CLRとしてWinRTを試してみました。C++/CXより良くなってる気がします。しばらく使っていくうちに現時点での不足部分もわかってきました。

記事にしていませんが、C++/WinRTを使ってUnityネイティブプラグインを書いてみたりもしました。

まだうまくいってなくて落ちたりするので、もう少しデバッグしてから記事にする予定です。

Unityネイティブプラグインに手出しした理由は、C++だけだとライブラリの充実度などの問題で、開発速度があがらないためです。たとえばVRMやglTFのモデルの取り込みとかはUnityのほうが圧倒的に進んでいる印象です。

そう思ってUnityで色々と試してみたものの、やはり慣れない環境というのは手間取ってしまいます。

特にHoloToolkitからMRTKへの移行で躓いてしまったのは残念ポイントです。そうこうしているうちに2019年が終わってしまいました。

2020年はリベンジです。

あと新型も買わなきゃ(レンタルかなぁ)ですね。

4. 記事にしてない大きなカテゴリ

そのうち記事にするので軽く触れてみます。(後者は記事にはしないかも)

  • TweLiteプロジェクトの再構築
  • 投資

twelite

TweLiteはZigBeeプロトコルでIoT的な用途で使える便利なやつです。 今回TweLite2525Aが出てたのと、開発環境がVSCodeになっていたので一通り作り直しました。

久しぶりの組み込みプログラミングでしたが、慣れている部分が多いのでサクっといきました。 TweLiteは次の点が優れていると思います。

なので、私のような低レベルレイヤで戦った人であればコード読んですぐ開発に入れる環境でした。 (OS固有の仕様とか無いので、一般的な組み込みプログラムの理論だけで通用する)

電子回路回りも作ってあるので、あとはどこかのイベントで実験するだけですね。

投資

投資はプログラムとは全く関係なくて、他の道を探すときにどうなるのだろうという試みの一つになります。

動機は

  • 不労所得が欲しい(収入を増やしたい)
  • 老後の資産形成

などです。

サラリーマンの収入は、勤めている企業の収益や社内政治に影響される部分が大きくて、収入を増やすにはそれらの理解が欠かせません。

私は社内政治というか人づきあいに興味が薄いので、市場理解のほうへ挑戦したというところです。

市場理解と投資の関係について少し述べると、市場そのものの動きを理解するために実際に投資・運用して、自分自身が投資家の立場としてどうするかを理解していくのが近道だと考えたためです。

2018年は実験、2019年は本格的に投資の売買をしました。

一旦の結論として「物事を分析して判断する力」があれば投資で稼ぐことは可能だということがわかりました。

この「物事を分析して判断する力」力は多くのソフトウェアエンジニアには自然と備わっていると思っていて、例えばリスク管理の分野で考えるなら

  • どこまでなら損を許せるか(リスク評価と対策)
  • 損した原因を分析できる(原因分析)
  • スコープを明確化できる(短期的な売買/長期的な売買を区別できる)

などが該当するのではないかと考えます。 「損」という言葉を「障害」や「影響範囲」などに置き換えれば身近なものに感じるのではないでしょうか。

ひとまず年利5%前後を達成できているので、継続していきたいと思います。

ファイナンスのための確率解析 II (連続時間モデル)

ファイナンスのための確率解析 II (連続時間モデル)

ファイナンスのための確率解析 I

ファイナンスのための確率解析 I

  • 作者:
  • 出版社/メーカー: 丸善出版
  • 発売日: 2012/04/20
  • メディア: 単行本

こんな本購入したので、こちらも機械学習のついでに読み進めていっています。 知らない用語ばかりなので難航していますが、新しい知識というものは楽しいものです。

2020年のテクノロジー予想

ARって未来の技術」とか「特別なもの」みたいに思われていたのが2018,2019年までかなと思っています。

ここ数年のテクノロジーを見ていると、ARデバイス単体でARができるのは当たり前で、次のステップに移りつつあると考えます。

具体的には

などが考えられます。

ガートナーが発表しているテクノロジのハイプサイクルで2018年に「過度な期待のピーク期」を超えて「幻滅期」へ、2019年には図から消えました。2018年の発表では、"今後5~10年"に"競争優位性をもたらす可能性が高い"との表現でした。

このことから、2020年はまさに啓蒙活動や生産の安定性にむけた大事な時期ではないかと考えます。

xRについて思うこと。

毎年がVR元年と言われているここ数年ですが、エンドユーザーに向けてxR(VR,AR,MR)を推すのは少々筋が悪いのでは?と思っています。

xRは表現手段、情報の表示手段の一つであってユーザーが求めているものではないはずです。ユーザーが求めているものは

  • 必要としている情報を
  • 必要なタイミングで
  • わかりやすく見せてくれること

だと思っています。

スマートフォンが普及した現代ではスマートフォンの画面に情報を表示するだけで充分な場合もあります。 逆にスマートフォンの画面だけではわかりにくい情報は、別の方法でユーザーに見せる必要があります。

前者はいわゆるビューワーやクーポンなどのアプリで、後者は地図アプリなどが代表例でしょうか。

xRでも同じことで、概念的には「従来と見せ方の手段が異なる」だけです。

しかしデバイスとしての制約(装着するなど)があるため、どうしても区別してユーザーに説明することになってしまうので、「これはVRである」「これはARである」みたいにあまり重要ではない部分に話を持っていかれている感がします。

AR,MRについて技術的な観点から考えると「仮想世界と現実世界の情報をシームレスにやりとりする」という点がポイントになるのかなと思っています。

キーボードやマウスを使わなくても片手に収まるタッチパネルだけで情報検索ができるようになったのと同じように、タッチパネルを使わなくても声だけ、周囲の情報だけで自動的にユーザーが必要としている情報を推論して必要な情報を提示するという形が将来のMRなのかなと思っています。

2020年やりたいこと

Hololensをメインにやっていこうと思います。今までサーバ側がメインだったので、HololensとUnityをしっかりと調べたいと思います。

Hololens2も来ますし、そっちに軸足を移しつつ進めていきたいですね。

また、2020年からは転職も視野に入れて活動しようかと思っています。

本業は都内のWeb屋で働いていて、器用貧乏な中年です。 もしこのブログの内容を見て興味あるという方いらっしゃいましたらtwitterかコメントでも連絡いただければと思います。

本業の内容はこのブログより深く高度なことをやってはいますが、どろり濃厚なので公にはしづらいのです。あしからず。

それはさておき、まずは自分の力で何かを作り上げていくのが何よりも大事だと考えているので、今年もそれに向けて頑張っていこうと思います。それではこれくらいで。

本年もよろしくお願いします

VS Code Remote Developmentを試してみた(感想)

晩秋から冬へ移り変わっていく季節ですね。世間では色々なものが発表されていて、どこから手を付けていこうかとワクワクしています。 今回は開発環境まわりがパワーアップしたので、その記事になります。

開発環境の構築

Web開発ではリモートマシンで作業することが多く、そのためには数多くの設定をしてあげる必要があります。

俗に「環境構築」とか呼ばれる作業で、これがとても面倒だったりします。

実際の使い方は公式チュートリアルが充実しているので省略します。

https://code.visualstudio.com/remote-tutorials/containers/getting-started

自分の場合は既にdockerとvscodeが動作する環境があり、dockerもそれなりに触っていたので、1時間前後で完了して雰囲気は掴めました。

今回は少しだけ昔を振り返りつつ、今回のVScode拡張機能で何ができるかを纏めます。

ちょっと昔の環境構築

ちょっとだけ昔、Web開発といえばVirtualBox仮想マシン設定からでした。(今もある程度はこういう作業必要だったりしますが)

https://catalina1344.hatenablog.jp/entry/2014/04/02/222630

少し進んでVagrantとかPappertみたいな環境構築ツールも登場しましたが、環境構築の苦労はあまり変わらなかった印象です。

なんで大変なの?て言われると、その時々によって状況が変わるという部分が大きくて、たとえば

  • 環境構築ツール(たとえばVagrant)のバージョンが違う
  • 開発担当者の環境そのものが違う
    • win/mac
    • 環境構築ツールを動かす言語(RubyとかPythonとか)のバージョン

みたいなのがあります。

毎回こういう地雷を踏みながら、特定条件下(社内だけとか)でしか使えないナレッジが溜まっていくわけです。

最近の環境構築

いまどきはDockerという軽量でポータブルな仮想環境があるので、これで環境構築がとても楽になった印象です。

楽になった部分はありますが、まだ解決できていなかった部分もあります。

尚、今回のエントリは「まだちょっとだった部分」が解決されたよというお話しです。

dockerを使うことで昔より楽になった

  • ターミナル設定
  • ファイル共有設定
  • ネットワーク設定

dockerになってから、すごくラクになりましたね。 トラブルが起きたら結局設定ファイル見て頑張るみたいなのはありますが、それはそれで。

dockerを使っても、まだちょっとだった部分(今回のvscodeで解決!)

  • エディタの設定
  • デバッガの設定
  • localとremoteでのバージョン合わせ

ここはdocker含めて、環境構築ツールでは解消しきれていなかった部分です。

そもそもDockerが提供するのは「仮想環境」であって、「開発環境」ではないというだけな話です。

問題を解消できていない理由自体もとても簡単です。開発環境として使う場合、ローカルマシンとリモートマシン両方の設定が必要になってくるからですね。

たとえば

  • エディタをどう動かすか
    • localで動かしてファイル共有だけする
    • sshfsとsshで直にremote編集か
  • リモートデバッガでは接続先に何かをインストールする必要がある
    • 使う言語にあわせたリモートデバッガが必要
    • ネットワークのポート設定も必要だったり
  • intellisense(コード補完)を効かせるための設定が面倒
    • remoteとファイル共有だけしてるケースだと、localとremoteで同一パッケージが必要

みたいなのがあったりして、とても手間でした。

特に最後のintellisenseの設定が鬼門で、たとえばPythonで開発する場合

  1. DockerコンテナのPython関係のファイルをWindowsから見えるようマウントする
    • パスが違ってたりしてうまくいかない。
    • この方法は諦めた
  2. ローカルのWindowsマシンにAnacondaでも入れてみる力技でいってみる
  3. パッケージの関数も補完してほしいからpip installとかをwindowsとコンテナ側それぞれでやる
    • ただしWindowsには入れにくいパッケージがあったりする。(graphvizとか)
    • pip instal時にビルドするタイプ、かつ依存ライブラリもってるやつ
  4. windows側に全部そろっていくなら、dockerいらなくね?

みたくなってました。

VS Code Remote Developmentを使う

公式:

https://code.visualstudio.com/docs/remote/remote-overview

図を見るとわかるとおり、RemoteOS上にVS Code Serverを置いて、そいつが全部面倒みてくれるという仕組みのようです。

こうすると何が起きるのかというと、remotedevelopment当初のキャッチフレーズのとおり

VScodeとdockerコンテナのシームレスな連携」

です。

まずは使ってみた感想

控え目に言って最高です。

何が最高かって

  • ものの数分で各プログラム言語のコンテナが立ち上がって開発を始められる
    • js, php, python, C#, R, C++, ...
    • 裏でdocker-pullして必要なものを追加してくれてる
  • デバッガやコード補完も動く
    • 過去インストールした拡張パックが影響してるかもしれないが、特に難しい設定もなく動く
    • Python, PHP, C#(dotnet core)で確認
  • 追加のライブラリを入れたい
    • 単にdockerfileを編集すればいける
  • 開発環境の面倒な初期設定全部やってくれる
    • 以下のような問題を気にしなくていい
      • コンテナ作るためのdockerfileどうするとか
      • ネットワーク割り当てどうしよう、nginxの設定どうしようとか
      • ファイルのマウントどうしよう。どこが適切か(パーミッション維持する必要あるなら尚更)とか

少し掘り下げて使ってみた感想

少し掘り下げて、実用上問題ないか試してみた感想

  • よくあるWebアプリ(CMSとか)の基本構成もサポートされてる
    • Webアプリ + DB みたいな構成
    • python + postgreSQLみたいなのもプリセットにあるので楽々作れる。
  • 過去に自分が作ったコンテナを利用できる
    • dockerfile もしくは docker-compose.yml を指定するだけ
      • 起動時にどのコンテナにアタッチするか訊かれるので、アプリが動いてるコンテナを指定
    • 昔作ったdocker-composeで動作確認できた
      • nginxコンテナをfront
      • pythonPHPコンテナををback

実用を考えた場合にどうなのか?

VS Code Remote Development を実際に使うことを考えてみると、まあアリなのではないかなと思っています。

ただ、手放しで全てOKといえるわけではなくて、本番環境のコンテナどうやって作るかなどは別途考える必要がありそうです。

(開発用の色々なものが入ったままのコンテナを本番に置く勇気は無いので。。。)

とはいえ、原理的にはdockerを開発環境のback-endに置くというだけのことなので、dockerを前提とした開発であればアリだと思います。

構成もシンプルなので、困ったときは通常のdockerコマンドで外から操作できるのも強いところです。

感想と今後の展望

VS Code Remote Developmentという武器を手に入れたので個人開発が大きく捗りそうです。

デバッガの設定が面倒だからと諦めてprintfデバッグしてた環境とか、大きく改善されますね。

あとは開発環境の宗教論争が始まった時に「Windows10Proとdockerとvscodeで統一すればいい」と火に油を注げるようになります。

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

OpenMVG, OpenMVSで3D再構築する

前回のエントリでOenMVGを使って三次元再構成ができることが確認できました。 今回はさらに進んで、ジオメトリを再構成してみます。

カメラ内部パラメータの設定

前回のエントリで生成したカメラ内部パラメータは間違えていました。正しくはこうなります。

間違えていた原因としては、OpenCVキャリブレーション関数でのRow/COlumnsの扱いが逆になっていたためでした。

手作業で格子状の画像からこんな感じの交点を抽出しましたが、これは横7, 縦5個の交点です。

    corners = np.array( [
        [1013, 321], [1052, 319], [1090, 318], [1129, 316], [1166, 315], [1204, 314], [1240, 313],
        [1012, 360], [1050, 358], [1088, 357], [1126, 353], [1164, 353], [1201, 352], [1237, 350],
        [1012, 395], [1050, 394], [1087, 394], [1124, 391], [1160, 389], [1197, 389], [1234, 386],
        [1011, 432], [1048, 431], [1085, 430], [1122, 427], [1158, 425], [1195, 424], [1230, 422],
        [1011, 469], [1047, 466], [1083, 465], [1120, 463], [1156, 460], [1192, 458], [1227, 456],
    ], np.float32  )

どういうことかというと、

    rows = 7
    cols = 5

として対応する画像上の点を生成するべきなのですが、rowとcolを逆に設定していたためにおかしなことになってました。 opencvあるあるですね。

というわけで、キャリブレーションやりなおすとこうなりました。

kp='2105.02823002;0;795.46581198;0;1386.68788826;422.25675948;0;0;1'
pIntrisics = subprocess.Popen( [os.path.join(OPENMVG_SFM_BIN, "openMVG_main_SfMInit_ImageListing"),  "-i", input_dir, "-o", matches_dir, "-k", kp, "-d", camera_file_params, "-c", "3"] )

キャリブレーションのコードはこちら

    cols = 5
    rows = 7

    # コーナー検出をせずに、自前で入力する
    corners = np.array( [
        [1013, 321], [1052, 319], [1090, 318], [1129, 316], [1166, 315], [1204, 314], [1240, 313],
        [1012, 360], [1050, 358], [1088, 357], [1126, 353], [1164, 353], [1201, 352], [1237, 350],
        [1012, 395], [1050, 394], [1087, 394], [1124, 391], [1160, 389], [1197, 389], [1234, 386],
        [1011, 432], [1048, 431], [1085, 430], [1122, 427], [1158, 425], [1195, 424], [1230, 422],
        [1011, 469], [1047, 466], [1083, 465], [1120, 463], [1156, 460], [1192, 458], [1227, 456],
    ], np.float32  )

    image_points = []
    image_points.append(corners)

    # 検出した画像座標上の点に対応する3次元上の点を作成する。
    world_points = np.zeros((rows * cols, 3), np.float32)
    world_points[:, :2] = np.mgrid[:cols, :rows].T.reshape(-1, 2)
    print('world_points shape:', world_points.shape)  # world_points shape: (54, 3)

    for img_pt, world_pt in zip(image_points[0], world_points):
        print('image coordinate: {} <-> world coordinate: {}'.format(img_pt, world_pt))

    # 画像の枚数個複製する。
    imagenum = 1
    object_points = [world_points] * imagenum

    np.set_printoptions(suppress=True)
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(object_points, image_points, (1920, 1017),None,None)
    print("camera instristics mtx = {}".format(mtx) )

PythonからOpenCVを使ってのキャリブレーションは、こちらのサイトを参考にさせていただきました。 http://pynote.hatenablog.com/entry/opencv-camera-calibration

キャリブレーションで得たパラメータを使ってOpenMVGを叩くコードはこんな感じです。 ほぼチュートリアルのままですが。 https://gist.github.com/javoren/dd7075211776ee51ac45c06b3827d905

これを実行すると

  • imagesディレクトリの画像をもとに三次元点群を生成する
  • tutorial_out/reconstruction_globalディレクトリに結果を出力する

という感じに動きます。

できあがったデータはこんな感じです。

f:id:Catalina1344:20191007140654p:plain

前回のエントリではキャリブレーションパラメータが間違えていて、何が何やら状態でしたが、これなら何か見えてきそうな気がしますね。

openMVS形式にファイルを変換する

できあがったデータはopenMVSにもっていきたいので、プロジェクトを変換します。

openmvgのコンテナ内で次のコマンドを実行します。

cd /opt/openMVG_Build/Linux-x86_64-RELEASE
openMVG_main_openMVG2openMVS -i /mnt/work/tutorial_out/reconstruction_global/sfm_data.bin /mnt/work/openMVS/scene.mvs -o /mnt/work/scene.mvs

これで/mnt/work/scene.mvsという、OpenMVSで利用可能なファイルが完成しました。

OpenMVSの用意

openmvsのリポジトリ公式の手順をもとにdockerコンテナを作成します。 まとめるのが下手なので、一旦個別にRUNしていくだけの簡単なものです。 rootディレクトリにすべて展開しちゃってるのはご愛敬ということで。。。

# Use Ubuntu 18.04 (will be supported until April 2023)
FROM ubuntu:16.04

# Add openMVG binaries to path
ENV PATH $PATH:/opt/openMVG_Build/install/bin

# Get dependencies
RUN apt-get update && apt-get install -y \
  cmake \
  build-essential \
  graphviz \
  git \
  coinor-libclp-dev \
  libceres-dev \
  libflann-dev \
  liblemon-dev \
  libjpeg-dev \
  libpng-dev \
  libtiff-dev \
  python-minimal; \
  apt-get autoclean && apt-get clean

##Prepare and empty machine for building:
RUN apt-get -y install build-essential git mercurial cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev libxmu-dev libxi-dev
RUN hg clone https://bitbucket.org/eigen/eigen#3.2
RUN mkdir eigen_build
RUN cd eigen_build && cmake . ../eigen
RUN cd eigen_build && make && make install

# boost, opencv
RUN apt-get -y install libboost-iostreams-dev libboost-program-options-dev libboost-system-dev libboost-serialization-dev
RUN apt-get -y install libopencv-dev
RUN apt-get -y install libcgal-dev libcgal-qt5-dev

##VCGLib (Required)
RUN git clone https://github.com/cdcseacave/VCG.git vcglib
RUN apt-get -y install libatlas-base-dev libsuitesparse-dev
RUN git clone https://ceres-solver.googlesource.com/ceres-solver ceres-solver
RUN mkdir ceres_build
RUN cd ceres_build && cmake . ../ceres-solver/ -DMINIGLOG=ON -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF
RUN cd ceres_build && make && make install

RUN apt-get -y install freeglut3-dev libglew-dev libglfw3-dev
RUN git clone https://github.com/cdcseacave/openMVS.git openMVS
RUN mkdir openMVS_build 
RUN cd openMVS_build && cmake . ../openMVS -DCMAKE_BUILD_TYPE=Release -DVCG_ROOT="$main_path/vcglib"

RUN cd openMVS_build && make && make install

このコンテナを起動するdocker-compose.ymlは次のようになりました。

version: "3"
services:
  openmvg:
    build:
      context: ./openMVG
      dockerfile: ./Dockerfile
    image: openmvg
    container_name: "openmvg"
    tty: true
    volumes:
      - "./work/:/mnt/work" 
      - "./work/images/:/opt/openMVG_Build/Linux-x86_64-RELEASE/images"

  openmvs:
    build:
      context: ./
      dockerfile: ./dockerfile
    image: openmvs
    container_name: "openmvs"
    tty: true
    volumes: 
      - "./work/:/mnt/work"
      - "./work/images/:/openMVS_build/bin/undistorted_images"

このコンテナに入って、次のコマンドを実行していけば三次元再構成ができます。

cd /openMVS_build/bin
# 密な点群にする
./DensifyPointCloud /mnt/work/openMVS/scene.mvs

密な点群にしてみるとこうなります。 f:id:Catalina1344:20191007140818p:plain

点の数がずいぶんと増えたので、キャラクタも武器も目視確認できますね。

cd /openMVS_build/bin
# メッシュにする
./ReconstructMesh /mnt/work/openMVS/scene_dense.mvs

点のままでは3DCGソフト上で扱いにくいのでメッシュにします。

f:id:Catalina1344:20191007141559p:plain

Blenderへのインポート時点でテクスチャがはがれてしまいましたが、ひとまず読み込みできました。

試しにキャラクタ部分のみ残してみるようにしたところ、およそ20万ポリゴンでした。

感想と今後の展望

ゲームのスクリーンショットをもとに三次元再構成ができることの確認ができました。 少し手直ししてあげれば3Dプリンタで出力することもできそうです。

ただし、今回の手法は弱点も存在していて、「光沢部分が凸なポリゴンとして認識されてしまう」という課題があります。 これはゲーム画面のレンダリング時は光沢として白色になるわけですが、分析時はその白色は凸のためにできたのか、もともと白かったからなのかという判断がうまくつかないからのようです。

もう少し入力データを工夫して色々と試してみたいと思います。

また、今回利用した手法(SfM)以外にも、DeepLearningを用いた手法もあるようなので、そちらも試していきたいと思います。

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

OpenMVGを試してみる(環境構築+α)

夏が終わって秋らしい気候になってきました。秋は色々と創作意欲を刺激されるイベントが多いので、今のうちから準備をやっていきたいと思います。

OpenMVGとは

公式はこれですね。 github.com

MultiViewGeometryの略で、複数視点から三次元ジオメトリを再構成しようってやつです。

技術的なカテゴリとしてはフォトグラメトリとかSLAMに近いもののようです。

これらの細かい区別はよくわかりません。あしからず。

モチベーション

なんでこういうのに興味を持ったかというと、ARアプリと3Dプリンタを組み合わせるときに大事な技術になるのではないかという思いからです。

以前のエントリで、ゲーム中の登場アイテムを3Dプリンタで制作してARエフェクトを出してみるってのをやりました。

catalina1344.hatenablog.jp

これの最大の問題点は工数がかかりすぎという点で、趣味レベルで余暇を使ってやっているとはいえざっとこれくらいかかりました。

  • モデリング2か月
  • プリント, 仕上げ塗装 それぞれ1か月
  • ARアプリ1か月

スケールアップを考えたとき、一番ネックになりそうなのはモデリングかなと思っていて、ここをツール群を使って自動化したいなと考えました。

で、画像からの3Dモデル再構築ってどうなのだろうってことで試してみました。

もちろんゲーム中のSSを解析したりする行為は著作権やソフトウェア利用許諾に記載されているリバースエンジニアリングの禁止条項にかかる懸念はありますので、それはその問題として別途解決しておく必要はあります。

では早速try。

OpenMVG実行環境をdockerで作る

まずは環境作りからです。

ビルド環境

環境はいつものです。 WIndows10-pro上のdocker desktop for winwowsを使用します。

公式リポジトリをcloneしてきて、ビルドパラメータを修正します。 dockerfile末尾でmakeしている箇所の-jオプションを削除します。

# Build
RUN mkdir /opt/openMVG_Build; \
  cd /opt/openMVG_Build; \
  cmake -DCMAKE_BUILD_TYPE=RELEASE \
    -DCMAKE_INSTALL_PREFIX="/opt/openMVG_Build/install" \
    -DOpenMVG_BUILD_TESTS=ON \
    -DOpenMVG_BUILD_EXAMPLES=OFF \
    -DFLANN_INCLUDE_DIR_HINTS=/usr/include/flann \
    -DLEMON_INCLUDE_DIR_HINTS=/usr/include/lemon \
    -DCOINUTILS_INCLUDE_DIR_HINTS=/usr/include \
    -DCLP_INCLUDE_DIR_HINTS=/usr/include \
    -DOSI_INCLUDE_DIR_HINTS=/usr/include \
    ../openMVG/src; \
#    make -j 4;
    make;

jオプションを削除する意味

まずmakeのjオプションはジョブの個数を指定するオプションです。デフォルトの指定だと4プロセス平行に走らせるという意味です。

大雑把にいうと4CPUあればいい感じに走るわけなのですが、Docker for windowsのデフォルト設定ではコンテナに1CPUしか割り当てていません(うちの環境だけかも?)

この1CPU環境でjオプションを指定しているとコンテナごと応答なし状態になってしまったので、1プロセスでやってしまうということにしました。

起動パラメータ

また、このdockerfileのあるディレクトリの一つ上の階層にdocker-compose.ymlファイルを配置します。 openMVGのdockerfileが生成するコンテナの起動パラメータをまとめただけのものです。

version: "3"

services:
  openmvg:
    build: 
      context: ./openMVG
      dockerfile: ./Dockerfile
    image: openmvg
    container_name: openmvg
    tty: true
    volumes:
      - ./work:/mnt/work/

動作確認

チュートリアルらしいファイルが置かれているので叩いてみます。

対象ファイルは/opt/openMVG/src/software/SfM/tutorial_demo.py.inですが、そのままではパス指定がダメなので少し編集します

Linux上で編集したりするの大変なので、/mnt/workにでもコピーしてきて編集するのが楽です。(docker-composeでホスト側にマウントするよう指定した)

冒頭の部分を次のように書き換えます。

# Indicate the openMVG binary directory
#OPENMVG_SFM_BIN = "@OPENMVG_SOFTWARE_SFM_BUILD_DIR@"
OPENMVG_SFM_BIN = "/opt/openMVG_Build/Linux-x86_64-RELEASE/"

# Indicate the openMVG camera sensor width directory
#CAMERA_SENSOR_WIDTH_DIRECTORY = "@OPENMVG_SOFTWARE_SFM_SRC_DIR@" + "/../../openMVG/exif/sensor_width_database"
CAMERA_SENSOR_WIDTH_DIRECTORY = "/opt/openMVG/src/software/SfM/" + "/../../openMVG/exif/sensor_width_database"

これでOK。 実行は python tutorial_demo.py.inです。建物の画像をダウンロードしてきて、解析した結果をtutorial_outディレクトリに出してくれます。 ply形式なので、meshlabとかで見られます。

少しいじってみる(失敗)

チュートリアルのコードを見ると、GitHub上から画像データをダウンロードしてきて、それをもとにSfMをやってるということがわかります。 せっかくなので自前のデータを用意して差し替えてみましょう。 コードを次のようにしてみました。

def get_parent_dir(directory):
    return os.path.dirname(directory)


#os.chdir(os.path.dirname(os.path.abspath(__file__)))
#input_eval_dir = os.path.abspath("./ImageDataset_SceauxCastle")
## Checkout an OpenMVG image dataset with Git
#if not os.path.exists(input_eval_dir):
#  pImageDataCheckout = subprocess.Popen([ "git", "clone", "https://github.com/openMVG/ImageDataset_SceauxCastle.git" ])
#  pImageDataCheckout.wait()
#
#output_eval_dir = os.path.join(get_parent_dir(input_eval_dir), "tutorial_out")
#input_eval_dir = os.path.join(input_eval_dir, "images")
#if not os.path.exists(output_eval_dir):
#  os.mkdir(output_eval_dir)

input_eval_dir = "./images"
output_eval_dir = "./tutorial_out"

if not os.path.exists(output_eval_dir):
  os.mkdir(output_eval_dir)

input_dir = input_eval_dir
output_dir = output_eval_dir
print ("Using input dir  : ", input_dir)
print ("      output_dir : ", output_dir)

step5あたりで The input SfM_Data file "./tutorial_out/reconstruction_global/sfm_data.bin" cannot be read.と言われてしまいます。 このエラーの原因は、直前のプロセスまでに生成されているべきSFM_data.binが存在しないために発生するエラーです。

なぜsfm_dataが生成されていないかというと、今回はゲーム中の適当な画像を使ったのでexifヘッダが存在しないためでした。 どうもOpenMVGの実装上、jpegフォーマットのexifヘッダからカメラ内部パラメータを推測しているようです。 (カメラパラメータは、例えばテストデータとして使っているリポジトリにはK.txtなどといった形で記載されているものです。)

さらに少しいじってみる(少し進展した)

step1でsfmdata\jsonを生成していますが、ここでカメラ内部パラメータを与えることができます。 適当に引っ張ってきた値なので、使用する画像に合わせたパラメータに置き換える必要があります。

kp = "2905.88;0;1920;0;2905.88;1017;0;0;1"

print ("1. Intrinsics analysis")
#pIntrisics = subprocess.Popen( [os.path.join(OPENMVG_SFM_BIN, "openMVG_main_SfMInit_ImageListing"),  "-i", input_dir, "-o", matches_dir, "-d", camera_file_params, "-c", "3"] )
pIntrisics = subprocess.Popen( [os.path.join(OPENMVG_SFM_BIN, "openMVG_main_SfMInit_ImageListing"), "-i", input_dir, "-o", matches_dir, "-k", kp, "-d", camera_file_params, "-c", "3"] )
pIntrisics.wait()

これで、plyファイルが出力されるようになりました。meshlabで見てみるとこんな感じになりました。 f:id:Catalina1344:20190923131225j:plain うーん、よくわからないですね。

父の日ですね

たまには文章書かないと日本語忘れてしまいそうです。職場でも人と話することもないので。かたりぃなです。

 

ブログで技術以外のことを書くのは初めてかもしれません。どちらかというと政治&過去語りな内容。

興味のない人はそっと閉じてくださいね。

 

まず、私の父はもういません十数年前に他界しました。

そんな父は私がプログラマになるための最後の助けをしてくれました。

 

上京・進学のとき

時代は20世紀末期。就職氷河期が始まっているので大学進学したところで就職先があるかどうかもわからないご時世。

国公立ならあるいは?という選択肢もありましたが、浪人などしようものなら阪神淡路大震災の影響で悪化していた家庭環境。居場所がないのは自明でした。実家は四国なので、震災の直接的な影響はなかったのですが、親戚関連とかでいろいろあったらしいです。

 

公務員という選択肢も考えましたが、当時は就職氷河期の折。公務員の競争率は跳ね上がっていましたし、田舎なので縁故採用らしき枠がほとんどを持って行ってしまうんですね。悲しい現実です。

 

そんなこんなで、私がお金を使ってしまうと弟や妹が進学できなくなってしまう。国公立に賭けるみたいなギャンブル打つくらいなら専門学校で。と進路を決めました。

国公立に余裕で行ける学力がなかったのは自分の勉強不足だったんだろうなと思います。

専門学校となると今まで視野に入れてなかったので色々調べる必要がありました。

私がいた地方の専門学校は一族経営だったり、人脈だけで成り立ってたりと、そういう所だったので、こうするほかなかったという状況だったので場所は東京となりました。

母は大反対でしたが、父は快諾とまでは言わないまでも「やれるだけやってみ。無理なら帰ってこい」と言ってくれました。

 

上京の日は父がついてきていました。寮母さんに挨拶してから、西友で生活に必要なものを買ってくれたのを記憶しています。「バイトで頑張って貯めたみたいだけど、それは生活のためじゃなくて自分のやりたいことをやるために使いなさい。」そんなことを言ってた気がします。

 

当時の私の地方のバイト代は時給600が平均値でしたから、ほとんど溜まっていない状況でした。あの生活基盤を作ってくれたお金がなかったら、あっというまに困窮していたと思います。父に感謝です。

 

パソコン

進学と同時にパソコンを買いたかったのですが、そんなお金はどこにもありませんでした。進学後はなんとかしようとバイトしつつも試行錯誤の毎日でした。

通学時間中に本を読んで、紙にコードを書いて、学校のPCで打ち込むという生活を半年近く続けました。バイトも掛け持ちしていたので当時の睡眠時間は3時間前後だったと思います。今にしてみればよく生きてたなと。

専門学校の一年目が終わるころにはC++, DirectXくらいはいじれるようになっていましたが、やっぱりパソコンなしでは限界がありました。

ダメもとで父に電話したところ「ボーナスが少し入った。少しだけなら」と。

当時としてはなかなかのスペックの中古PCとモニタ(Gateway製, Pentium100MHz, RAM=96M)みたいなマシンでした。

おかげで制作発表会では実写を取り込んだ2D格闘ゲームを展示できて反響もありました。

反響があったというのが嬉しくて、寝る間も惜しんで色々プログラムを作ってた気がします。

父のおかげです。当時の私はまだ19歳でした。

 

成人式

もともと出るつもりはなかったのですが、アルバイト先の仲間から出たほうがいいといわれ、休みももらえたので出ることにしました。

成人式前日に実家に帰ってみると、母が封筒をくれました「待ってたよ。父さんから預かったお金、スーツ代。リクルートスーツじゃせっかくの成人式が台無しだからね」と。

成人式に多少なりともまともなスーツで参加できたのも父のおかげです。

その後も何度か実家には帰ったりはしました。

父から聞いた最後の言葉は「気をつけてな。悪いやつに騙されるなよ。お前は人が良すぎる」でした。

 

父の他界

世間がサブプライム問題とか賑わっていた時期でした。

当時の会社の同僚の結婚披露宴の最中に電話が鳴りました。父が倒れたと。

すぐ羽田から四国へ飛び、病院に着いてみると父は手術中でした。

待っている間に状況を聞いてみると、あまりよくないようでした。

一度は医大病院に運ばれたが、医者が居ないからと県立病院へ移送、一刻も早く手術をしないといけない状況なのに、ここにも医者が居ないから手術まで数時間待ち。そんな状況でした。

幸いにも手術は成功しました。

 

私は安心して東京へ戻り、次の日には再び職場で仕事をしていました。「元気になったら好物でも買って行ってやろう。一緒に酒飲んだこともなかったな」なんて呑気に考えてました。

3日後、再び電話がなりました。合併症とのこと。もうダメだと。

私は喪服を用意して再び四国へ飛びました。「すぐ息を引き取るわけではないけれども、回復の見込みは極めて低い。」そんなお医者さんからのお話でした。

私はせめてその時くらい一緒にいてやろうと一週間有給を入れました。

 

当時の私の職場は下請け企業なのでリーマンショックの余波を受けての政治争い(受注争い)が大変ですが、そんなことよりも大切なことがあると思っています。

 

親のことが好きとか嫌いとか、自分の立場がとか、会社の業績がとか。

そんなものどうでもよかった。ただ今まで助けてくれた父の最期を看取ることくらいしたかったのです。

 

有給の期間が過ぎ、母が告げました「あんた、リーマンショックの影響で大変なんだから帰りな。あとはなんとかする。」と。

強情な母なのでそれを受けて、次の日に東京に戻ることで話がついた夜。病院から電話でした。

「血圧が低下しています。ご家族や親類の方を。」

没年50歳後半でした。

 

職場に電話して忌引き休暇の連絡、葬儀屋の手配、病院の請求書の処理。

葬儀は私が喪主を務め無事に終えることができました。

火葬のボタンを押すとき「今までありがとうございました」と。

 

後悔

せめて一緒に酒くらい呑んでやればよかった。嫁に合わせてやりたかった。

そんな思いで四十九日を終え、やっと本格的に職場復帰したころでした。

やはり長期間職場を不在にすると政治的に不利になりますね。

現場としてはそんな負荷を押し付けられたと思うでしょうし、総務や人事は知ったこっちゃないという状況です。

心理的にも耐えられないと思い、退職を申し出ました。しばらく田舎で父の残してくれたものの処理をしようと考えていました。

 

ここで父の最期の言葉を思い出しておけばよかったと悔やまれます。

離職票の退職日や保険の加入などを偽装され、金額的にも精神的にもかなりの損失を被りました。

ストレスからの暴飲暴食からの胃潰瘍で出血多量となって死にかけましたが、今も私はなんとか生きてます。

 

もう10年以上前のことです。

 

個人的に思うこと

私は親があまり好きではありませんでした。。

ただ、今にして思えば、生活が苦しい中で、父は本当に必要なものを選んで私に与えてくれていたんだと思います。

父が他界したとき、葬儀屋さんからいただいた言葉に救われた気がします。

「皆さん、親にお礼してないと悔やまれます。ただね、子供が親にするお礼というのはですね、10歳まで一緒に生きるというのが最初にして最高のお礼なんですよ。10歳過ぎたら、感謝しつつ自分の道を進んでいく。それで充分なんですよ」と。

確かにそれくらいまでの父の姿が一番記憶に残っている気がします。それを過ぎると反抗期も始まりますし。

 

政治に対して思うこと

法の下の平等というのは何なのだろう?と父の他界を機に本気で考え続けています。

 

ただ、一つだけ不平等だと言えるのは父の他界です。

小泉政権のときに「地方切り捨てだ!」などと叫ばれていましたが、私は他人事のように思っていました。

地方切り捨てが行われたらどうなるのかも考えず、自分の地方がその切り捨てられる側になるなんて考えず。

そして「医者がいないから手術ができなかった」という言葉をまさか父が受けることなるなんて考えてもいませんでした。

 

父はこのことを教えてくれたのかもしれません。

 

総括

日本の政治によって私の父は命を奪われた。私は今でもそう思っています。

ただ、憎しみは何も生まない。だから私は悲しむ人が少しでも減るような社会になってほしい。そう願います。

今の日本社会を見ていると、皆が自分のことで精一杯で余裕がないなと感じています。

 

だとしたら、それは今の政治であったり、社会、働き方、会社、色々なしくみが限界を迎えているのではないか?と。

 

少し暗い話になりましたが、できることなら、全世界から苦しみが無くなって幸せになれる世界が訪れるといいなと思っています。

これを仏教の言葉で「大欲得清浄」というらしいです。意味は大きな欲は清浄を生む。

自分だけ良ければそれでよいというのではなく、もっと大きな目で物事を見てみると何か大切なものが見えてくるかもしれません。

 

私にそれを教えてくれたのは父の命でした。

 

南無大師遍照金剛

MTGのカード情報の傾向分析を行う(準備編)

GWが終わって、しばらく祝日なしの日が続きます。 私は毎年この時期を「6月砂漠」と呼ぶことにしています。かたりぃなです。

今回はMTGのカードのテキストデータを分析してみようと思います。 画像処理ばかりやってると数式相手になることが多いので、時には気分転換も必要です。

目的とか

MTGのカードは時代によって流行り廃れがあります。 たとえば、去年はすごく流行っててカードショップでも高額で取引されていたカードが、今は全然見かけないとか。

これを分析することができれば、何かデータが見えてこないかなというのが今回の目的です。

テキストマイニングは初めてなので、できるだけ手軽に既存のツールやライブラリを組み合わせて実現していこうと思います。

まず今回は環境を作り上げて軽く動かすところまでやってみます。

環境

いつものです

  • ホストOS : Windows10 Pro
  • 仮想化 : Docker desktop for windows
  • ゲストOS : Linux
  • 分析基盤 : Elasticsearch
  • GUIフロントエンド : Kibana

コンテナ一式を立てる

Elasticsearchを使うためのコンテナ一式を立てます。

今回のElasticsearchの用途は次のとおりです

  • 他ホストからRESTでデータを投げ込んでもらう
  • 日本語解析にはkuromojiを使う
  • Kibanaで確認する

早速コンテナを立てるところからやってみます。

docker-compose

docker-compose.ymlはこうなりました。

version: "3"
services:
  elasticsearch:
    build: 
      context: .
      dockerfile: ./kuromoji.dockerfile
    image: elastic
    ports: 
      - "9200:9200"
      - "9300:9300"
    environment:
      - discovery.type=single-node
    networks: 
      - elastic-stack

  kibana:
    image: docker.elastic.co/kibana/kibana:6.5.4
    ports:
      - "5601:5601"
    networks: 
      - elastic-stack

networks:
  elastic-stack:

kuromoji.dockerfile

elasticsearchの日本語解析プラグインとしてkuromojiを使いたいので、それを組み込むためのdockerfileを書きます。

上記docker-compose.ymlが参照するkuromoji.dockerfileはこうなりました。

インストールするだけでした。

FROM docker.elastic.co/elasticsearch/elasticsearch:6.5.4

# 公式マニュアルにあるkuromojiをインストールする。
# https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html
RUN elasticsearch-plugin install analysis-kuromoji

これでelasticsearchの用意ができました。

起動確認

docker-compose up -dして起動確認します。うまくいかないときはdocker psで生存確認したり、docker-compose logsでログを見たりします。

うまく起動したら、kuromojiが有効になっているか確認します。

curl -X GET 'http://localhost:9200/_nodes/plugins?pretty' 

このあたりの基本的な操作はこちらのサイトを参考にさせていただきました。 http://pppurple.hatenablog.com/entry/2017/05/28/141143

Elasticsearchに投入するデータを用意する(MTGのカードデータ)

以下の手順でデータを注入します。

  1. MTGのカードデータをMTG公式のRESTから取得し、ファイルに保存
  2. 保存したファイルを整形して、ElasticsearchのRESTに渡す

データのダウンロードとElasticSearchへの注入を分けた理由は、MTG-APIが外部サーバだからというのと、個人的に空いた時間にちまちまとコードを書きたかったからです。

ネットが繋がらない状態でもローカルにファイルを持っていれば作業できるので。

MTG公式からのカードデータ取得と保存

python3でいきます。こうなりました。 以前DeepLearningのときに試したコードの引用ですね。

# -*- coding: utf-8 -*-

from mtgsdk import Card
from mtgsdk import Set
from mtgsdk import Type
from mtgsdk import Supertype
from mtgsdk import Subtype
from mtgsdk import Changelog

import json
import argparse
import pickle
import urllib
import os
import codecs

def print_allsets():
    print("================ cardset list ================")
    allsets = Set.all()
    for cardset in allsets:
        print('{}  ({})'.format(cardset.code, cardset.name) )
    print("================ cardset list ================")

def main():
    parser = argparse.ArgumentParser(description='mtg image download')
    parser.add_argument('--setcode',
        action='store', type=str, nargs=None, const=None, 
        default=None, choices=None,
        help='download card images from set code')
    args = parser.parse_args()    

    if args.setcode == None:
        print_allsets()
        parser.print_help()
        exit()

    cardlist = Card.where(set=args.setcode).where(language='japanese').all()
    workdir = os.path.join( '.', 'card-texts', args.setcode)

    print("{} card num = {}".format(args.setcode, len(cardlist) ) )
    print("image download to {}".format(workdir) )

    if not os.path.exists(workdir):
        os.makedirs(workdir)

    for index, card in enumerate(cardlist):
        # 有用な情報は多々とれそうだが、一旦日本語の部分だけ抽出してみる
        for foreign_names in card.foreign_names:
            if foreign_names['language'] == 'Japanese':
                # ファイル名にはユニークなイテレーションの数値を使う
                out_file_name = '%s/%d%s' % (workdir, index, '.txt')
                print("{} ( {} )".format(out_file_name, card.name) )
                out_file = codecs.open(out_file_name, "w", 'utf-8')
                out_file.writelines( json.dumps(foreign_names, ensure_ascii=False) )
                out_file.close()

    print("download complete")

if __name__ == '__main__':
    main()

使い方はこんな感じです

python download_cardtext.py --setcode=DOM

こうするとカレントディレクトリに./card-texts/DOMディレクトリが作られ、そこにドミナリアのカード情報一覧が保存されます。

たとえば./card-texts/DOM/0.txtはこんな内容になります。json形式ですね。

{"name": "アカデミーのドレイク", "text": "キッカー{4}(あなたはこの呪文を唱えるに際し、追加で{4}を支払ってもよい。)\n飛行\nアカデミーのドレイクがキッカーされていたなら、これは+1/+1カウンターが2個置かれた状態で戦場に出る。", "flavor": null, "imageUrl": "http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid=444273&type=card", "language": "Japanese", "multiverseid": 444273}

elasticsearchにデータを入れていく

ElasticSearchの前準備として次の2つを行います。

  • kuromoji日本語解析器を設定します
  • MTGのカードを解析するためのマッピング設定を行います。

簡単に説明します、前者の日本語解析器はいわゆる形態素解析してくれるモジュールです。

形態素解析 is 何?

私はその分野の専門家ではないので、今回の用途だけに絞って簡単に説明します。

興味ない人は読み飛ばしてください。

上記jsonのままでは、たとえばtextフィールドの長い文字列からの検索になってしまい、単語単位での分析がうまくできません。

おおざっぱに解釈すると、単語単位に分割する方法の一つが形態素解析らしいです。

そんな面倒な処理必要なの?って思ったので軽く調べたところ、

  • 英語やドイツ語などであれば、空白で区切れば単語単位になる
  • 日本語では空白で区切るという概念はない

あたりが要因のようです。

「日本語は助詞で区切ればいいのでは?」と思いますが、境界判別という問題があって、助詞がどこかわからない文字列だと解釈に困ります。

そのあたりを頑張ってくれるモジュールということなので、便利に使わせてもらいましょう。

設定はこうなりました。これをmapping.jsonとして保存しておいて

{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "kuromoji_user_dict": {
            "type": "kuromoji_tokenizer",
            "mode": "extended"
          }
        },
        "analyzer" : {
          "my_analyzer" : {
            "type" : "custom",
            "tokenizer" : "kuromoji_tokenizer"
          }
        }
      }
    }
  },
  "mappings": {
    "cards" : {
      "properties": {
        "name": {
          "type": "text",
          "fielddata": true,
          "analyzer": "kuromoji",
          "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
          }
        },
        "text": {
          "type": "text",
          "fielddata": true,
          "analyzer": "kuromoji",
          "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
          }
        },
        "flavor": {
          "type": "text",
          "fielddata": true,
          "analyzer": "kuromoji",
          "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
          }
        }
      }
    }
  }
}

こうやってelastic-searchに投げます。

curl -X PUT 'localhost:9200/mtg?&pretty=true' -d @mapping.json -H 'Content-Type: application/json'

うまくいけばこんなjsonレスポンスが返ってきます。

{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "mtg"
}

下準備ができたのでデータ注入します。

Elasticsearchに入力するデータは少々加工する必要があるので、スクリプト書きました。

スクリプトの概要はこんな感じです。

  • ローカル環境に保存してある特定のカードセットすべてのファイルを読み込んで、結合
  • ElasticSearchのために各カード情報にヘッダ(のようなもの)を付与
  • 動作確認用にall_<カードセット名>.txtとして保存
  • elasticSearchに入れる

思ったより簡単にできたのでブログに直接乗せてしまいます。

# -*- coding: utf-8 -*-

import json
import argparse
import urllib
import os
import codecs

from mtgsdk import Set

def print_allsets():
    print("================ cardset list ================")
    allsets = Set.all()
    for cardset in allsets:
        print('{}  ({})'.format(cardset.code, cardset.name) )
    print("================ cardset list ================")

def listup_carddata(workdir):
    for filename in os.listdir(workdir):
        fullpath = os.path.join(workdir, filename)
        if os.path.isfile(fullpath):
            yield fullpath

# 1件のエントリをelasticsearchに転送できるよう加工する
def deform_carddata(data):
    return '{ "index": {"_index":"mtg", "_type":"cards" } }\n' + data + '\n'


def collect_data(workdir):
    allcard = ''
    for filepath in listup_carddata(workdir):
        f = codecs.open(filepath, 'r', 'utf-8')
        data = f.read()
        allcard += deform_carddata(data)
    return allcard

def register_es(elasticsearch_host, indexname, json_data):
    headers = {'Content-Type': 'application/json',}
    api = '/_bulk/?pretty'
    url = urllib.parse.urljoin(elasticsearch_host, indexname) + api
    print("request url = {}".format(url) )
    req = urllib.request.Request(url=url, data=json_data.encode(), headers=headers )
    result = urllib.request.urlopen(req)
    print(result)

def main():
    parser = argparse.ArgumentParser(description='mtg image download')
    parser.add_argument('--setcode',
        action='store', type=str, nargs=None, const=None, 
        default=None, choices=None,
        help='download card images from set code')
    args = parser.parse_args()

    if args.setcode == None:
        print_allsets()
        parser.print_help()
        exit()

    workdir = os.path.join( '.', './card-texts', args.setcode)
    if not os.path.exists(workdir):
        exit()

    # データを集めて
    allcard = collect_data(workdir)
    allcard += '\n\n'  # esとしては末尾に改行が必要なので

    # データ形式があってるか目視確認用のファイルを保存
    f = codecs.open('all_'+args.setcode+'.txt', 'w', 'utf-8')
    f.write(allcard)
    f.close()

    # ElasticSearchに入れる
    elasticsearch_host = 'http://localhost:9200'
    es_index = 'mtg'
    register_es(elasticsearch_host, es_index, allcard)


if __name__ == '__main__':
    main()

動かします。

python register_cardlist.py --setcode=DOM
request url = http://localhost:9200/mtg/_bulk/?pretty
<http.client.HTTPResponse object at 0x000001FCA430A048>

エラーチェックがすっぽ抜けてますね。。。 HTTPResponseオブジェクトが返ってきてとりあえずいけました。

API叩いていても寂しいので、テキストマイニングっぽくワードクラウドを作ってみます。

これでkibanaから日本語で検索したり色々できるようになります。

ElasticSearchでワードクラウドを作る用意

kibanaで操作していきます。

インデックスパターンを作る

  1. kibanaの画面左のManagementを選択
  2. ElasticSearch の Index Pattern を選択
  3. インデックス名に"MTG"を入力
  4. あとは適当にOKおしていく

こんだけです。

このときの注意点で、フィールド名一覧の管理画面でtext,name,flavorの各フィールドの「Aggregatable」が有効になっていることを確認しておきます。

詳しいことはわからないのですが、どうやらRAM上に載せておいて色々な処理に使える(たとえば今回のワードクラウド)ようです。

データが入っているか確認する

  1. kibanaの画面左のDiscoverを選択
  2. 目視確認

それっぽいデータが入っていればOKです。

ワードクラウドを作る用意

ElasticSearchではタグクラウドというらしいです。

タグクラウドを作るための基本設定をしていきます。

  1. kibanaの画面左のVisualizeを選択
  2. "+ Create a visualization"を選択
  3. ビジュアライズ手法の最下部にある'Tag Cloud'を選択
  4. From a New Search, Select IndexのNameから'MTG'を選択(これはkibanaで最初に作ったインデックスの名前です)

これでタグクラウドの設定画面まできました。

タグクラウドを出してみる

タグクラウドの設定画面で色々設定してみます。

テキストマイニングは初めてなので、まずは雰囲気を掴んで面白いと思うところが大事だと思います。

設定値はこんな感じにしました。

  • Buckets
    • Aggregation = Terms
    • Field = text
    • Order By = metric:count
    • order = desenc
    • size = 40

こんな感じのできました。 f:id:Catalina1344:20190528133913p:plain

なかなか楽しいですね。

ぱっと見たところ、MTG固有の用語がうまく分析できてなさそうです。

  • プレインズウォーカー = (プレイン / ズウォーカー)
  • トークン = (トー / クン)
  • トレイリア(地名) = (トレイ / リア)

ひとまずテキストマイニングの土台ができたと思うので一旦良しとします。

感想と今後の展望

テキストの分析がうまくいっていないのは形態素解析の都合で、未知の単語をうまく解釈できてないことによるものです。

今回の用途(MTG専用の分析をやりたい)であれば、kuromojiの辞書に追加してあげるだけでよさそうです。

また単語の出現傾向を分析するのも色々楽しそうです。

今回のタグクラウドをいじっていると歴史的アーティファクトがセットで現れることが多いです。

ゲームが分かる人からみれば「ドミナリアでテストしたから当然でしょ」って内容ではありますが、こういう「当たり前だね」っていうのをデータで示しておくことは分析では重要なことだと思います。

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

WinRTを試してみる

桜の花も散っていき、春から初夏への移り変わりを感じます。かたりぃなです。

今回は新年度ということで、新しいことを試してみようと思います。C++/WinRTです。

C++/winRTを使う理由

Hololens用のUnityのネイティブプラグインを書こうと色々調べていたら、C++/CX終わるよみたいなアナウンスに気づきました。

https://docs.microsoft.com/ja-jp/cpp/cppcx/visual-c-language-reference-c-cx?view=vs-2019

要約すると、「C++17とWinRTを使うと、幸せになれるよ」ってことですね。

というわけで、今後のことも考えてwinrtを試してみます。ダメだったら戻ればいいかと思って試したところ、かなりイケてると思います。

なのでWinRT/C++でいきます。

WinRTとは

winRTはc++17の機能を使うことでC++/CXよりエレガントに記述できます。 実際に書いてみるとわかるのですが、C++使いなら直感的に記述できる感じでした。

プラットフォームの観点からは

  • UWPアプリを書ける
  • ネイティブアプリも書ける(従来のデスクトップアプリ)
  • Unityプラグインとしてもいける(はず?)

コードを書く観点から

  • 非同期操作が扱いやすくなっている(concurrencyTaskなんて無かった)
  • プロパティへのアクセスが直感的
  • イベントハンドラ登録が直感的に書ける
  • COMへの参照はwinrtの中で良しなにやってくれる

とかでしょうか。 特に非同期操作がラクになったのが大きくて、意味不明なテンプレートのエラーを見る回数が減ったと思います。

今回はまだ非同期操作まわりはよくわかってないので、同期的な処理で完結しています。

実際にwinrtでコード書いてみる

最終目標はunity ネイティブプラグインです。

なので、visual studio プロジェクトとしてはDLLになります。

また、DLLのテスト用にコンソールアプリケーションを作成します。

UWPアプリケーションからDLLを呼び出すテストはまた今度。。。(Hololens実機でのネイティブプラグイン相当)

環境

いつものです。

環境設定

Visual Studio 2017に色々いれたのですが、試行錯誤していたせいで失念してしまいました。

VisualStudioでオンラインのWinRTテンプレートをインストールか、VisualStudio のインストーラからコンポーネントを指定したかもしれません。

設定が終わると新規作成プロジェクトからC++/WinRTが選択できるようになります。

プロジェクト構成

VisualStudioのプロジェクトとしては2つ作ります。 1つはc++/winRTを使用したDLLそのものので、もう一つはテスト用プロジェクトです。

ソリューションにこの2つのプロジェクトを配置することにします。

このDLLは最終的にUnity側で利用するわけですが、テストのたびにUnityとVSを行ったり来たりしてのデバッグは大変なので、テストプロジェクトで普通のDLLとして扱えることを確認しておきます。

WinRTのDLLプロジェクトを作成する

WinRTのDLLプロジェクトのテンプレートが手元にないので、手作業で作ります。

具体的には次の手順です。

  1. 新規プロジェクト作成から、VisualC++ -> Windowsデスクトップ -> DLL を選択
  2. プロジェクトの設定値を変更する

これでWinRTでDLLが作成できるようになりました。

WinRTを利用するためのプロジェクト設定の手順はこちらのサイトを参考にさせていただきました。 https://blog.okazuki.jp/entry/2018/10/16/144556

実際のDLLのコードは後で示します。

テストプロジェクトを作成する

DLLをテストするプロジェクトとしてコンソールアプリケーションを作成します。 テストプロジェクトは、上記DLLプロジェクトと同じソリューションに含まれるようにしておきます。

具体的には ソリューションエクスプローラのroot要素(ソリューション)を右クリックして、

追加->新しいプロジェクト->コンソールアプリケーション

です。

テストプロジェクトは同じくコンソールアプリケーションを使います。

2つのプロジェクト間の依存関係の設定

テストプロジェクト側をスタートアッププロジェクトに設定し、プロジェクトの依存関係からDLLが先にビルドされるよう設定しておきます。

ついでに関数の呼び出し規約をstdcallに設定します。

ソリューションエクスプローラのプロジェクトを右クリックして「プロパティ」「C++タブ」「詳細設定」「呼び出し規約」のところを「stdcall/Gz」に設定します。

あとはビルドターゲットを

  • Releaseビルド
  • 32bit,64biをUnityのバージョンと合わせる

とします。

テストプログラムから呼び出す

DLLを呼び出すにはWindowsAPIでゴリゴリ(LoadLibrary, GetProcAddr)とかやってもいいのですが、手抜きでいきます。

DLLプロジェクトがDLLとインポートライブラリを生成してくれているので、インポートライブラリ利用します。

プロジェクト設定画面から、 インポートライブラリはリンカの設定で、DLL本体はデバッグ時のパスの設定で解決します。

リンク設定とデバッグ環境のパス設定はこんな感じです。

リンク設定

リンカ-> 入力-> 追加の依存ファイルに $(SolutionDir)x64\release\consoleDll.lib;%(AdditionalDependencies) を設定します。 ここでconsoleDllというのはDLL側プロジェクトの名前です。

デバッグ環境のパス設定

デバッグ-> 環境 の項目にPATH=$(SolutionDir)x64\release\を設定します。

ここまででDLLの生成とそれを読み込むテストプログラムの設定ができました。

設定が正しいかどうかDLLとアプリケーションをビルドして確認しておきます。パス指定を間違えたりしているとリンク時やデバッグ時にエラーが出るので状況確認してつぶしていきましょう。

実際にコードを書いてみる(DLL側)

単純なDLLではデスクトップアプリを作る場合と何も変わらないので、UWPで利用できるAPIを呼び出してみることにします。

昔の記事でC++/CXでやってたMediaCaptureを使ってみます。

#include <thread>

#pragma comment(lib, "windowsapp")

#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::UI::Notifications;

#include "winrt/Windows.Foundation.h"
#include "winrt/Windows.Web.Syndication.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

#include <winrt/Windows.Media.h>
#include <winrt/Windows.Media.Capture.h>
#include <winrt/Windows.Media.Capture.Frames.h>

using namespace Windows::Media::Capture;
using namespace Windows::Media::Capture::Frames;

// 新しいメディアフレームが到着した際にMediaCaptureから呼び出されるコールバックイベントハンドラ
void OnFrameArrived(
    MediaFrameReader sender,
    Windows::Media::Capture::Frames::MediaFrameArrivedEventArgs args)
{
    OutputDebugString(L"OnFrameArrived called \n");
    auto frame = sender.TryAcquireLatestFrame();
}


// MediaCaptureのための状態をまとめて管理する
struct MediacaptureWrapper {
    MediacaptureWrapper(MediaCapture cap, MediaFrameReader rd) :
        mc(cap),
        reader(rd) {};

    MediaCapture        mc;
    MediaFrameReader    reader;
};

// todo : できることならDLL利用側で管理してもらう
std::shared_ptr<MediacaptureWrapper>  g_capture;


extern "C" {
    // unityプラグインのP/Invoke呼び出し規約はstdcall
    __declspec(dllexport) int __stdcall mediatest(int a, int b) {
#if 1
        // todo : これはUnityエディタのときは呼び出してはいけない
        //        実行時環境を参照する方法を調べて、自動的に呼び出しを制御する方法を考える。
        winrt::init_apartment();

        // media capture の関数を一戸だけ呼び出すテスト
        // これ自体は問題なく成功するので、UnityからWinRTのAPIを呼びだすこと自体は問題ない
        // Win32アプリケーションでテストコードをかいて実験する必要あり
        auto groups = MediaFrameSourceGroup::FindAllAsync().get();

        MediaFrameSourceGroup selectedGroup = nullptr;
        MediaFrameSourceInfo selectedSourceInfo = nullptr;

        // 列挙したカメラの最初に見つけたものを使用する。
        // Hololens RS-4で確認したところ、NV12カラーフォーマットで列挙された
        for (MediaFrameSourceGroup sourceGroup : groups) {
            for (MediaFrameSourceInfo sourceInfo : sourceGroup.SourceInfos() ) {
                if (sourceInfo.SourceKind() == MediaFrameSourceKind::Color) {
                    selectedSourceInfo = sourceInfo;
                    break;
                }
            }

            if (selectedSourceInfo != nullptr) {
                selectedGroup = sourceGroup;
                break;
            }
        }

        // 利用可能なカメラが見つからない場合
        if (selectedGroup == nullptr || selectedSourceInfo == nullptr) {
            OutputDebugString(L"selected source group not found\n");
            // todo
        }

        // たぶんプロパティと同様の扱いでいいんだろうけど、どうなの?
        auto settings = MediaCaptureInitializationSettings();
        settings.MemoryPreference(MediaCaptureMemoryPreference::Cpu); // Need SoftwareBitmaps for FaceAnalysis
        settings.StreamingCaptureMode(StreamingCaptureMode::Video);   // Only need to stream video
        settings.SourceGroup(selectedGroup);

        // よくわかってないところ。
        // winrtのオブジェクトに対してnew演算子が使えなくて、こういう方法でインスタンス化できるっぽい?
        // ただ、MSDNにあるサンプルではインターフェースをもつクラスインスタンスを作る例なので、MediaCaptureではこれが正解かどうかは謎い
        auto mcaf = winrt::get_activation_factory<MediaCapture>();
        auto mc =  mcaf.ActivateInstance<MediaCapture>();

        // 使用するカメラのパラメータが確定したので、初期化を行う
        // 1, メディアキャプチャの初期化
        // 2, 読みだしストリームの作成
        // 3, キャプチャの開始
        mc.InitializeAsync(settings).get();
        auto mapview = mc.FrameSources();
        auto selectedSource = mapview.Lookup(selectedSourceInfo.Id() );

        auto media_reader = mc.CreateFrameReaderAsync(selectedSource).get();
        auto status = media_reader.StartAsync().get();

        // https://docs.microsoft.com/ja-jp/windows/uwp/cpp-and-winrt-apis/handle-events
        // デリゲートを使ってイベントを処理する方法
        media_reader.FrameArrived({&OnFrameArrived});

        g_capture = std::make_shared<MediacaptureWrapper>(mc, media_reader);

        if (status == MediaFrameReaderStartStatus::Success) {
            // mediacapture, mediaframereaderのペアができた
            // 選択されているカメラはselectedSourceである
            OutputDebugString(L"media capture start success\n");
        }
        else {
            // メディアキャプチャの読み出しが開始できない
            OutputDebugString(L"media capture start fail \n");
        }
        std::this_thread::sleep_for(std::chrono::seconds(5) );
#endif
        // 単純なDLLのテスト用として、加算した結果を返すだけ。
        return a + b;
    }
}

次にテストプログラム側です。

ライブラリのリンクはできているので、単純に呼び出すだけです。

int main()
{
    std::cout << "Hello World!\n"; 

    // dll のテスト
    mediatest(1,2);

    std::cout << "finish.\n";
}

コードの概要(全体像)

DLLはmediatestという関数をエクスポートし、テストプログラム側はこれを利用します。 引数、戻り値にint型をとっていますが、DLLが呼べるかなどの基本的なテスト用に残しています。

このコードは実行すると5秒間カメラから映像フレームを取得し続けます。

映像フレームは保存せずに捨てるだけなので害はありません。

コードの詳細(DLL側)

DLL側ではwinRTを使った実装をしています。慣れない部分は多々ありますが、理解した範囲でメモしておきます。

C++マングリング

ネームマングリングについて雑に説明します。

C++には関数のオーバーライドとか、デフォルト引数みたいな機能がたくさんあります。 これはC言語には存在していないものです。

関数を拡張したような機能を実現するためにC++コンパイラはネームマングリングといって、内部的に異なる関数名を生成します。

(リンクエラーで関数が見つからないときに記号や数字が付いた関数名が表示されると思いますが、アレがコンパイラ内部で扱われている関数名相当です。)

これがネームマングリングと呼ばれるもので、これだとDLLとして呼び出すときに非常に不便です。(マングル規則を知らないと呼び出せない)

そこで、「ネームマングリングしないで。Cの関数として扱って」というのがextern "C"です。

WinRTライブラリの指定

#pragma comment(lib, "windowsapp") は、ライブラリのリンクの指定で、ここではwindowsappのライブラリ一式をリンクする指定です。

includeとかusing

使う機能をincludeしてusingで以降の名前空間の指定を省略するという使い方です。

ここで注意点で、たとえばinclude <winrt/Windows.Media.Capture.Frames.h>を消すと、コンパイルは通るがリンクエラーになるという事態が起きます。

(これは公式マニュアルのFAQにもあるので読んでみるのが良いでしょう)

なぜコンパイルエラーではなくリンクエラーかというと、おそらくC++の「テンプレートの明示的インスタンス化」の影響だと考えています。(ヘッダオンリーライブラリにありがち)

たとえば#include <winrt/Windows.Media.Capture.h>をすると、MediaCaptureの入出力でFramesクラスを使うので「Framesクラスがあるよ」宣言が必要がありますが、ここでは実態は定義されません。で、テンプレートのインスタンスはFrames.h側にあるので、リンク時に問題になるわけですね。

順番ごちゃごちゃして汚いですが、こんな感じになりました。

#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::UI::Notifications;

#include "winrt/Windows.Foundation.h"
#include "winrt/Windows.Web.Syndication.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

#include <winrt/Windows.Media.h>
#include <winrt/Windows.Media.Capture.h>
#include <winrt/Windows.Media.Capture.Frames.h>

using namespace Windows::Media::Capture;
using namespace Windows::Media::Capture::Frames;

エクスポート関数の指定と呼び出し規約の指定

 __declspec(dllexport) int __stdcall mediatest(int a, int b) {

DLLが外部に公開する関数だということを明示します。今回はテスト用に1個の関数だけexportするので、defファイル書くまでもないなと思い、こうやってます。

__stdcallはプロジェクト設定でやってもいいですし、こうやって各関数個別に指定してもいいです。

COMの初期化

winrt::init_apartment()

winrtの内部実装はCOMらしいので、その初期化をします。 こいつも注意で、今回のテストプログラムから使うときは必要ですが、Unityから使う場合は消しておく必要があります。(Unityの中でCOMの初期化やってる?) まあ、DirectXもCOMですし、何かやっててもおかしくはないでしょう。

非同期操作(async)

非同期操作関連はコルーチンを使えばきれいに書けるみたいですが、コルーチンはまだよくわからないので同期的に処理することにしました。

たとえば auto groups = MediaFrameSourceGroup::FindAllAsync().get()では、FindAllAsync非同期操作の完了まで待機して結果を受け取ります。 もちろんFindAllAsync()を一旦変数で受けてからgetするというのもできます。

コレクションの処理

IVectorViewなどのWindows固有のコレクションも、範囲forで片づけます。

for (MediaFrameSourceGroup sourceGroup : groups)とかですね。

今回は勉強なので、各要素の型を明示しましたが、autoで受けても大丈夫でした。

プロパティ

C#のプロパティのように、クラスのフィールドにアクセスするには

  • 代入時は関数呼び出しの構文で
  • 取り出し時は変数 = プロパティ

と覚えておけばだいたい事足りました。

たとえばメディアキャプチャの設定をするのはこんな感じですね。

 auto settings = MediaCaptureInitializationSettings();
    settings.MemoryPreference(MediaCaptureMemoryPreference::Cpu);
    settings.StreamingCaptureMode(StreamingCaptureMode::Video);
    settings.SourceGroup(selectedGroup);

インスタンス

ここはまだ理解しきれていないので自信ないところです。

C++/CXのときはref new ClassName()とかしてた部分になります。

一旦activation_factoryテンプレートクラスを、インスタンス化したいクラス名のテンプレートとして構築し、そのactivationfactoryのActivateInstanceメソッドを呼ぶことでインスタンス化できるようです。

 auto mcaf = winrt::get_activation_factory<MediaCapture>();
    auto mc =  mcaf.ActivateInstance<MediaCapture>();

イベントハンドラ

イベントハンドラはコルーチン(?)に対するデリゲートでいけました。

media_reader.FrameArrived({&OnFrameArrived});

すごくシンプルですね。c++/WinRT素晴らしいですね。

一番楽になったのはコールバック関数をコルーチン(?)として与えることができるので、すごく楽になったと感じます。

ちなみにC++/CXではstd::bindにプレースホルダー指定しつつ、テンプレートのインスタンス化みたいな難読なコードでした。

できた

ここまでで、テストプログラムを実行するとコールバック関数が呼び出されることが確認できます。

DLL側にブレークポイントを打ってデバッグすることも可能です。

軽くUnityエディタで動かしてみたところ、問題なく動いたので、これを利用してネイティブプラグインの開発ができそうです。

感想と今後の展望

ここまでで、UWPのMediaCaptureを利用するDLLができました。

本当はUnityエディタから利用する部分まで書こうかと思っていたのですが、記事が長くなりそうなので今回はこれくらいにしておきます。

次回はUnityのネイティブプラグインのファイル配置、開発環境、デバッグ方法などを書いてみようと考えています。

少しずつですがUnityプラグイン作って色々やっていきたいと思います。

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