幕末古写真ジェネレータをハックする
幕末古写真ジェネレーター というウェブサービスがちょっと前に話題になりました。どんな写真も幕末の写真のように加工してくれる面白いサービスです。
この仕組みを 幕末古写真ジェネレーターの仕組み? - 将来が不安 で解析していたのが面白かったので、続きをやってみることにしました。
→成果を先に見たい人は、こちら からご覧くださいませ。
1. 真っ黒な画像を渡す
まずは、真っ黒な写真を幕末風にしてもらいます。
これが幕末風にするための型紙です。以降はテンプレート画像と呼ぶことにします。
2. RGB の階調を渡す
直感的に、ジェネレータの実装は
(元画像+色補正)+テンプレート画像
と考えられます。テンプレート画像は得られたので、あとは色補正のパラメータを探れればハック完了です。
そのために、RGB の階調をジェネレータに渡してみました。
結果はこうなりました。
おー。青色がほとんど消えてますね。グラデーションの方は、白飛び・黒飛びしていて、ダイナミックレンジが小さくなっているのも一目瞭然ですね。
3. 差分をとる
さて、2. で得られた画像ですが、テンプレート画像が邪魔なので、差分をとってやります。
(元画像+色補正)+テンプレート画像
からテンプレート画像を引くことで、
(元画像+色補正)
を手に入れる、という算段です。
プログラムで処理してもいいのですが、Photoshop が手元にある人は、レイヤースタイルの「差の絶対値」が激しく便利でしょう。
2つの画像をレイヤーに配置するだけで、差分をとってくれます。
だいぶ雑念が消えましたね。
でも、まだノイズがあります。おそらく、ざらついた印象をつけるために、ランダムでノイズをのせているのでしょう。
4. ぼかす
ノイズが邪魔なら、平均値を取ればいいですね。平均値を取るにはどうするか。ぼかしを入れます。
Photoshop のフィルタから、「ぼかし」>「ぼかし (ガウス)」を選択します。半径 6px ぐらいにしてやればこんな画像になります。
ノイズがなくなって、のっぺりしました。
上側の4つの四角から、色をスポイトで抜き出します。
#ff0000 -> #212121 #00ff00 -> #414141 #0000ff -> #0d0d0d #ffffff -> #6d6d6d #000000 -> #060606
RGB の各成分がどの程度の強さで変換されているかがわかります。
赤(#212121)と緑(#414141)と青(#0d0d0d)を足したら、だいたい白(#6d6d6d)の色になっています。RGB の色調補正を行っている、という推測が正しいことを裏付けていますね。
ここから、色調補正の手順が推測できますね。書き下してみるとこんな感じです。
- ダイナミックレンジをいじる
真ん中 1/3~2/3 のみを生かすぐらいに絞る。トーンカーブで表すとこんな具合。 - 色あいを調整する
赤を 1/8、緑を 1/4、青を 1/16 ぐらいまで落とす。
5. 実装する
アルゴリズムが分かれば、あとは ActionScript で実装するだけです。
(元画像+色補正)+テンプレート画像
を順番に実装していきます。サーバーレスで実装したかったので、BitmapData は使っていません。
色調補正は ColorMatrixFilter を2回適用することで実現しました。テンプレート画像との合成は、テンプレート画像を透明度50%で重ねることで実現しました。50% で重ねても薄くならないよう、2つの画像を2倍の明るさで作成しておきました。
出来上がったのが次の Flash です。URL を入力して、ボタンを押すと幕末写真化されますよ。
かなり本家に近くなった気のではないでしょうか。アルファチャンネルの扱いがオリジナルと違うけど、それは Flash の制限上、やむなし…。
本家に比べて何がすごいかというと、クライアント側だけで画像加工しているので高速です。即座に試せます。
大きな写真を表示させたい場合は、特設ページ 最速幕末古写真ジェネレータ からどうぞ。
ソースコード
ソースは以下に(120行)。
package{ import flash.display.*; import flash.text.*; import flash.events.*; import flash.filters.*; import flash.net.*; import flash.filters.ColorMatrixFilter; [SWF(backgroundColor="#ffffff")] public class Bakumatsu extends Sprite{ [Embed(source="BakumatsuTexture.jpg")] private var Texture:Class; private var canvas:Sprite; public function Bakumatsu(){ stage.scaleMode = "noScale"; stage.align = "TL"; // 入力欄 var tf:TextField = new TextField; tf.border = true; tf.text = "http://tech.nitoyon.com/img/title-blog.jpg"; tf.type = "input"; tf.width = 380; tf.height = 22; tf.x = tf.y = 5; addChild(tf); // ボタン var button:Sprite = new Sprite(); button.graphics.beginFill(0xcccccc); button.graphics.drawRect(0, 0, 50, 22); button.graphics.endFill(); button.filters = [new BevelFilter(2, 45, 0xffffff, 0.5, 0x000000, 0.5)]; button.mouseChildren = false; var label:TextField = new TextField(); label.width = 50; label.height = 22; label.htmlText = "<p align='center'>幕末化</p>"; button.addChild(label); button.buttonMode = true; button.x = 395; button.y = 5; addChild(button); // キャンバス canvas = new Sprite(); canvas.x = 5; canvas.y = 35; addChild(canvas); button.addEventListener("click", function(event:Event):void{ var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler); loader.contentLoaderInfo.addEventListener("ioError", ioErrorHandler); var req:URLRequest = new URLRequest(tf.text); loader.load(req); showMsg("Loading..."); }); button.dispatchEvent(new Event("click")); } private function completeHandler(event:Event):void{ var li:LoaderInfo = event.currentTarget as LoaderInfo; var loader:Loader = li.loader; bakumatsuNize(loader); } private function ioErrorHandler(event:Event):void{ showMsg("Not found!!"); } private function showMsg(msg:String):void{ var tf:TextField = new TextField(); tf.htmlText = "<font size='40'>" + msg + "</font>"; tf.autoSize = "left"; tf.selectable = false; bakumatsuNize(tf); } private function bakumatsuNize(img:DisplayObject):void{ while(canvas.numChildren > 0){ canvas.removeChildAt(0); } canvas.graphics.clear(); canvas.addChild(img); var f:ColorMatrixFilter = new ColorMatrixFilter([ 3, 0, 0, 0, -200, 0, 3, 0, 0, -200, 0, 0, 3, 0, -200, 0, 0, 0, 1, 0 ]); var f2:ColorMatrixFilter = new ColorMatrixFilter([ 1 / 4, 1 / 2, 1 / 8, 0, 0, 1 / 4, 1 / 2, 1 / 8, 0, 0, 1 / 4, 1 / 2, 1 / 8, 0, 0, 0, 0, 0, 1, 0 ]); img.filters = [f, f2]; var texture:Bitmap = new Texture(); texture.width = img.width; texture.height = img.height; canvas.addChild(texture); texture.alpha = 0.5; // 枠 img.x = img.y = texture.x = texture.y = 5; canvas.graphics.beginFill(0xf3f3f3); canvas.graphics.lineStyle(1, 0xcccccc); canvas.graphics.drawRect(0, 0, img.width + 10, img.height + 10); canvas.graphics.endFill(); canvas.filters = [new DropShadowFilter(2, 45, 0, 0.4)]; } } }