Category : JavaScript

前の記事(自動カラー選択ボタン(β版)のテストを開始しました)のつづき。

canvasに表示した画像から、カラーパターンを作成するスクリプトを書いてみました。処理にweb workersを利用しています。

DEMOサイト

Auto ColorPicker β – rokuro fire

Auto ColorPicker β - rokuro fire

処理の流れ

  • canvasで画像を読み込み、全ピクセルデータを取得
  • workerを作成し、ピクセルデータをworkerに渡す
  • workerで、色取得の処理を行う
  • workerから、取得した色データを返す

UIスレッドでの処理

下記2つの処理はUIスレッド側に書きます。

  • canvasで画像を読み込み、全ピクセルデータを取得
  • workerを作成し、ピクセルデータをworkerに渡す
// canvasで画像を読み込み、全ピクセルデータを取得
// canvasにはdrawImage済とする
var pixels = canvas.getContext('2d').getImageData(0,0,canvas.width,canvas.height)

// workerを作成し、ピクセルデータをworkerに渡す
// worker.jsにweb workerでの処理が書かれているとする
var worker = new Worker('/js/worker.js');
// workerに渡すメッセージ
var msgData = {
    width:canvas.width,
    height:canvas.height,
    pixdata:pixels
};
// workerからのメッセージ取得
worker.onmessage = function(e){
    // HTMLを出力するための何らかの処理をここに記述

    // ワーカーを終了
    worker.terminate();
    return false;
};

// workerにメッセージを渡す
worker.postMessage(msgData);

workerには msgData オブジェクトを渡しています。
workerではDOMやwindowオブジェクトを使った操作が一切出来ないため、必要な変数は事前に取得しておき、一緒に渡してしまいます。

web wrokersのスレッド

下記2つの処理は、worker側に書きます。

  • workerで、色取得の処理を行う
  • workerから、取得した色データを返す

色抽出の処理は非常に重い処理のため、worker側で行います。そのため、PCのスペックによっては、処理に時間がかかることがあります。
またweb workersをサポートしていないブラウザではこの機能は使えません。

色取得の処理について

本処理では、ベースカラー/サブカラー/キーカラー、それぞれの面積の比率(%)を設定し、各レンジの中で占有率の多い色を表示します。
通常、ベースカラー 70% サブカラー 25% アクセントカラー 5%くらいの比率ですが、これだとうまく取得できなかったため、試験的に ベースカラー 85% サブカラー 10% アクセントカラー 1%に設定しています。

近似色の計算

ただ色を取得しただけではRGBのどれかの値が1違うだけで別の色になってしまうため、近似色の計算もしています。計算式は下記です。

var r = r1 - r2; // R値の差
var g = g1 - g2; // G値の差
var b = b1 - b2; // B値の差
var d = Math.sqrt(r*r + g*g + b*b); // 2つの色の距離
if(d < 60) return true; // 閾値(60)以下なら、その色は近似色

worker処理

var worker_getAllColor = {};

