在matplotlib中定义颜色图的中点


87

我想设置色彩图的中间点,即我的数据从-5到10,我希望零成为中间点。我认为实现此目标的方法是归一化规范化并使用规范,但是我没有找到任何示例,我也不清楚我到底要实现什么。


这称为“发散”或“双极”色图,其中色图的中心点很重要,数据在该点的上方和下方。sandia.gov/~kmorel/documents/ColorMaps
endolith

3
该线程中的所有答案似乎都相当复杂。这个出色的答案显示了易于使用的解决方案,同时,该解决方案也已纳入matplotlib文档的“自定义归一化:两个线性范围”部分
ImportanceOfBeingErnest

Answers:


14

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

from matplotlib import colors
colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)

在matplotlib 3.2中,该类已重命名为TwoSlopesNorm


这看起来很有趣,但是似乎在绘制之前必须使用它来转换数据。颜色栏的图例将与转换后的数据有关,而不是原始数据。
bli19年

3
@bli并非如此。对norm图像进行归一化。norms与色彩图齐头并进。
Paul H

1
烦人不赞成这种方式为3.2,没有文档至于如何取代它:matplotlib.org/3.2.0/api/_as_gen/...
daknowles

1
是的,文档尚不清楚。我认为它已更名为TwoSlopeNormmatplotlib.org/3.2.0/api/_as_gen/...
macKaiver

91

我知道这对游戏来说太晚了,但是我只是经历了这个过程,并提出了一个解决方案,该解决方案可能不如将子类归一化可靠,但是要简单得多。我认为最好在此分享以供后代使用。

功能

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

一个例子

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([])

示例结果:

在此处输入图片说明


非常感谢您的杰出贡献!但是,代码是不是能够种植和转移相同的彩色地图,和你的指示都有点不准确和误导。现在,我已修复此问题,并可以自由编辑您的帖子。另外,我已经将其包含在我的一个个人图书馆中,并添加了您作为作者。希望你不要介意。
TheChymera

@TheChymera右下角的颜色图已被裁剪和重新设置。随意使用此功能。
Paul H

是的,它具有,可悲的是,它看起来只是巧合而已。如果startstop分别不是0和1,则在执行之后reg_index = np.linspace(start, stop, 257),您将无法再假定值129是原始cmap的中点,因此,无论何时裁剪,整个重新缩放都没有意义。另外,start应从0到0.5,stop从0.5到1,而不是两个都从0到1。
TheChymera 2014年

@TheChymera我尝试了您的版本,并对它有两个想法。1)在我看来,您创建的索引全为257,在matplotlib中默认为256,我认为是吗?2)假设我的数据范围是-1到1000,它以正数为主,因此应该向正数分支添加更多的层/层。但是您的函数为负值和正值都提供了128个级别,因此我认为不均匀地划分级别会更“公平”。
杰森

这是一个很好的解决方案,但是如果midpoint数据的等于0或1 ,则失败。有关此问题的简单修复,请参见下面的答案。
DaveTheScientist

22

这是子类归一化的解决方案。使用它

norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)

这是课程:

import numpy as np
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 * (value - 0.5)
            if val < 0: 
                return  val*abs(vmin-midpoint) + midpoint
            else:
                return  val*abs(vmax-midpoint) + midpoint

除了日志或符号日志扩展之外,是否可以使用此类而不需要创建更多子类?我当前的用例已经使用“ norm = SymLogNorm(linthresh = 1)”
AnnanFay 2016年

完美,这正是我想要的。也许您应该添加一张图片来说明差异?在这里,中点位于条形图的中心,这与其他中点归一化器相反,在其他归一化器中,中点可以拖动到四肢。
凌晨

18

仅使用vminandvmax参数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()

在此处输入图片说明


1
是否可以将示例更新为高斯曲线,以便我们更好地查看颜色的渐变?
Dat Chu

3
我不喜欢这种解决方案,因为它没有使用可用颜色的全部动态范围。我也想举一个规范化的例子来构建一种规范化的符号。
tillsten 2011年

2
@tillsten-我很困惑,然后...如果您想在中间使用0,就不能使用颜色条的全部动态范围,对吧?那您要非线性标尺吗?对于大于0的值,一个标度,对于小于0的值,一个标度?是的,您需要子类化Normalize。我仅需添加一个示例(假设其他人并没有击败我...)。
乔·肯顿

@乔:是的,它不是线性的(更确切地说,是两个线性部分)。使用vmin / vmax时,不使用小于-5的值的色域(在某些应用程序中有意义,但在我的应用程序中没有)。
tillsten 2011年

2
Z中的通用数据:vmax=abs(Z).max(), vmin=-abs(Z).max()
endolith

12

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

import numpy as np
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 np.ma.masked_array(np.interp(value, x, y))


vals = np.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()

结果: 图片1

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

图片2

特性:

  • 中点获得中间颜色。
  • 上限和下限范围通过相同的线性变换重新缩放。
  • 颜色栏中仅显示图片上显示的颜色。
  • 即使vmin大于,它似乎也可以正常工作midpoint(尽管未测试所有边缘情况)。

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


3
最佳答案,因为它很简单。仅当您已经是想要成为超级专家的Matplotlib专家时,其他答案才是最佳的。大多数matplotlib寻求答案的人都只是想做一些事情来回家养狗和/或家人,对他们来说,这个答案是最好的。
sapo_cosmico

