python上でraw現像を行って行くにあたり、これまでrawpyを使った現像に関していろいろ調べてきました。ただ、このライブラリのお手軽現像関数postprocessに少し足りない機能があり、それらの補填していきました。今回それらのまとめです。
PythonとOpenCVを使って、濃度域毎のトーン調整、彩度調整、シャープネス調整、周辺光量調整、レンズ歪曲補正を作ってきました。今回まとめの意味でそれらを関数化してみました。
前提
今回画像は16bitでデコードしたものを、最大値で割った0~1の値で正規化された64bit floatを前提にしています。理由はOpenCVの関数を使うにあたり、unsigned 16bitをうまく扱ってくれなかったからです。
# uint16 -> float64変換(0~1) img = img/65535
こんなイメージです。もちろん8bitデコードして255で割ってもいいんですが、トーン調整や彩度補正でビットが落ちるので極力高いビット数を使いたいので、16bitを前提にしています。もちろん処理時間は伸びるのでしょうけどね…。
ちなみにdoxygenスタイルでコメント入れてますが、doxygenの動きを確認していないので、うまく動いてくれているかは不明です…。Pythonでdoxygenを試したことがないので、今後そのあたりは検証してみたいと思います。
追記:doxygen試しました。間違ってました。直しました。詳細はこちら。
トーン調整
濃度域毎の明るさ調整です。スプライン曲線でガンマ補正するイメージで作りました。
今回関数化するにあたり、入力のアンカーポイントを0, 0.25, 0.5, 0.75, 1 に固定したときの出力のアンカーポイントを引数にしました。正直最初と最後の0と1は固定な気がしますが、一応引数にしました。
## # @fn tone_correct() # @brief 階調毎トーン調整関数 # @param img 0~1へ正規化された入力画像(numpy array) # @param anchor_point 補正値 (float array) 画像のダーク部、中央部、ハイライト部の補正値 # @return 補正後の画像 def tone_correct(img, anchor_point): xs = [0, 0.25, 0.5, 0.75, 1] tck = splrep(xs, anchor_point) # スプライン return splev(img, tck) # スプライン適用
こんな関数で、
img = tone_correct(img, [0, 0.15, 0.5, 0.85, 1])
こんな呼び方を想定しています。この場合、ダーク部をより暗く、ハイライト部をより明るくしているので、コントラスト強調しているようなイメージです。中間部はいじってません。
彩度・シャープネス補正
続いて、彩度補正とシャープネス補正です。この2つをいっぺんにやるのは、処理の途中でHSV変換をしているのですが、その変換を1回で済ますためです。あまりクールではないですが、変換ロスを抑えるためにあきらめることにします。
残念ながらこの処理だけ内部演算がfloat32になります。というのもどうやらOpenCVでの色変換がfloat64に対応していないためです。処理を抜けた結果もfloat32に丸まっている仕様にしています。
## # @fn sat_and_sharpness() # @brief 彩度およびシャープネス調整関数 # @param img 入力画像(numpy array) # @param sat_gain 彩度補正値 (float) 正の値 1より大で彩度UP、小で彩度DOWN # @param shp_gain シャープネス調整パラメータ (float) 適用率、0で無変化 # @param shp_size シャープネス調整パラメータ (int) ガウシアンのフィルタサイズ # @param shp_sigma シャープネス調整パラメータ (float) ガウシアンの標準偏差 # @details 画像の彩度調整、シャープネス調整を同時に行う(出力がfloat32へ縮退する) # @return 補正後の画像 def sat_and_sharpness(img, sat_gain, shp_gain, shp_size, shp_sigma): # 1.RGBからHSVへの色空間変換 img_hsv = cv2.cvtColor(img.astype(np.float32), cv2.COLOR_RGB2HSV) # 2.Sへのゲイン調整(彩度調整) img_hsv[:, :, 1] = np.clip(img_hsv[:, :, 1] * sat_gain, 0, 1) # 3.Vへのアンシャープマスク(シャープネス調整) img_blr = cv2.GaussianBlur(img_hsv[:, :, 2], (shp_size, shp_size), shp_sigma) img_hsv[:, :, 2] = np.clip(img_hsv[:, :, 2] + shp_gain * (img_hsv[:, :, 2] - img_blr), 0, 1) # 4.HSVからBGRへの色空間変換 return cv2.cvtColor(img_hsv, cv2.COLOR_HSV2RGB)
こんな関数です。
img = sat_and_sharpness(img, 1.4, 0.5, 7, 3)
こんな呼び方です。彩度を強調して、シャープネス(アンシャープマスク)を強めにかけてます。
周辺光量補正
続いて、周辺光量補正です。画像中心から外に向かって明るさを調整します。
中心を1.0として、外の明るさのゲイン差を引数として指定するスタイルにしました。ちょっとわかりにくいですが、周辺を中心に対して1.8倍明るくする場合には、0.8を指定するという仕様です。今回関数化するにあたり、RGB独立で値を指定できるようにしました。がホワイトバランス補正後であれば通常は同じ値ですかね。
また上の記事では逆ガンマを当ててから補正してましたが、ガンマの往復はやめにしました。良し悪しあると思いますが、ガンマ補正前に処理したければ現像時にOFFにする方がいいかなと思ってそうしました。
## # @fn peripheral_light_correct() # @brief 周辺減光補正関数 # @param img 入力画像(numpy array) # @param gain_params 補正値(R,G,B独立)(np array) # 例えば0.8を指定すると画像の長い側の辺の端で1.8倍のゲインがかかる。 # @details 画像中心からの距離に応じた減光補正を行う。 # @return 補正後の画像 def peripheral_light_correct(img, gain_params): 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.距離画像にゲインを乗じ、ゲインマップを作製 gain0 = gain_params[0] * r + 1 gain1 = gain_params[1] * r + 1 gain2 = gain_params[2] * r + 1 gainmap = np.dstack([gain0, gain1, gain2]) # 3.画像にゲインマップを乗じる return np.clip(img * gainmap, 0., 1.0)
な感じで、呼び方は
img = peripheral_light_correct(img, np.array([0.8, 0.8, 0.8]))
周りを中心の1.8倍明るくしています。
歪曲補正
最後は歪曲補正です。樽、糸巻のレンズ歪みを補正するものです。
関数の引数は、2乗の項と4乗の項と6乗の項をそれぞれ指定するタイプで、6乗の項だけデフォルト値0にしています。
## # @fn undistort() # @brief 歪曲補正関数 # @param img 入力画像(numpy array)float64である必要がある。 # @param k1 2乗の項(float) # @param k2 4乗の項(float) # @param k3 6乗の項 default = 0(float) # @details 画像中心からの距離に応じた補正を行う。<br> # (1 + k1*r^2 + k2*r^4 + k3*r^6)<br> # 値が負であれば樽型歪みの補正<br> # 値が正であれば糸巻型歪みの補正になる # @return 補正後の画像 def undistort(img, k1, k2, k3 = 0): h, w = img.shape[:2] f = max([h, w]) # カメラ行列 mtx = np.array([[f, 0., w / 2], [0., f, h / 2], [0., 0., 1 ]]) # 歪み補正パラメータ dist = np.array([k1, k2, 0, 0, k3]) n_mtx = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1)[0] remap = cv2.initUndistortRectifyMap(mtx, dist, np.eye(3), n_mtx, (w, h), cv2.CV_32FC1) return cv2.remap(img, remap[0], remap[1], cv2.INTER_CUBIC)
CUBIC補間がしたかったのと、必要箇所の切り出しもやりたかったので、cameramatrixは作り直しするタイプの呼び出しにしています。このあたりの詳細は前記事にて。なぜかこの関数float32で画像を渡すと補間演算に失敗するのか画像が一部不正になります。なので、彩度・シャープネス補正との呼び出し順は気にする必要があります。
呼び方は
img = undistort(img, 0.4, 0.2)
こんな感じ。この場合糸巻型歪みの補正になります。
まとめ
これらでまとめた関数を一つのファイルにでもまとめて、外部モジュールとしておいとけば、それっぽく便利に使えます。ここでまとめたEXIFの付与コード
も含めて、おまけで記載残しておきます。
処理の適応順がイマイチな気がしますが、とりあえずサンプルということで。
全コード
postpostprocess.py
## # @file postpostprocess.py # @brief 現像後補正関数群 # @details 与える画像は基本的にRGB順で値を0~1に正規化したfloat64であることを想定 import cv2 import numpy as np from scipy.interpolate import splrep, splev ## # @fn undistort() # @brief 歪曲補正関数 # @param img 入力画像(numpy array)float64である必要がある。 # @param k1 2乗の項(float) # @param k2 4乗の項(float) # @param k3 6乗の項 default = 0(float) # @details 画像中心からの距離に応じた補正を行う。<br> # (1 + k1*r^2 + k2*r^4 + k3*r^6)<br> # 値が負であれば樽型歪みの補正<br> # 値が正であれば糸巻型歪みの補正になる # @return 補正後の画像 def undistort(img, k1, k2, k3 = 0): h, w = img.shape[:2] f = max([h, w]) # カメラ行列 mtx = np.array([[f, 0., w / 2], [0., f, h / 2], [0., 0., 1 ]]) # 歪み補正パラメータ dist = np.array([k1, k2, 0, 0, k3]) n_mtx = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1)[0] remap = cv2.initUndistortRectifyMap(mtx, dist, np.eye(3), n_mtx, (w, h), cv2.CV_32FC1) return cv2.remap(img, remap[0], remap[1], cv2.INTER_CUBIC) ## # @fn peripheral_light_correct() # @brief 周辺減光補正関数 # @param img 入力画像(numpy array) # @param gain_params 補正値(R,G,B独立)(np array)<br> # 例えば0.8を指定すると画像の長い側の辺の端で1.8倍のゲインがかかる。 # @details 画像中心からの距離に応じた減光補正を行う。 # @return 補正後の画像 def peripheral_light_correct(img, gain_params): 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.距離画像にゲインを乗じ、ゲインマップを作製 gain0 = gain_params[0] * r + 1 gain1 = gain_params[1] * r + 1 gain2 = gain_params[2] * r + 1 gainmap = np.dstack([gain0, gain1, gain2]) # 3.画像にゲインマップを乗じる return np.clip(img * gainmap, 0., 1.0) ## # @fn sat_and_sharpness() # @brief 彩度およびシャープネス調整関数 # @param img 入力画像(numpy array) # @param sat_gain 彩度補正値 (float) 正の値 1より大で彩度UP、小で彩度DOWN # @param shp_gain シャープネス調整パラメータ (float) 適用率、0で無変化 # @param shp_size シャープネス調整パラメータ (int) ガウシアンのフィルタサイズ # @param shp_sigma シャープネス調整パラメータ (float) ガウシアンの標準偏差 # @details 画像の彩度調整、シャープネス調整を同時に行う(出力がfloat32へ縮退する) # @return 補正後の画像 def sat_and_sharpness(img, sat_gain, shp_gain, shp_size, shp_sigma): # 1.RGBからHSVへの色空間変換 img_hsv = cv2.cvtColor(img.astype(np.float32), cv2.COLOR_RGB2HSV) # 2.Sへのゲイン調整(彩度調整) img_hsv[:, :, 1] = np.clip(img_hsv[:, :, 1] * sat_gain, 0, 1) # 3.Vへのアンシャープマスク(シャープネス調整) img_blr = cv2.GaussianBlur(img_hsv[:, :, 2], (shp_size, shp_size), shp_sigma) img_hsv[:, :, 2] = np.clip(img_hsv[:, :, 2] + shp_gain * (img_hsv[:, :, 2] - img_blr), 0, 1) # 4.HSVからBGRへの色空間変換 return cv2.cvtColor(img_hsv, cv2.COLOR_HSV2RGB) ## # @fn tone_correct() # @brief 階調毎トーン調整関数 # @param img 0~1へ正規化された入力画像(numpy array) # @param anchor_point 補正値 (float array) 画像のダーク部、中央部、ハイライト部の補正値 # @return 補正後の画像 def tone_correct(img, anchor_point): xs = [0, 0.25, 0.5, 0.75, 1] tck = splrep(xs, anchor_point) # スプライン return splev(img, tck) # スプライン適用
main.py
# postpostprocess sample code import rawpy import numpy as np from PIL import Image import piexif import postpostprocess as ppp raw = rawpy.imread(filename) img = raw.postprocess(half_size=True, use_camera_wb=True, output_bps=16) # uint16 -> float64変換(0~1) img = img/65535 img = ppp.undistort(img, 0.4, 0.2) img = ppp.peripheral_light_correct(img, np.array([0.8, 0.8, 0.8])) img = ppp.tone_correct(img, [0, 0.15, 0.5, 0.85, 1]) img = ppp.sat_and_sharpness(img, 1.4, 0.5, 7, 3) # float64 -> uint8変換 img = (img*255).astype(np.uint8) exif_dict = piexif.load(filename) # 画像サイズだけ修正 exif_dict["0th"][piexif.ImageIFD.ImageWidth] = img.shape[1] exif_dict["0th"][piexif.ImageIFD.ImageLength] = img.shape[0] exif_bytes = piexif.dump(exif_dict) im = Image.fromarray(img) im.save(savename, "jpeg", exif=exif_bytes, quality=95)
コメント