Matplotlib 色图归一化指南

说明:文末提供完整示例代码下载。

使用色图的对象,默认将 vmin 到 vmax 之间的数据值线性映射到颜色。例如:

pcm = ax.pcolormesh(x, y, Z, vmin=-1., vmax=1., cmap='RdBu_r')

它将 Z 中的 -1 到 +1 线性映射到色图,因此 Z=0 对应 RdBu_r 的中心颜色,本例为白色。

Matplotlib 分两步完成:先将数据归一化到 [0, 1],再映射到色图索引。归一化类定义在 matplotlib.colors 中,默认线性归一化为 Normalize。

负责颜色映射的 Artist 用 vmin、vmax 构造 Normalize 实例,然后调用它:

>>> import matplotlib as mpl
>>> norm = mpl.colors.Normalize(vmin=-1, vmax=1)
>>> norm(0)
0.5

有些场景下,非线性映射更有用。

对数归一化

一种常见变换是对数据取以 10 为底的对数,适合展示跨越不同数量级的变化。colors.LogNorm 使用 log10 归一化。下例有两个峰,其中一个远小于另一个,采用对数归一化后,两者的形状和位置都更清晰:

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.cbook as cbook
import matplotlib.colors as colors

N = 100
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]

# A low hump with a spike coming out of the top right.  Needs to have
# z/colour axis on a log scale, so we see both hump and spike. A linear
# scale only shows the spike.
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2)
Z = Z1 + 50 * Z2

fig, ax = plt.subplots(2, 1)

pcm = ax[0].pcolor(X, Y, Z,
                   norm=colors.LogNorm(vmin=Z.min(), vmax=Z.max()),
                   cmap='PuBu_r', shading='auto')
fig.colorbar(pcm, ax=ax[0], extend='max')

pcm = ax[1].pcolor(X, Y, Z, cmap='PuBu_r', shading='auto')
fig.colorbar(pcm, ax=ax[1], extend='max')
plt.show()
线性与对数归一化对不同尺度峰值的显示效果
线性与对数归一化对不同尺度峰值的显示效果

以中心对称归一化

许多数据围绕某个中心对称,例如围绕零的正负异常值。希望把中心映射到 0.5,把偏离中心最远的值映射到 1.0,若其小于中心则映射到 0.0。colors.CenteredNorm 自动建立这种映射,适合搭配两端不同、中心为低饱和颜色的发散色图。

中心不是零时,用 vcenter 指定。两侧都需要对数缩放时,见下文 SymLogNorm;中心上下需要不同映射时,见 TwoSlopeNorm。

delta = 0.1
x = np.arange(-3.0, 4.001, delta)
y = np.arange(-4.0, 3.001, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (0.9*Z1 - 0.5*Z2) * 2

# select a divergent colormap
cmap = "coolwarm"

fig, (ax1, ax2) = plt.subplots(ncols=2)
pc = ax1.pcolormesh(Z, cmap=cmap)
fig.colorbar(pc, ax=ax1)
ax1.set_title('Normalize()')

pc = ax2.pcolormesh(Z, norm=colors.CenteredNorm(), cmap=cmap)
fig.colorbar(pc, ax=ax2)
ax2.set_title('CenteredNorm()')

plt.show()
Normalize 与 CenteredNorm 的中心映射对比
Normalize 与 CenteredNorm 的中心映射对比

对称对数归一化

数据同时有正负值,但又希望两侧使用对数缩放时,可以使用对称对数归一化。负数也按对数缩放,并映射到较小的归一化值。例如 vmin=-vmax 时,负数映射到 0–0.5,正数映射到 0.5–1。

零附近的对数值趋向无穷,因此必须在零周围保留一小段线性区间。linthresh 指定区间 (-linthresh, linthresh),linscale 控制该区间在色图中的大小。默认 linscale=1.0 时,线性区间正负两半各占用与对数区间一个数量级相同的空间。

N = 100
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (Z1 - Z2) * 2

fig, ax = plt.subplots(2, 1)

pcm = ax[0].pcolormesh(X, Y, Z,
                       norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03,
                                              vmin=-1.0, vmax=1.0, base=10),
                       cmap='RdBu_r', shading='auto')
fig.colorbar(pcm, ax=ax[0], extend='both')

pcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-np.max(Z), shading='auto')
fig.colorbar(pcm, ax=ax[1], extend='both')
plt.show()
对称对数归一化与零附近的线性区间
对称对数归一化与零附近的线性区间

