こんにちは。自称フロントエンドエンジニアの森です。
先日、face-apiを使ったミニゲームをローンチしたのですが、制作にあたって色々と躓き、勉強になったので、ここにログとして残したいと思います。(制作時の感情が甦り、若干口調がおかしくなる箇所がありますが、お目溢しください)
作ったゲーム
「ふたりでガッチャンコ!」
Webカメラを使い、2人の顔を絵の通りにくっつけられたら、成功!
というミニゲームです。
ソーシャルディスタンスが叫ばれるこの時代に、家族や友達のコミュニケーションを大事にしたい。そんな想いから企画されたゲームです。
プレイ中の写真がゲーム後に表示されます!
(想像に難くないと思いますが、1人で開発・検証するのはなかなか大変でした。左手がしんどい。)
face-api.js
さて本題。
このゲームではWebカメラの映像から、人の顔を検出する face-api.js という、なんとも素敵なJavaScript APIを使用させてもらってます。
face-apiの機能は主に下記の5つ
★ 顔の検出
★ 目・鼻・口など、顔の68個のポイントの検出
★ 顔の類似性判断
・表情の認識
・年齢、性別の推定
(★マークはゲームで使用した機能)
Webカメラの映像に限らず、静止画(画像)や、動画でも使用できます。
なんでもできるやん…
早速やってみる
簡単に顔の検出と、landmarksを表示させてみます。
1. 公式のGithubからface-apiをダウンロード
2. 必要なmodelを./models/に配置
modelは、ダウンロードしたzipのweightの中にあります。
必ず必要となるモデルは “ssdMobilenetv1” です。
それ以外は用途に応じて、足していきます。今回はランドマークを表示させたいので、”faceLandmark68Net “も配置しておきます。

