canvasに表示した画像から、カラーパターンを作成するスクリプトを書いてみました。処理にweb workersを利用しています。
##処理の流れ
- 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のスペックによって大きく変わってきます。
##元記事、DEMOなど
本エントリーは下記の転載になります。
rokuro Fire
DEMOサイト
Auto ColorPicker β - rokuro fire
##参考にしたサイト
[ActionScript入門Wiki - 近似色を探す](http://www40.atwiki.jp/spellbound/pages/293.html ActionScript入門Wiki - 近似色を探す)