HTML5 × CSS3 × jQueryを真面目に勉強 – #18 画像ファイルからカラーパレットを作ってみる (前編)

jhc_colorpalette_1

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

そんな訳で、HTML5 の Canvas を駆使して一枚の画像ファイルから色情報を取得する方法を学んでみました。まだ学習の途中ではありますが、画像ファイルからカラーパレットを生成するところまでは出来るようになったので、当ブログで紹介するとします。

と、思ったのですが、このシリーズにしては結構難易度の高い内容なので、いきなりその方法を紹介する前に、ここでは基本的なピクセルの操作や色情報の扱いなど、前段階としての小規模なサンプルを紹介していきたいと思います。

Step.1 カラーピッカーを作る

見出しだけを読むとしょっぱなから大それた印象を受けますが、マウスカーソル位置にあるピクセル情報を取得・解析して、そこから色に関する情報を画面側に表示するといった単純なものです。

1 | HTML を組む

とりあえず Bootstrap をベースにして適当に組んでみます。canvas 要素はピクセルでサイズを指定する必要があるので、明示的に高さと幅を指定します。色情報の出力先として、id="preview" の要素に選択中の色を出し、各 input 要素にはその色コードを表示させます。

<div class='container'>
  <div class='row'>
    <div class='col-sm-7'>
      <canvas height='290' id='panel' width='470'></canvas>
    </div>
    <div class='col-sm-5'>
      <div class='well well-sm'>
        <div class='row'>
          <div class='col-sm-5'>
            <div class='preview' id='preview'></div>
          </div>
          <div class='col-sm-7 monospace'>
            <div class='form-group'>
              <div class='input-group'>
                <span class='input-group-addon'>R</span>
                <input id="color_r" class="form-control input-sm" name="color_r" type="text" />
              </div>
            </div>
            <div class='form-group'>
              <div class='input-group'>
                <span class='input-group-addon'>G</span>
                <input id="color_g" class="form-control input-sm" name="color_g" type="text" />
              </div>
            </div>
            <div class='form-group'>
              <div class='input-group'>
                <span class='input-group-addon'>B</span>
                <input id="color_b" class="form-control input-sm" name="color_b" type="text" />
              </div>
            </div>
            <div class='form-group'>
              <div class='input-group'>
                <span class='input-group-addon'>#</span>
                <input id="color_hex" class="form-control input-sm" name="color_hex" type="text" />
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

2 | 画像を Canvas 上に描画させてみる

単に画像を表示させるだけなら img 要素を使えば事足りますが、それではピクセル単位での情報を取得することが出来ません。なので JavaScript を使って画像データを取得し、それを Canvas 上に描画します。Canvas ならピクセル単位での情報を取得できるので、まずはこれを実現するための準備をします。

var src = 'img.jpg';

var canvas = document.getElementById('panel'),
    context = canvas.getContext('2d');

var image = new Image();
	
// 画像要素を生成します
image.src = src;
image.onload = function() {
    // 画像をカンバス上に表示します
    context.drawImage(image, 0, 0, image.width, image.height);
};

ブラウザで確認してみると、Canvas 上に一行目で指定した画像ファイルと同じものが描画されているかと思います。描画するためのメソッドは drawImage です。

これで下準備が整いました。

3 | マウスイベントを拾ってカーソル位置にあるピクセル情報を取得する

jQuery を使ってmousemoveイベントを拾い、マウスカーソル位置にあるCanvasの情報を取得して色情報を解析するという流れです。

var canvasOffset = $(canvas).offset(),
    canvasX = 0,
    canvasY = 0,
    imageData = {},
    pixel = [],
    rgba = '',
    hex = '',
    preview = $('#preview'),
    input_r = $('#color_r'),
    input_g = $('#color_g'),
    input_b = $('#color_b'),
    input_hex = $('#color_hex');
    
