在 Matplotlib 中创建色谱#

Matplotlib 有许多内置色谱,可以通过 matplotlib.colormaps 访问。还有一些外部库,例如 palettable,它们提供了许多额外的色谱。

然而,我们也可能想要创建或操作自己的色谱。这可以通过使用类 ListedColormapLinearSegmentedColormap 来完成。这两个色谱类都将 0 到 1 之间的值映射到颜色。然而,它们之间存在差异,具体解释如下。

在手动创建或操作色谱之前,我们首先来看看如何从现有色谱类中获取色谱及其颜色。

获取色谱并访问其值#

首先,获取一个命名色谱(其中大多数列在 在 Matplotlib 中选择色谱 中)可以通过 matplotlib.colormaps 完成,它会返回一个色谱对象。用于内部定义色谱的颜色列表的长度可以通过 Colormap.resampled 进行调整。下面我们使用适中的值 8,这样就没有太多值需要查看。

import matplotlib.pyplot as plt
import numpy as np

import matplotlib as mpl
from matplotlib.colors import LinearSegmentedColormap, ListedColormap

viridis = mpl.colormaps['viridis'].resampled(8)

对象 viridis 是一个可调用对象,当传入 0 到 1 之间的浮点数时,它会返回色谱中的 RGBA 值

print(viridis(0.56))
(np.float64(0.122312), np.float64(0.633153), np.float64(0.530398), np.float64(1.0))

ListedColormap#

ListedColormap 将其颜色值存储在 .colors 属性中。构成色谱的颜色列表可以直接使用 colors 属性访问,也可以通过调用 viridis 并传入与色谱长度匹配的值数组来间接访问。请注意,返回的列表是 RGBA (N, 4) 数组的形式,其中 N 是色谱的长度。

print('viridis.colors', viridis.colors)
print('viridis(range(8))', viridis(range(8)))
print('viridis(np.linspace(0, 1, 8))', viridis(np.linspace(0, 1, 8)))
viridis.colors [[0.267004 0.004874 0.329415 1.      ]
 [0.275191 0.194905 0.496005 1.      ]
 [0.212395 0.359683 0.55171  1.      ]
 [0.153364 0.497    0.557724 1.      ]
 [0.122312 0.633153 0.530398 1.      ]
 [0.288921 0.758394 0.428426 1.      ]
 [0.626579 0.854645 0.223353 1.      ]
 [0.993248 0.906157 0.143936 1.      ]]
viridis(range(8)) [[0.267004 0.004874 0.329415 1.      ]
 [0.275191 0.194905 0.496005 1.      ]
 [0.212395 0.359683 0.55171  1.      ]
 [0.153364 0.497    0.557724 1.      ]
 [0.122312 0.633153 0.530398 1.      ]
 [0.288921 0.758394 0.428426 1.      ]
 [0.626579 0.854645 0.223353 1.      ]
 [0.993248 0.906157 0.143936 1.      ]]
viridis(np.linspace(0, 1, 8)) [[0.267004 0.004874 0.329415 1.      ]
 [0.275191 0.194905 0.496005 1.      ]
 [0.212395 0.359683 0.55171  1.      ]
 [0.153364 0.497    0.557724 1.      ]
 [0.122312 0.633153 0.530398 1.      ]
 [0.288921 0.758394 0.428426 1.      ]
 [0.626579 0.854645 0.223353 1.      ]
 [0.993248 0.906157 0.143936 1.      ]]

色谱是一个查找表,因此“过采样”色谱会返回最近邻插值(请注意下面列表中的重复颜色)

print('viridis(np.linspace(0, 1, 12))', viridis(np.linspace(0, 1, 12)))
viridis(np.linspace(0, 1, 12)) [[0.267004 0.004874 0.329415 1.      ]
 [0.267004 0.004874 0.329415 1.      ]
 [0.275191 0.194905 0.496005 1.      ]
 [0.212395 0.359683 0.55171  1.      ]
 [0.212395 0.359683 0.55171  1.      ]
 [0.153364 0.497    0.557724 1.      ]
 [0.122312 0.633153 0.530398 1.      ]
 [0.288921 0.758394 0.428426 1.      ]
 [0.288921 0.758394 0.428426 1.      ]
 [0.626579 0.854645 0.223353 1.      ]
 [0.993248 0.906157 0.143936 1.      ]
 [0.993248 0.906157 0.143936 1.      ]]

LinearSegmentedColormap#

LinearSegmentedColormap 没有 .colors 属性。然而,仍然可以用整数数组或 0 到 1 之间的浮点数组调用该色谱。

