豪鬼メモ

一瞬千撃

写真の仕上げに露出値を反映してみる

撮影データのEXIFタグに残る露出値を元に写真の輝度を調整する方法について再度検討してみた。デモサイトでも実装されているので、任意のJPEGTIFFやraw形式の画像をアップロードしてみていてだきたい。露出値を見てうまいこと画像を調整してくれる。
f:id:fridaynight:20181222161000j:plain


自分が調整して現像した写真の平均輝度と撮影時の露出値との相関を以前調べたが、相関はあるにしても非常に低いという結果であった。しかし、それは私が撮影時の印象を無視して、鑑賞時に見栄えが良いように仕上げる傾向があるからに過ぎない。現像の趣向に正解や不正解があるわけではないが、撮影時の雰囲気を記録するという目的に照らして考えると、仕上げ画像の平均輝度が露出値と全く相関しないというのもおかしな話だと思えてきた。見栄えの良い現像という範疇には留まりつつも、暗い撮影シーンの画像はちょっと暗めに、明るい撮影シーンの画像はちょっと明るめに現像した方がいいような気がしてきた。

写真1枚だけのことを考えれば、その画像をプレビューしながら見栄えがするように調整すればそれでいい。しかし、たくさんの写真を並べることを考えるとそうもいかない。とある一日の写真を時間を追って見ていくとして、同じ状況で昼間に撮った写真よりも夕方に撮った写真の方が明るかったりすると、ちょっとした違和感を生じるからだ。アルバムすなわち写真集を作り込むという作業にはそのような苦労が伴うはずだ。複数枚の写真の間で仕上げを調整するとなると、組み合わせ問題になって非常に手間がかかるだろう。当然、その作業には輝度の統一的な指標が欲しくなるはずで、それには撮影時の露出値がうってつけだろう。

さて、露出値(EV = Exposure Value)は 2 * log2(Aperture) - log2(ShutterSpeed) - log2(ISO/100) という関数の値だ。変数であるApertureとShutterSpeedとISOは最近のデジカメなら全ての機種でもEXIFデータとして記録されているだろうから、算出は容易だ。RAW画像からもJPEG画像からも多くの場合で取得することができる。ExifToolがあれば以下のコマンドで自動的に計算して表示してくれる。

$ exiftool -lightvalue input.jpg
Light Value : 14.0


暗い画像はちょっと暗めに、明るい画像はちょっと明るめと言っても、そもそも暗いとか明るいとかは相対的かつ主観的な現象であるから、何か客観的な基準がないと機械的に扱えない。私が撮った写真に関して言えば、過去1年間9125枚の写真の平均露出値が9.43ということなので、そのあたりを基準に考えればよさそうだ。そして、露出値7EV、8EV、9EV、10EV、11EVあたりの現像結果を見返しつつ、それらの撮影時の雰囲気を思い出して、画像より暗い印象だったのか、それとも明るい印象だったのかを数えてみた。結論としては、露出値8.5EVくらいを境界として、「画像より実際はもうちょっと明るかった」と感じる確率が、「画像より実際はもうちょい暗かった」と感じる確率を上回る。これはあくまで私の現像工程の癖と私の鑑賞時の趣向について調査したに過ぎないが、とにかく私の現像結果を再調整するにあたっては、8.5EVより低い画像は少し暗くし、8.5EVより高い画像は少し明るくすると、より撮影時の雰囲気が残った印象になりそうだ。

統計値から言えば、露出値の平均は9.43で、標準偏差は2.96で、歪度は-0.09で、中央値は9.29だった。ヒストグラムは以下のようになる。
f:id:fridaynight:20181222161018p:plain

統計値やヒストグラムから考えると、9EVあたりを基準にしてもよさそうな気もしてくるが、ここは自分の感覚に正直に行こう。つまり、私は自分の理想よりちょっと暗めの現像で甘んじていることが多いと解釈する。実際のところ、私は白飛びを嫌って露出アンダーで撮影する派なので、現像時の追い込み不足でアンダー気味になることの方が多いというのは合点が行く。てことで、露出値8.5EVが私の基準点だ。ただし、基準値の設定の大小は実はそれほど問題ではない。目と脳はすぐに慣れるからだ。多くの写真を並べて閲覧したときに、個々の写真の明るさが、撮影時の明るさに照らして逆転が少なければそれで良い。


