matplotlib – カラーマップを自作する方法について

目次

概要

データを視覚的に理解するために、適切なカラーマップを使用することが重要です。
本記事では matplotlib のカラーマップクラス colors.ListedColormap 及び colors.LinearSegmentedColormap の仕様を理解し、自作のカラーマップを作る方法を解説します。

カラーマップ

値をどの色で描画するかを決める対応関係のことをカラーマップ (color map) といいます。
カラーマップを使用することで、値に応じて色を変化させたグラフを描画でき、データを視覚的に理解するのに役立ちます。
matplotlib には100種類以上のカラーマップが用意されています。また、自分で新たにカラーマップを定義することもできます。

関数の値に応じて、等高線を描画した例です。 関数の値の大きい場所、小さい場所が色で視覚的に理解できます。
In [1]:
import matplotlib.pyplot as plt
import numpy as np


# データを作成する。
def f(x, y):
    return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)


x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 40)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

# 等高線を作成する。
fig, ax = plt.subplots()
c = ax.contourf(X, Y, Z, 20, cmap="jet")
fig.colorbar(c)

plt.show()

matplotlib で利用できるカラーマップ

matplotlib で利用できるカラーマップ一覧は以下の記事で解説しています。

[blogcard url=”https://pystyle.info/matplotlib-colormap”]

matplotlib のカラーマップの仕様

matplotlib のカラーマップを表すクラスには、次の種類があります。

  • colors.Colormap: 下記2つの親クラスです。このクラスを直接利用することはありません。
    • colors.ListedColormap: 円グラフ、棒グラフのように数種類の色分けを行いたい場合に使用するカラーマップのクラスです。
    • colors.LinearSegmentedColormap: 等高線、ヒートマップのように連続的に変化する値に応じて色分けを行いたい場合に使用するカラーマップのクラスです。

colors.Colormap クラス

colors.ListedColormap 及び colors.LinearSegmentedColormap はこのクラスを継承しているため、以下で紹介する内容は2つのクラスで共通の仕様になります。

Look Up Table (LUT)

colors.Colormap では、コンストラクタに渡した情報に基づき、整数と色の対応関係を表す Look Up Table (LUT) を作成します。 なお、matplotlib では、色は $[0, 255]$ の整数ではなく、$[0, 1]$ の浮動小数点数で表します。

カラーマップの色の種類が N としたとき、それに0未満の値、N - 1 より大きい値、無効な値に対応する色を加えた N + 3 が LUT のサイズになります。LUT は値を色に変換する際に以下のように参照します。

  • $i = 0, 1, \cdots, N – 1$: LUT[i] を参照します。
  • $i < 0$: LUT[N] を参照します。(デフォルトは LUT[0] と同じ色)
  • $i > N – 1$: LUT[N + 1] を参照します。(デフォルトは LUT[N - 1] と同じ色)
  • 無効な値: LUT[N + 2] を参照します。(デフォルトは透明 (0, 0, 0, 0))
0なら赤、1なら緑、2なら青に変換する LUT の例です。
In [2]:
from matplotlib.colors import ListedColormap

colors = ["red", "green", "blue"]
cmap = ListedColormap(colors, name="custom")
show_lut(cmap)
LUT色 (RGBA)
LUT[0]0(1.0, 0.0, 0.0, 1.0)
LUT[1]1(0.0, 0.5, 0.0, 1.0)
LUT[2]2(0.0, 0.0, 1.0, 1.0)
LUT[3]0未満の値(1.0, 0.0, 0.0, 1.0)
LUT[4]2より大きい値(0.0, 0.0, 1.0, 1.0)
LUT[5]無効な値(0.0, 0.0, 0.0, 0.0)

無効な値、範囲外の値を明示的に指定する

無効な値及び範囲外の値を colors.Colormap の以下の関数で明示的に指定することもできます。

  • set_under(): 0未満の値に対応付ける色を設定します。
  • set_over(): $N – 1$ より大きい値に対応付ける色を設定します。
  • set_bad(): 無効な値に対応付ける色を設定します。
In [3]:
from matplotlib.colors import ListedColormap

colors = ["red", "green", "blue", "pink", "orange"]
cmap = ListedColormap(colors, name="custom")
cmap.set_under("darkred")  # 0より小さい値
cmap.set_over("darkblue")  # 4より大きい値
cmap.set_bad("yellow")  # 無効な値
LUT色 (RGBA)
LUT[0]0(1.0, 0.0, 0.0, 1.0)
LUT[1]1(0.0, 0.5, 0.0, 1.0)
LUT[2]2(0.0, 0.0, 1.0, 1.0)
LUT[3]3(1.0, 0.8, 0.8, 1.0)
LUT[4]4(1.0, 0.6, 0.0, 1.0)
LUT[5]0未満の値(0.5, 0.0, 0.0, 1.0)
LUT[6]4より大きい値(0.0, 0.0, 0.5, 1.0)
LUT[7]無効な値(1.0, 1.0, 0.0, 1.0)

どのような値を無効な値とするかはユーザーが定義する必要があります。 numpy.ma.array() の引数 mask に無効な値かどうかを決める bool 型の配列を渡して、numpy.ma オブジェクトを作成します。 以下の例では、0未満の値を「無効な値」と定義し、黄色で描画しています。

In [4]:
# データを作成する。
def f(x, y):
    return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)


