JavaScript で Instagram の画像フィルターを作るまで

先日、Canvasを練習したいと思って画像変換でいろいろ遊んでたところ、「Instagram」の画像フィルターを実現してみたら面白そうかねと思って作ってみようと思いました。が、それまでに結構紆余曲折があったので、記録も兼ねて記事にまとめてみることにします。

個人的には、Instagramの本当のフィルターそっくりにはまだ動いていないものもあり、実践的でないものも多いかもしれませんが、もし「もっとこうしたほうがいい」とか「こんな技術・API・方法があるよ」ってのがあればぜひ教えて下さい :grin:

kenju/instagram_js_filter

Canvasで画像ピクセルを抽出

CanvasのAPIを必要があって練習していた時、canvasでは画像のピクセルをrgb値で取得できることを知りました。getContext('2d')getImageDate()を使います。

Filter.canvas.getPixels = function(img) {
  var canvas,
      context
  ;
  canvas = Filter.canvas.getCanvas(img.width, img.height);
  context = canvas.getContext('2d');
  context.drawImage(img, 0, 0);
  return context.getImageData(0, 0, canvas.width, canvas.height);
};

セピア化やミラー反転などで練習してみる若きあの頃

返り値としては、pixelsにひとつのピクセルに対してRed, Green, BlueそしてAlpha(透明度)の返り値を得ることができます。つまり、以下の様なデータ構造の配列を得ることが出来ました。

var pixels =
  [ 15, 16, 28, 255, // 1ピクセル目のRed, Green, Blue, Alpha
    22, 51, 59, 255, // 2ピクセル目のRed, Green, Blue, Alpha
    33, 55, 59, 255, // 3ピクセル目のRed, Green, Blue, Alpha
    ...
  ];

なんで、これをいろいろいじると、グレースケールとか、セピアかとか、ミラー反転とか、そういった画像変換を簡単にすることができます。(なお、実際にするときは、CSSのfilterを使うことが多いですが、Canvasでもできるよ、ということです。)

例1: グレースケール

// grayscale
Filter.grayscale = function(pix){
  for (var i = 0, n = pix.length; i < n; i += 4){
    // calculated from NTSC
    var grayscale = pix[i] * .29 + pix[i+1] * .58 + pix[i+2] * .11;
    pix[i]    = grayscale;
    pix[i+1]  = grayscale;
    pix[i+2]  = grayscale;
  };
};

例2: セピア

Filter.sepia = function(pix){
  for (var i = 0, n = pix.length; i < n; i += 4){
    pix[i]    = pix[i]    * 1.07;
    pix[i+1]  = pix[i+1]  * .74;
    pix[i+2]  = pix[i+2]  * .43;
  }
};

例3: 水平方向のミラー反転

Filter.horizontalMirror = function(pix, width, height) {
  for(var i = 0; i < height; i++) {
    for(var j = 0; j < width; j++) {
      var off = (i * width + j) * 4;
      var dstOff = (i * width + (width - j - 1)) * 4;
      pix[dstOff]   = pix[off];
      pix[dstOff+1] = pix[off+1];
      pix[dstOff+2] = pix[off+2];
      pix[dstOff+3] = pix[off+3];
    }
  }
};

フィルター効果を実現する方法がわからない

ここまでは簡単でした。サンプルコードもたくさんあります。

「よし、じゃあこの調子でInstagramのフィルターをつくろう!!!」

。。。

で、RGB値は手に入ると。Alpha値も手に入ると。あれ、どうしたらいいんだろ?

InstagramのAmaroとかToasterとか、Photoshopとかではトーンカーブいじれば簡単だけど、あれって結局なにやってんだろ?

。。。

JSでトーンカーブいじれない??

そこで、いろいろ調べてみました。

CSSのフィルターでInstagramを実現する試み

Instagram filters recreated using CSS3 filter effectsという記事では、CSSのfilterを使って全く同じようにInstagramのフィルターをつくろうとしている方がいました。これは結構面白く、CSSのfilterを使っているので簡単なのですが、どうしても限界はありました。

なんでかっていうと、CSSのfilterではグレースケールとかセピアかとかは簡単にできるけど、「青っぽくする」とか「赤っぽくする」とか、「ピクセルごとに値を変える」とかって、できない。先ほどの記事も、redditのページでは全然似てないよと叩かれてたり、まあ頑張ったけどやっぱ限界あるよね、みたいな意見が多かった。(個人的にはすごいなーと思いました)

Photoshopなら簡単なんだけど

Webデザイナの方なら、「Photoshopで簡単に出来るよ」とか「トーンカーブいじれば一瞬だよ」って方が多いと思います。実際、Photoshopなら、トーンカーブいじれるし、フィルターとかレイヤーとか使えるので、Instagramのフィルター作る、っていうチュートリアル、たくさんあるんですよね。自分もデザイナだったころはPhotoshopで画像編集とかで遊んでました。

そんな中、How to make Instagram Filters in Photoshopという記事では、トーンカーブだけでInstagramのフィルター効果を実現しているチュートリアルを公開していました。他のチュートリアルは、いろいろな効果を複合的にして作っているのが多かったのだけれど、トーンカーブだけで結構リアルに近づいたものができていたので、これいいなーと思いました。