ワークフローに対する提案は次のものだ。Lightroom等で個々の写真を現像する際には、その個々の写真の見栄えが最も良くなるように現像するものとする。その際に、撮影時の雰囲気がどうだったかとか、隣の写真と比べて明るさに矛盾がないかとかは考えなくてよいものとする。そして、一連の写真をエクスポートした後に、あるスクリプトを呼ぶと、個々の写真の露出値に応じた調整がなされ、アルバムとして矛盾の少ない画像群が生成される。

そのようなスクリプトをどう実装するかが本題である。まず、基準露出を8.5EVに設定して、写真の露出値と基準露出値との差を3で割った値を「露出バイアス値」とする。この3というのは露出値の標準偏差とほぼ同じ値だ。外れ値をうまく処理するために、露出バイアス値の下限は-2.5とし、上限は2.5とした上で、下限値が-1で下限値が1となるように正規化する。つまり、露出値1EV以下だと露出バイアス値は-1になり、露出値8.5EVだと露出バイアス値は0になり、露出値16EV以上だと露出バイアス値は1になる。

露出バイアス値をもとにImageMagickのスケール対数補正を駆使するのが良さそうだが、そのパラメータはどうするか。いろいろ試した結果、露出バイアス値をBとして、(|B|*2) ^ 2 をパラメータとするという経験則を編み出した。露出バイアス値が負数の場合にはスケール対数の逆関数を適用する。それだけだと、明るくする場合には暗部が持ち上がり過ぎて眠い画像になり、暗くする場合には暗部が潰れ過ぎて見づらい画像になってしまう。それを避けるため、スケール対数補正をかける前に、シグモイド補正でコントラストの調整を行うことが必要だ。そのパラメータは、スケール対数補正のパラメータをsとして、log(1 + |s|) * 1.5 くらいにすれば良さそうだ。露出バイアス値が負数の場合は、シグモイド補正の逆関数を適用する。また、暗くする場合にのみ、シグモイド補正の変曲点を、 log(1+|s|*0.5) / log(1+|s|) にする。これも黒潰れを防ぐ工夫だ。基本的に、露出値が高いほどコントラストが高く、露出値が低いほどコントラストは低いのが自然だ。

この仕様をPythonで実装すると以下のようになる。

def adjust_image(input_path, output_path):
  # Compute parameters.
  ev_mean = 8.5
  ev_stddev = 3.0
  ev = get_exposure_value(input_path)
  ev_bias = (ev - ev_mean) / ev_stddev
  ev_bias = min(max(ev_bias, -2.5), 2.5) / 2.5
  ev_slog = (abs(ev_bias) * 2.0) ** 2.0
  ev_contrast = math.log(1 + abs(ev_slog)) * 1.5
  ev_midpoint = math.log(1 + abs(ev_slog) * 0.5) / math.log(1 + abs(ev_slog))
  # Convert the image.
  cmd = ["convert", input_path]
  cmd += ["-colorspace", "rgb"]
  if ev_bias > 0:
    cmd += ["-sigmoidal-contrast", "{:.3f}".format(ev_contrast)]
    cmd += ["-evaluate", "log", "{:.4f}".format(ev_slog)]
  if ev_bias < 0:
    cmd += ["+sigmoidal-contrast", "{:.3f},{:.3f}%".format(ev_contrast, ev_midpoint)]
    cmd += ["-evaluate", "multiply", "{:.4f}".format(math.log(1 + abs(evslog)))]
    cmd += ["-evaluate", "exp", "1"]
    cmd += ["-evaluate", "subtract", "100%"]
    cmd += ["-evaluate", "divide", "{:.4f}".format(abs(ev_slog))]
  cmd += ["-colorspace", "srgb"]
  cmd += [output_path]
  subprocess.call(cmd)

