Kitsune Gadget

気になったことをつらつらと

CIE L*c*h* を RGB に変換したい

Rainmeter で CIELCH を扱いたいという理由で、CIELCHからRGBへの変換を Luaで実装したというものです。 ちなみに Rainmeter ではRGB値しか扱えません。

CIE L*c*h* (CIELCH) とは CIE L*a*b* (CIELAB) 空間を極座標で表したものです。RGB と HSL のような関係と言ったほうがわかりやすいでしょうか。なので本質は CIE L*a*b* 色空間 になります。

CIE L*a*b* は人が知覚する色の感度と座標距離が比較的一致するように定義した色空間です。 グラデーションなどで HSV や HSL を利用すると、同じ距離のRGB値でも人が知覚する実際の明るさは色によって差があります。

上:HSLによるグラデーション  下:CIELCHによるグラデーション

知覚的な明るさを合わせた色を配置することで、より滑らかな配色を実現できるツールになります。

L*a*b* 空間を説明する前に、その前提となるXYZ色空間を知っておきましょう。

XYZ 色空間

CIE 1931 で定義されている人間が知覚するおおよそすべての色を表したものです。

XYZは「三刺激値」と呼ばれ、Xが赤、Yが緑、Zが青の色味に強く対応したものになっています。しかしながら XYZ の数値を見ただけでは、それがどのような色かを判断するのは難しいです。*1

そこで加法混色として色の成分だけを取り出してみます。 XYZの混色比をxyzとして、x+y+z= 1という制約を設けることで色味だけにフォーカスを当てます。

 \displaystyle
x = \frac{X}{X+Y+Z} \\
\displaystyle
y = \frac{Y}{X+Y+Z} \\
\displaystyle
z = \frac{Z}{X+Y+Z} = 1-x-y \\

この制約からxyzの3次元空間で2軸が決まると残りの軸も定まるため、ひとつ軸を潰しても問題ありません。 z軸を無くした平面図がxy色度図です。x、yの平面で表せることで色相と彩度の関係が分かりやすくなります。

https://fujiwaratko.sakura.ne.jp/infosci/xyz_e.html

Yは明度(輝度)そのものでもあるため、色度と明度を合わせてxyY色空間という定義もあります。 明度によって色度図の層ができるイメージです。

Y が明度そのものなのは、人の比視感度(標準分光視感光率)、すなわち明るさの感度を緑の感度と同じ特性になるように規定したためです。よって Y をそのまま明度として扱うことができるようになっています。*2

代表的なM錐体細胞の正規化されたカーブと、明所視での標準観察者によるCIE 1931比視感度曲線の比較。

https://ja.wikipedia.org/wiki/CIE_1931_%E8%89%B2%E7%A9%BA%E9%96%93

CIE L*a*b* (CIELAB)

XYZ空間では色差(色の距離)が一定に保たれているわけではありませんでした。

CIE 1931色空間におけるマクアダム図。表示されている楕円は最大10倍も実際のサイズと異なる。

https://ja.wikipedia.org/wiki/%E8%89%B2%E5%B7%AE

上の図に表記される楕円においては色の違いが区別できません。(描画されている楕円のスケールは10倍されています。)

この楕円をなるべく円に近づけようとしたもの、すなわち色差を等間隔でおおよそ表せるようにしたのが CIELAB (CIE 1976)空間です。*3

L* は輝度または明度で取る値は 0~100です。 a* と b* は彩度平面で、それぞれの軸は 赤 ⇔ 緑、黄 ⇔ 青、の4つの方向になります。 この彩度は実質無限大の値をとることができますが実際にモニターで表せる色は限られています。 最大最小値はおよそ ±100~127程度が使われています。

CIELCH は CIELAB の平面座標を極座標で扱ったものです。 L はそのままで、半径距離はC、角度はHで表します。

https://www.konicaminolta.jp/instruments/knowledge/color/section2/02.html

