豪鬼メモ

MT車練習中

Dcrawでraw画像の現像を自動化

各社のraw画像を現像できる奇跡のツールDcrawを使ってraw現像工程を自動化してみよう。その前に、私がLightroomで手動で私好みに調整して現像した結果を示す。これになるべく近い結果を自動的に得られるようにするのが目標となる。


Dcrawは dcraw.c というC言語の単一のファイルとして配布される面白いツールで、それをコンパイルすると実行可能バイナリができあがる。もちろん、各種プラットフォーム用のバイナリ配布もある。コマンド引数として入力ファイルを与えると、それを現像して、デフォルトではPNM形式の画像を生成する。出力ファイル名は入力ファイル名の拡張子を.pnmに変えたものとなる。主なオプションとしては以下のものがある。

  • -T : 出力をTIFF形式にする。出力ファイルの拡張子は .tiff になる。
  • -6 : 出力の色深度を16ビットにする。
  • -w : カメラが指定したホワイトバランスを使う。
  • -g : 出力のガンマ値を指定する。引数として、ガンマ値と、線形変換とガンマ変換の閾値を指定する。-g 1 1 で線形出力となる。
  • -H 0 : 出力のホワイトレベルを、RGBのどれかのチャンネルの飽和点に設定し、余ったチャンネルの情報をクリップして偽色を抑制する。
  • -H 1 : 上述のチャンネルハイライトクリップを行わず、ハイライト部に色を残す。
  • -H 2 : ハイライトクリップをした画像としていない画像を混合する。
  • -H 3〜5 : ハイライトをうまいこと復元する。3が発色重視で、9が偽色抑制重視。
  • -W: 出力のホワイトレベルを固定する。つまり、ヒストグラム上の輝度の最大値を1にする変換を行わない。
  • -n : ウェーブレットノイズリダクションを行う。引数として閾値を取るが、それは100から1000の間にするらしい。

ということで、基本的には以下のようなコマンドを実行することになる。そうすると image.tiff というTIFF画像ができあがる。-T -6 で16ビットTIFFを指定し、-w でホワイトバランスを撮影時のものにして、-g 1 1 で線形RGBにして、-H 5 でハイライトをバランス重視で復元して、-n 100 で弱めのノイズリダクションをかけている。

dcraw -T -6 -w -g 1 1 -H 5 -n 100 image.orf

この設定での出力はこんな感じになる。線形RGBのTIFFをsRGBのJPEGに変換して示す。なんら調整をしていないので、やたら暗い画像になっているが、ちゃんと現像されていることはわかる。

特筆すべきは、ハイライトの粘りだ。-H 0 だと、左のフェンスの部分は白飛びしてしまうし、-H 1 だと、その部分はマゼンタの色がついてしまう。この画像を撮影したE-M10は(そしてその他の多くのデジカメは)、高輝度で白飛びする場合にRGBのGチャンネル(緑)が最初に飽和することが多いが、Rチャンネル(赤)とBチャンネル(青)はまだまだ粘る。なぜGが最初に飽和するかというと、白色光を入力した場合には、緑フィルタの透過率が最も高いからだ。なので、Gチェンネルの飽和点をホワイトポイントとして他のチャンネルをクリップしないと、緑の補色であるマゼンタの色づきが発生してしまう。しかしクリップしてしまうと情報が勿体無い。ということで、白飛びする直前の周囲の画素を参照して色相を推定し、輝度はRとBチャンネルから推定することで、ハイライトを復元する手法が考えられる。それが -H の3から9の挙動であり、多くの例では -H 5 で良い結果が得られる。苔生したフェンスがちゃんと緑になっていて、髪飾りの黄色がちゃんと残っている。一方で、子供の顔の右側の背景にある青っぽい物体のハイライト部分を見ると、マゼンタの偽色が出ている。これが復元の副作用だ。

ハイライトの設定は現像のパラメータとしてはホワイトバランスと並んで最も重要だ。ガンマ補正等は後工程でいくらでもいじれるが、その二点は現像時に適正値を設定しないと取り返しが難しい。なので、-H のその他の設定の結果も見てみよう。まずは -H 0 で、完全なチャンネルハイライトクリップ。フェンスがはっきりと白飛びしている。

つぎに、-H 1 でチャンネルハイライトクリップなし。激しいマゼンタの偽色で使い物にならない。