copper = mpl.colormaps['copper'].resampled(8)

print('copper(range(8))', copper(range(8)))
print('copper(np.linspace(0, 1, 8))', copper(np.linspace(0, 1, 8)))
copper(range(8)) [[0.         0.         0.         1.        ]
 [0.17647055 0.1116     0.07107143 1.        ]
 [0.35294109 0.2232     0.14214286 1.        ]
 [0.52941164 0.3348     0.21321429 1.        ]
 [0.70588219 0.4464     0.28428571 1.        ]
 [0.88235273 0.558      0.35535714 1.        ]
 [1.         0.6696     0.42642857 1.        ]
 [1.         0.7812     0.4975     1.        ]]
copper(np.linspace(0, 1, 8)) [[0.         0.         0.         1.        ]
 [0.17647055 0.1116     0.07107143 1.        ]
 [0.35294109 0.2232     0.14214286 1.        ]
 [0.52941164 0.3348     0.21321429 1.        ]
 [0.70588219 0.4464     0.28428571 1.        ]
 [0.88235273 0.558      0.35535714 1.        ]
 [1.         0.6696     0.42642857 1.        ]
 [1.         0.7812     0.4975     1.        ]]

创建列表色谱#

创建色谱本质上是上述操作的逆过程,我们向 ListedColormap 提供颜色规格列表或数组以创建新的色谱。

在继续本教程之前,我们先定义一个辅助函数,它接受一个或多个色谱作为输入,创建一些随机数据,并将色谱应用于该数据集的图像绘图。

def plot_examples(colormaps):
    """
    Helper function to plot data with associated colormap.
    """
    np.random.seed(19680801)
    data = np.random.randn(30, 30)
    n = len(colormaps)
    fig, axs = plt.subplots(1, n, figsize=(n * 2 + 2, 3),
                            layout='constrained', squeeze=False)
    for [ax, cmap] in zip(axs.flat, colormaps):
        psm = ax.pcolormesh(data, cmap=cmap, rasterized=True, vmin=-4, vmax=4)
        fig.colorbar(psm, ax=ax)
    plt.show()

在最简单的情况下,我们可能会输入一个颜色名称列表,从中创建色谱。

cmap = ListedColormap(["darkorange", "gold", "lawngreen", "lightseagreen"])
plot_examples([cmap])
colormap manipulation

事实上,该列表可以包含任何有效的 Matplotlib 颜色规范。对于创建自定义色谱特别有用的是 (N, 4) 形的数组。因为通过对这种数组进行各种 numpy 操作,从现有色谱构建新色谱变得非常简单。

例如,假设我们出于某种原因想将一个 256 长度的 "viridis" 色谱的前 25 个条目变为粉红色

viridis = mpl.colormaps['viridis'].resampled(256)
newcolors = viridis(np.linspace(0, 1, 256))
pink = np.array([248/256, 24/256, 148/256, 1])
newcolors[:25, :] = pink
newcmp = ListedColormap(newcolors)

plot_examples([viridis, newcmp])
colormap manipulation

我们可以减小色谱的动态范围;这里我们选择色谱的中间一半。但请注意,因为 viridis 是一个列表色谱,我们最终会得到 128 个离散值,而不是原始色谱中的 256 个值。此方法不会在颜色空间中进行插值来添加新颜色。

viridis_big = mpl.colormaps['viridis']
newcmp = ListedColormap(viridis_big(np.linspace(0.25, 0.75, 128)))
plot_examples([viridis, newcmp])
colormap manipulation

我们也可以轻松地连接两个色谱

top = mpl.colormaps['Oranges_r'].resampled(128)
bottom = mpl.colormaps['Blues'].resampled(128)

newcolors = np.vstack((top(np.linspace(0, 1, 128)),
                       bottom(np.linspace(0, 1, 128))))
newcmp = ListedColormap(newcolors, name='OrangeBlue')
plot_examples([viridis, newcmp])
colormap manipulation

当然,我们不必从命名的色谱开始,我们只需要创建 (N, 4) 数组并将其传递给 ListedColormap。这里我们创建一个从棕色(RGB: 90, 40, 40)到白色(RGB: 255, 255, 255)的色谱。

N = 256
vals = np.ones((N, 4))
vals[:, 0] = np.linspace(90/256, 1, N)
vals[:, 1] = np.linspace(40/256, 1, N)
vals[:, 2] = np.linspace(40/256, 1, N)
newcmp = ListedColormap(vals)
plot_examples([viridis, newcmp])
colormap manipulation

创建线性分段色谱#

LinearSegmentedColormap 类使用锚点指定色谱,在这些锚点之间对 RGB(A) 值进行插值。