3. html
Webカメラ用に<video>、描画用に<canvas>をhtmlに配置します。
WebCameraに関しては、チームの山下さんが記事を書いてくれているので、そちらもどうぞ!
<!DOCTYPE html> <head> <title>WebCam Test</title> <style> .container { position: relative; } #video { width: 640px; height: 360px; position: absolute; top: 0; left: 0; z-index: 1; } #canvas { width: 640px; height: 360px; position: absolute; top: 0; left: 0; z-index: 2; } </style> </head> <body> <div class="container"> <video id="video" autoplay muted playsinline></video> <canvas id="canvas"></canvas> </div> <script src="./js/face-api.js"></script> <script src="./js/main.js"></script> </body> </html>
4. JavaScript
// 2で配置したモデルを読み込む。 Promise.all([ faceapi.nets.ssdMobilenetv1.load('./models'), // 精度の高い顔検出モデル faceapi.nets.faceLandmark68Net.load('./models'), // 顔の68個のランドマークの検出モデル ]).catch((e) => { console.log(`face-apiを読み込むことができませんでした。${e}`); }); // Webカメラの起動 const video = document.getElementById('video'); const media = navigator.mediaDevices.getUserMedia({ audio: false, video: { width: 640, height: 360, aspectRatio: 1.77, facingMode: "user", } }).then((stream) => { video.srcObject = stream; video.onloadeddata = () => { video.play(); } }).catch((e) => { console.log(e); }); // 描画用canvasの設定 const cvs = document.getElementById('canvas'); const ctx = cvs.getContext('2d'); cvs.width = 640; cvs.height = 360; // face-apiで顔のランドマークを取得します。 let faceData; async function getLandMarks(){ faceData = await faceapi.detectSingleFace(video).withFaceLandmarks(); if(faceData == null) return; drawLandMarks(faceData.landmarks.positions); } // 取得したランドマークをcanvasに描画します。 function drawLandMarks(positions) { ctx.clearRect(0, 0, cvs.width, cvs.height); ctx.fillStyle = '#ffffff'; for(let i = 0; i < positions.length; i++){ ctx.fillRect(positions[i].x, positions[i].y, 3, 3) } } function render(){ requestAnimationFrame(render); getLandMarks(); } render();
※WebCameraの使用(getUserMedia)はHTTPS通信か、localhostでしか許可されません。
今回のデモは1人用なので、detectSingleFace()で、もっとも信頼度が高い顔が1つだけ検出されるようになっています。複数人を検出したい場合は、" tinyFaceDetector "のモデルを追加して、detectAllFaces()で可能です。
68箇所のlandmark 横を向いても 上を向いても
これだけのコードで顔を検出できます…!すごい。
口を開けたり、横向いたり、上向いたりしても…、大丈夫。すごい(2回目)
ガッチャンコよもやま話
このすごいface-apiを使って、件のゲーム「ふたりでガッチャンコ!」を作ったわけですが…
めちゃ苦労しました!
自分の知見が足りないことはもちろん、タイミングやら、状況やら…
というわけで、苦労した点を振り返りたいと思います。一部、face-apiに関係ないのもありますが、生暖かい目で見てやってください。
・iOS14 アップデート
ローンチの少し前、iOS13 からiOS14へのメジャーアップデートがありました。
先週末まで、問題なく動いていたはずのゲームが突然クラッシュし始めました。なんでぇ…?
諸々修正をした今となっては、『逆になんで動いてたんだろう』くらいなのですが、この時はショックでした…。ブラウザゲームを開発する時は、アプリやOSのアップデートには気をつけなきゃいけないですね…
結局、いろんなところを最適化することができたので、雨降って地固まったと己を納得させました。
・face-api は、初回起動が重い
この問題、山下さんも記事にしていましたが、初回起動が非常に重く、face-apiが起動した瞬間にクラッシュするという事象に見舞われました。
解決方法としては、できるだけ何も動いていないタイミングで、ダミーの真っ黒な画像をface-apiに認識させて、アイドリング的なことをさせる…、に落ち着きました。
・PIXI.js と face-api
今回、アニメーション部分にはPIXI.jsを使用しています。
face-apiは、WebGLを使用していますが、PIXI.jsもWebGLを使用しています…。2つが同時に動くと動きがもっさりしてしまい…。こちらは、pixi-legacy.jsを使用し、canvas2Dへのフォールバックで対応しました。
また、今回アニメーション部分を山下さんと分業していたのですが、開発当初「分業するなら、PIXIを2つ使えばいいか!」という単純な考えでPIXIを2つ使っていました。
はい。Containerで分ければ良いだけですね。すみません。
おそらく、【face-api.jsの初回起動が重い】の問題と、上記のもっさりする問題にも、2枚のPIXIがあったことは、大きく影響していたと思います。楽だからと言う理由で、適当なことをすると碌なことになりませんね。反省。
・FaceMatcherが重い!重い!
開発当初、右にいるプレイヤーと左にいるプレイヤーを個別に認識したくて、face-apiの" Matching Descriptors "という機能を使用していました。
これは、Matcherとして保存しておいた顔と、対象の顔がどれだけ類似しているかと0〜1の数値で出してくれるものです。「この中でAさんと一番顔が似てるのは、だーれだ!」って言うのを教えてくれる機能ですね。
ゲーム開始時に2人分のMatcherを取得して、それを元にプレイヤー1とプレイヤー2を区別していたのですが、、、(最初に右にいた人には、左に行っても右の画像しかくっついてこない仕様)
2人分の類似性判断を、描画ごとに行うとスマホではカクカクになり…
ならば、1人分ではどうだとMatcherを減らしてみたものの、スペックの低いスマホではやはりカクカクになり…
結局、この機能を使用するのは断念しました。
ですので、「2人でガッチャンコ!」では、「右にいる人に右の画像」「左にいる人に左の画像」という仕様になっています。
機能としてはおもしろいのに、残念…。PCに限定したブラウザゲームなら大丈夫なのかもしれません。
・opacityに注意
最初、何も考えずに画面を重ねて配置し、必要な画面はopacity:1;、不要な画面はopacity:0; transitionでfade-change!とやってたんですが、スマホでクラッシュが起きました。(原因特定に時間がかかった…!)
大きいDOMを大量に、opacityをかけておいておくとスマホでは耐えきれず、クラッシュするようです。基本はdisplay:none;で対応することで解決しました。
・toDataURL()はAndroidでは保存ができない
知ってました?
私は今回知りました。
toDataURL()で表示した画像は、Androidでは保存ができません…。toBlob()を使いましょう…
でもtoBlob()、重いですよね…
・toBlob()は明示的に消去をしない限り、データが削除されない
スマホで何回か繰り返しプレイをしていると、クラッシュする…
toBlob()のcreateObjectURL()で作成したURLは、ブラウザを更新するか、明示的にrevokeObjectURL()で削除してやらないといけないようです。
「ふたりでガッチャンコ!」は繰り返しプレイの際に、リロードさせていないので、メモリを食っていたようです。
・canvasはリサイズに注意
プレイ後の写真館で表示される3枚のスクリーンショットをcanvasに保存しているのですが、1番前に出ているゲームと一緒にリサイズを行ってしまっていて、保存したはずのcanvasが真っ白に…ということがありました。
リサイズしたら、コンテキストが吹っ飛ぶということを失念していました。初歩ォ…
・よく考えて、1つずつ
JavaScriptの非同期処理って、便利だけど、大変だな。と、今回本当に感じました。
今まで、「データを取ってくるまで、次の処理は待ってね」的なことは、経験してましたが、「A処理とB処理は一緒に行うと重すぎる」理由で、タイミングをずらすみたいなことを行ったのは初めてでした。今までは、あんまり考えずに「A処理とB処理が必要!クリックでどっちも呼び出し!」みたいな乱暴なことしかやってなかったのです。
ひとつずつ、着実に終わらせていくことが、結局近道…みたいな。
・まだまだある。
だがしかし、書く方も、読む方も疲れるので、この辺りで。
Special Thanks
開発に際し、いろんな地雷を踏みましたが、足を失わなかった(心が折れなかった)のは、ひとえにチームのみなさまのおかげです…。人に恵まれている…。
みなさま、ありがとうございます!
遊んでみてくださいね?
自画自賛ですが、とても良いゲームだと思います。
人と距離を取らなければならない、このご時世ですが、家族や友達とガッチャンコ!して、楽しく遊んでみてください!
コンテンツでは、この他にも様々なキッズ向けミニゲームを制作しています。ミニゲームの一覧は、【ぱおぴーのミニゲーム図鑑】で紹介しているので、ぜひ覗いていってください⭐️
最新のゲームはTwitter(@paopeee)でもチェックできます!