// ピクセル走査処理
worker_getAllColor.search = function(e){
    // e.data に、メッセージの値が入ってくる
    var pixdata = e.data.pixdata.data,
        colorNum = 5, // ベース/サブ/キーカラーそれぞれ5色を出力
        tmplabel = "",
        colorObj = {},
        colorAry = {
            base:[],
            sub:[],
            key:[]
        };
    // ピクセルの総数
    var pixNum = pixdata.length;

    // 全ピクセルデータからオブジェクトを作成する処理
    // colorObjのkeyを "Rの値,Gの値,Bの値"とし、valueには同色のピクセルの個数
    // 1件め追加
    colorObj[pixdata[0] + ',' + pixdata[1] + ',' + pixdata[2]] = 1;
    var flag;

    // 2件目以降
    // ピクセルデータは RGBaの順番で取得されるので、4番目のalpha(透明度)は今回は取得せずに飛ばす
    for(var i=4; i<pixNum; i++){
        if(i % 4 === 3) continue;
        if(i % 4 === 2) {
            tmplabel += pixdata[i];
            // 近似色を比較し、近似色なら元オブジェクトの数値をプラス
            for(var label in colorObj){
                if(!worker_getAllColor.checkNum(label,tmplabel)) {
                    colorObj[label] = colorObj[label]+1;
                    flag = true;
                    break;
                }
            }
            // 近似色でない場合、新たにオブジェクトを追加
            if(!flag) {
                colorObj[tmplabel] = 1;
            } else {
                flag = false;
            }
            tmplabel = "";
        } else {
            tmplabel += pixdata[i] + ',';
        }
    }

    // 返り値データの成型
    var count = 0;
    for(var label in colorObj) {
        // 数値が全ピクセルの15%以上ならベースカラー
        // 1%〜10%ならサブカラー
        // 1%以下ならキーカラー
        var lineNum = (colorObj[label]/pixNum * 100);
        if(lineNum > 15) { // ベースカラー
            var type = {name:"base"};
        } else if(lineNum <= 1) { // キーカラー
            var type = {name:"key"};
        } else if(lineNum <= 10) { // サブカラー
            var type = {name:"sub"};
        } else {
            continue;
        }

        // 配列の個数が規定数以下の時は配列に追加
        if(colorAry[type.name].length < colorNum) {
            colorAry[type.name].push({name:label, num:colorObj[label]});
        } else {
            // 各オブジェクトを比較し、valueが多い方を配列に残す
            for(var i=0,len=colorAry[type.name].length; i<len; i++){
                if(colorAry[type.name][i].num < colorObj[label]) {
                    colorAry[type.name][i] = {name:label, num:colorObj[label]};
                    break;
                }
            }
        }
        count++;
    }

    // それぞれvalueの多い順にソート
    colorAry.base.sort( function( a, b ) { return b.num - a.num; } );
    colorAry.sub.sort( function( a, b ) { return b.num - a.num; } );
    colorAry.key.sort( function( a, b ) { return b.num - a.num; } );

    // UIスレッドにオブジェクトを返す
    postMessage({ary:colorAry});
};

// 近似色かチェック(近似色ならfalse、別色ならtrueを返す)
worker_getAllColor.checkNum = function(obj1,obj2){
    var col1 = obj1.split(",");
    var col2 = obj2.split(",");
    var r = col1[0] - col2[0],
            g = col1[1] - col2[1],
            b = col1[2] - col2[2];
    var d = Math.sqrt(r*r + g*g + b*b);
//  postMessage({log:d});
    if(60 < d) return true; // 閾値
    return false;
};

// onmessageイベント
// ここに設定したオブジェクトから処理を開始
onmessage = worker_getAllColor.search;

ざざーっと書いてしまいましたが、文章にすると下記処理を行っています。

  • canvasの全ピクセルデータを1つずつ取得し、{ key(RGBの値):value(ピクセルの個数) } の形に成形する
  • 整形したオブジェクトごとに近似色かどうか比較する。近似色の場合は、比較元のピクセルの個数を+1していく。
  • 近似色のチェック終了後、今度は各オブジェクトのピクセルの個数によって、ベース/サブ/キーカラーに分けていく。

Workerの処理を書く上での注意点

注意というか戸惑った点は下記です。

  • worker内でDOM操作関連の処理はできない
  • Windowオブジェクトも取得できない
  • jQueryが利用できない
  • モバイルデバイスでは動くかどうかわからない

私はネイティブJSが好きなので特に問題ありませんが、普段jQueryを使っている場合、ちょっと大変かもしれません。

この処理をシングルスレッドで書くと?

書き方が悪いのかもですが、処理が重すぎてブラウザが止まってしまいました・・・処理時間の計測もちゃんとできず。
並列処理なら、動作もほとんど重くなりません。が、処理時間はPCのスペックによって大きく変わってきます。

参考にしたサイト