実際にはICCプロファイルの処理や色深度の設定などを加えることになるだろうけど、基本的には上述の実装で所望の動作をするはずだ。ev_biasに掛けている2.0という定数を変えると、補正の度合いを調整することができる。弱めにするなら1.6くらい、強めにするなら2.4くらいにするとよい。


実際の例をいくつか見てみよう。露出値による調整前の画像と調整後の画像を順に並べる。冒頭に挙げたデモサイトにて、"Brightness" という設定を "Auto" にしておくと露出値による調整が行われ、"Normal" だと何もしない。"Light" や "Dark" は露出値を無視して手動で調整するときに用いる。

石垣島の川平湾。13.6EVなので、かなり明るく、slog=1.850, contrast=1.571で調整された。調整前のは薄暗くって朝の雰囲気だが、調整後には昼近くのどぎつい日差しがうまく表現できている。

f:id:fridaynight:20181222195018j:plain:w300 f:id:fridaynight:20181222195028j:plain:w300

石垣島のリゾートヴィレッジ。12.9EVと、かなり明るく、slog=1.377, contrast=1.299で調整された。調整前のは人物の顔が見づらかったが、明るくなったおかげで見やすくなった。

f:id:fridaynight:20181222195043j:plain:w300 f:id:fridaynight:20181222195053j:plain:w300

石垣島のバンナ公園。12.3EVと、かなり明るく、slog=1.027, contrast=1.060で調整された。調整前のは滑り台の白さに引っ張られて露出アンダーが強かったのだが、明るくなったおかげで忠実な感じになった。

f:id:fridaynight:20181222195108j:plain:w300 f:id:fridaynight:20181222195119j:plain:w300

明治神宮で七五三。10.3EVと、やや明るめで、 slog=0.230, contrast=0.311で調整された。これは大して変わらないが、少しだけ明るくする調整の場合でもそれなりに良くなることがわかる。

f:id:fridaynight:20181222195133j:plain:w300 f:id:fridaynight:20181222195146j:plain:w300

家の寝室で色の実験。6.3EVと、やや暗めで、 slog=-0.344, contrast=-0.444で調整された。これも大して変わらないが、少しだけ暗くする調整の場合でもそれなりに良くなることがわかる。

f:id:fridaynight:20181222195159j:plain:w300 f:id:fridaynight:20181222195218j:plain:w300

居酒屋の刺身。 ev=5.2と、そこそこ暗く、slog=-0.774, contrast=-0.860で調整された。調整前は明るい部屋っぽい感じがしたが、調整後は薄暗い居酒屋の様子が良く再現されている。

f:id:fridaynight:20181222195259j:plain:w300 f:id:fridaynight:20181222195310j:plain:w300

家の書斎にある自転車。1.4EVと、かなり暗く、slog=-3.585, contrast=-2.284で調整された。調整前のはとても暗い部屋なのに様子が分かり過ぎたが、調整後は暗さが良く再現されている。

f:id:fridaynight:20181222195322j:plain:w300 f:id:fridaynight:20181223085406j:plain:w300


まとめ。EXIFデータから算出する露出値を使って写真の輝度を調整する手法について検討した。8.5EVより暗いシーンの画像は暗くして、それより明るいシーンの画像は明るくすると雰囲気が出る。その際にはスケール対数補正とシグモイド補正を組み合わせるとトーンカーブの指定が自動化できる。

現像時に既に仕上がったはずの画像を再調整するとは、しかもそれを自動化するとは、無謀な挑戦かもしれない。仮に適用前後の例にブラインドテストをしたとして、勝てる見込みがあるのか。私自身は内容を知っているので被験者になれないし、第三者を使って評価するほどの話ではないので、「作ってみました」で終わるしかない。私個人としては、9割くらいのケースで良くなるような気がしているが、プラセボ効果と言われればそうかもしれない。しかし、個々の写真の見栄えが最適でなくなるとしても、アルバム全体での輝度の露出値への相関を向上させるのが目的であるから、それに照らせばこの実装はそこそこうまくいっていると思う。