该解决方案确实确实是最好的,但不起作用!我只运行了测试脚本,结果是完全不同的(仅包括蓝色方块,没有红色)。@icemtel,可以请您检查一下吗?(旁边有缩进的问题def __call__
Filipe

好吧,我发现这个问题(S):在计算的数量normalized_minnormalized_max被当作整数。只需将其设置为0.0。另外,为了获得正确的图形输出,我不得不使用vals = sp.array([[-5.0, 0.0], [5.0, 10.0]]) 。无论如何,谢谢您的回答!
Filipe

嗨,@ Filipe,我无法在计算机上重现您的问题(Python 3.7,matplotlib 2.2.3,并且我认为在较新的版本上应该是相同的)。您有什么版本?无论如何,我做了一个小的编辑,使数组成为浮点型,并解决了缩进问题。感谢您指出
-icemtel

嗯..我刚尝试使用python3,它也可以工作。但是我正在使用python2.7。感谢您的修复和答复。使用非常简单!:)
Filipe

5

不知道您是否还在寻找答案。对我来说,尝试继承Normalize是不成功的。因此,我专注于手动创建新的数据集,刻度和刻度标签,以达到我认为您想要的效果。

scale在matplotlib中找到了该模块,该模块具有用于通过“ 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')

vmax = 10,vmin = -5和linthresh = 1e-4

随意调整VMAX脚本顶部的“常量”(例如),以确认其性能良好。


如下所示,感谢您的建议,我在子类化方面取得了成功。但是您的代码对于正确设置ticklabel仍然非常有用。
耕till

4

我使用了Paul H的出色答案,但遇到了一个问题,因为我的一些数据范围从负到正,而其他数据范围从0到正或从负到0。无论哪种情况,我都希望将0着色为白色(我正在使用的色图的中点)。在现有的实现中,如果您的midpoint值等于1或0,则不会覆盖原始映射。您可以在下图中看到: 编辑前的图形 第三列看起来正确,但是第二列中的深蓝色区域和其余列中的深红色区域都应该是白色的(它们的数据值实际上为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 /programming/7404116/defining-the-midpoint-of-a-colormap-in-matplotlib

    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

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


这看起来不错,但似乎参数已从Paul H的答案(和评论)中更改了...您可以在答案中添加示例调用吗?
Filipe

1

如果您不介意计算vmin,vmax和零之间的比率,则这是一个非常基本的线性映射,从蓝色到白色再到红色,根据比例设置白色z

def colormap(z):
    """custom colourmap for map plots"""

    cdict1 = {'red': ((0.0, 0.0, 0.0),
                      (z,   1.0, 1.0),
                      (1.0, 1.0, 1.0)),
              'green': ((0.0, 0.0, 0.0),
                        (z,   1.0, 1.0),
                        (1.0, 0.0, 0.0)),
              'blue': ((0.0, 1.0, 1.0),
                       (z,   1.0, 1.0),
                       (1.0, 0.0, 0.0))
              }

    return LinearSegmentedColormap('BlueRed1', cdict1)

cdict格式非常简单:行是要创建的渐变中的点:第一个条目是x值(沿渐变从0到1的比率),第二个是上一段的结束值,并且第三个是下一个片段的起始值-如果要平滑渐变,则后两个总是相同的。请参阅文档以获取更多详细信息。


1
还可以选择在LinearSegmentedColormap.from_list()元组中指定(val,color)并将它们作为列表传递给color此方法的 参数where val0=0<val1<...<valN==1
maurizio

0

我有一个类似的问题,但是我希望最高的值是全红色,但切掉蓝色的低值,这使得它看起来基本上像彩条的底部被切掉了。这对我有用(包括可选的透明度):

def shift_zero_bwr_colormap(z: float, transparent: bool = True):
    """shifted bwr colormap"""
    if (z < 0) or (z > 1):
        raise ValueError('z must be between 0 and 1')

    cdict1 = {'red': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)),
                      (z,   1.0, 1.0),
                      (1.0, 1.0, 1.0)),

              'green': ((0.0, max(-2*z+1, 0), max(-2*z+1, 0)),
                        (z,   1.0, 1.0),
                        (1.0, max(2*z-1,0),  max(2*z-1,0))),

              'blue': ((0.0, 1.0, 1.0),
                       (z,   1.0, 1.0),
                       (1.0, max(2*z-1,0), max(2*z-1,0))),
              }
    if transparent:
        cdict1['alpha'] = ((0.0, 1-max(-2*z+1, 0), 1-max(-2*z+1, 0)),
                           (z,   0.0, 0.0),
                           (1.0, 1-max(2*z-1,0),  1-max(2*z-1,0)))

    return LinearSegmentedColormap('shifted_rwb', cdict1)

cmap =  shift_zero_bwr_colormap(.3)

x = np.arange(0, np.pi, 0.1)
y = np.arange(0, 2*np.pi, 0.1)
X, Y = np.meshgrid(x, y)
Z = np.cos(X) * np.sin(Y) * 5 + 5
plt.plot([0, 10*np.pi], [0, 20*np.pi], color='c', lw=20, zorder=-3)
plt.imshow(Z, interpolation='nearest', origin='lower', cmap=cmap)
plt.colorbar()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.