【javaScript学習】ImageDataへの画像読み込みと画像処理

【javaScript学習】ImageDataへの画像読み込みと画像処理

ImageDataを使うと、canvas要素の画像を画像(数値)データとして取得したり、独自に作成した画像データをcanavas要素に描画できます。画像を数値データとして取得できれば、独自の画像処理を行うこともできそうですね。

実際に、画像に対して独自の画像処理を行うWebアプリを実現するには、何が必要なのか。一通りの流れを作ってみることにしましょう。

目次
  1. ImageDataに画像を読み込む
  2. canvas要素への画像描画とImageData作成の流れ
  3. ユーザーが選択した画像を読み込む
  4. 読み込んだ画像のデータをいじるには

##ImageDataに画像を読み込む

画像処理を行うためには、まず画像をImageData(数値データ配列として処理できるImageDataのdataプロパティ)に読み込む必要があります。

ImageDataは、「canvas要素の画像」を取り込めますから、「画像を描画したcanvas要素」があれば、そこからImageDataを作成できそうです。これは、canvas要素に画像を描くcontextのdrawImage()で実現できます。

画像を描画したら、現在のcanvas要素の画像をImageDataとして取得するcontext.getImageData()でImageDataを取り出す、という感じでしょうか。

###canvas要素への画像描画とImageData作成の流れ

以上のことをプログラムの手順としてまとめると、以下のようになります。

  1. img要素のsrc属性に画像の場所を指定して画像を読み込む
  2. canvas要素を用意し、そのcontextを取得する
  3. contextのdrawImage()でimg要素をcavnas要素に描画する
  4. contextのgetImageData()でImageDataを作成

この流れでは、canvas要素をWebブラウザ上に表示する(Webブラウザ上のHTMLの中に入れ込む)必要はありませんから、canvas要素はJavaScriptの作業用変数として作成すればよいでしょう。

canvas要素は、img要素と同じ大きさ(width/height)で作成し、getContext()で描画用のcontextを取得します。続いてimg要素の描画とImageDataの取得を行えば、「img要素の画像から作成した画像データ」を持つImageDataの完成です。

img要素からImageDataを作る一連の流れを関数にまとめると、以下のようになります。

function createImageData(img) {

    var cv = document.createElement('canvas');

    cv.width = img.naturalWidth;
    cv.height = img.naturalHeight;

    var ct = cv.getContext('2d');

    ct.drawImage(img, 0, 0);

    var data = ct.getImageData(0, 0, cv.width, cv.height);

    return data;

}

createElement()でWebブラウザに表示されるHTMLとは別にcanvas要素を作成し、そのcanvas要素にimg要素の画像を描画(転送)してImageDataを取得するわけですね。

img要素の大きさは、ブラウザの表示サイズなどに依存しないようnaturalWidth/naturalHeightで取得しています。

この関数を使って「同じディレクトリにあるtest.jpgを読み込んだimg要素を表示し、ボタンがクリックされたらImageDataを作成して下のcanvas要素に描く」htmlファイルを作ってみました。

<!DOCTYPE html>
<html>
<body>

<script>

function test() {

    var data = createImageData(document.getElementById('test_img'));

    document.getElementById('test_canvas').getContext('2d').putImageData(data, 0, 0);
}

function createImageData(img) {

    var cv = document.createElement('canvas');

    cv.width = img.naturalWidth;
    cv.height = img.naturalHeight;

    var ct = cv.getContext('2d');

    ct.drawImage(img, 0, 0);

    var data = ct.getImageData(0, 0, cv.width, cv.height);

    return data;

}

</script>

<p>
<img src="test.jpg" id="test_img">
<button onclick="test()">draw</button>
</p>

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

</body>
</html>

適当なディレクトリに適当な画像をtest.jpgとして配置したら、このhtmlファイルも同じディレクトリに作成していろいろなブラウザで表示してみてください。

image

実際に試してみると、FirefoxやEdgeなどでは下のcanvasに画像が表示されますが、Chromeではうまく行かないと思います。「デベロッパーツール」でconsoleをを見てみると、「canvasが汚染されているのでgetImageData()が失敗した」といったエラーメッセージが表示されています。

これは、ローカルに配置した環境でJavaScriptを実行すると。セキュリティ上の制約を受けるためです。セキュリティ上の制約を受ける場所から読み込んだ画像をcanvas要素に描画すると、そのcanvas要素は「汚染」されたと見なされ、ImageDataの取得(contextのgetImageData()呼び出し)が許可されなくなります。

Chromeでは同一ディレクトリに配置した画像に対してもこの制約が課されますが、Firefoxでは同一ディレクトリに配置された画像にはこの制約が課されないため、FirefoxではImageDataを取得しcanvas要素にそのImageDataをdrawImage()できたわけです(Firefoxのバージョン、あるいは設定によって変わる可能性があります)。

###ユーザーが選択した画像を読み込む

ただし、この制約は「ユーザーの操作」によって指定された画像には課されません。input要素などからユーザーが画像ファイルを指定し、指定された画像ファイルをJavaScriptでimg要素化すれば、ローカルファイルであってもImageDataにすることが可能です。

input要素から画像ファイルを指定しimg要素に読み込む流れは、以下のようになります。

  1. ファイルが選択されたら実行されるイベントハンドラ(onload)を設定する
  2. ファイルを読み込むFileReaderを作成し、読み込み完了時のイベントハンドラ(onload)を設定する。
  3. FileReaderのreadAsDataURL()で、ファイルの内容をimg要素のsrcに設定できる文字列(DataURL)として読み込む
  4. 読み込み完了時のイベントハンドラで、読み込んだ文字列をimgのsrcとして設定する

