在matplotlib中定义色彩映射的中点

til*_*ten 75 python matplotlib

我想设置一个色图的中间点,即我的数据从-5到10,我想零为中间.我认为这样做的方法是继承normalize和使用规范,但我没有找到任何例子,我不清楚,我究竟要实现什么.

Pau*_*l H 80

我知道这是游戏的后期,但我刚刚完成了这个过程,并提出了一个解决方案,它可能不如子类化规范化那么强大,但更简单.我认为在这里为后人分享它会很好.

功能

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import AxesGrid

def shiftedColorMap(cmap, start=0, midpoint=0.5, stop=1.0, name='shiftedcmap'):
    '''
    Function to offset the "center" of a colormap. Useful for
    data with a negative min and positive max and you want the
    middle of the colormap's dynamic range to be at zero.

    Input
    -----
      cmap : The matplotlib colormap to be altered
      start : Offset from lowest point in the colormap's range.
          Defaults to 0.0 (no lower offset). Should be between
          0.0 and `midpoint`.
      midpoint : The new center of the colormap. Defaults to 
          0.5 (no shift). Should be between 0.0 and 1.0. In
          general, this should be  1 - vmax / (vmax + abs(vmin))
          For example if your data range from -15.0 to +5.0 and
          you want the center of the colormap at 0.0, `midpoint`
          should be set to  1 - 5/(5 + 15)) or 0.75
      stop : Offset from highest point in the colormap's range.
          Defaults to 1.0 (no upper offset). Should be between
          `midpoint` and 1.0.
    '''
    cdict = {
        'red': [],
        'green': [],
        'blue': [],
        'alpha': []
    }

    # regular index to compute the colors
    reg_index = np.linspace(start, stop, 257)

    # shifted index to match the data
    shift_index = np.hstack([
        np.linspace(0.0, midpoint, 128, endpoint=False), 
        np.linspace(midpoint, 1.0, 129, endpoint=True)
    ])

    for ri, si in zip(reg_index, shift_index):
        r, g, b, a = cmap(ri)

        cdict['red'].append((si, r, r))
        cdict['green'].append((si, g, g))
        cdict['blue'].append((si, b, b))
        cdict['alpha'].append((si, a, a))

    newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
    plt.register_cmap(cmap=newcmap)

    return newcmap
Run Code Online (Sandbox Code Playgroud)

一个例子

biased_data = np.random.random_integers(low=-15, high=5, size=(37,37))

orig_cmap = matplotlib.cm.coolwarm
shifted_cmap = shiftedColorMap(orig_cmap, midpoint=0.75, name='shifted')
shrunk_cmap = shiftedColorMap(orig_cmap, start=0.15, midpoint=0.75, stop=0.85, name='shrunk')

fig = plt.figure(figsize=(6,6))
grid = AxesGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.5,
                label_mode="1", share_all=True,
                cbar_location="right", cbar_mode="each",
                cbar_size="7%", cbar_pad="2%")

# normal cmap
im0 = grid[0].imshow(biased_data, interpolation="none", cmap=orig_cmap)
grid.cbar_axes[0].colorbar(im0)
grid[0].set_title('Default behavior (hard to see bias)', fontsize=8)

im1 = grid[1].imshow(biased_data, interpolation="none", cmap=orig_cmap, vmax=15, vmin=-15)
grid.cbar_axes[1].colorbar(im1)
grid[1].set_title('Centered zero manually,\nbut lost upper end of dynamic range', fontsize=8)

im2 = grid[2].imshow(biased_data, interpolation="none", cmap=shifted_cmap)
grid.cbar_axes[2].colorbar(im2)
grid[2].set_title('Recentered cmap with function', fontsize=8)

im3 = grid[3].imshow(biased_data, interpolation="none", cmap=shrunk_cmap)
grid.cbar_axes[3].colorbar(im3)
grid[3].set_title('Recentered cmap with function\nand shrunk range', fontsize=8)

for ax in grid:
    ax.set_yticks([])
    ax.set_xticks([])
Run Code Online (Sandbox Code Playgroud)

结果示例:

在此输入图像描述


mac*_*ver 24

请注意,在 matplotlib 3.2+ 版中添加了TwoSlopeNorm类。我认为它涵盖了您的用例。它可以像这样使用:

from matplotlib import colors
divnorm=colors.TwoSlopeNorm(vmin=-5., vcenter=0., vmax=10)
pcolormesh(your_data, cmap="coolwarm", norm=divnorm)
Run Code Online (Sandbox Code Playgroud)

