Определение средней точки цветовой карты в matplotlib
Я хочу установить среднюю точку цветовой карты, т. е. мои данные идут от -5 до 10, я хочу, чтобы ноль был серединой. Я думаю, что способ сделать это-нормализовать подклассы и использовать норму, но я не нашел ни одного примера, и мне не ясно, что именно я должен реализовать.
8 ответов:
Я знаю, что это поздно для игры, но я просто прошел через этот процесс и придумал решение, которое, возможно, менее надежное, чем нормализация подклассов, но гораздо проще. Я подумал, что было бы хорошо поделиться им здесь для потомков.
функции
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([])
результаты пример:
вот решение подклассов нормализовать. Чтобы использовать его
norm = MidPointNorm(midpoint=3) imshow(X, norm=norm)
вот класс:
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
проще всего просто использовать
vmin
иvmax
аргументы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()
не уверен, что вы все еще ищете ответ. Для меня, пытаясь подкласс
Normalize
была неудачной. Поэтому я сосредоточился на ручном создании нового набора данных, ТИКов и меток тиков, чтобы получить эффект, который, как я думаю, вы стремитесь.нашел
scale
модуль в matplotlib, который имеет класс, используемый для преобразования линейных графиков по правилам "системного журнала", поэтому я использую его для преобразования данных. Затем я масштабирую данные так, что они идут от 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
) в верхней части скрипта, чтобы убедиться, что он ведет себя что ж.
я использовал отличный ответ от Paul H, но столкнулся с проблемой, потому что некоторые из моих данных варьировались от отрицательного до положительного, в то время как другие наборы варьировались от 0 до положительного или от отрицательного до 0; в любом случае я хотел, чтобы 0 был окрашен в белый цвет (середина цветовой карты, которую я использую). С существующей реализацией, если ваш
midpoint
значение равно 1 или 0, исходные сопоставления не были перезаписаны. Вы можете видеть это на следующем рисунке: 3-е место столбец выглядит правильно, но темно-синяя область во 2-м столбце и темно-красная область в остальных столбцах все должны быть белыми (их значения данных на самом деле 0). Использование моего исправления дает мне: Моя функция по существу такая же, как у Павла 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 https://stackoverflow.com/questions/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: я столкнулся с подобной проблемой еще раз, когда некоторые из моих данных варьировались от небольшого положительного значения до большего положительного значение, где очень низкие значения были окрашены в красный цвет вместо белого. Я исправил это, добавив строку
Edit #2
в коде выше.
это решение вдохновлено классом с тем же именем от на этой странице
здесь я создаю подкласс
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()
и тот же пример только с положительными данными!--2-->
просто подведем итог-эта норма имеет следующие свойства:
- середина получит середину цвет.
- верхний и Нижний диапазоны будут масштабироваться таким же образом, чтобы при правильной цветовой карте насыщенность цвета соответствовала расстоянию от середины.
- на цветовой панели будут отображаться только цвета, которые появляются на картинке.
- кажется, работает нормально, даже если
vmin
больше, чемmidpoint
(не проверял все крайние случаи, хотя).
Если вы не против разработки соотношения между 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), второе-конечное значение для предыдущего сегмента, а третье-начальное значение для следующего сегмента - если вы нужны плавные градиенты, последние два всегда одинаковы. посмотреть документы подробнее.
У меня была аналогичная проблема, но я хотел, чтобы самое высокое значение было полным красным и отсекало низкие значения синего, что делает его похожим на то, что нижняя часть цветовой панели была отрублена. Это сработало для меня (включая дополнительную прозрачность):
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()