簡易RAW現像ツールのダウンロードと使い方
【ダウンロード】
作成したツールは以下からダウンロードできます。
[basic-raw-developerをダウンロード(ZIP:約189MB)]
basic-raw-developer.zip - Google ドライブ
ZIPにはソースコードと動作確認用のRAW画像2枚が含まれています。そのため、展開後すぐに処理を試せます。
basic-raw-developer
カメラ由来の現像用メタデータを持たない、ベイヤーRAW画像を現像する簡易Pythonツールです。
1画素を2バイトの符号なし整数として保存したRAWファイルを読み込み、以下の処理を行います。
・ブラックレベル補正
・ホワイトバランス補正
・デモザイク
・正規化
・ガンマ補正
ARWやDNGなどのカメラRAW形式ではなく、イメージセンサーなどから出力されたシンプルな画素データを処理することを目的としています。
【動作確認環境】
以下の環境で動作を確認しています。
・OS:Windows 11
・Python:3.14.6
・NumPy:2.5.1
・Matplotlib:3.11.1
・Pillow:12.3.0
ほかのWindows環境やmacOSでは未検証です。Pythonと必要なライブラリをインストールすれば動作する可能性がありますが、すべての環境での動作は保証していません。
【フォルダ構成】
basic-raw-developerフォルダには、以下のファイルとフォルダが含まれています。
・input
入力RAWファイルを保存するフォルダです。
・output
処理結果のPNG画像を保存するフォルダです。
・tools
RAW現像の各処理を実装したPythonモジュールが入っています。
・main.py
RAW現像パイプラインを実行するメインプログラムです。
・requirements.txt
必要なPythonライブラリが書かれています。
・README.md
ツールの説明が書かれています。
【インストール】
ターミナルでbasic-raw-developerフォルダへ移動し、必要なライブラリをインストールします。
Windowsの場合:
python -m pip install -r requirements.txt
macOSの場合:
python3 -m pip install -r requirements.txt
【実行方法】
Windowsの場合:
python main.py
macOSの場合:
python3 main.py
ホワイトバランス調整用の画像が表示されたら、無彩色領域をマウスで選択し、Enterキーを押してください。
処理結果はoutputフォルダへ保存されます。
【入力RAWファイルの構造】
入力RAWファイルの画素データには、次の条件があります。
・1画素につき2バイト
・符号なし16bit整数として保存
・バイト順はリトルエンディアン
・非圧縮
・1画素につき1つのベイヤー画素値
ファイルの拡張子は問いません。今回のサンプルでは.binを使用していますが、内部構造が上記の条件を満たしていれば、.rawや.datなどの拡張子でも読み込めます。
10bit、12bit、14bitなどの画素値を、1画素につき2バイトのuint16として保存している場合に使用できます。例えば12bitの画素値であれば、値の範囲は通常0~4095ですが、ファイル内では各画素に2バイトを使用します。
10bit画素値 → 2バイトで保存
12bit画素値 → 2バイトで保存
14bit画素値 → 2バイトで保存
複数の画素を隙間なく詰め込む、10bit packed形式や12bit packed形式には対応していません。
【入力ファイルの変更】
入力ファイルを変更する場合は、main.pyのCALIBRATION_FILEとRAW_FILEを変更します。
CALIBRATION_FILE:
PROJECT_DIR / "input" / "ホワイトバランス用RAWファイル.bin"
RAW_FILE:
PROJECT_DIR / "input" / "現像対象RAWファイル.bin"
CALIBRATION_FILEは、ホワイトバランスのゲイン計算に使用する無彩色画像です。
RAW_FILEは、実際に現像する画像です。
拡張子を変更する場合は、ここで実際のファイル名を指定してください。
【使用上の注意】
このツールでは、入力RAWに関する一部の条件をサンプル画像の仕様に合わせコード内で指定しています。
・画像幅と画像高さの値の格納位置 → 先頭2バイト:画像幅、次の2バイト:画像高さ
・画素データの格納位置 → 5バイト目以降
・ベイヤーパターン → 左上画素がR画素のRGGBパターンとして指定
・ブラックレベル → HOPBエリアを検波し、1022DN@14bitを指定
・ホワイトバランスの求め方 → 無彩色エリアでは画素値はG,B,Rの順になると推定
・正規化に使用するパーセンタイル値 → 99%を指定
・ガンマ値 → 2.2を指定
異なるイメージセンサーやRAW形式を使用する場合は、これらの設定を入力データに合わせて変更してください。特に、画素値のビット深度が変わると、適切なブラックレベルや画素値の範囲も変わる可能性があります。
【対応していないRAW形式】
このツールは、以下のようなカメラRAWファイルを直接読み込むものではありません。
・ARW
・DNG
・NEF
・CR2
・CR3
これらの形式には、メタデータ、プレビュー画像、圧縮された画素データなどが含まれているため、専用の読み込み処理が必要です。
ソースコード解説
RAW現像フローに沿って、ソースコードを解説します。
①RAW画像読み込み
個人的にこの画像読み込み部分が最初にして最大の山場でした...
メタデータがある場合はrawpyで画素数情報を読み取り簡単に2次元Numpy配列を作れるのですが、メタデータが無い場合は、自力で画素数情報を読みとり、配列を準備しなければいけません。
raw_reader.pyのload_raw_image関数を使ってRAW画像を読み込みます。
以下がraw_reader.pyのソースコードです。
from pathlib import Path
import struct
import numpy as np
def load_raw_image (file_path: Path) -> np.ndarray:
"""バイナリファイルから16ビットRAW画像を読み込む。"""
if not file_path.exists():
raise FileNotFoundError (
f"RAW file not found: {file_path}"
)
with file_path.open("rb" ) as file:
width = struct.unpack("<H" , file.read(2 ))[0 ]
height = struct.unpack("<H" , file.read(2 ))[0 ]
raw = np.fromfile(
file_path,
dtype="<u2" ,
offset=4 ,
count=width * height,
)
expected_pixels = width * height
if raw.size != expected_pixels:
raise ValueError (
"Pixel count does not match. "
f"Expected={expected_pixels}, loaded={raw.size}"
)
return raw.reshape(height, width)
今回使用したRAWファイルは、ヘッダー部に画素数情報が埋め込まれており、その後に画素データが入っています。
つまり、
先頭2バイト:画像幅
次の2バイト:画像高さ
5バイト目以降:1画素につき2バイトの画素データ
という構成。
※1画素の有効bit数は14bitです
画像幅と高さは、struct.unpack関数を使って読み込みます。「<H」の「<」はリトルエンディアン、「H」は2バイトの符号なし整数を表します。
画素データは、NumPyのfromfile関数を使って読み込みます。これでまず一次元配列ができます。
dtypeには「<u2」を指定しています。これは、リトルエンディアンで保存された2バイトの符号なし整数を意味します。
offset=4を指定することで、画像幅と高さが格納されている先頭4バイトを飛ばし、5バイト目から画素データを読み込みます。
読み込んだ画素データは1次元配列なので、reshape関数を使って高さ×幅の2次元画像へ変換します。
load_raw_image関数が返す画像のデータ型はuint16です。
main.pyを実行すると、読み込んだ画像の幅は9609画素, 高さ6444画素であることがターミナルに表示されます。
RAW画像読み込みの後、正規化、PNG保存(8bit)がmain.py上で実行されます。(これらの処理の詳細は後述)
raw.pngが保存されます
②ブラックレベル補正
今回使用したRAW画像には左端にオプティカルブラック(OPB)が含まれているため、ざっくり先頭100列の画素値の中央値を確認します。
この処理には、print_col_median.pyのprint_col_median関数を使用します。
以下がprint_col_median.pyのソースコードです。
import numpy as np
def print_col_median (image: np.ndarray) -> None :
"""先頭100列の画素値の中央値を表示する。"""
col_median = np.median(image[:, :100 ], axis=0 )
print (" \n Median Pixel Values of the First 100 Columns" )
print ("Column: Median" )
for column, value in enumerate (col_median):
print (f"{column:4d}: {value:.1f}" )
np.median関数のaxis=0は、行方向に画素値を集計し、各列の中央値を求める指定です。
※ノイズや欠陥画素による外れ値の影響を除くため、平均値ではなく中央値を使用しました。
ターミナルに表示された中央値からブラックレベルを確認すると、1022DN@14bitのようです。こちらをブラックレベル補正に使用します。
実際の補正処理は、black_level.pyのapply_black_level_correction関数で行います。
補正では、すべての画素から一定のブラックレベルを減算します。(理想的には色毎にブラックレベルを検波・減算すべきですが今回は簡易ツールなので...)
以下がblack_level.pyのソースコードです。
import numpy as np
def apply_black_level_correction (
image: np.ndarray,
black_level: int = 1022 ,
) -> np.ndarray:
"""
シンプルなブラックレベル補正を適用する。
すべての画素から一定のブラックレベルを引き、
負の値は0にクリップする。
引数
----------
image : np.ndarray
2次元RAW画像。
black_level : int, 任意
すべての画素から引くブラックレベル。
デフォルトは1022。
戻り値
-------
np.ndarray
ブラックレベル補正済みのRAW画像。
"""
corrected = image.astype(np.int32)
corrected -= black_level
corrected = np.clip(corrected, 0 , None )
return corrected.astype(np.uint16)
uint16のまま減算すると、結果が負になった場合にアンダーフローが発生します。そのため、一度int32へ変換してからブラックレベルを引いています。
減算後に負となった画素値は、np.clip関数を使って0に収めます。最後に、画像のデータ型をuint16へ戻します。
③ホワイトバランス(WB)補正
ホワイトバランス(WB)補正は、次の2段階に分けて行います。
まず、キャリブレーション専用のRAW画像からWBゲイン(RゲインとBゲイン)を求めます。
次に、求めたゲインを現像対象のRAW画像へ適用します。
現像対象のRAW画像に飽和していない均一な無彩色領域(白い壁等)が含まれていれば、その領域からWBゲインを求めることも可能です。
今回はこのような無彩色チャートを撮影した専用のキャリブレーション画像を別に用意しました。
キャリブレーション画像 キャリブレーション画像と現像対象画像で照明の種類や色温度が変わると、求めたWBゲインが適切でなくなるため、両方を同じ照明条件で撮影する必要があります。
キャリブレーション画像を読み込んだら、まずブラックレベル補正がmain.pyから実行されます。(ブラックレベル補正をしないと、後のWBゲインの計算結果がおかしくなります)
WBゲインの計算には、white_balance_calibration.pyのcalibrate_white_balance関数を使用します。
以下がwhite_balance_calibration.pyのソースコードです。
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import RectangleSelector
def calibrate_white_balance (
image: np.ndarray,
) -> tuple [float , float ]:
"""
無彩色チャート画像からホワイトバランスゲインを求める。
処理内容
----------
1. マウスで無彩色領域をROI選択する。
2. ROIを4つのベイヤー位置に分ける。
3. 信号レベルが高い2位置をGとみなす。
4. 残り2位置のうち、高い方をB、低い方をRとみなす。
5. Gを1.0としてRとBのゲインを計算する。
前提
----------
- imageはブラックレベル補正済み。
- 選択するROIは均一な無彩色領域。
- Gの信号レベルがR/Bより高い。
- Bの信号レベルがRより高い。
- GrとGbのゲインはどちらも1.0とする。
引数
----------
image : np.ndarray
ブラックレベル補正済みの2次元RAW画像。
戻り値
-------
tuple[float, float]
RゲインとBゲイン。
"""
if image.ndim != 2 :
raise ValueError ("imageは2次元RAW画像である必要があります。" )
selected_roi = None
def on_select (eclick, erelease) -> None :
"""マウスで選択したROIの座標を保存する。"""
nonlocal selected_roi
if (
eclick.xdata is None
or eclick.ydata is None
or erelease.xdata is None
or erelease.ydata is None
):
return
x0 = int (min (eclick.xdata, erelease.xdata))
y0 = int (min (eclick.ydata, erelease.ydata))
x1 = int (max (eclick.xdata, erelease.xdata))
y1 = int (max (eclick.ydata, erelease.ydata))
selected_roi = (x0, y0, x1, y1)
fig, ax = plt.subplots(figsize=(12 , 8 ))
ax.imshow(
image,
cmap="gray" ,
vmin=np.percentile(image, 1 ),
vmax=np.percentile(image, 99 ),
)
ax.set_title(
"Select a neutral area and press Enter"
)
ax.set_xlabel("Column" )
ax.set_ylabel("Row" )
selector = RectangleSelector(
ax,
on_select,
button=[1 ],
useblit=True ,
interactive=True ,
)
def on_key_press (event) -> None :
"""Enterキーが押されたら画像ウィンドウを閉じる。"""
if event.key == "enter" :
plt.close(fig)
fig.canvas.mpl_connect("key_press_event" , on_key_press)
plt.tight_layout()
plt.show()
_ = selector
if selected_roi is None :
raise RuntimeError ("ROIが選択されていません。" )
x0, y0, x1, y1 = selected_roi
if x1 - x0 < 2 or y1 - y0 < 2 :
raise ValueError ("選択したROIが小さすぎます。" )
roi = image[y0:y1, x0:x1]
p00 = roi[0 ::2 , 0 ::2 ]
p01 = roi[0 ::2 , 1 ::2 ]
p10 = roi[1 ::2 , 0 ::2 ]
p11 = roi[1 ::2 , 1 ::2 ]
levels = {
"00" : float (np.median(p00)),
"01" : float (np.median(p01)),
"10" : float (np.median(p10)),
"11" : float (np.median(p11)),
}
ordered_positions = sorted (
levels,
key=levels.get,
reverse=True ,
)
green_positions = ordered_positions[:2 ]
red_blue_positions = ordered_positions[2 :]
blue_position = max (
red_blue_positions,
key=levels.get,
)
red_position = min (
red_blue_positions,
key=levels.get,
)
red_median = levels[red_position]
green_median = (
levels[green_positions[0 ]]
+ levels[green_positions[1 ]]
) / 2.0
blue_median = levels[blue_position]
if red_median <= 0 or blue_median <= 0 :
raise ValueError (
"RまたはBの信号レベルが0以下です。"
"別のROIを選択してください。"
)
red_gain = green_median / red_median
blue_gain = green_median / blue_median
print (" \n White-balance gains:" )
print (f"R gain: {red_gain:.6f}" )
print ("G gain: 1.000000" )
print (f"B gain: {blue_gain:.6f}" )
return red_gain, blue_gain
このプログラムは、結構苦労しました...
AIが無かったらこんなの正直自力だけでは書けないです。(笑)
プログラムを実行すると、まずホワイトバランスキャリブレーション用の画像が表示されます。画像上で均一な無彩色領域をマウスで選択し、Enterキーを押して確定します。
選択した領域をベイヤー配列の4つの画素位置に分け、それぞれの画素値の中央値を計算します。
今回使用する画像では、無彩色領域の信号レベルが「G、B、R」の順に高くなることを利用しています。4つの信号レベルのうち、値が高い2つをGrとGb、残りのうち値が高い方をB、低い方をRとして判別します。
その後、GrとGbの平均値をGとし、RとBの信号レベルをGにそろえるためのWBゲインを計算します。
ターミナルにWBゲインの計算結果が表示されます。
今回6500Kの照明を使って撮影しましたが、下のような結果になりました。
次に、求めたWBゲインを現像対象のRAW画像へ適用します。この処理はwhite_balance.pyのapply_white_balance関数で行います。
以下がwhite_balance.pyのソースコードです。
import numpy as np
def apply_white_balance (
image: np.ndarray,
red_gain: float ,
blue_gain: float ,
) -> np.ndarray:
"""
RGGBベイヤーRAW画像にホワイトバランスゲインを適用する。
ベイヤーパターンは以下を前提とする。
R Gr
Gb B
画像の左上画素 image[0, 0] はRとする。
Gのゲインは1.0固定とし、
R画素とB画素だけにゲインを掛ける。
引数
----------
image : np.ndarray
ブラックレベル補正済みの2次元RAW画像。
red_gain : float
R画素に掛けるホワイトバランスゲイン。
blue_gain : float
B画素に掛けるホワイトバランスゲイン。
戻り値
-------
np.ndarray
ホワイトバランス補正済みのRAW画像。
"""
if image.ndim != 2 :
raise ValueError (
"imageは2次元RAW画像である必要があります。"
)
corrected = image.astype(
np.float32,
copy=True ,
)
corrected[0 ::2 , 0 ::2 ] *= red_gain
corrected[1 ::2 , 1 ::2 ] *= blue_gain
return corrected
今回のベイヤーパターンは、左上から次のように並ぶRGGBです。
R Gr
Gb B
R画素にはRゲインを掛け、B画素にはBゲインを掛けます。Gr画素とGb画素のゲインは1.0としているため、値を変更しません。
ゲインを掛けると小数が発生するため、ホワイトバランス補正後の画像はfloat32になります。
④デモザイク
ベイヤーRAW画像では、各画素がR、G、Bのうち1色の情報しか持っていません。そこで、各画素に不足している色成分を周囲の画素から補間します。
今回のツールはあくまで簡易ツールなので、最もシンプルな方式である線形補間デモザイクを採用しています。つまり、周辺画素の平均値を使って値を補間しています。
こちらの処理はdemosaic.pyのdemosaic_bilinear関数を使います。
以下がdemosaic.pyのソースコードです。
import numpy as np
def demosaic_bilinear (image: np.ndarray) -> np.ndarray:
"""
RGGB Bayer RAW画像を線形補間法でデモザイクする。
Bayerパターンは以下を前提とする。
R G
G B
各画素で不足している色成分を、
周囲の同色画素の平均値で補間する。
Parameters
----------
image : np.ndarray
ホワイトバランス補正済みの2次元Bayer RAW画像。
Returns
-------
np.ndarray
デモザイク後のRGB画像。
shapeは (height, width, 3)。
チャンネル順は R, G, B。
"""
if image.ndim != 2 :
raise ValueError (
"imageは2次元Bayer RAW画像である必要があります。"
)
raw = image.astype(np.float32)
height, width = raw.shape
rgb = np.zeros(
(height, width, 3 ),
dtype=np.float32,
)
rgb[0 ::2 , 0 ::2 , 0 ] = raw[0 ::2 , 0 ::2 ]
rgb[0 ::2 , 1 ::2 , 1 ] = raw[0 ::2 , 1 ::2 ]
rgb[1 ::2 , 0 ::2 , 1 ] = raw[1 ::2 , 0 ::2 ]
rgb[1 ::2 , 1 ::2 , 2 ] = raw[1 ::2 , 1 ::2 ]
padded = np.pad(
raw,
1 ,
mode="edge" ,
)
up = padded[:-2 , 1 :-1 ]
down = padded[2 :, 1 :-1 ]
left = padded[1 :-1 , :-2 ]
right = padded[1 :-1 , 2 :]
up_left = padded[:-2 , :-2 ]
up_right = padded[:-2 , 2 :]
down_left = padded[2 :, :-2 ]
down_right = padded[2 :, 2 :]
green = (
up
+ down
+ left
+ right
) / 4.0
rgb[0 ::2 , 0 ::2 , 1 ] = green[0 ::2 , 0 ::2 ]
rgb[1 ::2 , 1 ::2 , 1 ] = green[1 ::2 , 1 ::2 ]
red_horizontal = (
left
+ right
) / 2.0
rgb[0 ::2 , 1 ::2 , 0 ] = red_horizontal[0 ::2 , 1 ::2 ]
red_vertical = (
up
+ down
) / 2.0
rgb[1 ::2 , 0 ::2 , 0 ] = red_vertical[1 ::2 , 0 ::2 ]
red_diagonal = (
up_left
+ up_right
+ down_left
+ down_right
) / 4.0
rgb[1 ::2 , 1 ::2 , 0 ] = red_diagonal[1 ::2 , 1 ::2 ]
blue_vertical = (
up
+ down
) / 2.0
rgb[0 ::2 , 1 ::2 , 2 ] = blue_vertical[0 ::2 , 1 ::2 ]
blue_horizontal = (
left
+ right
) / 2.0
rgb[1 ::2 , 0 ::2 , 2 ] = blue_horizontal[1 ::2 , 0 ::2 ]
blue_diagonal = (
up_left
+ up_right
+ down_left
+ down_right
) / 4.0
rgb[0 ::2 , 0 ::2 , 2 ] = blue_diagonal[0 ::2 , 0 ::2 ]
return rgb
最初に、R、G、Bの3チャンネルを持つ空の配列を作り、元のベイヤー画素を対応するチャンネルへ配置します。
その後、Gチャンネル、Rチャンネル、Bチャンネルの不足している値を、上下左右または斜め方向にある画素の平均値から求めます。
入力は高さ×幅の2次元配列ですが、デモザイク後は高さ×幅×3のRGB配列になります。出力のデータ型はfloat32です。
⑤正規化
デモザイク後のRGB画像は、そのままではガンマ補正に適した範囲になっていません。そこで、normalization.pyのnormalize_rgb関数を使い、画素値を0.0~1.0の範囲へ正規化します。
以下がnormalization.pyのソースコードです。
import numpy as np
def normalize_raw (
image: np.ndarray,
percentile: float = 99.0 ,
) -> np.ndarray:
"""2次元RAW画像を0.0~1.0の範囲へ正規化する。"""
if image.ndim != 2 :
raise ValueError (
"imageは2次元RAW画像である必要があります。"
)
if not 0.0 < percentile <= 100.0 :
raise ValueError (
"percentileは0より大きく100以下である必要があります。"
)
normalized = image.astype(
np.float32,
copy=False ,
)
maximum = float (
np.percentile(
normalized,
percentile,
)
)
if maximum <= 0 :
raise ValueError (
"正規化に使用できる画素値がありません。"
)
normalized = normalized / maximum
normalized = np.clip(
normalized,
0.0 ,
1.0 ,
)
return normalized
def normalize_rgb (
image: np.ndarray,
percentile: float = 99.0 ,
) -> np.ndarray:
"""RGB画像を0.0~1.0の範囲へ正規化する。"""
if image.ndim != 3 or image.shape[2 ] != 3 :
raise ValueError (
"imageはshape=(height, width, 3)のRGB画像である必要があります。"
)
if not 0.0 < percentile <= 100.0 :
raise ValueError (
"percentileは0より大きく100以下である必要があります。"
)
normalized = image.astype(
np.float32,
copy=False ,
)
maximum = float (
np.percentile(
normalized,
percentile,
)
)
if maximum <= 0 :
raise ValueError (
"正規化に使用できる画素値がありません。"
)
normalized = normalized / maximum
normalized = np.clip(
normalized,
0.0 ,
1.0 ,
)
return normalized
このモジュールには、2次元RAW画像用のnormalize_raw関数と、3チャンネルRGB画像用のnormalize_rgb関数があります。
今回の正規化では、画像内の最大値ではなく99パーセンタイル値を基準にしています。最大値を基準にすると、ごく少数の極端に明るい画素によって画像全体が暗くなる場合があります。99パーセンタイル値を使うことで、この影響を抑えています。
画素値を基準値で割ったあと、0.0未満を0.0、1.0を超えた値を1.0に収めます。
正規化後の画像データ型はfloat32です。
正規化の後、PNG保存(8bit)がmain.py上で実行されます。
demosaic.pngが保存されます
⑥ガンマ補正
デモザイク後のRGB画像は、画素値と光の強さがほぼ比例する線形画像です。そのまま表示すると、中間調が暗く見えます。
そこで、画素値を「1÷ガンマ値」乗し、表示しやすい明るさへ変換します。
正規化後のRGB画像に、gamma.pyのapply_gamma_correction関数を使ってガンマ補正を適用します。
以下がgamma.pyのソースコードです。
import numpy as np
def apply_gamma_correction (
image: np.ndarray,
gamma: float = 2.2 ,
) -> np.ndarray:
"""0.0~1.0の線形RGB画像にガンマ補正を適用する。"""
if image.ndim != 3 or image.shape[2 ] != 3 :
raise ValueError (
"imageはshape=(height, width, 3)のRGB画像である必要があります。"
)
if gamma <= 0 :
raise ValueError (
"gammaは0より大きい必要があります。"
)
corrected = image.astype(
np.float32,
copy=False ,
)
corrected = corrected ** (1.0 / gamma)
return corrected
今回のガンマ値は2.2です。
ガンマ補正の中心となる処理は、次の計算です。
corrected = corrected ** (1.0 / gamma)
入力画像と出力画像は、どちらも0.0~1.0の範囲を持つfloat32のRGB画像です。
ガンマ補正の後、PNG保存(8bit)がmain.py上で実行されます。
gamma.pngが保存されます
8bitPNG保存
各処理段階の画像は、png_writer.pyのsave_png関数を使って8bit PNGとして保存します。
以下がpng_writer.pyのソースコードです。
from pathlib import Path
import numpy as np
from PIL import Image
def save_png (
image: np.ndarray,
file_path: Path,
) -> None :
"""0.0~1.0の画像を8bit PNGとして保存する。"""
if image.ndim == 2 :
mode = "L"
elif image.ndim == 3 and image.shape[2 ] == 3 :
mode = "RGB"
else :
raise ValueError (
"imageは2次元画像またはRGB画像である必要があります。"
)
image_uint8 = np.rint(
np.clip(image, 0.0 , 1.0 ) * 255.0
).astype(np.uint8)
Image.fromarray(
image_uint8,
mode=mode,
).save(file_path)
save_png関数は、入力画像が2次元画像かRGB画像かを判定します。2次元画像の場合はグレースケール、3チャンネル画像の場合はRGBとして保存します。
入力画像の画素値は0.0~1.0なので、255を掛けて0~255へ変換します。
その後、四捨五入してuint8へ変換し、Pillowを使ってPNGファイルとして保存します。
今回のツールでは、次の3枚をoutputフォルダへ保存します。
raw.png
RAW画像を読み込み、表示用に正規化した画像です。
demosaic.png
ブラックレベル補正、ホワイトバランス補正、デモザイク、正規化まで行った画像です。
gamma.png
正規化後のRGB画像にガンマ補正を適用した最終画像です。
main.pyで各処理をつなぐ
最後に、これまで説明した各関数をmain.pyで順番に呼び出します。
以下がmain.pyのソースコードです。
from pathlib import Path
from tools.raw_reader import load_raw_image
from tools.normalization import (normalize_raw, normalize_rgb)
from tools.print_col_median import print_col_median
from tools.black_level import apply_black_level_correction
from tools.white_balance_calibration import calibrate_white_balance
from tools.white_balance import apply_white_balance
from tools.demosaic import demosaic_bilinear
from tools.gamma import apply_gamma_correction
from tools.png_writer import save_png
PROJECT_DIR = Path(__file__).parent
RAW_FILE = PROJECT_DIR / "input" / "sample_raw.bin"
CALIBRATION_FILE = PROJECT_DIR / "input" / "white_balance_calibration_raw.bin"
OUTPUT_DIR = PROJECT_DIR / "output"
def main () -> None :
"""シンプルなRAW現像パイプラインを実行する。"""
image = load_raw_image(RAW_FILE)
height, width = image.shape
print (f"width={width}, height={height}" )
normalized_raw_image = normalize_raw(image)
save_png(
normalized_raw_image,
OUTPUT_DIR / "raw.png"
)
print_col_median(image)
image = apply_black_level_correction(image)
calibration_image = load_raw_image(CALIBRATION_FILE)
calibration_image = apply_black_level_correction(calibration_image)
red_gain, blue_gain = calibrate_white_balance(
calibration_image
)
image = apply_white_balance(
image,
red_gain,
blue_gain,
)
rgb_image = demosaic_bilinear(image)
normalized_rgb_image = normalize_rgb(
rgb_image
)
save_png(
normalized_rgb_image,
OUTPUT_DIR / "demosaic.png" ,
)
gamma_rgb_image = apply_gamma_correction(
normalized_rgb_image
)
save_png(
gamma_rgb_image,
OUTPUT_DIR / "gamma.png" ,
)
if __name__ == "__main__" :
main()
main.pyでは、toolsフォルダ内の各モジュールから必要な関数をインポートしています。
最初に、現像対象のRAWファイル、ホワイトバランスキャリブレーション用のRAWファイル、PNG画像の出力先を指定します。
その後、次の順番でRAW現像を行います。
① RAW画像の読み込み
② ブラックレベル補正
③ ホワイトバランス補正
④ デモザイク
⑤ 正規化
⑥ ガンマ補正
各処理を別のモジュールへ分割し、main.pyでは処理の順番だけを記述することで、RAW現像全体の流れを確認しやすい構成にしています。