ハイライト復元で偽色抑制重視の -H 3 の結果はこれ。-H 5 とあまり変わらないかな。

ハイライト復元で発色重視の -H 9 の結果はこれ。髪飾りに偽色が出ているが、フェンスの部分の復元っぷりはかなり素敵だ。ハイライトがこんなに粘っているとは 、E-M10をちょっと見直した。

個人的には偽色は極力避けたいが、完全にクリップするのは白飛びがひどすぎるので、自動化するなら -H 3 くらいがいいと思う。


情報を失わないように現像ができたら、あとは明るさとコントラストを適宜調整すれば、それなりに見られる画像が出てくることになる。大抵の場合、Dcrawはハイライトの情報を失わないように暗い画像を生成してくるので、画像を明るくするように後処理を施す必要がある。

ImageMagickのconvertコマンドを使うと後処理は実現できる。基本戦略としては、-contrast-stretch オペレータでダイナミックレンジを必要十分に狭めてから、-sigmoidal-contrast オペレータを重ねがけして暗部のコントラストを保ちつつ明るさを調整する。試しに、-contrast-stretch 0.05x0.02% として、画面の 0.05% を黒つぶれさせ、画面の 0.02% を白飛びさせてみる。その程度の量の情報欠損は目立たないし、それでいて画面全体のコントラストが確保できる。さらに、-sigmoidal-contrast 3,20% を7回重ねがけして、全体を明るくする。20%と中心にしてシグモイド3でコントラストを上げると、20%輝度だった画素の明るさは20%以上になって明るくなる。繰り返して同じ処理を行なった場合には、最初の場所よりも少し暗い場所を強調することになる。強いコントラスト操作を一点にかけると画像が破綻するので、弱い操作を何度もやるのが重要だ。トーンカーブを手動で設定すれば同様のことが一撃でできるのだが、自動化するのが今回の目的なので、迂遠なことをしている。

工夫の甲斐あって、結果はこうなる。フリーソフトウェアの組み合わせだけでここまでできるとは、意外と言っては失礼だが、感心した。

全体として人に見せても問題ない出来にはなっていると思う。欠点はあるが、受忍できる範囲だ。冒頭の自分で追い込んだ画像よりはパンチが足りないし、その割に白飛びが目につく(厳密に言えば飛んではいないのだが、ゾーン的にほぼ白飛び扱いになる輝度の画素が多い)。まあこの例をきちんと仕上げるなら人物の部分にだけマスクをかけて選択的に明るくするのが理想であり、自動化するのであればこの程度の瑕疵は仕方ないと割り切るべきだ。

自動化で常に問題となるのは、入力データによって最適なパラメータが異なることである。今まで使っていた画像は、背景のハイライトが飛ばないように、露出アンダーで撮影したものだ。Dcrawが暗めの画像を出力するという以上に、入力のrawデータがアンダーなのを補正していた。もともと適正露出の画像に同じ処理を行うと、このように明るくなりすぎてしまう。

適正露出は撮影者の作画意図によって変わるため、rawデータからそれを自動的に判断することはできない。AIに判断させるというのはAdobe様やGoogle様ならやってくれるだろうけど、ここではちと手に余る。ここでは簡便法として次の経験則を用いる。(1) 主要被写体は多分、画面の端にはない。(2) 主要被写体付近の平均輝度はsRGBで33%以上だと見やすい。で、これを実現するには、投機的にコントラスト調整をかけてその結果が条件を満たすか判定しなければならない。そのためには、現像後に画面の端を切り落とした上で縮小画像を作ってから、それに対してコントラスト調整をかけていって、適切な回数を割り出せば良い。そうすると、明るい方の画像もこのように適切なところで落ち着く。

ところで、Dcrawはrawファイルに付随するEXiF等のメタデータを出力ファイルに伝搬させてくれない。よって、必要であればexiftoolでメタデータの伝搬を行うことになる。その際に、画像の回転を示すorientationをコピーしてはいけない。なぜならdcrawはorientationを見て画像を回転後の状態に正規化するからだ。また、この手順ではカラープロファイルを常にsRGBにするので、カラープロファイルの有無を示すcolorspaceは "srgb" (実際の値は1)にする。

以上の手順を自動化するには、こんな感じのPythonスクリプトを呼べばよい。DcrawとImageMagickとExifToolが入っていればMacでもLinuxでもWindowsでも動くはず。自動化ついでに上記より複雑な処理を入れている。

