Chcę ustawić środkowy punkt mapy kolorów, tj. Moje dane mają zakres od -5 do 10, chcę, aby zero było środkiem. Myślę, że sposobem na to jest podklasa normalizacji i użycie normy, ale nie znalazłem żadnego przykładu i nie jest dla mnie jasne, co dokładnie mam zaimplementować.
python
matplotlib
tillsten
źródło
źródło
Odpowiedzi:
Zauważ, że w matplotlib w wersji 3.1 została dodana klasa DivergingNorm . Myślę, że obejmuje twój przypadek użycia. Można go używać w następujący sposób:
from matplotlib import colors colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)
W matplotlib 3.2 nazwa klasy została zmieniona na TwoSlopesNorm
źródło
norm
robi normalizacji obrazu.norms
idź ramię w ramię z colormaps.TwoSlopeNorm
: matplotlib.org/3.2.0/api/_as_gen/ ...Wiem, że to późna pora na grę, ale właśnie przeszedłem przez ten proces i wymyśliłem rozwiązanie, które być może jest mniej niezawodne niż normalizacja podklas, ale znacznie prostsze. Pomyślałem, że dobrze byłoby podzielić się tym tutaj dla potomności.
Funkcja
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
Przykład
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([])
Wyniki przykładu:
źródło
start
istop
nie wynoszą odpowiednio 0 i 1, po wykonaniu tej czynnościreg_index = np.linspace(start, stop, 257)
nie można już zakładać, że wartość 129 jest środkiem oryginalnego cmap, dlatego całe przeskalowanie nie ma sensu przy każdym kadrowaniu. Ponadto,start
powinny być od 0 do 0,5 istop
od 0,5 do 1, nie zarówno od 0 do 1, jak poinstruować.midpoint
dane są równe 0 lub 1. Zobacz moją odpowiedź poniżej, aby znaleźć proste rozwiązanie tego problemu.Oto podklasa rozwiązania Normalizuj. Aby z niego skorzystać
norm = MidPointNorm(midpoint=3) imshow(X, norm=norm)
Oto klasa:
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
źródło
Najłatwiej jest po prostu użyć argumentów
vmin
i (zakładając, że pracujesz z danymi obrazu), a nie podklasy .vmax
imshow
matplotlib.colors.Normalize
Na przykład
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()
źródło
Normalize
. Za chwilę dodam przykład (zakładając, że ktoś inny mnie w tym nie pobije ...).vmax=abs(Z).max(), vmin=-abs(Z).max()
Tutaj tworzę podklasę,
Normalize
a następnie minimalny przykład.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()
Wynik:
Ten sam przykład z tylko dodatnimi danymi
vals = np.array([[1., 3], [6, 10]])
Nieruchomości:
vmin
jest większy niżmidpoint
(chociaż nie przetestowano wszystkich przypadków skrajnych).To rozwiązanie jest inspirowane klasą o tej samej nazwie z tej strony
źródło
def __call__
)normalized_min
inormalized_max
są traktowane jako liczby całkowite. Po prostu ustaw je jako 0,0. Ponadto, aby uzyskać prawidłowe wyniki twojej figury, musiałem użyćvals = sp.array([[-5.0, 0.0], [5.0, 10.0]])
. W każdym razie dzięki za odpowiedź!Nie jestem pewien, czy nadal szukasz odpowiedzi. Dla mnie próba podklasy
Normalize
zakończyła się niepowodzeniem. Skupiłem się więc na ręcznym utworzeniu nowego zestawu danych, znaczników i etykiet znaczników, aby uzyskać efekt, do którego myślę, że chcesz.Znalazłem
scale
moduł w matplotlib, który ma klasę używaną do przekształcania wykresów liniowych według reguł 'syslog', więc używam go do transformacji danych. Następnie skaluję dane tak, aby przechodziły od 0 do 1 (coNormalize
zwykle ma miejsce), ale skaluję liczby dodatnie inaczej niż liczby ujemne. Dzieje się tak, ponieważ twoje vmax i vmin mogą nie być takie same, więc .5 -> 1 może obejmować większy zakres dodatni niż .5 -> 0, zakres ujemny. Łatwiej było mi stworzyć procedurę obliczania wartości tików i etykiet.Poniżej znajduje się kod i przykładowy rysunek.
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')
Możesz dowolnie dostosować „stałe” (np.
VMAX
) U góry skryptu, aby potwierdzić, że zachowuje się on dobrze.źródło
Używałem doskonałej odpowiedzi Paula H., ale napotkałem problem, ponieważ niektóre z moich danych wahały się od negatywnych do pozytywnych, podczas gdy inne zestawy zawierały się w zakresie od 0 do pozytywnych lub od negatywnych do 0; w obu przypadkach chciałem, aby 0 było pokolorowane jako białe (środek mapy kolorów, której używam). W istniejącej implementacji, jeśli
midpoint
wartość jest równa 1 lub 0, oryginalne mapowania nie zostały nadpisane. Możesz to zobaczyć na poniższym rysunku: Trzecia kolumna wygląda poprawnie, ale ciemnoniebieski obszar w drugiej kolumnie i ciemnoczerwony obszar w pozostałych kolumnach powinny być białe (ich wartości danych w rzeczywistości wynoszą 0). Użycie mojej poprawki daje mi: Moja funkcja jest zasadniczo taka sama, jak funkcja Paula H, z moimi edycjami na początkufor
pętli: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
EDYCJA: Ponownie napotkałem podobny problem, gdy niektóre z moich danych wahały się od małej wartości dodatniej do większej wartości dodatniej, gdzie bardzo niskie wartości były zabarwione na czerwono zamiast na biało. Naprawiłem to, dodając wiersz
Edit #2
w powyższym kodzie.źródło
Jeśli nie masz nic przeciwko obliczeniu stosunku między vmin, vmax i zerem, to jest to dość podstawowa liniowa mapa od niebieskiego do białego do czerwonego, która ustawia biały zgodnie ze stosunkiem
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)
Format cdict jest dość prosty: wiersze to punkty w utworzonym gradiencie: pierwszy wpis to wartość x (stosunek wzdłuż gradientu od 0 do 1), drugi to wartość końcowa poprzedniego segmentu, a trzecia to wartość początkowa następnego segmentu - jeśli chcesz płynnych gradientów, dwa ostatnie są zawsze takie same. Więcej szczegółów znajdziesz w dokumentacji .
źródło
LinearSegmentedColormap.from_list()
krotek(val,color)
i przekazania ich jako listy docolor
argumentu tej metody gdzieval0=0<val1<...<valN==1
.Miałem podobny problem, ale chciałem, aby najwyższa wartość była pełna czerwieni i odcięła niskie wartości niebieskiego, dzięki czemu wyglądało to tak, jakby dolna część paska kolorów została odcięta. To zadziałało dla mnie (obejmuje opcjonalną przezroczystość):
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()
źródło