Moiz's journal

プログラミングやFPGAなどの技術系の趣味に関するブログです

ゼロから作るRAW現像その1 - 基本的な処理

追記

このブログ記事「ゼロから作るRAW現像」を大きく再構成してより読みやすくした書籍「PythonとColabでできる-ゼロから作るRAW現像」を【技術書典6】にて頒布しました。

現在はBOOTHにて入手可能です。書籍+PDF版は2200円プラス送料、PDF版は1200円です。

moiz.booth.pm

はじめに

会社の同僚にrawpyというPython用のライブラリの存在を教えてもらいました。 これを使うと各種デジカメのRAWファイルから、BayerのRAWデータを抽出できます。以前はDCRAWのソースコードを改造してrawデータのダンプなどしていたのですが、ずいぶん良い時代になったものです。 せっかくなのでrawpyで抽出したBayerフォーマット画像データから、普通の画像ビューワーなどで表示できる画像ファイルをできるだけスクラッチから作成して見たいと思います。

今回の内容はJupyter Notebook ファイルとしてgithubにアップロードしてあります。RAWデータNotebook ファイルをダウンロードしてJupyter Notebookから開くことで同様の処理が行なえます

RAWファイルおよびRAWデータについて

RAWファイルやRAWデータというのは厳密な定義はないのですが、カメラ処理でRAWというとBayerフォーマットの画像データを指すことが多いようです。したがって多くの場合、RAWデータはBayerフォーマットの画像データ、RAWファイルはそのRAWデータを含んだファイルということになります。

まず前提として、今使われているカメラの画像センサーの殆どはBayer配列というものを使ってフルカラーを実現しています。

画像センサーは、碁盤の目状にならんだ小さな光センサーの集まりでできています。一つ一つの光センサーはそのままでは色の違いを認識できません。そこで色を認識するためには、3原色のうち一色を選択して光センサーにあてて、その光の強度を測定する必要があります。 方法としてはまず、分光器を使って光を赤、青、緑に分解して、3つの画像センサーにあてて、それぞれの色の画像を認識し、その後その3枚をあわせることでフルカラーの画像を合成するという方法がありました。これは3板方式などとよばれることがあります。この方法は手法的にもわかりやすく、また、余計な処理が含まれないためフルカラーの画像がきれい、といった特徴があり高級ビデオカメラなどで採用されていました。欠点としては分光器と3つの画像センサーを搭載するためにサイズが大きくなるという点があります。

これに対して、画像センサー上の一つ一つの光センサーの上に、一部の波長の光だけを通す色フィルターを載せ、各画素が異なる色を取り込むという方法もあります。この方法では1枚の画像センサーでフルカラー画像を取り込めるため、3板方式に対して単板方式とよばれることもあります。3版方式とくらべた利点としては分光器が不要で1枚のセンサーで済むのでサイズが小さい。逆に欠点としては、1画素につき1色の情報しか無いので、フルカラーの画像を再現するには画像処理が必要になる、という点があります。

単版方式の画像センサーの上に載る色フィルターの種類としては、3原色を通す原色フィルターと、3原色の補色(シアン・マゼンダ・イエロー)を通す補色フィルター1というものがあります。補色フィルターは光の透過率が高いためより明るい画像を得ることができます。それに対して原色フィルターは色の再現度にすぐれています。Bayer配列はこの単版原色フィルター方式のうち最もポピュラーなものです。

こういうわけでBayer配列の画像センサーの出力では1画素につき一色しか情報をもちません。Bayer配列のカラーフィルターはこの図左のように、2x2ブロックの中に、赤が1画素、青が1画素、緑が2画素ならぶようになっています。緑は対角線上にならびます。緑が2画素あるのは、可視光の中でも最も強い光の緑色を使うことで解像度を稼ぐため、という解釈がなされています。Bayerというのはこの配列の発明者の名前です。

f:id:uzusayuu:20180923132843p:plain