#! /usr/bin/python3

import os
import re
import shutil
import subprocess
import sys
import tempfile

CMD_DCRAW = "dcraw"
CMD_CONVERT = "convert"
CMD_IDENTIFY = "identify"
CMD_EXIFTOOL = "exiftool"


# Execute a command and return its exit status code.
def run_command(cmd_args):
  with open(os.devnull, "r+b")  as devnull_file:
    return subprocess.call(cmd_args, stdin=devnull_file, stdout=devnull_file, stderr=devnull_file)


# Execute a command and return the content of the standard output.
def get_command_result(cmd_args):
  with open(os.devnull, "r+b")  as devnull_file:
    try:
      return subprocess.check_output(cmd_args, stdin=devnull_file, stderr=devnull_file)
    except subprocess.CalledProcessError as error:
      return error.output


# Develop a raw image file into TIFF, PNG, PNM or JPEG.
def develop_raw(input_path, output_path):
  tmp_dir = tempfile.gettempdir()
  pid = str(os.getpid())
  raw_path = os.path.join(tmp_dir, "develop_raw-{}-{}".format(pid, os.path.basename(input_path)))
  raw_stem, raw_ext = os.path.splitext(raw_path)
  tiff_path = raw_stem + ".tiff"
  sample_path = os.path.join(tmp_dir, "develop_raw-{}-sample.tif".format(pid))
  
  try:
    # Develop the raw image.
    shutil.copyfile(input_path, raw_path)
    cmd = [CMD_DCRAW, "-T", "-6", "-w", "-g", "1", "1","-H", "4", "-W", "-n", "100", raw_path]
    run_command(cmd)
    if not os.path.isfile(tiff_path):
      raise RuntimeError("TIFF file is missing.")

    # Adjust dynamic range.
    cmd = [CMD_CONVERT, tiff_path]
    cmd += ["-set", "colorspace", "rgb"]
    cmd += ["-colorspace", "srgb"]
    cmd += ["-contrast-stretch", "0.05x0.02%"]
    cmd += ["-colorspace", "rgb"]
    cmd += ["+level", "0,96%"]
    cmd += ["-evaluate", "log", "1.2"]
    cmd += ["-colorspace", "srgb"]
    cmd += [tiff_path]
    run_command(cmd)

    # Make a sample file.
    cmd = [CMD_CONVERT, tiff_path]
    cmd += ["-colorspace", "rgb"]
    cmd += ["-gravity", "center", "-extent", "80x80%"]
    cmd += ["-resize", "1000x1000"]
    cmd += ["-colorspace", "srgb"]
    cmd += [sample_path]
    run_command(cmd)

    # Calculate times for contrast adjustment.
    level = 0
    while level < 15:
      cmd = [CMD_IDENTIFY, "-format", "%[mean]", sample_path]
      id_result = get_command_result(cmd)
      brightness = float(id_result.strip()) / 65535.0
      if brightness >= 0.35:
        break
      level += 1
      cmd = [CMD_CONVERT, sample_path]
      cmd += ["-colorspace", "rgb"]
      cmd += ["-sigmoidal-contrast", "3,20%"]
      cmd += ["-colorspace", "srgb"]
      cmd += [sample_path]
      run_command(cmd)

    # Apply the contrast adjustment.
    cmd = [CMD_CONVERT, tiff_path]
    cmd += ["-colorspace", "rgb"]
    for i in range(level):
      cmd += ["-sigmoidal-contrast", "3,20%"]
    cmd += ["-colorspace", "srgb"]
    cmd += [output_path]
    run_command(cmd)
    
  finally:
    # Clean up temporary resources.
    for tmp_path in (sample_path, tiff_path, raw_path):
      if os.path.isfile(tmp_path):
        os.remove(tmp_path)

  # Propergate metadata.
  cmd = [CMD_EXIFTOOL, "-tagsfromfile", input_path]
  cmd += ["-orientation=", "-thumbnailimage=", "-colorspace=srgb"]
  cmd += ["-f", "-m", "-overwrite_original"]
  cmd += [output_path]
  run_command(cmd)


# Run the command line.
if __name__ == "__main__":
  if len(sys.argv) != 3:
    print("usage: develop_raw.py input_path output_path", file=sys.stderr)
    exit(1)
  develop_raw(sys.argv[1], sys.argv[2])

