JavaScriptで画像データ(ImageData)を使ってみる



JavaScriptで画像データ(ImageData)を使ってみる

Webブラウザ上にJavaScriptのコードで絵を描けるcanvas要素。そのcanvas要素には、ピクセルごとの色(RGBA)データであるImageDataオブジェクトを作成・設定する機能が用意されています。自分で作成したImageData(画像データ)をcanvas要素に反映させれば、標準の描画APIにない描画処理を行うこともできるわけです。

そんな無限の可能性を秘めたImageDataを使ってみることにしましょう。

目次
  1. ImageDataの利用法
  2. 画像データへのアクセス
  3. canvas要素の画像からImageDataを作成
  4. ImageDataで画像処理Webアプリを

ImageDataの利用法

ImageDataは、画像データ(フレームバッファ)本体や画像サイズなどいくつかのプロパティをまとめたオブジェクトです。画像処理プログラミングの経験をお持ちの方なら、「RGBA形式の32ビット(4バイト)単位フレームバッファ」として扱うとわかりやすいかもしれません。

自分で画像データを設定するためのImageDataは、canvas要素を扱うcontextのcreateImageData()で作成することができます。

たとえば256×256のcanvas要素test_canvasから同サイズのImageDataを作成するには、以下のようにします。

var test_context = document.getElementById('test_canvas').getContext('2d');

var image_data = test_context.createImageData(256, 256);

引数に幅と高さを指定するわけですね。

これで256×256ピクセル、合計65536ピクセル分の画像データを保持するImageDataが作成されました。

ImageData自体はcanvas要素に直接依存するわけではないので、最近のWebブラウザではcanvas要素のcontextを通さずにImageDataを作成するコンストラクタも利用できるようになりつつあります。

そのコンストラクタを使うと、以下のように1行でImageDataを作成することができcanvas要素もcontextも必要ありません。ただし、Webブラウザによっては動作しない可能性もあるので、公開用のWebアプリなどで使う場合は注意が必要でしょう。

var image_data = new ImageData(256, 256);

画像データへのアクセス

ImageDataの画像データは、dataプロパティに8ビット整数(符号なし)の配列として格納されます。一つのピクセルの色情報はRGBAの順に4つの要素を占め、全体ではピクセル数×4つの要素を持つ配列になっています。

データの格納順序は、一般的な画像データと同じく左から右を一列とし、その一列分のデータが上から下の順に格納される形式です。横一列のピクセルの色情報を格納した横幅×4つ分の要素が、上から順に高さ分だけ並んでいるイメージになります。

今回は横256ピクセルのImageDataを作成したので、(x, y)のピクセルの色情報は

(x + y * 256) * 4

の位置から4つ並んでいるわけですね。各要素の意味は、

image_data.data[(x + y * 256) * 4] = 赤成分
image_data.data[(x + y * 256) * 4 + 1] = 緑成分
image_data.data[(x + y * 256) * 4 + 2] = 青成分
image_data.data[(x + y * 256) * 4 + 3] = アルファ成分

になります。各成分の値は、いずれも0-255の8ビット整数です。

大きなImageDataを扱う場合は、配列のインデックス計算も無視できない計算量になる場合があるので、最初に

var index = (x + y * 256) * 4;

などと「ピクセルの先頭からの位置」を計算し、その値に+1、+2、+3していく形でインデックスを求めていく方が良いかもしれません。

こうして作成したImageDataは、contextのputImageData()にImageData、x座標、y座標の順で渡して描画することができます。

試しに、contextからImageDataを作成して中心付近に青い点を四つ打ちcanvas要素に表示してみましょう。

<!DOCTYPE html>
<html>
<body>

<canvas id="test_canvas" width=256 height=256 style="border: 1px solid;"></canvas>

<script>

var test_context = document.getElementById('test_canvas').getContext('2d');

var image_data = test_context.createImageData(256, 256);

image_data.data[(128 + 128 * 256) * 4] = 255;
image_data.data[(128 + 128 * 256) * 4 + 1] = 0;
image_data.data[(128 + 128 * 256) * 4 + 2] = 0;
image_data.data[(128 + 128 * 256) * 4 + 3] = 255;

image_data.data[(129 + 128 * 256) * 4] = 255;
image_data.data[(129 + 128 * 256) * 4 + 1] = 0;
image_data.data[(129 + 128 * 256) * 4 + 2] = 0;
image_data.data[(129 + 128 * 256) * 4 + 3] = 255;

image_data.data[(128 + 129 * 256) * 4] = 255;
image_data.data[(128 + 129 * 256) * 4 + 1] = 0;
image_data.data[(128 + 129 * 256) * 4 + 2] = 0;
image_data.data[(128 + 129 * 256) * 4 + 3] = 255;

image_data.data[(129 + 129 * 256) * 4] = 255;
image_data.data[(129 + 129 * 256) * 4 + 1] = 0;
image_data.data[(129 + 129 * 256) * 4 + 2] = 0;
image_data.data[(129 + 129 * 256) * 4 + 3] = 255;

test_context.putImageData(image_data, 0, 0);
</script>

</body>
</html>

Webブラウザで表示すると、枠で囲われたcanvas要素の中央付近に小さな赤い点が描かれているのがわかると思います。

image

この例では、各ピクセルの色データのうち最後のアルファ(不透明度)を最大の255にしている点に注意してください。アルファの設定を行わないとアルファ値0(完全透明)として扱われるため、見えないピクセルになってしまいます。