x = np.linspace(0, 5, 300)
y = np.linspace(0, 5, 300)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

# 無効な値を True、そうでない値を False とした bool 型の配列を mask 引数に指定する。
masked_Z = np.ma.array(Z, mask=Z < 0)

# カラーマップ
cmap = plt.cm.jet
cmap.set_bad((1, 1, 0, 1))  # 無効な値に対応する色

# 等高線を可視化する。
fig, ax = plt.subplots()
ax.imshow(masked_Z, cmap=cmap)

plt.show()

浮動小数点数を変換するルール

値が浮動小数点数の場合は、以下の手順で整数に変換して、LUT を参照します。

  1. 値に色の種類 N を乗算する。
  2. 整数にキャストする。
以下の例では、$N = 256$ のカラーマップにおいて、浮動小数点数がどのように LUT の色に対応付けられるかを表しています。
整数に変換後の値色 (RGBA)
-0.1-1(0.0, 0.0, 0.5, 1.0)
0.00(0.0, 0.0, 0.5, 1.0)
0.125(0.0, 0.0, 0.9, 1.0)
0.251(0.0, 0.3, 1.0, 1.0)
0.376(0.0, 0.7, 1.0, 1.0)
0.4102(0.2, 1.0, 0.8, 1.0)
0.5128(0.5, 1.0, 0.5, 1.0)
0.6153(0.8, 1.0, 0.2, 1.0)
0.7179(1.0, 0.8, 0.0, 1.0)
0.8204(1.0, 0.4, 0.0, 1.0)
0.9230(0.9, 0.0, 0.0, 1.0)
1.0255(0.5, 0.0, 0.0, 1.0)
1.1281(0.5, 0.0, 0.0, 1.0)

通常、LUT のサイズ N を乗算する前に値を $[0, 1]$ の範囲に正規化します。正規化により、浮動小数点数は $[0, N]$ の範囲のいずれかの整数に変換されます。 カラーマップを cmap 引数で指定できる関数は norm 引数で正規化方法を指定できるようになっています。

正規化方法に colors.PowerNorm を指定した例です。

In [5]:
from matplotlib.colors import PowerNorm

# データを作成する。
def f(x, y):
    return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)


x = np.linspace(0, 5, 300)
y = np.linspace(0, 5, 300)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

# 等高線を可視化する。
fig, ax = plt.subplots()
ax.imshow(Z, cmap="jet", norm=PowerNorm(0.5))

plt.show()

カラーマップを反転させる。

colors.Colormapreversed() で反転させたカラーマップを作成して、返します。

In [6]:
import matplotlib.pyplot as plt
import numpy as np


# データを作成する。
def f(x, y):
    return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)


x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 40)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

# カラーマップ
cmap1 = plt.cm.get_cmap("jet")
cmap2 = cmap1.reversed()

# 等高線を作成する。
fig, ax = plt.subplots()
c = ax.contourf(X, Y, Z, 20, cmap=cmap1)
fig.colorbar(c)
plt.show()

fig, ax = plt.subplots()
c = ax.contourf(X, Y, Z, 20, cmap=cmap2)
fig.colorbar(c)
plt.show()

カラーマップで値を色に変換する。

colors.Colormap には __call__() が実装されており、関数呼び出しすることで値をカラーマップの対応する色に変換できます。

  • alpha を指定することで、変換後の色のアルファ値をその値にできます。
  • bytes=True を指定することで、変換後の色が $[0, 1]$ の浮動小数点数ではなく、$[0, 255]$ の整数になります。
In [7]:
cmap = plt.cm.get_cmap("jet")

# 整数 -> 色
print(cmap(10))

# 浮動小数点数 -> 色
print(cmap(0.5))

# 配列も OK
print(cmap([0, 10, 20, 30]))
# [[0.         0.         0.5        1.        ]
#  [0.         0.         0.67825312 1.        ]
#  [0.         0.         0.85650624 1.        ]
#  [0.         0.         1.         1.        ]]

