色弱色差シミュレータを作ってみた

オープンソースでウェブ用の色弱色差シミュレータが無かったので作った。
===
リポジトリ　　https://github.com/Kahiro-M/DichroSim
お試しページ　https://dichrosim.web.app/

##### 2019/05/26追記
色差の程度を表示する機能を追加し、解説記事を追記しました。

##### 2018/11/10追記
色差計算のライブラリに問題がないことを確認したので、再度公開いたします。
問題の詳細は以下をご確認ください。
https://github.com/Kahiro-M/DichroSim/issues/1


##色弱？
かつては色盲と呼ばれたり、二色型色覚異常などの医学的な呼び方もあるが、ここではカラーユニバーサルデザイン機構で呼称されている呼び方（色弱）で統一する。

## どうやって使う？
color1,2のそれぞれに色度値を入力し、最上部のボタンを押すと、二色間の色差が表示される。
色差は各色覚タイプの場合を考慮して算出している。

色覚タイプはそれぞれ……

 - C型:正常色覚。日本人男性の95%はこのタイプ。自分もこのタイプ。
 - P型:L錐体の働きが弱い、あるいは機能していないタイプ。赤と緑の違いが分かりづらい。
 - D型:M錐体の働きが弱い、あるいは機能していないタイプ。P型同様、赤と緑の違いがわかりづらいが、色の組み合わせが少し異なる。

## どんな時に使う？
 - 測色のノウハウはあるが、色弱対応のノウハウがない！
 - カラーユニバーサルデザインに対応したいが、どんな色を使っていいかわからない！
 - 色彩工学設計者に色弱者の色の見え方を伝えたい！
 - 
ざっと、こんな場合。
内包しているライブラリからL*a*b*への変換スクリプトだけ抜き出しても良い。
（自分自身、半分はその目的で作ったので。）

## 使い方
Color1とColor2にそれぞれ値を入力し、画面上部の「色差計算」ボタンをクリック。
各色覚タイプでの色差とJIS規格で規定されている「色の違いの程度」が表示される。
![使用例.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310125/1b61e6e7-d440-7cb1-e728-3b7c1cad52f3.png)



