Windowsシェルプログラミングで色差（CIEDE2000） 計算電卓をつくる

タイトルの通り、使えるのはWindowsの標準機能だけという縛りを自らに課して
CIEDE2000に基づいた色差を計算するコンソール電卓アプリを作っていこうと思います。
ノンプログラマがノンプログラマ向けに書きますので冗長で見苦しくても許してください。
# そもそもCIEDE2000とは？
色の差（色差）は2つの色のユークリッド距離だと説明されることがあります。この考え方で色差を求める式はCIE1976式は、しかしヒトの知覚と必ずしも一致しないのだそうです。
[こちらのブログ記事](https://yuki-koyama.hatenablog.com/entry/2018/05/04/135347)の画像を見ていただくと一目瞭然です。
上記のようなヒトの知覚の特性を考慮して改良され、2000年に提案された計算方法がCIEDE2000（CIE delta E 2000）です。$ ΔE_{00} $ と表記することもあります。
# 参考文献
[1] The CIEDE2000 color-difference formula: Implementation notes, supplementary test data, and mathematical observations. DOI: [https://doi.org/10.1002/col.20070](https://doi.org/10.1002/col.20070)

Rochester大学のこちらのリンクからこの論文のPDFが読めます。
[https://www.hajim.rochester.edu/ece/sites/gsharma/ciede2000/ciede2000noteCRNA.pdf](https://www.hajim.rochester.edu/ece/sites/gsharma/ciede2000/ciede2000noteCRNA.pdf)
# 開発の方針
1. Powershellで書いて.NETのSystem.Mathライブラリを呼んで計算する
1. Windowsコマンドスクリプト(.cmd)に1で作成したPowershellスクリプトを埋め込む
1. IExpress.EXEアプリケーションで2のCMDファイルをEXEファイルに変換する
1. 完成！

# 1. CIEDE2000を計算するPowershellスクリプト
参考文献[1]（以下単に「論文」）に従って数式を採番します。恐ろしいことに20本ほどの数式がある。
最初Copilotに書かせたのですが標準入力から`Read-Host`するときに[float]で受けていたら論文のpair #10で計算誤差が発生したので、[double]じゃないとダメです。

- 入力を受け付ける

もちろん、引数で受けてバッチ処理に対応してもいいと思います。
```powershell:calc_CIEDE2000.ps1　入力部
# L*a*b*値の設定
# 変数を標準入力から読み取る
[double]$L1 = Read-Host "Enter value for L1"
[double]$a1 = Read-Host "Enter value for a1"
[double]$b1 = Read-Host "Enter value for b1"
[double]$L2 = Read-Host "Enter value for L2"
[double]$a2 = Read-Host "Enter value for a2"
[double]$b2 = Read-Host "Enter value for b2"
```
- $ C_{i}' $ と $ h_{i}' $ を計算する

```powershell:calc_CIEDE2000.ps1　式(2)～(7)
# /// 論文参照開始 ///
# 数式の番号は以下の論文を参照してください：
# [The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations](https://hajim.rochester.edu/ece/sites/gsharma/ciede2000/ciede2000noteCRNA.pdf)

# Equation (2)~(3) : 平均C*の計算
$C1 = [math]::Sqrt([math]::Pow($a1,2) + [math]::Pow($b1,2))
$C2 = [math]::Sqrt([math]::Pow($a2,2) + [math]::Pow($b2,2))
$C_ = ($C1 + $C2) / 2

# Equation (4) : Gの計算
$G = 0.5 * (1 - [math]::Sqrt([math]::Pow($C_,7) / ([math]::Pow($C_,7) + [math]::Pow(25,7))))

# Equation (5) : a'の計算
$a1_prime = (1 + $G) * $a1
$a2_prime = (1 + $G) * $a2

# Equation (6),(9) : C'の計算
$C1_prime = [math]::Sqrt([math]::Pow($a1_prime,2) + [math]::Pow($b1,2))
$C2_prime = [math]::Sqrt([math]::Pow($a2_prime,2) + [math]::Pow($b2,2))
$C_prime_diff = $C2_prime - $C1_prime

# Equation (7) : h'の計算
$h1_prime = [math]::Atan2($b1, $a1_prime) * 180 / [math]::PI
if ($h1_prime -lt 0) { $h1_prime += 360 }
$h2_prime = [math]::Atan2($b2, $a2_prime) * 180 / [math]::PI
if ($h2_prime -lt 0) { $h2_prime += 360 }

```
- $ ΔL' $ と $ ΔC' $ と $ ΔH' $ を計算する
```powershell:calc_CIEDE2000.ps1　式(8)～(11)
# Equation (8) : ΔL'の計算
$ΔL = $L2 - $L1

# Equation (9) : ΔC'の計算
$C_prime_diff = $C2_prime - $C1_prime

# Equation (10) : Δh'の計算
if ([math]::Abs($h1_prime - $h2_prime) -le 180) {
    $Δh_prime = $h2_prime - $h1_prime
} elseif ($h2_prime -le $h1_prime) {
    $Δh_prime = $h2_prime - $h1_prime + 360
} else {
    $Δh_prime = $h2_prime - $h1_prime - 360
}

# Equation (11) : ΔH'の計算
$ΔH_prime = 2 * [math]::Sqrt($C1_prime * $C2_prime) * [math]::Sin([math]::PI * $Δh_prime / 360)

```
- CIEDE2000色差 $ ΔE_{00} $ を計算する
```powershell:calc_CIEDE2000.ps1　式(12)～(22)
# Equation (12)~(14) : 平均L', C', H'の計算
$L_prime_avg = ($L1 + $L2) / 2
$C_prime_avg = ($C1_prime + $C2_prime) / 2

if ([math]::Abs($h1_prime - $h2_prime) -gt 180) {
    $H_prime_avg = ($h1_prime + $h2_prime + 360) / 2
} else {
    $H_prime_avg = ($h1_prime + $h2_prime) / 2
}

# Equation (15) : Tの計算
$T = 1 - 0.17 * [math]::Cos([math]::PI * ($H_prime_avg - 30) / 180) + 0.24 * [math]::Cos([math]::PI * (2 * $H_prime_avg) / 180) + 0.32 * [math]::Cos([math]::PI * (3 * $H_prime_avg + 6) / 180) - 0.20 * [math]::Cos([math]::PI * (4 * $H_prime_avg - 63) / 180)

# Equation (16) : Δθの計算
$Δθ = 30 * [math]::Exp(-[math]::Pow((($H_prime_avg - 275) / 25),2))

# Equation (17) : RCの計算
$RC = 2 * [math]::Sqrt([math]::Pow($C_prime_avg,7) / ([math]::Pow($C_prime_avg,7) + [math]::Pow(25,7)))

# Equation (18)~(20) : SL, SC, SHの計算
$SL = 1 + (0.015 * [math]::Pow(($L_prime_avg - 50),2)) / [math]::Sqrt(20 + [math]::Pow(($L_prime_avg - 50),2))
$SC = 1 + 0.045 * $C_prime_avg
$SH = 1 + 0.015 * $C_prime_avg * $T

# Equation (21) : RTの計算
$RT = -[math]::Sin(2 * [math]::PI * $Δθ / 180) * $RC

# Equation (22) : ΔE00の計算
$ΔE00 = [math]::Sqrt(([math]::Pow(($ΔL / $SL),2)) + ([math]::Pow(($C_prime_diff / $SC),2)) + ([math]::Pow(($ΔH_prime / $SH),2)) + $RT * ($C_prime_diff / $SC) * ($ΔH_prime / $SH))

# /// 論文参照終了 ///
```
- 結果及び途中計算を出力する：小数点以下4桁への丸め処理と標準出力への書き出し

論文と同じく有効数字が小数点以下4桁までになるよう適切に丸めます。

```powershell:calc_CIEDE2000.ps1　出力部
# 丸め処理
$ΔL00_round = [math]::Round($($ΔL / $SL), 4)
$ΔC00_round = [math]::Round($($C_prime_diff / $SC), 4)
$ΔH00_round = [math]::Round($($ΔH_prime / $SH), 4)
$RT_round   = [math]::Round($RT, 4)
$ΔE00_round = [math]::Round($ΔE00, 4)

# 入力された値を表示する
Write-Output ""
Write-Output "L1  : $L1"
Write-Output "a1  : $a1"
Write-Output "b1  : $b1"
Write-Output "L2  : $L2"
Write-Output "a2  : $a2"
Write-Output "b2  : $b2"

# 途中計算を表示する
Write-Output "ΔL00: $ΔL00_round"
Write-Output "ΔC00: $ΔC00_round"
Write-Output "ΔH00: $ΔH00_round"
Write-Output "R_T : $RT_round"
Write-Output "ΔE00: $ΔE00_round"

pause # デバッグ用
```

# 2. PowershellスクリプトをCMDファイルに埋め込む
既によい記事があります。私は[こちらの記事](https://qiita.com/zetamatta/items/694a7234e82c89959a03)を参考にさせていただきました。

```batchfile:shebang.cmd
@set "args=%*"
@powershell "iex((@('')*3+(cat '%~f0'|select -skip 3))-join[char]10)"
@exit /b %ERRORLEVEL%

Write-Output "Hello, World!"
```
その他に[こんなやりかた](https://qiita.com/earthdiver1/items/cab769aad623a03a0f2d)でもいいみたいなのですが、あいにく私には読めず。。

# 3. IExpress.EXEでCMDをEXEに変換する
Windows標準搭載のIExpress.EXEを使います。
生成されたEXE形式の実行ファイルをダブルクリックすると`%tmp%`つまり`%userprofile%\Appdata\Local\Temp`に`IEX001.tmp`みたいな名前の一時フォルダができてスクリプトが走る仕組みになっています。
複数ファイルを選択することができるのでハッシュ値計算バッチファイルも一緒にパッケージ化したのですが、上記の挙動を理解していなかったので少しつまずきました。

# 4. 完成！
誰が使うんだろこれ。
論文PDFの24ページTABLE 1に掲載の34組の $ (L_1, a_1, b_1) $と$ (L_2, a_2, b_2) $ を電卓で計算してみて計算結果がちゃんとしていることを確認して使用します。Pair #10があってれば他はあってるんじゃないかという気がしますが、一応参考までに電卓が誤った結果を返した場合の解釈は下表のとおりです。なお論文にはPair #1～#6の意図の部分にHueとChromaの誤記がある。

|pair#  |テストの意図と解釈                                       |
|-------|-------------------------------------------------------|
|1-3    |色相の差$ ΔH $は負の量を扱える                           |
|4-6    |彩度の差$ ΔC $は負の量を扱える                           |
|7-16   |(7)式の$ \tan ^{-1} $が正しく計算されている              |
|7-16   |(14)式の平均色相$ \dfrac{}{h}, $が正しく計算されている    |
|17-20  |実務上意味のないほどデカい色差を扱っても電卓がバグらない    |
|25-34  |CIE technical reportとthe article by Luo, Cui and Rigg |

# 終わりに
この記事の一番のミソはcmdへのpwsh埋め込み→IExpressによるexe化なのでは。
Windowsに標準でこういうのが組み込まれてるなんて知らなかった。
それでは皆様よき色彩工学ライフをお過ごしください。