カメラ用センサーでは2000年代初頭までは、補色フィルターや3板方式もそれなりの割合で使われていたのですが、センサーの性能向上やカメラの小型化と高画質化の流れの中でほとんどがBayer方式にかわりました。今では、SigmaのFoveonのような意欲的な例外を除くと、DSLRやスマートフォンで使われているカラー画像センサーの殆どがBayer方式を採用しています。したがって、ほとんどのカメラの中ではBayerフォーマットの画像データをセンサーから受取り、フルカラーの画像に変換するという処理が行われている、ということになります。

こういったBayerフォーマットの画像ファイルは、すなわちセンサーの出力に近いところで出力されたことになり、カメラが処理したJPEGに比べて以下のような利点があります。

  • ビット数が多い(RGBは通常8ビット。Bayerは10ビットから12ビットが普通。さらに多いものもある)
  • 信号が線形(ガンマ補正などがされていない)
  • 余計な画像処理がされていない
  • 非可逆圧縮がかけられていない(情報のロスがない)

したがって、優秀なソフトウェアを使うことで、カメラが出力するJPEGよりもすぐれた画像を手に入れる事ができる可能性があります。

逆に欠点としては

  • データ量が多い(ビット数が多い。通常非可逆圧縮がされていない)
  • 手を加えないと画像が見れない
  • 画像フォーマットの情報があまりシェアされていない
  • 実際にはどんな処理がすでに行われているのか不透明

などがあります。最後の点に関して言うと、RAWデータといってもセンサー出力をそのままファイルに書き出すことはまずなく、欠陥画素除去など最低限の前処理が行われいるのが普通です。 しかし、実際にどんな前処理がおこなわれているのかは必ずしも発表されていません。

カメラ画像処理

Bayerからフルカラーの画像を作り出すカメラ画像処理のうち、メインになる部分の例はこんな感じになります2f:id:uzusayuu:20180923122237p:plain このうち、最低限必要な処理は、以下のものです。

  • ブラックレベル補正
  • ホワイトバランス補正(デジタルゲイン補正も含む)
  • デモザイク(Bayerからフルカラー画像への変換)
  • ガンマ補正

これらがないと、まともに見ることのできる画像を作ることができません。

さらに、最低限の画質を維持するには、通常は、

  • 線形性補正
  • 欠陥画素補正
  • 周辺減光補正
  • カラーマトリクス

が必要です。ただし、線形性補正や欠陥画素補正は、カメラがRAWデータを出力する前に処理されていることが多いようです。また、センサーの特性がよければ線形性補正やカラーマトリクスの影響は小さいかもしれません。

次に、より良い画質を実現するものとして、

  • ノイズ除去
  • エッジ強調・テクスチャ補正

があります。RGB->YUV変換はJPEGMPEGの画像を作るのには必要ですが、RGB画像を出力する分にはなくてもかまいません。

この他に、最近のカメラでは更に画質を向上させるために

  • レンズ収差補正
  • レンズ歪補正
  • 偽色補正
  • グローバル・トーン補正
  • ローカル・トーン補正
  • 高度なノイズ処理
  • 高度な色補正
  • ズーム
  • マルチフレーム処理

などの処理が行われるのが普通です。今回はベーシックな処理のみとりあげるので、こういった高度な処理は行いません。

結局、今回扱うのは次の部分のみです3

f:id:uzusayuu:20180923160440p:plain

準備

まずRAW画像を用意します。今回はSony α 7 IIIで撮影したこの画像を使います。 f:id:uzusayuu:20180923124230p:plain

使用するRAWファイルはこちらからダウンロードできます。 https://github.com/moizumi99/raw_process/blob/master/sample.ARW

次にPython3が実行できる環境を用意します。今回はUbuntu18.04上のPython3.6を使用しました。

さらにPythonのライブラリとしてrawpyが必要です。pipが導入してあれば、次のコマンドでインストールできます。

pip install rawpy

他に、以下ののライブラリも必要なので導入済みでなければインストールしておいてください。

  • matplotlib
  • PIL
  • numpy
  • imageio
  • math

また必ずしも必須ではありませんが、Jupyter Notebookが使えれば以下で解説する内容を実行するのが楽になると思います。

今回の実行例はすべてJupyter Notebook上でのもので、githubにアップロードしてあります。 ローカルにダウンロード後、Jupyter上からファイルを開けば同様の内容が実行できるはずです。