## 解説
### 処理の流れ
1. 二色の色度値であるxyY値を取得(htmlのfromから取得)
2. 色度値をXYZ値に変換(XYZとxyYの関係は[CIE1931色空間(wiki)](https://ja.wikipedia.org/wiki/CIE_1931_%E8%89%B2%E7%A9%BA%E9%96%93)を参照)
3. XYZ値を錐体反応値に変換(CIE測色標準観察者の錐体分好感度に基づく)
4. 各色覚タイプの錐体反応値へ変換[^1](P型、D型色覚者の色の見え方に変換)
[^1]: Hans Brettle, Francoise Vienot, John Deutan. Mollon：Computerized simulation of color appearance for dichromats：J.Opt.Soc.Am.A, Vol.14, No.10, pp. 2647-2655 (1997)
5. 錐体反応値からXYZ値に逆変換
6. XYZ値からL*a*b*に変換([Lab色空間(wiki)](https://ja.wikipedia.org/wiki/Lab%E8%89%B2%E7%A9%BA%E9%96%93#CIE_XYZ_%E3%81%8B%E3%82%89%E3%81%AE%E5%A4%89%E6%8F%9B))
7. 各タイプのL*a*b*から色差を算出([CIE DE 2000](https://qiita.com/hachisukansw/items/860f061a2ab7a4f2d06f))
8. 色差を[日本工業規格](https://www.toyoink1050plus.com/color/chromatics/basic/005.php)に基づき程度を判定

2~7の処理はDichroSimライブラリとしてまとめている。



### 二色の色度値であるxyY値を取得(htmlのfromから取得)

```.js
//htmlから引数を取得
var Color1 = new ColorData();

//setColorData(x,y,Y)でColorDataオブジェクトにメンバを代入
Color1 = setColorData(	document.formInput.x値を格納している要素の名称.value,
          document.formInput.y値を格納している要素の名称.value,
          document.formInput.Y値を格納している要素の名称.value	);
```

### 色度値をXYZ値に変換
```.js
//色度値とXYZ値の変換のためのオブジェクトを用意
var culcXYZ = new CulcXYZ;

//色度値をXYZ値に変換
var commonXYZColor1 = culcXYZ.makeXYZMatrix(Color1);
```

### XYZ値を錐体反応値に変換
```.js
//錐体反応値や各色覚タイプへの変換するためのオブジェクトを用意
var beDichromat = new DichromatTrans;

//XYZ値をLMS値に変換
var lmsColor1 = beDichromat.transXYZtoLMS(commonXYZColor1);
```

### 各色覚タイプの錐体反応値へ変換
```.js
//LMS値をProtanLMS値に変換
var protanLMSColor1 = beDichromat.transLMStoProtan(lmsColor1);

//LMS値をDeutanLMS値に変換
var deutanLMSColor1 = beDichromat.transLMStoDeutan(lmsColor1);

```

### 錐体反応値からXYZ値に逆変換
```.js
//ProtanLMS値をProtanXYZ値に変換
var protanXYZColor1 = beDichromat.transLMStoXYZ(protanLMSColor1);

//DeutanLMS値をDeutanXYZ値に変換
var deutanXYZColor1 = beDichromat.transLMStoXYZ(deutanLMSColor1);
```

### XYZ値からL*a*b*に変換
```.js
//////色覚タイプごとの色差計算処理//////
//LMS値をLab値に変換(C型での色差計算用)
var commonLabColor1 = transXYZtoLab(commonXYZColor1);

//LMS値をLab値に変換(P型での色差計算用)
var protanLabColor1 = transXYZtoLab(protanXYZColor1);

//LMS値をLab値に変換(D型での色差計算用)
var deutanLabColor1 = transXYZtoLab(deutanXYZColor1);
```

### 各タイプのL*a*b*から色差を算出
```.js
//CIE delta E 2000
document.formInput.C型の結果表示.value = 
          ciede2000(commonLabColor1.get([0]),commonLabColor1.get([1]),commonLabColor1.get([2]),
                    commonLabColor2.get([0]),commonLabColor2.get([1]),commonLabColor2.get([2]));

document.formInput.P型の結果表示.value = 
          ciede2000(protanLabColor1.get([0]),protanLabColor1.get([1]),protanLabColor1.get([2]),
                    protanLabColor2.get([0]),protanLabColor2.get([1]),protanLabColor2.get([2]));

document.formInput.D型の結果表示.value = 
          ciede2000(deutanLabColor1.get([0]),deutanLabColor1.get([1]),deutanLabColor1.get([2]),
                    deutanLabColor2.get([0]),deutanLabColor2.get([1]),deutanLabColor2.get([2]));

```

### 色差を日本工業規格に基づき程度を判定
```.js
discriminateDeltaE(document.formInput.C型の結果表示.value,
                   document.getElementById("判定結果を表示する場所"));

discriminateDeltaE = function(resultFormInput,displayResultElement){
	if((Number(resultFormInput))<=THRESHOLD_NON_COLORIMETRY_AREA){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_NON_COLORIMETRY_AREA;
		displayResultElement.style.backgroundColor = COLOR_NON_COLORIMETRY_AREA;
	}
	else if(Number(resultFormInput)<=THRESHOLD_IDENTIFICATION_COLOR_DIFFERENCE){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_IDENTIFICATION_COLOR_DIFFERENCE;
		displayResultElement.style.backgroundColor = COLOR_IDENTIFICATION_COLOR_DIFFERENCE;
	}
	else if(Number(resultFormInput)<=THRESHOLD_AAA){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_AAA;
		displayResultElement.style.backgroundColor = COLOR_AAA;
	}
	else if(Number(resultFormInput)<=THRESHOLD_AA){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_AA;
		displayResultElement.style.backgroundColor = COLOR_AA;
	}
	else if(Number(resultFormInput)<=THRESHOLD_A){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_A;
		displayResultElement.style.backgroundColor = COLOR_A;
	}
	else if(Number(resultFormInput)<=THRESHOLD_B){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_B;
		displayResultElement.style.backgroundColor = COLOR_B;
	}
	else if(Number(resultFormInput)<=THRESHOLD_C){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_C;
		displayResultElement.style.backgroundColor = COLOR_C;
	}
	else if(Number(resultFormInput)<=THRESHOLD_D){
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_D;
		displayResultElement.style.backgroundColor = COLOR_D;
	}
	else{
		displayResultElement.innerHTML = "";
		displayResultElement.innerHTML = STRING_OTHER_COLOR;
		displayResultElement.style.backgroundColor = COLOR_OTHER_COLOR;
	}
}

//特別に調整された測色器械でも誤差の範囲にあり、人では識別不能
const THRESHOLD_NON_COLORIMETRY_AREA = 0.2;

//十分に調整された測色器械の再現精度の範囲で、訓練を積んだ人が再現性を持って識別できる限界
const THRESHOLD_IDENTIFICATION_COLOR_DIFFERENCE = 0.3;

//	目視判定の再現性からみて、厳格な許容色差の規格を設定できる限界
const THRESHOLD_AAA = 0.4;

//色の隣接比較で、わずかに色差が感じられるレベル。一般の測色器械間の誤差を含む許容色差の範囲	
const THRESHOLD_AA = 0.8;

//色の離間比較では、ほとんど気付かれない色差レベル。一般的には同じ色だと思われているレベル	
const THRESHOLD_A = 1.6;

//印象レベルでは同じ色として扱える範囲。塗料業界やプラスチック業界では色違いでクレームになることがある	
const THRESHOLD_B = 3.2;

//ＪＩＳ標準色票、マンセル色票等の１歩度に相当する色差	
const THRESHOLD_C = 6.5;

//細分化された系統色名で区別ができる程度の色の差で、この程度を超えると別の色名のイメージになる	
const THRESHOLD_D = 13.0;

//完全に別の色
const THRESHOLD_OTHER_COLOR = 25.0;


// 結果表示に使用する文字列
const STRING_NON_COLORIMETRY_AREA = "評価不能領域";
const STRING_IDENTIFICATION_COLOR_DIFFERENCE = "識別限界";
const STRING_AAA = "AAA級許容差";
const STRING_AA = "AA級許容差";
const STRING_A = "A級許容差";
const STRING_B = "B級許容差";
const STRING_C = "C級許容差";
const STRING_D = "D級許容差";
const STRING_OTHER_COLOR = "別の色";


// 結果表示に使用する色
const COLOR_NON_COLORIMETRY_AREA = "#FF1F1F";
const COLOR_IDENTIFICATION_COLOR_DIFFERENCE = "#E33B3B";
const COLOR_AAA = "#C75757";
const COLOR_AA = "#AB7373";
const COLOR_A = "#8F8F8F";
const COLOR_B = "#73ABAB";
const COLOR_C = "#57C7C7";
const COLOR_D = "#3BE3E3";
const COLOR_OTHER_COLOR = "#1FFFFF";
```



## 使用上の注意
ライブラリ自体は入力値のテストをしないため、スペクトル軌跡の範囲内に収まっているかはライブラリを使用する側で確認する必要がある。
スペクトル軌跡外の数値を入力して、その値を鵜呑みにしてはいけない。（戒め）


## シミュレータとは言うが、表示できるのは光学的な数値だけとは、何と情けない。
シミュレータというと、あたかもその状況が再現されるものを想像すると思うが、このシミュレータはそこまでできない。
自分の技術不足なので、今後はそこを改善したい。

### 言い訳をさせてもらうと……
色を表示するディスプレイはデバイスごとに固有のクセ（ガンマ値、最大輝度、コントラスト比、ユーザごとの出力モード）がある。
これらのクセを受け止めて統一した光学的な出力をすることができない。
なので、今の自分の技術ではデバイスごとの出力の手前までのシミュレーションしかできなかった……
ちなみに、このクセを測って補正することをキャリブレーションという。