Moiz's journal

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

ゼロから作るRAW現像 その3 - デモザイク処理基本編

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

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

moiz.booth.pm

はじめに

これは「ゼロから作るRAW現像 その1 - 基本的な処理」および「ゼロから作るRAW現像 その2 - 処理のモジュール化」の続きです。 これらの記事の内容を前提としていますので、まだお読みでない方はそちらからお読みいただくことをおすすめします。

簡易デモザイク処理の問題点

「ゼロから作るRAW現像 その1 - 基本的な処理」では、デモザイク処理(Bayer配列の画像からフルカラーの画像を作り出す処理)として、簡易的な画像サイズが1/4になるものを使いました。単純な処理の割に意外なほどきれいな出力が得られるのですが、いかんせん画像が小さくなるのは問題です。また、出力画像が1/4になるので、とうぜん細かい部分は潰れてしまいます。

たとえば、同じシーンを、カメラの出力するJPEGと、前回の簡易RAW現像処理で処理した画像とで比べてみましょう。

f:id:uzusayuu:20180930075749p:plain

左がraw_process.pyによる出力、右がカメラの出力したJPEGです。同じ倍率で表示しています。サイズの違いが一目瞭然ですね。

拡大してみましょう。

f:id:uzusayuu:20180930081702p:plain

文字や図形の大きさがほぼ同じになるように、左側の画像は800%、右の画像は400%に拡大してあります。 明るさやコントラストの違いがまっさきに目につきますが、それは次回以降考えましょう。解像度に注目すると、意外なほど健闘はしているのですが、縦のラインの分解能が低かったり、印刷の目が再現されていなかったり、といった点がわかると思います。このあたりは簡易デモザイクによる画像サイズの低下の影響といえるでしょう。

現代のカメラ内部のデモザイクはかなり高度な処理をしているはずなので、右側のJPEG画像並みの解像度を得るのは難しいと思いますが、せめてもとの画像サイズを取り戻せるような処理を導入してみましょう。

ベイヤー配列再訪

デモザイクの細部に入る前にベイヤー配列がどんなものか再確認しておきましょう。ベイヤー配列では、各画素が、赤、青、緑、のうち一色だけをもっています。 rawpyのraw_imageでは、配列は次のようにして確認できます。

bayer_pattern = raw.raw_pattern
print(bayer_pattern)

[[0 1]
 [3 2]]

ここで、各番号と色の関係は以下のようになっています。カッコ内は略称です

番号
0 赤 (R)
1 緑 (Gr)
2 青 (B)
3 緑 (Gb)

ここで緑にGrとGbがあるのは、赤の行の緑と青の行の緑を区別するためです。カメラ画像処理では両者を区別することが多々あり、両者をGrとGbと表す事が多いです。 両者を区別する必要が無い場合はどちらもGであらわします。

この対応関係を考えると、この画像の各画素の色は、左上から

赤 緑

緑 青

のように並んでいることがわかります。これを図示するとこうなります。

f:id:uzusayuu:20180930105005p:plain

では、元のRAWデータを、この色のまま再現してみましょう。

まずは、RAW画像を読み出し、ブラックレベル補正を行っておきます。

import os, sys
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
    sys.path.append(module_path)
import raw_process

raw = raw_process.read("sample2.ARW")
raw_array = raw_process.get_raw_array(raw)
blc_raw = raw_process.black_level_correction(raw, raw_array)

次にもともと赤があった位置の画素を赤、緑の位置を緑、青の位置を青であらわします。

import numpy as np

h, w = blc_raw.shape
bayer_img = np.zeros((h, w, 3))
bayer_pattern[bayer_pattern == 3] = 1
gain = [2, 1, 2]
for y in range(0, h):
    for x in range(0, w):
        color = bayer_pattern[y % 2, x % 2]
        bayer_img[y, x, color] = gain[color] * blc_raw[y, x]

元のままだと、画素の数が多い分緑がかってしまうので、赤と青の強度を倍にしてあります。

表示してみましょう。

f:id:uzusayuu:20180930130104p:plain

なんだかちょっとだけそれらしい絵がでていますね。

拡大します。

f:id:uzusayuu:20180930130223p:plain

やはりだめです。拡大するとまるでテレビの画面を虫眼鏡で拡大したようなタイル模様が見えてきます。

