• SELECT MENU

2020.06.19

PoseNetを使用したミニゲームを制作したお話

NATSUKI YAMASHITA
[ENGINEER]
kawaiiを大事にしたい

こんにちは、フロントエンド勉強中の2年目エンジニア山下です。

最近はリモートワークをさせていただいております。
たまに吸う外の空気は本当に美味しいですね。

今回は、初めてPoseNetを利用したミニゲーム(キッズコンテンツ)を制作したお話をさせていただきます。

今までもチームとしてはデジタルサイネージの案件としてPoseNetを利用したキッズコンテンツを制作していたのですが、私は携わったことがなかったのです。
なのでまず、PoseNetとはなんぞ?ということで調べてみました。

PoseNetとは

PoseNetは簡単に言うと、機械学習によってリアルタイムで人の姿勢を推定できる技術です。
PoseNet自体はTensorFlowというオープンソースとしてgoogleが公開している機械学習ライブラリのモデルなのですが、
今回はこれのTensorFlow.js版を使用します。ブラウザで動くということはwebカメラで人のポーズが推定できるということです。すごい!

しかもライセンスがApache 2.0 Licenseなので、商用利用もできます。

PoseNetを使う

-PoseNetのロードと検出

さて、PoseNetの実際の使い方ですが、以下PoseNetのGitHubページをご覧ください。
デモも用意されており、わかりやすいです。私はこれを必死にDeepL翻訳で翻訳していました。
ーGitHub:tfjs-models/posenet at master · tensorflow/tfjs-models
https://github.com/tensorflow/tfjs-models/tree/master/posenet

今回webカメラの映像を使用して検出するので、することとしては
1、webカメラのストリームデータをMediaDevices.getUserMediaで取得
2、HTMLVideoElement要素にストリームデータをセット
3、PoseNetのモデルを読み込む
4、HTMLVideoElementを使って検出
これだけです。

リアルタイムで推定を行う場合には毎フレーム検出しなければいけません。

今回は以下のようにPoseNetのモデルをロードしました。
各オプションについては上のGitHubページをご覧ください。

let multiplier = IS_MOBILE ? .5 : .75;
  let net = await posenet.load({
   architecture: 'MobileNetV1',
   outputStride: 16,
   inputResolution: 256,
   multiplier: multiplier,
   quantBytes: 2
  });

モバイル使用時はmultiplierは0.5が推奨とのことです。
以下を実行すると、推定結果が得られます。

net.estimateSinglePose(videoImage, {
   flipHorizontal: true
  }).then((pose) => {
   //推定結果を使用した処理
  });

-推定結果を使用する

PoseNetで出力されるデータにkeypointsという配列があり、その中に人の”どのパーツ”が”どの位置”に”どれだけの正確さ”で検出されたかというものが格納されます。
今回は手を振る動作を必要とするので、推定結果の手首のデータを使用します。

下の画像は作成したデモですが、肩と肘と手首が検出されているのが分かります(顔など他の部分も本当は検出されているのですが、画像を切り抜いています)。
推定する時は、出来るだけ体全体が映っている方が精度が良くなります。

-MediaDevices.getUserMediaについて

端末のカメラからストリームデータを取得する、MediaDevices.getUserMediaメソッドについても軽く触れておきます。
詳しくはこちらをご覧ください。
ーMediaDevices.getUserMedia() – Web API | MDN
https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia

前まではNavigator.getUserMediaが使用されていましたが、こちらが非推奨になり、代わりにMediaDevices.getUserMediaが推奨になっているとのことです。

以下が実際の取得部分のコードです。

let media = navigator.mediaDevices.getUserMedia({
'audio': false,
'video': {
aspectRatio: { exact: 1.7777777778 },
width: { min: 568, ideal: 1920 },
height: { min: 320, ideal: 1080 },
facingMode: "user",
}
})
media.then((stream) => {
//ここでstreamを使った処理
//今回はHTMLVideoElementにstreamをセット
})

取得時にはwidthやheightやaspectRatioなどを指定します。
aspectRatioはアスペクト比を指定します。
また、その中ではmax,min,ideal,exactが使えます。
idealとは理想値、max,min,exact(min=max)は必須な値を指定する際に使います。

この指定をどう捉え、実際どのようなサイズで取得するのかはブラウザによります。
とはいえ基本は皆同じなのですが、今回私はsafariの挙動に惑わされました。

safariの挙動

今回つまずいた箇所が2点あります。

1.aspectRatioの指定

safariで
・aspectRatio: { ideal: 1.7777777778 }
・aspectRatio: 1.7777777778
以上の設定にすると最初のフレームだけ描写され、フリーズしてしまう現象が確認できました。
・aspectRatio: 1.77
だと動くので、小数部の桁数の問題のようですが、謎です…。idealは理想値なのでスルーしてくれるはずなのですが…。

最初はなぜフリーズしているのかが分からず、時間を取られました。
結局、exactにすることで解決しました。

2.videoの再生

HTMLVideoElementに関してなのですが、safariではMediaStreamを使用した表示の時はplaysinline属性がないと、再生できません!
最初とてもハマりました。。。