上記を develop_raw.py とかいう名前で保存して実行権限をつけてインストールしたら、以下のように実行できる。

$ develop_raw.py input.orf output.tif

実装上の細かいメモ。dcrawに -o 3 とか -o 4 とかのオプションをつけるとそれぞれWide Gamut RGB D65とKodak ProPhoto RGB D65の色空間で現像してカラープロファイルもつけてくれる。注意すべきは、それらはあくまで現像時の色空間(この例では線形RGB)に対してのプロファイルなので、そのプロファイルを適用できるのはあくまで線形RGBのデータだけだ。つまりsRGBに変換した画像に同じプロファイルをつけていると破綻する。したがって、sRGB空間用のICCプロファイルのファイルを別途用意しておいてから、現像後のデータに適用してプロファイルを置き換える必要がある。今回はそのあたりが煩雑なので広色域の現像は省略してsRGBプロファイル決め打ちにした。あと、ImageMagickの-contrast-stretchオペレータはなぜか線形RGBの画像に適用すると絵が破綻するので、sRGBに変換してから適用しないといけない。それからRGB空間に変換した後にその他の階調補正を行なっている。+levelは-levelの逆関数で、"0,96%" とすると、全体の輝度が96%になる。-contrast-stretchのおかげでダイナミックレンジの全体を使い切ることになるので、そのままだと後に明るくする処理でハイライトの質感がなくなってしまう。よって4%の余裕を持たせている。それから -evaluate log 1.2 とかやって画面全体を明るくしている。すなわち log(1+1.2+value)/log(1+1.2) という変換が行われる。シグモイド曲線だけで持ち上げるとハイライトが圧縮されすぎるし、ガンマ曲線を使うとシャドーを伸長しすぎるので、間をとって対数曲線で調整している。


Dcrawによる自動化現像を適用した他の例も見ていく。各写真について、Lightroomの手動調整現像の出力と、Dcrawによる自動現像の出力の順で並べる。まずはうまくいく例。画面全体でダイナミックレンジを使い切っていて、かつ主要被写体の輝度が中庸である場合には、手動調整現像と区別がつかないくらいの絵が出てくることもある。コントラストも彩度もちょっと弱めな感はあるけど、そういう意図なのかなという程度で納得てきる。






次に、あまりうまく行かない例。基本的には、画面全体でダイナミックレンジを使い切らずに、ハイキーやローキーの撮影意図がある場合には、自動処理はその意図を無視してしまうのでイマイチの絵になる。主要被写体の明るさが中庸でない場合にも、理想より暗かったり明るかったりする絵が出てくる。



とはいえ、うまくいかない例でも、ちゃんと写真としては成立しているし、カメラJPEGで露出設定がうまくいかなかった場合よりはかなりマシな仕上がりになっていると思う。細かいことをいうと、Dcrawは歪曲収差補正や倍率色収差補正や周辺減光補正などのレンズプロファイルを要する補正を行わないので、レンズに由来する欠点が目につくことはある。また、歪曲収差補正を行わない関係で、Lightroom現像の例よりも画角がちょっと広くなることがある。良くも悪くも、Dcrawを使う方法は、レンズやカメラの素性をそのまま味わえる現像手法と言えるだろう。そのあたりの補正に対応したフリーソフトウェアの現像アプリケーションとしては、RawTherapeeなどがある。


まとめ。Dcrawを使うと現像処理の自動化ができるのだが、その際にはハイライトの復元や露出の適正化について考慮した設定を行わなければならない。今回は画面中央部の平均輝度から適正露出の範囲を自動検出して設定したが、もちろんそれが最適解ではない。しかし、多くの場合で見るに耐える現像例が出てくるとは思う。

そもそもカメラJPEGでなくrawデータを記録するのは手動で現像設定をして自己満足するためなのに、それを自動化するとはナンセンスにも感じる。また、Lightroom等をつかえばもっとましな自動処理ができるのに、なぜわざわざDcrawを使うのか。例のアルバム管理ツールでraw画像の格納と表示をサポートしたいためだ。手動で追い込むかどうかは別にして、そうすべきかどうか判断するために、そこそこ見られる画像を自動的にサーバ側で生成できたら便利だ。え、それってAdobe Creative Cloud…。