在 matplotlib 3.1 中,该类被称为DivergingNorm

  • @bli 事实并非如此。`norm` 对您的图像进行标准化。“规范”与色彩图密切相关。 (3认同)
  • 令人烦恼的是,从 3.2 开始,这已被弃用,没有关于如何替换它的文档:https://matplotlib.org/3.2.0/api/_as_gen/matplotlib.colors.DivergingNorm.html (2认同)
  • 是的,文档不清楚。我认为它已被重命名为“TwoSlopeNorm”:https://matplotlib.org/3.2.0/api/_as_gen/matplotlib.colors.TwoSlopeNorm.html#matplotlib.colors.TwoSlopeNorm (2认同)

til*_*ten 20

这是一个子类化Normalize的解决方案.使用它

norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)
Run Code Online (Sandbox Code Playgroud)

这是班级:

from numpy import ma
from matplotlib import cbook
from matplotlib.colors import Normalize

class MidPointNorm(Normalize):    
    def __init__(self, midpoint=0, vmin=None, vmax=None, clip=False):
        Normalize.__init__(self,vmin, vmax, clip)
        self.midpoint = midpoint

    def __call__(self, value, clip=None):
        if clip is None:
            clip = self.clip

        result, is_scalar = self.process_value(value)

        self.autoscale_None(result)
        vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

        if not (vmin < midpoint < vmax):
            raise ValueError("midpoint must be between maxvalue and minvalue.")       
        elif vmin == vmax:
            result.fill(0) # Or should it be all masked? Or 0.5?
        elif vmin > vmax:
            raise ValueError("maxvalue must be bigger than minvalue")
        else:
            vmin = float(vmin)
            vmax = float(vmax)
            if clip:
                mask = ma.getmask(result)
                result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
                                  mask=mask)

            # ma division is very slow; we can take a shortcut
            resdat = result.data

            #First scale to -1 to 1 range, than to from 0 to 1.
            resdat -= midpoint            
            resdat[resdat>0] /= abs(vmax - midpoint)            
            resdat[resdat<0] /= abs(vmin - midpoint)

            resdat /= 2.
            resdat += 0.5
            result = ma.array(resdat, mask=result.mask, copy=False)                

        if is_scalar:
            result = result[0]            
        return result

    def inverse(self, value):
        if not self.scaled():
            raise ValueError("Not invertible until scaled")
        vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

        if cbook.iterable(value):
            val = ma.asarray(value)
            val = 2 * (val-0.5)  
            val[val>0]  *= abs(vmax - midpoint)
            val[val<0] *= abs(vmin - midpoint)
            val += midpoint
            return val
        else:
            val = 2 * (val - 0.5)
            if val < 0: 
                return  val*abs(vmin-midpoint) + midpoint
            else:
                return  val*abs(vmax-midpoint) + midpoint
Run Code Online (Sandbox Code Playgroud)


Joe*_*ton 16

最简单的方法是使用vminvmax参数imshow(假设你正在处理图像数据)而不是子类化matplotlib.colors.Normalize.

例如

import numpy as np
import matplotlib.pyplot as plt

data = np.random.random((10,10))
# Make the data range from about -5 to 10
data = 10 / 0.75 * (data - 0.25)

plt.imshow(data, vmin=-10, vmax=10)
plt.colorbar()

plt.show()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

  • 我不喜欢这种解决方案,因为它不使用可用颜色的完整动态范围.另外,我想举一个规范化的例子来构建一个symlog类的规范化. (2认同)
  • @tillsten - 我很困惑,然后......如果你想在中间0,你不能使用colorbar的完整动态范围,对吗?那你想要一个非线性的尺度吗?值大于0的一个刻度,下面的值是一个?在那种情况下,是的,你需要继承`Normalize`.我将稍微添加一个例子(假设其他人没有打败我...). (2认同)
  • 对于Z中的通用数据:`vmax = abs(Z).max(),vmin = -abs(Z).max()` (2认同)

ice*_*tel 8

此解决方案的灵感来自此页面上同名的类

在这里,我创建一个的子类,Normalize然后是一个最小的示例。

import scipy as sp
import matplotlib as mpl
import matplotlib.pyplot as plt


