室内や日陰で撮った写真が期待より暗くなってしまうという失敗をバッチ処理で修正する方法について模索する日々だが、かなり実用的な方法が実装できたので紹介してみる。
トーンカーブの手動修正
何も考えずにオートで写真を撮ると、室内や日陰だと以下のような暗い写真ができ上がってくることが多い。サングラス越しに見ているような陰鬱な感じだ。
このような暗い写真は、PhotoshopやGIMPで開いてヒストグラムを確認すると、ハイライト側が完全に空いてしまっていることがわかる。これを修正するには、山の右端が最も明るくなるように、トーンカーブの右肩を天井にくっつけてあげればいい。
そうすると以下のようにいい感じに修正できる。ヌケた感じっていうのか知らんが、さっきのに比べると、サングラスを外して裸眼で見ているような感じがするのは俺だけではあるまい。
山の右端よりも天井との接点をちょっと右側にしておけば、既に明るい部分にもちゃんと階調が残る。山の右端よりも左にかかってしまうと、以下のように明るい部分の階調が飛んでしまう「白トビ」が発生することになる。布団のシワがなくなってる!
自動修正の既存手法
トーンカーブ機能が便利なことはわかったが、いちいち手動で修正するのはプログラマとしていけてないので、自動化を模索する。まずはGIMPの自動修正機能(Color - Auto - Stretch Contrast)を試してみると、以下のようになる。全然修正されてない。この機能でうまくいく写真もあるけど、だいぶ保守的な味付けになっているらしく、うまく修正できないことも多い。
次に、ImageMagickのconvertコマンドのcontrast-stretchオペレータを試してみる。これは、画像の中で最も明るい点が真っ白になるように修正してくれるという賢い機能であり、大抵の場合にうまくいく。
ただ、弱点もある。以下の画像のように、最も明るい点が多数あるような場合だ。暗めの室内で撮影した場合や、雪や水面のように輝度が高い景色を白トビ防止のために露出アンダーで撮った場合にこのような状況になることが多い。
これにcontrast-stretchをかけると、画面全体が白トビしてしまう。アグレッシブすぎるのだ。
convertコマンドはlevelオペレータというのもあり、これは真っ白にする輝度(white point)を指定することができる。「画像内の最も明るい点よりもちょっと右側」を指定するといい感じの修正をかけることができる。しかし、具体的に指定すべき値はヒストグラムを見ないとわからないので、自動化できないという問題がある。
ImageMagickには画像の輝度の統計値を出力するidentifyというコマンドがある。輝度分布の最小値、最大値、平均値、標準偏差、尖度、歪度がわかる。この最大値を使ってwhite pointを設定するのがよさそうだと思ったが、画像の中に1ピクセルでも明るいものがあればそれを拾ってしまうがいただけない。ノイズや反射のピクセルに影響されて大抵は最大値が65535になってしまって使いものにならないのだ。
新しい方法
white pointを適当に設定してレベル補正をかけてみて、もし白トビが一定以上発生したらwhite pointを上げて再試行して、白トビが一定以上発生しなければwhite pointを下げて再試行して、二分探索で最適値を探すという方法を思いついた。阿呆っぽいが、しかし最も確実な方法だ。どの程度の白トビを許容するかを自分で設定できるのが最大の利点だ。
ここで、白トビが発生したかどうかを機械的に知るにはどうすればいいかが重要になる。結論としては、元画像の輝度の平均値を予め測っておいて、レベル補正した画像の輝度の平均値をwhite pointの比率で割った値と比較すればわかる。もしwhite pointより上の区間にピクセルがなければ、変換後の輝度の平均値はwhite pointを狭めた区間の長さに比例するはずだ。もしwhite pointより上の区間にピクセルがあれば、そのピクセルはwhite pointに切り下げられるので、変換後の平均輝度はより低くなるはずだ。
レベル補正や統計値算出の処理には結構時間がかかるので、それを高速化するために、元画像のサムネイルを予め作っておき、それを対象に集計処理を行う。量子化誤差を最小にするために16ビットPNGを出力する。ついでにサムネイルにボカシ処理をかけるとノイズに対する耐性を高めることもできる。また、画像の端はレンズの収差で輝度が安定しない傾向にあるので、それを削っておく。
以上のことをRubyスクリプトで実装するとこんな感じになる。
C_tmp_dir = '/tmp' C_cnv_cmd = 'convert' C_id_cmd = 'identify' C_highlight_margin = 1.0 # 指定したパスの画像のwhite pointをパーセントで返す def check_highlight(src_path) # 一時ファイルのパス thumb_path = "#{C_tmp_dir}/imgcnvblog-thumb.png" test_path = "#{C_tmp_dir}/imgcnvblog-test.png" # サムネイルを作る cmd = "#{C_cnv_cmd}" cmd += " -resize \"640x640\"" cmd += " -blur \"2x2\"" cmd += " -chop \"5%\"" cmd += " -depth \"16\"" cmd += " \"#{src_path}\"" cmd += " \"#{thumb_path}\"" system(cmd) # サムネイルの平均輝度を算出する orig_mean = `#{C_id_cmd} -format \"%[mean]\" \"#{thumb_path}\"`.to_f white = 100 if orig_mean > 0 # 二分探索 high = 100.0 low = 50.0 while high - low >= 0.1 mid = (high + low) / 2.0 # レベル補正を行う cmd = "#{C_cnv_cmd}" cmd += " -level \"0%,#{mid}%,1.0\"" cmd += " -depth \"16\"" cmd += " \"#{thumb_path}\"" cmd += " \"#{test_path}\"" system(cmd) # 補正後の平均輝度を算出する test_mean = `#{C_id_cmd} -format \"%[mean]\" \"#{test_path}\"`.to_f ratio = test_mean * (mid / 100.0) / orig_mean # 白トビ許容値より高いか低いか if ratio < 0.99999 low = mid else high = mid white = mid end end end # 一時ファイルを削除する begin File::unlink(test_path) rescue end begin File::unlink(thumb_path) rescue end # 山の右端よりちょっと右をwhite pointにする white += C_highlight_margin white >= 100.0 ? 100.0 : white end
白トビしそうでしないくらいまで明るくする設定にしているが、C_highlight_marginの値を3.0とかにするともうちょっと保守的な振る舞いになる。
バッチコマンド
上記の関数を使ったコマンドを書いた。ついでに、ブログ用に画像をリサイズしたりその他の画像補正をかけるのも同時に行うようにする。俺のカメラ(RX100)は基本的に暗い画像を作ってくるのでガンマ補正1.05をかけ、またオートホワイトバランスが寒色気味(色温度高め)にしてくるので、色レベル補正で相殺するようにしている。このあたりの設定は用途やカメラの種類によって変えるとよいだろう。
以下のように実行すると、複数の画像ファイルに一括して変換処理を実行して、結果をimgcnvblog-outというサブディレクトリの中に保存してくれる。
$ imgcnvblog *.JPG
結果としては、こんなふうに修正される。かなり俺ごのみの設定にできるようになった。変換前と変換後を並べてみる。GIMPの自動補正で失敗する例とconvertのstretch-contrastで失敗する例の両方とも、このスクリプトではうまく扱えるのだ。
風景撮りなどで既にそこそこコントラストが高い画像に対して適用しても大丈夫。雲が(元画像以上には)白トビしないのがナイス。
まとめ
手動でトーンカーブの修正をするのが面倒な場合には、ImageMagickのconvertコマンドをバッチで呼ぶと便利だ。その際に、contrast-stretchオペレータを使うと過度に白トビが発生することがあるので、投機的にレベル補正をかけて最適値を算出する方法を考えてみた。結果的になかなか満足いくものができたと思う。
これで、大抵の画像の輝度を自動的に持ち上げても破綻しないようになったので、オートのままで撮りまくっても大丈夫になった。あるいは、白トビしそうな場合には明示的に露出を下げて取るという選択肢も可能になった。楽だなー。
追記:この手法を洗練させたものをCGIスクリプト実装し、Webブラウザから使えるようにした。Simple Photo Correctorをお試しあれ。