こちらに言及されています。
ーモバイルブラウザのビデオ再生がいろいろ変わるので確かめてみた – Qiita
https://qiita.com/tomoyukilabs/items/cb9dd1d3e7eb0cc7f58a

そして、Android版Firefoxでは、mutedを設定しないとinline再生できません。
勝手にmutedはautoplayとセットのイメージを持っていたので、気づけませんでした…。

結局今回は、playsinlina,mutedを指定し、play()で再生しました。

知らないことがあったり意外な挙動をしたりするので、検証は早めにしておきましょう(自分への戒め)。

ゲームでの使用

今回制作するのは「あわあわだーれだ?」というミニゲームです。
タイトル画面は最初の方にお見せしたものです。
デザインは弊社のデザイナー担当なのですが、とても可愛い…癒される…。

動物のキャラクターが手洗いをしており、その手元がゲーム画面です。
最初はあわで手元が隠れているので、その手元からあわを消して、現れた手を見てなんの動物かな?と想像する内容です。
手を動かす動作であわを消します。ここでPoseNetを使用します。

ゲーム内の処理では手首の位置を一定の間隔で検出しているので、その経過時間と手首の動いた距離によって、手の動きのスピードを出しています。そのスピードが早い方が、よりダイナミックな演出になるようにしています。
また、手を振るたびにあわが小さくなっていき、完全に消えると次のシーンへいきます。

この子が手を洗っていました!

PoseNetでつまずいたこと

-モデルのロード時間が長い

ロード画面を挟むことでユーザビリティとしては改善されました。

-初回の推定が重い

TensorFlowはGPUを使用して処理しているため、そのせいでアニメーションがカクつきました。
この問題は、ロード画面前の何もアニメーションのないところで一度推定させることで改善できました。
ロード画面前が少し長くなるのがネックですが、その箇所以外にアニメーションをしていないシーンがないので致し方なかったです。

-処理速度がデバイスのスペックに左右される

社内のメンバーに検証してもらったところ、どうしても時間が足りずクリアできないというフィードバックをいただきました。
これはPoseNetの処理速度がデバイスのスペックに左右されるのが原因でした。

この改善としては、WebWorkerにPoseNetの処理を逃がすことである程度改善できました。(WebWorkerを上司からのアドバイスで初めて知りました…。)
WebWorkerとは、javascriptをマルチスレッドで処理させるためのものです。
メインスレッドと別のスレッドに処理を逃すことで、メインスレッドの処理を止めることなく重たい処理をすることができます。

ただし現時点(2020/6/19)では、WebWorkerにてWebGLバックグラウンドでTensorFlow.jsを動かせるのはChromeやEdgeのみとなっています。
OffscreenCanvasを使用することで、WebWorkerでもWebGLバックグラウンド処理が可能となるのですが、OffscreenCanvasが実装されているのは一部ブラウザとなっているためです。
他のブラウザでは、CPUバックグラウンドで動いてしまうので、かえって処理が遅くなります。

制作全体を通して

デザインがとっても可愛いので、それをいかに生かすか、主にアニメーションで悩みました。
アニメーションについては上司からのアドバイスもいただき、ブラッシュアップの連続でした。
というか、終わりが分からないです、お絵かきと同じです。
動物のキャラクターには、キャラ付けすると動きのイメージが固まりやすかったです。
デザイナーさんからのフィードバックとして、動物の特徴を捉えていると言っていただけたので、伝わってよかったと思いました。

文字が出てくるシーンの表示時間は、低年齢層を想定としているので最後まで読めるようにゆっくりめに設定しました。

感想・まとめ

PoseNetを使ってみて、Webカメラで人の動きを認識できるなんて…しかもオープンソース…という感動を覚えました。

しかし、PoseNetを使用するにあたって課題もまだあるので、少しずつ改善していきたいと思います。

ゲーム自体の制作については、キャラクターに動きを与えるのは初めてだったのと、キッズコンテンツということで、子どもたちがどう見るのか、どう思うのか想像しながら制作するのは難しくもあり、新鮮で面白かったです。

(ちなみに私が3歳ごろに遊んでいたゲームは、家にあったMacintosh LC2に入っていた「shufflepuck cafe」というエアホッケーゲームなのですが、出てくるキャラが個性的で、動きが面白くてめちゃくちゃハマっていました。やはり動きも重要な要素ですね…!)

そして何より、社内のメンバーからフィードバックや反応をもらうことで段階を踏んでどんどん良いものになっていった実感が強くありました。自分だけでは気づかないことがとても多かったです。

さらに生き生きとした面白いコンテンツができるように、アニメーション・コーディング共に学んでいきたいと思います。

今回は私の普段の業務とは少し違う、新鮮な開発となりました。
引き続き、キッズコンテンツの開発を出来ればと思っております!

 
 
 ※ミニゲーム「あわあわだーれだ?」はこちら
https://www.contents.ne.jp/kidscontents/awaawa/

※当社へのお仕事のご相談・ご依頼は、CONTACT USよりお待ちしております。ZoomやMicrosoft Teams、Google Meetなどオンライン会議ツールによる打ち合わせが可能ですので、ご希望の方は書き添えていただけると幸いです。

glasses

[181]

Webテクノロジー

© 2017 Contents Co.,Ltd.