在 Matplotlib 中选择色图

Matplotlib 提供了许多内置色图,可以通过 matplotlib.colormaps 访问。第三方库还提供了更多选项,见 第三方色图。下面简要介绍如何在这些选项中进行选择。需要自己创建色图时,请参阅 创建色图。

列出全部已注册的色图:

from matplotlib import colormaps
list(colormaps)

概览

选择合适色图的核心,是在三维颜色空间中为数据集找到合适的表示。哪个色图最合适,取决于多个因素:

  • 表示的是形态还是度量数据,参见 Ware。
  • 对数据集的了解,例如是否存在一个关键值,其他值围绕它偏离。
  • 所绘参数是否有直观的配色方式。
  • 所属领域是否存在观众习惯使用的标准。

对于许多应用,感知均匀的色图是最好的选择:数据中相等的步长,在颜色空间中也被感知为相等的步长。研究者发现,与色相变化相比,人脑更容易把明度变化理解为数据变化。因此,明度沿色图单调增加的配色,更容易被观众正确解读。第三方色图部分也有很好的感知均匀实例。

颜色可以用多种三维空间表示,其中一种是 CIELAB。在 CIELAB 中,颜色由明度 (L^)、红—绿分量 (a^) 和黄—蓝分量 (b^) 表示。明度参数 (L^) 可以帮助我们了解观众如何感知 Matplotlib 的色图。

IBM 是学习色图与人类感知的一个很好的入门资料。

色图的类别

按照功能,色图通常可分为以下几类,参见 Moreland:

  1. 顺序型(Sequential):明度和通常也包括颜色饱和度逐渐变化,往往采用单一色相;适合表示有顺序的信息。
  2. 发散型(Diverging):两种颜色的明度、以及可能的饱和度发生变化,在中间的低饱和度颜色处相遇;适合存在关键中间值的信息,例如地形,或围绕零偏离的数据。
  3. 循环型(Cyclic):两种颜色的明度变化,在中间及首尾的低饱和度颜色处相接;适合端点回绕的值,例如相位角、风向或一天中的时间。
  4. 定性型(Qualitative):通常由不同颜色组成;适合没有顺序或关联的信息。
from colorspacious import cspace_converter

import matplotlib.pyplot as plt
import numpy as np

import matplotlib as mpl

先展示各个色图的完整范围。注意,有些色图看起来比其他色图变化得更“快”。

cmaps = {}

gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))

def plot_color_gradients(category, cmap_list):
    # Create figure and adjust figure height to number of colormaps
    nrows = len(cmap_list)
    figh = 0.35 + 0.15 + (nrows + (nrows - 1) * 0.1) * 0.22
    fig, axs = plt.subplots(nrows=nrows + 1, figsize=(6.4, figh))
    fig.subplots_adjust(top=1 - 0.35 / figh, bottom=0.15 / figh,
                        left=0.2, right=0.99)
    axs[0].set_title(f'{category} colormaps', fontsize=14)
    for ax, name in zip(axs, cmap_list):
        ax.imshow(gradient, aspect='auto', cmap=mpl.colormaps[name])
        ax.text(-0.01, 0.5, name, va='center', ha='right', fontsize=10,
                transform=ax.transAxes)

    # Turn off *all* ticks & spines, not just the ones with colormaps.
    for ax in axs:
        ax.set_axis_off()

    # Save colormap list for later.
    cmaps[category] = cmap_list

顺序型

在顺序型图中,明度值沿色图单调增加,这是理想的性质。有些色图的 (L^) 覆盖 0 到 100,例如 binary 和其他灰度色图;另一些则从约 (L^=20) 开始。(L^) 范围较小的色图,感知范围也相应较小。不同色图的 (L^) 函数形状并不相同:有些接近线性,有些更弯曲。

plot_color_gradients('Perceptually Uniform Sequential',
                     ['viridis', 'plasma', 'inferno', 'magma', 'cividis'])

原文示例图:感知均匀的顺序型色图

