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

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

前回の記事にて Canvas 上に画像データを表示したり、ピクセル単位で色情報を取得するなどといった操作の基本を学びました。これらの知識を踏まえて、画像ファイルから特定の色を抽出してカラーパレットを生成する処理を JavaScript でやってみるとします。結構難易度は高めですが(※オレ調べ)、なるべく丁寧に解説していきたいと思います。

はじめに - カラーパレット

まずはカラーパレットの要件を大まかに書き出してみます。

  • 画像ファイルを読み込み、その画像を構成している色から平均的な色情報を抽出
  • カラーパレットの色数制限はなし

まだまだ試行錯誤の段階ということで、要件はこの程度に留めておきます。次にどういった処理の流れで実現するかを書き出してみます。

  1. 画像ファイルを Canvas 上に読み込み、表示させる
  2. 読み込んだ画像データをタテ・ヨコ 20 のグリッドに分割する
  3. 分割したグリッドを1つずつピクセル単位で解析し、グリッド毎のサンプルカラーを取得する
  4. 隣接するグリッド同士の色情報を比較し、互いの差が予め定めたしきい値以下の場合は類似色と判断する

完成予想イメージはこちら。

img-colorpalette

では実装を始めるとします。

Step.1 画像を Canvas 上に表示させる

すでに前回の記事で解説済みですが、復習を兼ねて今一度書き記しておきます。

1| HTML をマークアップ

画像を表示するためのcanvas要素と、カラーパレットを表示するためのtable要素を用意します。

<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-smColor'>
        <h3 class='page-header'>Color palette</h3>
        <table class='table table-striped table-hover monospace' id='color-palette'>
          <tbody></tbody>
        </table>
      </div>
    </div>
  </div>
</div>

スタイルは Bootstrap をベースにしているため、そのためのクラスが各要素に記述されています。

4行目で Canvas 要素を定義していますが、幅と高さを明示的に指定する必要があるため、インラインで指定しています。(※サンプルコードなのでインライン指定してますが、横着せずにCSS側で指定するのがお作法でしょう。)9〜11行目でカラーパレットを表示するためのTable 要素を定義しています。ここに実際の色を表示したカラーチップとカラーコードを表示させます。

2| JavaScript の実装