デモザイクはこのタイル模様からフルカラーの画像を再現する画像処理になります。ある意味カメラ画像処理でもっとも肝になる部分です。

線形補完法

デモザイクアルゴリズムの中で、縮小する方法の次に簡単なのは、線形補間法です。 線形補間というとものものしいですが、ようするに、距離に応じて間の値をとるわけです。たとえば、緑の画素ならこうなります。

f:id:uzusayuu:20180930113526p:plain

赤の画素ではこうです。

f:id:uzusayuu:20180930152512p:plain

青の画素でも、赤の場合と同じような補完を行います。

では実際やってみましょう。

def mirror(x, min, max):
    if x < min:
        return min - x
    elif x >= max:
        return 2 * max - x - 2
    else:
        return x

dms_img = np.zeros((h, w, 3))
bayer_pattern = raw.raw_pattern
for y in range(0, h):
    for x in range(0, w):
        color = bayer_pattern[y % 2, x % 2]
        y0 = mirror(y-1, 0, h)
        y1 = mirror(y+1, 0, h)
        x0 = mirror(x-1, 0, w)
        x1 = mirror(x+1, 0, w)
        if color == 0:
            dms_img[y, x, 0] = blc_raw[y, x]
            dms_img[y, x, 1] = (blc_raw[y0, x] + blc_raw[y, x0] + blc_raw[y, x1] + blc_raw[y1, x])/4
            dms_img[y, x, 2] = (blc_raw[y0, x0] + blc_raw[y0, x1] + blc_raw[y1, x0] + blc_raw[y1, x1])/4
        elif color == 1:
            dms_img[y, x, 0] = (blc_raw[y, x0] + blc_raw[y, x1]) / 2
            dms_img[y, x, 1] = blc_raw[y, x]
            dms_img[y, x, 2] = (blc_raw[y0, x] + blc_raw[y1, x]) / 2
        elif color == 2:
            dms_img[y, x, 0] = (blc_raw[y0, x0] + blc_raw[y0, x1] + blc_raw[y1, x0] + blc_raw[y1, x1])/4
            dms_img[y, x, 1] = (blc_raw[y0, x] + blc_raw[y, x0] + blc_raw[y, x1] + blc_raw[y1, x])/4
            dms_img[y, x, 2] = blc_raw[y, x]
        else:
            dms_img[y, x, 0] = (blc_raw[y0, x] + blc_raw[y1, x]) / 2
            dms_img[y, x, 1] = blc_raw[y, x]
            dms_img[y, x, 2] = (blc_raw[y, x0] + blc_raw[y, x1]) / 2

ここでmirror()は画像のヘリでxやyがはみ出ないように処理しています。

明るさを調整して表示してみます。

f:id:uzusayuu:20180930144416p:plain

それらしいものがでました。

つづけて残りのホワイトバランス、カラーマトリクス、ガンマ補正をかけます。

img_wb = raw_process.white_balance(raw, dms_img)
color_matrix = [1141, -205, 88, -52, 1229, -154, 70, -225, 1179]
img_ccm = raw_process.color_correction_matrix(img_wb, color_matrix)
rgb_image = raw_process.gamma_correction(img_ccm)

結果はこうなります。

f:id:uzusayuu:20180930131235p:plain

明るさコントラストに不満はありますが、フルカラーの画像ができました。

セーブしてみます。

raw_process.write(rgb_image, "output3.png")

ではセーブした画像をカメラのJPEGと比べてみましょう。

f:id:uzusayuu:20180930133057p:plain

解像度が低いのはともかく、細部にJPEGにはない余計な色が出ているのが気になります。このような余計な色は偽色などと呼ばれることがあります。英語ではColor Artifactなどといわれます。

他には、印刷の模様などのティテールも消えています。シャープさが足りないのはエッジ強調で多少もどせるかもしれませんが、ディテールの細かい部分を戻すのはむずかしいでしょう。なお、このような部分をテクスチャといいます。

やはりこのあたりは単純な線形補間の限界のようです。いくら処理の内容を見ていくのが目的で、最高の画質をもとめてはいないとはいえども、もう少し改善してから次のステップに行きたいところです。

FIRフィルターを利用した高速化

ちょっと最後にひとつだけ。

