必要条件

この記事に必要な予備知識

JavaScript初級/中級、HTML5初級/中級、およびjQueryの基礎知識。 getUserMediaおよびオーディオAPIを使用するため、デモにはChrome Canaryが必要です。また、デモはローカルWebサーバーを介して実行する必要があります。

ユーザーレベル

すべて

必要なその他の製品(サードパーティ/ラボ/オープンソース)


この記事では、Webカメラのストリームを使って、ユーザーの動きをJavaScriptで検出する方法について説明します。デモでは、ユーザーのWebカメラから収集したビデオが表示されます。ユーザーは、自分の体の動きを使って、HTMLで構築した木琴をリアルタイムに演奏できます。このデモは、「開発中の」2つのHTML5をベースにしています。つまり、getUserMedia APIでユーザーのWebカメラを表示し、Audio APIで木琴を演奏します。

また、このデモでは、ブレンドモードを使ってユーザーの動きをキャプチャする方法についても紹介します。ブレンドモードは、グラフィックスAPIを含む言語や、ほとんどのグラフィックスソフトウェアでは一般的な機能です。ブレンドモードをWikipediaから引用すると、「デジタル画像編集におけるブレンドモードは、2つのレイヤーを互いに合成する方法を決定するために使われる」とあります。ブレンドモードはJavaScriptではネイティブにサポートされていませんが、ピクセル間の数値演算にすぎないため、ここでは、ブレンドモード「差の絶対値」を作成してみます。

Webテクノロジーの方向性を示すデモを作成しました。JavaScriptとHTML5によって、新しいタイプのインタラクティブなWebアプリケーション用の様々なツールが提供されるでしょう。非常に楽しみです。

HTML5のgetUserMedia API

この記事を執筆している時点では、getUserMedia APIはまだ開発中です。W3Cによる5番目のHTMLメジャーリリースにより、ネイティブハードウェアデバイスへのアクセスを提供する新しいAPIが急増しました。navigator.getUserMediaAPI() APIは、Webサイトでオーディオとビデオをキャプチャできるツールを提供します。

このAPIの基本的な使用方法については、様々なブログの多くの記事で取り上げられています。そのため、このAPIについての詳細は省きます。この記事の最後に、getUserMediaに関する役立つリンクのリストがあります。詳細を知りたい方はそちらを参照してください。ここでは、デモのためにこのAPIを有効にする方法について説明します。

現在、getUserMedia APIは、Opera 12とChrome Canaryのみで使用できますが、どちらもまだ一般公開はされていません。このデモでは、木琴を演奏するためのAudioContextをサポートしているChrome Canaryを使用する必要があります。AudioContextはOperaではサポートされていません。

Chrome Canaryをインストールして起動したら、APIを有効にします。 アドレスバーに、「about:flags」と入力します。「Enable MediaStream(MediaStream を有効にする)」オプションで、トグルスイッチをクリックしてAPIを有効にします。

最後に、セキュリティ制限により、ローカルのfile:///ではビデオカメラにアクセスできないため、このサンプルファイルはローカルのWebサーバー内で実行する必要があります。

すべての準備が整って有効になったら、videoタグを追加してWebカメラのストリームを再生します。受信したストリームは、JavaScriptを使ってvideoタグのsrcプロパティに関連付けられます。

<video id="webcam" autoplay width="640" height="480"></video>

このJavaScriptでは、2つの手順を実行します。まず、ユーザーのブラウザーでgetUserMedia APIが使用可能であるかどうかを確認します。

function hasGetUserMedia() { return !!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); }

次に、ユーザーのWebカメラからストリームの取得を試みます。

注意:この記事およびこのデモではjQueryを使用していますが、ネイティブの document.querySelectorなど、好みのセレクターを自由に使用して構いません。

