LoginSignup
10
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-10-04

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

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

DEMOサイト
Auto ColorPicker β - rokuro fire

参考にしたサイト

ActionScript入門Wiki - 近似色を探す

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10