【JavaScript】sRGB色空間からLa*b*空間に変換・逆変換する関数

#概要
この記事では、sRGB色空間からLa*b*色空間へ変換する関数を紹介しています（勉強がてらにメモしています）。この色空間たちは直接変換することができないため、一旦sRGBからXYZ色空間に変換したのち、La*b*色空間へ変換しています。

##追記(2021/02/05)
変換式のソースコードを修正し、さらに逆変換式も追加しました。

#コード
##sRGB→La*b*変換式
```js
function RGBtoLab(x, y, z) {
	return XYZtoLab(...RGBtoXYZ(x, y, z));
}
function XYZtoRGB(r, g, b) {
	[r, g, b] = [r, g, b].map(n => {
		// sRGBからリニアRGBに変換する
		return (n <= 0.040450) ? (n / 12.92) : Math.pow((n + 0.055) / 1.055, 2.4);
	});
	let x = 0.412391 * r + 0.357584 * g + 0.180481 * b,
		y = 0.212639 * r + 0.715169 * g + 0.072192 * b,
		z = 0.019331 * r + 0.119195 * g + 0.950532 * b;
	return [x, y, z];
}
function XYZtoLab(x, y, z) {
	// ホワイトポイントはD65光源とする。
	const whitePoint = [95.047, 100, 108.883];
	[x, y, z] = [x, y, z].map((value, index) => {
		return value * 100 / whitePoint[index];

	}).map((value, index) => {
		// x, y, z の補正式: ※ 0.008856 = Math.pow(6 / 29, 3), 7.787037 = 1 / (3 * Math.pow(6 / 29, 2))
		return value > 0.008856 ? Math.pow(value, 1 / 3) : (7.787037 * value) + 0.008856;
	});

	// xyz → La*b* 変換式
	const L = (116 * y) - 16,
		  a = 500 * (x - y),
		  b = 200 * (y - z);
	
	[L, a, b] = [L, a, b].map(n => {return n / 100});
	
	return [L, a, b];
}
```

##La*b*→sRGB変換式
```js
function LabtoRGB(L, a, b) {
	return XYZToRGB(...LabToXYZ(L, a, b));
}
function LabtoXYZ(L, a, b) {
	[L, a, b] = [L, a, b].map(n => {return n * 100});
	// La*b* → xyz 変換式
	let y = (L + 16) / 116;
	let x = y + a / 500;
	let z = y - b / 200;
	
	// ホワイトポイントはD65光源とする。
	const whitePoint = [95.047, 100, 108.883];
	[x, y, z] = [x, y, z].map((value, index) => {
		// x, y, z の補正式: ※ 0.206897 = (6 / 29), 7.787037 = 1 / (3 * Math.pow(6 / 29, 2));
		return value > 0.206897 ? Math.pow(value, 3) : (value - 0.206897) / 7.787037;
		
	}).map((value, index) => {
		return value * whitePoint[index] / 100;
	});
	
	return [x, y, z];
}
function XYZtoRGB(x, y, z) {
	let r =  3.240970 * x - 1.537383 * y - z * 0.498611,
		g = -0.969244 * x + 1.875968 * y + z * 0.041555,
		b =  0.055630 * x - 0.203977 * y + z * 1.056972;
	[r, g, b] = [r, g, b].map(n => {
		// リニアRGBからsRGBに変換する
		return (n <= 0.0031308) ? (n * 12.92) : (1.055 * Math.pow(n, 1 / 2.4) - 0.055);
	});
	return [r, g, b];
}
```

#補足・解説
##値域
RGBの値域は全て、0 ≦ x ≦ 1としています。
L の値域は、0 ≦ L ≦ 1、a* と b* の値域は、(sRGBから変換した場合) -0.86 ≦ a* ≦ 0.98, -1.07 ≦ b* ≦ 0.94 となります[[1](https://ja.wikipedia.org/wiki/Lab%E8%89%B2%E7%A9%BA%E9%96%93)]。
（実際に動かして色味を見てみたところ、a*がマゼンタ⇔緑、b*が黄⇔青に対応していました）

##ホワイトポイントとは？
XYZ色空間から、La*b*色空間へ変換する際に、「**ホワイトポイント(白色点)**」という言葉が現れます。これは、XYZ色空間における「白色」をどの基準点とするかによって変化します。今回は（くわしくは知らないのですが）ベタそうな**D65光源**を基準としました。他にも、D50光源やD55光源というものもあるようです[[2](https://ja.wikipedia.org/wiki/D65%E5%85%89%E6%BA%90)]。

##xyzの意味
xとy, zは、それぞれが「**虚色**」という色を示しています。~~名前がカッコいい（中二病）。~~ 虚色とは物理的に存在しない仮想の色のことで、虚数のようなものです。xyzは、それぞれ次のような意味を持っています[[3](http://rock77.fc2web.com/main/color2/color2-2-6.html)]。

- x : 明るさを持たないが、赤みを持つ
- y : 明るさと、緑みを持つ
- z : 明るさを持たないが、青みを持つ

例えば、物理的に存在する色の場合、「明るさを持たない＝黒＝無彩色」となるため、明るさを持たないのに色みを持つものはあり得ません。逆に、色みを持つならば必ず明るさを持っているのです。

XYZ色空間においては、明るさの成分を持つのが y だけなので、輝度である L へ変換するときには x や z が出てこないんですね。へー。

#おまけ
##LC*h*色空間への変換
La*b*色空間とLC*h*色空間は、直交座標と極座標系の関係に等しいです。なので、次の式で値を変換できます。

```js
let a = C * Math.cos(h);
let b = C * Math.sin(h);

let h = Math.atan2(b, a);
let C = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
```

#参考資料
- https://qiita.com/hachisukansw/items/09caabe6bec46a2a0858
- http://w3.kcua.ac.jp/~fujiwara/infosci/colorspace/colorspace2.html
- http://yamatyuu.net/other/color/cie1976lab/index.html
- https://www.awm.jp/~yoya/cache/w3.kcua.ac.jp/~fujiwara/infosci/colorspace/colorspace2.html
- http://yamatyuu.net/other/color/cie1976lab/index.html
- https://syncer.jp/color-converter