指定这些色谱的格式允许在锚点处存在不连续性。每个锚点在矩阵中指定为一行,形式为 [x[i] yleft[i] yright[i]],其中 x[i] 是锚点,yleft[i]yright[i] 是锚点两侧的颜色值。

如果没有不连续性,则 yleft[i] == yright[i]

cdict = {'red':   [[0.0,  0.0, 0.0],
                   [0.5,  1.0, 1.0],
                   [1.0,  1.0, 1.0]],
         'green': [[0.0,  0.0, 0.0],
                   [0.25, 0.0, 0.0],
                   [0.75, 1.0, 1.0],
                   [1.0,  1.0, 1.0]],
         'blue':  [[0.0,  0.0, 0.0],
                   [0.5,  0.0, 0.0],
                   [1.0,  1.0, 1.0]]}


def plot_linearmap(cdict):
    newcmp = LinearSegmentedColormap('testCmap', segmentdata=cdict, N=256)
    rgba = newcmp(np.linspace(0, 1, 256))
    fig, ax = plt.subplots(figsize=(4, 3), layout='constrained')
    col = ['r', 'g', 'b']
    for xx in [0.25, 0.5, 0.75]:
        ax.axvline(xx, color='0.7', linestyle='--')
    for i in range(3):
        ax.plot(np.arange(256)/256, rgba[:, i], color=col[i])
    ax.set_xlabel('index')
    ax.set_ylabel('RGB')
    plt.show()

plot_linearmap(cdict)
colormap manipulation

为了在锚点处创建不连续性,第三列与第二列不同。“红”、“绿”、“蓝”以及可选的“透明度”的矩阵设置如下

cdict['red'] = [...
                [x[i]      yleft[i]     yright[i]],
                [x[i+1]    yleft[i+1]   yright[i+1]],
               ...]

对于传入色谱的 x[i]x[i+1] 之间的值,插值发生在 yright[i]yleft[i+1] 之间。

在下面的例子中,红色在 0.5 处有一个不连续点。0 到 0.5 之间的插值是从 0.3 到 1,而 0.5 到 1 之间的插值是从 0.9 到 1。请注意,red[0, 1]red[2, 2] 对于插值都是多余的,因为 red[0, 1](即 yleft[0])是 0 左侧的值,而 red[2, 2](即 yright[2])是 1 右侧的值,它们都在颜色映射域之外。

cdict['red'] = [[0.0,  0.0, 0.3],
                [0.5,  1.0, 0.9],
                [1.0,  1.0, 1.0]]
plot_linearmap(cdict)
colormap manipulation

直接从列表中创建分段色谱#

上述方法非常通用,但不可否认实现起来有些繁琐。对于某些基本情况,使用 LinearSegmentedColormap.from_list 可能会更容易。它会根据提供的颜色列表创建具有等间距的分段色谱。

colors = ["darkorange", "gold", "lawngreen", "lightseagreen"]
cmap1 = LinearSegmentedColormap.from_list("mycmap", colors)

如果需要,色谱的节点可以作为 0 到 1 之间的数字给出。例如,可以使偏红色的部分在色谱中占据更多空间。

nodes = [0.0, 0.4, 0.8, 1.0]
cmap2 = LinearSegmentedColormap.from_list("mycmap", list(zip(nodes, colors)))

plot_examples([cmap1, cmap2])
colormap manipulation

反转色谱#

Colormap.reversed 创建一个新的色谱,它是原始色谱的反转版本。

colors = ["#ffffcc", "#a1dab4", "#41b6c4", "#2c7fb8", "#253494"]
my_cmap = ListedColormap(colors, name="my_cmap")

my_cmap_r = my_cmap.reversed()

plot_examples([my_cmap, my_cmap_r])
colormap manipulation

如果没有传入名称,.reversed 还会通过在原始色谱名称后附加 '_r' 来命名副本。

注册色谱#

色谱可以添加到 matplotlib.colormaps 的命名色谱列表中。这使得色谱可以在绘图函数中通过名称访问

# my_cmap, my_cmap_r from reversing a colormap
mpl.colormaps.register(cmap=my_cmap)
mpl.colormaps.register(cmap=my_cmap_r)

data = [[1, 2, 3, 4, 5]]

fig, (ax1, ax2) = plt.subplots(nrows=2)

ax1.imshow(data, cmap='my_cmap')
ax2.imshow(data, cmap='my_cmap_r')

plt.show()
colormap manipulation

脚本总运行时间: (0 分 5.023 秒)

由 Sphinx-Gallery 生成的画廊