plot_color_gradients('Sequential',
                     ['Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds',
                      'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu',
                      'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn'])

原文示例图:顺序型色图

顺序型第二组(Sequential2)

这一组中许多色图的 (L^*) 单调增加,但部分色图(autumn、cool、spring 和 winter)在明度空间中出现平台,甚至先升后降。其他色图(afmhot、copper、gist_heat 和 hot)的明度函数存在转折。在色图的平台或转折区域表示数据,可能让观众感知到条带,参见 mycarta-banding 的实例。

plot_color_gradients('Sequential (2)',
                     ['gray', 'bone', 'pink', 'spring', 'summer', 'autumn',
                      'winter', 'cool', 'Wistia', 'hot', 'afmhot', 'gist_heat',
                      'copper'])

原文示例图:顺序型第二组

为保持向后兼容,Matplotlib 还支持以下色图名称,它们与其他内置色图完全相同。不推荐继续使用这些名称,应改用建议的替代名称。

色图 完全相同的替代名称
gist_gray gray
gist_yarg gray_r
binary gray_r

发散型

发散型色图应当让 (L^) 单调增加到一个接近 100 的最大值,然后单调下降。两端的最低明度应大致相同。按这些标准,BrBG 和 RdBu 是不错的选项。coolwarm 也不错,但其 (L^) 范围不大,下面的灰度部分会进一步说明。

berlin、managua 和 vanimo 是暗色模式的发散型色图:中央明度最低,两端明度最高。它们来自 F. Crameri 的 scientific-colour-maps 8.0.1 版。

plot_color_gradients('Diverging',
                     ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu',
                      'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic',
                      'berlin', 'managua', 'vanimo'])

原文示例图:发散型色图

循环型

循环型色图应从同一种颜色开始并结束,并在中间经过对称的中心点。(L^) 应从起点到中点单调变化,再从中点到终点反向变化。上升与下降两侧应对称,仅色相不同。在端点与中点,(L^) 会改变变化方向,应在明度空间中平滑处理这些位置以减少伪影。循环型色图设计的更多信息见 kovesi-colormaps。

常用的 HSV 色图也列在这一组中,但它并不围绕中心点对称。此外,它的 (L^*) 在整个色图中变化很大,因此不适合让观众按感知方式解读数据。mycarta-jet 对这个问题有进一步说明。

plot_color_gradients('Cyclic', ['twilight', 'twilight_shifted', 'hsv'])

原文示例图:循环型色图

定性型

定性型色图的目标不是提供感知均匀的映射,观察明度参数也能证实这一点。(L^*) 在整个色图中不断上下变化,显然并不单调增加。因此,它们不适合作为感知均匀的色图。

plot_color_gradients('Qualitative',
                     ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'okabe_ito',
                      'Dark2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20',
                      'tab20b', 'tab20c'])

原文示例图:定性型色图

其他色图

这一类的部分色图是为特定用途设计的。例如,gist_earth、ocean 和 terrain 看起来是为了同时绘制地形(绿/棕色)和水深(蓝色)而创建的。因此,我们预期它们具有发散性质,但多处转折可能不够理想,gist_earth 和 terrain 就是如此。CMRmap 为良好的灰度转换而设计,不过其 (L^*) 似乎仍有一些小转折。

cubehelix 旨在让明度和色相都平滑变化,但在绿色色相区域似乎有一个小凸起。turbo 则为深度和视差数据设计。

常用的 jet 也在这一组。可以看到,其 (L^*) 在整个色图中变化很大,因此不适合让观众按感知方式解读数据。更多讨论见 mycarta-jet 和 turbo。

plot_color_gradients('Miscellaneous',
                     ['flag', 'prism', 'ocean', 'gist_earth', 'terrain',
                      'gist_stern', 'gnuplot', 'gnuplot2', 'CMRmap',
                      'cubehelix', 'brg', 'gist_rainbow', 'rainbow', 'jet',
                      'turbo', 'nipy_spectral', 'gist_ncar'])

plt.show()

原文示例图:其他色图

