豪鬼メモ

一瞬千撃

スケール対数曲線をトーンカーブとする画像補正

画像の明るさを変える際にトーンカーブを与えるのは一般的だが、GUIを使わない自動化を前提としたImageMagickなどのツールを使う場合には、トーンカーブの曲線を数式として与えねばならない。よく使われるガンマ補正だが、もうちょい使いやすい曲線はないのかと思っていた。最近のお気に入りは、スケール対数曲線である。それについてのメモ。
f:id:fridaynight:20180421171146j:plain


Dcrawが生成する暗い画像を明るくする処理を実装していたのだが、その際にガンマ補正を適用することを検討した。しかし、ガンマ補正によるゲインはシャドー部が明るくなりすぎて「ゆるふわ調」になってしまうので使えない。かといって、シグモイド曲線だけで補正すると、ハイライト部が明るくなりすぎて飛び気味になってしまう。シャドーはあんまり持ち上げずに、中間部を持ち上げて、しかもハイライトを天井に貼り付けないような曲線はないものか。

ImageMagickの-evaluateオペレータの仕様を眺めていたところ、logという関数が使えそうだと気づいた。vを元画素の輝度とすると、log(1+v*C)/log(1+C) という式となる。Cは0以上の任意の数値をパラメータとして指定できる。ちなみに、ガンマ補正の式は v ^ (1/C) である。注意すべきは、スケール対数曲線の場合はC=0の時に線形になるのに対し、ガンマ曲線の場合はC=1の時に線形になるということだ。

輝度50%の画素を輝度60%に上げたいとすると、ガンマ補正の場合、0.5 ^ (1/1.37) = 0.6 という変換になり、対数補正の場合、log(1+0.5*1.3)/log(1+1.3) = 0.6 という変換になる。その際に、輝度5%の画素はどうなるかというと、ガンマ補正だと0.112となり、対数補正だと0.076となる。対数補正の方がシャドーの立ち上がりが鈍いのだ。表とグラフで可視化してみた。gamma 1.37とlog 1.3は輝度50%を輝度60%に上げる設定で、gamma 2.0とlog 6.0は輝度50%を輝度70%に上げる設定である。

x linear gamma 1.37 gamma 2.0 log 1.3 log 6.0
0.000 0.000 0.000 0.000 0.000 0.000
0.050 0.050 0.112 0.224 0.076 0.135
0.100 0.100 0.186 0.316 0.147 0.242
0.150 0.150 0.250 0.387 0.214 0.330
0.200 0.200 0.309 0.447 0.277 0.405
0.250 0.250 0.364 0.500 0.338 0.471
0.300 0.300 0.415 0.548 0.395 0.529
0.350 0.350 0.465 0.592 0.450 0.581
0.400 0.400 0.512 0.632 0.503 0.629
0.450 0.450 0.558 0.671 0.553 0.672
0.500 0.500 0.603 0.707 0.601 0.712
0.550 0.550 0.646 0.742 0.648 0.750
0.600 0.600 0.689 0.775 0.692 0.784
0.650 0.650 0.730 0.806 0.735 0.817
0.700 0.700 0.771 0.837 0.777 0.847
0.750 0.750 0.811 0.866 0.817 0.876
0.800 0.800 0.850 0.894 0.856 0.903
0.850 0.850 0.888 0.922 0.894 0.929
0.900 0.900 0.926 0.949 0.930 0.954
0.950 0.950 0.963 0.975 0.966 0.977
1.000 1.000 1.000 1.000 1.000 1.000

f:id:fridaynight:20181116013025p:plain

グラフの黄色線と橙色線を比べると、50%付近で交わりつつも、0に近いシャドー部では橙色が低いのがよくわかる。ハイライト部の描写は両者でそんなに変わらない。


てことで、画像を明るくするには、以下のコマンドを実行すればよい。入力画像を読み込み、座標系を線形RGBに変換してから、対数補正を適用して、座標系をsRGBに戻して、出力画像を書き出すのだ。

$ convert input.jpg -colorspace rgb -evaluate log 1.3 -colorspace srgb output.jpg


実際の画像に適用するとどうなるのか。露出アンダーめの画像を用意して、それにガンマ補正と対数補正を適用した結果を並べて見てみよう。元画像、ガンマ補正結果、対数補正結果の順で並べていく。
f:id:fridaynight:20181117000439j:plain
f:id:fridaynight:20181116014158j:plain
f:id:fridaynight:20181116014214j:plain

f:id:fridaynight:20181116014501j:plain
f:id:fridaynight:20181116014513j:plain
f:id:fridaynight:20181116014531j:plain

