豪鬼メモ

一瞬千撃

口径食による周辺減光を再現する

ImageMagickでレンズの口径食による写真像面の周辺減光を再現するにはどうすればいいか考えてみた。
f:id:fridaynight:20181019012838j:plain


写真を撮ると画像の中央あたりよりも周辺部分の方が暗くなっていることが多いが、それは周辺減光とかヴィネッティングとか呼ばれる現象であり、レンズに入射した光線がセンサーに届くまでの光路が一部遮られることによって主に発生する。光路が遮られるのはレンズの前玉や後玉の口径が足りないからなので、口径食とも呼ばれる。センサーの周辺部になるほど画角に占める受光可能角度が減っていくので、周辺が暗くなるというわけだ。このサイトの説明が非常にわかりやすい。なんで点光源でレモン型のボケが現れるかっていうと、外側は前玉の口径食で、内側は後玉の口径食で影ができるからなのね。

口径食は情報の欠損なので基本的に悪者なのだが、周辺減光は中心部に視線を誘導する効果があるので、うまくつかうと好ましい結果が得られることもある。その効果を狙って口径食のあるレンズを積極的に選ぶ人もいるらしい。しかし、私はレンズの口径食は少なければ少ないほど良いと思っている。なぜなら、視線を誘導したい場所が画面の中央にあるとは限らないからだ。あるいは、トリミングした後の状態で周辺を暗くする方が合理的だからだ。なので、現像時には周辺減光補正をオンにして口径食の影響をできるだけ少なくした上で、仕上げの段階で必要に応じて周辺減光を加えるようにしている。

ところで、周辺減光を加えた写真データを保存するのはあまり望ましくない。中央に強烈に視線を誘導したいという意図が現像時にあったとしても、別の見せ方をする時にはそうでないかもしれない。個々の写真を見ていく場合と、本やスライドのページに複数の写真を載せる場合では望ましい仕上げは異なるはずだ。なので、JPEGTIFFなどの現像済みのデータを保存するならば、あまり手を加えずに忠実性を重視した仕上げにした方がよい。

となると、閲覧時に画像加工を行って周辺減光を再現するのが望ましい。デジタル写真においては写真の完成は撮影時でも現像時でもなく、閲覧時になされるものなのだ。それもあって、例の写真アルバム閲覧ツールには、閲覧時に画像加工をする機能がついている。


前置きが長くなったが、ImageMagickを使って既存の写真データに周辺減光効果を加えるにはどうするのがベストプラクティスなのか考えたい。まず、ImageMagickには-vignetteというドンピシャリなオペレータがあるので、それが使えるか知りたい。まずはベースラインとして、完全に白い物体を写した状態を再現した画像を作る。

$ convert -size 1200x800 xc:white white.jpg

f:id:fridaynight:20181019011532j:plain

次に、-vignetteをデフォルトパラメータで呼んでみる。デフォルトでは、縦横それぞれ周辺から10%の位置に長軸および短軸の端が来る楕円を描き、その外側が完全に陰になる。全ての画像加工の前に色空間をRGBに直すのを忘れずに。

$ convert -size 1200x800 xc:white -colorspace rgb -background black -vignette 0x0 -colorspace srgb vignette-default.jpg

f:id:fridaynight:20181019011553j:plain

短辺の10%もが影になってしまうほどイメージサークルが狭いレンズは売っていない。長辺の100%くらいまではちょっとでも光が入ってほしい。なお、イメージサークルという名の通り周辺減光は同心円状に発生するのが普通なのだが、なぜか後処理による周辺減光はアスペクト比に応じた楕円形を使う人が多いらしく、Lightroomのデフォルトでもそうなっている。しかし、ここでは現実のレンズに忠実に同心円状の周辺減光を作ることとする。陰の淵はx軸およびy軸のオフセットを指定することができて、各軸に対するパーセンテージで指定することになっているっぽい。この例の場合は長辺はx軸で1200で、短辺はy軸800なので、長辺は 0% で、短辺は ((1200 - 800) - 1) / 2 = 25% を指定することになる。

$ convert -size 1200x800 xc:white -colorspace rgb -background black -vignette 0x0-0-25% -colorspace srgb vignette-circle.jpg

f:id:fridaynight:20181019011616j:plain

そして、陰の境界をぼかす。上記で0x0の部分はガウシアンぼかしの半径とシグマを指定するらしい。半径は0にしておくと3シグマくらいが自動的に指定されるので、シグマだけ考えればよい。正規分布なので1シグマ内に68.3%、2シグマ内に95.5%、3シグマ内に99.7%の色が溶け出すので、陰の境界から中心までの距離が600ピクセルであることを考えると、シグマを100くらいにしておくと自然な感じになる。実際のレンズによる周辺減光が正規分布になっているわけではないだろうが、自然に感じるグラデーションが実現できればいいのでこれで問題ないとされているのだろう。

$ convert -size 1200x800 xc:white -colorspace rgb -background black -vignette 0x100-0-25% -colorspace srgb vignette-blur.jpg

f:id:fridaynight:20181019011756j:plain

この設定の周辺減光を実際の画像に適用するとこうなる。適用前と適用後を両方示す。

$ convert input.jpg -colorspace rgb -background black -vignette 0x100-0-25% -colorspace srgb vignette-output.jpg

f:id:fridaynight:20180923132940j:plain
f:id:fridaynight:20181019011851j:plain

だいたい良い感じになっていると思う。しかし、ここで注意せねばならないのは、上述の設定が妥当であるためには、入力画像のサイズが1200x800であることが前提であることだ。実際の6000x4000とかの画像に適用するとなると、シグマは500とかにしなければならない。しかし、それは現実的ではない。シグマ500ピクセルのガウシアンぼかしなんて考えられない。3シグマで打ち切るにしても、1ピクセルあたり1500 * 1500 * 3.14 = 7065000回とかの足し算をすることになる。実際、シグマを500にするといつまでも処理が終わらない。つまるところ、この-vignette関数はそのままでは使い物にならない。これを実装した人は小さい画像しか扱っていなかったのだろう。