class MidpointNormalize(mpl.colors.Normalize):
    def __init__(self, vmin, vmax, midpoint=0, clip=False):
        self.midpoint = midpoint
        mpl.colors.Normalize.__init__(self, vmin, vmax, clip)

    def __call__(self, value, clip=None):
        normalized_min = max(0, 1 / 2 * (1 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax))))
        normalized_max = min(1, 1 / 2 * (1 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin))))
        normalized_mid = 0.5
        x, y = [self.vmin, self.midpoint, self.vmax], [normalized_min, normalized_mid, normalized_max]
        return sp.ma.masked_array(sp.interp(value, x, y))


vals = sp.array([[-5., 0], [5, 10]]) 
vmin = vals.min()
vmax = vals.max()

norm = MidpointNormalize(vmin=vmin, vmax=vmax, midpoint=0)
cmap = 'RdBu_r' 

plt.imshow(vals, cmap=cmap, norm=norm)
plt.colorbar()
plt.show()
Run Code Online (Sandbox Code Playgroud)

结果: 图片1

同样的例子只有正面数据 vals = sp.array([[1., 3], [6, 10]])

图片2

总结一下-该规范具有以下属性:

  • 中点将获得中间颜色。
  • 上限和下限范围将以相同的方式重新缩放,因此使用正确的色图,颜色饱和度将对应于距中点的距离。
  • 颜色栏将仅显示图片上显示的颜色。
  • 即使vmin大于,似乎也可以正常工作midpoint(尽管未测试所有边缘情况)。

  • 由于其简单性,最佳答案。只有当您已经是试图成为超级专家的 Matplotlib 专家时,其他答案才是最好的。大多数 matplotlib 答案寻求者只是想完成一些事情以回家给他们的狗和/或家人,对他们来说,这个答案是最好的。 (3认同)

Yan*_*ann 5

不确定你是否还在寻找答案.对我来说,尝试子类Normalize是不成功的.所以我专注于手动创建一个新的数据集,刻度和刻度标签,以获得我认为你的目标.

我发现scalematplotlib中的模块有一个用于通过'syslog'规则转换线图的类,所以我用它来转换数据.然后我缩放数据,使其从0变为1(Normalize通常是这样),但我将正数与负数不同地缩放.这是因为你的vmax和vmin可能不一样,所以.5 - > 1可能会覆盖比.5 - > 0更大的正范围,负范围也是如此.我更容易创建一个例程来计算刻度和标签值.

下面是代码和示例图.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.mpl as mpl
import matplotlib.scale as scale

NDATA = 50
VMAX=10
VMIN=-5
LINTHRESH=1e-4

def makeTickLables(vmin,vmax,linthresh):
    """
    make two lists, one for the tick positions, and one for the labels
    at those positions. The number and placement of positive labels is 
    different from the negative labels.
    """
    nvpos = int(np.log10(vmax))-int(np.log10(linthresh))
    nvneg = int(np.log10(np.abs(vmin)))-int(np.log10(linthresh))+1
    ticks = []
    labels = []
    lavmin = (np.log10(np.abs(vmin)))
    lvmax = (np.log10(np.abs(vmax)))
    llinthres = int(np.log10(linthresh))
    # f(x) = mx+b
    # f(llinthres) = .5
    # f(lavmin) = 0
    m = .5/float(llinthres-lavmin)
    b = (.5-llinthres*m-lavmin*m)/2
    for itick in range(nvneg):
        labels.append(-1*float(pow(10,itick+llinthres)))
        ticks.append((b+(itick+llinthres)*m))
    # add vmin tick
    labels.append(vmin)
    ticks.append(b+(lavmin)*m)

    # f(x) = mx+b
    # f(llinthres) = .5
    # f(lvmax) = 1
    m = .5/float(lvmax-llinthres)
    b = m*(lvmax-2*llinthres) 
    for itick in range(1,nvpos):
        labels.append(float(pow(10,itick+llinthres)))
        ticks.append((b+(itick+llinthres)*m))
    # add vmax tick
    labels.append(vmax)
    ticks.append(b+(lvmax)*m)

    return ticks,labels


data = (VMAX-VMIN)*np.random.random((NDATA,NDATA))+VMIN

# define a scaler object that can transform to 'symlog'
scaler = scale.SymmetricalLogScale.SymmetricalLogTransform(10,LINTHRESH)
datas = scaler.transform(data)

# scale datas so that 0 is at .5
# so two seperate scales, one for positive and one for negative
data2 = np.where(np.greater(data,0),
                 .75+.25*datas/np.log10(VMAX),
                 .25+.25*(datas)/np.log10(np.abs(VMIN))
                 )

