アンチエイリアスとの戦い

最近は、shadowコマンドのアンチエイリアス部分の処理を改善する方法がないか、四六時中考えていた。影とアンチエイリアス処理された部分の色が「混ざっちまっているんだからどうしようもないじゃないか」と最初は諦め気分だったのだが、考え続けているといろいろな作戦が思い浮かんでくるものである。
以下は影付き画像から、影の部分だけをいかに美しく取り除くことができるか、その試行錯誤の記録である。

ことの経過

  • だらしなく無駄に広がるOSXのウィンドウのスクリーンショットの影。
  • 影付きウィンドウのスクリーンショットをそのままブログに使うと、影の部分が大き過ぎて、肝心のウィンドウ内部の絵が小さくなってしまう。
  • そうかといって、影なしの画像ではメリハリのない印象で、何か足りない気がする。
  • 無駄に広い影をスリムな必要最小限の影にしたい!
  • また、AppleScriptAutomatorなどから手軽に利用できる仕組みがいい。
  • shadowコマンドの開発はそこから始まった。
  • スクリーンショットは、影なし撮影も可能だ。
  • やってみると、影なし画像に影を付けるのは、とても簡単だった。
    • NSShadowの影の設定をして、NSImageに元画像を再描画するだけでOK。
  • ところが、影あり画像の影も調整できるようにしようと思い始めると、問題が山積した。
    • 影なしのスクリーンショットが撮影できるのだから、本来不要な機能かもしれないが、
    • 触発されたMiniShadow.appは、影付き画像の影まで調整できるのだ!
    • この機能に純粋に感動したので、shadowコマンドにもぜひ実装したいと思っていた。
  • 影つき画像の影を調整するには、いったん影の部分を取り除いた画像にする必要がある。
    • 影なしの画像になれば、NSShadowに好みの設定をして、影を自由に調整できるのだ。
  • 四角いウィンドウなら話は簡単。上下左右の影の部分を切り取ってしまえばいいのだ。
  • ところが、OSXのウィンドウは角が丸い。25年以上も前のMacintoshの頃から角丸ウィンドウ(ジョブズのビルこだわり)だった。
  • すると、単純に四角領域に影の部分をカットしただけでは、コーナー外周部分の影が黒く残ってしまうのである...。

20121122111519

  • この黒く残った影を取り除くにはどうすれば良いのか?
  • 効率的な方法は思いつかなくて、やってみたのは1ピクセルずつ色情報を調べて、
  • もし影だったら透明色にしてしまう、という何の工夫もない地道な方法である。
  • 当初、影か 影でないかの判定には、色情報のアルファ値(透明度の情報)に少しでも透明が入っていたら、そのピクセルは影と見なして透明色で塗りつぶしていた。
  • するとコーナー外周部の黒い影はきれいさっぱり消えたのだが、今度は別の問題が発生。
  • コーナー部分が予想したよりも大きく削り取られてしまうのである。
影を切り取ったコーナー(shadow) 理想のコーナー(OSX標準)
20121122090503 20121122091218
  • 予想より大きく切り取られてしまうのは、コーナーのカーブをより美しく表現するために、OSXアンチエイリアス処理をしているからである。
  • アンチエイリアス処理されたピクセルには、影の色情報とウィンドウの色情報がブレンドされてしまっているのだ。
  • そのピクセルは、影でもあり、実体でもある。という、とっても曖昧な状態になっているのだ。
  • その優柔不断なピクセルから、きっちり影の色情報だけ取り除けば理想のコーナーになるはずなのだが、一体どうすればいいのか...。
  • 曖昧なアンチエイリアスな境界から、真実の境界を見つけ出す。果たして、そんな方法が存在するのか?

拡大・縮小作戦

  • まず思いついたのが、高精細な画像を用意しておいて、それを必要なサイズに縮小する方法。
  • 高精細な画像が縮小される過程で、必要に応じてアンチエイリアスな画像処理になるのではないか?と考えてしまった。
  • しかし、いったん撮影されたスクリーンショットの画像の解像度を高めることは困難である。
    • でも昔、ビットマップ画像をベクター画像のように拡大できる画像処理エンジンをどこかで見た気がする。
  • また、撮影時に高精細な画像にしておくくらいなら、最初から影なしで撮影すれば良い訳で、本末転倒。
  • さらには1.5倍して、0.6666...倍すれば、端数処理の過程でアンチエイリアス処理が復活するかもしれない!
  • と思ってやってみたが、無情にもまったく効果はなかった。(というよりさらに悪くなった感じ)
  • 結局、拡大・縮小作戦では、あえなく撃沈されてしまった...。
  • 一方、アンチエイリアスから美しいカーブを取り出すことはできなかったが、
  • その過程で追加した拡大・縮小の機能は、-zオプションとして実装した。
      • -zオプションの「z」はズームの「z」