以降は LCH, LAB と表記していきます。*4

変換について

変換は LCH → LAB → XYZ → RGB という流れを行います。 ホワイトポイントやガンマを考慮する必要があるため、直接変換する良い方法はおそらくありません。

LCH → LAB

LCH の極座標を LAB にするには、直交座標に変換するだけです。

 
L^* = L \\ 
a^* = C \cos H^{\circ } \\
b^* = C \sin H^{\circ }

LAB → XYZ

CIELAB color space - WikipediaOpenCV: Color conversions を参考に。

完全拡散反射面における相対輝度の光源である3値刺激 Xn Yn Zn を定義する必要があります。 これは Y=1 を基準として正規化した白色点(ホワイトポイント)のXYZ座標であり、 利用する光源によって値が異なります。D65 と D50 での値は以下の通りです。

D65光源における白色点 Xn Yn Zn

 
X_{n} = 0.950456 \\
Y_{n} = 1 \\
Z_{n} = 1.088754

D50光源における白色点 Xn Yn Zn

 
X_{n} = 0.964212 \\
Y_{n} = 1 \\
Z_{n} = 0.825188

この定数を使って以下のように計算します。

 \displaystyle 
X = X_{n}f\left(\frac{L^*+16}{116} + \frac{a^*}{500}\right) \\
\displaystyle 
Y = Y_{n}f\left(\frac{L^*+16}{116}\right) \\
\displaystyle 
Z = Z_{n}f\left(\frac{L^*+16}{116} - \frac{b^*}{200}\right) \\
 \displaystyle 
f(t)=\begin{cases} t^{3}  \qquad\qquad\qquad \text{if} \quad t \gt δ \\ 
3δ^{2}\left(t-\dfrac{4}{29}\right) \quad \text{otherwise}
\end{cases} \\
\displaystyle 
δ = \frac{6}{29}

δ は t = 0 での無限の傾きを防ぐための閾値です。

XYZ → RGB

この変換も利用する光源によって値が変わります。

D65光源における変換行列

 
\begin{bmatrix}R \\ G \\ B \end{bmatrix}
=
\begin{bmatrix}
3.240479 & -1.537150 & -0.498535 \\
-0.969256 & 1.875991 & 0.041556 \\
0.055648 & -0.204043 &  1.057311\\
\end{bmatrix}
\begin{bmatrix}X \\ Y \\ Z \end{bmatrix}

D50光源における変換行列

 
\begin{bmatrix}R \\ G \\ B \end{bmatrix}
=
\begin{bmatrix}
3.134187 & -1.617209 & -0.490964 \\
-0.978749 & 1.91613 & 0.033433 \\
0.071964 & -0.228994 &  1.405754 \\
\end{bmatrix}
\begin{bmatrix}X \\ Y \\ Z \end{bmatrix}

上記式のD65においては、OpenCVに書いてあるもので小さい値が違いますが、3値刺激 Xn Yn ZnもOpenCVに書かれているものを採用したため同一元にしています。

リニアRGB から sRGB へ

そして大事なのはここで変換されるRGB値はリニアRGB(線形RGB)と呼ばれる値になります。このリニアRGBをsRGBに戻す必要があります。

https://www.mathworks.com/help/images/ref/lin2rgb_ja_JP.html

上の図を見るとリニアRGBよりsRGBのほうが明るくなるのが早いです。

一般的にリニアRGB⇔sRGBの変換はガンマ補正と呼ばれ、モニターや画像編集にもその設定があり、多く見たことがあると思います。

もともとはCRTモニターの輝度特性を補正する用途で使われていたものです。CRTモニターではおおよそ2.2の曲線で輝度特性があったため、sRGBではこの特性にそのまま対応するようにリニアRGBから1/2.2で補正した値で画像を扱っていました。現在の液晶モニターではこのような輝度特性は起きないためモニター側で2.2のガンマ補正をあえて作り出しています。つまり、先ほどの図のグラデーションはsRGBのほうがリニアRGBより明暗の流れが均一に見えているはずです。*5

