123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151 |
- """
- A module for converting numbers or color arguments to *RGB* or *RGBA*.
- *RGB* and *RGBA* are sequences of, respectively, 3 or 4 floats in the
- range 0-1.
- This module includes functions and classes for color specification conversions,
- and for mapping numbers to colors in a 1-D array of colors called a colormap.
- Mapping data onto colors using a colormap typically involves two steps: a data
- array is first mapped onto the range 0-1 using a subclass of `Normalize`,
- then this number is mapped to a color using a subclass of `Colormap`. Two
- sublasses of `Colormap` provided here: `LinearSegmentedColormap`, which uses
- piecewise-linear interpolation to define colormaps, and `ListedColormap`, which
- makes a colormap from a list of colors.
- .. seealso::
- :doc:`/tutorials/colors/colormap-manipulation` for examples of how to
- make colormaps and
- :doc:`/tutorials/colors/colormaps` for a list of built-in colormaps.
- :doc:`/tutorials/colors/colormapnorms` for more details about data
- normalization
- More colormaps are available at palettable_.
- The module also provides functions for checking whether an object can be
- interpreted as a color (`is_color_like`), for converting such an object
- to an RGBA tuple (`to_rgba`) or to an HTML-like hex string in the
- "#rrggbb" format (`to_hex`), and a sequence of colors to an (n, 4)
- RGBA array (`to_rgba_array`). Caching is used for efficiency.
- Matplotlib recognizes the following formats to specify a color:
- * an RGB or RGBA (red, green, blue, alpha) tuple of float values in closed
- interval ``[0, 1]`` (e.g., ``(0.1, 0.2, 0.5)`` or ``(0.1, 0.2, 0.5, 0.3)``);
- * a hex RGB or RGBA string (e.g., ``'#0f0f0f'`` or ``'#0f0f0f80'``;
- case-insensitive);
- * a shorthand hex RGB or RGBA string, equivalent to the hex RGB or RGBA
- string obtained by duplicating each character, (e.g., ``'#abc'``, equivalent
- to ``'#aabbcc'``, or ``'#abcd'``, equivalent to ``'#aabbccdd'``;
- case-insensitive);
- * a string representation of a float value in ``[0, 1]`` inclusive for gray
- level (e.g., ``'0.5'``);
- * one of the characters ``{'b', 'g', 'r', 'c', 'm', 'y', 'k', 'w'}``, which
- are short-hand notations for shades of blue, green, red, cyan, magenta,
- yellow, black, and white. Note that the colors ``'g', 'c', 'm', 'y'`` do not
- coincide with the X11/CSS4 colors. Their particular shades were chosen for
- better visibility of colored lines against typical backgrounds.
- * a X11/CSS4 color name (case-insensitive);
- * a name from the `xkcd color survey`_, prefixed with ``'xkcd:'`` (e.g.,
- ``'xkcd:sky blue'``; case insensitive);
- * one of the Tableau Colors from the 'T10' categorical palette (the default
- color cycle): ``{'tab:blue', 'tab:orange', 'tab:green', 'tab:red',
- 'tab:purple', 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan'}``
- (case-insensitive);
- * a "CN" color spec, i.e. 'C' followed by a number, which is an index into the
- default property cycle (:rc:`axes.prop_cycle`); the indexing is intended to
- occur at rendering time, and defaults to black if the cycle does not include
- color.
- .. _palettable: https://jiffyclub.github.io/palettable/
- .. _xkcd color survey: https://xkcd.com/color/rgb/
- """
- from collections.abc import Sized
- import functools
- import itertools
- from numbers import Number
- import re
- import numpy as np
- import matplotlib.cbook as cbook
- from matplotlib import docstring
- from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
- class _ColorMapping(dict):
- def __init__(self, mapping):
- super().__init__(mapping)
- self.cache = {}
- def __setitem__(self, key, value):
- super().__setitem__(key, value)
- self.cache.clear()
- def __delitem__(self, key):
- super().__delitem__(key)
- self.cache.clear()
- _colors_full_map = {}
- # Set by reverse priority order.
- _colors_full_map.update(XKCD_COLORS)
- _colors_full_map.update({k.replace('grey', 'gray'): v
- for k, v in XKCD_COLORS.items()
- if 'grey' in k})
- _colors_full_map.update(CSS4_COLORS)
- _colors_full_map.update(TABLEAU_COLORS)
- _colors_full_map.update({k.replace('gray', 'grey'): v
- for k, v in TABLEAU_COLORS.items()
- if 'gray' in k})
- _colors_full_map.update(BASE_COLORS)
- _colors_full_map = _ColorMapping(_colors_full_map)
- def get_named_colors_mapping():
- """Return the global mapping of names to named colors."""
- return _colors_full_map
- def _sanitize_extrema(ex):
- if ex is None:
- return ex
- try:
- ret = ex.item()
- except AttributeError:
- ret = float(ex)
- return ret
- def _is_nth_color(c):
- """Return whether *c* can be interpreted as an item in the color cycle."""
- return isinstance(c, str) and re.match(r"\AC[0-9]+\Z", c)
- def is_color_like(c):
- """Return whether *c* can be interpreted as an RGB(A) color."""
- # Special-case nth color syntax because it cannot be parsed during setup.
- if _is_nth_color(c):
- return True
- try:
- to_rgba(c)
- except ValueError:
- return False
- else:
- return True
- def same_color(c1, c2):
- """
- Return whether the colors *c1* and *c2* are the same.
- *c1*, *c2* can be single colors or lists/arrays of colors.
- """
- c1 = to_rgba_array(c1)
- c2 = to_rgba_array(c2)
- n1 = max(c1.shape[0], 1) # 'none' results in shape (0, 4), but is 1-elem
- n2 = max(c2.shape[0], 1) # 'none' results in shape (0, 4), but is 1-elem
- if n1 != n2:
- raise ValueError('Different number of elements passed.')
- # The following shape test is needed to correctly handle comparisons with
- # 'none', which results in a shape (0, 4) array and thus cannot be tested
- # via value comparison.
- return c1.shape == c2.shape and (c1 == c2).all()
- def to_rgba(c, alpha=None):
- """
- Convert *c* to an RGBA color.
- Parameters
- ----------
- c : Matplotlib color or ``np.ma.masked``
- alpha : float, optional
- If *alpha* is not ``None``, it forces the alpha value, except if *c* is
- ``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``.
- Returns
- -------
- tuple
- Tuple of ``(r, g, b, a)`` scalars.
- """
- # Special-case nth color syntax because it should not be cached.
- if _is_nth_color(c):
- from matplotlib import rcParams
- prop_cycler = rcParams['axes.prop_cycle']
- colors = prop_cycler.by_key().get('color', ['k'])
- c = colors[int(c[1:]) % len(colors)]
- try:
- rgba = _colors_full_map.cache[c, alpha]
- except (KeyError, TypeError): # Not in cache, or unhashable.
- rgba = None
- if rgba is None: # Suppress exception chaining of cache lookup failure.
- rgba = _to_rgba_no_colorcycle(c, alpha)
- try:
- _colors_full_map.cache[c, alpha] = rgba
- except TypeError:
- pass
- return rgba
- def _to_rgba_no_colorcycle(c, alpha=None):
- """
- Convert *c* to an RGBA color, with no support for color-cycle syntax.
- If *alpha* is not ``None``, it forces the alpha value, except if *c* is
- ``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``.
- """
- orig_c = c
- if c is np.ma.masked:
- return (0., 0., 0., 0.)
- if isinstance(c, str):
- if c.lower() == "none":
- return (0., 0., 0., 0.)
- # Named color.
- try:
- # This may turn c into a non-string, so we check again below.
- c = _colors_full_map[c]
- except KeyError:
- if len(orig_c) != 1:
- try:
- c = _colors_full_map[c.lower()]
- except KeyError:
- pass
- if isinstance(c, str):
- # hex color in #rrggbb format.
- match = re.match(r"\A#[a-fA-F0-9]{6}\Z", c)
- if match:
- return (tuple(int(n, 16) / 255
- for n in [c[1:3], c[3:5], c[5:7]])
- + (alpha if alpha is not None else 1.,))
- # hex color in #rgb format, shorthand for #rrggbb.
- match = re.match(r"\A#[a-fA-F0-9]{3}\Z", c)
- if match:
- return (tuple(int(n, 16) / 255
- for n in [c[1]*2, c[2]*2, c[3]*2])
- + (alpha if alpha is not None else 1.,))
- # hex color with alpha in #rrggbbaa format.
- match = re.match(r"\A#[a-fA-F0-9]{8}\Z", c)
- if match:
- color = [int(n, 16) / 255
- for n in [c[1:3], c[3:5], c[5:7], c[7:9]]]
- if alpha is not None:
- color[-1] = alpha
- return tuple(color)
- # hex color with alpha in #rgba format, shorthand for #rrggbbaa.
- match = re.match(r"\A#[a-fA-F0-9]{4}\Z", c)
- if match:
- color = [int(n, 16) / 255
- for n in [c[1]*2, c[2]*2, c[3]*2, c[4]*2]]
- if alpha is not None:
- color[-1] = alpha
- return tuple(color)
- # string gray.
- try:
- c = float(c)
- except ValueError:
- pass
- else:
- if not (0 <= c <= 1):
- raise ValueError(
- f"Invalid string grayscale value {orig_c!r}. "
- f"Value must be within 0-1 range")
- return c, c, c, alpha if alpha is not None else 1.
- raise ValueError(f"Invalid RGBA argument: {orig_c!r}")
- # tuple color.
- if not np.iterable(c):
- raise ValueError(f"Invalid RGBA argument: {orig_c!r}")
- if len(c) not in [3, 4]:
- raise ValueError("RGBA sequence should have length 3 or 4")
- if not all(isinstance(x, Number) for x in c):
- # Checks that don't work: `map(float, ...)`, `np.array(..., float)` and
- # `np.array(...).astype(float)` would all convert "0.5" to 0.5.
- raise ValueError(f"Invalid RGBA argument: {orig_c!r}")
- # Return a tuple to prevent the cached value from being modified.
- c = tuple(map(float, c))
- if len(c) == 3 and alpha is None:
- alpha = 1
- if alpha is not None:
- c = c[:3] + (alpha,)
- if any(elem < 0 or elem > 1 for elem in c):
- raise ValueError("RGBA values should be within 0-1 range")
- return c
- def to_rgba_array(c, alpha=None):
- """
- Convert *c* to a (n, 4) array of RGBA colors.
- If *alpha* is not ``None``, it forces the alpha value. If *c* is
- ``"none"`` (case-insensitive) or an empty list, an empty array is returned.
- If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0)
- row for each masked value or row in *c*.
- """
- # Special-case inputs that are already arrays, for performance. (If the
- # array has the wrong kind or shape, raise the error during one-at-a-time
- # conversion.)
- if (isinstance(c, np.ndarray) and c.dtype.kind in "if"
- and c.ndim == 2 and c.shape[1] in [3, 4]):
- mask = c.mask.any(axis=1) if np.ma.is_masked(c) else None
- c = np.ma.getdata(c)
- if c.shape[1] == 3:
- result = np.column_stack([c, np.zeros(len(c))])
- result[:, -1] = alpha if alpha is not None else 1.
- elif c.shape[1] == 4:
- result = c.copy()
- if alpha is not None:
- result[:, -1] = alpha
- if mask is not None:
- result[mask] = 0
- if np.any((result < 0) | (result > 1)):
- raise ValueError("RGBA values should be within 0-1 range")
- return result
- # Handle single values.
- # Note that this occurs *after* handling inputs that are already arrays, as
- # `to_rgba(c, alpha)` (below) is expensive for such inputs, due to the need
- # to format the array in the ValueError message(!).
- if cbook._str_lower_equal(c, "none"):
- return np.zeros((0, 4), float)
- try:
- return np.array([to_rgba(c, alpha)], float)
- except (ValueError, TypeError):
- pass
- # Convert one at a time.
- if isinstance(c, str):
- # Single string as color sequence.
- # This is deprecated and will be removed in the future.
- try:
- result = np.array([to_rgba(cc, alpha) for cc in c])
- except ValueError as err:
- raise ValueError(
- "'%s' is neither a valid single color nor a color sequence "
- "consisting of single character color specifiers such as "
- "'rgb'. Note also that the latter is deprecated." % c) from err
- else:
- cbook.warn_deprecated(
- "3.2", message="Using a string of single character colors as "
- "a color sequence is deprecated since %(since)s and will be "
- "removed %(removal)s. Use an explicit list instead.")
- return result
- if len(c) == 0:
- return np.zeros((0, 4), float)
- else:
- return np.array([to_rgba(cc, alpha) for cc in c])
- def to_rgb(c):
- """Convert *c* to an RGB color, silently dropping the alpha channel."""
- return to_rgba(c)[:3]
- def to_hex(c, keep_alpha=False):
- """
- Convert *c* to a hex color.
- Uses the ``#rrggbb`` format if *keep_alpha* is False (the default),
- ``#rrggbbaa`` otherwise.
- """
- c = to_rgba(c)
- if not keep_alpha:
- c = c[:3]
- return "#" + "".join(format(int(round(val * 255)), "02x") for val in c)
- ### Backwards-compatible color-conversion API
- cnames = CSS4_COLORS
- hexColorPattern = re.compile(r"\A#[a-fA-F0-9]{6}\Z")
- rgb2hex = to_hex
- hex2color = to_rgb
- class ColorConverter:
- """
- A class only kept for backwards compatibility.
- Its functionality is entirely provided by module-level functions.
- """
- colors = _colors_full_map
- cache = _colors_full_map.cache
- to_rgb = staticmethod(to_rgb)
- to_rgba = staticmethod(to_rgba)
- to_rgba_array = staticmethod(to_rgba_array)
- colorConverter = ColorConverter()
- ### End of backwards-compatible color-conversion API
- def _create_lookup_table(N, data, gamma=1.0):
- r"""
- Create an *N* -element 1-d lookup table.
- This assumes a mapping :math:`f : [0, 1] \rightarrow [0, 1]`. The returned
- data is an array of N values :math:`y = f(x)` where x is sampled from
- [0, 1].
- By default (*gamma* = 1) x is equidistantly sampled from [0, 1]. The
- *gamma* correction factor :math:`\gamma` distorts this equidistant
- sampling by :math:`x \rightarrow x^\gamma`.
- Parameters
- ----------
- N : int
- The number of elements of the created lookup table; at least 1.
- data : Mx3 array-like or callable
- Defines the mapping :math:`f`.
- If a Mx3 array-like, the rows define values (x, y0, y1). The x values
- must start with x=0, end with x=1, and all x values be in increasing
- order.
- A value between :math:`x_i` and :math:`x_{i+1}` is mapped to the range
- :math:`y^1_{i-1} \ldots y^0_i` by linear interpolation.
- For the simple case of a y-continuous mapping, y0 and y1 are identical.
- The two values of y are to allow for discontinuous mapping functions.
- E.g. a sawtooth with a period of 0.2 and an amplitude of 1 would be::
- [(0, 1, 0), (0.2, 1, 0), (0.4, 1, 0), ..., [(1, 1, 0)]
- In the special case of ``N == 1``, by convention the returned value
- is y0 for x == 1.
- If *data* is a callable, it must accept and return numpy arrays::
- data(x : ndarray) -> ndarray
- and map values between 0 - 1 to 0 - 1.
- gamma : float
- Gamma correction factor for input distribution x of the mapping.
- See also https://en.wikipedia.org/wiki/Gamma_correction.
- Returns
- -------
- array
- The lookup table where ``lut[x * (N-1)]`` gives the closest value
- for values of x between 0 and 1.
- Notes
- -----
- This function is internally used for `.LinearSegmentedColormap`.
- """
- if callable(data):
- xind = np.linspace(0, 1, N) ** gamma
- lut = np.clip(np.array(data(xind), dtype=float), 0, 1)
- return lut
- try:
- adata = np.array(data)
- except Exception as err:
- raise TypeError("data must be convertible to an array") from err
- shape = adata.shape
- if len(shape) != 2 or shape[1] != 3:
- raise ValueError("data must be nx3 format")
- x = adata[:, 0]
- y0 = adata[:, 1]
- y1 = adata[:, 2]
- if x[0] != 0. or x[-1] != 1.0:
- raise ValueError(
- "data mapping points must start with x=0 and end with x=1")
- if (np.diff(x) < 0).any():
- raise ValueError("data mapping points must have x in increasing order")
- # begin generation of lookup table
- if N == 1:
- # convention: use the y = f(x=1) value for a 1-element lookup table
- lut = np.array(y0[-1])
- else:
- x = x * (N - 1)
- xind = (N - 1) * np.linspace(0, 1, N) ** gamma
- ind = np.searchsorted(x, xind)[1:-1]
- distance = (xind[1:-1] - x[ind - 1]) / (x[ind] - x[ind - 1])
- lut = np.concatenate([
- [y1[0]],
- distance * (y0[ind] - y1[ind - 1]) + y1[ind - 1],
- [y0[-1]],
- ])
- # ensure that the lut is confined to values between 0 and 1 by clipping it
- return np.clip(lut, 0.0, 1.0)
- @cbook.deprecated("3.2",
- addendum='This is not considered public API any longer.')
- @docstring.copy(_create_lookup_table)
- def makeMappingArray(N, data, gamma=1.0):
- return _create_lookup_table(N, data, gamma)
- def _warn_if_global_cmap_modified(cmap):
- if getattr(cmap, '_global', False):
- cbook.warn_deprecated(
- "3.3",
- message="You are modifying the state of a globally registered "
- "colormap. In future versions, you will not be able to "
- "modify a registered colormap in-place. To remove this "
- "warning, you can make a copy of the colormap first. "
- f'cmap = copy.copy(mpl.cm.get_cmap("{cmap.name}"))'
- )
- class Colormap:
- """
- Baseclass for all scalar to RGBA mappings.
- Typically, Colormap instances are used to convert data values (floats)
- from the interval ``[0, 1]`` to the RGBA color that the respective
- Colormap represents. For scaling of data into the ``[0, 1]`` interval see
- `matplotlib.colors.Normalize`. Subclasses of `matplotlib.cm.ScalarMappable`
- make heavy use of this ``data -> normalize -> map-to-color`` processing
- chain.
- """
- def __init__(self, name, N=256):
- """
- Parameters
- ----------
- name : str
- The name of the colormap.
- N : int
- The number of rgb quantization levels.
- """
- self.name = name
- self.N = int(N) # ensure that N is always int
- self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything.
- self._rgba_under = None
- self._rgba_over = None
- self._i_under = self.N
- self._i_over = self.N + 1
- self._i_bad = self.N + 2
- self._isinit = False
- #: When this colormap exists on a scalar mappable and colorbar_extend
- #: is not False, colorbar creation will pick up ``colorbar_extend`` as
- #: the default value for the ``extend`` keyword in the
- #: `matplotlib.colorbar.Colorbar` constructor.
- self.colorbar_extend = False
- def __call__(self, X, alpha=None, bytes=False):
- """
- Parameters
- ----------
- X : float or int, ndarray or scalar
- The data value(s) to convert to RGBA.
- For floats, X should be in the interval ``[0.0, 1.0]`` to
- return the RGBA values ``X*100`` percent along the Colormap line.
- For integers, X should be in the interval ``[0, Colormap.N)`` to
- return RGBA values *indexed* from the Colormap with index ``X``.
- alpha : float, None
- Alpha must be a scalar between 0 and 1, or None.
- bytes : bool
- If False (default), the returned RGBA values will be floats in the
- interval ``[0, 1]`` otherwise they will be uint8s in the interval
- ``[0, 255]``.
- Returns
- -------
- Tuple of RGBA values if X is scalar, otherwise an array of
- RGBA values with a shape of ``X.shape + (4, )``.
- """
- if not self._isinit:
- self._init()
- mask_bad = X.mask if np.ma.is_masked(X) else np.isnan(X) # Mask nan's.
- xa = np.array(X, copy=True)
- if not xa.dtype.isnative:
- xa = xa.byteswap().newbyteorder() # Native byteorder is faster.
- if xa.dtype.kind == "f":
- with np.errstate(invalid="ignore"):
- xa *= self.N
- # Negative values are out of range, but astype(int) would
- # truncate them towards zero.
- xa[xa < 0] = -1
- # xa == 1 (== N after multiplication) is not out of range.
- xa[xa == self.N] = self.N - 1
- # Avoid converting large positive values to negative integers.
- np.clip(xa, -1, self.N, out=xa)
- xa = xa.astype(int)
- # Set the over-range indices before the under-range;
- # otherwise the under-range values get converted to over-range.
- xa[xa > self.N - 1] = self._i_over
- xa[xa < 0] = self._i_under
- xa[mask_bad] = self._i_bad
- if bytes:
- lut = (self._lut * 255).astype(np.uint8)
- else:
- lut = self._lut.copy() # Don't let alpha modify original _lut.
- if alpha is not None:
- alpha = np.clip(alpha, 0, 1)
- if bytes:
- alpha = int(alpha * 255)
- if (lut[-1] == 0).all():
- lut[:-1, -1] = alpha
- # All zeros is taken as a flag for the default bad
- # color, which is no color--fully transparent. We
- # don't want to override this.
- else:
- lut[:, -1] = alpha
- # If the bad value is set to have a color, then we
- # override its alpha just as for any other value.
- rgba = lut[xa]
- if not np.iterable(X):
- # Return a tuple if the input was a scalar
- rgba = tuple(rgba)
- return rgba
- def __copy__(self):
- cls = self.__class__
- cmapobject = cls.__new__(cls)
- cmapobject.__dict__.update(self.__dict__)
- if self._isinit:
- cmapobject._lut = np.copy(self._lut)
- cmapobject._global = False
- return cmapobject
- def set_bad(self, color='k', alpha=None):
- """Set the color for masked values."""
- _warn_if_global_cmap_modified(self)
- self._rgba_bad = to_rgba(color, alpha)
- if self._isinit:
- self._set_extremes()
- def set_under(self, color='k', alpha=None):
- """
- Set the color for low out-of-range values when ``norm.clip = False``.
- """
- _warn_if_global_cmap_modified(self)
- self._rgba_under = to_rgba(color, alpha)
- if self._isinit:
- self._set_extremes()
- def set_over(self, color='k', alpha=None):
- """
- Set the color for high out-of-range values when ``norm.clip = False``.
- """
- _warn_if_global_cmap_modified(self)
- self._rgba_over = to_rgba(color, alpha)
- if self._isinit:
- self._set_extremes()
- def _set_extremes(self):
- if self._rgba_under:
- self._lut[self._i_under] = self._rgba_under
- else:
- self._lut[self._i_under] = self._lut[0]
- if self._rgba_over:
- self._lut[self._i_over] = self._rgba_over
- else:
- self._lut[self._i_over] = self._lut[self.N - 1]
- self._lut[self._i_bad] = self._rgba_bad
- def _init(self):
- """Generate the lookup table, ``self._lut``."""
- raise NotImplementedError("Abstract class only")
- def is_gray(self):
- if not self._isinit:
- self._init()
- return (np.all(self._lut[:, 0] == self._lut[:, 1]) and
- np.all(self._lut[:, 0] == self._lut[:, 2]))
- def _resample(self, lutsize):
- """Return a new color map with *lutsize* entries."""
- raise NotImplementedError()
- def reversed(self, name=None):
- """
- Return a reversed instance of the Colormap.
- .. note:: This function is not implemented for base class.
- Parameters
- ----------
- name : str, optional
- The name for the reversed colormap. If it's None the
- name will be the name of the parent colormap + "_r".
- See Also
- --------
- LinearSegmentedColormap.reversed
- ListedColormap.reversed
- """
- raise NotImplementedError()
- class LinearSegmentedColormap(Colormap):
- """
- Colormap objects based on lookup tables using linear segments.
- The lookup table is generated using linear interpolation for each
- primary color, with the 0-1 domain divided into any number of
- segments.
- """
- def __init__(self, name, segmentdata, N=256, gamma=1.0):
- """
- Create color map from linear mapping segments
- segmentdata argument is a dictionary with a red, green and blue
- entries. Each entry should be a list of *x*, *y0*, *y1* tuples,
- forming rows in a table. Entries for alpha are optional.
- Example: suppose you want red to increase from 0 to 1 over
- the bottom half, green to do the same over the middle half,
- and blue over the top half. Then you would use::
- cdict = {'red': [(0.0, 0.0, 0.0),
- (0.5, 1.0, 1.0),
- (1.0, 1.0, 1.0)],
- 'green': [(0.0, 0.0, 0.0),
- (0.25, 0.0, 0.0),
- (0.75, 1.0, 1.0),
- (1.0, 1.0, 1.0)],
- 'blue': [(0.0, 0.0, 0.0),
- (0.5, 0.0, 0.0),
- (1.0, 1.0, 1.0)]}
- Each row in the table for a given color is a sequence of
- *x*, *y0*, *y1* tuples. In each sequence, *x* must increase
- monotonically from 0 to 1. For any input value *z* falling
- between *x[i]* and *x[i+1]*, the output value of a given color
- will be linearly interpolated between *y1[i]* and *y0[i+1]*::
- row i: x y0 y1
- /
- /
- row i+1: x y0 y1
- Hence y0 in the first row and y1 in the last row are never used.
- See Also
- --------
- LinearSegmentedColormap.from_list
- Static method; factory function for generating a smoothly-varying
- LinearSegmentedColormap.
- makeMappingArray
- For information about making a mapping array.
- """
- # True only if all colors in map are identical; needed for contouring.
- self.monochrome = False
- Colormap.__init__(self, name, N)
- self._segmentdata = segmentdata
- self._gamma = gamma
- def _init(self):
- self._lut = np.ones((self.N + 3, 4), float)
- self._lut[:-3, 0] = _create_lookup_table(
- self.N, self._segmentdata['red'], self._gamma)
- self._lut[:-3, 1] = _create_lookup_table(
- self.N, self._segmentdata['green'], self._gamma)
- self._lut[:-3, 2] = _create_lookup_table(
- self.N, self._segmentdata['blue'], self._gamma)
- if 'alpha' in self._segmentdata:
- self._lut[:-3, 3] = _create_lookup_table(
- self.N, self._segmentdata['alpha'], 1)
- self._isinit = True
- self._set_extremes()
- def set_gamma(self, gamma):
- """Set a new gamma value and regenerate color map."""
- self._gamma = gamma
- self._init()
- @staticmethod
- def from_list(name, colors, N=256, gamma=1.0):
- """
- Create a `LinearSegmentedColormap` from a list of colors.
- Parameters
- ----------
- name : str
- The name of the colormap.
- colors : array-like of colors or array-like of (value, color)
- If only colors are given, they are equidistantly mapped from the
- range :math:`[0, 1]`; i.e. 0 maps to ``colors[0]`` and 1 maps to
- ``colors[-1]``.
- If (value, color) pairs are given, the mapping is from *value*
- to *color*. This can be used to divide the range unevenly.
- N : int
- The number of rgb quantization levels.
- gamma : float
- """
- if not np.iterable(colors):
- raise ValueError('colors must be iterable')
- if (isinstance(colors[0], Sized) and len(colors[0]) == 2
- and not isinstance(colors[0], str)):
- # List of value, color pairs
- vals, colors = zip(*colors)
- else:
- vals = np.linspace(0, 1, len(colors))
- cdict = dict(red=[], green=[], blue=[], alpha=[])
- for val, color in zip(vals, colors):
- r, g, b, a = to_rgba(color)
- cdict['red'].append((val, r, r))
- cdict['green'].append((val, g, g))
- cdict['blue'].append((val, b, b))
- cdict['alpha'].append((val, a, a))
- return LinearSegmentedColormap(name, cdict, N, gamma)
- def _resample(self, lutsize):
- """Return a new color map with *lutsize* entries."""
- new_cmap = LinearSegmentedColormap(self.name, self._segmentdata,
- lutsize)
- new_cmap._rgba_over = self._rgba_over
- new_cmap._rgba_under = self._rgba_under
- new_cmap._rgba_bad = self._rgba_bad
- return new_cmap
- # Helper ensuring picklability of the reversed cmap.
- @staticmethod
- def _reverser(func, x):
- return func(1 - x)
- def reversed(self, name=None):
- """
- Return a reversed instance of the Colormap.
- Parameters
- ----------
- name : str, optional
- The name for the reversed colormap. If it's None the
- name will be the name of the parent colormap + "_r".
- Returns
- -------
- LinearSegmentedColormap
- The reversed colormap.
- """
- if name is None:
- name = self.name + "_r"
- # Using a partial object keeps the cmap picklable.
- data_r = {key: (functools.partial(self._reverser, data)
- if callable(data) else
- [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)])
- for key, data in self._segmentdata.items()}
- new_cmap = LinearSegmentedColormap(name, data_r, self.N, self._gamma)
- # Reverse the over/under values too
- new_cmap._rgba_over = self._rgba_under
- new_cmap._rgba_under = self._rgba_over
- new_cmap._rgba_bad = self._rgba_bad
- return new_cmap
- class ListedColormap(Colormap):
- """
- Colormap object generated from a list of colors.
- This may be most useful when indexing directly into a colormap,
- but it can also be used to generate special colormaps for ordinary
- mapping.
- Parameters
- ----------
- colors : list, array
- List of Matplotlib color specifications, or an equivalent Nx3 or Nx4
- floating point array (*N* rgb or rgba values).
- name : str, optional
- String to identify the colormap.
- N : int, optional
- Number of entries in the map. The default is *None*, in which case
- there is one colormap entry for each element in the list of colors.
- If ::
- N < len(colors)
- the list will be truncated at *N*. If ::
- N > len(colors)
- the list will be extended by repetition.
- """
- def __init__(self, colors, name='from_list', N=None):
- self.monochrome = False # Are all colors identical? (for contour.py)
- if N is None:
- self.colors = colors
- N = len(colors)
- else:
- if isinstance(colors, str):
- self.colors = [colors] * N
- self.monochrome = True
- elif np.iterable(colors):
- if len(colors) == 1:
- self.monochrome = True
- self.colors = list(
- itertools.islice(itertools.cycle(colors), N))
- else:
- try:
- gray = float(colors)
- except TypeError:
- pass
- else:
- self.colors = [gray] * N
- self.monochrome = True
- Colormap.__init__(self, name, N)
- def _init(self):
- self._lut = np.zeros((self.N + 3, 4), float)
- self._lut[:-3] = to_rgba_array(self.colors)
- self._isinit = True
- self._set_extremes()
- def _resample(self, lutsize):
- """Return a new color map with *lutsize* entries."""
- colors = self(np.linspace(0, 1, lutsize))
- new_cmap = ListedColormap(colors, name=self.name)
- # Keep the over/under values too
- new_cmap._rgba_over = self._rgba_over
- new_cmap._rgba_under = self._rgba_under
- new_cmap._rgba_bad = self._rgba_bad
- return new_cmap
- def reversed(self, name=None):
- """
- Return a reversed instance of the Colormap.
- Parameters
- ----------
- name : str, optional
- The name for the reversed colormap. If it's None the
- name will be the name of the parent colormap + "_r".
- Returns
- -------
- ListedColormap
- A reversed instance of the colormap.
- """
- if name is None:
- name = self.name + "_r"
- colors_r = list(reversed(self.colors))
- new_cmap = ListedColormap(colors_r, name=name, N=self.N)
- # Reverse the over/under values too
- new_cmap._rgba_over = self._rgba_under
- new_cmap._rgba_under = self._rgba_over
- new_cmap._rgba_bad = self._rgba_bad
- return new_cmap
- class Normalize:
- """
- A class which, when called, linearly normalizes data into the
- ``[0.0, 1.0]`` interval.
- """
- def __init__(self, vmin=None, vmax=None, clip=False):
- """
- Parameters
- ----------
- vmin, vmax : float or None
- If *vmin* and/or *vmax* is not given, they are initialized from the
- minimum and maximum value, respectively, of the first input
- processed; i.e., ``__call__(A)`` calls ``autoscale_None(A)``.
- clip : bool, default: False
- If ``True`` values falling outside the range ``[vmin, vmax]``,
- are mapped to 0 or 1, whichever is closer, and masked values are
- set to 1. If ``False`` masked values remain masked.
- Clipping silently defeats the purpose of setting the over, under,
- and masked colors in a colormap, so it is likely to lead to
- surprises; therefore the default is ``clip=False``.
- Notes
- -----
- Returns 0 if ``vmin == vmax``.
- """
- self.vmin = _sanitize_extrema(vmin)
- self.vmax = _sanitize_extrema(vmax)
- self.clip = clip
- @staticmethod
- def process_value(value):
- """
- Homogenize the input *value* for easy and efficient normalization.
- *value* can be a scalar or sequence.
- Returns
- -------
- result : masked array
- Masked array with the same shape as *value*.
- is_scalar : bool
- Whether *value* is a scalar.
- Notes
- -----
- Float dtypes are preserved; integer types with two bytes or smaller are
- converted to np.float32, and larger types are converted to np.float64.
- Preserving float32 when possible, and using in-place operations,
- greatly improves speed for large arrays.
- """
- is_scalar = not np.iterable(value)
- if is_scalar:
- value = [value]
- dtype = np.min_scalar_type(value)
- if np.issubdtype(dtype, np.integer) or dtype.type is np.bool_:
- # bool_/int8/int16 -> float32; int32/int64 -> float64
- dtype = np.promote_types(dtype, np.float32)
- # ensure data passed in as an ndarray subclass are interpreted as
- # an ndarray. See issue #6622.
- mask = np.ma.getmask(value)
- data = np.asarray(value)
- result = np.ma.array(data, mask=mask, dtype=dtype, copy=True)
- return result, is_scalar
- def __call__(self, value, clip=None):
- """
- Normalize *value* data in the ``[vmin, vmax]`` interval into the
- ``[0.0, 1.0]`` interval and return it.
- Parameters
- ----------
- value
- Data to normalize.
- clip : bool
- If ``None``, defaults to ``self.clip`` (which defaults to
- ``False``).
- Notes
- -----
- If not already initialized, ``self.vmin`` and ``self.vmax`` are
- initialized using ``self.autoscale_None(value)``.
- """
- if clip is None:
- clip = self.clip
- result, is_scalar = self.process_value(value)
- self.autoscale_None(result)
- # Convert at least to float, without losing precision.
- (vmin,), _ = self.process_value(self.vmin)
- (vmax,), _ = self.process_value(self.vmax)
- if vmin == vmax:
- result.fill(0) # Or should it be all masked? Or 0.5?
- elif vmin > vmax:
- raise ValueError("minvalue must be less than or equal to maxvalue")
- else:
- if clip:
- mask = np.ma.getmask(result)
- result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
- mask=mask)
- # ma division is very slow; we can take a shortcut
- resdat = result.data
- resdat -= vmin
- resdat /= (vmax - vmin)
- result = np.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 both vmin and vmax are set")
- (vmin,), _ = self.process_value(self.vmin)
- (vmax,), _ = self.process_value(self.vmax)
- if np.iterable(value):
- val = np.ma.asarray(value)
- return vmin + val * (vmax - vmin)
- else:
- return vmin + value * (vmax - vmin)
- def autoscale(self, A):
- """Set *vmin*, *vmax* to min, max of *A*."""
- A = np.asanyarray(A)
- self.vmin = A.min()
- self.vmax = A.max()
- def autoscale_None(self, A):
- """If vmin or vmax are not set, use the min/max of *A* to set them."""
- A = np.asanyarray(A)
- if self.vmin is None and A.size:
- self.vmin = A.min()
- if self.vmax is None and A.size:
- self.vmax = A.max()
- def scaled(self):
- """Return whether vmin and vmax are set."""
- return self.vmin is not None and self.vmax is not None
- class TwoSlopeNorm(Normalize):
- def __init__(self, vcenter, vmin=None, vmax=None):
- """
- Normalize data with a set center.
- Useful when mapping data with an unequal rates of change around a
- conceptual center, e.g., data that range from -2 to 4, with 0 as
- the midpoint.
- Parameters
- ----------
- vcenter : float
- The data value that defines ``0.5`` in the normalization.
- vmin : float, optional
- The data value that defines ``0.0`` in the normalization.
- Defaults to the min value of the dataset.
- vmax : float, optional
- The data value that defines ``1.0`` in the normalization.
- Defaults to the the max value of the dataset.
- Examples
- --------
- This maps data value -4000 to 0., 0 to 0.5, and +10000 to 1.0; data
- between is linearly interpolated::
- >>> import matplotlib.colors as mcolors
- >>> offset = mcolors.TwoSlopeNorm(vmin=-4000.,
- vcenter=0., vmax=10000)
- >>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.]
- >>> offset(data)
- array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
- """
- self.vcenter = vcenter
- self.vmin = vmin
- self.vmax = vmax
- if vcenter is not None and vmax is not None and vcenter >= vmax:
- raise ValueError('vmin, vcenter, and vmax must be in '
- 'ascending order')
- if vcenter is not None and vmin is not None and vcenter <= vmin:
- raise ValueError('vmin, vcenter, and vmax must be in '
- 'ascending order')
- def autoscale_None(self, A):
- """
- Get vmin and vmax, and then clip at vcenter
- """
- super().autoscale_None(A)
- if self.vmin > self.vcenter:
- self.vmin = self.vcenter
- if self.vmax < self.vcenter:
- self.vmax = self.vcenter
- def __call__(self, value, clip=None):
- """
- Map value to the interval [0, 1]. The clip argument is unused.
- """
- result, is_scalar = self.process_value(value)
- self.autoscale_None(result) # sets self.vmin, self.vmax if None
- if not self.vmin <= self.vcenter <= self.vmax:
- raise ValueError("vmin, vcenter, vmax must increase monotonically")
- result = np.ma.masked_array(
- np.interp(result, [self.vmin, self.vcenter, self.vmax],
- [0, 0.5, 1.]), mask=np.ma.getmask(result))
- if is_scalar:
- result = np.atleast_1d(result)[0]
- return result
- @cbook.deprecation.deprecated('3.2', alternative='TwoSlopeNorm')
- class DivergingNorm(TwoSlopeNorm):
- ...
- class LogNorm(Normalize):
- """Normalize a given value to the 0-1 range on a log scale."""
- def _check_vmin_vmax(self):
- if self.vmin > self.vmax:
- raise ValueError("minvalue must be less than or equal to maxvalue")
- elif self.vmin <= 0:
- raise ValueError("minvalue must be positive")
- def __call__(self, value, clip=None):
- if clip is None:
- clip = self.clip
- result, is_scalar = self.process_value(value)
- result = np.ma.masked_less_equal(result, 0, copy=False)
- self.autoscale_None(result)
- self._check_vmin_vmax()
- vmin, vmax = self.vmin, self.vmax
- if vmin == vmax:
- result.fill(0)
- else:
- if clip:
- mask = np.ma.getmask(result)
- result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
- mask=mask)
- # in-place equivalent of above can be much faster
- resdat = result.data
- mask = result.mask
- if mask is np.ma.nomask:
- mask = (resdat <= 0)
- else:
- mask |= resdat <= 0
- np.copyto(resdat, 1, where=mask)
- np.log(resdat, resdat)
- resdat -= np.log(vmin)
- resdat /= (np.log(vmax) - np.log(vmin))
- result = np.ma.array(resdat, mask=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")
- self._check_vmin_vmax()
- vmin, vmax = self.vmin, self.vmax
- if np.iterable(value):
- val = np.ma.asarray(value)
- return vmin * np.ma.power((vmax / vmin), val)
- else:
- return vmin * pow((vmax / vmin), value)
- def autoscale(self, A):
- # docstring inherited.
- super().autoscale(np.ma.masked_less_equal(A, 0, copy=False))
- def autoscale_None(self, A):
- # docstring inherited.
- super().autoscale_None(np.ma.masked_less_equal(A, 0, copy=False))
- class SymLogNorm(Normalize):
- """
- The symmetrical logarithmic scale is logarithmic in both the
- positive and negative directions from the origin.
- Since the values close to zero tend toward infinity, there is a
- need to have a range around zero that is linear. The parameter
- *linthresh* allows the user to specify the size of this range
- (-*linthresh*, *linthresh*).
- """
- def __init__(self, linthresh, linscale=1.0, vmin=None, vmax=None,
- clip=False, *, base=None):
- """
- Parameters
- ----------
- linthresh : float
- The range within which the plot is linear (to avoid having the plot
- go to infinity around zero).
- linscale : float, default: 1
- This allows the linear range (-*linthresh* to *linthresh*)
- to be stretched relative to the logarithmic range. Its
- value is the number of powers of *base* to use for each
- half of the linear range.
- For example, when *linscale* == 1.0 (the default) and
- ``base=10``, then space used for the positive and negative
- halves of the linear range will be equal to a decade in
- the logarithmic.
- base : float, default: None
- If not given, defaults to ``np.e`` (consistent with prior
- behavior) and warns.
- In v3.3 the default value will change to 10 to be consistent with
- `.SymLogNorm`.
- To suppress the warning pass *base* as a keyword argument.
- """
- Normalize.__init__(self, vmin, vmax, clip)
- if base is None:
- self._base = np.e
- cbook.warn_deprecated(
- "3.2", removal="3.4", message="default base will change from "
- "np.e to 10 %(removal)s. To suppress this warning specify "
- "the base keyword argument.")
- else:
- self._base = base
- self._log_base = np.log(self._base)
- self.linthresh = float(linthresh)
- self._linscale_adj = (linscale / (1.0 - self._base ** -1))
- if vmin is not None and vmax is not None:
- self._transform_vmin_vmax()
- 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 = self.vmin, self.vmax
- if vmin > vmax:
- raise ValueError("minvalue must be less than or equal to maxvalue")
- elif vmin == vmax:
- result.fill(0)
- else:
- if clip:
- mask = np.ma.getmask(result)
- result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
- mask=mask)
- # in-place equivalent of above can be much faster
- resdat = self._transform(result.data)
- resdat -= self._lower
- resdat /= (self._upper - self._lower)
- if is_scalar:
- result = result[0]
- return result
- def _transform(self, a):
- """Inplace transformation."""
- with np.errstate(invalid="ignore"):
- masked = np.abs(a) > self.linthresh
- sign = np.sign(a[masked])
- log = (self._linscale_adj +
- np.log(np.abs(a[masked]) / self.linthresh) / self._log_base)
- log *= sign * self.linthresh
- a[masked] = log
- a[~masked] *= self._linscale_adj
- return a
- def _inv_transform(self, a):
- """Inverse inplace Transformation."""
- masked = np.abs(a) > (self.linthresh * self._linscale_adj)
- sign = np.sign(a[masked])
- exp = np.power(self._base,
- sign * a[masked] / self.linthresh - self._linscale_adj)
- exp *= sign * self.linthresh
- a[masked] = exp
- a[~masked] /= self._linscale_adj
- return a
- def _transform_vmin_vmax(self):
- """Calculate vmin and vmax in the transformed system."""
- vmin, vmax = self.vmin, self.vmax
- arr = np.array([vmax, vmin]).astype(float)
- self._upper, self._lower = self._transform(arr)
- def inverse(self, value):
- if not self.scaled():
- raise ValueError("Not invertible until scaled")
- val = np.ma.asarray(value)
- val = val * (self._upper - self._lower) + self._lower
- return self._inv_transform(val)
- def autoscale(self, A):
- # docstring inherited.
- super().autoscale(A)
- self._transform_vmin_vmax()
- def autoscale_None(self, A):
- # docstring inherited.
- super().autoscale_None(A)
- self._transform_vmin_vmax()
- class PowerNorm(Normalize):
- """
- Linearly map a given value to the 0-1 range and then apply
- a power-law normalization over that range.
- """
- def __init__(self, gamma, vmin=None, vmax=None, clip=False):
- Normalize.__init__(self, vmin, vmax, clip)
- self.gamma = gamma
- def __call__(self, value, clip=None):
- if clip is None:
- clip = self.clip
- result, is_scalar = self.process_value(value)
- self.autoscale_None(result)
- gamma = self.gamma
- vmin, vmax = self.vmin, self.vmax
- if vmin > vmax:
- raise ValueError("minvalue must be less than or equal to maxvalue")
- elif vmin == vmax:
- result.fill(0)
- else:
- if clip:
- mask = np.ma.getmask(result)
- result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
- mask=mask)
- resdat = result.data
- resdat -= vmin
- resdat[resdat < 0] = 0
- np.power(resdat, gamma, resdat)
- resdat /= (vmax - vmin) ** gamma
- result = np.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")
- gamma = self.gamma
- vmin, vmax = self.vmin, self.vmax
- if np.iterable(value):
- val = np.ma.asarray(value)
- return np.ma.power(val, 1. / gamma) * (vmax - vmin) + vmin
- else:
- return pow(value, 1. / gamma) * (vmax - vmin) + vmin
- class BoundaryNorm(Normalize):
- """
- Generate a colormap index based on discrete intervals.
- Unlike `Normalize` or `LogNorm`, `BoundaryNorm` maps values to integers
- instead of to the interval 0-1.
- Mapping to the 0-1 interval could have been done via piece-wise linear
- interpolation, but using integers seems simpler, and reduces the number of
- conversions back and forth between integer and floating point.
- """
- def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'):
- """
- Parameters
- ----------
- boundaries : array-like
- Monotonically increasing sequence of boundaries
- ncolors : int
- Number of colors in the colormap to be used
- clip : bool, optional
- If clip is ``True``, out of range values are mapped to 0 if they
- are below ``boundaries[0]`` or mapped to ``ncolors - 1`` if they
- are above ``boundaries[-1]``.
- If clip is ``False``, out of range values are mapped to -1 if
- they are below ``boundaries[0]`` or mapped to *ncolors* if they are
- above ``boundaries[-1]``. These are then converted to valid indices
- by `Colormap.__call__`.
- extend : {'neither', 'both', 'min', 'max'}, default: 'neither'
- Extend the number of bins to include one or both of the
- regions beyond the boundaries. For example, if ``extend``
- is 'min', then the color to which the region between the first
- pair of boundaries is mapped will be distinct from the first
- color in the colormap, and by default a
- `~matplotlib.colorbar.Colorbar` will be drawn with
- the triangle extension on the left or lower end.
- Returns
- -------
- int16 scalar or array
- Notes
- -----
- *boundaries* defines the edges of bins, and data falling within a bin
- is mapped to the color with the same index.
- If the number of bins, including any extensions, is less than
- *ncolors*, the color index is chosen by linear interpolation, mapping
- the ``[0, nbins - 1]`` range onto the ``[0, ncolors - 1]`` range.
- """
- if clip and extend != 'neither':
- raise ValueError("'clip=True' is not compatible with 'extend'")
- self.clip = clip
- self.vmin = boundaries[0]
- self.vmax = boundaries[-1]
- self.boundaries = np.asarray(boundaries)
- self.N = len(self.boundaries)
- self.Ncmap = ncolors
- self.extend = extend
- self._N = self.N - 1 # number of colors needed
- self._offset = 0
- if extend in ('min', 'both'):
- self._N += 1
- self._offset = 1
- if extend in ('max', 'both'):
- self._N += 1
- if self._N > self.Ncmap:
- raise ValueError(f"There are {self._N} color bins including "
- f"extensions, but ncolors = {ncolors}; "
- "ncolors must equal or exceed the number of "
- "bins")
- def __call__(self, value, clip=None):
- if clip is None:
- clip = self.clip
- xx, is_scalar = self.process_value(value)
- mask = np.ma.getmaskarray(xx)
- xx = np.atleast_1d(xx.filled(self.vmax + 1))
- if clip:
- np.clip(xx, self.vmin, self.vmax, out=xx)
- max_col = self.Ncmap - 1
- else:
- max_col = self.Ncmap
- iret = np.digitize(xx, self.boundaries) - 1 + self._offset
- if self.Ncmap > self._N:
- scalefac = (self.Ncmap - 1) / (self._N - 1)
- iret = (iret * scalefac).astype(np.int16)
- iret[xx < self.vmin] = -1
- iret[xx >= self.vmax] = max_col
- ret = np.ma.array(iret, mask=mask)
- if is_scalar:
- ret = int(ret[0]) # assume python scalar
- return ret
- def inverse(self, value):
- """
- Raises
- ------
- ValueError
- BoundaryNorm is not invertible, so calling this method will always
- raise an error
- """
- raise ValueError("BoundaryNorm is not invertible")
- class NoNorm(Normalize):
- """
- Dummy replacement for `Normalize`, for the case where we want to use
- indices directly in a `~matplotlib.cm.ScalarMappable`.
- """
- def __call__(self, value, clip=None):
- return value
- def inverse(self, value):
- return value
- def rgb_to_hsv(arr):
- """
- Convert float rgb values (in the range [0, 1]), in a numpy array to hsv
- values.
- Parameters
- ----------
- arr : (..., 3) array-like
- All values must be in the range [0, 1]
- Returns
- -------
- (..., 3) ndarray
- Colors converted to hsv values in range [0, 1]
- """
- arr = np.asarray(arr)
- # check length of the last dimension, should be _some_ sort of rgb
- if arr.shape[-1] != 3:
- raise ValueError("Last dimension of input array must be 3; "
- "shape {} was found.".format(arr.shape))
- in_shape = arr.shape
- arr = np.array(
- arr, copy=False,
- dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints.
- ndmin=2, # In case input was 1D.
- )
- out = np.zeros_like(arr)
- arr_max = arr.max(-1)
- ipos = arr_max > 0
- delta = arr.ptp(-1)
- s = np.zeros_like(delta)
- s[ipos] = delta[ipos] / arr_max[ipos]
- ipos = delta > 0
- # red is max
- idx = (arr[..., 0] == arr_max) & ipos
- out[idx, 0] = (arr[idx, 1] - arr[idx, 2]) / delta[idx]
- # green is max
- idx = (arr[..., 1] == arr_max) & ipos
- out[idx, 0] = 2. + (arr[idx, 2] - arr[idx, 0]) / delta[idx]
- # blue is max
- idx = (arr[..., 2] == arr_max) & ipos
- out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1]) / delta[idx]
- out[..., 0] = (out[..., 0] / 6.0) % 1.0
- out[..., 1] = s
- out[..., 2] = arr_max
- return out.reshape(in_shape)
- def hsv_to_rgb(hsv):
- """
- Convert hsv values to rgb.
- Parameters
- ----------
- hsv : (..., 3) array-like
- All values assumed to be in range [0, 1]
- Returns
- -------
- (..., 3) ndarray
- Colors converted to RGB values in range [0, 1]
- """
- hsv = np.asarray(hsv)
- # check length of the last dimension, should be _some_ sort of rgb
- if hsv.shape[-1] != 3:
- raise ValueError("Last dimension of input array must be 3; "
- "shape {shp} was found.".format(shp=hsv.shape))
- in_shape = hsv.shape
- hsv = np.array(
- hsv, copy=False,
- dtype=np.promote_types(hsv.dtype, np.float32), # Don't work on ints.
- ndmin=2, # In case input was 1D.
- )
- h = hsv[..., 0]
- s = hsv[..., 1]
- v = hsv[..., 2]
- r = np.empty_like(h)
- g = np.empty_like(h)
- b = np.empty_like(h)
- i = (h * 6.0).astype(int)
- f = (h * 6.0) - i
- p = v * (1.0 - s)
- q = v * (1.0 - s * f)
- t = v * (1.0 - s * (1.0 - f))
- idx = i % 6 == 0
- r[idx] = v[idx]
- g[idx] = t[idx]
- b[idx] = p[idx]
- idx = i == 1
- r[idx] = q[idx]
- g[idx] = v[idx]
- b[idx] = p[idx]
- idx = i == 2
- r[idx] = p[idx]
- g[idx] = v[idx]
- b[idx] = t[idx]
- idx = i == 3
- r[idx] = p[idx]
- g[idx] = q[idx]
- b[idx] = v[idx]
- idx = i == 4
- r[idx] = t[idx]
- g[idx] = p[idx]
- b[idx] = v[idx]
- idx = i == 5
- r[idx] = v[idx]
- g[idx] = p[idx]
- b[idx] = q[idx]
- idx = s == 0
- r[idx] = v[idx]
- g[idx] = v[idx]
- b[idx] = v[idx]
- rgb = np.stack([r, g, b], axis=-1)
- return rgb.reshape(in_shape)
- def _vector_magnitude(arr):
- # things that don't work here:
- # * np.linalg.norm: drops mask from ma.array
- # * np.sum: drops mask from ma.array unless entire vector is masked
- sum_sq = 0
- for i in range(arr.shape[-1]):
- sum_sq += arr[..., i, np.newaxis] ** 2
- return np.sqrt(sum_sq)
- class LightSource:
- """
- Create a light source coming from the specified azimuth and elevation.
- Angles are in degrees, with the azimuth measured
- clockwise from north and elevation up from the zero plane of the surface.
- `shade` is used to produce "shaded" rgb values for a data array.
- `shade_rgb` can be used to combine an rgb image with an elevation map.
- `hillshade` produces an illumination map of a surface.
- """
- def __init__(self, azdeg=315, altdeg=45, hsv_min_val=0, hsv_max_val=1,
- hsv_min_sat=1, hsv_max_sat=0):
- """
- Specify the azimuth (measured clockwise from south) and altitude
- (measured up from the plane of the surface) of the light source
- in degrees.
- Parameters
- ----------
- azdeg : float, default: 315 degrees (from the northwest)
- The azimuth (0-360, degrees clockwise from North) of the light
- source.
- altdeg : float, default: 45 degrees
- The altitude (0-90, degrees up from horizontal) of the light
- source.
- Notes
- -----
- For backwards compatibility, the parameters *hsv_min_val*,
- *hsv_max_val*, *hsv_min_sat*, and *hsv_max_sat* may be supplied at
- initialization as well. However, these parameters will only be used if
- "blend_mode='hsv'" is passed into `shade` or `shade_rgb`.
- See the documentation for `blend_hsv` for more details.
- """
- self.azdeg = azdeg
- self.altdeg = altdeg
- self.hsv_min_val = hsv_min_val
- self.hsv_max_val = hsv_max_val
- self.hsv_min_sat = hsv_min_sat
- self.hsv_max_sat = hsv_max_sat
- @property
- def direction(self):
- """The unit vector direction towards the light source."""
- # Azimuth is in degrees clockwise from North. Convert to radians
- # counterclockwise from East (mathematical notation).
- az = np.radians(90 - self.azdeg)
- alt = np.radians(self.altdeg)
- return np.array([
- np.cos(az) * np.cos(alt),
- np.sin(az) * np.cos(alt),
- np.sin(alt)
- ])
- def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.):
- """
- Calculate the illumination intensity for a surface using the defined
- azimuth and elevation for the light source.
- This computes the normal vectors for the surface, and then passes them
- on to `shade_normals`
- Parameters
- ----------
- elevation : array-like
- A 2d array (or equivalent) of the height values used to generate an
- illumination map
- vert_exag : number, optional
- The amount to exaggerate the elevation values by when calculating
- illumination. This can be used either to correct for differences in
- units between the x-y coordinate system and the elevation
- coordinate system (e.g. decimal degrees vs. meters) or to
- exaggerate or de-emphasize topographic effects.
- dx : number, optional
- The x-spacing (columns) of the input *elevation* grid.
- dy : number, optional
- The y-spacing (rows) of the input *elevation* grid.
- fraction : number, optional
- Increases or decreases the contrast of the hillshade. Values
- greater than one will cause intermediate values to move closer to
- full illumination or shadow (and clipping any values that move
- beyond 0 or 1). Note that this is not visually or mathematically
- the same as vertical exaggeration.
- Returns
- -------
- ndarray
- A 2d array of illumination values between 0-1, where 0 is
- completely in shadow and 1 is completely illuminated.
- """
- # Because most image and raster GIS data has the first row in the array
- # as the "top" of the image, dy is implicitly negative. This is
- # consistent to what `imshow` assumes, as well.
- dy = -dy
- # compute the normal vectors from the partial derivatives
- e_dy, e_dx = np.gradient(vert_exag * elevation, dy, dx)
- # .view is to keep subclasses
- normal = np.empty(elevation.shape + (3,)).view(type(elevation))
- normal[..., 0] = -e_dx
- normal[..., 1] = -e_dy
- normal[..., 2] = 1
- normal /= _vector_magnitude(normal)
- return self.shade_normals(normal, fraction)
- def shade_normals(self, normals, fraction=1.):
- """
- Calculate the illumination intensity for the normal vectors of a
- surface using the defined azimuth and elevation for the light source.
- Imagine an artificial sun placed at infinity in some azimuth and
- elevation position illuminating our surface. The parts of the surface
- that slope toward the sun should brighten while those sides facing away
- should become darker.
- Parameters
- ----------
- fraction : number, optional
- Increases or decreases the contrast of the hillshade. Values
- greater than one will cause intermediate values to move closer to
- full illumination or shadow (and clipping any values that move
- beyond 0 or 1). Note that this is not visually or mathematically
- the same as vertical exaggeration.
- Returns
- -------
- ndarray
- A 2d array of illumination values between 0-1, where 0 is
- completely in shadow and 1 is completely illuminated.
- """
- intensity = normals.dot(self.direction)
- # Apply contrast stretch
- imin, imax = intensity.min(), intensity.max()
- intensity *= fraction
- # Rescale to 0-1, keeping range before contrast stretch
- # If constant slope, keep relative scaling (i.e. flat should be 0.5,
- # fully occluded 0, etc.)
- if (imax - imin) > 1e-6:
- # Strictly speaking, this is incorrect. Negative values should be
- # clipped to 0 because they're fully occluded. However, rescaling
- # in this manner is consistent with the previous implementation and
- # visually appears better than a "hard" clip.
- intensity -= imin
- intensity /= (imax - imin)
- intensity = np.clip(intensity, 0, 1)
- return intensity
- def shade(self, data, cmap, norm=None, blend_mode='overlay', vmin=None,
- vmax=None, vert_exag=1, dx=1, dy=1, fraction=1, **kwargs):
- """
- Combine colormapped data values with an illumination intensity map
- (a.k.a. "hillshade") of the values.
- Parameters
- ----------
- data : array-like
- A 2d array (or equivalent) of the height values used to generate a
- shaded map.
- cmap : `~matplotlib.colors.Colormap`
- The colormap used to color the *data* array. Note that this must be
- a `~matplotlib.colors.Colormap` instance. For example, rather than
- passing in ``cmap='gist_earth'``, use
- ``cmap=plt.get_cmap('gist_earth')`` instead.
- norm : `~matplotlib.colors.Normalize` instance, optional
- The normalization used to scale values before colormapping. If
- None, the input will be linearly scaled between its min and max.
- blend_mode : {'hsv', 'overlay', 'soft'} or callable, optional
- The type of blending used to combine the colormapped data
- values with the illumination intensity. Default is
- "overlay". Note that for most topographic surfaces,
- "overlay" or "soft" appear more visually realistic. If a
- user-defined function is supplied, it is expected to
- combine an MxNx3 RGB array of floats (ranging 0 to 1) with
- an MxNx1 hillshade array (also 0 to 1). (Call signature
- ``func(rgb, illum, **kwargs)``) Additional kwargs supplied
- to this function will be passed on to the *blend_mode*
- function.
- vmin : float or None, optional
- The minimum value used in colormapping *data*. If *None* the
- minimum value in *data* is used. If *norm* is specified, then this
- argument will be ignored.
- vmax : float or None, optional
- The maximum value used in colormapping *data*. If *None* the
- maximum value in *data* is used. If *norm* is specified, then this
- argument will be ignored.
- vert_exag : number, optional
- The amount to exaggerate the elevation values by when calculating
- illumination. This can be used either to correct for differences in
- units between the x-y coordinate system and the elevation
- coordinate system (e.g. decimal degrees vs. meters) or to
- exaggerate or de-emphasize topography.
- dx : number, optional
- The x-spacing (columns) of the input *elevation* grid.
- dy : number, optional
- The y-spacing (rows) of the input *elevation* grid.
- fraction : number, optional
- Increases or decreases the contrast of the hillshade. Values
- greater than one will cause intermediate values to move closer to
- full illumination or shadow (and clipping any values that move
- beyond 0 or 1). Note that this is not visually or mathematically
- the same as vertical exaggeration.
- Additional kwargs are passed on to the *blend_mode* function.
- Returns
- -------
- ndarray
- An MxNx4 array of floats ranging between 0-1.
- """
- if vmin is None:
- vmin = data.min()
- if vmax is None:
- vmax = data.max()
- if norm is None:
- norm = Normalize(vmin=vmin, vmax=vmax)
- rgb0 = cmap(norm(data))
- rgb1 = self.shade_rgb(rgb0, elevation=data, blend_mode=blend_mode,
- vert_exag=vert_exag, dx=dx, dy=dy,
- fraction=fraction, **kwargs)
- # Don't overwrite the alpha channel, if present.
- rgb0[..., :3] = rgb1[..., :3]
- return rgb0
- def shade_rgb(self, rgb, elevation, fraction=1., blend_mode='hsv',
- vert_exag=1, dx=1, dy=1, **kwargs):
- """
- Use this light source to adjust the colors of the *rgb* input array to
- give the impression of a shaded relief map with the given *elevation*.
- Parameters
- ----------
- rgb : array-like
- An (M, N, 3) RGB array, assumed to be in the range of 0 to 1.
- elevation : array-like
- An (M, N) array of the height values used to generate a shaded map.
- fraction : number
- Increases or decreases the contrast of the hillshade. Values
- greater than one will cause intermediate values to move closer to
- full illumination or shadow (and clipping any values that move
- beyond 0 or 1). Note that this is not visually or mathematically
- the same as vertical exaggeration.
- blend_mode : {'hsv', 'overlay', 'soft'} or callable, optional
- The type of blending used to combine the colormapped data values
- with the illumination intensity. For backwards compatibility, this
- defaults to "hsv". Note that for most topographic surfaces,
- "overlay" or "soft" appear more visually realistic. If a
- user-defined function is supplied, it is expected to combine an
- MxNx3 RGB array of floats (ranging 0 to 1) with an MxNx1 hillshade
- array (also 0 to 1). (Call signature
- ``func(rgb, illum, **kwargs)``)
- Additional kwargs supplied to this function will be passed on to
- the *blend_mode* function.
- vert_exag : number, optional
- The amount to exaggerate the elevation values by when calculating
- illumination. This can be used either to correct for differences in
- units between the x-y coordinate system and the elevation
- coordinate system (e.g. decimal degrees vs. meters) or to
- exaggerate or de-emphasize topography.
- dx : number, optional
- The x-spacing (columns) of the input *elevation* grid.
- dy : number, optional
- The y-spacing (rows) of the input *elevation* grid.
- Additional kwargs are passed on to the *blend_mode* function.
- Returns
- -------
- ndarray
- An (m, n, 3) array of floats ranging between 0-1.
- """
- # Calculate the "hillshade" intensity.
- intensity = self.hillshade(elevation, vert_exag, dx, dy, fraction)
- intensity = intensity[..., np.newaxis]
- # Blend the hillshade and rgb data using the specified mode
- lookup = {
- 'hsv': self.blend_hsv,
- 'soft': self.blend_soft_light,
- 'overlay': self.blend_overlay,
- }
- if blend_mode in lookup:
- blend = lookup[blend_mode](rgb, intensity, **kwargs)
- else:
- try:
- blend = blend_mode(rgb, intensity, **kwargs)
- except TypeError as err:
- raise ValueError('"blend_mode" must be callable or one of {}'
- .format(lookup.keys)) from err
- # Only apply result where hillshade intensity isn't masked
- if np.ma.is_masked(intensity):
- mask = intensity.mask[..., 0]
- for i in range(3):
- blend[..., i][mask] = rgb[..., i][mask]
- return blend
- def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None,
- hsv_min_val=None, hsv_min_sat=None):
- """
- Take the input data array, convert to HSV values in the given colormap,
- then adjust those color values to give the impression of a shaded
- relief map with a specified light source. RGBA values are returned,
- which can then be used to plot the shaded image with imshow.
- The color of the resulting image will be darkened by moving the (s, v)
- values (in hsv colorspace) toward (hsv_min_sat, hsv_min_val) in the
- shaded regions, or lightened by sliding (s, v) toward (hsv_max_sat,
- hsv_max_val) in regions that are illuminated. The default extremes are
- chose so that completely shaded points are nearly black (s = 1, v = 0)
- and completely illuminated points are nearly white (s = 0, v = 1).
- Parameters
- ----------
- rgb : ndarray
- An MxNx3 RGB array of floats ranging from 0 to 1 (color image).
- intensity : ndarray
- An MxNx1 array of floats ranging from 0 to 1 (grayscale image).
- hsv_max_sat : number, default: 1
- The maximum saturation value that the *intensity* map can shift the
- output image to.
- hsv_min_sat : number, optional
- The minimum saturation value that the *intensity* map can shift the
- output image to. Defaults to 0.
- hsv_max_val : number, optional
- The maximum value ("v" in "hsv") that the *intensity* map can shift
- the output image to. Defaults to 1.
- hsv_min_val : number, optional
- The minimum value ("v" in "hsv") that the *intensity* map can shift
- the output image to. Defaults to 0.
- Returns
- -------
- ndarray
- An MxNx3 RGB array representing the combined images.
- """
- # Backward compatibility...
- if hsv_max_sat is None:
- hsv_max_sat = self.hsv_max_sat
- if hsv_max_val is None:
- hsv_max_val = self.hsv_max_val
- if hsv_min_sat is None:
- hsv_min_sat = self.hsv_min_sat
- if hsv_min_val is None:
- hsv_min_val = self.hsv_min_val
- # Expects a 2D intensity array scaled between -1 to 1...
- intensity = intensity[..., 0]
- intensity = 2 * intensity - 1
- # Convert to rgb, then rgb to hsv
- hsv = rgb_to_hsv(rgb[:, :, 0:3])
- hue, sat, val = np.moveaxis(hsv, -1, 0)
- # Modify hsv values (in place) to simulate illumination.
- # putmask(A, mask, B) <=> A[mask] = B[mask]
- np.putmask(sat, (np.abs(sat) > 1.e-10) & (intensity > 0),
- (1 - intensity) * sat + intensity * hsv_max_sat)
- np.putmask(sat, (np.abs(sat) > 1.e-10) & (intensity < 0),
- (1 + intensity) * sat - intensity * hsv_min_sat)
- np.putmask(val, intensity > 0,
- (1 - intensity) * val + intensity * hsv_max_val)
- np.putmask(val, intensity < 0,
- (1 + intensity) * val - intensity * hsv_min_val)
- np.clip(hsv[:, :, 1:], 0, 1, out=hsv[:, :, 1:])
- # Convert modified hsv back to rgb.
- return hsv_to_rgb(hsv)
- def blend_soft_light(self, rgb, intensity):
- """
- Combine an rgb image with an intensity map using "soft light" blending,
- using the "pegtop" formula.
- Parameters
- ----------
- rgb : ndarray
- An MxNx3 RGB array of floats ranging from 0 to 1 (color image).
- intensity : ndarray
- An MxNx1 array of floats ranging from 0 to 1 (grayscale image).
- Returns
- -------
- ndarray
- An MxNx3 RGB array representing the combined images.
- """
- return 2 * intensity * rgb + (1 - 2 * intensity) * rgb**2
- def blend_overlay(self, rgb, intensity):
- """
- Combines an rgb image with an intensity map using "overlay" blending.
- Parameters
- ----------
- rgb : ndarray
- An MxNx3 RGB array of floats ranging from 0 to 1 (color image).
- intensity : ndarray
- An MxNx1 array of floats ranging from 0 to 1 (grayscale image).
- Returns
- -------
- ndarray
- An MxNx3 RGB array representing the combined images.
- """
- low = 2 * intensity * rgb
- high = 1 - 2 * (1 - intensity) * (1 - rgb)
- return np.where(rgb <= 0.5, low, high)
- def from_levels_and_colors(levels, colors, extend='neither'):
- """
- A helper routine to generate a cmap and a norm instance which
- behave similar to contourf's levels and colors arguments.
- Parameters
- ----------
- levels : sequence of numbers
- The quantization levels used to construct the `BoundaryNorm`.
- Value ``v`` is quantized to level ``i`` if ``lev[i] <= v < lev[i+1]``.
- colors : sequence of colors
- The fill color to use for each level. If *extend* is "neither" there
- must be ``n_level - 1`` colors. For an *extend* of "min" or "max" add
- one extra color, and for an *extend* of "both" add two colors.
- extend : {'neither', 'min', 'max', 'both'}, optional
- The behaviour when a value falls out of range of the given levels.
- See `~.Axes.contourf` for details.
- Returns
- -------
- cmap : `~matplotlib.colors.Normalize`
- norm : `~matplotlib.colors.Colormap`
- """
- slice_map = {
- 'both': slice(1, -1),
- 'min': slice(1, None),
- 'max': slice(0, -1),
- 'neither': slice(0, None),
- }
- cbook._check_in_list(slice_map, extend=extend)
- color_slice = slice_map[extend]
- n_data_colors = len(levels) - 1
- n_expected = n_data_colors + color_slice.start - (color_slice.stop or 0)
- if len(colors) != n_expected:
- raise ValueError(
- f'With extend == {extend!r} and {len(levels)} levels, '
- f'expected {n_expected} colors, but got {len(colors)}')
- cmap = ListedColormap(colors[color_slice], N=n_data_colors)
- if extend in ['min', 'both']:
- cmap.set_under(colors[0])
- else:
- cmap.set_under('none')
- if extend in ['max', 'both']:
- cmap.set_over(colors[-1])
- else:
- cmap.set_over('none')
- cmap.colorbar_extend = extend
- norm = BoundaryNorm(levels, ncolors=n_data_colors)
- return cmap, norm
|