赤い点が描かれているのを確認したら、各ピクセルの2番目や3番目の値を変えて指定した値がどの色に対応するか確認してみましょう。

小さな点を打つだけでは寂しいので、全体をグラデーションで覆ってみましょうか。今回作成するcanvas要素はちょうど256ピクセル四方(座標値で0-255)ですから、x方向を赤、y方向を緑のグラデショーンにしてみます。

<!DOCTYPE html>
<html>
<body>

<canvas id="test_canvas" width=256 height=256 style="border: 1px solid;" onclick="test_process()"></canvas>

<script>

var test_context = document.getElementById('test_canvas').getContext('2d');

var image_data = test_context.createImageData(256, 256);

for (var y = 0;y < 256;y++) {
    for (var x = 0;x < 256;x++) {

        var r = x;
        var g = y;
        var b = 0;

        image_data.data[(x + y * 256) * 4] = r;
        image_data.data[(x + y * 256) * 4 + 1] = g;
        image_data.data[(x + y * 256) * 4 + 2] = 0;
        image_data.data[(x + y * 256) * 4 + 3] = 255;

    }
}

test_context.putImageData(image_data, 0, 0);

</script>

</body>
</html>

単純に赤成分をx座標の値に、緑成分をy座標の値にしただけですが、なかなか奇麗なグラデーションが表示されました。ImageDataでは、純粋に「計算」で色を設定できるので、工夫次第ではごく簡単な数式で驚くほど幻想的な画像を作成することもできます。

image

canvas要素上に表示されている画像は、右クリックなどで「名前を付けて保存」できますから、鑑賞(?)用に保存しておくのもよいでしょう。

canvas要素の画像からImageDataを作成

続いて、「canvas要素に描かれている画像のピクセル色情報」をImageDataとして取得してみましょう。canvas要素に現在表示されている画像を、RGBA形式の画像データとして取得するわけです。

canvas要素の画像データを格納したImageDataは、contextのgetImageData()で取得できます。ImageDataの引数は、左上x座標、左上y座標、横幅、高さの4つです。

先の例で今回作成したcanvas要素test_canvasは256×256でしたので、canvas全体のImageDataを取得するには以下のようにします。

var test_context = document.getElementById('test_canvas').getContext('2d');

var image_data = test_context.getImageData(0, 0, 256, 256);

getImageData()でcanvas要素に表示されている画像のImageDataを取得する場合は、何らかの処理を行って処理結果をputImageDataで再びcanvas要素に反映させる、という流れになる場合が多いと思います。

その一連の流れを試すために、canvas要素にいくつか長方形を描いてcanvas要素がクリックされたら色を反転させる(255から各成分の値を減算する)画像処理を行うhtmlファイルを作ってみました。

<!DOCTYPE html>
<html>
<body>

<canvas id="test_canvas" width=256 height=256 style="border: 1px solid;" onclick="test_process()"></canvas>

<script>

function test_process() {

    var image_data = test_context.getImageData(0, 0, 256, 256);

    for (var i = 0;i < 256 * 256;i++) {

        var r = 255 - image_data.data[i * 4];
        var g = 255 - image_data.data[i * 4 + 1];
        var b = 255 - image_data.data[i * 4 + 2];

        image_data.data[i * 4] = r;
        image_data.data[i * 4 + 1] = g;
        image_data.data[i * 4 + 2] = b;

    }

    test_context.putImageData(image_data, 0, 0);

}

var test_context = document.getElementById('test_canvas').getContext('2d');

test_context.fillStyle = '#ff0000';
test_context.fillRect(0, 0, 256, 128);

test_context.fillStyle = '#404040';
test_context.fillRect(0, 128, 256, 128);

test_context.fillStyle = '#00ff00';
test_context.fillRect(64, 128, 64, 64);

test_context.fillStyle = '#0000ff';
test_context.fillRect(128, 128, 64, 64);

</script>

</body>
</html>

Webブラウザでhtmlファイルを開いたら、枠で囲われたcanvas要素の部分をクリックしてみてください。クリックする度に色が反転します。

image

putImageData()は、追加の引数で描画する領域を設定することもできます。描画領域を設定する場合の引数は、「ImageData、x座標、y座標、描画領域左上x座標、描画領域左上y座標、、描画領域横幅、描画領域高さ」です。

先の例のtest_process()内にあるputImageDataを以下のように書き換えてみましょう。これで、ImageDataの画像は中央付近の64×64ピクセルの部分だけに反映されるようになります。

    test_context.putImageData(image_data, 0, 0, 96, 96, 64, 64);

この描画範囲を設定(限定)する機能は、画像処理アプリを作る際に必要な部分だけ更新することで性能を向上させるために利用することもできます。

ImageDataで画像処理Webアプリを

ImageDataに画像データを作成し、canvas要素に反映させる、また現在canvas要素に描かれている画像の画像データを取得する……今回の実験で、「ユーザーの操作に応じて画像を取得・処理して、結果を表示する」画像処理アプリの基本部分が実現できたことになります。

インストール不要のWebアプリ形式で画像処理機能を実現できれば、移動中に撮影した写真をボタン一つで「奇麗」にするなど日常生活に密着したお手軽画像系サイトを提供できるかもしれませんね。

関連記事

宍戸輝光
この記事を書いた人
宍戸輝光
\無料体験開催中!/自分のペースで確実に習得!
オンライン・プログラミングレッスンNo.1のCodeCamp