なお処理自体に関係ありませんが、いくつかのパラメータを取得するのにexiftoolsを使っています。

RAW画像読み込み

では、raw画像を読み込んでみましょう。まず、sample.ARWをダウンロードしたディレクトリで、Jupyterを起動します。 JupyterがなければPythonの対話ウィンドウで同様の事ができると思います。

%matplotlib inline
import rawpy
raw = rawpy.imread('sample.ARW')

これでrawpyを使って画像が読み込めます。簡単ですね。 ちゃんと読めたか確認しましょう。

from matplotlib.pyplot import imshow
img_preview = raw.postprocess(use_camera_wb=True)
imshow(img_preview)

f:id:uzusayuu:20180923140802p:plain

実はrawpyにはLibRaw(内部的にはdcraw)を利用したraw画像現像機能があるので、このようにして現像後の画像を見ることができます。 ただ、今回の目的はBayerから最終画像フォーマットまでの処理を一つ一つ追いかけていくことなので、これはあくまで参考とします。 最終的にこの画像と似たようなものが得られれば成功としましょう。

画像データ変換

扱いやすいように画像をnumpyのarrayに変換しておきましょう。まず、RAWデータのフォーマットを確認しておきます。

print(raw.sizes)

ImageSizes(raw_height=4024, raw_width=6048, height=4024, width=6024, top_margin=0, left_margin=0, iheight=4024, iwidth=6024, pixel_aspect=1.0, flip=0)

RAWデータのサイズは4024 x 6048のようです。numpy arrayにデータを移しましょう。

import numpy as np
h, w = raw.sizes.raw_height, raw.sizes.raw_width
raw_image = raw.raw_image.copy()
raw_array = np.array(raw_image).reshape((h, w)).astype('float')

このデータを無理やり画像として見ようとするとこうなります。

outimg = raw_array.copy()
outimg = outimg.reshape((h, w))
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg, cmap='gray')

f:id:uzusayuu:20180925140131p:plain

拡大するとこんな感じです

f:id:uzusayuu:20180925125658p:plain

明るいところが緑、暗いところが赤や青の画素のはずです。でも、これじゃなんのことやらさっぱりわかりませんね。 そこで、デモザイク処理でフルカラーの画像を作る必要があるわけです。

ブラックレベル補正

RAWデータの黒に対応する値は通常0より大きくなっています。画像を正常に表示するにはこれを0にもどす必要があります。これをやって置かないと黒が十分黒くない、カスミがかかったような眠い画像になってしまいますし、色もずれてしまいます。

まず、rawpyの機能でブラックレベルを確認しましょう。

blc = raw.black_level_per_channel
print(blc)

[512, 512, 512, 512]

どうやら全チャンネルでブラックレベルは512のようですが、他のRAWファイルでもこのようになっているとは限りません。各画素ごとのチャンネルに対応した値を引くようにしておきましょう。

さて、先程とりだしたrawデータの配列は以下の方法で確認できます。

bayer_pattern = raw.raw_pattern
print(bayer_pattern)

[[0 1]
 [1 2]]

どうやら0が赤、1が緑、2が青をしめしているようです。つまり、画像データの中から2x2のブロックを重複なくとりだすと、その中の左上が赤、右下が青、その他が緑、という事のようです。 このチャンネルに合わせて正しいブラックレベルを引きます。

print(raw_array.min(), raw_array.max())
blc_raw = raw_array.copy()
for y in range(0, h, 2):
    for x in range(0, w, 2):
        blc_raw[y + 0, x + 0] -= blc[bayer_pattern[0, 0]]
        blc_raw[y + 0, x + 1] -= blc[bayer_pattern[0, 1]]
        blc_raw[y + 1, x + 0] -= blc[bayer_pattern[1, 0]]
        blc_raw[y + 1, x + 1] -= blc[bayer_pattern[1, 1]]
print(blc_raw.min(), blc_raw.max())

0.0 8180.0
-512.0 7668.0

処理後の画像は、最大値と最小値が512小さくなっているのが確認できました。