(function() {

    var src = 'img1.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 上に画像を表示させる手順としては、まず image オブジェクトを生成し、src プロパティに画像ファイルのパスを指定します。Canvas 要素が直接画像を読み込むという仕様が無いために、このような手順を踏みます。その後で16行目にあるようにimage オブジェクトを drawImage() メソッドの引数として渡すことで Canvas 上に画像が表示されます。注意点として画像ファイルの読み込みが完了した後にこの処理を行う必要があるため、image オブジェクトの onload イベントを使います。このように記述することで、画像読み込み処理が完了したタイミングで代入した function 内の処理が実行されるようになります。

img-colorpalette_1

このように画像が表示されれば成功です。

Step.2 画像をタテ・ヨコ均等に分割し、各グリッドのサンプルカラーを取得する

タイトルだけでは何のことだかサッパリなので、詳しく見ていくとします。

まずはじめに読み込んだ画像を以下の様な感じで分割します。

img-sliced_image_a

この例ではタテ・ヨコそれぞれ十等分することで、合計100個のセルを作っています。で、それぞれのグリッドにあるピクセルがどんな色情報を持っているのか解析します。具体的にどうするかですが、ピクセルは RGBA の値を持っており、R、G、Bそれぞれの合計値をグリッド一つ辺りのピクセル数で割れば、平均色が算出できるというわけです。とはいえ、全てのピクセルをループ処理で見ていくとなると処理が重すぎるため、任意の範囲(一部)のピクセルのみに対してこの処理を行うようにします。その分正確さに欠けるかもしれませんが、完全に同じ色を抽出することに拘ってモッサリした動作になるよりも、パフォーマンスを優先させるためにこのような実装にします。まとめます。

  1. 画像を等しいサイズのグリッドに等分割する(※何等分が妥当かはいろいろ試して自分なりの落とし所を見極めます。)
  2. 各グリッド内の任意の範囲のピクセルを1つずつ解析して RGBA 値を取り出す
  3. Rの合計値、G の合計値、B の合計値をそれぞれ求める
  4. それぞれの合計値を 1 グリッドあたりのピクセル数で割って平均値を求める
  5. RGB それぞれの平均値からなる色が、そのグリッドにおけるサンプルカラーとなる

ではコードに落としこんでみます。

var RATIO = 20; // 画像の分割比

// グリッドの平均色を算出します
var generateSampleColor = function(data) {};

// カラーパレットとなるユニークな色情報を取得します
// 画像をタテ・ヨコ均等に分割し、分割したグリッドのサンプルカラーを取得します
var getUniqueColors = function() {
    var sampleColors = [],
        rows = RATIO,
        cells = RATIO,
        cellWidth = (canvas.width / cells) | 0,
        cellHeight = (canvas.height / rows) | 0;

    for (var i=0; i<rows; i++) {
        for (var j=0; j<cells; j++) {
            var colorArray = context.getImageData(cellWidth * j, cellHeight * i, cellWidth, cellHeight);
            var sampleColor = generateSampleColor(colorArray.data);
            sampleColors.push(sampleColor);
        }
    }
};
[/javascript]
<p>今回は画像をタテ・ヨコ20分割にします。整数で割り切れない場合は小数点以下を切り捨てます。<tt>Math.floor()</tt> を使うのが一般的でしょうが、Math 関数は処理が重くなるので、<strong>9,10行目</strong>にあるような小技を使うことで高速化を図っています。グリッドに分割できたらそれらをひとつずつ <tt>generateSampleColor()</tt> というメソッドにかけて平均色を算出します。ではgenerateSampleColor メソッドの中身を書いていきます。</p>


<p>2 つの色を引数として受け取り、RGB それぞれの値の差を求めます。差の絶対値が予め定義したしきい値以下だったら類似した色と判定し、そうでなければ別物として取り扱うようにします。</p>
<p>次に類似すると判定された2つの色の平均色を生成する処理を実装します。</p>

// 2つのグリッド同士の色の平均値を取得します
var getAverageColor = function(color1, color2) {
    var averageColor = [];

    averageColor.r = (((color1.r + color2.r) / 2) + 0.5) | 0;
    averageColor.g = (((color1.g + color2.g) / 2) + 0.5) | 0;
    averageColor.b = (((color1.b + color2.b) / 2) + 0.5) | 0;

    return averageColor;
};

あとはこれらのメソッドを呼び出すだけですが、while() 文をネストすることで実現します。getUniqueColors() に以下のように追記します。

// カラーパレットとなるユニークな色情報を取得します
// 画像をタテ・ヨコ均等に分割し、分割したグリッドのサンプルカラーを取得します
// サンプルカラー情報同士を比較してユニークな色情報だけを取得します
var getUniqueColors = function() {
    var sampleColors = [],
        uniqueColors = [],
        rows = RATIO,
        cells = RATIO,
        cellWidth = (canvas.width / cells) | 0,
        cellHeight = (canvas.height / rows) | 0;

    for (var i=0; i<rows; i++) {
        for (var j=0; j<cells; j++) {
            var colorArray = context.getImageData(cellWidth * j, cellHeight * i, cellWidth, cellHeight);
            var sampleColor = generateSampleColor(colorArray.data);
            sampleColors.push(sampleColor);
        }
    }

    while (sampleColors.length > 0) {
        var baseCol = sampleColors.shift(),
            avgColor = baseCol,
            k = 0;
        while (sampleColors.length > k) {
            var secondCol = sampleColors[k];
            if (areSimilarColors(baseCol, secondCol)) {
                avgColor = getAverageColor(avgColor, sampleColors.splice(k, 1)[0]);
            } else {
                k++;
            }
        }
        uniqueColors.push(avgColor);
    }
    return uniqueColors;
};

これでカラーパレットに使うだけのユニークな色だけを絞り込むことが出来ました。

Step.4 カラーパレットを画面上に表示する

ここまででカラーパレットのための色情報を取得することが出来ました。あとはこれを元に画面上にカラーパレットを表示するだけです。

// カラーパレットを生成します
var generateColorPalette = function(colors) {
    var palette = document.querySelector('#color-palette tbody');
    var html = '';
    for (var i=0, len=colors.length; i<len; i++) {
        var col = colors[i];
        html += '<tr><th style="background:' + 'rgb(' + col.r + ',' + col.g + ',' + col.b + ')' + '"></th>';
        html += '<td>#' + fillZero(col.r) + fillZero(col.g) + fillZero(col.b) + '</td></tr>';
    }
    palette.innerHTML = html;
};

// セロ埋めして二桁の文字列を返します
var fillZero = function(i) {
    return ("0" + i.toString(16)).substr(-2);
};

最後にこれらのメソッドを画像ファイル読み込み完了後に呼び出せば完成です。

// 画像要素を生成します
image.src = src;

// 画像読み込み完了
image.onload = function() {
    // 画像をキャンバス上に表示します
    context.drawImage(image, 0, 0, image.width, image.height);

    var uniqueColors = getUniqueColors();

    generateColorPalette(uniqueColors);
};

img-colorpalette

おわりに

色々と試行錯誤してみた結果、なんとなくそれっぽいものが出来ました。まだまだ改善の余地だらけですが、画像処理の奥深さを知る良い機会になったかと思います。こういった画像処理を始めとしたビジュアル要素の強いプログラムは、主に Flash が得意分野としているところなので、JavaScript で探すよりもそちらで検索するほうが有益な情報を見つけやすかったりします。