Matplotlib 色图的明度

下面检查 Matplotlib 色图的明度。关于这些色图还有一些资料,见 list-colormaps。

mpl.rcParams.update({'font.size': 12})

# Number of colormap per subplot for particular cmap categories
_DSUBS = {'Perceptually Uniform Sequential': 5, 'Sequential': 6,
          'Sequential (2)': 6, 'Diverging': 6, 'Cyclic': 3,
          'Qualitative': 4, 'Miscellaneous': 6}
# Spacing between the colormaps of a subplot
_DC = {'Perceptually Uniform Sequential': 1.4, 'Sequential': 0.7,
       'Sequential (2)': 1.4, 'Diverging': 1.4, 'Cyclic': 1.4,
       'Qualitative': 1.4, 'Miscellaneous': 1.4}

# Indices to step through colormap
x = np.linspace(0.0, 1.0, 100)
# Do plot
for cmap_category, cmap_list in cmaps.items():

    # Do subplots so that colormaps have enough space.
    # Default is 6 colormaps per subplot.
    dsub = _DSUBS.get(cmap_category, 6)
    nsubplots = int(np.ceil(len(cmap_list) / dsub))

    # squeeze=False to handle similarly the case of a single subplot
    fig, axs = plt.subplots(nrows=nsubplots, squeeze=False,
                            figsize=(7, 2.6*nsubplots))

    for i, ax in enumerate(axs.flat):
        locs = []  # locations for text labels

        for j, cmap in enumerate(cmap_list[i*dsub:(i+1)*dsub]):

            # Get RGB values for colormap and convert the colormap in
            # CAM02-UCS colorspace.  lab[0, :, 0] is the lightness.
            rgb = mpl.colormaps[cmap](x)[np.newaxis, :, :3]
            lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb)
            # Plot colormap L values.  Do separately for each category
            # so each plot can be pretty.  To make scatter markers change
            # color along plot:
            # https://stackoverflow.com/q/8202605/
            if cmap_category == 'Sequential':
                # These colormaps all start at high lightness, but we want them
                # reversed to look nice in the plot, so reverse the order.
                y_ = lab[0, ::-1, 0]
                c_ = x[::-1]
            else:
                y_ = lab[0, :, 0]
                c_ = x

            dc = _DC.get(cmap_category, 1.4)  # cmaps horizontal spacing
            ax.scatter(x + j*dc, y_, c=c_, cmap=cmap, s=300, linewidths=0.0)
            # Store locations for colormap labels
            if cmap_category in ('Perceptually Uniform Sequential',
                                 'Sequential'):
                locs.append(x[-1] + j*dc)
            elif cmap_category in ('Diverging', 'Qualitative', 'Cyclic',
                                   'Miscellaneous', 'Sequential (2)'):
                locs.append(x[int(x.size/2.)] + j*dc)
        # Set up the axis limits:
        #   * the 1st subplot is used as a reference for the x-axis limits
        #   * lightness values goes from 0 to 100 (y-axis limits)
        ax.set_xlim(axs[0, 0].get_xlim())
        ax.set_ylim(0.0, 100.0)
        # Set up labels for colormaps
        ax.xaxis.set_ticks_position('top')
        ticker = mpl.ticker.FixedLocator(locs)
        ax.xaxis.set_major_locator(ticker)
        formatter = mpl.ticker.FixedFormatter(cmap_list[i*dsub:(i+1)*dsub])
        ax.xaxis.set_major_formatter(formatter)
        ax.xaxis.set_tick_params(rotation=50)
        ax.set_ylabel('Lightness $L^*$', fontsize=12)

    ax.set_xlabel(cmap_category + ' colormaps', fontsize=14)
    fig.tight_layout(h_pad=0.0, pad=1.5)
    plt.show()

原文明度图组分别展示感知均匀顺序型、顺序型、顺序型第二组、发散型、循环型、定性型与其他色图的明度变化。

灰度转换

