豪鬼メモ

一瞬千撃

ガンマ曲線とシグモイド曲線による画像補正

PhotoshopLightroomGimpなどの画像処理ソフトで画像に任意のトーンカーブを適用して画像の明るさやコントラストを調整するのはよくやられることだが、自動化する場合にどうやってその曲線を定義しているのかという話。

まずはガンマ曲線のグラフだ。ガンマ0.5で暗くする曲線と、ガンマ1.0で全く変化しない直線と、ガンマ2.0で明るくする曲線を描いた。

f:id:fridaynight:20170713203217p:plain


ガンマ補正関数の入出力をパラメータ毎に表にするとこうなる。

x g=0.5 g=0.6 g=0.7 g=0.8 g=0.9 g=1.0 g=1.2 g=1.4 g=1.6 g=1.8 g=2.0
0.0 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
0.1 0.010 0.022 0.037 0.056 0.077 0.100 0.147 0.193 0.237 0.278 0.316
0.2 0.040 0.068 0.100 0.134 0.167 0.200 0.262 0.317 0.366 0.409 0.447
0.3 0.090 0.134 0.179 0.222 0.262 0.300 0.367 0.423 0.471 0.512 0.548
0.4 0.160 0.217 0.270 0.318 0.361 0.400 0.466 0.520 0.564 0.601 0.632
0.5 0.250 0.315 0.371 0.420 0.463 0.500 0.561 0.610 0.648 0.680 0.707
0.6 0.360 0.427 0.482 0.528 0.567 0.600 0.653 0.694 0.727 0.753 0.775
0.7 0.490 0.552 0.601 0.640 0.673 0.700 0.743 0.775 0.800 0.820 0.837
0.8 0.640 0.689 0.727 0.757 0.780 0.800 0.830 0.853 0.870 0.883 0.894
0.9 0.810 0.839 0.860 0.877 0.890 0.900 0.916 0.928 0.936 0.943 0.949
1.0 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000


入力が[0,1]の範囲だとすると、ガンマ補正関数の式は x^(1/gain) ということで非常に単純である。Rubyで書くとこんな。

def gamma(x, gain)
  x ** (1.0 / gain)
end


次にシグモイド曲線のグラフだ。シグモイド-5でコントラストを下げる曲線と、シグモイド0で全く変化しない直線と、シグモイド5でコントラストを上げる曲線。
f:id:fridaynight:20170713203735p:plain


シグモイド補正関数の入出力をパラメータ毎に表にするとこうなる。

x g=-5.0 g=-4.0 g=-3.0 g=-2.0 g=-1.0 g=0.0 g=1.0 g=2.0 g=3.0 g=4.0 g=5.0
0.0 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000 0.000
0.1 0.169 0.146 0.127 0.112 0.103 0.100 0.097 0.089 0.077 0.064 0.051
0.2 0.275 0.253 0.232 0.215 0.204 0.200 0.196 0.185 0.168 0.147 0.126
0.3 0.359 0.343 0.327 0.313 0.303 0.300 0.297 0.286 0.271 0.251 0.228
0.4 0.431 0.423 0.415 0.407 0.402 0.400 0.398 0.392 0.383 0.370 0.356
0.5 0.500 0.500 0.500 0.500 0.500 0.500 0.500 0.500 0.500 0.500 0.500
0.6 0.569 0.577 0.585 0.593 0.598 0.600 0.602 0.608 0.617 0.630 0.644
0.7 0.641 0.657 0.673 0.687 0.697 0.700 0.703 0.714 0.729 0.749 0.772
0.8 0.725 0.747 0.768 0.785 0.796 0.800 0.804 0.815 0.832 0.853 0.874
0.9 0.831 0.854 0.873 0.888 0.897 0.900 0.903 0.911 0.923 0.936 0.949
1.0 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000 1.000


