注意
前往末尾 下载完整示例代码。
在 Matplotlib 中选择色彩映射#
Matplotlib 有许多内置的色彩映射,可通过 matplotlib.colormaps
访问。还有许多外部库提供了额外的色彩映射,这些可以在 Matplotlib 文档的 第三方色彩映射 部分查看。这里我们简要讨论如何在众多选项中进行选择。有关创建自己的色彩映射的帮助,请参阅 在 Matplotlib 中创建色彩映射。
要获取所有注册色彩映射的列表,您可以执行以下操作:
from matplotlib import colormaps
list(colormaps)
概述#
选择一个好的色彩映射的理念是为您的数据集在三维色彩空间中找到一个好的表示。对于任何给定数据集的最佳色彩映射取决于许多因素,包括:
是表示形式数据还是度量数据 ([Ware])
您对数据集的了解(例如,是否存在一个关键值,其他值都偏离该值?)
如果您正在绘制的参数有直观的配色方案
如果该领域存在受众可能期望的标准
对于许多应用而言,感知均匀色彩映射是最佳选择;也就是说,色彩映射中的数据等步长在色彩空间中被感知为等步长。研究人员发现,人脑感知亮度参数的变化比感知色相变化更能有效地识别数据变化。因此,在整个色彩映射中亮度单调递增的色彩映射将更容易被观察者理解。在 第三方色彩映射 部分也能找到感知均匀色彩映射的精彩示例。
颜色可以通过多种方式在三维空间中表示。一种表示颜色的方式是使用 CIELAB。在 CIELAB 中,色彩空间由亮度 \(L^*\);红-绿 \(a^*\);以及黄-蓝 \(b^*\) 表示。亮度参数 \(L^*\) 可用于进一步了解 Matplotlib 色彩映射将如何被观察者感知。
了解人类对色彩映射感知的极佳入门资源来自 [IBM]。
色彩映射的类别#
色彩映射通常根据其功能分为几个类别(参见,例如,[Moreland])
顺序型:亮度(通常还有颜色饱和度)逐渐变化,通常使用单一色相;应用于表示具有顺序的信息。
发散型:两种不同颜色的亮度和可能还有饱和度发生变化,并在中间以非饱和色相交;应用于绘制的信息具有关键中间值时,例如地形图或数据围绕零点偏离时。
循环型:两种不同颜色的亮度发生变化,在中间以及起始/结束点以非饱和色相交;应用于端点处会循环的值,例如相位角、风向或一天中的时间。
定性型:通常是各种杂色;应用于表示不具备顺序或关系的信息。
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(二进制和其他灰度),而其他则从 \(L^*=20\) 左右开始。那些 \(L^*\) 范围较小的,其感知范围也相应较小。另请注意,\(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'])

顺序型2#
顺序型2图中的许多 \(L^*\) 值是单调递增的,但有些(autumn、cool、spring 和 winter)会形成平台或甚至在 \(L^*\) 空间中上下波动。其他(afmhot、copper、gist_heat 和 hot)的 \(L^*\) 函数存在扭结。在色彩映射中处于平台或扭结区域表示的数据,会导致观察者对这些数据值产生条带感(参见 [mycarta-banding] 了解一个很好的例子)。
plot_color_gradients('Sequential (2)',
['binary', 'gist_yarg', 'gist_gray', 'gray', 'bone',
'pink', 'spring', 'summer', 'autumn', 'winter', 'cool',
'Wistia', 'hot', 'afmhot', 'gist_heat', 'copper'])

发散型#
对于发散型色彩映射,我们希望 \(L^*\) 值单调递增至最大值(应接近 \(L^*=100\)),然后单调递减。我们希望色彩映射两端的最小 \(L^*\) 值大致相等。根据这些标准,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^*\) 将反转方向,这应在 \(L^*\) 空间中进行平滑处理以减少伪影。有关循环型色彩映射设计的更多信息,请参阅 [kovesi-colormaps]。
常用的 HSV 色彩映射也包含在此色彩映射集中,尽管它不对称于中心点。此外,\(L^*\) 值在整个色彩映射中变化很大,使其成为感知上显示数据的糟糕选择。有关此观点的扩展,请参阅 [mycarta-jet]。
plot_color_gradients('Cyclic', ['twilight', 'twilight_shifted', 'hsv'])

定性型#
定性型色彩映射并非旨在成为感知映射,但查看亮度参数可以为我们证实这一点。\(L^*\) 值在整个色彩映射中波动很大,并且明显不是单调递增的。这些不适合用作感知色彩映射。
plot_color_gradients('Qualitative',
['Pastel1', 'Pastel2', 'Paired', 'Accent', '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^*\) 值上是单调递增的,那么它在转换为灰度时会以合理的方式呈现。
考虑到这一点,我们发现顺序型色彩映射在灰度表示上表现合理。一些顺序型2色彩映射具有足够好的灰度表示,尽管有些(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])。此外,还有一些工具可以将图像转换为在不同类型色觉缺陷下的显示效果。
最常见的色觉缺陷形式是区分红色和绿色。因此,通常避免使用同时包含红色和绿色的色彩映射将避免许多问题。
参考文献#
脚本总运行时间: (0 分钟 13.539 秒)