var webcamError = function(e) { alert('Webcam error!', e); }; var video = $('#webcam')[0]; if (navigator.getUserMedia) { navigator.getUserMedia({audio: true, video: true}, function(stream) { video.src = stream; }, webcamError); } else if (navigator.webkitGetUserMedia) { navigator.webkitGetUserMedia({audio:true, video:true}, function(stream) { video.src = window.webkitURL.createObjectURL(stream); }, webcamError); } else { //video.src = 'video.webm'; // fallback. }

これで、HTMLページにWebカメラのストリームを表示する準備ができました。次の節では、デモで使用される構造とアセットの概要について説明します。

実際の環境でgetUserMediaを使用する場合は、次に示すAddy Osmaniのライブラリをお勧めします。AddyはFlashによる代替手段を持つ、getUserMedia API用のshimを作成しています。

ブレンドモード「差の絶対値」

ユーザーの動きは、ブレンドモード「差の絶対値」を使用して検出します。

差の絶対値を使って2つの画像を合成することは、ピクセル値の引き算をすることと同じです。Wikipediaのブレンドモードに関する記事では、差の絶対値について次のような説明があります。「差の絶対値は、常に正の値が得られるように、下のレイヤーから上のレイヤーを引き算する、またはその逆を実行する。黒と合成すると、すべてのカラー値が0なので、変化は生じない。」

この操作を実行するには2つの画像が必要です。このコードでは、一方の画像の各ピクセルをループし、最初の画像のカラーチャンネルと2番目の画像のカラーチャンネルの差を求めます。

例えば、2つの赤色の画像がある場合、両方の画像のピクセル単位のカラーチャンネルは次のようになります。

-赤:255(0xFF)

-緑:0

-青:0

次の操作では、これらの画像のカラー値の差を求めます。

-赤:255 – 255 = 0

-緑:0 – 0 = 0

-青:0 – 0 = 0

つまり、ブレンドモード「差の絶対値」を2つの同じ画像に適用すると、黒色の画像が生成されます。これがどのように役立つのか、また、これらの画像がどのように生成されるのかについて説明していきます。

このプロセスを順を追って説明します。まず、一定の間隔で、Webカメラのストリームから取得した画像をキャンバスに描画します。このデモでは、必要な数より多くなりますが、毎秒60コマの画像を描画します。Webカメラのストリームに表示されている現在の画像は、もう1つの画像と合成する最初の画像です。

2番目の画像は、前回のインターバルでWebカメラからキャプチャした別の画像です。2つの画像が揃ったので、ピクセル値を引き算します。画像が同一である場合、つまり、ユーザーがまったく動いていない場合は、黒い画像が生成されます。

しかし、ユーザーが動き始めると変化が起きます。現在のインターバルで取得された画像が、前回のインターバルで取得された画像とわずかに異なるとします。異なる値の差を取ると、何らかの色が見え始めます。これは、2つのフレーム間で何かが移動したことを意味します。

これでモーション検出プロセスはほぼ完了したので、状況が見えてきました。最後の手順では、合成された画像のすべてのピクセルをループして、黒でないピクセルがあるかどうかを確認します。

アセットの準備

このデモを実行するには、すべてのWebサイトに共通するいくつかのものが必要となります。canvasタグとvideoタグが含まれたHTMLページや、アプリケーションを実行するためのJavaScriptはもちろん必要です。

次に、木琴の画像が必要になりますが、背景が透明の画像(png)が理想的です。さらに、木琴の各鍵盤の画像を、輝度を少し変更した個別の画像として用意する必要があります。ロールオーバー効果を使用すると、トリガーされた鍵盤が強調表示されて、ユーザーに視覚的なフィードバックを提供できます。

各鍵盤の音を含むmp3ファイルなどのオーディオファイルも必要です。音のしない木琴を演奏しても楽しくないからです。

最後に、ユーザーが自身のWebカメラを使うことができない、または使いたくない場合に備えて、代替用のデモビデオを表示することも重要です。ビデオを作成し、そのビデオを、すべてのブラウザーに対応できるようにmp4、ogg、およびwebmにエンコードする必要があります。次の節では、ビデオのエンコードに使用するツールのリストを紹介します。すべてのアセットは、js_motion_detection.zipというデモのzipファイルに含まれています。

HTML5ビデオのエンコード

代替用のデモビデオを、HTML5のビデオを表示するためによく使われる3つのファイル形式にエンコードしました。

webm形式は少し厄介でした。エンコードのために見つけたほとんどのツールに、ビットレートを選択するオプションがなかったからです。ビットレートは、ビデオファイルのサイズと品質の両方を設定するもので、通常はkbps(1秒当たりのキロビット数)で表します。最終的には、WebM形式(VP8)に変換するためのOnline ConVertのVideo converterを使用することでうまくいきました。

mp4バージョンの場合は、Adobe Media EncoderAfter EffectsなどのAdobe Creative Suiteのツールを使用できます。または、Handbrakeのような無料の代替ツールを使用することもできます。

ogg形式では、同時にすべての形式にエンコードできるツールを使用しました。webm形式とmp4形式については、品質に満足することができませんでした。品質を変更することはできないように思われましたが、ogg形式のビデオは良好でした。Easy HTML5ビデオまたはMiroのいずれかを使用できます。

HTMLの準備

これは、JavaScriptのコーディングを開始する前の最後の手順です。HTMLタグをいくつか設定します。ここに必要なHTMLタグを示します。(代替用のデモビデオのコードなど、使用したすべてのものを記載したわけではありません。それらはソースでダウンロードできます。)

ユーザーのWebカメラのフィードを受信するための単純なvideoタグが必要です。autoplayプロパティは忘れずに設定してください。これがないと、受信した最初のフレームでストリームが一時停止します。

<video id="webcam" autoplay width="640" height="480"></video>

videoタグは実際には表示されません。videoタグのcssの表示スタイルは、「None(なし)」に設定されています。代わりに、canvasで受信したストリームを描画して、モーション検出に使用できるようにします。キャンバスを作成してWebカメラのストリームを描画します。

<canvas id="canvas-source" width="640" height="480"></canvas>

モーション検出中に起きていることをリアルタイムに表示するには、もう1つのcanvasが必要です。

<canvas id="canvas-blended" width="640" height="480"></canvas>

木琴の画像を含むdivを作成します。 木琴をWebカメラの上に配置します。ユーザーは自身の手を使ってバーチャルに木琴を演奏できます。木琴の上には、非表示の鍵盤を配置します。これらの鍵盤は、トリガーされるとロールオーバーで表示されます。

<div id="xylo"> <div id="back"><img id="xyloback" src="images/xylo.png"/></div> <div id="front"> <img id="note0" src="images/note1.png"/> <img id="note1" src="images/note2.png"/> <img id="note2" src="images/note3.png"/> <img id="note3" src="images/note4.png"/> <img id="note4" src="images/note5.png"/> <img id="note5" src="images/note6.png"/> <img id="note6" src="images/note7.png"/> <img id="note7" src="images/note8.png"/> </div> </div>

JavaScriptによるモーション検出

このデモを実行するためのJavaScriptの手順は次のとおりです。

  • アプリケーションでgetUserMedia APIが使用可能かどうかを検出します。
  • Webカメラのストリームが受信されているかどうかを検出します。
  • 木琴の鍵盤の音をロードします。
  • インターバルを開始して、update関数を呼び出します。
  • インターバルごとに、Webカメラのフィードをキャンバスに描画します。
  • インターバルごとに、現在のWebカメラの画像を前回のWebカメラの画像に合成します。
  • インターバルごとに、合成した画像をキャンバスに描画します。
  • インターバルごとに、木琴の鍵盤の領域にあるピクセルカラーの値を確認します。
  • インターバルごとに、モーションが検出されると、木琴の特定の鍵盤の音を再生します。

手順1:変数の準備

描画とモーション検出に使用するものを格納するための変数を準備します。キャンバスへの2つの参照、各キャンバスのコンテキストを格納するための変数、描画されたWebカメラのストリームを格納するための変数が必要です。また、木琴の各鍵盤のx軸の位置、再生する音、および音に関連する変数も格納します。

var notesPos = [0, 82, 159, 238, 313, 390, 468, 544]; var timeOut, lastImageData; var canvasSource = $("#canvas-source")[0]; var canvasBlended = $("#canvas-blended")[0]; var contextSource = canvasSource.getContext('2d'); var contextBlended = canvasBlended.getContext('2d'); var soundContext, bufferLoader; var notes = [];

また、ユーザーが鏡の前にいるような気分になるように、Webカメラのストリームのx軸を反転します。これにより、演奏するユーザーの動きが少し楽になります。次にその方法を示します。

contextSource.translate(canvasSource.width, 0); contextSource.scale(-1, 1);

手順2:ビデオの更新と描画

updateという関数を作成します。この関数は、毎秒60回実行されます。また、Webカメラのストリームをキャンバスに描画し、画像を合成して、モーションを検出するほかの関数を呼び出します。

function update() { drawVideo(); blend(); checkAreas(); timeOut = setTimeout(update, 1000/60); }

ビデオをキャンバスに描画することはとても簡単です。次に示すように、たった1行で済みます。

function drawVideo() { contextSource.drawImage(video, 0, 0, video.width, video.height); }

手順3:ブレンドモード「差の絶対値」の作成

helper関数を作成し、引き算の結果が常に正の値になるようにします。組み込みの関数であるMath.absを使用することもできますが、ここでは、2項演算子を使用して同等のものを作成しました。多くの場合、2項演算子を使用した方が、良いパフォーマンスが得られます。このことを正確に理解する必要はありません。そのまま使用してください。

function fastAbs(value) { // equivalent to Math.abs(); return (value ^ (value >> 31)) - (value >> 31); }

それでは、ブレンドモード「差の絶対値」を作成してみます。この関数は、次の3つのパラメーターを受け取ります。

  • 引き算の結果を格納するピクセルのフラット配列
  • Webカメラのストリームから取得した現在の画像のピクセルのフラット配列
  • Webカメラのストリームから取得した前回の画像のピクセルのフラット配列

ピクセルの配列はフラット化され、赤、緑、青、アルファのカラーチャンネル値を含みます。

  • pixels[0] =赤の値
  • pixels[1] =緑の値
  • pixels[2] =青の値
  • pixels[3] =アルファ値
  • pixels[4] =赤の値
  • pixels[5] =緑の値
  • のように続けることができます。

このデモでは、Webカメラのストリームのサイズは、幅640ピクセル×高さ480ピクセルです。配列のサイズは、640 * 480 * 4 = 1,228,000になります。

ピクセルの配列をループする最善の方法は、4つ(赤、緑、青、アルファ)ずつ増分していく方法です。これによって反復回数は307,200となり、かなり改善されます。

function difference(target, data1, data2) { var i = 0; while (i < (data1.length / 4)) { var red = data1[i*4]; var green = data1[i*4+1]; var blue = data1[i*4+2]; var alpha = data1[i*4+3]; ++i; } }

これで、画像のピクセル値の引き算ができます。パフォーマンス向上のために(これによって大きな違いが出ると思われます)、カラーチャンネル値が既に0で、アルファ値が自動的に255(0xFF)に設定されている場合は、引き算を実行しません。ここに、完成したブレンドモードのdifference関数を示します(自由に最適化してください)。

function difference(target, data1, data2) { // blend mode difference if (data1.length != data2.length) return null; var i = 0; while (i < (data1.length * 0.25)) { target[4*i] = data1[4*i] == 0 ? 0 : fastAbs(data1[4*i] - data2[4*i]); target[4*i+1] = data1[4*i+1] == 0 ? 0 : fastAbs(data1[4*i+1] - data2[4*i+1]); target[4*i+2] = data1[4*i+2] == 0 ? 0 : fastAbs(data1[4*i+2] - data2[4*i+2]); target[4*i+3] = 0xFF; ++i; } }

デモでは、精度を上げるために少し異なるバージョンを使用しました。カラー値に適用するthreshold関数を作成しました。このメソッドは、ピクセルのカラー値を、特定の範囲を下回る場合は黒に、その範囲を上回る場合は白に変更します。このメソッドはそのまま使用することもできます。

function threshold(value) { return (value > 0x15) ? 0xFF : 0; }

また、3つのカラーチャネル値の平均値を算出しました。これにより、ピクセル値が黒または白のいずれかの画像が生成されます。

function differenceAccuracy(target, data1, data2) { if (data1.length != data2.length) return null; var i = 0; while (i < (data1.length * 0.25)) { var average1 = (data1[4*i] + data1[4*i+1] + data1[4*i+2]) / 3; var average2 = (data2[4*i] + data2[4*i+1] + data2[4*i+2]) / 3; var diff = threshold(fastAbs(average1 - average2)); target[4*i] = diff; target[4*i+1] = diff; target[4*i+2] = diff; target[4*i+3] = 0xFF; ++i; } }

結果は、次に示すような白黒の画像になります。

手順4:合成キャンバス

画像を合成する関数を準備できたので、後はその関数に正しい値、つまり、ピクセルの配列を送る必要があります。

JavaScriptの描画APIには、ImageDataオブジェクトのインスタンスを取得するメソッドが用意されています。このオブジェクトには、幅や高さなどの便利なプロパティや、必要なピクセル配列であるデータプロパティが含まれています。また、空のImageDataインスタンスを作成して結果を取得し、次のインターバルのために、現在のWebカメラの画像を格納することもできます。画像を合成して結果をキャンバスに描画する関数をここに示します。

function blend() { var width = canvasSource.width; var height = canvasSource.height; // get webcam image data var sourceData = contextSource.getImageData(0, 0, width, height); // create an image if the previous image doesn’t exist if (!lastImageData) lastImageData = contextSource.getImageData(0, 0, width, height); // create a ImageData instance to receive the blended result var blendedData = contextSource.createImageData(width, height); // blend the 2 images differenceAccuracy(blendedData.data, sourceData.data, lastImageData.data); // draw the result in a canvas contextBlended.putImageData(blendedData, 0, 0); // store the current webcam image lastImageData = sourceData; }

手順5:ピクセルの検索

このデモの最後の手順では、前の節で作成した合成画像を使用してモーション検出を行います。

アセットを準備する際に、木琴の8つの鍵盤の画像を配置して、それらの画像をロールオーバーとして使用します。これらの鍵盤の位置とサイズを矩形領域として使用し、合成画像からピクセルを取得します。次に、これらのピクセルをループして白いピクセルを探します。

ループの中で、カラーチャンネルの平均値を取得し、その結果を変数に追加します。ループが完了すると、この領域のすべてのピクセルのグローバルな平均値が取得できます。

下限を10に設定することで、ノイズや小さな動きを回避します。値が10を超えた場合は、最後のフレームから何かが移動したとみなします。これがモーション検出です。

そして、該当する音を再生し、鍵盤のロールオーバーを表示します。この関数は次のようになります。

function checkAreas() { // loop over the note areas for (var r=0; r<8; ++r) { // get the pixels in a note area from the blended image var blendedData = contextBlended.getImageData( notes[r].area.x, notes[r].area.y, notes[r].area.width, notes[r].area.height); var i = 0; var average = 0; // loop over the pixels while (i < (blendedData.data.length / 4)) { // make an average between the color channel average += (blendedData.data[i*4] + blendedData.data[i*4+1] + blendedData.data[i*4+2]) / 3; ++i; } // calculate an average between of the color values of the note area average = Math.round(average / (blendedData.data.length / 4)); if (average > 10) { // over a small limit, consider that a movement is detected // play a note and show a visual feedback to the user playSound(notes[r]); notes[r].visual.style.display = "block"; $(notes[r].visual).fadeOut(); } } }

次のステップ

この記事が、ビデオベースのインタラクションを作成するための新しいアイデアをもたらし、私のようなFlash開発者が長年携わってきた分野(事前に作成されたビデオまたはWebカメラのストリームをベースとしたビデオインタラクションの活用)への足がかりになることを願っています。

getUserMedia APIの詳細情報は、ChromeブラウザーおよびOperaブラウザーの開発情報や、W3CのWebサイトにあるAPIのドラフトから入手できます。

モーション検出やビデオベースのアプリケーションに関するクリエイティブなアイデアがほしい場合は、Flash開発者やモーション設計者によるAfter Effectsを使用した実験を参考にすることをお勧めします。JavaScriptとHTMLの勢いが増し、新しい機能が増加している現状では、学習すべきことがたくさんあります。ぜひ次のリソースを参照して実験してみてください。