Я хочу установить среднюю точку цветовой карты, то есть мои данные идут от -5 до 10, и я хочу, чтобы ноль был средней точкой. Я думаю, что способ сделать это - создать подклассы normalize и использовать норму, но я не нашел ни одного примера, и мне непонятно, что именно я должен реализовать?
Определение середины палитры в matplotlib
Ответы (9)
Обратите внимание, что в matplotlib версии 3.2+ добавлен класс TwoSlopeNorm. Я думаю, это покрывает ваш вариант использования. Его можно использовать так:
from matplotlib import colors
divnorm=colors.TwoSlopeNorm(vmin=-5., vcenter=0., vmax=10)
pcolormesh(your_data, cmap="coolwarm", norm=divnorm)
В matplotlib 3.1 класс назывался DivergingNorm.
norm
выполняет нормализацию вашего изображения. norms
идут рука об руку с палитрой.
- person Paul H; 26.09.2019
TwoSlopeNorm
: matplotlib.org/3.2.0/api/_as_gen/
- person macKaiver; 08.06.2020
Я знаю, что это поздно для игры, но я просто прошел через этот процесс и придумал решение, которое, возможно, менее надежно, чем подкласс нормализации, но намного проще. Я подумал, что было бы хорошо поделиться этим здесь для потомков.
Функция
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([])
Результаты примера:
start
и stop
не равны 0 и 1 соответственно, после того, как вы сделаете reg_index = np.linspace(start, stop, 257)
, вы больше не сможете предполагать, что значение 129 является средней точкой исходного cmap, поэтому полное изменение масштаба не имеет смысла при кадрировании. Кроме того, start
должно быть от 0 до 0,5 и stop
от 0,5 до 1, а не от 0 до 1, как вы указываете.
- person TheChymera; 26.04.2014
midpoint
данных равно 0 или 1. См. Мой ответ ниже для простого решения этой проблемы.
- person DaveTheScientist; 21.06.2017
Проще всего использовать аргументы vmin
и vmax
для imshow
(при условии, что вы работаете с данными изображения), а не создавать подклассы matplotlib.colors.Normalize
.
E.g.
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
. Я добавлю пример чуть позже (при условии, что кто-то другой меня не опередит ...).
- person Joe Kington; 13.09.2011
vmax=abs(Z).max(), vmin=-abs(Z).max()
- person endolith; 04.03.2013
Вот подкласс решения Normalize. Использовать это
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
Здесь я создаю подкласс 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()
Тот же пример только с положительными данными vals = np.array([[1., 3], [6, 10]])
Характеристики:
- Средняя точка приобретает средний цвет.
- Верхний и нижний диапазоны масштабируются с помощью одного и того же линейного преобразования.
- На палитре цветов отображается только тот цвет, который изображен на картинке.
- Кажется, работает нормально, даже если
vmin
большеmidpoint
(хотя не проверял все крайние случаи).
Это решение основано на классе с таким же именем с этой страницы
def __call__
)
- person Filipe; 20.01.2019
normalized_min
и normalized_max
принимаются как целые числа. Просто поставьте их как 0,0. Кроме того, чтобы получить правильный результат вашей фигуры, мне пришлось использовать vals = sp.array([[-5.0, 0.0], [5.0, 10.0]])
. В любом случае спасибо за ответ!
- person Filipe; 20.01.2019
Не уверен, что вы все еще ищете ответ. Для меня попытка создать подкласс 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
) в верхней части скрипта, чтобы убедиться, что он ведет себя хорошо.
Я использовал отличный ответ Пола Х, но столкнулся с проблемой, потому что некоторые из моих данных варьировались от отрицательных до положительных, в то время как другие наборы варьировались от 0 до положительных или от отрицательных до 0; в любом случае я хотел, чтобы 0 был окрашен в белый цвет (средняя точка цветовой карты, которую я использую). В существующей реализации, если ваше значение midpoint
равно 1 или 0, исходные сопоставления не перезаписывались. Вы можете увидеть это на следующем рисунке: Третий столбец выглядит правильно, но темно-синяя область во втором столбце и темно-красная область в остальных столбцах должны быть белыми. (их значения данных фактически равны 0). Использование моего исправления дает мне: Моя функция практически такая же, как и у Пола Х, с моими изменениями в начале цикла 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 #2
в код выше.
W
содержит мои данные. Мне удалось запустить это с помощью этих двух строк: cm = shiftedColorMap(cmap=plt.get_cmap("coolwarm"), min_val=W.min(), max_val=W.max(), name="coolwarm")
, затем pcolormesh(W, cmap=cm)
Тем не менее, я не рекомендую использовать эту функцию. Он дополнительно смещает центр палитры каждый раз при вызове. Я рекомендую использовать TwoSlopeNorm, как описано в stackoverflow.com/a/56699813/1273751
- person Homero Esmeraldo; 08.07.2021
Если вы не возражаете против определения соотношения между 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), вторая - конечное значение для предыдущего сегмента и третий - начальное значение для следующего сегмента - если вы хотите плавные градиенты, последние два всегда одинаковы. Дополнительные сведения см. в документации.
LinearSegmentedColormap.from_list()
(val,color)
и передать их как список в color
аргумент этого метода, где val0=0<val1<...<valN==1
.
- person maurizio; 07.09.2016
У меня была аналогичная проблема, но я хотел, чтобы максимальное значение было полностью красным, а низкие значения синего отсекали, чтобы это выглядело по существу так, как будто нижняя часть шкалы цветов была обрезана. Это сработало для меня (включая дополнительную прозрачность):
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()