上記のデモザイクを実行してみるとわかると思うのですが、結構時間がかかります。一つ一つの画素の処理は単純でも、24M画素もあると流石に重くなります。 Pythonでできる範囲で高速化を考えてみたいと思います。

まず、緑画素の補完式をよく見てみると、これはこんな2次元FIRフィルターをかけているのと等価だという事がわかります。

[[   0, 1/4,   0],
 [ 1/4,   1, 1/4],
 [   0, 1/4,   0]]

つまり、2次元FIRフィルターをかけるライブラリーが使えるということです。具体的にはscipyを使って次のようにします。

from scipy import signal

blc_green = blc_raw.copy()
blc_green[(raw.raw_colors == 0) | (raw.raw_colors == 2)] = 0
g_flt = np.array([[0, 1/4, 0], [1/4, 1, 1/4], [0, 1/4, 0]])
green = signal.convolve2d(blc_green, g_flt, boundary='symm', mode='same')

ここでraw_colorsは各画素の色を表しており、blc_greenはBayerから緑画素のみを取り出したものです。

g_fltは上で示したFIRフィルターを表し、これがconvolve2dにより重畳されています。

結果はどうでしょう?明るさを調整してみてみましょう。

f:id:uzusayuu:20180930135927p:plain

なかなか良い感じです。

同様に赤と青チャンネルでは、対応するFIRフィルターはこのようになります。

[[1/4, 1/2, 1/4],
 [1/2,   1, 1/4].
 [1/4, 1/2, 1/4]]

まず赤画素を処理してみます

blc_red = blc_raw.copy()
blc_red[raw.raw_colors != 0] = 0
rb_flt = np.array([[1/4, 1/2, 1/4], [1/2, 1, 1/2], [1/4, 1/2, 1/4]])
red = signal.convolve2d(blc_red, rb_flt, boundary='symm', mode='same')

また明るさを調整して結果をみてみましょう。

f:id:uzusayuu:20180930140326p:plain

良さそうです。青画素も同様の処理ができるはずです。

では、三色分の処理を行ってみましょう。

dms_img2 = np.zeros((h, w, 3))

blc_green = blc_raw.copy()
blc_green[(raw.raw_colors == 0) | (raw.raw_colors == 2)] = 0
g_flt = np.array([[0, 1/4, 0], [1/4, 1, 1/4], [0, 1/4, 0]])
dms_img2[:, :, 1] = signal.convolve2d(blc_green, g_flt, boundary='symm', mode='same')

blc_red = blc_raw.copy()
blc_red[raw.raw_colors != 0] = 0
rb_flt = np.array([[1/4, 1/2, 1/4], [1/2, 1, 1/2], [1/4, 1/2, 1/4]])
dms_img2[:, :, 0] =  signal.convolve2d(blc_red, rb_flt, boundary='symm', mode='same')

blc_blue = blc_raw.copy()
blc_blue[raw.raw_colors != 2] = 0
dms_img2[:, :, 2] =  signal.convolve2d(blc_blue, rb_flt, boundary='symm', mode='same')

これで先程の1画素づつ処理するコードに比べてかなり速くなりました。

出力画像はこうなります。

f:id:uzusayuu:20180930141910p:plain

さらに、ホワイトバランス、カラーマトリクス、ガンマ補正をかけると、先ほどと同様の結果が得られます。

img_wb = raw_process.white_balance(raw, dms_img2)
color_matrix = [1141, -205, 88, -52, 1229, -154, 70, -225, 1179]
img_ccm = raw_process.color_correction_matrix(img_wb, color_matrix)
rgb_image = raw_process.gamma_correction(img_ccm)

f:id:uzusayuu:20180930142520p:plain

この処理は前回のraw_process.pyに追加して、あらたにraw_process2.pyというファイルとしてgithubにアップロードしてあります。

使用法はraw_process.pyと同様、

python3 raw_process2.py INPUTFILE [OUTPUTFILE] [MATRIX]

です。

まとめ

今回は、簡易的な縮小デモザイクを、比較的単純な線形補間アルゴリズムをつかったデモザイクで置き換えました。 jupyter上の例はSimple_Demosaic.ipynbとして、前述のraw_process2.pyと共にgithubにアップロードしてあります。

github.com

次回はもう少し複雑なデモザイクを紹介しようと思います。

次の記事

uzusayuu.hatenadiary.jp