# アルファ値を指定する。
print(cmap(10, alpha=0.5))

# bytes=True で値の範囲を [0, 255] の整数で返す。
print(cmap(10, bytes=True))
(0.0, 0.0, 0.67825311942959, 1.0)
(0.4901960784313725, 1.0, 0.4775458570524984, 1.0)
[[0.         0.         0.5        1.        ]
 [0.         0.         0.67825312 1.        ]
 [0.         0.         0.85650624 1.        ]
 [0.         0.         1.         1.        ]]
(0.0, 0.0, 0.67825311942959, 0.5)
(0, 0, 172, 255)

ListedColormap クラス

colors.ListedColormap は、円グラフ、棒グラフのように数種類の色分けを行いたい場合に使用するカラーマップのクラスです。

colors.ListedColormap は、色の種類 N 及び色の一覧 colors をコンストラクタに与えて、次のように LUT を作成します。

  • N を指定しない場合は、N = len(colors) になります。
  • 整数 $i, (i = 0, 1, \cdots, N – 1)$ は、色 colors[i] に対応付けます。
5種類の色を指定して colors.ListedColormap を作成する例です。
In [8]:
from matplotlib.colors import ListedColormap

colors = ["red", "green", "blue", "pink", "orange"]
cmap = ListedColormap(colors, name="custom")

この色一覧から以下の LUT が作成されます。

LUT色 (RGBA)
LUT[0]0(1.0, 0.0, 0.0, 1.0)
LUT[1]1(0.0, 0.5, 0.0, 1.0)
LUT[2]2(0.0, 0.0, 1.0, 1.0)
LUT[3]3(1.0, 0.8, 0.8, 1.0)
LUT[4]4(1.0, 0.6, 0.0, 1.0)
LUT[5]0未満の値(1.0, 0.0, 0.0, 1.0)
LUT[6]4より大きい値(1.0, 0.6, 0.0, 1.0)
LUT[7]無効な値(0.0, 0.0, 0.0, 0.0)

N < len(colors) の場合、colors[:N] までの色を使って、LUT を作成します。
以下の場合、colors[:3] = ["red", "green", "blue"] だけ使用して LUT を作成します。

In [9]:
from matplotlib.colors import ListedColormap

colors = ["red", "green", "blue", "pink", "orange"]
cmap = ListedColormap(colors, name="custom", N=3)
LUT色 (RGBA)
LUT[0]0(1.0, 0.0, 0.0, 1.0)
LUT[1]1(0.0, 0.5, 0.0, 1.0)
LUT[2]2(0.0, 0.0, 1.0, 1.0)
LUT[3]0未満の値(1.0, 0.0, 0.0, 1.0)
LUT[4]2より大きい値(0.0, 0.0, 1.0, 1.0)
LUT[5]無効な値(0.0, 0.0, 0.0, 0.0)

N > len(colors) の場合、colors を繰り返し参照して、LUT を作成します。

In [10]:
from matplotlib.colors import ListedColormap

colors = ["red", "green", "blue", "pink", "orange"]
cmap = ListedColormap(colors, name="custom", N=10)
LUT色 (RGBA)
LUT[0]0(1.0, 0.0, 0.0, 1.0)
LUT[1]1(0.0, 0.5, 0.0, 1.0)
LUT[2]2(0.0, 0.0, 1.0, 1.0)
LUT[3]3(1.0, 0.8, 0.8, 1.0)
LUT[4]4(1.0, 0.6, 0.0, 1.0)
LUT[5]5(1.0, 0.0, 0.0, 1.0)
LUT[6]6(0.0, 0.5, 0.0, 1.0)
LUT[7]7(0.0, 0.0, 1.0, 1.0)
LUT[8]8(1.0, 0.8, 0.8, 1.0)
LUT[9]9(1.0, 0.6, 0.0, 1.0)
LUT[10]0未満の値(1.0, 0.0, 0.0, 1.0)
LUT[11]9より大きい値(1.0, 0.6, 0.0, 1.0)
LUT[12]無効な値(0.0, 0.0, 0.0, 0.0)
In [11]:
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)

fig, ax = plt.subplots()

N = 50
x = np.random.rand(N)
y = np.random.rand(N)
area = np.random.uniform(100, 1000, len(x))

plt.scatter(x, y, s=area, c=area, alpha=0.5, cmap="Accent")
plt.show()

LinearSegmentedColormap クラス

colors.LinearSegmentedColormap は、実数を色に対応付けるカラーマップです。3D グラフのようにとり得る値が無限個あり、連続的に変化する場合に利用します。