幂律归一化

有时需要按幂律关系 y=x 的 gamma 次方重新映射颜色,使用 colors.PowerNorm,通过 gamma 指定指数。gamma=1.0 就是默认线性归一化。

注意:使用这种变换应有充分理由。技术读者通常熟悉线性和对数轴,幂律变换较少见,因此应明确告知读者已经使用它。

N = 100
X, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]
Z1 = (1 + np.sin(Y * 10.)) * X**2

fig, ax = plt.subplots(2, 1, layout='constrained')

pcm = ax[0].pcolormesh(X, Y, Z1, norm=colors.PowerNorm(gamma=0.5),
                       cmap='PuBu_r', shading='auto')
fig.colorbar(pcm, ax=ax[0], extend='max')
ax[0].set_title('PowerNorm()')

pcm = ax[1].pcolormesh(X, Y, Z1, cmap='PuBu_r', shading='auto')
fig.colorbar(pcm, ax=ax[1], extend='max')
ax[1].set_title('Normalize()')
plt.show()
PowerNorm 与线性 Normalize 对比
PowerNorm 与线性 Normalize 对比

离散边界

colors.BoundaryNorm 除了 vmin、vmax,还接受一系列边界,数据被映射到边界之间,颜色在线性分布的这些区间中分配。还可以用 extend 将上界和/或下界之外的值纳入颜色分配范围。例如:

>>> import matplotlib.colors as colors
>>> bounds = np.array([-0.25, -0.125, 0, 0.5, 1])
>>> norm = colors.BoundaryNorm(boundaries=bounds, ncolors=4)
>>> print(norm([-0.2, -0.15, -0.02, 0.3, 0.8, 0.99]))
[0 0 1 2 3 3]

与其他归一化器不同,它返回 0 到 ncolors-1 之间的值。

N = 100
X, Y = np.meshgrid(np.linspace(-3, 3, N), np.linspace(-2, 2, N))
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = ((Z1 - Z2) * 2)[:-1, :-1]

fig, ax = plt.subplots(2, 2, figsize=(8, 6), layout='constrained')
ax = ax.flatten()

# Default norm:
pcm = ax[0].pcolormesh(X, Y, Z, cmap='RdBu_r')
fig.colorbar(pcm, ax=ax[0], orientation='vertical')
ax[0].set_title('Default norm')

# Even bounds give a contour-like effect:
bounds = np.linspace(-1.5, 1.5, 7)
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)
pcm = ax[1].pcolormesh(X, Y, Z, norm=norm, cmap='RdBu_r')
fig.colorbar(pcm, ax=ax[1], extend='both', orientation='vertical')
ax[1].set_title('BoundaryNorm: 7 boundaries')

# Bounds may be unevenly spaced:
bounds = np.array([-0.2, -0.1, 0, 0.5, 1])
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)
pcm = ax[2].pcolormesh(X, Y, Z, norm=norm, cmap='RdBu_r')
fig.colorbar(pcm, ax=ax[2], extend='both', orientation='vertical')
ax[2].set_title('BoundaryNorm: nonuniform')

# With out-of-bounds colors:
bounds = np.linspace(-1.5, 1.5, 7)
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256, extend='both')
pcm = ax[3].pcolormesh(X, Y, Z, norm=norm, cmap='RdBu_r')
# The colorbar inherits the "extend" argument from BoundaryNorm.
fig.colorbar(pcm, ax=ax[3], orientation='vertical')
ax[3].set_title('BoundaryNorm: extend="both"')
plt.show()
默认归一化、均匀边界、不均匀边界与扩展边界的对比
默认归一化、均匀边界、不均匀边界与扩展边界的对比

TwoSlopeNorm:中心两侧采用不同映射

有时需要在概念中心两侧使用不同色图,并采用不同线性尺度。例如地形图以零作为陆地与海洋的分界,但陆地高程范围通常大于海水深度范围,两侧也经常采用不同色图。

dem = cbook.get_sample_data('topobathy.npz')
topo = dem['topo']
longitude = dem['longitude']
latitude = dem['latitude']

fig, ax = plt.subplots()
# make a colormap that has land and ocean clearly delineated and of the
# same length (256 + 256)
colors_undersea = plt.colormaps["terrain"](np.linspace(0, 0.17, 256))
colors_land = plt.colormaps["terrain"](np.linspace(0.25, 1, 256))
all_colors = np.vstack((colors_undersea, colors_land))
terrain_map = colors.LinearSegmentedColormap.from_list(
    'terrain_map', all_colors)

