このページの本文へ

PROGRAMMING 古籏一浩のJavaScriptラボ第82回

iOS 5で使えるWeb Workersでカメラアプリ作ってみた

2012年02月22日 11時00分更新

古籏一浩

  • この記事をはてなブックマークに追加
本文印刷

Web Workersを使わずに画像を処理する

 最初に、Web Workersを使わずに画像処理を実行してみましょう。カメラ画像の取り込みは後回しにするとして、あらかじめ用意してある画像をCanvasに描画し、グレースケールに変換してみます(カラー画像が白黒画像になる)。

 Canvas内にあるピクセルデータは以下のようにして読み出します。


var imageData = context.getImageData(0,0, canvasObj.width, canvasObj.height);
var pixelData = imageData.data;

 pixelDataは配列形式で構成され、4要素(RGBα)で1ピクセルを表しています。これが画像のピクセル分あるわけです。画像をグレースケールに変換するにはRGBの値を読み出し、「緑×0.6+赤×0.3+青×0.1」でピクセルの輝度を求めます。この値を配列に書き戻して、putImageData()メソッドを使ってCanvasに描画します。

 実際のプログラムはサンプル1です。画像サイズが小さいので、iPhone 4Sで実行してもすぐに処理が終わります。

【図】fig1.png
処理前。「画像フィルタ」ボタンをタップすると...
【図】fig2.png
グレースケールに変換される

 次に、Canvasのサイズを大きくして処理させてみましょう。


<canvas id="myCanvas" width="1500" height="3000" style="border:1px solid black"></canvas>

 この場合も、iPhone 4Sでは数秒で処理が完了しますが、処理中はページのスクロールやボタンなどのUI操作を受け付けません(*1)。より負荷の高い処理を実行すると、かなり長い間、操作ができなくなります。こんなときに、Web Workersの出番です。

【図】fig3.png
Canvasサイズを大きくして実行。数秒間はUI操作ができない
【図】fig4.png
グレースケールに変換された後であれば、UI操作が可能になる

■サンプル1


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content= "initial-scale=1, user-scalable=no">
    <title>画像フィルタ</title>
  </head>
  <body>
    <h1>画像フィルタ</h1>
    <form>
      <input type="button" id="effect" value="画像フィルタ">
    </form>
    <canvas id="myCanvas" width="200" height="300" style="border:1px solid black"></canvas>
    <script>
      // Canvasに画像を描画
      var canvasObj = document.getElementById("myCanvas");
      var context = canvasObj.getContext("2d");
      var canvasW = canvasObj.width;
      var canvasH = canvasObj.height;
      var imgObj = new Image();
      imgObj.src = "photo.jpg"; // 画像のURLを指定
      imgObj.onload = function(){
        context.drawImage(imgObj,0,0, canvasObj.width, canvasObj.height);
      }
      // クリックしたらエフェクト処理を開始
      document.getElementById("effect").addEventListener("click", function(){
        var imageData = context.getImageData(0,0, canvasObj.width, canvasObj.height);
        var pixelData = imageData.data;  // ピクセルデータを読み出し
        for(var y=0; y<canvasObj.height; y++){
          for(var x=0; x<canvasObj.width; x++){
            var pointer = (y * imageData.width + x ) * 4; // RGBαで4配列
            var red = pixelData[pointer + 0];
            var green = pixelData[pointer + 1];
            var blue = pixelData[pointer + 2];
            var gray = Math.floor(green * 0.6 + red * 0.3 + blue * 0.1);
            pixelData[pointer + 0] = gray;
            pixelData[pointer + 1] = gray;
            pixelData[pointer + 2] = gray;
          }
        }
        context.putImageData(imageData, 0, 0);
      }, false);
    </script>
  </body>
</html>

*1 Android 2.x〜4.0ではサンプル1のように処理してもUI操作は可能です。また、Android 4を搭載したGalaxy Nexusは、iOS5と比較して非常に高速に画像を処理できます。