彩色图可能会在黑白打印机上输出,因此必须留意灰度转换。如果没有认真考虑这一点,色图中的灰度可能不可预测地变化,导致读者难以理解图形。

灰度转换有多种方法,见 bw。较好的方法通常对像素的 RGB 值作线性组合,并按人对颜色强度的感知加权。一种非线性方法是使用像素的 (L^) 值。通常,这里适用的原则与感知表达数据相同:选择 (L^) 单调增加的色图,就能得到较合理的灰度打印效果。

据此,顺序型色图在灰度下有合理的表示。顺序型第二组中的一些色图也尚可,但 autumn、spring、summer 和 winter 的灰度变化很小。如果使用这样的色图并转为灰度,大量信息可能被映射到相同的灰度值。发散型色图大多从两端较暗的灰色过渡到中间的白色。

部分色图,如 PuOr 和 seismic,一侧的灰度明显比另一侧更暗,因此并不十分对称。coolwarm 的灰度范围较小,打印后的图形会更均一,从而丢失许多细节。叠加带标签的等高线可以帮助区分色图两侧,因为打印成灰度后已无法依赖颜色。

许多定性型和其他色图,例如 Accent、hsv、jet 和 turbo,在色图中从深灰变浅,又变回深灰。一旦打印为灰度,观众就难以正确解读其中的信息。

mpl.rcParams.update({'font.size': 14})
# Indices to step through colormap.
x = np.linspace(0.0, 1.0, 100)

gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))


def plot_color_gradients(cmap_category, cmap_list):
    fig, axs = plt.subplots(nrows=len(cmap_list), ncols=2)
    fig.subplots_adjust(top=0.95, bottom=0.01, left=0.2, right=0.99,
                        wspace=0.05)
    fig.suptitle(cmap_category + ' colormaps', fontsize=14, y=1.0, x=0.6)

    for ax, name in zip(axs, cmap_list):
        # Get RGB values for colormap.
        rgb = mpl.colormaps[name](x)[np.newaxis, :, :3]

        # Get colormap in CAM02-UCS colorspace. We want the lightness.
        lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb)
        L = lab[0, :, 0]
        L = np.float32(np.vstack((L, L, L)))
        ax[0].imshow(gradient, aspect='auto', cmap=mpl.colormaps[name])
        ax[1].imshow(L, aspect='auto', cmap='binary_r', vmin=0., vmax=100.)
        pos = list(ax[0].get_position().bounds)
        x_text = pos[0] - 0.01
        y_text = pos[1] + pos[3]/2.
        fig.text(x_text, y_text, name, va='center', ha='right', fontsize=10)

    # Turn off *all* ticks & spines, not just the ones with colormaps.
    for ax in axs.flat:
        ax.set_axis_off()

    plt.show()

for cmap_category, cmap_list in cmaps.items():

    plot_color_gradients(cmap_category, cmap_list)

原文彩色与灰度对照图组按上述七类色图逐项比较彩色条带与明度灰度条带。

色觉差异

关于色觉障碍有很多资料,例如 colorblindness。此外,还有工具能把图像转换成不同类型色觉障碍者看到的样子。

最常见的色觉障碍涉及红色与绿色的区分。因此,避免使用同时包含红色和绿色的色图,通常能减少很多问题。

参考资料

完整示例下载

官方页面提供 完整 Python 源码,也提供 Jupyter notebook 和 ZIP 下载入口,见原文末尾。页面记录的原脚本运行时间为 18.862 秒,这是官方生成文档时的记录,并非本次运行结果。


来源:Matplotlib Development Team《Choosing Colormaps in Matplotlib》,对应 Matplotlib 3.11.2 文档。原文;版本化源文件。Copyright (c) 2012- Matplotlib Development Team; All Rights Reserved。依据 Matplotlib 1.3.0 及以后版本许可使用;完整适用许可保存在同目录 license.txt。修改摘要:将正文翻译为中文,转换为 Markdown,保留示例代码及原图说明和官方链接;未修改示例代码,未运行或重新生成图形。原文图形以链接保留,视觉尚未核验。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容