colors.LinearSegmentedColormap は、色の種類 N 及び補完に使用する segmentdata をコンストラクタに与えて、次のように LUT を作成します。 N を指定しない場合は、N = 256 になります。

In [12]:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LinearSegmentedColormap

cdict = {
    "red": [
        (0.0, 0.0, 0.3),
        (0.25, 0.2, 0.4),
        (0.5, 0.8, 0.9),
        (0.75, 0.9, 1.0),
        (1.0, 0.4, 1.0),
    ],
    "green": [
        (0.0, 0.0, 0.2),
        (0.25, 0.2, 0.5),
        (0.5, 0.5, 0.8),
        (0.75, 0.8, 0.9),
        (1.0, 0.9, 1.0),
    ],
    "blue": [
        (0.0, 0.0, 0.4),
        (0.25, 1.0, 0.9),
        (0.5, 1.0, 0.7),
        (1.0, 0.8, 0.9),
    ],
}

cmap = LinearSegmentedColormap("custom", cdict, 12)

dict はキーがチャンネル名、値が (m, 3) の配列を指定します。
配列の各要素は3つの値 x, y0, y1 からなります。x は0から1に単調増加するようにし、y0, y1 は各 x でのそのチャンネルの値を指定します。
上記の cdict を可視化すると以下のようになります。 青、オレンジの点がそれぞれ各 x における y0 及び y1 の値になります。

まず y0, y1 から線分 (y1[0], y0[1]), (y1[1], y0[2]), ..., (y1[m - 2], y0[m - 1]) を作成します。 なお、y[0], y1[m - 1] の値は LUT の作成には使用されません。

区間 $[0, 1]$ 上に等間隔に $N$ 個の点をとり、先程作成した線分の対応する $y$ を求めます。

こうして求めたチャンネルごとの $y$ の値を使って、LUT を作成します。

LUT色 (RGBA)
LUT[0]0(0.3, 0.2, 0.4, 1.0)
LUT[1]1(0.3, 0.2, 0.6, 1.0)
LUT[2]2(0.2, 0.2, 0.8, 1.0)
LUT[3]3(0.4, 0.5, 0.9, 1.0)
LUT[4]4(0.6, 0.5, 0.9, 1.0)
LUT[5]5(0.7, 0.5, 1.0, 1.0)
LUT[6]6(0.9, 0.8, 0.7, 1.0)
LUT[7]7(0.9, 0.8, 0.7, 1.0)
LUT[8]8(0.9, 0.8, 0.7, 1.0)
LUT[9]9(0.8, 0.9, 0.8, 1.0)
LUT[10]10(0.6, 0.9, 0.8, 1.0)
LUT[11]11(0.4, 0.9, 0.8, 1.0)
LUT[12]0未満の値(0.3, 0.2, 0.4, 1.0)
LUT[13]11より大きい値(0.4, 0.9, 0.8, 1.0)
LUT[14]無効な値(0.0, 0.0, 0.0, 0.0)

少々ややこしいですが、以下のような不連続なカラーマップを作成できるようにするために、このような仕様になっています。

作成したカラーマップを使って、等高線を描画してみましょう。

In [13]:
def f(x, y):
    return np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)


# 関数の入力を作成する。
x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 40)
X, Y = np.meshgrid(x, y)
Z = f(X, Y)

# 等高線を可視化する。
plt.contourf(X, Y, Z, 20, cmap=cmap)

plt.show()

colors.LinearSegmentedColormap を作成する際に gamma を指定することで、LUT を作成する際にガンマ補正を行い、明るさを調整できます。 gamma を小さい値にするほど明るく、大きい値にするほど暗い色になります。

In [14]:
x = np.linspace(0, 1, 256).reshape(1, 256)

fig = plt.figure(figsize=(5, 2))

for i, p in enumerate([0.1, 0.5, 1, 1.5, 3.0], 1):
    ax = fig.add_subplot(5, 1, i)
    ax.set_axis_off()
    cmap = LinearSegmentedColormap("custom", plt.cm.jet._segmentdata, gamma=p)
    ax.imshow(x, cmap=cmap, aspect="auto")
    ax.text(-10, 0, f"gamma={p:.2f}", va='center', ha='right', fontsize=10)
plt.show()

segmentdata を指定して作成する代わりに colors.LinearSegmentedColormap.from_list() を使って、カラーマップを作成することもできます。 この場合、$[0, 1]$ の区間を色の数で等間隔に分割し、色を割り当て、LUT を作成します。

In [15]:
cmap = LinearSegmentedColormap.from_list("custom", ["red", "green", "blue", "pink", "orange"], 12)

コメント

コメントする

目次