色当てゲームの勝敗判定をどう作るか
RGB距離、OKLab距離、CIEDE2000を実装して、色の近さをゲームの勝敗判定として扱う方法を整理したメモ。時間短縮のため大部分をAIで生成しました。後で読み返します。
この記事は、時間短縮のため大部分をAIで生成しました。後で読み返しながら、自分用の備忘録として整えていきます。
親が想像した色を、子が予想して当てる「色当てゲーム」を作っている。
ルールはシンプル。
- 親が頭の中で色をイメージする
- 親がGUIで正解色を作る
- 子もGUIで予想色を作る
- 正解色と各回答色の距離を計算する
- もっとも近い色を出した子が勝つ
最初は、色を #FFFFFF のようなHEXカラーとして扱い、RGB距離で勝敗判定していた。
ただ、実装しているうちに気になった。
RGBの数値が近いことと、人間が見て「近い色だ」と感じることは、本当に同じなのか?
この記事は、その疑問から始まった調査と実装の備忘録である。
最終的には、JavaScriptの色変換・色差ライブラリである culori を使い、次の3方式を切り替えられるようにした。
differenceEuclidean("oklab")
differenceEuclidean("rgb")
differenceCiede2000()
デフォルト判定は OKLab距離。RGB距離とCIEDE2000は、比較・学習用として残した。
目次
- このゲームで本当に測りたいもの
- 入力色はsRGBとして扱う
- なぜRGB距離だけでは不安なのか
- LabとDelta Eの考え方
- OKLabとは何か
- culoriで3方式を実装する
- スコア化の設計
- ランキングはraw distanceで決める
- 判定方式を切り替えられるようにした
- 方式差が出やすいエッジケース
- エッジケースをテストデータとして残す
- OKLab距離をデフォルトにした理由
- 実装上の注意点
- 今回の結論
- 参考資料
このゲームで本当に測りたいもの
このゲームで測りたいのは、厳密には「RGB値の差」ではない。
測りたいのは、プレイヤーが結果を見たときに、
たしかに、この子の色が一番近いね。
と納得できる近さである。
色の近さには、いくつかの見方がある。
| 近さの種類 | 意味 | ゲームでの重要度 |
|---|---|---|
| RGB数値上の近さ | R/G/Bの数値差が小さい | 低〜中 |
| 物理的な近さ | 光の波長分布や反射率が近い | 低 |
| 測色上の近さ | LabやDelta Eなどで定義される色差 | 中〜高 |
| 見た目の近さ | 人間が見て似ていると感じる | 高 |
このゲームは、塗料や印刷物を測定するゲームではない。Webアプリ上のGUIで作った色を比較するゲームである。
つまり、必要なのは「Web UI上に表示された色同士を見比べたときの納得感」だ。
ここを間違えると、数式としては正しくても、ゲームとしては気持ち悪い結果になる。
入力色はsRGBとして扱う
このゲームでは、親も子もGUIで色を作る。アプリ内では、色を次のようなRGB値として持っている。
{
red: 0-255,
green: 0-255,
blue: 0-255
}
見た目としては、#FFFFFF のようなHEXカラーに近い。
CSS Color Module Level 4 では、#RRGGBB のようなHEXカラー記法は、sRGBの各成分を16進数で指定するものとして扱われる。つまり、このゲームの入力色は基本的に sRGBのコード値 として考えればよい。
ただし、ここで注意が必要。
#3FA7F5
これは、見た目の明るさ・色相・鮮やかさを直接表す値ではない。sRGBの赤・緑・青のコード値である。
たとえば、#3FA7F5 は次のように分解できる。
R = 0x3F = 63
G = 0xA7 = 167
B = 0xF5 = 245
アプリ内の値をculoriに渡すときは、0〜255を0〜1に正規化する。
{
mode: "rgb",
r: red / 255,
g: green / 255,
b: blue / 255
}
ここで得られる r, g, b は、あくまでsRGBの値である。OKLab距離やCIEDE2000を使う場合は、このRGB値をそのまま距離計算するのではなく、ライブラリに色空間変換を任せる。
なぜRGB距離だけでは不安なのか
RGB距離は、RGB空間上のユークリッド距離である。
dRGB = sqrt(
(R1 - R2)^2 +
(G1 - G2)^2 +
(B1 - B2)^2
)
0〜1に正規化したRGBなら、最大距離は黒 #000000 から白 #FFFFFF までの距離で、sqrt(3) になる。
RGB距離は実装が簡単で、直感的にも分かりやすい。しかし、RGB距離が測っているのは、あくまでRGBコード値の差である。
人間の目が感じる明るさ、色味、鮮やかさの差を均等に表しているわけではない。
たとえば、白 #FFFFFF に対して次の3色を比べる。
#FFFF00 yellow
#00FFFF cyan
#FF00FF magenta
RGB距離では、どれも1チャンネルだけが255違う。
| 比較 | 違うチャンネル |
|---|---|
#FFFFFF → #FFFF00 | Bだけ違う |
#FFFFFF → #00FFFF | Rだけ違う |
#FFFFFF → #FF00FF | Gだけ違う |
そのため、RGB距離では全部同点になる。
でも、見た目として本当に同じだけ白から離れているように感じるかというと、かなり怪しい。
これがRGB距離の限界である。
LabとDelta Eの考え方
RGB距離の弱点を避けるために出てくるのが、Lab色空間やDelta Eである。
Labは、ざっくり言えば、人間の色の見え方に寄せて作られた色空間である。RGBが赤・緑・青の量で色を表すのに対して、Labはおおまかに次の成分で色を表す。
| 成分 | 意味 |
|---|---|
| L* | 明るさ |
| a* | 緑 ↔ 赤 |
| b* | 青 ↔ 黄 |
Delta Eは、Labなどの色空間をもとに、2つの色の差を数値化する考え方である。
代表的なものに、次の3つがある。
- ΔE76
- ΔE94
- CIEDE2000 / ΔE00
ΔE76は、Lab空間上の単純なユークリッド距離。ΔE94は、明度・彩度・色相の差に補正を入れたもの。CIEDE2000はさらに補正を増やし、人間が感じる色差との相関を高めようとした色差式である。
CIEDE2000は本格的で、色差評価としてはかなり有力。ただし、式は複雑で、自前実装するには向いていない。
このゲームでは、culoriの differenceCiede2000() に任せている。
OKLabとは何か
今回のデフォルト判定にしたのが、OKLab距離である。
OKLab は、Björn Ottossonによって提案された知覚寄りの色空間である。OKLabは、知覚的な明るさ・彩度・色相を扱いやすくしつつ、画像処理や実装で扱いやすいシンプルな色空間として設計されている。
OKLabは、Labと同じように3成分で色を表す。
| 成分 | 意味 |
|---|---|
| L | 知覚的な明るさ |
| a | 緑 ↔ 赤方向 |
| b | 青 ↔ 黄方向 |
OKLab距離は、このOKLab空間上のユークリッド距離である。
dOKLab = sqrt(
(L1 - L2)^2 +
(a1 - a2)^2 +
(b1 - b2)^2
)
考え方はかなりシンプル。
- RGBをOKLabに変換する
- OKLab空間上で距離を測る
- 距離が一番小さい子が勝つ
プレイヤー向けにも説明しやすい。
RGBの数字の差ではなく、人間の見た目に近くなるよう設計されたOKLab空間で、正解色との距離が一番小さい回答を勝ちにしています。
ゲームとしては、この説明しやすさが大きい。
culoriで3方式を実装する
今回の実装では、色差計算にculoriを使っている。
culoriのAPIには、色差を測るための関数群が用意されている。differenceEuclidean(mode) は指定した色空間上のユークリッド距離を返す関数を作り、differenceCiede2000() はCIEDE2000系の色差を計算する関数を返す。
実装した判定方式は3つ。
differenceEuclidean("oklab")
differenceEuclidean("rgb")
differenceCiede2000()
OKLab距離
differenceEuclidean("oklab")
RGBをOKLab色空間に変換してから、OKLab上の距離で近さを判定する。現在のデフォルト判定。
RGB距離
differenceEuclidean("rgb")
RGBを0〜1に正規化したうえで、RGB空間上のユークリッド距離で判定する。以前の判定に近いが、自前計算ではなくculori経由にした。
Delta E
differenceCiede2000()
CIEDE2000系の色差で判定する。より標準的な知覚色差に近いが、ゲームの説明としてはやや専門的。
スコア化の設計
距離そのものをUIに出しても、プレイヤーには分かりづらい。
そこで、距離を100点満点に変換している。
score = Math.max(0, Math.round((1 - distance / maxDistance) * 100));
意味はこう。
| 距離 | 点数 |
|---|---|
distance = 0 | 100点 |
| distanceが小さい | 高得点 |
| distanceが大きい | 低得点 |
| 最大距離以上 | 0点 |
最大距離は方式ごとに変えている。
| 方式 | スコア化の基準 |
|---|---|
| OKLab距離 | 1 |
| RGB距離 | Math.sqrt(3) |
| Delta E | 111.42 |
RGB距離の Math.sqrt(3) は分かりやすい。RGBを0〜1で扱うと、黒から白までの対角線距離が最大になるからである。
OKLab距離の 1 は、スコア化のための実用的な基準値として扱っている。
Delta Eの 111.42 は、sRGB内の代表的な最大級の距離として使っている。ただし、これは厳密な理論上限として扱わない方がよい。CIEDE2000はRGBキューブの対角線のように単純な最大距離を持つ感覚では扱いにくく、111.42 を超える組み合わせもあり得る。
その場合は、今の式だと0点に丸められる。
Math.max(0, ...)
ゲーム用途では、最大級に遠い色は0点、という扱いで十分自然だと思う。
ランキングはraw distanceで決める
ここはかなり重要。
スコアは Math.round() で丸めている。そのため、距離が少し違っていても、表示点数は同じになることがある。
たとえば、AもBも75点でも、内部距離ではAの方がわずかに近い、というケースがある。
したがって、実装上は分けるべき。
| 用途 | 使う値 |
|---|---|
| ランキング判定 | raw distance |
| UI表示 | score |
これはゲームの納得感にも関わる。
もし表示点数が同じなのに順位が違うなら、UI上で「僅差」「ほぼ同点」「距離ではAがわずかに近い」のように補足してもよい。
判定方式を切り替えられるようにした
見出しの「判定: OKLab距離」はボタン化している。
タップすると、次の順で切り替わる。
OKLab距離 → RGB距離 → Delta E
切り替えると、ランキング計算とスコア計算もその方式で再計算する。
これは、単に機能として便利なだけではない。学習用としてかなり面白い。
同じ正解色、同じ回答色でも、方式によって勝者が変わる。
RGB距離ではAが勝つ
OKLab距離ではBが勝つ
Delta EではCが勝つ
色距離の違いを、ゲームの中で体験できるようになった。
方式差が出やすいエッジケース
ここからは、方式差が出やすい具体例。
数値は、次の前提での距離とスコアである。
OKLab距離: differenceEuclidean("oklab")
RGB距離: differenceEuclidean("rgb")
Delta E: differenceCiede2000()
score = Math.max(0, Math.round((1 - distance / maxDistance) * 100))
ケース1: 白に対する黄色・シアン・マゼンタ
target: #FFFFFF
A: #FFFF00 yellow
B: #00FFFF cyan
C: #FF00FF magenta
| 候補 | RGB距離 / 点 | OKLab距離 / 点 | CIEDE2000 / 点 |
|---|---|---|---|
A #FFFF00 | 1.0000 / 42 | 0.2134 / 79 | 30.52 / 73 |
B #00FFFF | 1.0000 / 42 | 0.1812 / 82 | 25.50 / 77 |
C #FF00FF | 1.0000 / 42 | 0.4393 / 56 | 42.21 / 62 |
| 方式 | 勝者 |
|---|---|
| RGB距離 | A/B/C 同点 |
| OKLab距離 | B #00FFFF |
| CIEDE2000 | B #00FFFF |
RGBでは、白から見てどれも1チャンネルだけが最大差なので同点になる。しかし、OKLabやCIEDE2000では差が出る。
これは、RGB距離の限界を説明するのにとても分かりやすい。
ケース2: 黒に対する赤・緑・青
target: #000000
A: #FF0000 red
B: #00FF00 green
C: #0000FF blue
| 候補 | RGB距離 / 点 | OKLab距離 / 点 | CIEDE2000 / 点 |
|---|---|---|---|
A #FF0000 | 1.0000 / 42 | 0.6788 / 32 | 50.41 / 55 |
B #00FF00 | 1.0000 / 42 | 0.9152 / 8 | 87.86 / 21 |
C #0000FF | 1.0000 / 42 | 0.5499 / 45 | 39.68 / 64 |
| 方式 | 勝者 |
|---|---|
| RGB距離 | A/B/C 同点 |
| OKLab距離 | C #0000FF |
| CIEDE2000 | C #0000FF |
RGB距離では、黒から赤・緑・青はすべて同じ距離になる。だが、見た目の明るさや知覚上の差まで考えると、同じ扱いにはならない。
特に緑は黒からかなり遠く出る。
ケース3: グレーに対する赤寄り・緑寄り・青寄り
target: #808080
A: #FF8080 red tint
B: #80FF80 green tint
C: #8080FF blue tint
| 候補 | RGB距離 / 点 | OKLab距離 / 点 | CIEDE2000 / 点 |
|---|---|---|---|
A #FF8080 | 0.4980 / 71 | 0.2120 / 79 | 28.21 / 75 |
B #80FF80 | 0.4980 / 71 | 0.3609 / 64 | 39.82 / 64 |
C #8080FF | 0.4980 / 71 | 0.1937 / 81 | 27.95 / 75 |
| 方式 | 勝者 |
|---|---|
| RGB距離 | A/B/C 同点 |
| OKLab距離 | C #8080FF |
| CIEDE2000 | C #8080FF |
RGBでは、どれも1チャンネルだけ同じ量ずれているので同点。しかし、OKLabとCIEDE2000では緑方向のズレがより大きく扱われる。
この例は、実際のゲームでも起こりやすい。親がグレーっぽい色を作り、子が少し赤寄り・緑寄り・青寄りに外すようなケースである。
なお、この例ではCIEDE2000のAとCがどちらも75点になる。しかしraw distanceではCの方がわずかに近い。
つまり、表示点数は同じでも内部順位は違う、というUI上の注意点も見える。
ケース4: OKLabとCIEDE2000が強く反転する
target: #FF00FF
A: #000000 black
B: #00E040 green-ish
| 候補 | RGB距離 / 点 | OKLab距離 / 点 | CIEDE2000 / 点 |
|---|---|---|---|
A #000000 | 1.4142 / 18 | 0.7722 / 23 | 56.71 / 49 |
B #00E040 | 1.5273 / 12 | 0.5761 / 42 | 106.20 / 5 |
| 方式 | 勝者 |
|---|---|
| RGB距離 | A #000000 |
| OKLab距離 | B #00E040 |
| CIEDE2000 | A #000000 |
このケースはかなり面白い。
OKLab距離では、Bの方がマゼンタに近い。CIEDE2000では、Bはかなり遠く、黒の方が近い。
つまり、OKLab距離とCIEDE2000は、どちらも知覚寄りではあるが、同じものではない。
OKLab距離は、OKLab空間上のシンプルな直線距離。CIEDE2000は、Lab系の色差に複数の補正を入れた式。
特に高彩度色や補色に近い組み合わせでは、順位が変わりやすい。
ケース5: 3方式がすべて別の勝者を選ぶ
target: #E649C3
A: #298F04 dark green
B: #65EF3F bright green
C: #072A2E dark cyan
| 候補 | RGB距離 / 点 | OKLab距離 / 点 | CIEDE2000 / 点 |
|---|---|---|---|
A #298F04 | 1.0889 / 37 | 0.4183 / 58 | 87.08 / 22 |
B #65EF3F | 0.9735 / 44 | 0.4984 / 50 | 95.83 / 14 |
C #072A2E | 1.0588 / 39 | 0.4774 / 52 | 42.54 / 62 |
| 方式 | 勝者 |
|---|---|
| RGB距離 | B #65EF3F |
| OKLab距離 | A #298F04 |
| CIEDE2000 | C #072A2E |
これは、判定方式の違いを見せる教材としてかなり良い。
同じtarget、同じanswersでも、3方式で勝者が全部違う。
RGB距離は、RGB成分の差が小さいBを選ぶ。OKLab距離は、OKLab空間上で近いAを選ぶ。CIEDE2000は、色差式上で近いCを選ぶ。
このような例を見ると、色距離は単なる技術選定ではなく、ゲームルールそのものだと分かる。
ケース6: RGBでは遠いが、CIEDE2000では近い
target: #4D9433
A: #771764 purple
B: #EF303B red
C: #E3FABC pale yellow-green
| 候補 | RGB距離 / 点 | OKLab距離 / 点 | CIEDE2000 / 点 |
|---|---|---|---|
A #771764 | 0.5517 / 68 | 0.3598 / 64 | 75.65 / 32 |
B #EF303B | 0.7472 / 57 | 0.3169 / 68 | 67.55 / 39 |
C #E3FABC | 0.8914 / 49 | 0.3613 / 64 | 30.88 / 72 |
| 方式 | 勝者 |
|---|---|
| RGB距離 | A #771764 |
| OKLab距離 | B #EF303B |
| CIEDE2000 | C #E3FABC |
CはRGB距離では一番遠い。しかしCIEDE2000では一番近い。
targetは緑系で、Cは明るい黄緑系。RGB成分だけ見ると大きく離れているが、色味の系統としては近い。
これは、RGB数値上は遠いが、見た目・色味としては近い、という例として使いやすい。
エッジケースをテストデータとして残す
実装のテストやデバッグには、こういうケースをそのまま使える。
const edgeCases = [
{
name: "RGB tie on white, perceptual methods prefer cyan",
target: "#FFFFFF",
answers: [
{ id: "A", color: "#FFFF00" },
{ id: "B", color: "#00FFFF" },
{ id: "C", color: "#FF00FF" },
],
expected: {
rgb: ["A", "B", "C"],
oklab: ["B"],
ciede2000: ["B"],
},
},
{
name: "RGB tie on black, perceptual methods prefer blue",
target: "#000000",
answers: [
{ id: "A", color: "#FF0000" },
{ id: "B", color: "#00FF00" },
{ id: "C", color: "#0000FF" },
],
expected: {
rgb: ["A", "B", "C"],
oklab: ["C"],
ciede2000: ["C"],
},
},
{
name: "RGB tie on gray, perceptual methods penalize green tint",
target: "#808080",
answers: [
{ id: "A", color: "#FF8080" },
{ id: "B", color: "#80FF80" },
{ id: "C", color: "#8080FF" },
],
expected: {
rgb: ["A", "B", "C"],
oklab: ["C"],
ciede2000: ["C"],
},
},
{
name: "OKLab and CIEDE2000 disagree strongly",
target: "#FF00FF",
answers: [
{ id: "A", color: "#000000" },
{ id: "B", color: "#00E040" },
],
expected: {
rgb: ["A"],
oklab: ["B"],
ciede2000: ["A"],
},
},
{
name: "All three methods choose different winners",
target: "#E649C3",
answers: [
{ id: "A", color: "#298F04" },
{ id: "B", color: "#65EF3F" },
{ id: "C", color: "#072A2E" },
],
expected: {
rgb: ["B"],
oklab: ["A"],
ciede2000: ["C"],
},
},
{
name: "RGB says purple, OKLab says red, CIEDE2000 says pale yellow-green",
target: "#4D9433",
answers: [
{ id: "A", color: "#771764" },
{ id: "B", color: "#EF303B" },
{ id: "C", color: "#E3FABC" },
],
expected: {
rgb: ["A"],
oklab: ["B"],
ciede2000: ["C"],
},
},
];
こういうテストケースを持っておくと、後から実装を変えたときに確認しやすい。
- 意図した判定方式で順位が出ているか
- RGB距離に戻ってしまっていないか
- 丸めたスコアで順位を決めていないか
OKLab距離をデフォルトにした理由
最終的に、デフォルト判定はOKLab距離にした。
理由は、CIEDE2000が悪いからではない。むしろ、CIEDE2000は色差評価としてはかなり有力である。
ただ、このゲームは工業製品の色差検査ではない。
必要なのは、次のバランスである。
- プレイヤーが見て納得しやすい
- 説明しやすい
- 実装しやすい
- 保守しやすい
その観点では、OKLab距離がちょうどよい。
| 観点 | OKLab距離 |
|---|---|
| RGB距離より見た目に寄るか | 寄る |
| CIEDE2000より説明しやすいか | 説明しやすい |
| 実装しやすいか | culoriで簡単 |
| 計算コストは問題か | 問題にならない |
| ゲームルールとして説明できるか | できる |
| 学習用に比較できるか | できる |
プレイヤー向けには、これで十分だと思う。
このゲームでは、RGBの数字の差ではなく、人間の見た目に近くなるよう設計されたOKLab色空間で、正解色との距離が一番小さい回答を勝ちにしています。
開発者向けには、こう。
アプリ内のRGB値をculoriのrgb形式に変換し、
differenceEuclidean("oklab")で距離を計算する。ランキングはraw distanceで決め、scoreは表示用に100点化する。
実装上の注意点
透明色は扱わない
このゲームでは、alphaは扱わない方がよい。
半透明色は背景によって見た目が変わる。
rgba(255, 0, 0, 0.5)
これは、白背景では薄い赤に見えるし、黒背景では暗い赤に見える。
つまり、透明色を許すと「色そのものの距離」ではなく「背景と合成した後の色の距離」を測る必要が出てくる。
ゲームルールが複雑になるので、まずは不透明色だけでよい。
距離の数値スケールを混ぜない
RGB距離、OKLab距離、CIEDE2000は、数値のスケールが違う。
| 方式 | スケール感 |
|---|---|
| RGB距離 | 最大 sqrt(3) 前後 |
| OKLab距離 | 今回は 1 を基準にスコア化 |
| CIEDE2000 | 100前後まで大きくなる |
そのため、OKLab距離 0.3 と CIEDE2000 30 を見て、どちらが「より近い方式」なのかを直接比較してはいけない。
比較するなら、同じ方式の中で比較する。
- OKLab距離でAとBを比較する
- CIEDE2000でAとBを比較する
という形にする。
今回の結論
このゲームで気にしていたことは、単なる色差計算ではなかった。
本当に気にしていたのは、次のことだった。
- 勝敗判定として納得できるか
- プレイヤーに説明できるか
- 実装として安全か
- 後から読み返して理解できるか
RGB距離は簡単だが、人間の見た目とはズレやすい。CIEDE2000は標準的な色差として有力だが、ゲームルールとしては少し重い。OKLab距離は、その中間のちょうどよい場所にある。
だから、今回の最終方針はこう。
- 入力はアプリ内RGB値
- culori用のrgb形式に変換
- デフォルトは
differenceEuclidean("oklab") - ランキングはraw distanceで決める
- scoreは表示用に100点化する
- RGB距離とCIEDE2000は学習・比較用に残す
実装としてはこれで十分シンプル。でも、切り替えて比べると、色距離の考え方の違いがちゃんと見える。
このゲームは、色を当てるゲームであると同時に、
色の近さとは何か?
を体験できる小さな教材にもなった。
参考資料
| 資料 | 参照した内容 |
|---|---|
| W3C CSS Color Module Level 4 | #RRGGBB がsRGB成分を指定するHEX記法であること、CSSにおける色空間の扱い。 |
| culori API Reference | differenceEuclidean()、differenceCiede2000() などの色差関数の使い方。 |
| Björn Ottosson, “A perceptual color space for image processing” | OKLabの設計目的、知覚的な明るさ・彩度・色相を扱うための色空間という位置づけ。 |
| CIE, “Colorimetry - Part 6: CIEDE2000 Colour-Difference Formula” | CIEDE2000の位置づけ、CIE 142-2001に基づく色差式であること。 |