rawpyでPython現像その16(output color)

python

 postprocess関数の引数のoutput_colorに関して確認します。またその過程で取得する変数color_matrixに関しても簡単に調べてみました。

 postprocessに関しての詳細はこちら

 いわゆる色変換です。デバイスでセンシング(光電変換)したRGBの信号をデバイス非依存の標準的な色空間へ変換するオプションです。

 正直どんな撮影環境(光源)でとったか未知で、そのホワイトバランスも調整されているとなると、その標準にどれだけの意味があるのかどうかわかりませんが、そのデバイスの癖を吸収してくれる変換をしてくれると思いたいです。特定の光源で、色が既知のものを撮影したときに、その色を再現するような変換パラメータなのかなと思っています。

色空間

 色空間とかカラーマネジメントの話をしだすとそれだけで記事が終わってしまうので、今回は割愛することにします。

 postprocessでは、この変換オプションとして用意されている色空間として、以下の6種類があるようです。

 raw, sRGB, Adobe, Wide, ProPhoto, XYZ

 それぞれの特徴を一言でまとめてみると、

raw無変換(カメラセンサ値そのもの)
sRGBベーシックなRGB色空間 空間は狭い
Adobe RGB比較的広く使われているsRGBより広いRGB色空間
Wide gamut RGBとてつもなく広い RGB色空間
ProPhoto RGBこれもまたとてつもなく広い RGB色空間
XYZ中間的な物理量として扱いやすい色空間(RGBでない)

 なイメージ。で大外しはしてないと思います。Wide gamut RGBないしProPhoto RGBは調べてみても日本語のWikiは存在しなそうです。が海外のWikiを見てもその色空間の広さは図を見るとわかります。はやってんのかな?

 これらの色変換は基本的には行列演算で求めているようです。

$$\left(\begin{array}{c}R_{out} \\ G_{out} \\ B_{out}\end{array}\right) = \left(\begin{array}{c}a & b & c \\ d & e & f \\ g & h & i \end{array}\right)\left(\begin{array}{c}R_{raw} \\ G_{raw} \\ B_{raw}\end{array}\right)$$

 処理としてはオーソドックスです。

color_matrix

 ドキュメントを読むと、どうやらこのマトリクスは外から取得できるように書かれています。

import rawpy

raw = rawpy.imread(filename)
matrix = raw.color_matrix

 こんな感じで。取得される配列は3行4列の配列になっています。おそらくオフセット値も持てる仕様なのだと思います。\(aR+bG+cB+ofst\)みたいな感じで。ただし、必ずこの行列が取れる保証はなさそうです。現に、Canon、PanasonicのRawからはとれませんでした(全ゼロの配列が返ってきました)が、Olympus、iPhoneからは取れました。以下はOlympusのそれ。

[[ 1.71875   -0.59375   -0.125      0.       ]
 [-0.28125    1.515625  -0.234375   0.       ]
 [ 0.0546875 -0.53125    1.4765625  0.       ]]

 ただ、当然配列が取れないメーカーのRawも色空間を指定すればそれなりに絵が変わるので、何か行列変換はしてそうです。後述しますが、固定の変換をしているわけではなさそうです。ルールは不明です。

 またEXIFにもこの行列記載されていることがあります。exiftoolというツールを使ってみると、Olympusのrawデータには、

Color Matrix                    : 440 -152 -32 -72 388 -60 14 -136 378

 このような値が埋め込まれていました。これは3×3の行列です。8bitに正規化されてそうです。iPhoneのrawデータのEXIFには

Color Matrix 1                  : 1.32691133 -0.6003777981 -0.2564359307 -0.3622747958 1.340274453 0.1768969595 -0.008134222589 0.1007148176 0.7401231527
Color Matrix 2                  : 0.7093279958 -0.2275772095 -0.02130203508 -0.6051968932 1.248973966 0.3010800779 -0.1126812845 0.1317028701 0.5886504054

 と2つも入っています。使い分けがわかりません。

 さらに言うと取得された配列も何の色空間に変換するものか、さらに言うと、RGBどれに対する重みなのか、記述はありません。調べるとどうやらsRGBへの変換行列のようですが。

