Canvas + WebWorkersを利用して、画像からカラーパターンを作成してみるβ



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

##処理の流れ

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

##UIスレッドでの処理

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

* canvasで画像を読み込み、全ピクセルデータを取得
* workerを作成し、ピクセルデータをworkerに渡す

```main.js
// 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違うだけで別の色になってしまうため、近似色の計算もしています。計算式は下記です。

```2色が近似色かどうかの比較
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処理

```worker.js
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](http://www.rokurofire.info/2013/10/05/js_colorpattern/ "rokuro Fire")

DEMOサイト
[Auto ColorPicker β - rokuro fire](http://pf.rokurofire.info/samples/pickupcolor/ "Auto ColorPicker β - rokuro fire")

##参考にしたサイト
[ActionScript入門Wiki - 近似色を探す](http://www40.atwiki.jp/spellbound/pages/293.html ActionScript入門Wiki - 近似色を探す)