python上でraw現像を続けます。これまでrawpyを使った現像に関していろいろ調べてきました。ただ、このライブラリのお手軽現像関数postprocessに少し足りない機能があり、それらを補填していこうシリーズの第3弾です。
トーン調整、彩度調整、シャープネス調整と、仕上げ処理系の処理をこれまで試してきましたが、今度は光学系の最初に戻って、周辺光量補正を行っていきます。rawpyには色収差の補正は備わっているのですが、光学的な補正として、歪み補正と周辺光量補正は備わっていないので、今回は周辺光量調整処理を補填します。
周辺光量落ち
周辺光量落ちとか、周辺減光とか表現するようですが、撮影した写真が、レンズの外に向かって暗くなっていく現象の事です。
詳細な説明は割愛しますが、センサに届く光の量がセンサの外ほど少なくなってしまうことに起因します。なのでレンズ中心から外に向かって暗くなってしまいます。イメージとしては
こんな感じ。これは作為的に作った画像ですが、均一な真っ白を撮影してみると意外とこんな感じで回りが暗くなってしまいます。一般的に、絞りを開いた状態で(F値を小さくした状態で)、レンズが広角ほどこの現象は顕著になります。色(RGB)によっても程度に差が出ます。
中心に主題となる被写体があり、それを際立たせたいようなときは必ずしも悪い作用だけではないと思いますが、風景とかそんな被写体の時は補正して取り去ってしまいたいです。
厳密にはデモザイク前、ホワイトバランス調整前のRGBに対して処理したいところですが、ここは割り切って、現像済みの16bit画像に対して処理することにします。
モデル化
正直レンズによって、撮影条件によってこの暗くなる程度はまちまちになってしまうので、「これ」といったモデルにはしにくいみたいです。
本気で補正するならば、そのレンズ、カメラを使って白を撮影して、その逆補正をしてやるとよいです。白い紙や布をカメラで覆って、後ろから光を均一に当てて撮影してみましょう。そうするとおそらく上記イメージ画像のようなものが撮影できると思います。そのようにして撮影した画像の明るさの変化をプロファイルしてみると、その減光の様子がわかるはずです。これを補正してあげればよいことになります。
ただ、レンズやセンササイズが違えば、その程度も変わり正直小難しい理屈もいろいろあるようですが、ここはシンプルに、外に向かって線形に(1次式で)暗くなる。を仮定します。
この場合ガンマ補正の前の、純粋な光の量に対してこの過程を置くので、ガンマ補正込みの画像に対しては、そのガンマ補正をキャンセルする必要があります。処理としては、とりあえず画素値を2.2乗をしたうえで補正し、また1/2.2乗を行うことにします。厳密にはrawpyのガンマ補正は、以前確認したように、暗部のノイズ対策の工夫が入っています(BT709)が今回はそれは無視することにします。
処理
フローとしては以下の感じです。
0.16bit現像
1.画像の中心からの距離画像作成
2.距離画像にゲインを乗じ、ゲインマップを作製
3.逆ガンマ補正(2.2乗)
4.画像にゲインマップを乗じる
5.ガンマ補正(1/2.2乗)
6.8bit化して保存
この1番の中心からの距離画像の作成にはmeshgridという関数を使うと便利だということがわかりました。簡単にメモを残しておきます。
今回の場合、-1~+1までの配列を画像幅(高さ)分だけ作って、それを2次元に展開するようなイメージです。wが幅、hが高さ、sizeを幅か高さの大きい方として、
x = np.linspace(-w/size, w/size, w) y = np.linspace(-h/size, h/size, h)
によりx,yにはそれぞれの幅に応じた1次元配列
-1, -0.8, -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1
が作られます。この配列をmeshgridを使って2次元に敷き詰めます。
xx, yy = np.meshgrid(x, y)
-1, -0.8, -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1 -1, -0.8, -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1 : -1, -0.8, -0.6, -0.4, -0.2, 0, 0.2, 0.4, 0.6, 0.8, 1
xxはこんなイメージで出来上がります。yyには縦向きに同じように
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 -0.8, -0.8, -0.8, -0.8, -0.8, -0.8, -0.8, -0.8, -0.8, -0.8 : 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
と敷き詰められます。これにより、xx,yyには画像の中心を原点としたx座標、y座標を意味した2枚の配列が作られます。この2乗和のルートを取れば中心からの距離になります。
r = np.sqrt(xx**2 + yy**2)
これで中心からの距離rの配列(画像)が作れます。今回の場合は半径1で正規化しているので、画像の幅か高さの長い方の距離が1となるような距離画像が作られています。
このxx, yy, rを3次元にプロット(高さ方向にr)すると、
画像中心を0として、きれいに中心からの距離が取れてます。
この距離画像に対して、中心が1.0となるようなゲインマップを作製します。処理のポイントとしてはこれくらいです。
コード
コードを載せます。この例では一番端で1.8倍のゲインがかかる画像が作られます。この1.8倍というのが処理のパラメータになります。写真次第で調整する必要があります。
これだけゲインをかけると、全体少し明るくなりすぎるので、あらかじめ画像は少し暗めに現像しています。(bright = 0.8)また最大値が1となるように画素値は正規化(16bitの最大値で割る)しています。
またRGB同じゲインを乗じることにします。全体のホワイトバランスが整っている前提です。本当は周辺光量を補正した後にホワイトバランスを整えるのが正解なのでしょうけど…。
import rawpy import numpy as np import cv2 # 0.16bit現像(最大が1になるように65535で割る) raw = rawpy.imread(filename) img = raw.postprocess(use_camera_wb=True, bright=0.80, output_bps=16)[:,:,[2,1,0]] / 65535 h, w = img.shape[:2] size = max([h, w]) # 幅、高の大きい方を確保 # 1.画像の中心からの距離画像作成 x = np.linspace(-w/size, w/size, w) y = np.linspace(-h/size, h/size, h) # 長い方の辺が1になるように正規化 xx, yy = np.meshgrid(x, y) r = np.sqrt(xx**2 + yy**2) # 2.距離画像にゲインを乗じ、ゲインマップを作製 gain = 0.8*r + 1 gainmap = np.dstack([gain, gain, gain]) # RGB同じゲイン # 3.逆ガンマ補正(2.2乗) img = img ** 2.2 # 4.画像にゲインマップを乗じる img = np.clip(img * gainmap, 0., 1.0) # 5.ガンマ補正(1/2.2乗) img = img ** (1/2.2) # 6.8bit化して保存 cv2.imwrite("save.jpg",(img*255).astype(np.uint8))
結果
超広角のレンズでとった画像をサンプルとします。
上が無補正の写真です。明るさもデフォルトの状態で現像させたものです。周辺に向かって暗くなっているのがわかります。
これが補正したもの。ちょっと全体的にも明るくなってしまいましたが、空の感じはよくなりました。これで端っこは1.8倍以上のゲインがかかっている状態です。かなりどぎつく補正してもこの程度です。これだけ光量が落ちていたということになります。
この写真に関してはこのパラメータでの補正でちょうどよさそうです。レンズと撮影条件次第ですね。こだわったらきりがなさそう。
まとめ
rawpy postprocessでは処理できない、現像のプロセスではあると便利な周辺光量の調整のコードを、書いてみました。結果も思った通り動いてそうです。
この処理は、本来最初(デモザイク前)に行うべき処理なので、現像後に行うのは少し邪道な気がします。減光のモデルも適当ですし。本気でやるならレンズとセンサからプロファイルを取って、デモザイク前に施すくらいしないといけませんね。
正直ガンマ補正後の信号に対して線形に行ってもそれっぽい結果が出たので、あえてガンマ補正キャンセルしなくて(上記の3と5の処理は飛ばして)もいいかもしれません。処理重たいし。
まぁちょっと画像の周りが暗いなぁ。と思った時のお化粧くらいな感じの処理と思うことにします。
コメント