// マウスカーソル上にあるピクセルの色情報を取得します
$('#panel').on('mousemove', function(e) {
    canvasX = Math.floor(e.pageX - canvasOffset.left);
    canvasY = Math.floor(e.pageY - canvasOffset.top);

    imageData = context.getImageData(canvasX, canvasY, 1, 1);
    pixel = imageData.data;
    rgba = 'rgba(' + pixel[0] + ',' + pixel[1] + ',' + pixel[2] + ',' + pixel[3] + ')';
    hex = pixel[0].toString(16) + pixel[1].toString(16) + pixel[2].toString(16);

    // 取得した色情報を画面側に渡します
    preview.css({backgroundColor: rgba});
    input_r.val(pixel[0]);
    input_g.val(pixel[1]);
    input_b.val(pixel[2]);
    input_hex.val(hex);
});

ImageData について

19行目にあるように getImageData メソッドを呼び出して ImageData オブジェクトを取得しています。ImageData とは一言で言うとCanvas 内のバイト列を扱うためのオブジェクトです。canvas 内の各バイト毎に透明度の情報を配列として保持しているので、ここから色情報を取得することが出来ます。

20行目以降は、その取得した情報を画面側に表示するための処理になります。

img-colorpicker

画像の上でカーソルをグリグリ動かすと、そこにある色情報が表示されます。

Step.2 Canvas 上にピクセル単位でグラデーションを描画する

Canvas にはグラデーションカラーを描画するためのメソッドが最初から用意されていますが、あえてそれを使わずに自前で描画してみるとします。物凄く地味な内容ですが、画像からカラーパレットを生成するためには、自前でグラデーションが描画出来るくらいの知識が必要なのです。

1 | HTML を組む

<div class='container'>
  <canvas height='210' id='panel' width='210'></canvas>
</div>

2 | グラデーションを描画してみる

ImageData オブジェクトは、バイト毎に透明度の情報を配列として保持していると先ほど説明しました。

ImageData.data == [赤, 緑, 青, 透明度, 赤, 緑, 青, 透明度, 赤, 緑, 青, 透明度,... ]

こんな感じで4要素単位でズラ~っと配列で保持されているのをイメージしてもらえれば良いでしょう。ということで、Canvas のタテ・ヨコそれぞれ1ピクセル単位でループさせ、赤, 緑, 青, 透明度の値を順番に格納していけば OK です。コードは以下のようになります。

// カンバスオブジェクトを作成
// width, height を取得
var canvas = document.getElementById('panel'),
    context = canvas.getContext('2d'),
    w = canvas.width,
    h = canvas.height,

    imageData = context.getImageData(0, 0, w, h);

// 横方向のループ処理
for (var x=0; x<w; x++) {
    //  縦方向のループ処理
    for (var y=0; y<h; y++) {
        var targetPixel = (x + y * w) * 4;
        imageData.data[targetPixel + 0] = x;
        imageData.data[targetPixel + 1] = y;
        imageData.data[targetPixel + 2] = (x + y);
        imageData.data[targetPixel + 3] = 255;
    }
}

context.putImageData(imageData, 0, 0);

ブラウザで確認してみると、なんとも幻想的(?)なグラデーションカラーが表示されているかと思います。

このコードにもう一手間加えて、別のグラデーションに変えてみます。先ほどのコードに以下のように別のループ処理文を追記します。

... ...
// 横方向のループ処理
for (var x=0; x<w; x++) {
    //  縦方向のループ処理
    for (var y=0; y<h; y++) {
        var targetPixel = (x + y * w) * 4;
        imageData.data[targetPixel + 0] = x;
        imageData.data[targetPixel + 1] = y;
        imageData.data[targetPixel + 2] = (x + y);
        imageData.data[targetPixel + 3] = 255;
    }
}

for (var i=0; i<x*h; i++) {
    var targetPixel = i * 4;
    imageData.data[targetPixel + 0] = i / w;
    imageData.data[targetPixel + 1] = i / h;
    imageData.data[targetPixel + 2] = 255;
    imageData.data[targetPixel + 3] = 255;
}

context.putImageData(imageData, 0, 0);

18行目はRGBABすなわち青に関する色情報です。ここの値を 255 に固定することで青系統の色に限定されました。また16行目の赤と17行目の緑に格納する値のアルゴリズムを上記のように変えることで、暗い青から明るい青へのグラデーションが表現されます。

img-pixelpainting

Step.3 減色処理をやってみる