ワークアラウンドを考えねばならない。とりあえず思いついたのは、小さい画像を周辺減光させてから拡大する作戦だ。といっても、処理対象の画像を縮小したり拡大したりすると画質が壊滅的になってしまうので、周辺減光用のマップを別途用意して、それのみを拡大する。その後、元画像と減光用マップをオーバーレイして積算する作戦だ。まずは対象画像のサイズを確認した上で、読み込む。それとだいたい同じアスペクト比の小さい周辺減光マップを新規レイヤーの上に-vignetteオペレータで作り、元画像と同じ大きさに拡大する。最後に加工対象の画像とオーバーレイして積算で合成して、書き出す。

$ identify big-input.jpg
big-input.jpg JPEG 2400x1620 2400x1620+0+0 8-bit sRGB 1.404MB 0.140u 0:00.130
$ convert big-input.jpg -colorspace rgb \( -page +0+0 -size 600x400 xc:white -colorspace rgb -background black -vignette 0x50-0-25% -resize '2400x1620!' \) -background white -compose multiply -flatten -colorspace srgb big-output.jpg

はまりどころとしては、-flatten オペレータは -background で指定した背景色の上で行われるので、背景色を白色にしておかないとmultiplyモードの -compose はうまく動かないことだ。あと、減光用のマップが小さすぎると拡大時にカラーバンディングが発生する恐れがあるので、600x400とかくらいの大きさは用意した方がよさげだ。なお、-resize オペレータで拡大する場合、Mitchellというフィルタで画素補完するので、よほどでなければ綺麗なグラデーションになる。上述のコマンドで生成される実際の周辺減光マップはこんな感じだ。
f:id:fridaynight:20181019012047j:plain

で、最終的に生成される画像はこれ。完全に意図通りの画像が生成できた。処理時間も数秒だ。
f:id:fridaynight:20180923132938j:plain

周辺減光の形が楕円の方が望ましいという場合には、こんな感じのコマンドになる。正方形の周辺減光マップを長方形に変換する過程で真円が楕円になる。真円の減光マップを作る際には、画面外側10%のところを境界にしてから、画面10%の長さ(50ピクセル)をシグマとしてぼかしている。

$ convert big-input.jpg -colorspace rgb \( -page +0+0 -size 500x500 xc:white -colorspace rgb -background black -vignette 0x50-10-10% -resize '2400x1620!' \) -background white -compose multiply -flatten -colorspace srgb oval-output.jpg

楕円の周辺減光だと、レンズの口径食というよりは、自分の目が閉じかけて視野が狭くなっているような印象になる。好みの問題だが、私はやはり円形の方が好きかな。
f:id:fridaynight:20180923132939j:plain

ところで、DxOMarkのVignettingの測定結果とかを見ていると、面白いことがわかる。開放時には画面中心からすでに減光が始まって、画面周辺に向かって線形に近い形で減光していくことが多いっぽい。2段ほど絞ると、画面周辺まで減光があまりない状態になるが、画面周辺だけは少し落ちるような感じだ。で、ガウシアンぼかしによる周辺減光は、この絞った状態の模様に近い。しかし絞った状態での減光は非常に少ないのが普通なので、上述の例はちょっと違和感がある。なので、なので、-vignette による減光用マップを作るときには、-background を gray するなどで、もう少し程度を抑える操作をした方がよさげだ。

さて、絞り開放付近にしたレンズで撮影した時の周辺減光を再現するには、radial-gradation を使って周辺減光マップを作った方がよい。こんな風にする。

convert big-input.jpg -colorspace RGB \( -page +0+0 -size 2400x1620 -define gradient:radii=2000,2000 radial-gradient:white-gray -colorspace RGB -level 0,80% \) -background white -compose multiply -flatten -colorspace sRGB radius-output.jpg

この場合の周辺減光マップはこうなる。中央のほんの少しの領域は減光がないが、ちょっと離れた場所から画面周辺に向かって光量がなだらかに落ちていく。
f:id:fridaynight:20181019033821j:plain

これを元画像に適用した結果がこれ。個人的にはこれが一番好みかな。
f:id:fridaynight:20180923132941j:plain

radial-gradientで中心が白、画面端が完全に黒となるように周辺減光をかけるとこうなる。これと元写真の間で適切な強度を探すのが主な調整作業になるだろう。円の半径を増大させれば画面端での輝度の落ち込みのタイミングを遅らせることができるし、背景色のグレーの明るさを変えれば円の端の位置でかかる最大の減光量を調整できる。
f:id:fridaynight:20181019100355j:plain

radial-gradientの中心をずらすことで、視線誘導の先を変えることもできる。この例だと馬の尻よりも子供の顔を見て欲しいので、x=30%,y=30%のあたりに中心を置いてみてもいいかもしれない。あんまり露骨にずらすといかにもCGっぽい感じになってしまうが。
f:id:fridaynight:20181019125028j:plain


まとめ。ImageMagickでレンズの口径食による周辺減光を再現することができた。-vignetteオペレータはデフォルトではアスペクト比に合わせた楕円の減光を作ろうとするので、円形の減光が欲しい場合にはオフセットを適宜計算する必要がある。また、大きい画像に適用すると遅いので。小さい周辺減光マップをレイヤー内で作ってから拡大して適用するという手順が必要になる。自分で周辺減光マップを作るならradial-gradationとか使って任意の模様を描くのがよさげだ。私ならそうする。