行列推定

 取得できないのであれば、推定してみるしかありません。幸い以前最小二乗方による変数推定にはトライしたことがあるので、今回はそれを応用します。

 詳細の説明はそちらの記事を読んでいただければと思いますが、結論としては、\(R_{out} = aR_{raw}+bG_{raw}+cB_{raw}\) として、n画素の画像を変換した結果から、\(\left(\begin{array}{c}R_{raw1} & G_{raw1} & B_{raw1} & \\ R_{raw2} & G_{raw2} & B_{raw2} \\ : & : & : \\ R_{rawn} & G_{rawn} & B_{rawn} \end{array}\right)\)を \(A\)とし、\(\left(\begin{array}{c}a \\ b \\ c \end{array}\right)\)を \(x\)、 \(\left(\begin{array}{c}R_{out1} \\ R_{out2} \\ : \\R_{outn} \end{array}\right)\) を \(b\)とすると、

$$ A^{\top} Ax=A^{\top}b$$

 この式を解けばよく、結果RGBに対する重み\(a,b,c\)は、

$$x=(A^{\top} A)^{-1}A^{\top}b$$

 を解けばよいことになります。これをRGBそれぞれについて解いてみます。コードはこんな感じ。

import rawpy
import numpy as np

raw = rawpy.imread(filename)
gam = [1.00,0.00]
raw  = raw.postprocess(half_size=True,
                       demosaic_algorithm=rawpy.DemosaicAlgorithm.LINEAR,
                       no_auto_bright=True,
                       output_color=rawpy.ColorSpace.raw,
                       output_bps=16,
                       gamma=gam,
                       ) / 65535

srgb = raw.postprocess(half_size=True,
                       demosaic_algorithm=rawpy.DemosaicAlgorithm.LINEAR,
                       no_auto_bright=True,
                       output_color=rawpy.ColorSpace.sRGB,
                       output_bps=16,
                       gamma=gam,
                       ) / 65535

h, w = raw.shape[:2]
raw = raw.reshape([w*h,3])
srgb = srgb.reshape([w*h,3])

invX1 = np.linalg.inv(np.dot(raw.T, raw))  # 逆行列変換
# Rに対する重み
X1tY = np.dot(raw.T, srgb[:, 0])
ans1 = np.dot(invX1, X1tY)

# Gに対する重み
X1tY = np.dot(raw.T, srgb[:, 1])
ans2 = np.dot(invX1, X1tY)

# Bに対する重み
X1tY = np.dot(raw.T, srgb[:, 2])
ans3 = np.dot(invX1, X1tY)

# 3x3行列へ結合
ans = np.concatenate([ans1, ans2, ans3]).reshape(3,3)

 現像後の画像は16bit精度にしたうえで、0~1までのfloatにしています。推定した行列をもとのraw画像に対して適用して検算してみると、srgbで現像したものと同じような値になるので、おおよそあってそうです。

結果

 このようにして推定(近似)した行列を列挙します。法則はつかめません。ココではolympusのカメラとしては、OM-D EM10 MkII。Canonのカメラとして、EOS 5D MkII。Panasonicのカメラとして、DMC-GM1。のrawデータを用いました。がある1枚を使っただけなので、すべてのデータで同じかまでは検証してません。

raw to sRGB

olympus
 [[ 1.75389208  -0.66778639  -0.09077232]
  [-0.26792387   1.75870544  -0.49173254]
  [ 0.03206899  -0.49317099   1.46110139]]
canon
 [[ 2.0976753   -1.33398326   0.2048325 ]
  [-0.02524351   1.33991503  -0.31485632]
  [ 0.06379722  -0.39845633   1.32931957]]
panasonic
 [[ 1.60817275  -0.56746481  -0.04606059]
  [-0.15732087   1.58047431  -0.42352369]
  [ 0.06232235  -0.60332228   1.54059369]]

 olympusのそれは、前述したcolor_matrixとして取得できた行列と似ているので、ここからcolor_matrixで取得される行列はrawからsRGBへの変換行列と予想されます。

 行列が取得できないからと言って、同じ変換行列を使ってなさそうなことが、canonとpanasonicの行列の違いからも予想されます。ハードコードされているわけではなさそうです。