Step.1 と Step.2 で Canvas 上に描画したモノは RGB をふんだんに使ったフルカラーというやつです。カラーパレットといった限定した色を抽出するには、フルカラーでは情報が余りに多すぎるため、色の数を大幅に減らす必要がある訳です。

減色処理のアルゴリズムは色々とあるようですが、今回は短いコードで実現できる均等量子化法というのを試してみました。

均等量子化とは、元画像での色分布を考慮せず単純に階調を落とす手法です。フルカラーの画像は RGB それぞれ 256 階調 (24bit) で描画されていますが、これを32 階調 (8bit) におとせば、色数は単純に半分になるという理屈です。24bit の画像を 8bit に落とすには各画素の下位 bit をビットマスクで落とすことで実現できます。

int newRed   = red & 0xF0;
int newGreen = green & 0xF0;
int newBlue  = blue & 0x80;

凄く適当ですが、イメージとしてはこんな感じです。ビット演算子(※ここでは論理積)を用いて階調を落としています。コレを参考に先ほどStep.2 のサンプルコードを書き換えてみました。

var canvas = document.getElementById('panel'),
    context = canvas.getContext('2d'),
    w = canvas.width,
    h = canvas.height,

    imageData = context.getImageData(0, 0, w, h);

// 減色処理 ~ 均等量子化法
for (var x=0; x<w; x++) {
    for (var y=0; y<h; y++) {
        var targetPixel = (x + y * w) * 4;
        imageData.data[targetPixel + 0] = x       & -(0xF0/8);
        imageData.data[targetPixel + 1] = y       & -(0xF0/8);
        imageData.data[targetPixel + 2] = (x + y) & -(0xF0/4);
        imageData.data[targetPixel + 3] = 255;
    }
}

context.putImageData(imageData, 0, 0);

量子化処理の部分は、そのままだと荒っぽすぎるので調整を加えています。

img-pixel-painting

更に画像データに対しても減色処理を加えてみるとします。

<div class='container'>
  <div class='row'>
    <div class='col-sm-6'>
      <canvas height='290' id='panel' width='470'></canvas>
    </div>
    <div class='col-sm-6' id='panel2-container'></div>
  </div>
</div>
var src = 'img2.jpg';

var canvas1 = document.getElementById('panel'),
    context1 = canvas1.getContext('2d'),
    w = canvas1.width,
    h = canvas1.height;

var canvas2 = document.createElement('canvas'),
    context2 = canvas2.getContext('2d');
canvas2.width = w;
canvas2.height = h;

var panel2Container = document.getElementById('panel2-container')

var image = new Image();


// 画像要素を生成します
image.src = src;
image.onload = function() {
    context2.drawImage(image, 0, 0, image.width, image.height);

    var imageData = context2.getImageData(0, 0, image.width, image.height);
    
    // 減色処理 ~ 均等量子化法
    for (var x=0; x<w; x++) {
        for (var y=0; y<h; y++) {
            var targetPixel = (x + y * w) * 4;
            imageData.data[targetPixel + 0] = imageData.data[targetPixel + 0] & -(0x100/8);
            imageData.data[targetPixel + 1] = imageData.data[targetPixel + 1] & -(0x100/8);
            imageData.data[targetPixel + 2] = imageData.data[targetPixel + 2] & -(0x100/4);
            imageData.data[targetPixel + 3] = 255;
        }
    }
    context2.putImageData(imageData, 0, 0);

    // オリジナル画像をカンバス上に表示します
    context1.drawImage(image, 0, 0, image.width, image.height);
    // 減色画像を画面に表示します
    panel2Container.appendChild(canvas2);
}

減色処理前と処理後の画像を比較できるようにしています。かなり色数に違いが出ているのが分かるかと思います。

img-quontize

とはいえ、色数は減っているものの、同時に画質の劣化が激しいです。そのためファイルサイズ圧縮のための手法としては殆ど使われることがありません。カラーパレットを生成するための手段としても、ループ処理で1ピクセルずつチェックしていくというのは、あまりに処理が重すぎます。今回はあくまで実験として試みました。そんな訳で、後編では別のアプローチでカラーパレットを生成する方法を紹介したいと思います。

参考サイト