https://learnopengl.com/Advanced-Lighting/Gamma-Correction

現在もガンマの値は2.2で近似していますが、sRGBにおいて厳密には低輝度で直線、高輝度で2.4です。この低輝度付近は傾き無限になることを防ぐために直線近似になっています。*6

今回は リニアRGB→sRGB の逆方向になるため、1/2.4で逆変換することになります。*7

 \displaystyle 
C_{sRGB}=\begin{cases}
12.92C_{linear}  \qquad\qquad\quad C_{linear}  \leq 0.0031308 \\ 
1.055C_{linear}^{1/2.4} - 0.055  \qquad C_{linear} \gt 0.0031308
\end{cases}

式に現れる 0.055 という値も black offset とよばれる標準的なモニターの暗電流のオフセットとなっています。 また、入力に負の値が含まれる場合は正に反転した値で変換させて、再び負に戻します。

変換した値は 0~1 の範囲のため、RGBの8bitにするには 0~255 の範囲に調整して整数に丸めます。

このようにして LCH から RGB に変換することができました。

変換結果

javascript で実装し、ブラウザにおいて CSS の lch 関数との差異を見てみます。

D65光源での変換 CSSにおけるlch関数との比較

CSSのlch関数とこちらで変換したRGBにほぼ違いはありません。しかしながら色相の紫色あたりでは少し違いがありました。W3CのCRを見るとホワイトポイントにD50光源を利用していたようで、D65光源での実装では違いが出てしまったようです。D50光源の値で試してみたところ紫色相の部分も同じになります。

D50光源での変換 CSSにおけるlch関数との比較

ブラウザ基準にするならD50光源を利用しましょう。

変換後の値がRGBで表せない場合も

LCH からRGBに変換すると、設定する値によっては各要素が100%を超えたり負数になります。 これはRGBで表せる色域の外側の領域であるためです。 LCHで指定した値がRGB色域外の場合、クリップされるため実際のLCHで指定した色と表示される色は異なってしまいます。 LCHを設定する場合は、このあたりを考慮する必要があります。

L*a*b* 空間の平面図における sRGB (一番内) と P3-D65 と Adobe RGB の色域。 外側の曲面は最適色立体の色域=MacAdam limit。

https://fujiwaratko.sakura.ne.jp/infosci/lab.html

上の図から分かるようにシアン方向、すなわち a と b の負数方向では一般的なモニターの色域で再現できる色がほとんど無いことがわかります。 LCHでの明度固定グラデーションは使う色によって彩度を変えるか色相の範囲を限定する必要があるといえます。

okLab と okLch

LAB空間の色差の均一性は完全なものではありません。最近では改良された okLab 空間も登場したようです。okLchの定義もあり、LCH同様に okLab を極座標にしたものとなっています。

A constant CIE LCH hue slice, showing the sRGB gamut around primary blue. A noticeable purpling is immediately evident.

A constant Oklch hue slice, showing the sRGB gamut around primary blue. The visual hue remains constant.

https://www.w3.org/TR/css-color-4/#ok-lab

okLabでは色相と彩度の均一性が向上しています。 このことは、LAB空間のグラデーション直線に違和感があるのを解消します。(主に青紫方向で顕著です。)

An interactive review of Oklab | Raph Levien’s blog ではいくつかの種類の色空間グラデーションで問題をわかりやすく見ることができます。

okLab のとる値は LAB と似ていますが、 a と b の値が ±0.5 程度までが事実上の上下限です。

また、RGBへの計算方法は XYZ空間を経由したものではなくLMS空間から直接計算する必要があるようです。

XYZ空間やリニアRGBとの相互変換は以下で表記されています。

bottosson.github.io

Lua で実装したもの

以上の計算を踏まえて、Rainmeter用にLuaで実装したものになります。