$ shadow -z 500 test.png 
  • 画像test.pngを一辺の最大値が500ピクセルに縮小して影付けを行う。
  • 500ピクセル未満の画像は、拡大はせず、そのままの等倍で処理する。
$ shadow -z 0.7 test.png
  • 画像test.pngを0.7倍のサイズに縮小して影付けを行う。
  • -zオプションの値には、上記のようにピクセル指定と倍率指定の二つの意味がある。
  • その判断の違いは、-zオプションの値の大きさ。
    • 8を超える値は、ピクセル制限として処理する。
    • 8以下の値は、倍率として処理する。


今やshadowコマンドは自由に拡大・縮小できるようになったのだ!
でも、肝心なアンチエイリアスから真実の境界を見つける方は、さっぱりダメ...。

分析

  • 自分の勘やひらめきだけでは、なかなか解決できないと思った。
  • そもそも、アンチエイリアスとは何なのか?影とは何なのか?
  • まずは問題となっている対象の本質を探っておく必要がありそう。
  • 以下は、四角領域にクリッピング後、透明度のあるピクセルだけを検出した、座標と色情報のダンプリストである。
    • 情報の並びは以下の順
    • (x, y) a透明度 r赤 g緑 b青
  • ちょうど、この画像のコーナー部分を想像すると良いかもしれない。(左下原点で)

20121122144652

    • a透明度が0に近いほど、透明になる。
    • a透明度が1に近いほど、rgbの色が濃くなる。
影がコーナーに残った画像
2012-11-15 04:28:57.149 shadow[65140:707] (0, 0) 0.082353 0.000000 0.000000 0.000000
2012-11-15 04:28:57.150 shadow[65140:707] (1, 0) 0.101961 0.000000 0.000000 0.000000
2012-11-15 04:28:57.151 shadow[65140:707] (2, 0) 0.152941 0.000000 0.000000 0.000000
2012-11-15 04:28:57.151 shadow[65140:707] (3, 0) 0.211765 0.000000 0.000000 0.000000
2012-11-15 04:28:57.151 shadow[65140:707] (4, 0) 0.435294 0.603604 0.603604 0.603604
2012-11-15 04:28:57.152 shadow[65140:707] (5, 0) 0.650980 0.807229 0.807229 0.807229
2012-11-15 04:28:57.152 shadow[65140:707] (6, 0) 0.831373 0.886792 0.886792 0.886792
2012-11-15 04:28:57.156 shadow[65140:707] (0, 1) 0.101961 0.000000 0.000000 0.000000
2012-11-15 04:28:57.157 shadow[65140:707] (1, 1) 0.172549 0.000000 0.000000 0.000000
2012-11-15 04:28:57.157 shadow[65140:707] (2, 1) 0.317647 0.382716 0.382716 0.382716
2012-11-15 04:28:57.157 shadow[65140:707] (3, 1) 0.647059 0.806061 0.806061 0.806061
2012-11-15 04:28:57.160 shadow[65140:707] (0, 2) 0.156863 0.000000 0.000000 0.000000
2012-11-15 04:28:57.161 shadow[65140:707] (1, 2) 0.321569 0.365854 0.365854 0.365854
2012-11-15 04:28:57.161 shadow[65140:707] (2, 2) 0.721569 0.831522 0.831522 0.831522
2012-11-15 04:28:57.164 shadow[65140:707] (0, 3) 0.219608 0.000000 0.000000 0.000000
2012-11-15 04:28:57.165 shadow[65140:707] (1, 3) 0.650980 0.795181 0.795181 0.795181
2012-11-15 04:28:57.167 shadow[65140:707] (0, 4) 0.439216 0.580357 0.580357 0.580357
2012-11-15 04:28:57.170 shadow[65140:707] (0, 5) 0.654902 0.790419 0.790419 0.790419
2012-11-15 04:28:57.173 shadow[65140:707] (0, 6) 0.831373 0.872642 0.872642 0.872642
...中略...
影なしウィンドウの画像(理想のコーナー)
2012-11-15 04:28:59.286 shadow[65140:707] (0, 0) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.286 shadow[65140:707] (1, 0) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.287 shadow[65140:707] (2, 0) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.287 shadow[65140:707] (3, 0) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.288 shadow[65140:707] (4, 0) 0.274510 0.942857 0.942857 0.942857
2012-11-15 04:28:59.288 shadow[65140:707] (5, 0) 0.552941 0.943262 0.943262 0.943262
2012-11-15 04:28:59.288 shadow[65140:707] (6, 0) 0.780392 0.944724 0.944724 0.944724
2012-11-15 04:28:59.292 shadow[65140:707] (0, 1) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.293 shadow[65140:707] (1, 1) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.293 shadow[65140:707] (2, 1) 0.125490 0.937500 0.937500 0.937500
2012-11-15 04:28:59.293 shadow[65140:707] (3, 1) 0.549020 0.942857 0.942857 0.942857
2012-11-15 04:28:59.297 shadow[65140:707] (0, 2) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.297 shadow[65140:707] (1, 2) 0.125490 0.937500 0.937500 0.937500
2012-11-15 04:28:59.297 shadow[65140:707] (2, 2) 0.639216 0.944785 0.944785 0.944785
2012-11-15 04:28:59.300 shadow[65140:707] (0, 3) 0.000000 0.000000 0.000000 0.000000
2012-11-15 04:28:59.301 shadow[65140:707] (1, 3) 0.549020 0.935714 0.935714 0.935714
2012-11-15 04:28:59.303 shadow[65140:707] (0, 4) 0.274510 0.928571 0.928571 0.928571
2012-11-15 04:28:59.306 shadow[65140:707] (0, 5) 0.552941 0.936170 0.936170 0.936170
2012-11-15 04:28:59.309 shadow[65140:707] (0, 6) 0.780392 0.924623 0.924623 0.924623
...中略...


