i want hue

データサイエンティストのための色。 最適に区別できる色のパレットを生成し、調整します。

コンセプト:

色を知覚的に一貫した空間で均等に分配し、
扱いやすい設定で制約をかけながら、高品質なカスタムパレットを生成する。

ランダムな色を作る最良の方法は何か?──ここからすべてが始まりました。

色を表記する一般的な方法はいくつかあります。

  • CSS で広く使われている16進数表記(例: #FF0000)。RGB の各値を16進数にしたものです。
  • RGB は Red / Green / Blue の略で、色を分類するひとつの「色空間」です。 モニターが色光を出す 3 つのチャネルに対応し、0〜255 の数値で表されます。 [0, 0, 0] は黒、[255, 255, 255] は白になります。
  • HSV(Hue, Saturation, Value)やその変種である HSL は、色を人間の感覚に近い形で扱える色空間です。 私たちが「青」「赤」などと呼ぶのは主に Hue(色相)で、RGB と違いこの要素だけを直接扱える利点があります。

多くのライブラリ(たとえば d3.js)は、これらの表記を相互に変換できます。 したがって乱数で色を生成するとき、RGB でも HSV でも構いません。 直感的には HSV/HSL の方が人間の感覚に近いので良さそうに思えますし、ネットでもそういう意見を見かけます。 しかし本当にそうかを確かめるため、乱数生成を観察できるツールを作りました。以下がその結果です。



HSL でのランダムカラー

下図は HSL でランダムに生成した 12 色のパレットです。

特に暗い色など、明らかに似た色が含まれています。12 色しか生成していないのに取り違えが起きかねず、可視化には不向きです。 データ可視化では色を明確に区別できることが最優先になります。

  • #451228

  • #13D907

  • #F9F4FC

  • #41039B

  • #47544E

  • #122B53

  • #59DA63

  • #7B1386

  • #4B381C

  • #CDBCD0

  • #EAEAEA

  • #B1CDE4


これら 12 色を HSL 空間に配置するとこうなります。

分布自体は均等に見えますが、似た色が出るのは偶然ではなく HSL 空間の性質に起因します。 Hue×Lightness の投影をみると、左上と右上がどちらも黒になっています。 空間上では離れているのに、知覚上は同じに見えてしまうのです。明度が低い色は色相に関わらず同じに見えますし、明度が高い場合や彩度が低い場合も同様です。

RGB でのランダムカラー

こちらは RGB でランダムに生成した 12 色のパレットです。

先ほどより明らかに良く、似た色も少ないのが分かります。RGB 空間の方が性質が良いのです。

  • #DBA714

  • #391FEA

  • #46385C

  • #E5154B

  • #3E9A71

  • #3495F5

  • #57510C

  • #D9B182

  • #8625B8

  • #83CA5D

  • #6252A1

  • #DB5E3A


このパレットを HSL 空間に投影するとこうなります。

色は暗い領域や極端に明るい領域、グレー領域には偏っていません。 それらは私たちが色の違いを見分けやすい領域に集まる傾向があります。 直感的にも、知覚的に多様な色を得るにはこうなってほしいと感じるはずです。

どの色空間を使うかで、乱数の分布は大きく変わります。

では、ランダムな色を生成するのに最適な色空間はどれでしょうか?

L*a*b* 色空間

CIE L*a*b* 色空間は人が知覚できる色をほぼ網羅し、知覚的に一様になるよう設計されています。 つまり、この空間での距離は知覚上の距離として扱え、近ければ見た目も似て、離れていれば違って見えるのです。求めていた特性がここにあります。

ただしこの色空間には「穴」があります。実在する色に対応する座標は一部で、その他は空白です。 少し扱いが難しくなりますが、十分対処できます。

形状は右図の通りです。

この色空間の優れている点は、黄色が明るく見え、紫が暗く見えるといった人の感覚を反映していることです。 明度を示す値に * が付いているのはそのためで、バイアスのない明るさを意味します。 また、暗色やグレーよりも彩度の高い色に広い領域を割り当てています。とても暗い色はすべて似た見た目になるので、暗色の領域は狭くなっています。


以降の分布評価では、この CIE L*a*b* 色空間を基準として使用します。

この空間で 1 万色をランダム生成すると、右図のような分布になります。 

これを基準に、他の色空間でのランダム分布と比較してみましょう。


HSL のランダム色を CIE L*a*b* で表示 

HSL のランダム色が中央に集中している様子がよく分かります。 この領域は暗色・グレー・非常に明るい色、すなわち彩度が低い色に対応しており、最初の例で似た色ばかりになった理由が視覚化されています。


RGB のランダム色を CIE L*a*b* で表示 

分布は基準に非常に近く、CIE L*a*b* で直接生成した場合ほどではないにせよ、RGB のランダム色も十分満足できる結果になります。

手軽なジェネレーターが必要なら、RGB のランダム色を選びましょう。期待できる最良の結果と大差ありません。

色を均等に配置する

次のステップは、似た色が生まれないよう互いの位置関係で色を配置することです。 ここでは k-MeansForce Vector(反発ベクトル)の 2 つのアルゴリズムを使います。

Force Vector:反発による配置

Force Vector はもっとも単純で、すべての色がお互いを押しのけ合い、可能な限り離れるようにします。 RGB 空間(立方体)で 8 色を生成して反発させると、各色は自然と角に移動していきます。

RGB Force Vector による 8 色パレット 

結果として黒と白、3 原色、3 補色が得られます。


  • #00FF00

  • #0000FF

  • #FF0000

  • #FFFFFF

  • #000000

  • #FFFF00

  • #FF00FF

  • #00FFFF


同じアルゴリズムを CIE L*a*b* 空間で使うと、空間の形状がゆがんでいるため異なる結果になりますが、それでも互いによく離れた色が得られます。

L*a*b* Force Vector による 8 色パレット 


  • #A09C04

  • #F161FE

  • #27B0CC

  • #FA231D

  • #04103B

  • #FA918F

  • #1A5A18

  • #963700

k-Means クラスタリング

色を均等に配置する別の方法は、色空間をクラスタリングすることです。 ここでは k-Means アルゴリズムを用い、色空間を k 個の領域に分割し、それぞれの中心点をパレットとして採用します。

RGB k-Means による 8 色パレット 

RGB キューブを 8 分割すると 8 色が得られますが、色は必ずしも頂点には配置されません。 これが Force Vector との大きな違いで、k-Means は Force Vector ほど色同士を離しません。


  • #BE3CBE

  • #3CBE3C

  • #3C3C3C

  • #BE3C3C

  • #3CBEBE

  • #BEBE3C

  • #3C3CBE

  • #BEBEBE


こちらも CIE L*a*b* 空間では形状が異なるため、別の結果になります。

L*a*b* k-Means による 8 色パレット 


  • #C7743B

  • #9B4DCA

  • #93C4A2

  • #513363

  • #94C64D

  • #514C34

  • #C45271

  • #969CC6

パレットを意図的に操る

次のステップは、生成される色に制約をかけて種類をコントロールすることです。 たとえば彩度を抑え、派手すぎる色を避けたい場合などが典型です。

HCL 色空間

RGB が画面が色を生成する仕組みに、CIE L*a*b* が私たちが色を知覚する仕組みに対応しているなら、HCL は色をどう考えるかに対応しています。 HSL に似ていますが、知覚バイアスを除いています。CIE L*a*b* をベースにしつつ、色相を独立した次元として扱う発想です。 そのため HCL では通常の Hue、バイアスのない彩度である Chroma、そして L*a*b* の(バイアスのない)Lightness* を使います。

この巧妙な手法は Gregor Aisch によって(再)発見されました。このブログ記事で詳しく説明されています。 ぜひ一読してください。なお、私がこのツールで多用した Chroma.js も彼が開発したものです。

HCL という用語についての注意: この色空間は当初、CSL(chroma / saturation / lightness)と呼ばれていました。 上記の記事では「CSL」と表現されていますが、現在の Chroma.js と同様に、ここで言う HCL を指しています。


HCL 色空間は CIE L*a*b* よりもさらにねじれており、彩度の低い色が一か所にまとまっているわけではありませんが、色空間に制約をかけるには最適です。

バイアスのない明度の効果がよく分かります。黄色は明るすぎず、藍は暗すぎません。全体がとても滑らかに見えるはずです。

Hue・Chroma・Lightness* の範囲を指定するだけで、L*a*b* 色空間を限定できます。 任意の L*a*b* の色を HCL に変換し、指定範囲に収まっているかテストするだけです。 Force Vector は色が境界外へ押し出されるのを防ぎ、k-Means はクラスタリング対象となるサンプリングをその範囲に絞り込むため、処理も速くなります。

つまり、計算処理では L*a*b* を用い、ユーザーが色域を絞るために HCL を扱う、という役割分担です。

Hue・Chroma・Lightness の範囲指定

以下はパレット作成に使える色サブスペースの例です。 色空間がねじれているため、Chroma を変えると Hue や Lightness* にも影響が出る点に注意してください。

Hue 200°〜250° 

やや青寄りの 170 色が得られます。Chroma や Lightness* を制限していないため、黒・灰・白も混ざります。

色相を限定すると、L*a*b* 空間をパイの一切れのように切り取るイメージになります。

Hue 100°〜250° 

青〜緑系の 577 色が抽出されます。範囲を広げれば色数も増えます。

色空間を大きく切り出したような形で、下部は暗く、上部は明るく、中心はグレー、外周は高彩度です。

ちなみに CIE L*a*b* 全体をサンプリングすると 1,782 色になります。

Chroma 0〜0.3 

低彩度の 66 色です。わずかに緑やオレンジ寄りになりますが、これはそれらの色相が低彩度に見えやすいためです。

Chroma を低く限定すると、色空間の中心付近だけを残す形になります。

Chroma 1〜1.2 

彩度の高い 215 色です。

Chroma の幅を狭くするとボウル状の形になります。Chroma が低いほど小さく、高いほど大きいボウルになります。

Chroma 2 以上 

非常に高彩度な 363 色です。ごく一部の緑を除き、青から紫にかけての色のみがこの彩度で知覚されます。

高い Chroma を選ぶと、色空間に大きな穴を開けたような形になりますが、空間の形が均一でないため特定の色相だけが残ります。

Lightness* 1.2 以上 

ごく明るい 196 色です。黄色系が多いのは、この色相が特に明るく知覚されるからです。

Lightness* を指定すると、色空間を水平にスライスするイメージになります。

カスタム色空間を活用する

データサイエンティストとして次のような要件があります。

  • 注意をそらす派手な色を避ける
  • 全体の一貫性を保つ
  • 必要に応じて特定要素を目立たせたり抑制したりできること
そして当然ながら、互いに区別しやすい色が必要です。


パレットを制御できれば、次のような工夫が可能です。

  • 派手さを抑えるには Chroma の上限を設定します。
  • 一貫性を保つには Lightness* の範囲を狭くするのが効果的です。
  • Hue を限定する手法も有効ですが、必要な色数に依存します。
  • 目立たせたくない場合は Chroma を下げ、背景に近い Lightness* を選びます。

少数の色を扱うときのおすすめ設定例です。 

Chroma < 0.3 かつ Lightness* > 1.2:ごく柔らかな 19 色。

A.1.
A.2.
B.1.
B.2.
B.3.
C.1.
C.2.
C.3.

同じ色が同時に現れることはありません。

要素の一部だけを強調したい場合の例です。 

基本は同じ色空間を使い、強調したい要素だけ次の条件にします。

0.4 < Chroma < 0.6 かつ 0.8 < Lightness* < 1:32 色。

A.
B.
Top Moumoute
C.
D.
E.

背景が暗い場合は次のような色空間を用います。

Chroma < 0.4 かつ 0.6 < Lightness* < 0.8:14 色。

0.4 < Chroma < 0.8 かつ 1 < Lightness* < 1.2:76 色。

A.
B.
Top Moumoute
C.
D.
E.