Moiz's journal

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

ゼロから作るRAW現像 その5 - ラズベリーパイのRAW画像処理

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

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

moiz.booth.pm

はじめに

これは「ゼロから作るRAW現像 」という一連の記事の一つです。 これらの記事の内容を前提としていますので、まだお読みでない方はこちらの記事からお読みいただくことをおすすめします。

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

「ゼロから作るRAW現像 その2 - 処理のモジュール化」

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

「ゼロから作るRAW現像 その4 - デモザイク処理応用編」

ラズベリーパイのRAW画像

これまでRAW現像の対象として、ソニーα7iiiで撮影した画像を使ってきました。 これは基本的な処理の説明をするにあたって、ノイズが少ない、歪がない、解像感が高い、など質の良いRAW画像のほうがやりやすかったからです。 しかし、実際のカメラ画像処理では、ノイズや意図しない不鮮明さなど好ましくない性質を修正するというのが大きな目的になります。そういった処理を行うにあたって、ミラーレスカメラの高画質データは逆に扱いにくいものになってしまいます。なにしろ直したいノイズもボケもあまりありませんから。

今回はラズベリーパイでRAW画像をキャプチャーして、これまで行ったRAW現像処理を行い、次回以降のテーマの準備を行います。

ラズベリーパイの準備とRAW画像のキャプチャー

今回紹介する内容について前提とする環境は以下のとおりです。

他のラズベリーパイやv1.3カメラでも実行は可能だとは思われますが、試してはいません。

  • 画像処理PCサイド
    • Python3が動作し前回までの内容が実行できるLinux環境
    • SDカードリーダーやラズベリーパイとのネットワーク接続など、ラズベリーパイとデータを交換する手段

以下の内容はラズベリーイカメラv2.1がラズベリーパイに接続され正常に動作していることを前提としていますので、まず公式ドキュメント などに従ってカメラの動作を確認下さい1

特に、GUIの左上のラズベリーパイアイコンから選択できる「設定」、「RaspberryPiの設定」、から「インターフェイス」タブで「カメラ」を有効にしておく必要がありますので、ご注意ください。

RAW画像のキャプチャー

ラズベリーパイにカメラをセッティングした状態で、以下のコマンドを実行します。

> raspistill -r -o raw_capture.jpg

-rがRAWキャプチャーを行うことをしめしています。

うまくいけば、カレントディレクトリにraw_capture.jpgという名前のJPEGファイルができているはずです。 このファイルは一見通常のJPEGファイルに見えますが、8MPのJPG画像ファイルにしては15MB前後と巨大なファイルサイズです。これは、RAWデータが埋め込みデータとしてファイル中に組み込まれているからです。

キャプチャーが成功したら、このファイルをUSBメモリーで移す、ネットワークコピー、またはRaspbianの入ったSDCARDをホストPCに読み込ませる、などの方法で、前回までの作業を行ったPCに移動します2

うまくキャプチャーできたか、JPEGファイルのJPEG画像部分を見てみましょう。通常の画像ビューワーや画像エディタで確認できます。

f:id:uzusayuu:20181014122747p:plain

あまり良い画像ではないですが、キャプチャーできていることは確認できました。

ラズベリーパイのRAW画像の抽出

以下の内容はgithubからJupyter Notebookファイルとしてダウンロードできます

今回の画像はこれまでと違い、JPEGファイルの中に埋め込まれているので、rawpy以外の方法で取り出す必要があります。 なお、この部分は今回の本筋ではないので、簡単な説明ですまします。詳しい内容は picameraのドキュメント を参照ください。

これ以降は、Jupyterなどpython3のインタラクティブ環境で作業します。

BayerデータはJPEGファイルの最後の部分に位置するので、読み出してから末尾だけ取り出します。

with open("raw_capture.jpg", "rb") as input_file:
    data = input_file.read()
data = data[-10237440:]

(未確認ですがカメラv1.3ではdata = data[-6371328:]とするとよいようです。)

これでBayer部分が読み込めたはずです。データを見てみましょう。

import numpy as np