sRGB to XYZ

olympus
 [[0.3955229   0.40434432  0.15325947]
  [0.20406477  0.73898304  0.05829982]
  [0.08377068  0.17840913  0.76020462]]
canon
 [[0.36972467  0.42706424  0.16528782]
  [0.18891657  0.75453375  0.06257793]
  [0.00513105  0.13344352  0.95342038]]
panasonic
 [[ 0.41262465   0.36795568   0.17285401]
  [ 0.21269765   0.72065728   0.06819326]
  [ 0.13782675  -0.19511701   1.09595565]]

 似てます。がどうもolympus panasonicのZの近似がイマイチです。Canonはびったり合うのですが…。原因は不明。

 軽く調べた感じの標準的なsRGB to XYZの変換行列が

  0.4124564  0.3575761  0.1804375
  0.2126729  0.7151522  0.0721750
  0.0193339  0.1191920  0.9503041

 まぁまぁいい感じで近似できてそうです。逆に言うとrawpyの中ではそれほどおかしな変換はしてなさそうです。

Adobe RGB to XYZ

olympus
 [[0.56046952  0.22277597  0.16886697]
  [0.289157    0.64622094  0.06544694]
  [0.12241477  0.10135007  0.79824989]]
canon
 [[0.54156449  0.23643667  0.17933791]
  [0.27652236  0.6583413   0.06875275]
  [0.01219733  0.08480397  0.99438642]]
panasonic
 [[ 0.57599417   0.18933926   0.18587382]
  [ 0.29689842   0.62950441   0.07400001]
  [ 0.18847503  -0.29419311   1.14392252]]

 これに対して、正解(と信じ込んでいるサイトの行列)が

  0.5767309  0.1855540  0.1881852
  0.2973769  0.6273491  0.0752741
  0.0270343  0.0706872  0.9911085

 まぁ近い。やはりZに対しての近似がうまくない。Canonはうまく行くのに…。

Wide gamut RGB to XYZ

olympus
 [[0.65566507  0.126088    0.16984567]
  [0.23438456  0.74212529  0.02379459]
  [0.1208822   0.07287439  0.82802503]]
canon
 [[ 0.64547194   0.12929519   0.17945929]
  [ 0.2287195    0.74736987   0.02516783]
  [-0.00563181   0.06343663   1.0323316 ]]
panasonic
 [[ 0.67682916   0.08593215   0.18797859]
  [ 0.2400889    0.73116812   0.02881455]
  [ 0.27833933  -0.46430572   1.22399768]]

 そして正解

  0.7161046  0.1009296  0.1471858
  0.2581874  0.7249378  0.0168748
  0.0000000  0.0517813  0.7734287

 コメントが難しい

ProPhoto RGB to XYZ

olympus
 [[0.752847564  0.117051289  0.0807513076]
  [0.267278254  0.716821039  0.0159581984]
  [0.150299319  0.000306994  0.870826705]]
canon
 [[ 0.75464904   0.11372746   0.08217889]
  [ 0.26455986   0.71994111   0.01559361]
  [ 0.00505475  -0.01358501   1.09718548]]
panasonic
 [[ 0.75535138   0.1132472    0.08192347]
  [ 0.26808965   0.71554957   0.01640173]
  [ 0.31076634  -0.50745907   1.23452579]]

 そして正解

  0.7976749  0.1351917  0.0313534
  0.2880402  0.7118741  0.0000857
  0.0000000  0.0000000  0.8252100

 PanasonicのZはだいぶ違う。近似誤差も大きい。何がまずいんだろう…。

まとめ

  output_colorで指定した色空間へ、センサRGBの値を3×3の行列変換をすることで変換している。

 その変換行列は外部から取得できる場合とできない場合がある。また取得できたものはrawからsRGBへの変換行列のようである。

 sRGB以外の色空間への変換は、標準的な変換式を掛け合わせて生成されている。

 といった感じだと予想されます。

python現像
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました