前回はraw画像がJPEG画像とはかけ離れていて、いろいろな処理が必要ということがわかりました。
今回はrawpyで得られる情報から画像処理を行い、人が見た時に近い画像を出力してみます。
今回もMOIZさんの記事とオーム社から出版されている「デジカメの画像処理*1」の本を参考にしました。
uzusayuu.hatenadiary.jp
以下は画像処理のパイプラインをまとめました*2。
ベイヤー配列の確認
一つづつ処理を見ていく前にベイヤー配列の順番を確認しておきます。
各補正をかけるときに画素によって補正量が異なることがあるためです。
ベイヤー配列はrawpyのraw_patternで確認できます。
raw_pattern=raw.raw_pattern
print(raw_pattern)
以下のような結果が返ってきました。
[[0 1]
[3 2]]
0〜3の値が何を意味するかはrawpyのcolor_descで確認できます。
'RGBG'という答えが返ってきます。
0がR、1がG、2がB、3がGを指します。この指示に沿ってRGBを並べてみると以下のような配列になります。rawpyのdocumentにも書いてありますが、Gの信号が厳密には違う信号のカメラもある(Rの隣のGとBの隣のGの感度が厳密には異なる?)ので区別して書いているそうです。この記事の中ではRの隣のGのことをGr、Bの隣のGのことをGbと呼ぶことにします。
人の目は緑に敏感に反応する特性があるため、ベイヤーセンサーはGの画素を多く配列して解像を高めています。
各補正を見ていきます。
ブラックレベル補正
フォトダイオード*3の暗電流*4により固定パターンのノイズ(インパルスノイズ)が発生し、光が全く入ってこない場合に得られる信号が0以上になることがある。そうすると黒い被写体を撮影した時に、黒を黒として表現できないため、光が入ってこない時の信号が0になるように補正します。
ブラックレベルはrarpyのblack_level_per_channelで調べることができます。
各カメラメーカーで使っているセンサーは違うのでメーカーごとにこの値は違うと思います。
rawデータの読み込み後*5にブラックレベルを取得します。
black_level = raw.black_level_per_channel
print(black_level)
そうすると、[512,512,512,512]という値が出てきます。
ブラックレベルは各チャンネル(R,Gr,B,Gb)同じ補正で良さそうです。
ブラックレベルを引く前に、引く前の画像の最大・最小値を確認します(引き過ぎる可能性があるため)。
import numpy as np
print(np.min(raw_image),np.max(raw_image))
491, 9421という値が出てきました。最小値が491なので512引いてしまうと負の値になってしまうので、引きすぎないようにする必要があります。
各チャンネルにブラックレベル補正します。
img_bl = raw_array.copy()
h=raw.sizes.raw_height
w=raw.sizes.raw_width
for y in range(0, h, 2):
for x in range(0, w, 2):
img_bl[y + 0, x + 0] -= min(img_bl[y+0,x+0],black_level[bayer_pattern[0, 0]]) # R
img_bl[y + 0, x + 1] -= min(img_bl[y+0,x+1],black_level[bayer_pattern[0, 1]]) # Gr
img_bl[y + 1, x + 0] -= min(img_bl[y+1,x+0],black_level[bayer_pattern[1, 0]]) # Gb
img_bl[y + 1, x + 1] -= min(img_bl[y+1,x+1],black_level[bayer_pattern[1, 1]]) # B
ブラックレベルを引きすぎないようにブラックレベルが信号値より大きければクリップするようにしました。
念の為、ブラックレベルを引いた後の画像の最大・最小値を確認します。
print(np.min(raw_bl),np.max(raw_bl))
0,8909という値が出てきました。引きすぎも起きていないし、最大値もきちんと512引かれています。
補正前後の画像を並べて表示します。
import cv2
max_signal=np.max([np.max(raw_array),np.max(img_bl)])
show_img=cv2.hconcat([raw_image/max_signal,img_bl/max_signal])
plt.imshow(show_img)
補正前(左)は暗部が浮いていましたが、補正後(右)の方が暗部が締まって見えます。
デモザイク
色を識別するために輝度を識別できるセンサーの上に赤、緑、青のカラーフィルターを市松模様に並べるセンサーのことをベイヤーセンサーといいます。
各画素は赤、緑、青のいづれかの値しか取ることはできないので、存在しない色をデモザイク処理で補完する。
デモザイク処理は解像に影響するとても重要な処理で、さまざまな補完方法があります*6が、今回は一番簡単な処理で補完してみます。
隣接する画素の平均で補完しました(コード長いです)。
for y in range(h):
for x in range(w):
if y%2 ==0 and x%2 == 0:
# R
img_dms[y,x,0]=img_bl[y,x]
# G
if x+1<w:
img_dms[y,x,1]=(img_bl[y,x+1]+img_bl[y,x-1])/2
else:
img_dms[y,x,1]=img_bl[y,x-1]
# B
if y+1>h and x+1<w:
img_dms[y,x,2]=(img_bl[y-1,x-1]+img_bl[y+1,x-1]+img_bl[y+1,x+1]+img_bl[y-1,x+1])/4
elif y+1==h and x+1<w:
img_dms[y,x,2]=(img_bl[y-1,x-1]+img_bl[y-1,x+1])/2
elif y+1>h and x+1==w:
img_dms[y,x,2]=(img_bl[y-1,x-1]+img_bl[y+1,x-1])/2
else:
img_dms[y,x,2]=img_bl[y-1,x-1]
elif y%2 ==0 and x%2 != 0:
# R
if x+1<w:
img_dms[y,x,0]=(img_bl[y,x+1]+img_bl[y,x-1])/2
else:
img_dms[y,x,0]=img_bl[y,x-1]
# G
img_dms[y,x,1]=img_bl[y,x]
# B
if y+1<h:
img_dms[y,x,2]=(img_bl[y-1,x]+img_bl[y+1,x])/2
else:
img_dms[y,x,2]=img_bl[y-1,x-1]
elif y%2 != 0 and x%2 == 0:
# R
if y+1<h:
img_dms[y,x,0]=(img_bl[y-1,x]+img_bl[y+1,x])/2
else:
img_dms[y,x,0]=img_bl[y-1,x-1]
# G
img_dms[y,x,1]=img_bl[y,x]
# B
if x+1<w:
img_dms[y,x,2]=(img_bl[y,x+1]+img_bl[y,x-1])/2
else:
img_dms[y,x,2]=img_bl[y,x-1]
else:
# R
if y+1>h and x+1<w:
img_dms[y,x,0]=(img_bl[y-1,x-1]+img_bl[y+1,x-1]+img_bl[y+1,x+1]+img_bl[y-1,x+1])/4
elif y+1==h and x+1<w:
img_dms[y,x,0]=(img_bl[y-1,x-1]+img_bl[y-1,x+1])/2
elif y+1>h and x+1==w:
img_dms[y,x,0]=(img_bl[y-1,x-1]+img_bl[y+1,x-1])/2
else:
img_dms[y,x,0]=img_bl[y-1,x-1]
# G
if x+1<w:
img_dms[y,x,1]=(img_bl[y,x+1]+img_bl[y,x-1])/2
else:
img_dms[y,x,1]=img_bl[y,x-1]
# B
img_dms[y,x,2]=img_bl[y,x]
補完した画像を表示します。
画像が暗くて分かりづらいですが、格子状のアーティファクトは補正されました。
ノイズ除去
センサーには光の入力に応じて変化するランダムノイズがあり、低照度な環境で撮影するほどノイズが多く、画像がざらついた感じになります。ノイズ除去でランダムノイズを除去し滑らかな画像にします。numpyでノイズ処理に関する情報が得られないので今回はこの処理は行いません。
周辺減光補正
センサーに入力する光はレンズの性能によりセンサーの周辺ほど光が減少し、画像が暗くなります。画像の中心と周辺で明るさの差が発生しないよう補正を行います。
この処理もスキップします。
ホワイトバランス
太陽光、蛍光灯、LEDなどの照明は色を持っていますが、人の目では照らしている照明の色を感じることはほとんどありません。それはどのような色の光源の下でも白いものを白く認識できるよう補正が脳の中で行われているため。それと同じ補正をカメラでも行います。
ホワイトバランスはrawpyのcamera_whietbalanceで取得できます。
G信号で割ってゲインにします。
WhiteBalance=np.array(raw.camera_whitebalance)
WBGain=WhiteBalance[:3]/WhiteBalance[1]
それぞれをprintで出力すると以下のようになります。
WhiteBalance[1542 1024 1326 1024]、WBGain[1.50585938 1 1.29492188]
デモザイク後の画像の各チャンネルにWBGainを適用します。
img_wb = img_dms.copy()
for ch in range(3):
img_wb[:,:,ch] *= WBGain[ch]
WBGain前後の画像を並べてみます。
max_signal=np.max([np.max(img_dms),np.max(img_wb)])
show_img=cv2.hconcat([img_dms/max_signal,img_wb/max_signal])
plt.imshow(show_img)
空がより青っぽい色に変化しました。
人の目が色を認識する感度と、センサーが色を認識する感度には違いがあるため、センサーの出力をそのまま表示すると彩度の低い不自然な色合いに見えます。
カラーマトリックスで、カメラで撮影された画像が人の目で見た時と同じように見えるように補正を行います。
ColorMatrixはrawpyのraw_colormatrixで取得できます。
ColorMatrix=raw.color_matrix
ColorMatrixを出力すると3x4のゼロ行列が出てきました。
この行列をかけると画像が真っ黒になってしまうので、補正はかけずにスキップします。
ガンマ補正
センサーは入力した光に対して線形的に信号を出力するが、人の目は入力した光に対して、ガンマ特性(y=x^2.2 x:入力 y:出力)に似た変化をします。
センサーの信号が人の目の特性に合わせる補正を行うので、逆ガンマ補正(y=x^(1/2.2))を行います。
img_gamma = img_wb.copy()
img_gamma=img_gamma/np.max(img_gamma)
img_out=np.zeros([h,w,3])
for ch in range(3):
img_out[:,:,ch] = np.power(img_gamma[:,:,ch],1/2.2)
画像を表示してみます。
だいぶみやすい画像にはなったけれど、彩度やコントラストがJPEG画像と比べて低い感じがします(ColorMatrixをかけられなかったからかな?)。
エッジ強調
画像の輪郭を強調することで、解像感を調整します。
ここもスキップします。
YUV変換
色彩の変化よりも輝度変化に人の目が敏感という特性を利用して、YUVに変換しデータを圧縮してJPEG画像に変換します。
ここもスキップします。
rawpyで得られた情報を使って画像処理をしてみました。
パラメータが不明なものもあり、同時に記録していたJPEG画像相当にはできませんでしたが、RAW画像からJPEG画像を作るまでにいろいろな補正が行われて綺麗な画像になっていることがわかりました。