先日、Canvas
を練習したいと思って画像変換でいろいろ遊んでたところ、「Instagram」の画像フィルターを実現してみたら面白そうかねと思って作ってみようと思いました。が、それまでに結構紆余曲折があったので、記録も兼ねて記事にまとめてみることにします。
個人的には、Instagramの本当のフィルターそっくりにはまだ動いていないものもあり、実践的でないものも多いかもしれませんが、もし「もっとこうしたほうがいい」とか「こんな技術・API・方法があるよ」ってのがあればぜひ教えて下さい :grin:
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でもできるよ、ということです。)
// 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;
};
};
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;
}
};
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でトーンカーブいじれない??
そこで、いろいろ調べてみました。
Instagram filters recreated using CSS3 filter effectsという記事では、CSSのfilter
を使って全く同じようにInstagramのフィルターをつくろうとしている方がいました。これは結構面白く、CSSのfilter
を使っているので簡単なのですが、どうしても限界はありました。
なんでかっていうと、CSSのfilter
ではグレースケールとかセピアかとかは簡単にできるけど、「青っぽくする」とか「赤っぽくする」とか、「ピクセルごとに値を変える」とかって、できない。先ほどの記事も、redditのページでは全然似てないよと叩かれてたり、まあ頑張ったけどやっぱ限界あるよね、みたいな意見が多かった。(個人的にはすごいなーと思いました)
Webデザイナの方なら、「Photoshopで簡単に出来るよ」とか「トーンカーブいじれば一瞬だよ」って方が多いと思います。実際、Photoshopなら、トーンカーブいじれるし、フィルターとかレイヤーとか使えるので、Instagramのフィルター作る、っていうチュートリアル、たくさんあるんですよね。自分もデザイナだったころはPhotoshopで画像編集とかで遊んでました。
そんな中、How to make Instagram Filters in Photoshopという記事では、トーンカーブだけでInstagramのフィルター効果を実現しているチュートリアルを公開していました。他のチュートリアルは、いろいろな効果を複合的にして作っているのが多かったのだけれど、トーンカーブだけで結構リアルに近づいたものができていたので、これいいなーと思いました。
もちろん、実際のInstagramのフィルターを作るアルゴリズムは、Instagramの知的財産なので僕たちは正確には知り得ないのですが、だいたい似たようなものを作るにはこれがいいかな、と。
ということで、次は 「JSでトーンカーブをいじれればいいんじゃん?」 ということになりました。でも、自分は数学詳しくないし、画像編集研究したとかでもないので、どうしたらいいか全くわかりませんでした。
トーンカーブというのは、RGBそれぞれの値は0~255まで256個ありますが、それぞれの明るさにたいしてより明るくするか、暗くするかを決めることができます。Photoshopなどで画像編集をしたことがある方ならご存じの方も多いかもしれませんが、いくつか点を打つことによってなだらかな曲線ができます。
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]);
}
}
};
これで、なんとなくそれっぽいのができました。
Before:
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は知らなかった方の参考にはなればと思います。