jsでまばたき認識とその他の失敗
まばたきを検出することになったので検証。
仕事というより研究に近いので手段とかユーザーの環境は特に制限は無い。
けどできるだけ多くの人に見てもらえるほうがいいけども。
Unity、Cinder、openframeworks、Flash、javascriptで検証。
結局jsに落ち着いた。
デモは後述。
●Unity
3D表現もできれば使いたかったのでまずはUnityから。
WebPlayerもあるし。
OpenCVで目の位置を検出してみる。
OpenCVSharpを使用。
Macは設定が面倒そうなのでとりあえずWindowsで。
目の位置が検出できなければ「閉じている」とする。
閉じてても検出できちゃうことも多いけどまあまあいい感じにできた。
WebPlayerで書き出し、サーバに上げてみた。
検出器の読み込みの「CvHaarClassifierCascade」がファイルパスしか指定できない。
URLの指定はできない。
WWWで一度ローカルに保存してから使おうと思ったけどセキュリティの関係でWebPlayerではWWWで保存できないらしい。
OpenCVSharpを書き換えればできるんだろうけど、そんな能力は無し。
スタンドアロンに切り替える。
ビルドするといろいろファイルが書き出されてなんか嫌だ。
Mac版もできる保証無し。
一旦やめよう。
●Cinder
ブラウザではダメだけどWin、Macのアプリ作れるしやってみる。
とりあえずMacで。
方法は同じくOpenCVで目の検出。成功。
書き出したアプリを別の人へ渡す。エラーで動かず。
このへん探るの面倒。
一旦やめておく。
●oF
CinderよりoFのほうがいいんだろうけど、個人的にoFはパスのエラーとかで
悩まされることが多く、相性が悪いと思ってる。(俺の知識のせいだけど)
でもまばたき検出できそうなのがあったので試す。
https://github.com/kylemcdonald/ofxFaceTracker
「example-advanced」は何とか動いたけど「example-blink」は動かず。。
やっぱり基本的なとこでつまづくのでoFはいい印象が無い。
「example-advanced」の方の顔のトラッキングは精度いいかも。
一旦やめておく。
まずはOpenCVを試す。
古いけど「Marilena」というライブラリがある。
ただ顔の検出はできたけど目はできず。なんでだ??
そういえばSEKAI NO OWARIのコンテンツでフェイストラッキングしてたなと。
http://www.star-dome.jp/
調べると↓このSDK使ってるっぽい。
http://www.beyond-reality-face.com/sdk
商用利用なので問い合わせ・・・高い!Unity Pro買えちゃうよ!
とりあえずデモで試す。
FaceDetectionで目の検出はできるけどなんかカクカクする。
FaceEstimationは結構滑らか。
2パターンやってみた。
1.目の周りの画像の変化量を調べる
1フレーム前の画像と比較していく。
部屋の明るさとかWEBカメラの性能でけっこう変わってくるのでやりにくい。
画像の明るさとコントラストを調整してブラーかけたらマシにはなった。
ただそれっぽくはできた。
2.目、眉毛あたりの特徴点の変化量を調べる
鼻の位置の変化量より目、眉の位置の変化量が多ければまばたきしているとする。
けっこういい感じだがフェイストラッキング自体の動きが少しカクカク。
1のほうが精度はいい。部屋を明るくしてね!とか注釈入れとけばなんとかいける。
というわけで無しではないが。。高いし。。
●javascript
これ使う。
https://github.com/auduno/clmtrackr
「Tracking in video」のサンプルを見た感じ、フェイストラッキングがかなり滑らか。
まばたきした時の変化量もわかりやすそうだ。
ただchromeとfirefoxでしか動かないし、js好きじゃないので最初のほうに見つけてたけど避けてた。
他のがイマイチだったのでやっと重い腰を上げる。
結果としてはほぼカンペキ。
MBAで試したけど100%に近い検出。
(作った人だからコツが分かってるというのもあるかもしれない)
頭を前後に動かした時に誤認識するけど、そんなに気にならない。
余力があればここはもう少し詰める
デモはこちら
「start」ボタン押すと始まります。
まばたきする度に四角の色が変化します。
左目しか判定してないけど、右目だけつぶっても左目もけっこう動くので両目でも片目でも反応しちゃいます。
以下スクリプト。
サンプルを書き換えただけなのでjs部分のみ。
見ての通り適当な判定だけど精度は良い。
var webcam = document.getElementById('webcam'); var overlay = document.getElementById('overlay'); var overlayCC = overlay.getContext('2d'); var oldLeftEye = new Array(2); var oldRightEye = new Array(2); var oldNose = new Array(2); var leftMode = 0; var leftInterval = 10; var closeFlag = false; var ctrack = new clm.tracker({useWebGL : true}); ctrack.init(pModel); stats = new Stats(); stats.domElement.style.position = 'absolute'; stats.domElement.style.top = '0px'; document.getElementById('container').appendChild( stats.domElement ); function enablestart() { var startbutton = document.getElementById('startbutton'); startbutton.value = "start"; startbutton.disabled = null; } navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; window.URL = window.URL || window.webkitURL || window.msURL || window.mozURL; // check for camerasupport if (navigator.getUserMedia) { // set up stream var videoSelector = {video : true}; if (window.navigator.appVersion.match(/Chrome\/(.*?) /)) { var chromeVersion = parseInt(window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10); if (chromeVersion < 20) { videoSelector = "video"; } }; navigator.getUserMedia(videoSelector, function( stream ) { if (webcam.mozCaptureStream) { webcam.mozSrcObject = stream; } else { webcam.src = (window.URL && window.URL.createObjectURL(stream)) || stream; } webcam.play(); }, function() { //insertAltVideo(vd); //document.getElementById('gum').className = "hide"; //document.getElementById('nogum').className = "nohide"; //alert("There was some problem trying to fetch video from your webcam, using a fallback video instead."); }); } else { //insertAltVideo(vid); //document.getElementById('gum').className = "hide"; //document.getElementById('nogum').className = "nohide"; //alert("Your browser does not seem to support getUserMedia, using a fallback video instead."); } webcam.addEventListener('canplay', onLoadWebcam, false); function onLoadWebcam() { enablestart(); } function start() { startVideo(); } function startVideo() { // start video webcam.play(); // start tracking ctrack.start(webcam); // start loop to draw face drawLoop(); } function drawLoop() { requestAnimFrame(drawLoop); overlayCC.clearRect(0, 0, 400, 300); if (ctrack.getCurrentPosition()) { ctrack.draw(overlay); } var list = ctrack.getCurrentPosition(); if (list.length > 50) { var leftEye = list[27]; var rightEye = list[32]; var nose = list[37]; var dxLE = leftEye[0] - oldLeftEye[0]; var dyLE = leftEye[1] - oldLeftEye[1]; var dLE = Math.sqrt(dxLE*dxLE+dyLE*dyLE); var dxRE = rightEye[0] - oldRightEye[0]; var dyRE = rightEye[1] - oldRightEye[1]; var dRE = Math.sqrt(dxRE*dxRE+dyRE*dyRE); var dxN = nose[0] - oldNose[0]; var dyN = nose[1] - oldNose[1]; var dN = Math.sqrt(dxN*dxN+dyN*dyN); var dyLE = leftEye[1] - oldLeftEye[1]; var dyRE = rightEye[1] - oldRightEye[1]; var dyN = nose[1] - oldNose[1]; //1回検出後はすぐに検出しないようにする if (leftInterval < 0) { //目が下方向(yの+方向)にある程度動いた場合(目を閉じた) if (dyLE > 0.5) { //鼻の変化量dNより目の変化量dLEのほうが大きい if (dLE - dN > 0.3) { if (leftMode == 0) { leftMode = 1; } else { leftMode = 0; } leftInterval = 10; onChangeMode(); } } } oldLeftEye[0] = leftEye[0]; oldLeftEye[1] = leftEye[1]; oldRightEye[0] = rightEye[0]; oldRightEye[1] = rightEye[1]; oldNose[0] = nose[0]; oldNose[1] = nose[1]; //overlayCC.beginPath(); overlayCC.rect(270, 200, 100, 100); if (leftMode == 0) { overlayCC.fillStyle = 'rgb(255, 255, 0)'; } else { overlayCC.fillStyle = 'rgb(255, 0, 0)'; } overlayCC.fill(); leftInterval--; } } // update stats on every iteration document.addEventListener('clmtrackrIteration', function(event) { stats.update(); }, false);