« 2012年04月 | メイン | 2012年06月 »

2012年05月 アーカイブ

2012年05月03日

JavaScriptの顔認識ライブラリをチューニングしたら実用レベルになったという話

2012/05/08 追記
再度検証して検証データを公開しました。
追記終わり。
JavaScriptの顔認識ライブラリにccv.jsという有名なものがある。
ただ、WebRTCで顔認識させようとすると遅くてしかたがなかった。
最初は速いこともあるが、10回ぐらい認識をさせるとすぐに遅くなる。
とりあえず、デモ。
そこで、チューニングをしてみることにした。
まず、JavaScriptの定番の高速化を試してみた。
例えば、正の数で使える「Math.floor(x)」を「(x | 0)」に、整数で使える「x * Math.pow(2, y)」を「x << y」にする等。
これで、10~30%高速化できた。

次に、遅くなっている部分を調べたら、Web Workersで分散するための仕組みが遅くなる原因だとわかった。
これは、Web Workersを使わない場合にも影響が出ていた。
じゃあ、Web Workersを使えば速くなるのかといえばその逆で、20倍遅くなっていた。
詳しくは調べてないけど、多分Workerスレッドに処理データを渡す時にJSON化が走っているためっぽい。
(ちなみにccv.jsは、Worker数をひとつしか指定できない。)
この仕組みを全て外したのに加え、functionの呼び出しコスト等を考えて処理を色々弄ってみた。
これで、150~250%も高速化できた。
さすがにこれには驚いた。
(前述のデモと同時に開くと遅いので注意。)
手元の環境では、190ms~230msぐらいで処理できている。
これなら、実用でもなんとか使えるんじゃないかと。

あと、顔認識の知識がなかったので、gihyo.jpの記事を読んでからコード読むとかなり理解できたことを書いておく。
2012/05/04 追記
ちょっと勘違いされそうな書き方だったので2点追記します。

1点目。
オリジナルのコードでは、同期/非同期を切り替えるためにparallableという関数を定義しています。
主観ですが、このparallableを使うと、(通常では問題ないレベルで)メモリを比較的大きめにとるようです。
しかしWebRTCを使ってリアルタイムで処理を行うと、10回ぐらい顔認識を行うとGCが実行され始めるようです。
勘でしかありませんが、GCの処理が邪魔をして、顔認識のリアルタイム性が落ちてしまっていると感じました。

2点目は、Web WorkersとJSON化について。
非同期で処理を行った場合、処理時間が20倍になってしまうのですが、気になったので確認しました。
Workerスレッドには、基本的なオブジェクトしか渡せません。
parallable内ではtryの中で一度postMessageを実行し、catchされた場合にJSON化して文字列としてpostMessageを実行していました。
ccv.jsがWorkerスレッドに渡そうとしていたのは、CanvasPixelArrayを含むオブジェクトのため、JSON化されてしまいます。
手元の環境ではこのJSON化の処理に2000ms以上かかっていました。
次に時間がかかっていたのは、WorkerスレッドがJSON化された文字列を受け取ったところです。
JSONをparseするのに、手元の環境で1500ms程かかっていました。
WorkerスレッドからメインスレッドへのpostMessageは、JSON化されていませんでした。
2012/05/04 追記2
ccv.jsについてもう少し説明。
ccv.jsは、OpenCVの一部の機能をJavaScriptに移植したもののようです。
OpenCVはコンピュータビジョン(Computer Vision)のためのライブラリで、画像解析等でよく使われる処理をAPIとしてまとめたものです。
ccv.jsには、画像内のオブジェクト検出のAPIと、オブジェクト検出の前処理として必要なグレースケール化のAPIが含まれています。
オブジェクト検出には、機械学習の学習結果データが必要になるのですが、ccv.jsには顔検出の学習結果データが添付しています。
といっても、傾きなしの正面向きのデータしか含まれていないようで、現段階では用途は限られると思います。
このような理由のため、現バージョンのccv.jsは「顔検出のためのJavaScriptライブラリ」という認識がされています。
他のオブジェクト検出の学習結果データを準備すれば、他の色々な物を検出できるはずです。
2012/05/04 追記3
このエントリーのタイトルに「顔認識」と入っていますが、実はこれは誤りでした。
正確には「顔検出」です。
顔検出は画像の中から顔を探し出すことを指し、顔認識はその顔が誰なのかを判断することを指すようです。

2012年05月08日

続・JavaScriptの顔認識ライブラリをチューニングしたら実用レベルになったという話(データ編)

前のエントリーの続き。
なんか、数字が一人歩きしそうで怖いので実データを取り直して掲載することにした。
検証環境
MacBook Air(2011年モデル)
Mac OS X 10.7.3(Lion)
1.8 GHz Intel Core i7
4GB 1333MHz DDR3
Intel HD Graphics 3000 384 MB

Google Chrome 18.0.1025.168
「chrome://flags/」でMediaStreamを有効に変更済み。
検証方法
WebRTCで自分の顔を映し、顔をカメラ方向に向けて正面から計測。
計測は連続300回で、背景は単色の家庭用カーテンで、検出対象の顔の数は1つ。
(主観ではあるが、背景が複雑であったり、顔の数が増えると速度が落ちる傾向がある模様。)
他のアプリケーションを起動していない比較的理想的な状態で計測。