まとめ

人間の色覚特性を反映したRGB空間、それを計算利便性に特化したXYZ空間の定義がありました。さらにLAB空間では数値と知覚する色の距離が均等になるように定義されています。そのため、LCHからの変換は LAB → XYZ → RGB の流れが必要でした。

全ての場合において LCH が有効というわけではありません。高彩度ではモニターの色域から外れやすいという問題が存在するからです。RGBよりも色域の広いものは P3 や Rec.2020 がありますが、そのような製品は割と高価です。とはいえ、それらの色域でもLCHの範囲をすべて表すことができるわけではありません。*8

等間隔の数値で色差を保ちながら色を利用できるのは素晴らしいことですが、色方向によって正しく表示できる範囲が限られることを知っておく必要があるでしょう。

さらにLABの座標直線における色差が改善されたokLab空間についても知ることができたため、こちらを利用するほうがより良いですね。

今回は Rainmeter 向けに変換を実装しましたが、CSS においてはビルトイン関数として lch も oklch も実装されていて使うことができます。MDNにリファレンスページもあるため確認しつつ利用すると良いでしょう。

okLchについてのRGBとの関係は以下で見てみるのが分かりやすいです。

oklch.com

参考

  1. CIE 1931 色空間 - Wikipedia

  2. CIELAB color space - Wikipedia

  3. sRGB - Wikipedia

  4. 異なる色空間間での変換について - MATLAB & Simulink - MathWorks 日本

  5. 線形 RGB 値へのガンマ補正の適用 - MATLAB lin2rgb

  6. OpenCV: Color conversions

  7. CSS Color Module Level 4

  8. 【HDRと色彩管理2】色空間 #Unity - Qiita

  9. ガンマ補正のうんちく #ガンマ補正 - Qiita

  10. LearnOpenGL - Gamma Correction

  11. 色の数値化には、表色系を使用します。1-楽しく学べる知恵袋 | コニカミノルタ

  12. 光と色の話 第一部|事例・ガイド・コラム|CCS:シーシーエス株式会社

  13. sRGB色空間と国際標準化

  14. 情報科学講義資料 - 藤原隆男 (元京都市立芸術大学)

*1:RGB等色実験によりRGB色空間が作られましたが、負の値が含まれるものでした。計算上の不便点を解消するべく正の値のみにしたものがXYZ空間です。そのため RGB とは違い XYZ それぞれは他原色も混ざった値です。XYZの原点は可視範囲に合わせて定めた虚色の位置になっています。

*2:実際にYは相対輝度と呼ばれるもので、人が知覚する明るさの量です。

*3:CIELUV空間という定義も存在し、CIELABとお互いに長所短所があるため両方とも残っています。CIELUVもCIELABも色差を均一にしようとした色空間です。その代わりにXYZの色計算における利便性を犠牲にしています。

*4: L*a*b*の前身となった、Hunter Lab 空間との区別をつけるために公式には「*」を含めた表記をしています。現在ではLabといえばL*a*b*のことを差すことが多いですが、CIELABと表記するほうが曖昧さを無くせるそうです。

*5:液晶モニターの出力はCRTモニターの特性で定義してしまった画像規格に互換性を持たせるためにガンマ補正をしているともいえます。しかしながら人の視覚特性(ウェーバー・フェフィナーの法則)とこのガンマ曲線が偶然にも近かったこともあり、ガンマ補正は現在も重要な存在です。

*6:AdobeRGBに対しては低輝度の直線は使わず、全体に対して2.2で単純変換します。また計算パフォーマンスのために2.2でsRGBを単純変換する場合もあります。

*7:累乗計算は重いため、画素ごとに計算を繰り返す画像処理ではルックアップテーブル(LUT)を作ってあらかじめ計算された値を使うのが一般的です。今回の実装では利用していません。

*8:P3色域のものはiPhone等のApple製品で普及しているところはあります。