w = 3282  # for v1.3 w = 2592
h = 2480  # for v1.3 h = 1944
img = np.zeros((h, w))
stride = math.ceil(w * 10 / 8 / 32) * 32
for y in range(h):
    for x in range(w // 4):
        word = data[y * stride + x * 5: y * stride + x * 5 + 5]
        img[y, 4 * x    ] = (word[0] << 2) | ((word[4] >> 6) & 3) 
        img[y, 4 * x + 1] = (word[1] << 2) | ((word[4] >> 4) & 3) 
        img[y, 4 * x + 2] = (word[2] << 2) | ((word[4] >> 2) & 3) 
        img[y, 4 * x + 3] = (word[3] << 2) | ((word[4]     ) & 3)

最後の部分ですが、picameraのドキュメントによると、ラズベリーパイのBayerデータは4画素毎に5バイトのデータにまとまっていて、最初の4バイトがそれぞれの画像の上位8ビット、最後のバイトの2ビットずつが、各画素の下位2ビットになっているとのことです。

さて、ちゃんとデータが読めたのか見てみましょう。

from matplotlib.pyplot import imshow 

outimg = img.copy()
outimg[outimg < 0] = 0
outimg = outimg / outimg.max()
imshow(outimg, cmap='gray')

f:id:uzusayuu:20181014124822p:plain

どうやらそれらしい画像が取り出せたようです。

ラズベリーイカメラのRAW画像の現像

だいぶ手間がかかりましたが、ここから本題のラズベリーパイのRAW画像の現像に入ります。

まず前回までのスクリプトを使いたいところですが、これまで使った関数は殆どがrawpyのインスタンスに依存していました。 今回扱うラズベリーパイのRAW画像はrawpyで読み込んだものではないのでそのままでは使えません。

そこで各関数を、rawデータと指定した任意のパラメータで実行できるように書き換えて、raw_process4.pyとしました。 それぞれの関数の変更点は以下の説明で触れます。

まずはモジュールを読み込みましょう。

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

ブラックレベル補正

ブラックレベル補正は、引数としてブラックレベルの数値を与えるようにしました。 引数で与えるブラックレベルは、左上、右上、左下、右下、の順にブラックレベルを格納したリストです。

def black_level_correction(raw_array, black_level):
    blc_raw = raw_array.copy()
    blc_raw[0::2, 0::2] -= black_level[0]
    blc_raw[0::2, 1::2] -= black_level[1]
    blc_raw[1::2, 0::2] -= black_level[2]
    blc_raw[1::2, 1::2] -= black_level[3]
    return blc_raw

ブラックレベルを64と仮定して処理してみましょう。

blacklevel = [64] * 4
blc_raw = raw_process.black_level_correction(img, blacklevel)

f:id:uzusayuu:20181014152120p:plain

ホワイトバランス補正

def white_balance_Bayer(raw_array, wbg, bayer_pattern):
    img_wb = raw_array.copy()
    img_wb[0::2, 0::2] *= wbg[bayer_pattern[0, 0]] 
    img_wb[0::2, 1::2] *= wbg[bayer_pattern[0, 1]]
    img_wb[1::2, 0::2] *= wbg[bayer_pattern[1, 0]]
    img_wb[1::2, 1::2] *= wbg[bayer_pattern[1, 1]]
    return img_wb

ホワイトバランス補正には、R・G・Bのゲインを並べたリストのwbgと、ベイヤーのパターンを渡すようになりました。

今回ホワイトバランスははっきりしないので仮に赤のゲインx1.5、青のゲインx2.2を与えます。 今回のラズベリーパイの画像では左上が青色画素ですので、bayer_patternは[[2, 1], [1, 0]]になります。

wbg = np.array([1.5, 1, 2.2, 1])
bayer_pattern = np.array([[2, 1], [1, 0]])
wb_raw = raw_process.white_balance_Bayer(blc_raw, wbg, bayer_pattern)

f:id:uzusayuu:20181014145358p:plain

デモザイク

前回のデモザイクは画像の左上隅が赤色画素であることを仮定していました。 今回のラズベリーパイの画像では左上が青色画素ですのでこのままでは実行できません。 ベイヤーパターンごとの違いは各チャンネルの位相を180度変える事になります。

def advanced_demosaic(dms_input, bayer_pattern):
    hlpf = np.array([[1, 2, 3, 4, 3, 2, 1]]) / 16
    vlpf = np.transpose(hlpf)
    hhpf = np.array([[-1, 2, -3, 4, -3, 2, -1]]) / 16
    vhpf = np.transpose(hhpf)
    identity_filter = np.zeros((7, 7))
    identity_filter[3, 3] = 1

    # generate FIR filters to extract necessary components
    FC1 = np.matmul(vhpf, hhpf)
    FC2H = np.matmul(vlpf, hhpf)
    FC2V = np.matmul(vhpf, hlpf)
    FL = identity_filter - FC1 - FC2V - FC2H

    # f_C1 at 4 corners
    c1_mod = signal.convolve2d(dms_input, FC1, boundary='symm', mode='same')
    # f_C1^1 at wy = 0, wx = +Pi/-Pi
    c2h_mod = signal.convolve2d(dms_input, FC2H, boundary='symm', mode='same')
    # f_C1^1 at wy = +Pi/-Pi, wx = 0
    c2v_mod = signal.convolve2d(dms_input, FC2V, boundary='symm', mode='same')
    # f_L at center
    f_L = signal.convolve2d(dms_input, FL, boundary='symm', mode='same')

    # Move c1 to the center by shifting by Pi in both x and y direction
    # f_c1 = c1 * (-1)^x * (-1)^y
    f_c1 = c1_mod.copy()
    f_c1[:, 1::2] *= -1
    f_c1[1::2, :] *= -1
    if bayer_pattern[0, 0] == 1 or bayer_pattern[0, 0] == 3:
        f_c1 *= -1
    # Move c2a to the center by shifting by Pi in x direction, same for c2b in y direction
    c2h = c2h_mod.copy()
    c2h[:, 1::2] *= -1
    if bayer_pattern[0, 0] == 2 or bayer_pattern[1, 0] == 2:
        c2h *= -1
    c2v = c2v_mod.copy()
    c2v[1::2, :] *= -1
    if bayer_pattern[0, 0] == 2 or bayer_pattern[0, 1] == 2:
        c2v *= -1
    # f_c2 = (c2v_mod * x_mod + c2h_mod * y_mod) / 2
    f_c2 = (c2v + c2h) / 2

    # generate RGB channel using 
    # [R, G, B] = [[1, 1, 2], [1, -1, 0], [1, 1, - 2]] x [L, C1, C2]
    height, width = dms_input.shape
    dms_img = np.zeros((height, width, 3))
    dms_img[:, :, 0] = f_L + f_c1 + 2 * f_c2
    dms_img[:, :, 1] = f_L - f_c1
    dms_img[:, :, 2] = f_L + f_c1 - 2 * f_c2

    return dms_img

実行してみます。

dms_img = raw_process.advanced_demosaic(wb_raw, bayer_pattern)

f:id:uzusayuu:20181014145414p:plain

カラーマトリクス補正

カラーマトリクスの値もはっきりしません。今回は色が多少強調されるように、以下のようなマトリクスをかけ合わせてみます。

[[1536, -256, -256], 
 [-256, 1536, -256],
 [-256, -256, 1536]]

実行します。

img_ccm = raw_process.color_correction_matrix(dms_img, [1536, -256, -256, -256, 1536, -256, -256, -256, 1536])

f:id:uzusayuu:20181014145426p:plain

ガンマ補正

最後にガンマ補正です。ガンマ値を引数でもたせるようにしました。

def gamma_correction(rgb_array, gamma):
    img_gamma = rgb_array.copy()
    img_gamma[img_gamma < 0] = 0
    img_gamma = img_gamma / img_gamma.max()
    img_gamma = np.power(img_gamma, 1/gamma)
    return img_gamm

実行します。

img_gamma = raw_process.gamma_correction(img_ccm, 2.2)

画像を保存して確認してみましょう。

raw_process.write(img_gamma, "raspi_raw_out.png")

import imageio
from pylab import imshow, show
imshow(imageio.imread('raspi_raw_out.png'))
show()

f:id:uzusayuu:20181014140022p:plain

お世辞にもきれいな画像とは言えませんがどうにかラズベリーパイでキャプチャーしたRAWデータを現像して画像ファイルにすることができました。

まとめ

Raspberry Pi 3BでキャプチャーしたRAW画像を現像して画像ファイルに変換しました。 今回はどうにか現像するところで終わりでしたが、次回以降画質に手をいれて行きたいと思います。

今回の内容はRAW画像データ及びraw_process4.pyと共にgithubにアップロードしてあります。

github.com

次の記事

uzusayuu.hatenadiary.jp


  1. 公式ドキュメント以外では以下の記事が参考になります。Raspberry PiカメラモジュールV2でデジカメを作ってみた

  2. 前回までの環境をRaspberry Pi上で構築できていれば同じことができるはずですが、私の環境ではうまくいきませんでしたので、別のPCにデータを運んでから以下の作業を行っています。