ticks,labels=makeTickLables(VMIN,VMAX,LINTHRESH)

cmap = mpl.cm.jet
fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(data2,cmap=cmap,vmin=0,vmax=1)
cbar = plt.colorbar(im,ticks=ticks)
cbar.ax.set_yticklabels(labels)

fig.savefig('twoscales.png')
Run Code Online (Sandbox Code Playgroud)

vmax = 10,vmin = -5,并且linthresh = 1e-4

您可以随意调整VMAX脚本顶部的"常量"(例如)以确认其运行良好.


Dav*_*ist 5

我使用了 Paul H 的优秀答案,但遇到了一个问题,因为我的一些数据范围从负数到正数,而其他数据集的范围从 0 到正数或从负数到 0;在任何一种情况下,我都希望将 0 着色为白色(我正在使用的颜色图的中点)。对于现有实现,如果您的midpoint值等于 1 或 0,则不会覆盖原始映射。您可以在下图中看到: 编辑前的图表 第 3 列看起来是正确的,但第 2 列中的深蓝色区域和其余列中的深红色区域都应该是白色的(它们的数据值实际上是 0)。使用我的修复程序给了我: 编辑后的图形 我的功能与 Paul H 的功能基本相同,我在for循环开始时进行了编辑:

def shiftedColorMap(cmap, min_val, max_val, name):
    '''Function to offset the "center" of a colormap. Useful for data with a negative min and positive max and you want the middle of the colormap's dynamic range to be at zero. Adapted from /sf/ask/518288151/

    Input
    -----
      cmap : The matplotlib colormap to be altered.
      start : Offset from lowest point in the colormap's range.
          Defaults to 0.0 (no lower ofset). Should be between
          0.0 and `midpoint`.
      midpoint : The new center of the colormap. Defaults to
          0.5 (no shift). Should be between 0.0 and 1.0. In
          general, this should be  1 - vmax/(vmax + abs(vmin))
          For example if your data range from -15.0 to +5.0 and
          you want the center of the colormap at 0.0, `midpoint`
          should be set to  1 - 5/(5 + 15)) or 0.75
      stop : Offset from highets point in the colormap's range.
          Defaults to 1.0 (no upper ofset). Should be between
          `midpoint` and 1.0.'''
    epsilon = 0.001
    start, stop = 0.0, 1.0
    min_val, max_val = min(0.0, min_val), max(0.0, max_val) # Edit #2
    midpoint = 1.0 - max_val/(max_val + abs(min_val))
    cdict = {'red': [], 'green': [], 'blue': [], 'alpha': []}
    # regular index to compute the colors
    reg_index = np.linspace(start, stop, 257)
    # shifted index to match the data
    shift_index = np.hstack([np.linspace(0.0, midpoint, 128, endpoint=False), np.linspace(midpoint, 1.0, 129, endpoint=True)])
    for ri, si in zip(reg_index, shift_index):
        if abs(si - midpoint) < epsilon:
            r, g, b, a = cmap(0.5) # 0.5 = original midpoint.
        else:
            r, g, b, a = cmap(ri)
        cdict['red'].append((si, r, r))
        cdict['green'].append((si, g, g))
        cdict['blue'].append((si, b, b))
        cdict['alpha'].append((si, a, a))
    newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
    plt.register_cmap(cmap=newcmap)
    return newcmap
Run Code Online (Sandbox Code Playgroud)

编辑:当我的一些数据范围从一个小的正值到一个更大的正值时,我再次遇到了类似的问题,其中非常低的值被着色为红色而不是白色。我通过Edit #2在上面的代码中添加行来修复它。


小智 5

对于 matplotlib 3.4 或更高版本,最简单的解决方案可能是使用新的CenteredNorm

使用 CenteredNorm 和发散颜色图之一的示例:

import matplotlib.pyplot as plt
import matplotlib as mpl

plt.pcolormesh(data_to_plot, norm=mpl.colors.CenteredNorm(), cmap='coolwarm')
Run Code Online (Sandbox Code Playgroud)

简单来说,CenteredNorm它是对称的,因此如果数据从 -5 到 10,颜色图将从 -10 拉伸到 10。如果您希望在中心的两侧有不同的映射,则颜色图范围从 -5到 10,使用TwoSlopeNorm,如 @macKaiver 的答案中所述。