f:id:fridaynight:20181116014911j:plain
f:id:fridaynight:20181116014922j:plain
f:id:fridaynight:20181116014940j:plain

f:id:fridaynight:20180518190952j:plain
f:id:fridaynight:20180518190953j:plain
f:id:fridaynight:20180518190954j:plain

やはり対数補正の方が私好みの絵を出してくると感じる。つか、ガンマ補正の結果が変すぎる。最後の例のように最暗部のみを持ち上げたい場合にはガンマ補正の方が効果的だけど、一般的にちょっと暗めな写真を明るくしたいような場合には、対数補正の方が断然使える。ディスプレイの明るさを変えるのと写真の明るさを変えるのは勝手が違うということか。


全体を明るくしたいけどシャドーを明るくしたくないという気持ちがもっと強いのなら、ガンマの逆関数と対数関数を合成するといい。ガンマ値を1以上にするとシャドーが重点的に明るくなるが、ガンマ補正のパラメータを逆数にすると元の値の逆関数になる。てことは、逆数でガンマ補正をかけて暗くしてから、それを相殺すべく対数補正をかければ、シャドーをあんまり上げずに中間部からハイライトにかけてなだらかに上げるという曲線が描けるはずだ。いろいろ試した結果、全体のパラメータをCとした場合、最初にパラメータ 0.9^C でガンマ補正をかけ、その結果に対してパラメータ 4^(C-1) で対数補正をかけると、双方が相殺しあって、所望の曲線が得られることがわかった。この複合関数と対数関数を可視化してみた。パラメータ1の対数補正とパラメータ1.6の複合補正がだいたい同じような効果になる。
f:id:fridaynight:20181116233650p:plain

対数補正1.3に匹敵するのは複合補正1.8くらいなので、それを実際の画像に適用してみよう。すなわち、ガンマ補正を 0.9^1.8=0.827 でかけて暗くしてから、対数補正を 4^(1.8-1)=3.03 でかけて明るくする。

$ convert input.jpg -colorspace rgb -gamma 0.827 -evaluate log 3.03 -colorspace srgb output.jpg

シャドーの締まりを保ったまま全体を明るくできているはずだ。水墨画調といった感じか。画像を別タブで開いて元画像と比べてみてほしい
f:id:fridaynight:20181117000335j:plain

現実的には、シャドーも相応に持ち上げた方が自然なので、上記の合成関数の出番はあまりないだろう。そういう変換もできると知っておくだけで十分だ。やはり素の対数関数が最も汎用的な気がする。


対数関数を使って暗くするには、逆関数を使うことになる。log(1+v*C)/log(1+C) の逆関数は (exp(v*log(1+C))-1)/C である。C=2の場合の対数関数とその逆関数の曲線をグラフで示す。
f:id:fridaynight:20181120145438p:plain

上記のトーンカーブを適用する素朴な方法として、-fx オペレータがある。C=2の対数関数とその逆関数はそれぞれ以下のように適用できる。-fxの数式内ではlogでなくlnを使うべし。

$ convert input,jpg -colorspace rgb -fx "ln(1+2*u)/ln(1+2)" -colorspace srgb output-light-fx.jpg
$ convert input,jpg -colorspace rgb -fx "(exp(u*ln(1+2))-1)/2" -colorspace srgb output-dark-fx.jpg

しかし、-fxオペレータは動作が遅いので、別の方法を考えるべきだ。明るくする対数関数に関しては、-evaluate log 2 とやればいいことは既に述べた。その逆関数は -evaluate exp 2 とやれたら嬉しいのだが、exp関数の式はそうではなく、単なる exp(v*C) なので、使えない。しかたないので、以下のように分解して-evaluateオペレータを適用する。1.098612 はlog(1+2)を事前計算した結果である。なお、この方法だと計算の途中経過が1を超えてしまうので、HDIRを有効にしたImageMagickでしか使えない(7以降ならデフォルトで有効)。

$ convert input.jpg -colorspace rgb \
  -evaluate multiply 1.098612 -evaluate exp 1 -evaluate subtract 100% -evaluate divide 2 \
  -colorspace srgb output-dark-evaluate.jpg

C=1.3の対数補正後画像、元画像、C=1.3の逆対数補正画像を順に並べる。やはり、明るくする場合にも、暗くする場合にも、対数関数による補正は自然に感じられる結果が得られると言えそうだ。
f:id:fridaynight:20181120154339j:plain
f:id:fridaynight:20181120154349j:plain
f:id:fridaynight:20181120154817j:plain


まとめ。ImageMagickの-evaluateオペレータのlog関数は、画像を自然な感じで明るくするのにとっても使えるぞ。君もやってみてネ。