outimg = blc_raw.copy()
outimg = outimg.reshape((h, w))
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg, cmap='gray')

f:id:uzusayuu:20180925140209p:plain

画像も少し暗くなっています。

簡易デモザイク

次にとうとうBayer配列からフルカラーの画像を作ります。これが終わるとやっと画像がまともに確認できるようになります。

この処理はデモザイクと呼ばれることが多いです。本来デモザイクはカメラ画像処理プロセス(ISP)の肝になる部分で、画質のうち解像感や、偽色などの不快なアーティファクトなどを大きく左右します。 したがって手を抜くべきところではないのですが、今回は簡易処理なので、考えうる限りでもっとも簡単な処理を採用します。

その簡単な処理というのは、ようするに3色の情報を持つ最小単位の2x2のブロックから、1画素のみをとりだす、というものです。

f:id:uzusayuu:20180923134843p:plain

結果として得られる画像サイズは1/4になりますが、もとが24Mもあるので、まだ6M残っています。今回の目的には十分でしょう。 なお、解像度低下をともなわないデモザイクアルゴリズムは次回以降とりあげようと思います。

では、簡易デモザイク処理してみましょう。2x2ピクセルの中に2画素ある緑は平均値をとります。

for y in range(0, h, 2):
    for x in range(0, w, 2):
        colors = [0, 0, 0, 0]
        colors[bayer_pattern[0, 0]] += blc_raw[y + 0, x + 0]
        colors[bayer_pattern[0, 1]] += blc_raw[y + 0, x + 1]
        colors[bayer_pattern[1, 0]] += blc_raw[y + 1, x + 0]
        colors[bayer_pattern[1, 1]] += blc_raw[y + 1, x + 1]
        dms_img[y // 2, x // 2, 0] = colors[0]
        dms_img[y // 2, x // 2, 1] = (colors[1] + colors[3])/ 2
        dms_img[y // 2, x // 2, 2] = colors[2]

さてこれでフルカラーの画像ができたはずです。見てみましょう。

outimg = dms_img.copy()
outimg = outimg.reshape((h // 2, w //2, 3))
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg)

f:id:uzusayuu:20180923154414p:plain

でました。画像が暗く、色も変ですが、それは予定通りです。そのあたりをこれから直していきます。

ホワイトバランス補正

次にホワイトバランス補正をかけます4。これは、センサーの色ごとの感度や、光のスペクトラムなどの影響を除去して、本来の白を白として再現するための処理です。 そのためには各色の画素に、別途計算したゲイン値をかけてあげます。今回はカメラが撮影時に計算したゲイン値をRAWファイルから抽出して使います。

ますはどんなホワイトバランス値かみてみましょう。RAWファイルの中に記録されたゲインを見てみましょう。

wb = np.array(raw.camera_whitebalance)
print(wb)

[2288. 1024. 1544. 1024.]

これは赤色にかけるゲインがx2288/1024、緑色がx1.0、青色がx1544/1024、という事のようです。処理してみましょう。

img_wb = dms_img.copy().flatten().reshape((-1, 3))
for index, pixel in enumerate(img_wb):
    pixel = pixel * wb[:3] /max(wb)
    img_wb[index] = pixel

f:id:uzusayuu:20180923154505p:plain

色がだいぶそれらしくなりました。

カラーマトリクス補正

次にカラーマトリクス補正を行います。カラーマトリクスというのは処理的には3x3の行列に、3色の値を成分としたベクトルをかけるという処理になります。

f:id:uzusayuu:20180925135315p:plain

なぜこんな事をするかというと、カメラのセンサーの色ごとの感度が人間の目とは完全には一致しないためです。例えば人間の目はある光の周波数の範囲を赤、青、緑、と感じるのですが、センサーが緑を検知する範囲は人間が緑と感じる領域とは微妙に異なっています。同じように青や赤の範囲も違います。これは、センサーが光をなるべく沢山取り込むため、だとか、製造上の制限、などの理由があるようです。 さらに、人間の目には、ある色を抑制するような領域まであります。これはセンサーで言えばマイナスの感度があるようなものですが、そんなセンサーは作れません。

こういったセンサー感度と人間の目の間隔とがなるべく小さくなるように、3色を混ぜて、より人間の感覚に近い色を作り出す必要があります。この処理を通常は行列を使って行い、これをカラーマトリクス処理と呼びます。

さて、ここでrawpyを使ってRAWデータの中に含まれる、マトリクスを調べるとこんなふうになってしまいます。

print(raw.color_matrix)

[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

仕方がないのでexiftoolsでマトリクスを元のARWファイルから取り出したところ、次のような値のようです。

Color Matrix                    : 1141 -205 88 -52 1229 -154 70 -225 1179

この値を使って処理を行いましょう。

color_matrix = np.array([[1141, -205, 88], [-52, 1229, -154], [70, -225, 1179]]) / 1024

img_ccm = np.zeros_like(img_wb)
for index, pixel in enumerate(img_wb):
    pixel = np.dot(color_matrix, pixel)
    img_ccm[index] = pixel

f:id:uzusayuu:20180923154644p:plain

あまり影響が感じられません。元のセンサーが良いので極端な補正はかけなくてよいのかもしれません。

ガンマ補正

最後にガンマ補正をかけます。 ガンマ補正というのは、もともとテレビがブラウン管だった頃にテレビの出力特性と信号の強度を調整するために使われていたものです。 今でも残っているのは、ガンマ補正による特性が結果的に人間の目の非線形的な感度と相性が良かったからのようです。 そんなわけで現在でもディスプレイの輝度は信号に対してブラウン管と似たような信号特性を持って作られており、画像にはガンマ補正をかけておかないと出力は暗い画像になってしまいます。

ガンマ特性自体は次の式で表されます

 y = x^{2.2}

f:id:uzusayuu:20180925135153p:plain

ガンマ補正はこれを打ち消す必要があるので、このようになります。

 y = x^{\frac{1}{2.2}}

f:id:uzusayuu:20180925135208p:plain

やってみましょう。

import math

img_gamma = img_ccm.copy().flatten()
img_gamma[img_gamma < 0] = 0
img_gamma = img_gamma/img_gamma.max()
for index, val in enumerate(img_gamma):
    img_gamma[index] = math.pow(val, 1/2.2)
img_gamma = img_gamma.reshape((h//2, w//2, 3))

f:id:uzusayuu:20180923154725p:plain

だいぶきれいになりました。

まとめ

今回は簡易的な処理でしたが、元のRAW画像のデータが高品質なのでこの程度の画質を実現することができました。

最後にセーブしておきます。

import imageio

outimg = img_gamma.copy().reshape((h // 2, w //2, 3))
outimg[outimg < 0] = 0
outimg = outimg * 255
imageio.imwrite("sample.png", outimg.astype('uint8'))

最後に

今回はrawpyを使ってカメラのRAWファイルからBayerデータを取り出し、Pythonでできるだけスクラッチから簡易的なカメラ画像処理を作成して、RAW現像を行いました。 今の所、周辺減光補正、ノイズ処理、エッジ強調がない、など主要な処理が抜けていますし、デモザイクは簡易的なものですので、次回以降こういった処理を追加していこうと思います。

今回使用したRAWファイル、Jupyter notebookでの実行例、出力したPNGファイルはすべてGitHubにアップロードしてあります。

github.com

次の記事

uzusayuu.hatenadiary.jp

改定履歴

2018-10-28: @karaage氏のご厚意により、記事中ブラックレベル補正の余分なコードとデモザイクのバグを修正しました。 2018-10-31: 再び@karaage氏の指摘に基づきデモザイクのコードをより汎用性の高いものに変更しました。


  1. 実際にはこの他に緑色の画素もあり、2x2の4画素のパターンになっているのが普通

  2. これはあくまで一例です。実際のカメラ内で行われる処理はメーカーや機種ごとに異なる可能性があります。

  3. 周辺減光補正は本来必要な処理ですが、今回は影響が少ない事やメタデータの解析が必要な事もあり、対象から省きました。

  4. ダイアグラムでの処理の順番に比べて、デモザイクとホワイトバランスの順番が逆になっていますが、今回採用した簡易デモザイクでは影響ありません。