もちろん、実際のInstagramのフィルターを作るアルゴリズムは、Instagramの知的財産なので僕たちは正確には知り得ないのですが、だいたい似たようなものを作るにはこれがいいかな、と。

トーンカーブをいじればいけんじゃん?

ということで、次は 「JSでトーンカーブをいじれればいいんじゃん?」 ということになりました。でも、自分は数学詳しくないし、画像編集研究したとかでもないので、どうしたらいいか全くわかりませんでした。

トーンカーブというのは、RGBそれぞれの値は0~255まで256個ありますが、それぞれの明るさにたいしてより明るくするか、暗くするかを決めることができます。Photoshopなどで画像編集をしたことがある方ならご存じの方も多いかもしれませんが、いくつか点を打つことによってなだらかな曲線ができます。

Tone Curve image

Red系列の色だけ、Green系列の色だけ、Blue系列の色だけそれぞれでもトーンカーブをいじることもできます。ということは、さきほどのHow to make Instagram Filters in Photoshopという記事とも合わせると、 「複数の点を打って、そこからなだらかな曲線を作り出す」 ことができれば、トーンカーブのように画像を変換することができないか、ということになりまう。

それには、「ラグランジュ補間」を使えばいい、ということがわかりました。

(ようやく次のステップにいけた笑)

ラグランジュ補間が使えそう

ラグランジュ補間とは、ざっくり言うと「通るn+1点からn次以下の多項式が1つ定まる」ための補間式です。トーンカーブのパネルを、「補正前のデータ値」をX軸、「補正後のデータ値」をY軸とおけば、y=f(x)の多項式を描くことと同義です。

補間法(ラグランジュ補間とスプライン補間)などの論文でも指摘されているように、ラグランジュ補間では補間のための点が増えてくると振動が大きくなりもはや補間とはいえなくなる、といった限界があるにはあります。しかし今回はとりあえず、二日間しかなかったので、ラグランジュ補間で作ることにしました。さきほどのHow to make Instagram Filters in Photoshopという記事に紹介されているトーンカーブの点も多くて6,7個だったため、許容範囲かなと思ったわけです。

こちらのGistなどを参考に、自身のプログラムにラグランジュ補間を用いてトーンカーブを作成するコードを書いてみました。

たとえば「Toaster」という、ちょっと赤みのかかった画像変換フィルターであれば、以下の様なコードになります。

Filter.toaster()

Filter.toaster = function(pix){

  var lag_r  = new Lagrange(0, 0, 1, 1);
  var lag_g  = new Lagrange(0, 0, 1, 1);
  var lag_b  = new Lagrange(0, 0, 1, 1);

  // red
  var r = [
    [ 0,   0, 120 ], // [index, before, after]
    [ 1,  50, 160 ],
    [ 2, 105, 198 ],
    [ 3, 145, 215 ],
    [ 4, 190, 230 ],
    [ 5, 255, 255 ]
  ];
  // green
  var g = [
    [ 0,   0,   0 ],
    [ 1,  22,  60 ],
    [ 2, 125, 180 ],
    [ 3, 255, 255 ]
  ];
  // blue
  var b = [
    [ 0,   0,  50],
    [ 1,  40,  60],
    [ 2,  80, 102],
    [ 3, 122, 148],
    [ 4, 185, 185],
    [ 5, 255, 210]
  ];

  lag_r.addMultiPoints(r);
  lag_g.addMultiPoints(g);
  lag_b.addMultiPoints(b);

  for (var i = 0, n = pix.length; i < n; i += 4){
    pix[i]    = lag_r.valueOf(pix[i]);
    pix[i+1]  = lag_b.valueOf(pix[i+1]);
    pix[i+2]  = lag_g.valueOf(pix[i+2]);
  }
};

ちなみに、上記のaddMultiPoints()という関数は、以下のように二次元配列を用いて補間点を入力するためのメソッドです。

addMultiPoints()

Lagrange.prototype.addMultiPoints = function(arr){
  for(var i = 0, n = arr.length; i < n; i++){
    if(arr[i][0] !== 0 && arr[i][0] !== 1){
      this.addPoint(arr[i][1], arr[i][2]);
    }
  }
};

でもこれってめっちゃ時間かかりそう? → Web Worker APIで解決

これで、なんとなくそれっぽいのができました。

Before:

Before

After:

After

でも、これってめちゃくちゃ重い処理です(笑)なんで、最後にこれをWeb Worker APIを用いて、マルチスレッドで処理するようにしました。これで、重い計算処理をして可能な限りユーザに負担をかけないような設計にしています。

Filter.process = function(img){

  ...

  // extract pixels data
  pixels = Filter.canvas.getPixels(img);

  // send the pixels to a worker thread
  worker = new Worker('js/worker.js');
  obj = {
    pixels: pixels,
    effects: effect
  };
  worker.postMessage(obj); // ここでピクセルデータをワーカスレッドに送り並列処理で計算を行う

  ...
};

最後に

以上、「JSでInstagramの画像フィルターを作る」に至るまでの経緯をご紹介しました。自分はまだ経験も浅いので、ベストプラクティスではない部分や、そもそも理解が根本的に間違っているところもあるかもしれないのでぜひ教えていただきたいです。と同時に、逆にライブラリを作る際の思考プロセスなどを知ってみたい、はじめてプログラミングに触れたような方や、CanvasやWeb Worker APIは知らなかった方の参考にはなればと思います。

2015-07-11