読み込み完了を待って次の処理を行う必要があるため、やや手順が多くなってしまいますね。

FileReaderのreadAsDataURL()は、ファイルのデータをDataURLという特殊なテキスト形式で読み込む関数です。DataURLは、バイナリデータを64文字の文字集合に変換(Base64エンコード)して作成される文字列で、img要素のsrc属性などにファイルパスやネット上のアドレスと同じような感覚で設定することができます。

img要素のsrc属性にDataURLの文字列を設定すると、img要素はDataURL内にエンコードされているバイナリデータを復元し画像データとして読み込むわけです。

上の流れでも、img要素はWebブラウザのHTMLとは独立して扱うことができそうです。画像を読み込んでcanvas要素に描画するためのimg要素(と同じ感覚で扱えるImageオブジェクト)も、JavaScriptで作成することにしましょう。

img要素をユーザーが指定した画像ファイルから作成する形にすると、HTMLファイルは以下のようになります。先ほどImageDataの作成に失敗したChromeで実行してみましょう。

<!DOCTYPE html>
<html>
<body>

<script>

function onFileSelected(input) {

    var file = input.files[0];

    var reader = new FileReader();

    reader.onload = onFileLoaded;

    reader.readAsDataURL(file);

}

function onFileLoaded(e) {

    var src_data = e.target.result;

    var img = new Image();

    img.onload = onImageSetted;
    img.src = src_data;

}

function onImageSetted(e) {

    var data = createImageData(e.target);

    document.getElementById('test_canvas').getContext('2d').putImageData(data, 0, 0);

}

function createImageData(img) {

    var cv = document.createElement('canvas');

    cv.width = img.naturalWidth;
    cv.height = img.naturalHeight;

    var ct = cv.getContext('2d');

    ct.drawImage(img, 0, 0);

    var data = ct.getImageData(0, 0, cv.width, cv.height);

    return data;

}

</script>

<p>
<input type="file" onchange="onFileSelected(this)">
</p>

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

</body>
</html>

今度はImageDataが作成されて、下のcanvas要素に選択された画像が表示されたと思います。

image

###読み込んだ画像のデータをいじるには

これでユーザーが指定した画像をImageData、つまり「JavaScriptで扱うことができる数値データの配列」に読み込むことができました。せっかくですので、簡単な画像処理をやってみることにしましょう。

画像を読み込んでImageDataを作成したら、そのImageDataをWebブラウザ上のcanvas要素に描画した上でcanvas要素のimg_dataプロパティとして保存します。これで、後からcanvas要素に描いたImageDataをimg_dataプロパティの参照で取得できるようになりました。

続いてcanvas要素がクリックされたら、画像処理関数processImageData()を呼び出すイベントハンドラの設定も行います。

document.getElementById('test_canvas').addEventListener('click', processImageData);

processImageData()の中では、ImageDataの赤成分と青成分を入れ替える画像処理をやってみることにしましょう。これは、ImageDataの各ピクセルにおいてR成分のデータとB成分のデータを入れ替えるだけですね。

processImage()は、canvas要素に対して設定したイベントハンドラなので、processImage()ではthisがcanvas要素になっています。つまり、先ほど保存しておいたImageDataは「this.img_data」で取得できるわけです。

このprocessImageData()を組み込んだHTMLファイルは、以下のようになります。

<!DOCTYPE html>
<html>
<body>

<script>

function onFileSelected(input) {

    var file = input.files[0];

    var reader = new FileReader();

    reader.onload = onFileLoaded;

    reader.readAsDataURL(file);

}

function onFileLoaded(e) {

    var src_data = e.target.result;

    var img = new Image();

    img.onload = onImageSetted;
    img.src = src_data;

}

function onImageSetted(e) {

    var img_data = createImageData(e.target);

    document.getElementById('test_canvas').width = img_data.width;
    document.getElementById('test_canvas').height = img_data.height;

    document.getElementById('test_canvas').getContext('2d').putImageData(img_data, 0, 0);

    document.getElementById('test_canvas').img_data = img_data;

    document.getElementById('test_canvas').addEventListener('click', processImageData);

}

function createImageData(img) {

    var cv = document.createElement('canvas');

    cv.width = img.naturalWidth;
    cv.height = img.naturalHeight;

    var ct = cv.getContext('2d');

    ct.drawImage(img, 0, 0);

    var data = ct.getImageData(0, 0, cv.width, cv.height);

    return data;

}

function processImageData() {

    var img_data = this.img_data;

    for (var y = 0;y < img_data.height;y++) {
        for (var x = 0;x < img_data.width;x++) {

            var index = (x + y * img_data.width) * 4;

            var r = img_data.data[index];
            var b = img_data.data[index + 2];

            img_data.data[index] = b;
            img_data.data[index + 2] = r;

        }
    }

    document.getElementById('test_canvas').getContext('2d').putImageData(img_data, 0, 0);

}

</script>

<p>
<input type="file" onchange="onFileSelected(this)">
</p>

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

</body>
</html>

画像を読み込んでcanvasに画像が表示されたら、表示された画像をクリックしてみてください。大きな画像だとかなり処理時間がかかる場合があるので、小さめの画像で試してみると良いでしょう。

image

宍戸輝光
ライター
宍戸輝光

関連記事