# make the norm:  Note the center is offset so that the land has more
# dynamic range:
divnorm = colors.TwoSlopeNorm(vmin=-500., vcenter=0, vmax=4000)

pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm,
                    cmap=terrain_map, shading='auto')
# Simple geographic plot, set aspect ratio because distance between lines of
# longitude depends on latitude.
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
ax.set_title('TwoSlopeNorm(x)')
cb = fig.colorbar(pcm, shrink=0.6)
cb.set_ticks([-500, 0, 1000, 2000, 3000, 4000])
plt.show()
TwoSlopeNorm 分别映射陆地高程与海洋深度
TwoSlopeNorm 分别映射陆地高程与海洋深度

让颜色条使用线性刻度

颜色条默认采用与归一化器相同的轴缩放。例如 TwoSlopeNorm 的色图段线性分布,而刻度位置非线性分布,如前图和下图左侧所示。

要让刻度间距改为线性,可以调用 cb.ax.set_yscale('linear'),如右侧所示。刻度会等距排列,两个斜率区域中较小的那个会在色图中显得被压缩。

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
divnorm = colors.TwoSlopeNorm(vmin=-500., vcenter=0, vmax=4000)

for ax, title in zip([ax1, ax2],
                     ['Default: Scaled colorbar', 'Linear colorbar spacing']):
    pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm,
                        cmap=terrain_map, shading='auto')
    ax.set_aspect(1 / np.cos(np.deg2rad(49)))
    ax.set_title(title)
    cb = fig.colorbar(pcm, ax=ax, shrink=0.6)
    cb.set_ticks(np.arange(-500, 4001, 500))

# Set linear scale for the right colorbar
cb.ax.set_yscale('linear')
默认缩放颜色条与线性刻度间距颜色条的对比
默认缩放颜色条与线性刻度间距颜色条的对比

FuncNorm:任意函数归一化

如果已有归一化器不满足要求,可以使用 FuncNorm 自定义。下例与指数为 0.5 的 PowerNorm 等价:

def _forward(x):
    return np.sqrt(x)


def _inverse(x):
    return x**2

N = 100
X, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]
Z1 = (1 + np.sin(Y * 10.)) * X**2
fig, ax = plt.subplots()

norm = colors.FuncNorm((_forward, _inverse), vmin=0, vmax=20)
pcm = ax.pcolormesh(X, Y, Z1, norm=norm, cmap='PuBu_r', shading='auto')
ax.set_title('FuncNorm(x)')
fig.colorbar(pcm, shrink=0.6)
plt.show()
使用 FuncNorm 自定义平方根归一化
使用 FuncNorm 自定义平方根归一化

自定义归一化:手动实现两个线性区间

前面的 TwoSlopeNorm 是定义自有归一化器的好例子。注意,为了让颜色条正常工作,必须提供逆变换:

class MidpointNormalize(colors.Normalize):
    def __init__(self, vmin=None, vmax=None, vcenter=None, clip=False):
        self.vcenter = vcenter
        super().__init__(vmin, vmax, clip)

    def __call__(self, value, clip=None):
        # I'm ignoring masked values and all kinds of edge cases to make a
        # simple example...
        # Note also that we must extrapolate beyond vmin/vmax
        x, y = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1.]
        return np.ma.masked_array(np.interp(value, x, y,
                                            left=-np.inf, right=np.inf))

    def inverse(self, value):
        y, x = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1]
        return np.interp(value, x, y, left=-np.inf, right=np.inf)


fig, ax = plt.subplots()
midnorm = MidpointNormalize(vmin=-500., vcenter=0, vmax=4000)

pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=midnorm,
                    cmap=terrain_map, shading='auto')
ax.set_aspect(1 / np.cos(np.deg2rad(49)))
ax.set_title('Custom norm')
cb = fig.colorbar(pcm, shrink=0.6, extend='both')
cb.set_ticks([-500, 0, 1000, 2000, 3000, 4000])

plt.show()
自行实现中心两侧的线性归一化
自行实现中心两侧的线性归一化

原文记录的脚本总运行时间为 0 分 9.756 秒。

原文图库由 Sphinx-Gallery 生成。


原文:Colormap normalization。作者/维护者:Matplotlib 文档贡献者。本文为原文的中文译文;代码保留原文内容。

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

请登录后发表评论

    暂无评论内容