Web Workersによる画像処理

 サンプル1をWeb Workersを使ったプログラムに書き換えてみましょう。処理に時間がかかっているCanvas内のピクセル操作部分(グレースケール変換処理)を抜き出し、「effect.js」という別ファイルにして保存します。このeffect.jsが、ワーカー内で処理するスクリプトになります。なお、呼び出し元とワーカー内ではスコープが異なるため、同名の変数や関数を使っても名前の衝突による不具合は起きません。

 ワーカーを利用するには、処理するデータをワーカーに渡し、ワーカー内の処理が終わったらワーカーから呼び出し元へデータを返します。iOS5では、Canvas内のピクセルデータを直接ワーカーに渡すとエラーになるので、いったん配列にコピーし、その配列をワーカーに渡す必要があります。

 サンプル1をWeb Workersを使って書き直したのがサンプル2です。サンプル1に比べて処理速度はかなり低下しますが、ピクセル処理中でもUI操作を受け付けます。

【図】fig5.png
ボタンをタップするとワーカーでの画像処理が開始される
【図】fig6.png
ワーカーでの処理が終わるとアラートダイアログが表示される
【図】fig7.png
画像がグレースケールに変換される

■サンプル2


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content= "initial-scale=1">
    <title>画像フィルタ</title>
  </head>
  <body>
    <h1>画像フィルタ</h1>
    <form>
      <input type="button" id="effect" value="画像フィルタ">
    </form>
    <canvas id="myCanvas" width="1000" height="1500" style="border:1px solid black"></canvas>
    <script>
      // Canvasに画像を描画
      var canvasObj = document.getElementById("myCanvas");
      var context = canvasObj.getContext("2d");
      var canvasW = canvasObj.width;
      var canvasH = canvasObj.height;
      var imgObj = new Image();
      imgObj.src = "photo.jpg"; // 画像のURLを指定
      imgObj.onload = function(){
        context.drawImage(imgObj,0,0, canvasObj.width, canvasObj.height);
      }
      // クリックしたらエフェクト処理を開始
      document.getElementById("effect").addEventListener("click", function(){
        var imageData = context.getImageData(0,0, canvasObj.width, canvasObj.height);
        // ピクセルデータを配列にコピーする(Firefoxでは不要)
        var pixelData = new Array();
        for(var i=0; i<imageData.data.length; i++){
          pixelData[i] = imageData.data[i];
        }
        // ワーカー生成&イベント設定
        var effectWorker = new Worker("effect.js");
        effectWorker.addEventListener("message", function(event){
          pixelData = null; // メモリ解放
          alert("ワーカーの処理が終了しました");
          var outputData = context.createImageData(canvasW, canvasH);
          // ワーカーから渡されたピクセルを配列にコピー
          for(var i=0; i<event.data.length; i++){
            outputData.data[i] = event.data[i];
          }
          context.putImageData(outputData, 0, 0);
        }, true);
        // ワーカーのエフェクト処理を実行
        effectWorker.postMessage({
          pixels: pixelData,  // ピクセルデータ(配列)
          width: canvasW,   // Canvasの横幅
          height: canvasH   // Canvasの縦幅
        });
      }, false);
    </script>
  </body>
</html>

■effect.js(ワーカー側のスクリプト)


addEventListener("message", function(event){
  var pixelData = event.data.pixels;
  for(var y=0; y<event.data.height; y++){
    for(var x=0; x<event.data.width; x++){
      var pointer = (y * event.data.width + x ) * 4// RGBαで4配列
      var red = pixelData[pointer + 0];
      var green = pixelData[pointer + 1];
      var blue = pixelData[pointer + 2];
      var gray = Math.floor(green * 0.6 + red * 0.3 + blue * 0.1);
      pixelData[pointer + 0] = gray;
      pixelData[pointer + 1] = gray;
      pixelData[pointer + 2] = gray;
    }
  }
  postMessage(pixelData);  // 終了したら呼び出し元にピクセルデータを返す
}, false);

処理対象の画像のサイズによっては、変換結果が表示に反映されない場合があります。ピンチイン/ピンチアウトすると画面が書き換わり、変換した画像が表示されます。もしくは<meta name="viewport" content= "initial-scale=1">を削除し、画像がページ全体に表示されるようにすると、不具合は解消されます。

Web Professionalトップページバナー

この記事の編集者は以下の記事をオススメしています

ASCII.jp会員サービス 週刊Web Professional登録

Webディレクター江口明日香が行く