前回の計測は、曇った昼間にカーテン全開で計測を行っているため条件が違っているので注意。
ccv.js original
平均384.89ms。
このデータでは、十数回目の顔検出の後に250ms前後を推移する一つ目の山があり、二つ目以降の山は450ms前後になっている。
しかし、ひとつめの山から450msに達することもかなりあり、その原因は不明。
谷の出現頻度がもっと低いことが多いが、ここではばらつきがあることを示すためにこのデータを用いた。
このデータには現れていないが、同じ条件でも山が550~600msに達することもある。
(条件によってはもっと跳ね上がることもある。)
ccv for realtime
平均195.40ms。
前回計測した時よりもばらつきが少なく、検出時間も若干(10ms~20ms)短め。
このデータでは検証開始直後に高い値が1回あるが、全くない場合や数回続くこともある。
実データ
以下、生データ。
ccv.js original
216,187,205,201,185,176,177,180,174,254,257,261,257,262,269,256,258,250,260,257,257,267,259,252,258,258,260,261,254,250,251,260,262,261,262,263,261,251,255,259,259,267,255,255,249,226,196,200,200,196,195,200,202,196,446,455,451,457,440,455,444,440,437,439,439,442,443,440,445,441,432,433,466,446,450,449,443,442,444,446,452,446,445,444,452,448,434,456,446,456,436,441,446,438,437,456,462,226,211,199,210,198,197,195,208,196,446,449,444,453,446,432,434,457,447,443,434,450,436,436,437,445,440,443,439,440,443,449,443,458,451,444,452,434,441,438,434,447,446,443,446,440,449,440,439,435,447,434,461,438,444,436,439,431,455,447,437,446,444,452,438,441,429,452,236,202,199,203,204,198,193,192,191,438,440,444,435,439,450,436,444,447,433,442,448,454,431,444,440,434,438,438,456,443,449,453,443,448,439,443,436,447,432,455,432,439,435,438,440,451,457,437,433,450,440,434,432,457,438,438,444,469,440,443,440,435,440,456,439,446,449,446,445,460,455,435,440,444,443,433,216,201,199,205,204,206,202,195,195,460,436,454,446,446,445,468,444,441,436,451,445,441,438,453,442,440,435,459,428,431,444,450,452,443,449,441,452,450,438,460,441,449,440,449,440,430,466,442,454,436,428,433,441,441,439,451,445,440,437,441
ccv for realtime
382,223,205,235,196,192,190,219,198,190,194,195,188,195,186,197,191,208,209,190,199,199,202,240,240,261,216,208,192,190,184,194,187,215,187,196,185,194,185,199,214,185,186,184,189,184,183,191,209,199,185,191,193,193,190,213,189,186,194,195,195,214,189,185,188,192,197,189,196,204,187,188,183,191,185,195,190,220,184,187,184,184,193,211,187,188,189,191,200,202,202,184,197,192,190,184,214,193,189,190,188,194,186,220,184,185,184,195,187,222,187,192,195,185,189,191,210,185,191,192,190,197,186,213,187,193,190,195,185,188,187,216,186,189,184,195,191,185,200,212,190,185,192,185,183,219,198,184,186,193,189,216,184,196,189,190,191,215,184,183,185,189,193,186,188,185,216,193,191,194,209,193,186,215,187,185,184,184,188,195,226,187,183,196,204,188,192,204,200,189,214,223,214,229,221,214,215,195,191,190,188,191,189,208,192,186,189,185,184,191,216,192,185,191,186,188,186,197,202,198,191,192,183,185,184,186,210,190,191,186,193,187,195,189,224,191,184,189,202,189,184,228,196,187,185,189,186,195,200,201,187,189,182,195,185,185,196,220,193,187,189,186,189,183,218,185,185,187,189,195,198,217,190,201,184,189,188,211,204,196,183,185,187,194,188,193,186,217,192,193,192,189,189,188,209,191,196,192,192,190

2012年05月27日

WebSocket APIの仕様が変更されたようです

昨年末にCandidate RecommendationsとなったWebSocket APIですが、今回はLast Call Working Draftと書かれています。
W3Cの承認プロセスに詳しくないのでよくわかりませんが...。

それぞれの仕様は以下から読めます。
簡単に比較してみましたが、大きな変更はバイナリの扱いです。

これまではWebSocket APIで送信できるデータはDOMStringとArrayBufferとBlobの3つでしたが、新しい仕様ではDOMStringとArrayBufferViewとBlobになっています。

ArrayBufferViewについては以下のURLを参照してください。
例えば、ArrayBufferViewのサブクラスInt8Arrayを送信する場合、以下のようにする必要がありました。
var ary = new Int8Array(length);
/*
 ...aryの処理...
*/
// bufferプロパティからArrayBufferを取得し送信
ws.send(ary.buffer);
新しい仕様では、以下のようになるようです。
var ary = new Int8Array(length);
/*
 ...aryの処理...
*/
// Int8ArrayはArrayBufferViewのサブクラスなのでそのまま送信可能?
ws.send(ary);
受信側は、ArrayBufferのまま変更はないようです。
var handler = function(e) {
    // ArrayBufferを意図した型に変更
    // ArrayBuffer => Int8Array
    var ary = new Int8Array(e.data);
    /*
     ...aryの処理...
    */
}
var ws = new WebSocket(/*url*/);
ws.binaryType = "arraybuffer";
ws addEventListener("message", handler, false);
新しいAPIの実装はまだ出てませんが、近いうちに両仕様が混在しそうです。
両方の仕様に対応したコードを模索する必要がありそうですね。
Google

タグ クラウド