入力が[0,1]の範囲だとすると、シグモイド関数の式は 1/(1+e^(-a*x)) なのだが、変曲点をx=0でなくx=0.5の位置にずらしたいので、1/(1+e^(-a*(x-0.5)とする。さらにそれだけだと出力が[0,1]の範囲を使いきらないので、sig(0)とsig(1)が0と1になるようにスケール変換してあげる必要がある。下記では、変曲点のx座標もパラメータとして指定できるようにしている。

def naive_sigmoid(x, gain, mid)
  1.0 / (1.0 + Math::exp((mid - x) * gain))
end

def scaled_sigmoid(x, gain, mid)
  min = naive_sigmoid(0.0, gain, mid)
  max = naive_sigmoid(1.0, gain, mid)
  s = naive_sigmoid(x, gain, mid)
  (s - min) / (max - min)
end

ネットで調べると、スケール変換しないで単にx軸をずらしたシグモイド曲線を使っている人をたまに見かけるが、そうすると変域を合わせるためにゲインを設定して画像が変化してしまうし、ホワイトポイントとブラックポイントのずれが起きて非常に扱いにくいから注意。


ちなみに変曲点を0.25と0.75にして描画するとこうなる。全体的に明るくしたいけどシャドーの締りは保ちたいという場合はmid=0.25のシグモイド補正が有効だ。逆に全体的に暗くしたいけどハイライトの輝きは保ちたいという場合にはmid=0.75のシグモイド補正が有効。
f:id:fridaynight:20170713204010p:plain
f:id:fridaynight:20170713204023p:plain


ところで、シグモイド補正関数でゲインを負数にした場合には正の場合の逆関数として機能してほしくなる。ImageMagickのenhance.cにある実装をRubyで書きなおしたのがこれ。もうちょい簡潔にならんもんかと思うけども、一応正しい答えを出すみたい。

def naive_inverse_sigmoid(x, gain, mid)
  min = naive_sigmoid(0.0, gain, mid)
  max = naive_sigmoid(1.0, gain, mid)
  s = naive_sigmoid(x, gain, mid)
  a = (max - min) * x + min
  Math::log(1.0 / a - 1.0)
end

def scaled_inverse_sigmoid(x, gain, mid)
  if x < 0.00001 || x >= 0.99999
    return x
  end
  min = naive_inverse_sigmoid(0.0, gain, mid)
  max = naive_inverse_sigmoid(1.0, gain, mid)
  s = naive_inverse_sigmoid(x, gain, mid)
  (s - min) / (max - min)
end


補正例を挙げておく。まずは元画像。
f:id:fridaynight:20170713205655j:plain

ガンマ補正2.0。全体的に明るくなるのだが、シャドー部が特に明るくなるのが特徴。言い換えれば、シャドー部に階調を多く割り当て、ハイライト部の階調を圧縮する。
f:id:fridaynight:20170713205742j:plain

ガンマ補正0.5。全体的に暗くなるのだが、ハイライト部が特に暗くなるのが特徴。言い換えれば、ハイライト部に階調を多く割り当て、シャドー部の階調を圧縮する。
f:id:fridaynight:20170713210057j:plain

シグモイド補正5の変曲点0.5。コントラストが高まってどぎつい画像になる。輝度50%付近に豊かな階調を割り当て、そこから離れたシャドーとハイライトは階調が圧縮される。
f:id:fridaynight:20170713210201j:plain

シグモイド補正マイナス5の変曲点0.5。コントラストが低まって眠い画像になる。輝度50%付近の階調を圧縮し、そこから離れたシャドーとハイライトは階調が豊かになる。
f:id:fridaynight:20170713210636j:plain

シグモイド補正5の変曲点0.25。輝度25%付近に豊かな階調を割り当て、そこから離れたハイライトは階調が圧縮される。
f:id:fridaynight:20170713210735j:plain

シグモイド補正5の変曲点0.75。輝度75%付近に豊かな階調を割り当て、そこから離れたシャドーは階調が圧縮される。
f:id:fridaynight:20170713210819j:plain

この例だと元画像が既によく撮れているのであまり調整の余地はないのだが、敢えてやるなら、手前の人物達を明るくするためにガンマ補正2.0をかけつつ、そうすると圧縮されてしまう背景の階調を変曲点0.8のシグモイド補正4で回復してあげるくらいかな。

convert input.tif -gamma "2.0" -sigmoidal-contrast "4.0x80%" output.jpg

f:id:fridaynight:20170714110618j:plain

ガンマ補正2.0(赤線)、シグモイド補正4の80%(橙線)、両者の合成(緑線)のトーンカーブを比較すると、何が起こったかわかりやすい。ガンマ補正だけだと最暗部に階調を割り当てすぎるきらいがあるのだが、変曲点を高めにしたシグモイド補正を組み合わせるといい感じになるのだ。
f:id:fridaynight:20170714110852p:plain

画面全体にかける補正は、限られた空間におけるパイの取り合いである。シャドーの階調を豊かにすればハイライトが犠牲になり、ハイライトの階調を豊かにすればシャドーが犠牲になる。なので、基本的には主要被写体の階調を豊かにして、他は犠牲にしてもいいという優先順位付けが必要となる。画面全体の視認性を良くしたいなら、レタッチソフトウェアの補正ブラシとかマスクとかいった機能を使って手前の人物だけを明るくする必要があるだろう。そもそも、撮影時にフラッシュを炊いて人物を明るくしておけば後処理が必要なかったのだろうが、それもまた面倒だから、後処理で頑張ると割り切るのもありかと。


もっと多彩な変化球を投げ分けたい御仁のために、いくつか関数を組み合わせて曲線を描いてみた。gはガンマ補正関数、sはシグモイド補正関数を示す。注意すべきは、関数を適用する順番で曲線が変わるということ。
f:id:fridaynight:20170714143119p:plain
f:id:fridaynight:20170714184501p:plain


同じ話を以前に書いた記憶がうっすらあるのだが、過去ログ探しても見つからなかったのでまた書いてしまった。各種のチューニングをしている際に、たまにこの表を見たくなってしまうので。

でもまあ、正直言ってLigthroomのシャドー&ハイライト補正がやたら便利なのでImagemagickはあんまり使わなくなった。暗い写真は露出補正をプラス側に振って明るく補正して、圧縮されたハイライト側をハイライト補正をマイナス側に振って回復してあげればいい。明るい写真の場合はその逆で、露出補正を下げてシャドーを上げる。結果的にちょっとコントラストが下がった感がある場合には、コントラスト補正をプラスに振って相殺することになるだろう。同じことをImageMagickなどの非WISIWIG環境でやろうとするとかなり頭を使わないといけない。それこそ上で書いたグラフや表が脳内に出来上がっていないといけないのだ。