以上のダンプリストから分かること。

  • ウィンドウの影とは、色情報を持たない(rgbすべて0。つまり真っ黒)透明度の変化である。*1
  • アンチエイリアス処理とは、色情報を持った影である。
  • rgbの値が常に同じなので、ウィンドウのアンチエイリアス処理はモノクロ情報である。
  • rgbが0である部分は、影のみと考えられる。(アンチエイリアス処理は混ざっていない)
  • よって、rgb情報を持つ透明化されたピクセルは、アンチエイリアス処理の部分である。
    • 影が残った画像では、影も混ざっているかもしれない。
  • 影が残った画像のrgb情報を見て、いかに理想のコーナーのrgb情報に近づけるか、その勝負である。
  • もっと言えば、影が残った画像のargb情報を入力して、理想のコーナーのargb情報を出力する関数を作ればいいのだ!

試行錯誤

  • なるほど、今までは影・アンチエイリアス部分の両方を有無を言わさずa=r=g=b=0にしていたから、コーナー部分がひと回り大きく削ぎ落とされていた。
  • ならば、明確に影しかない部分だけ0にしたらどうなるのだろう?
  • 0にするのは透明度aの値だけ。rgbの値は現状を維持してみる。

すると...
20121122090633
おっ、格段に良くなった感じ!

  • このとき思ったこと。「そうか、argbは割合の情報だから、かけ算するとうまくいくかもしれない!」
  • そう考えて、意味のありそうなかけ算をいろいろ試してみた。ここからは想像力でひたすら試すのみ。
  • そして、最終的に行き着いたのが、r=r/a、g=g/a、b=b/a、a=a * r * g * b。
  • aにrgbをかけ算したのだから、今度はrgbからaを取り除いてみたらどうかと。そんな発想。

進化の過程

すると、どうだろう?今までの計算式の中で、見た目が最も理想のコーナーに近づいた感じがする!(4番)

切り抜きの様子 処理方法
1 20121122090503 r=0、g=0、b=0、a=0
2 20121122090633 rgbは現状保持、a=a * r
3 20121122090943 rgbは現状保持、a=a * r * g * b
4 20121122091045 r=r/a、g=g/a、b=b/a、、a=a * r * g * b
5 20121122091218 影なし画像に対してshadowを実行
(理想のコーナー)
  • なぜ、「r=r/a、g=g/a、b=b/a、、a=a * r * g * b」によってアンチエイリアスから影の情報が取り除かれるのか、理論的にはまったく分かっていない...。
  • さらには、このような現象が起きるのは、OSXのウィンドウの影において限定のことかもしれない。
  • しかし、影つき画像をshadowコマンドで処理した結果は、格段に良くなった!
  • もはや左右に並べて比較しない限り、元が影付きだったのかどうか判断するのは難しいレベルかも。

ダウンロード


久々にコードの世界の楽しさにハマってしまった。(いや、試行錯誤しかなかったが...。)

*1:NSShadowの世界では、色情報を持った影も設定できる。