この記事にはプロモーションが含まれています。
前回の記事では、現像の下準備としてRAWファイルのExif情報の読み取り方法を確認しました。
今回はいよいよ現像処理を行っていきます。
動作環境
動作環境はrawpy導入時と変わってませんが、一応記載しておきます。
- OS : Windows11
- Python 3.10.7
RAW現像処理フロー
最もシンプルなRAWの現像処理フローを以下に示します。
Exif情報を読み取りながら、この処理を施すコードを以下に示します。
一覧性をよくするために、平書きで書いてます。
以降で各処理について解説していきます。
import rawpy
import cv2
import os
from argparse import ArgumentParser
import numpy as np
# --- Options ---
# Commandline arg.
def get_option():
argparser = ArgumentParser()
argparser.add_argument('--in_raw_img', type=str, default = ".\\test.ARW", help='')
argparser.add_argument('--out_dir', type=str,default = ".\\", help='')
return argparser.parse_args()
# Save image file
def save_img(filename, in_img):
in_img[in_img < 0] = 0
in_img = ((in_img / np.max(in_img)) * 255).astype('uint8')
out_img_bgr = cv2.cvtColor(in_img, cv2.COLOR_RGB2BGR)
cv2.imwrite(filename, out_img_bgr)
return 0
# --- Image processing ---
# Black level
def black(in_img, black_level, white_level):
in_img[in_img < black_level] = black_level
in_img[in_img > white_level] = white_level
out_img = in_img - black_level
return out_img
# Demosic
def demosic(in_img):
in_img[in_img < 0] = 0
img_r = in_img[0::2, 0::2]
img_gr = in_img[1::2, 0::2]
img_gb = in_img[0::2, 1::2]
img_b = in_img[1::2, 1::2]
img_g = ((img_gr + img_gb) / 2)
out_img = np.dstack((img_r, img_g, img_b))
return out_img
# White balance
def white_balance(in_img, white_balance_array):
in_img[in_img < 0] = 0
h, w, c = in_img.shape
out_img = np.zeros((h, w, c))
out_img[:, :, 0] = white_balance_array[0] * in_img[:, :, 0] # R
out_img[:, :, 1] = white_balance_array[1] * in_img[:, :, 1] # G
out_img[:, :, 2] = white_balance_array[2] * in_img[:, :, 2] # B
return out_img
# color matrix
def color_matrix(in_img, color_matrix_array):
in_img[in_img < 0] = 0
h, w, c = in_img.shape
out_img = np.zeros((h, w, c))
out_img[:, :, 0] = color_matrix_array[0, 0] * in_img[:, :, 0] + color_matrix_array[0, 1] * in_img[:, :, 1] + color_matrix_array[0, 2] * in_img[:, :, 2] # R
out_img[:, :, 1] = color_matrix_array[1, 0] * in_img[:, :, 0] + color_matrix_array[1, 1] * in_img[:, :, 1] + color_matrix_array[1, 2] * in_img[:, :, 2] # G
out_img[:, :, 2] = color_matrix_array[2, 0] * in_img[:, :, 0] + color_matrix_array[2, 1] * in_img[:, :, 1] + color_matrix_array[2, 2] * in_img[:, :, 2] # B
return out_img
# Gamma
def gamma(in_img, max_val):
# normalization
in_img = in_img / max_val
in_img[in_img < 0] = 0
in_img[in_img > 1] = 1
# gammma correction
alpha = 1.09929683
beta = 0.01805397
out_img = np.where(in_img < beta, 4.5 * in_img, alpha * (in_img ** 0.45) - (alpha - 1))
return out_img
if __name__ == "__main__":
args = get_option()
raw_img_file = args.in_raw_img
raw_img_filename = raw_img_file.rsplit('\\')[-1].split('.')[0]
out_dir = args.out_dir
os.makedirs(out_dir, exist_ok = True)
out_filename = os.path.join(out_dir, raw_img_filename)
print("Input file : ", raw_img_file)
print("Output dir. : ", out_dir)
# read raw
raw = rawpy.imread(raw_img_file)
img_raw = raw.raw_image.copy()
save_img(out_filename + "_out_00_raw.jpg", img_raw)
# black level
print("--- Black Level ---")
print("black_level : ", raw.black_level_per_channel)
print("white_level : ", raw.camera_white_level_per_channel)
black_level_val = raw.black_level_per_channel[0]
white_level_val = raw.camera_white_level_per_channel[0]
img_black = black(img_raw, black_level_val, white_level_val)
save_img(out_filename + "_out_01_black.jpg", img_black)
# demosic
print("--- Demosic ---")
print("color_desc : ", raw.color_desc)
print("raw_pattern : \n", raw.raw_pattern)
img_demosic = demosic(img_black)
save_img(out_filename + "_out_02_demosic.jpg", img_demosic)
# white balance
print("--- White Balance ---")
white_balance_array = raw.camera_whitebalance / np.min(raw.camera_whitebalance)
print("white_balance : ", raw.camera_whitebalance, " -> ", white_balance_array)
img_wb = white_balance(img_demosic, white_balance_array)
save_img(out_filename + "_out_03_wb.jpg", img_wb)
# Color matrix
print("--- Color Matrix ---")
print("color_matrix_rawpy : \n", raw.color_matrix)
exif_color_matri = np.array([1177, -211, 91, -54, 1267, -159, 72, -232, 1216]) / 1024
color_matrix_array = exif_color_matri.reshape((3, 3))
print("color_matrix_exif : \n", color_matrix_array)
img_cm = color_matrix(img_wb, color_matrix_array)
save_img(out_filename + "_out_04_cm.jpg", img_cm)
# gamma
print("--- Gamma ---")
img_gamma = gamma(img_cm, white_level_val)
save_img(out_filename + "_out_05_gamma.jpg", img_gamma)
補助的な関数
まずは、画像処理以外の補助的な関数から。
コマンドライン引数
9~13行目のget_option関数は、コマンドライン引数で読み出すRAW画像ファイルと処理後の画像ファイルの保存先を指定してます。
def get_option():
argparser = ArgumentParser()
argparser.add_argument('--in_raw_img', type=str, default = ".\\test.ARW", help='')
argparser.add_argument('--out_dir', type=str,default = ".\\", help='')
return argparser.parse_args()
コマンドライン引数の使い方の解説は以下の記事で行ってますので、気になる方はご参照ください。
なお、このプログラムではmain関数内では先頭のファイル操作部分で使ってます。
if __name__ == "__main__":
args = get_option()
raw_img_file = args.in_raw_img
raw_img_filename = raw_img_file.rsplit('\\')[-1].split('.')[0]
out_dir = args.out_dir
画像保存
16~21行目のsave_img関数は、画像保存を行います。
def save_img(filename, in_img):
in_img[in_img < 0] = 0
in_img = ((in_img / np.max(in_img)) * 255).astype('uint8')
out_img_bgr = cv2.cvtColor(in_img, cv2.COLOR_RGB2BGR)
cv2.imwrite(filename, out_img_bgr)
return 0
jpegなどの一般的な画像フォーマットは0~255の整数値で諧調が切られてます。
まずは、入力された配列に負の値があれば、クリップを行い、諧調を255で正規化しています。
また、本コードでは画像処理はカラーチャンネルの並びをRGB順の前提で行ってますが、opencvのカラーチャンネルの並びはBGR順です。
このため、opencvで画像保存するために、色配列をRGB→BGRに変更後に保存処理を行ってます。
画像処理の関数
それでは、本処理になる画像処理について、フロー順にコードを解説していきます。
main関数の先頭で画像処理指定後、85~88行目でrawpyの関数を使って、RAW画像データを読み出してます。
# read raw
raw = rawpy.imread(raw_img_file)
img_raw = raw.raw_image.copy()
save_img(out_filename + "_out_00_raw.jpg", img_raw)
この後の画像処理結果と比較するために、このRAW画像を一度保存して、次の処理に移行します。
黒レベル補正
6~21行目のblack関数は、黒レベル補正を行います。
def black(in_img, black_level, white_level):
in_img[in_img < black_level] = black_level
in_img[in_img > white_level] = white_level
out_img = in_img - black_level
return out_img
画像信号には黒レベルと言われるオフセット成分が乗っているので、これを除去してます。
また、無いとは思いますが、一応、飽和よりも大きな信号をクリップ処理してます。
main関数内では、RAW画像に埋め込まれた黒レベルと飽和レベルを使って、黒レベル補正処理をしてます。
# black level
print("--- Black Level ---")
print("black_level : ", raw.black_level_per_channel)
print("white_level : ", raw.camera_white_level_per_channel)
black_level_val = raw.black_level_per_channel[0]
white_level_val = raw.camera_white_level_per_channel[0]
img_black = black(img_raw, black_level_val, white_level_val)
save_img(out_filename + "_out_01_black.jpg", img_black)
今回のRAWでは、黒レベル・飽和レベルは以下だったようです。
--- Black Level ---
black_level : [512, 512, 512, 512]
white_level : [15360, 15360, 15360, 15360]
では処理前後の画像を確認してみます。
これはオフセット成分を削っただけなので、画像の変化はあまりないです。
デモザイク (簡易)
32~40行目のdemosic関数は、デモザイク処理を行います。
def demosic(in_img):
in_img[in_img < 0] = 0
img_r = in_img[0::2, 0::2]
img_gr = in_img[1::2, 0::2]
img_gb = in_img[0::2, 1::2]
img_b = in_img[1::2, 1::2]
img_g = ((img_gr + img_gb) / 2)
out_img = np.dstack((img_r, img_g, img_b))
return out_img
デモザイクとは、白黒の単一チャンネルの配列をRGBの3chの配列に置き換える処理のことを言います。
RAW画像は何も処理がされてないので、カラーチャンネルが存在しません。
実際、適当な領域を拡大するとモザイク状の模様が見えます。
これはイメージセンサのカラーフィルターの配列が見えたものです。
RGGBのBayer配列の場合、R:G:Bは1:2:1の比率で色がイメージセンサ上に分布しており、2×2画素を最小単位として、以下のように配置されてます。
(Gは2個あるので、便宜上R行のGをGr、B行のGをGbとここでは呼ぶ)
この単一チャンネルの画像をRGBの3チャンネルに紐解く必要があるということです。
様々な技法がありますが、ここでは最も簡易的な方法として、2×2画素からR,B,Gをそれぞれ抽出することとします。
R,Bは何もせずそのままの値を、GはGrとGbの平均値を採用します。
今回使っているdemosic関数では色の並びが決め打ちの処理になっているので、Main関数ではraw_patternのメソッドを使って色の並びがRGGBかを確認できるようにしてます。
# demosic
print("--- Demosic ---")
print("color_desc : ", raw.color_desc)
print("raw_pattern : \n", raw.raw_pattern)
img_demosic = demosic(img_black)
save_img(out_filename + "_out_02_demosic.jpg", img_demosic)
記述順が特殊な気もしますが、RGGBの配列で問題ないみたいです。
--- Demosic ---
color_desc : b'RGBG'
raw_pattern :
[[0 1]
[3 2]]
では処理前後の画像を確認してみます。
色味がガラッと変わります。緑っぽく見えるのは、Gの画素の感度がR,Bより高いからでしょう。
また、このデモザイク処理は超簡易版のため、解像度が1/4に落ちます。
ホワイトバランス補正
43~50行目のwhite_balance関数は、デモザイク処理を行います。
def white_balance(in_img, white_balance_array):
in_img[in_img < 0] = 0
h, w, c = in_img.shape
out_img = np.zeros((h, w, c))
out_img[:, :, 0] = white_balance_array[0] * in_img[:, :, 0] # R
out_img[:, :, 1] = white_balance_array[1] * in_img[:, :, 1] # G
out_img[:, :, 2] = white_balance_array[2] * in_img[:, :, 2] # B
return out_img
デモザイクで見たように、イメージセンサの特性そのままでは人間が見たときにキレイな写真にはなりません。
そこで、RGBの信号比率を黒~白を表現するのに適切な比率に調整します。
$$\begin{bmatrix} R_o \\ G_o \\B_o \end{bmatrix} = \begin{bmatrix}Gain_R \\ Gain_G \\ Gain_B \end{bmatrix} \begin{bmatrix} R_i \\ G_i \\B_i \end{bmatrix} $$
この処理をホワイトバランス補正と言います。
こちらもいろいろな手法がありますが、main関数側ではExif情報をそのまま使ってしまいます。
この際に、一点注意点として、Exifは整数値しか埋め込まれていないので、Gの補正値が1以外の数字が入ってしまってます。
通常、GはそのままでR,Bにゲインをかけにいくため、Gの値が1になるように正規化してあげます。
# white balance
print("--- White Balance ---")
white_balance_array = raw.camera_whitebalance / np.min(raw.camera_whitebalance)
print("white_balance : ", raw.camera_whitebalance, " -> ", white_balance_array)
img_wb = white_balance(img_demosic, white_balance_array)
save_img(out_filename + "_out_03_wb.jpg", img_wb)
Rに強めのゲインをかけているようです。
--- White Balance ---
white_balance : [2228, 1024, 1848, 1024] -> [2.17578125 1. 1.8046875 1. ]
では処理前後の画像を確認してみます。
ホワイトバランス補正をかけた後だと、画像が赤みがかるようになりました。
これはRに大きくゲインがかかり、飽和レベルを超えた状態で画像化したためです。
お試しで、補正後の画像に飽和レベルでクリップする処理を加えてみると、だいぶんそれっぽくなってきました。
img_wb_clip = img_wb.copy()
img_wb_clip[img_wb_clip > white_level_val] = white_level_val
save_img(out_filename + "_out_03_wb_clip.jpg", img_wb_clip)
カラーマトリックス補正
53~60行目のcolor_matrix関数は、カラーマトリックス補正処理を行います。
def color_matrix(in_img, color_matrix_array):
in_img[in_img < 0] = 0
h, w, c = in_img.shape
out_img = np.zeros((h, w, c))
out_img[:, :, 0] = color_matrix_array[0, 0] * in_img[:, :, 0] + color_matrix_array[0, 1] * in_img[:, :, 1] + color_matrix_array[0, 2] * in_img[:, :, 2] # R
out_img[:, :, 1] = color_matrix_array[1, 0] * in_img[:, :, 0] + color_matrix_array[1, 1] * in_img[:, :, 1] + color_matrix_array[1, 2] * in_img[:, :, 2] # G
out_img[:, :, 2] = color_matrix_array[2, 0] * in_img[:, :, 0] + color_matrix_array[2, 1] * in_img[:, :, 1] + color_matrix_array[2, 2] * in_img[:, :, 2] # B
return out_img
ホワイトバランスでは黒白を正しくしましたが、今度は色を正しくする処理をします。
イメージセンサの出力にはRGBが含まれていますが、このRGBが人間が感じる主観的なRGBと一致するとは限りません。
そこで、イメージセンサのRGBをいい感じにブレンドして、人間の主観のRGBに置き換える調整を行っているのがカラーマトリックス補正になります。
$$ \begin{bmatrix}R_o \\G_o \\ B_o \end{bmatrix}=\begin{bmatrix}Coeff._{RR} & Coeff._{RG} & Coeff._{RB} \\ Coeff._{GR} & Coeff._{GG} & Coeff._{GB} \\ Coeff._{BR} & Coeff._{BG} & Coeff._{BB} \end{bmatrix}\begin{bmatrix}R_i \\G_i \\ B_i \end{bmatrix} $$
演算の係数の良し悪しは人間の主観に依存します。
このため、合わせこみのパラメータという扱いとなり、どのように落とし込んでいくかは各社でノウハウがあります。
一般的な求め方は、RGBの色空間のままでは人間の主観値との乖離があるため、演算する色空間を操作するかから始まります。
このあたりの話は込み入ってくるのでここでは割愛しますが、触りを知りたい方は以下のサイトが参考になるかもです。
また、詳しく知りたい方は、こちらの書籍を参照してみてください。2001年出版と少し古いですが、良書です。
ここでは処理による画像の変化を簡単に確認するために、main関数側ではExif情報をそのまま使ってしまいます。
ただ、rawpyで読み取れるカラーマトリックス係数は空でした。
そこで、exiftoolを使って読み取ったカラーマトリックス係数を使ってます。
# Color matrix
print("--- Color Matrix ---")
print("color_matrix_rawpy : \n", raw.color_matrix)
exif_color_matri = np.array([1177, -211, 91, -54, 1267, -159, 72, -232, 1216]) / 1024
color_matrix_array = exif_color_matri.reshape((3, 3))
print("color_matrix_exif : \n", color_matrix_array)
img_cm = color_matrix(img_wb, color_matrix_array)
save_img(out_filename + "_out_04_cm.jpg", img_cm)
ちなみに、処理の入力画像はホワイトバランスをかけた直後の配列です。
これはカラーマトリックス補正の過程で、飽和を超えた信号量も使う可能性があるためです。
--- Color Matrix ---
color_matrix_rawpy :
[[0. 0. 0. 0.]
[0. 0. 0. 0.]
[0. 0. 0. 0.]]
color_matrix_exif :
[[ 1.14941406 -0.20605469 0.08886719]
[-0.05273438 1.23730469 -0.15527344]
[ 0.0703125 -0.2265625 1.1875 ]]
では処理前後の画像を確認してみます。
ホワイトバランスのときと同様に、カラーマトリックス補正でも画像が赤みがかってます。
原因はホワイトバランスのときと同じなので、お試しで補正後の画像に飽和レベルでクリップする処理を加えてみます。
被写体の色の数があまりないためか分かりづらいところありますが、より色がくっきり分かるようになった感じがします。
ガンマ補正
63~72行目のgamma関数は、ガンマ補正を行います。
def gamma(in_img, max_val):
# normalization
in_img = in_img / max_val
in_img[in_img < 0] = 0
in_img[in_img > 1] = 1
# gammma correction
alpha = 1.09929683
beta = 0.01805397
out_img = np.where(in_img < beta, 4.5 * in_img, alpha * (in_img ** 0.45) - (alpha - 1))
return out_img
ディスプレイの特性に合わせて、諧調表現カーブの補正を行う処理になります。
まずはマイナスの値や飽和を超えた信号をクリップ処理で落としていき、信号量を1で正規化します。
この後の処理として最も簡易なものは(1/2.2)乗をとるになりますが、今回はRec.2020という規格に則ってみました。
$$ E’ = \left\{
\begin{array}{l l}
4.5E & \quad \text{0 < E < }\beta\\
\alpha E^{0.45}-(\alpha -1) & \quad \beta\text{ < E < 1 }\
\end{array}
\right.$$
main関数側の処理は、このgamma関数にカラーマトリックス後の画像と飽和信号を入力するだけです。
# gamma
print("--- Gamma ---")
img_gamma = gamma(img_cm, white_level_val)
save_img(out_filename + "_out_05_gamma.jpg", img_gamma)
では処理前後の画像を確認してみます。
違和感の少ない写真になりました。
まとめ
かなり簡易的にですが、これで現像できました。
ただ、カメラ現像と比較すると、色味、ディテールともにまだまだな印象になります。
実際の現像処理はもっと賢い処理がたくさん入ってますが、ここで紹介した処理は基本にあたります。
現像処理をより深く学習するにあたって、導入になればと思います。
手元での処理再現にあたっては、こちら関連記事も参照してみてください。
コメント