123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686 |
- """
- The image module supports basic image loading, rescaling and display
- operations.
- """
- from io import BytesIO
- import math
- import os
- import logging
- from numbers import Number
- from pathlib import Path
- import urllib.parse
- import numpy as np
- from matplotlib import rcParams
- import matplotlib.artist as martist
- from matplotlib.backend_bases import FigureCanvasBase
- import matplotlib.colors as mcolors
- import matplotlib.cm as cm
- import matplotlib.cbook as cbook
- # For clarity, names from _image are given explicitly in this module:
- import matplotlib._image as _image
- # For user convenience, the names from _image are also imported into
- # the image namespace:
- from matplotlib._image import *
- from matplotlib.transforms import (Affine2D, BboxBase, Bbox, BboxTransform,
- IdentityTransform, TransformedBbox)
- _log = logging.getLogger(__name__)
- # map interpolation strings to module constants
- _interpd_ = {
- 'antialiased': _image.NEAREST, # this will use nearest or Hanning...
- 'none': _image.NEAREST, # fall back to nearest when not supported
- 'nearest': _image.NEAREST,
- 'bilinear': _image.BILINEAR,
- 'bicubic': _image.BICUBIC,
- 'spline16': _image.SPLINE16,
- 'spline36': _image.SPLINE36,
- 'hanning': _image.HANNING,
- 'hamming': _image.HAMMING,
- 'hermite': _image.HERMITE,
- 'kaiser': _image.KAISER,
- 'quadric': _image.QUADRIC,
- 'catrom': _image.CATROM,
- 'gaussian': _image.GAUSSIAN,
- 'bessel': _image.BESSEL,
- 'mitchell': _image.MITCHELL,
- 'sinc': _image.SINC,
- 'lanczos': _image.LANCZOS,
- 'blackman': _image.BLACKMAN,
- }
- interpolations_names = set(_interpd_)
- def composite_images(images, renderer, magnification=1.0):
- """
- Composite a number of RGBA images into one. The images are
- composited in the order in which they appear in the `images` list.
- Parameters
- ----------
- images : list of Images
- Each must have a `make_image` method. For each image,
- `can_composite` should return `True`, though this is not
- enforced by this function. Each image must have a purely
- affine transformation with no shear.
- renderer : RendererBase instance
- magnification : float
- The additional magnification to apply for the renderer in use.
- Returns
- -------
- tuple : image, offset_x, offset_y
- Returns the tuple:
- - image: A numpy array of the same type as the input images.
- - offset_x, offset_y: The offset of the image (left, bottom)
- in the output figure.
- """
- if len(images) == 0:
- return np.empty((0, 0, 4), dtype=np.uint8), 0, 0
- parts = []
- bboxes = []
- for image in images:
- data, x, y, trans = image.make_image(renderer, magnification)
- if data is not None:
- x *= magnification
- y *= magnification
- parts.append((data, x, y, image._get_scalar_alpha()))
- bboxes.append(
- Bbox([[x, y], [x + data.shape[1], y + data.shape[0]]]))
- if len(parts) == 0:
- return np.empty((0, 0, 4), dtype=np.uint8), 0, 0
- bbox = Bbox.union(bboxes)
- output = np.zeros(
- (int(bbox.height), int(bbox.width), 4), dtype=np.uint8)
- for data, x, y, alpha in parts:
- trans = Affine2D().translate(x - bbox.x0, y - bbox.y0)
- _image.resample(data, output, trans, _image.NEAREST,
- resample=False, alpha=alpha)
- return output, bbox.x0 / magnification, bbox.y0 / magnification
- def _draw_list_compositing_images(
- renderer, parent, artists, suppress_composite=None):
- """
- Draw a sorted list of artists, compositing images into a single
- image where possible.
- For internal matplotlib use only: It is here to reduce duplication
- between `Figure.draw` and `Axes.draw`, but otherwise should not be
- generally useful.
- """
- has_images = any(isinstance(x, _ImageBase) for x in artists)
- # override the renderer default if suppressComposite is not None
- not_composite = (suppress_composite if suppress_composite is not None
- else renderer.option_image_nocomposite())
- if not_composite or not has_images:
- for a in artists:
- a.draw(renderer)
- else:
- # Composite any adjacent images together
- image_group = []
- mag = renderer.get_image_magnification()
- def flush_images():
- if len(image_group) == 1:
- image_group[0].draw(renderer)
- elif len(image_group) > 1:
- data, l, b = composite_images(image_group, renderer, mag)
- if data.size != 0:
- gc = renderer.new_gc()
- gc.set_clip_rectangle(parent.bbox)
- gc.set_clip_path(parent.get_clip_path())
- renderer.draw_image(gc, round(l), round(b), data)
- gc.restore()
- del image_group[:]
- for a in artists:
- if isinstance(a, _ImageBase) and a.can_composite():
- image_group.append(a)
- else:
- flush_images()
- a.draw(renderer)
- flush_images()
- def _resample(
- image_obj, data, out_shape, transform, *, resample=None, alpha=1):
- """
- Convenience wrapper around `._image.resample` to resample *data* to
- *out_shape* (with a third dimension if *data* is RGBA) that takes care of
- allocating the output array and fetching the relevant properties from the
- Image object *image_obj*.
- """
- # decide if we need to apply anti-aliasing if the data is upsampled:
- # compare the number of displayed pixels to the number of
- # the data pixels.
- interpolation = image_obj.get_interpolation()
- if interpolation == 'antialiased':
- # don't antialias if upsampling by an integer number or
- # if zooming in more than a factor of 3
- pos = np.array([[0, 0], [data.shape[1], data.shape[0]]])
- disp = transform.transform(pos)
- dispx = np.abs(np.diff(disp[:, 0]))
- dispy = np.abs(np.diff(disp[:, 1]))
- if ((dispx > 3 * data.shape[1] or
- dispx == data.shape[1] or
- dispx == 2 * data.shape[1]) and
- (dispy > 3 * data.shape[0] or
- dispy == data.shape[0] or
- dispy == 2 * data.shape[0])):
- interpolation = 'nearest'
- else:
- interpolation = 'hanning'
- out = np.zeros(out_shape + data.shape[2:], data.dtype) # 2D->2D, 3D->3D.
- if resample is None:
- resample = image_obj.get_resample()
- _image.resample(data, out, transform,
- _interpd_[interpolation],
- resample,
- alpha,
- image_obj.get_filternorm(),
- image_obj.get_filterrad())
- return out
- def _rgb_to_rgba(A):
- """
- Convert an RGB image to RGBA, as required by the image resample C++
- extension.
- """
- rgba = np.zeros((A.shape[0], A.shape[1], 4), dtype=A.dtype)
- rgba[:, :, :3] = A
- if rgba.dtype == np.uint8:
- rgba[:, :, 3] = 255
- else:
- rgba[:, :, 3] = 1.0
- return rgba
- class _ImageBase(martist.Artist, cm.ScalarMappable):
- """
- Base class for images.
- interpolation and cmap default to their rc settings
- cmap is a colors.Colormap instance
- norm is a colors.Normalize instance to map luminance to 0-1
- extent is data axes (left, right, bottom, top) for making image plots
- registered with data plots. Default is to label the pixel
- centers with the zero-based row and column indices.
- Additional kwargs are matplotlib.artist properties
- """
- zorder = 0
- def __init__(self, ax,
- cmap=None,
- norm=None,
- interpolation=None,
- origin=None,
- filternorm=True,
- filterrad=4.0,
- resample=False,
- **kwargs
- ):
- martist.Artist.__init__(self)
- cm.ScalarMappable.__init__(self, norm, cmap)
- self._mouseover = True
- if origin is None:
- origin = rcParams['image.origin']
- self.origin = origin
- self.set_filternorm(filternorm)
- self.set_filterrad(filterrad)
- self.set_interpolation(interpolation)
- self.set_resample(resample)
- self.axes = ax
- self._imcache = None
- self.update(kwargs)
- def __getstate__(self):
- state = super().__getstate__()
- # We can't pickle the C Image cached object.
- state['_imcache'] = None
- return state
- def get_size(self):
- """Return the size of the image as tuple (numrows, numcols)."""
- if self._A is None:
- raise RuntimeError('You must first set the image array')
- return self._A.shape[:2]
- def set_alpha(self, alpha):
- """
- Set the alpha value used for blending - not supported on all backends.
- Parameters
- ----------
- alpha : float
- """
- if alpha is not None and not isinstance(alpha, Number):
- alpha = np.asarray(alpha)
- if alpha.ndim != 2:
- raise TypeError('alpha must be a float, two-dimensional '
- 'array, or None')
- self._alpha = alpha
- self.pchanged()
- self.stale = True
- self._imcache = None
- def _get_scalar_alpha(self):
- """
- Get a scalar alpha value to be applied to the artist as a whole.
- If the alpha value is a matrix, the method returns 1.0 because pixels
- have individual alpha values (see `~._ImageBase._make_image` for
- details). If the alpha value is a scalar, the method returns said value
- to be applied to the artist as a whole because pixels do not have
- individual alpha values.
- """
- return 1.0 if self._alpha is None or np.ndim(self._alpha) > 0 \
- else self._alpha
- def changed(self):
- """
- Call this whenever the mappable is changed so observers can
- update state
- """
- self._imcache = None
- self._rgbacache = None
- cm.ScalarMappable.changed(self)
- def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
- unsampled=False, round_to_pixel_border=True):
- """
- Normalize, rescale, and colormap the image *A* from the given *in_bbox*
- (in data space), to the given *out_bbox* (in pixel space) clipped to
- the given *clip_bbox* (also in pixel space), and magnified by the
- *magnification* factor.
- *A* may be a greyscale image (M, N) with a dtype of float32, float64,
- float128, uint16 or uint8, or an (M, N, 4) RGBA image with a dtype of
- float32, float64, float128, or uint8.
- If *unsampled* is True, the image will not be scaled, but an
- appropriate affine transformation will be returned instead.
- If *round_to_pixel_border* is True, the output image size will be
- rounded to the nearest pixel boundary. This makes the images align
- correctly with the axes. It should not be used if exact scaling is
- needed, such as for `FigureImage`.
- Returns
- -------
- image : (M, N, 4) uint8 array
- The RGBA image, resampled unless *unsampled* is True.
- x, y : float
- The upper left corner where the image should be drawn, in pixel
- space.
- trans : Affine2D
- The affine transformation from image to pixel space.
- """
- if A is None:
- raise RuntimeError('You must first set the image '
- 'array or the image attribute')
- if A.size == 0:
- raise RuntimeError("_make_image must get a non-empty image. "
- "Your Artist's draw method must filter before "
- "this method is called.")
- clipped_bbox = Bbox.intersection(out_bbox, clip_bbox)
- if clipped_bbox is None:
- return None, 0, 0, None
- out_width_base = clipped_bbox.width * magnification
- out_height_base = clipped_bbox.height * magnification
- if out_width_base == 0 or out_height_base == 0:
- return None, 0, 0, None
- if self.origin == 'upper':
- # Flip the input image using a transform. This avoids the
- # problem with flipping the array, which results in a copy
- # when it is converted to contiguous in the C wrapper
- t0 = Affine2D().translate(0, -A.shape[0]).scale(1, -1)
- else:
- t0 = IdentityTransform()
- t0 += (
- Affine2D()
- .scale(
- in_bbox.width / A.shape[1],
- in_bbox.height / A.shape[0])
- .translate(in_bbox.x0, in_bbox.y0)
- + self.get_transform())
- t = (t0
- + (Affine2D()
- .translate(-clipped_bbox.x0, -clipped_bbox.y0)
- .scale(magnification)))
- # So that the image is aligned with the edge of the axes, we want to
- # round up the output width to the next integer. This also means
- # scaling the transform slightly to account for the extra subpixel.
- if (t.is_affine and round_to_pixel_border and
- (out_width_base % 1.0 != 0.0 or out_height_base % 1.0 != 0.0)):
- out_width = math.ceil(out_width_base)
- out_height = math.ceil(out_height_base)
- extra_width = (out_width - out_width_base) / out_width_base
- extra_height = (out_height - out_height_base) / out_height_base
- t += Affine2D().scale(1.0 + extra_width, 1.0 + extra_height)
- else:
- out_width = int(out_width_base)
- out_height = int(out_height_base)
- out_shape = (out_height, out_width)
- if not unsampled:
- if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)):
- raise ValueError(f"Invalid shape {A.shape} for image data")
- if A.ndim == 2:
- # if we are a 2D array, then we are running through the
- # norm + colormap transformation. However, in general the
- # input data is not going to match the size on the screen so we
- # have to resample to the correct number of pixels
- # TODO slice input array first
- inp_dtype = A.dtype
- a_min = A.min()
- a_max = A.max()
- # figure out the type we should scale to. For floats,
- # leave as is. For integers cast to an appropriate-sized
- # float. Small integers get smaller floats in an attempt
- # to keep the memory footprint reasonable.
- if a_min is np.ma.masked:
- # all masked, so values don't matter
- a_min, a_max = np.int32(0), np.int32(1)
- if inp_dtype.kind == 'f':
- scaled_dtype = A.dtype
- # Cast to float64
- if A.dtype not in (np.float32, np.float16):
- if A.dtype != np.float64:
- cbook._warn_external(
- f"Casting input data from '{A.dtype}' to "
- f"'float64' for imshow")
- scaled_dtype = np.float64
- else:
- # probably an integer of some type.
- da = a_max.astype(np.float64) - a_min.astype(np.float64)
- # give more breathing room if a big dynamic range
- scaled_dtype = np.float64 if da > 1e8 else np.float32
- # scale the input data to [.1, .9]. The Agg
- # interpolators clip to [0, 1] internally, use a
- # smaller input scale to identify which of the
- # interpolated points need to be should be flagged as
- # over / under.
- # This may introduce numeric instabilities in very broadly
- # scaled data
- # Always copy, and don't allow array subtypes.
- A_scaled = np.array(A, dtype=scaled_dtype)
- # clip scaled data around norm if necessary.
- # This is necessary for big numbers at the edge of
- # float64's ability to represent changes. Applying
- # a norm first would be good, but ruins the interpolation
- # of over numbers.
- self.norm.autoscale_None(A)
- dv = np.float64(self.norm.vmax) - np.float64(self.norm.vmin)
- vmid = self.norm.vmin + dv / 2
- fact = 1e7 if scaled_dtype == np.float64 else 1e4
- newmin = vmid - dv * fact
- if newmin < a_min:
- newmin = None
- else:
- a_min = np.float64(newmin)
- newmax = vmid + dv * fact
- if newmax > a_max:
- newmax = None
- else:
- a_max = np.float64(newmax)
- if newmax is not None or newmin is not None:
- np.clip(A_scaled, newmin, newmax, out=A_scaled)
- A_scaled -= a_min
- # a_min and a_max might be ndarray subclasses so use
- # item to avoid errors
- a_min = a_min.astype(scaled_dtype).item()
- a_max = a_max.astype(scaled_dtype).item()
- if a_min != a_max:
- A_scaled /= ((a_max - a_min) / 0.8)
- A_scaled += 0.1
- # resample the input data to the correct resolution and shape
- A_resampled = _resample(self, A_scaled, out_shape, t)
- # done with A_scaled now, remove from namespace to be sure!
- del A_scaled
- # un-scale the resampled data to approximately the
- # original range things that interpolated to above /
- # below the original min/max will still be above /
- # below, but possibly clipped in the case of higher order
- # interpolation + drastically changing data.
- A_resampled -= 0.1
- if a_min != a_max:
- A_resampled *= ((a_max - a_min) / 0.8)
- A_resampled += a_min
- # if using NoNorm, cast back to the original datatype
- if isinstance(self.norm, mcolors.NoNorm):
- A_resampled = A_resampled.astype(A.dtype)
- mask = (np.where(A.mask, np.float32(np.nan), np.float32(1))
- if A.mask.shape == A.shape # nontrivial mask
- else np.ones_like(A, np.float32))
- # we always have to interpolate the mask to account for
- # non-affine transformations
- out_alpha = _resample(self, mask, out_shape, t, resample=True)
- # done with the mask now, delete from namespace to be sure!
- del mask
- # Agg updates out_alpha in place. If the pixel has no image
- # data it will not be updated (and still be 0 as we initialized
- # it), if input data that would go into that output pixel than
- # it will be `nan`, if all the input data for a pixel is good
- # it will be 1, and if there is _some_ good data in that output
- # pixel it will be between [0, 1] (such as a rotated image).
- out_mask = np.isnan(out_alpha)
- out_alpha[out_mask] = 1
- # Apply the pixel-by-pixel alpha values if present
- alpha = self.get_alpha()
- if alpha is not None and np.ndim(alpha) > 0:
- out_alpha *= _resample(self, alpha, out_shape,
- t, resample=True)
- # mask and run through the norm
- output = self.norm(np.ma.masked_array(A_resampled, out_mask))
- else:
- if A.shape[2] == 3:
- A = _rgb_to_rgba(A)
- alpha = self._get_scalar_alpha()
- output_alpha = _resample( # resample alpha channel
- self, A[..., 3], out_shape, t, alpha=alpha)
- output = _resample( # resample rgb channels
- self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
- output[..., 3] = output_alpha # recombine rgb and alpha
- # at this point output is either a 2D array of normed data
- # (of int or float)
- # or an RGBA array of re-sampled input
- output = self.to_rgba(output, bytes=True, norm=False)
- # output is now a correctly sized RGBA array of uint8
- # Apply alpha *after* if the input was greyscale without a mask
- if A.ndim == 2:
- alpha = self._get_scalar_alpha()
- alpha_channel = output[:, :, 3]
- alpha_channel[:] = np.asarray(
- np.asarray(alpha_channel, np.float32) * out_alpha * alpha,
- np.uint8)
- else:
- if self._imcache is None:
- self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))
- output = self._imcache
- # Subset the input image to only the part that will be
- # displayed
- subset = TransformedBbox(clip_bbox, t0.inverted()).frozen()
- output = output[
- int(max(subset.ymin, 0)):
- int(min(subset.ymax + 1, output.shape[0])),
- int(max(subset.xmin, 0)):
- int(min(subset.xmax + 1, output.shape[1]))]
- t = Affine2D().translate(
- int(max(subset.xmin, 0)), int(max(subset.ymin, 0))) + t
- return output, clipped_bbox.x0, clipped_bbox.y0, t
- def make_image(self, renderer, magnification=1.0, unsampled=False):
- """
- Normalize, rescale, and colormap this image's data for rendering using
- *renderer*, with the given *magnification*.
- If *unsampled* is True, the image will not be scaled, but an
- appropriate affine transformation will be returned instead.
- Returns
- -------
- image : (M, N, 4) uint8 array
- The RGBA image, resampled unless *unsampled* is True.
- x, y : float
- The upper left corner where the image should be drawn, in pixel
- space.
- trans : Affine2D
- The affine transformation from image to pixel space.
- """
- raise NotImplementedError('The make_image method must be overridden')
- def _draw_unsampled_image(self, renderer, gc):
- """
- Draw unsampled image. The renderer should support a draw_image method
- with scale parameter.
- """
- im, l, b, trans = self.make_image(renderer, unsampled=True)
- if im is None:
- return
- trans = Affine2D().scale(im.shape[1], im.shape[0]) + trans
- renderer.draw_image(gc, l, b, im, trans)
- def _check_unsampled_image(self, renderer):
- """
- Return whether the image is better to be drawn unsampled.
- The derived class needs to override it.
- """
- return False
- @martist.allow_rasterization
- def draw(self, renderer, *args, **kwargs):
- # if not visible, declare victory and return
- if not self.get_visible():
- self.stale = False
- return
- # for empty images, there is nothing to draw!
- if self.get_array().size == 0:
- self.stale = False
- return
- # actually render the image.
- gc = renderer.new_gc()
- self._set_gc_clip(gc)
- gc.set_alpha(self._get_scalar_alpha())
- gc.set_url(self.get_url())
- gc.set_gid(self.get_gid())
- if (self._check_unsampled_image(renderer) and
- self.get_transform().is_affine):
- self._draw_unsampled_image(renderer, gc)
- else:
- im, l, b, trans = self.make_image(
- renderer, renderer.get_image_magnification())
- if im is not None:
- renderer.draw_image(gc, l, b, im)
- gc.restore()
- self.stale = False
- def contains(self, mouseevent):
- """
- Test whether the mouse event occurred within the image.
- """
- inside, info = self._default_contains(mouseevent)
- if inside is not None:
- return inside, info
- # 1) This doesn't work for figimage; but figimage also needs a fix
- # below (as the check cannot use x/ydata and extents).
- # 2) As long as the check below uses x/ydata, we need to test axes
- # identity instead of `self.axes.contains(event)` because even if
- # axes overlap, x/ydata is only valid for event.inaxes anyways.
- if self.axes is not mouseevent.inaxes:
- return False, {}
- # TODO: make sure this is consistent with patch and patch
- # collection on nonlinear transformed coordinates.
- # TODO: consider returning image coordinates (shouldn't
- # be too difficult given that the image is rectilinear
- x, y = mouseevent.xdata, mouseevent.ydata
- xmin, xmax, ymin, ymax = self.get_extent()
- if xmin > xmax:
- xmin, xmax = xmax, xmin
- if ymin > ymax:
- ymin, ymax = ymax, ymin
- if x is not None and y is not None:
- inside = (xmin <= x <= xmax) and (ymin <= y <= ymax)
- else:
- inside = False
- return inside, {}
- def write_png(self, fname):
- """Write the image to png file with fname"""
- from matplotlib import _png
- im = self.to_rgba(self._A[::-1] if self.origin == 'lower' else self._A,
- bytes=True, norm=True)
- with cbook.open_file_cm(fname, "wb") as file:
- _png.write_png(im, file)
- def set_data(self, A):
- """
- Set the image array.
- Note that this function does *not* update the normalization used.
- Parameters
- ----------
- A : array-like or `PIL.Image.Image`
- """
- try:
- from PIL import Image
- except ImportError:
- pass
- else:
- if isinstance(A, Image.Image):
- A = pil_to_array(A) # Needed e.g. to apply png palette.
- self._A = cbook.safe_masked_invalid(A, copy=True)
- if (self._A.dtype != np.uint8 and
- not np.can_cast(self._A.dtype, float, "same_kind")):
- raise TypeError("Image data of dtype {} cannot be converted to "
- "float".format(self._A.dtype))
- if not (self._A.ndim == 2
- or self._A.ndim == 3 and self._A.shape[-1] in [3, 4]):
- raise TypeError("Invalid shape {} for image data"
- .format(self._A.shape))
- if self._A.ndim == 3:
- # If the input data has values outside the valid range (after
- # normalisation), we issue a warning and then clip X to the bounds
- # - otherwise casting wraps extreme values, hiding outliers and
- # making reliable interpretation impossible.
- high = 255 if np.issubdtype(self._A.dtype, np.integer) else 1
- if self._A.min() < 0 or high < self._A.max():
- _log.warning(
- 'Clipping input data to the valid range for imshow with '
- 'RGB data ([0..1] for floats or [0..255] for integers).'
- )
- self._A = np.clip(self._A, 0, high)
- # Cast unsupported integer types to uint8
- if self._A.dtype != np.uint8 and np.issubdtype(self._A.dtype,
- np.integer):
- self._A = self._A.astype(np.uint8)
- self._imcache = None
- self._rgbacache = None
- self.stale = True
- def set_array(self, A):
- """
- Retained for backwards compatibility - use set_data instead.
- Parameters
- ----------
- A : array-like
- """
- # This also needs to be here to override the inherited
- # cm.ScalarMappable.set_array method so it is not invoked by mistake.
- self.set_data(A)
- def get_interpolation(self):
- """
- Return the interpolation method the image uses when resizing.
- One of 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16',
- 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric',
- 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos',
- or 'none'.
- """
- return self._interpolation
- def set_interpolation(self, s):
- """
- Set the interpolation method the image uses when resizing.
- if None, use a value from rc setting. If 'none', the image is
- shown as is without interpolating. 'none' is only supported in
- agg, ps and pdf backends and will fall back to 'nearest' mode
- for other backends.
- Parameters
- ----------
- s : {'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16',
- 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', \
- 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'none'}
- """
- if s is None:
- s = rcParams['image.interpolation']
- s = s.lower()
- cbook._check_in_list(_interpd_, interpolation=s)
- self._interpolation = s
- self.stale = True
- def can_composite(self):
- """Return whether the image can be composited with its neighbors."""
- trans = self.get_transform()
- return (
- self._interpolation != 'none' and
- trans.is_affine and
- trans.is_separable)
- def set_resample(self, v):
- """
- Set whether image resampling is used.
- Parameters
- ----------
- v : bool or None
- If None, use :rc:`image.resample` = True.
- """
- if v is None:
- v = rcParams['image.resample']
- self._resample = v
- self.stale = True
- def get_resample(self):
- """Return whether image resampling is used."""
- return self._resample
- def set_filternorm(self, filternorm):
- """
- Set whether the resize filter normalizes the weights.
- See help for `~.Axes.imshow`.
- Parameters
- ----------
- filternorm : bool
- """
- self._filternorm = bool(filternorm)
- self.stale = True
- def get_filternorm(self):
- """Return whether the resize filter normalizes the weights."""
- return self._filternorm
- def set_filterrad(self, filterrad):
- """
- Set the resize filter radius only applicable to some
- interpolation schemes -- see help for imshow
- Parameters
- ----------
- filterrad : positive float
- """
- r = float(filterrad)
- if r <= 0:
- raise ValueError("The filter radius must be a positive number")
- self._filterrad = r
- self.stale = True
- def get_filterrad(self):
- """Return the filterrad setting."""
- return self._filterrad
- class AxesImage(_ImageBase):
- """
- Parameters
- ----------
- ax : `~.axes.Axes`
- The axes the image will belong to.
- cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap`
- The Colormap instance or registered colormap name used to map scalar
- data to colors.
- norm : `~matplotlib.colors.Normalize`
- Maps luminance to 0-1.
- interpolation : str, default: :rc:`image.interpolation`
- Supported values are 'none', 'antialiased', 'nearest', 'bilinear',
- 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite',
- 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell',
- 'sinc', 'lanczos'.
- origin : {'upper', 'lower'}, default: :rc:`image.origin`
- Place the [0, 0] index of the array in the upper left or lower left
- corner of the axes. The convention 'upper' is typically used for
- matrices and images.
- extent : tuple, optional
- The data axes (left, right, bottom, top) for making image plots
- registered with data plots. Default is to label the pixel
- centers with the zero-based row and column indices.
- filternorm : bool, default: True
- A parameter for the antigrain image resize filter
- (see the antigrain documentation).
- If filternorm is set, the filter normalizes integer values and corrects
- the rounding errors. It doesn't do anything with the source floating
- point values, it corrects only integers according to the rule of 1.0
- which means that any sum of pixel weights must be equal to 1.0. So,
- the filter function must produce a graph of the proper shape.
- filterrad : float > 0, default: 4
- The filter radius for filters that have a radius parameter, i.e. when
- interpolation is one of: 'sinc', 'lanczos' or 'blackman'.
- resample : bool, default: False
- When True, use a full resampling method. When False, only resample when
- the output image is larger than the input image.
- **kwargs : `.Artist` properties
- """
- def __str__(self):
- return "AxesImage(%g,%g;%gx%g)" % tuple(self.axes.bbox.bounds)
- def __init__(self, ax,
- cmap=None,
- norm=None,
- interpolation=None,
- origin=None,
- extent=None,
- filternorm=1,
- filterrad=4.0,
- resample=False,
- **kwargs
- ):
- self._extent = extent
- super().__init__(
- ax,
- cmap=cmap,
- norm=norm,
- interpolation=interpolation,
- origin=origin,
- filternorm=filternorm,
- filterrad=filterrad,
- resample=resample,
- **kwargs
- )
- def get_window_extent(self, renderer=None):
- x0, x1, y0, y1 = self._extent
- bbox = Bbox.from_extents([x0, y0, x1, y1])
- return bbox.transformed(self.axes.transData)
- def make_image(self, renderer, magnification=1.0, unsampled=False):
- # docstring inherited
- trans = self.get_transform()
- # image is created in the canvas coordinate.
- x1, x2, y1, y2 = self.get_extent()
- bbox = Bbox(np.array([[x1, y1], [x2, y2]]))
- transformed_bbox = TransformedBbox(bbox, trans)
- return self._make_image(
- self._A, bbox, transformed_bbox,
- self.get_clip_box() or self.axes.bbox,
- magnification, unsampled=unsampled)
- def _check_unsampled_image(self, renderer):
- """
- Return whether the image would be better drawn unsampled.
- """
- return (self.get_interpolation() == "none"
- and renderer.option_scale_image())
- def set_extent(self, extent):
- """
- Set the image extent.
- Parameters
- ----------
- extent : 4-tuple of float
- The position and size of the image as tuple
- ``(left, right, bottom, top)`` in data coordinates.
- Notes
- -----
- This updates ``ax.dataLim``, and, if autoscaling, sets ``ax.viewLim``
- to tightly fit the image, regardless of ``dataLim``. Autoscaling
- state is not changed, so following this with ``ax.autoscale_view()``
- will redo the autoscaling in accord with ``dataLim``.
- """
- self._extent = xmin, xmax, ymin, ymax = extent
- corners = (xmin, ymin), (xmax, ymax)
- self.axes.update_datalim(corners)
- self.sticky_edges.x[:] = [xmin, xmax]
- self.sticky_edges.y[:] = [ymin, ymax]
- if self.axes._autoscaleXon:
- self.axes.set_xlim((xmin, xmax), auto=None)
- if self.axes._autoscaleYon:
- self.axes.set_ylim((ymin, ymax), auto=None)
- self.stale = True
- def get_extent(self):
- """Return the image extent as tuple (left, right, bottom, top)."""
- if self._extent is not None:
- return self._extent
- else:
- sz = self.get_size()
- numrows, numcols = sz
- if self.origin == 'upper':
- return (-0.5, numcols-0.5, numrows-0.5, -0.5)
- else:
- return (-0.5, numcols-0.5, -0.5, numrows-0.5)
- def get_cursor_data(self, event):
- """
- Return the image value at the event position or *None* if the event is
- outside the image.
- See Also
- --------
- matplotlib.artist.Artist.get_cursor_data
- """
- xmin, xmax, ymin, ymax = self.get_extent()
- if self.origin == 'upper':
- ymin, ymax = ymax, ymin
- arr = self.get_array()
- data_extent = Bbox([[ymin, xmin], [ymax, xmax]])
- array_extent = Bbox([[0, 0], arr.shape[:2]])
- trans = BboxTransform(boxin=data_extent, boxout=array_extent)
- point = trans.transform([event.ydata, event.xdata])
- if any(np.isnan(point)):
- return None
- i, j = point.astype(int)
- # Clip the coordinates at array bounds
- if not (0 <= i < arr.shape[0]) or not (0 <= j < arr.shape[1]):
- return None
- else:
- return arr[i, j]
- def format_cursor_data(self, data):
- if np.ndim(data) == 0 and self.colorbar:
- return (
- "["
- + cbook.strip_math(
- self.colorbar.formatter.format_data_short(data)).strip()
- + "]")
- else:
- return super().format_cursor_data(data)
- class NonUniformImage(AxesImage):
- def __init__(self, ax, *, interpolation='nearest', **kwargs):
- """
- Parameters
- ----------
- interpolation : {'nearest', 'bilinear'}
- **kwargs
- All other keyword arguments are identical to those of `.AxesImage`.
- """
- super().__init__(ax, **kwargs)
- self.set_interpolation(interpolation)
- def _check_unsampled_image(self, renderer):
- """Return False. Do not use unsampled image."""
- return False
- def make_image(self, renderer, magnification=1.0, unsampled=False):
- # docstring inherited
- if self._A is None:
- raise RuntimeError('You must first set the image array')
- if unsampled:
- raise ValueError('unsampled not supported on NonUniformImage')
- A = self._A
- if A.ndim == 2:
- if A.dtype != np.uint8:
- A = self.to_rgba(A, bytes=True)
- self.is_grayscale = self.cmap.is_gray()
- else:
- A = np.repeat(A[:, :, np.newaxis], 4, 2)
- A[:, :, 3] = 255
- self.is_grayscale = True
- else:
- if A.dtype != np.uint8:
- A = (255*A).astype(np.uint8)
- if A.shape[2] == 3:
- B = np.zeros(tuple([*A.shape[0:2], 4]), np.uint8)
- B[:, :, 0:3] = A
- B[:, :, 3] = 255
- A = B
- self.is_grayscale = False
- x0, y0, v_width, v_height = self.axes.viewLim.bounds
- l, b, r, t = self.axes.bbox.extents
- width = (round(r) + 0.5) - (round(l) - 0.5)
- height = (round(t) + 0.5) - (round(b) - 0.5)
- width *= magnification
- height *= magnification
- im = _image.pcolor(self._Ax, self._Ay, A,
- int(height), int(width),
- (x0, x0+v_width, y0, y0+v_height),
- _interpd_[self._interpolation])
- return im, l, b, IdentityTransform()
- def set_data(self, x, y, A):
- """
- Set the grid for the pixel centers, and the pixel values.
- Parameters
- ----------
- x, y : 1D array-likes
- Monotonic arrays of shapes (N,) and (M,), respectively, specifying
- pixel centers.
- A : array-like
- (M, N) ndarray or masked array of values to be colormapped, or
- (M, N, 3) RGB array, or (M, N, 4) RGBA array.
- """
- x = np.array(x, np.float32)
- y = np.array(y, np.float32)
- A = cbook.safe_masked_invalid(A, copy=True)
- if not (x.ndim == y.ndim == 1 and A.shape[0:2] == y.shape + x.shape):
- raise TypeError("Axes don't match array shape")
- if A.ndim not in [2, 3]:
- raise TypeError("Can only plot 2D or 3D data")
- if A.ndim == 3 and A.shape[2] not in [1, 3, 4]:
- raise TypeError("3D arrays must have three (RGB) "
- "or four (RGBA) color components")
- if A.ndim == 3 and A.shape[2] == 1:
- A.shape = A.shape[0:2]
- self._A = A
- self._Ax = x
- self._Ay = y
- self._imcache = None
- self.stale = True
- def set_array(self, *args):
- raise NotImplementedError('Method not supported')
- def set_interpolation(self, s):
- """
- Parameters
- ----------
- s : str, None
- Either 'nearest', 'bilinear', or ``None``.
- """
- if s is not None and s not in ('nearest', 'bilinear'):
- raise NotImplementedError('Only nearest neighbor and '
- 'bilinear interpolations are supported')
- AxesImage.set_interpolation(self, s)
- def get_extent(self):
- if self._A is None:
- raise RuntimeError('Must set data first')
- return self._Ax[0], self._Ax[-1], self._Ay[0], self._Ay[-1]
- def set_filternorm(self, s):
- pass
- def set_filterrad(self, s):
- pass
- def set_norm(self, norm):
- if self._A is not None:
- raise RuntimeError('Cannot change colors after loading data')
- super().set_norm(norm)
- def set_cmap(self, cmap):
- if self._A is not None:
- raise RuntimeError('Cannot change colors after loading data')
- super().set_cmap(cmap)
- class PcolorImage(AxesImage):
- """
- Make a pcolor-style plot with an irregular rectangular grid.
- This uses a variation of the original irregular image code,
- and it is used by pcolorfast for the corresponding grid type.
- """
- def __init__(self, ax,
- x=None,
- y=None,
- A=None,
- cmap=None,
- norm=None,
- **kwargs
- ):
- """
- cmap defaults to its rc setting
- cmap is a colors.Colormap instance
- norm is a colors.Normalize instance to map luminance to 0-1
- Additional kwargs are matplotlib.artist properties
- """
- super().__init__(ax, norm=norm, cmap=cmap)
- self.update(kwargs)
- if A is not None:
- self.set_data(x, y, A)
- def make_image(self, renderer, magnification=1.0, unsampled=False):
- # docstring inherited
- if self._A is None:
- raise RuntimeError('You must first set the image array')
- if unsampled:
- raise ValueError('unsampled not supported on PColorImage')
- fc = self.axes.patch.get_facecolor()
- bg = mcolors.to_rgba(fc, 0)
- bg = (np.array(bg)*255).astype(np.uint8)
- l, b, r, t = self.axes.bbox.extents
- width = (round(r) + 0.5) - (round(l) - 0.5)
- height = (round(t) + 0.5) - (round(b) - 0.5)
- # The extra cast-to-int is only needed for python2
- width = int(round(width * magnification))
- height = int(round(height * magnification))
- if self._rgbacache is None:
- A = self.to_rgba(self._A, bytes=True)
- self._rgbacache = A
- if self._A.ndim == 2:
- self.is_grayscale = self.cmap.is_gray()
- else:
- A = self._rgbacache
- vl = self.axes.viewLim
- im = _image.pcolor2(self._Ax, self._Ay, A,
- height,
- width,
- (vl.x0, vl.x1, vl.y0, vl.y1),
- bg)
- return im, l, b, IdentityTransform()
- def _check_unsampled_image(self, renderer):
- return False
- def set_data(self, x, y, A):
- """
- Set the grid for the rectangle boundaries, and the data values.
- Parameters
- ----------
- x, y : 1D array-likes or None
- Monotonic arrays of shapes (N + 1,) and (M + 1,), respectively,
- specifying rectangle boundaries. If None, will default to
- ``range(N + 1)`` and ``range(M + 1)``, respectively.
- A : array-like
- (M, N) ndarray or masked array of values to be colormapped, or
- (M, N, 3) RGB array, or (M, N, 4) RGBA array.
- """
- A = cbook.safe_masked_invalid(A, copy=True)
- if x is None:
- x = np.arange(0, A.shape[1]+1, dtype=np.float64)
- else:
- x = np.array(x, np.float64).ravel()
- if y is None:
- y = np.arange(0, A.shape[0]+1, dtype=np.float64)
- else:
- y = np.array(y, np.float64).ravel()
- if A.shape[:2] != (y.size-1, x.size-1):
- raise ValueError(
- "Axes don't match array shape. Got %s, expected %s." %
- (A.shape[:2], (y.size - 1, x.size - 1)))
- if A.ndim not in [2, 3]:
- raise ValueError("A must be 2D or 3D")
- if A.ndim == 3 and A.shape[2] == 1:
- A.shape = A.shape[:2]
- self.is_grayscale = False
- if A.ndim == 3:
- if A.shape[2] in [3, 4]:
- if ((A[:, :, 0] == A[:, :, 1]).all() and
- (A[:, :, 0] == A[:, :, 2]).all()):
- self.is_grayscale = True
- else:
- raise ValueError("3D arrays must have RGB or RGBA as last dim")
- # For efficient cursor readout, ensure x and y are increasing.
- if x[-1] < x[0]:
- x = x[::-1]
- A = A[:, ::-1]
- if y[-1] < y[0]:
- y = y[::-1]
- A = A[::-1]
- self._A = A
- self._Ax = x
- self._Ay = y
- self._rgbacache = None
- self.stale = True
- def set_array(self, *args):
- raise NotImplementedError('Method not supported')
- def get_cursor_data(self, event):
- # docstring inherited
- x, y = event.xdata, event.ydata
- if (x < self._Ax[0] or x > self._Ax[-1] or
- y < self._Ay[0] or y > self._Ay[-1]):
- return None
- j = np.searchsorted(self._Ax, x) - 1
- i = np.searchsorted(self._Ay, y) - 1
- try:
- return self._A[i, j]
- except IndexError:
- return None
- class FigureImage(_ImageBase):
- zorder = 0
- _interpolation = 'nearest'
- def __init__(self, fig,
- cmap=None,
- norm=None,
- offsetx=0,
- offsety=0,
- origin=None,
- **kwargs
- ):
- """
- cmap is a colors.Colormap instance
- norm is a colors.Normalize instance to map luminance to 0-1
- kwargs are an optional list of Artist keyword args
- """
- super().__init__(
- None,
- norm=norm,
- cmap=cmap,
- origin=origin
- )
- self.figure = fig
- self.ox = offsetx
- self.oy = offsety
- self.update(kwargs)
- self.magnification = 1.0
- def get_extent(self):
- """Return the image extent as tuple (left, right, bottom, top)."""
- numrows, numcols = self.get_size()
- return (-0.5 + self.ox, numcols-0.5 + self.ox,
- -0.5 + self.oy, numrows-0.5 + self.oy)
- def make_image(self, renderer, magnification=1.0, unsampled=False):
- # docstring inherited
- fac = renderer.dpi/self.figure.dpi
- # fac here is to account for pdf, eps, svg backends where
- # figure.dpi is set to 72. This means we need to scale the
- # image (using magnification) and offset it appropriately.
- bbox = Bbox([[self.ox/fac, self.oy/fac],
- [(self.ox/fac + self._A.shape[1]),
- (self.oy/fac + self._A.shape[0])]])
- width, height = self.figure.get_size_inches()
- width *= renderer.dpi
- height *= renderer.dpi
- clip = Bbox([[0, 0], [width, height]])
- return self._make_image(
- self._A, bbox, bbox, clip, magnification=magnification / fac,
- unsampled=unsampled, round_to_pixel_border=False)
- def set_data(self, A):
- """Set the image array."""
- cm.ScalarMappable.set_array(self,
- cbook.safe_masked_invalid(A, copy=True))
- self.stale = True
- class BboxImage(_ImageBase):
- """The Image class whose size is determined by the given bbox."""
- @cbook._delete_parameter("3.1", "interp_at_native")
- def __init__(self, bbox,
- cmap=None,
- norm=None,
- interpolation=None,
- origin=None,
- filternorm=1,
- filterrad=4.0,
- resample=False,
- interp_at_native=True,
- **kwargs
- ):
- """
- cmap is a colors.Colormap instance
- norm is a colors.Normalize instance to map luminance to 0-1
- kwargs are an optional list of Artist keyword args
- """
- super().__init__(
- None,
- cmap=cmap,
- norm=norm,
- interpolation=interpolation,
- origin=origin,
- filternorm=filternorm,
- filterrad=filterrad,
- resample=resample,
- **kwargs
- )
- self.bbox = bbox
- self._interp_at_native = interp_at_native
- self._transform = IdentityTransform()
- @cbook.deprecated("3.1")
- @property
- def interp_at_native(self):
- return self._interp_at_native
- def get_transform(self):
- return self._transform
- def get_window_extent(self, renderer=None):
- if renderer is None:
- renderer = self.get_figure()._cachedRenderer
- if isinstance(self.bbox, BboxBase):
- return self.bbox
- elif callable(self.bbox):
- return self.bbox(renderer)
- else:
- raise ValueError("unknown type of bbox")
- def contains(self, mouseevent):
- """Test whether the mouse event occurred within the image."""
- inside, info = self._default_contains(mouseevent)
- if inside is not None:
- return inside, info
- if not self.get_visible(): # or self.get_figure()._renderer is None:
- return False, {}
- x, y = mouseevent.x, mouseevent.y
- inside = self.get_window_extent().contains(x, y)
- return inside, {}
- def make_image(self, renderer, magnification=1.0, unsampled=False):
- # docstring inherited
- width, height = renderer.get_canvas_width_height()
- bbox_in = self.get_window_extent(renderer).frozen()
- bbox_in._points /= [width, height]
- bbox_out = self.get_window_extent(renderer)
- clip = Bbox([[0, 0], [width, height]])
- self._transform = BboxTransform(Bbox([[0, 0], [1, 1]]), clip)
- return self._make_image(
- self._A,
- bbox_in, bbox_out, clip, magnification, unsampled=unsampled)
- def imread(fname, format=None):
- """
- Read an image from a file into an array.
- Parameters
- ----------
- fname : str or file-like
- The image file to read: a filename, a URL or a file-like object opened
- in read-binary mode.
- format : str, optional
- The image file format assumed for reading the data. If not
- given, the format is deduced from the filename. If nothing can
- be deduced, PNG is tried.
- Returns
- -------
- imagedata : :class:`numpy.array`
- The image data. The returned array has shape
- - (M, N) for grayscale images.
- - (M, N, 3) for RGB images.
- - (M, N, 4) for RGBA images.
- Notes
- -----
- Matplotlib can only read PNGs natively. Further image formats are
- supported via the optional dependency on Pillow. Note, URL strings
- are not compatible with Pillow. Check the `Pillow documentation`_
- for more information.
- .. _Pillow documentation: http://pillow.readthedocs.io/en/latest/
- """
- if format is None:
- if isinstance(fname, str):
- parsed = urllib.parse.urlparse(fname)
- # If the string is a URL (Windows paths appear as if they have a
- # length-1 scheme), assume png.
- if len(parsed.scheme) > 1:
- ext = 'png'
- else:
- basename, ext = os.path.splitext(fname)
- ext = ext.lower()[1:]
- elif hasattr(fname, 'geturl'): # Returned by urlopen().
- # We could try to parse the url's path and use the extension, but
- # returning png is consistent with the block above. Note that this
- # if clause has to come before checking for fname.name as
- # urlopen("file:///...") also has a name attribute (with the fixed
- # value "<urllib response>").
- ext = 'png'
- elif hasattr(fname, 'name'):
- basename, ext = os.path.splitext(fname.name)
- ext = ext.lower()[1:]
- else:
- ext = 'png'
- else:
- ext = format
- if ext != 'png':
- try: # Try to load the image with PIL.
- from PIL import Image
- except ImportError:
- raise ValueError('Only know how to handle PNG; with Pillow '
- 'installed, Matplotlib can handle more images')
- with Image.open(fname) as image:
- return pil_to_array(image)
- from matplotlib import _png
- if isinstance(fname, str):
- parsed = urllib.parse.urlparse(fname)
- # If fname is a URL, download the data
- if len(parsed.scheme) > 1:
- from urllib import request
- fd = BytesIO(request.urlopen(fname).read())
- return _png.read_png(fd)
- with cbook.open_file_cm(fname, "rb") as file:
- return _png.read_png(file)
- def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None,
- origin=None, dpi=100, *, metadata=None, pil_kwargs=None):
- """
- Save an array as an image file.
- Parameters
- ----------
- fname : str or PathLike or file-like
- A path or a file-like object to store the image in.
- If *format* is not set, then the output format is inferred from the
- extension of *fname*, if any, and from :rc:`savefig.format` otherwise.
- If *format* is set, it determines the output format.
- arr : array-like
- The image data. The shape can be one of
- MxN (luminance), MxNx3 (RGB) or MxNx4 (RGBA).
- vmin, vmax : scalar, optional
- *vmin* and *vmax* set the color scaling for the image by fixing the
- values that map to the colormap color limits. If either *vmin*
- or *vmax* is None, that limit is determined from the *arr*
- min/max value.
- cmap : str or `~matplotlib.colors.Colormap`, optional
- A Colormap instance or registered colormap name. The colormap
- maps scalar data to colors. It is ignored for RGB(A) data.
- Defaults to :rc:`image.cmap` ('viridis').
- format : str, optional
- The file format, e.g. 'png', 'pdf', 'svg', ... The behavior when this
- is unset is documented under *fname*.
- origin : {'upper', 'lower'}, optional
- Indicates whether the ``(0, 0)`` index of the array is in the upper
- left or lower left corner of the axes. Defaults to :rc:`image.origin`
- ('upper').
- dpi : int
- The DPI to store in the metadata of the file. This does not affect the
- resolution of the output image.
- metadata : dict, optional
- Metadata in the image file. The supported keys depend on the output
- format, see the documentation of the respective backends for more
- information.
- pil_kwargs : dict, optional
- If set to a non-None value, always use Pillow to save the figure
- (regardless of the output format), and pass these keyword arguments to
- `PIL.Image.save`.
- If the 'pnginfo' key is present, it completely overrides
- *metadata*, including the default 'Software' key.
- """
- from matplotlib.figure import Figure
- from matplotlib import _png
- if isinstance(fname, os.PathLike):
- fname = os.fspath(fname)
- if format is None:
- format = (Path(fname).suffix[1:] if isinstance(fname, str)
- else rcParams["savefig.format"]).lower()
- if format in ["pdf", "ps", "eps", "svg"]:
- # Vector formats that are not handled by PIL.
- if pil_kwargs is not None:
- raise ValueError(
- f"Cannot use 'pil_kwargs' when saving to {format}")
- fig = Figure(dpi=dpi, frameon=False)
- fig.figimage(arr, cmap=cmap, vmin=vmin, vmax=vmax, origin=origin,
- resize=True)
- fig.savefig(fname, dpi=dpi, format=format, transparent=True,
- metadata=metadata)
- else:
- # Don't bother creating an image; this avoids rounding errors on the
- # size when dividing and then multiplying by dpi.
- sm = cm.ScalarMappable(cmap=cmap)
- sm.set_clim(vmin, vmax)
- if origin is None:
- origin = rcParams["image.origin"]
- if origin == "lower":
- arr = arr[::-1]
- rgba = sm.to_rgba(arr, bytes=True)
- if format == "png" and pil_kwargs is None:
- with cbook.open_file_cm(fname, "wb") as file:
- _png.write_png(rgba, file, dpi=dpi, metadata=metadata)
- else:
- try:
- from PIL import Image
- from PIL.PngImagePlugin import PngInfo
- except ImportError as exc:
- if pil_kwargs is not None:
- raise ImportError("Setting 'pil_kwargs' requires Pillow")
- else:
- raise ImportError(f"Saving to {format} requires Pillow")
- if pil_kwargs is None:
- pil_kwargs = {}
- pil_shape = (rgba.shape[1], rgba.shape[0])
- image = Image.frombuffer(
- "RGBA", pil_shape, rgba, "raw", "RGBA", 0, 1)
- if format == "png" and metadata:
- # cf. backend_agg's print_png.
- if "pnginfo" in pil_kwargs:
- cbook._warn_external("'metadata' is overridden by the "
- "'pnginfo' entry in 'pil_kwargs'.")
- else:
- pnginfo = PngInfo()
- for k, v in metadata.items():
- pnginfo.add_text(k, v)
- pil_kwargs["pnginfo"] = pnginfo
- if format in ["jpg", "jpeg"]:
- format = "jpeg" # Pillow doesn't recognize "jpg".
- color = tuple(
- int(x * 255)
- for x in mcolors.to_rgb(rcParams["savefig.facecolor"]))
- background = Image.new("RGB", pil_shape, color)
- background.paste(image, image)
- image = background
- pil_kwargs.setdefault("format", format)
- pil_kwargs.setdefault("dpi", (dpi, dpi))
- image.save(fname, **pil_kwargs)
- def pil_to_array(pilImage):
- """Load a `PIL image`_ and return it as a numpy array.
- .. _PIL image: https://pillow.readthedocs.io/en/latest/reference/Image.html
- Returns
- -------
- numpy.array
- The array shape depends on the image type:
- - (M, N) for grayscale images.
- - (M, N, 3) for RGB images.
- - (M, N, 4) for RGBA images.
- """
- if pilImage.mode in ['RGBA', 'RGBX', 'RGB', 'L']:
- # return MxNx4 RGBA, MxNx3 RBA, or MxN luminance array
- return np.asarray(pilImage)
- elif pilImage.mode.startswith('I;16'):
- # return MxN luminance array of uint16
- raw = pilImage.tobytes('raw', pilImage.mode)
- if pilImage.mode.endswith('B'):
- x = np.frombuffer(raw, '>u2')
- else:
- x = np.frombuffer(raw, '<u2')
- return x.reshape(pilImage.size[::-1]).astype('=u2')
- else: # try to convert to an rgba image
- try:
- pilImage = pilImage.convert('RGBA')
- except ValueError:
- raise RuntimeError('Unknown image mode')
- return np.asarray(pilImage) # return MxNx4 RGBA array
- def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear',
- preview=False):
- """
- Make a thumbnail of image in *infile* with output filename *thumbfile*.
- See :doc:`/gallery/misc/image_thumbnail_sgskip`.
- Parameters
- ----------
- infile : str or file-like
- The image file -- must be PNG, or Pillow-readable if you have Pillow_
- installed.
- .. _Pillow: http://python-pillow.org/
- thumbfile : str or file-like
- The thumbnail filename.
- scale : float, optional
- The scale factor for the thumbnail.
- interpolation : str, optional
- The interpolation scheme used in the resampling. See the
- *interpolation* parameter of `~.Axes.imshow` for possible values.
- preview : bool, optional
- If True, the default backend (presumably a user interface
- backend) will be used which will cause a figure to be raised if
- `~matplotlib.pyplot.show` is called. If it is False, the figure is
- created using `FigureCanvasBase` and the drawing backend is selected
- as `~matplotlib.figure.savefig` would normally do.
- Returns
- -------
- figure : `~.figure.Figure`
- The figure instance containing the thumbnail.
- """
- im = imread(infile)
- rows, cols, depth = im.shape
- # This doesn't really matter (it cancels in the end) but the API needs it.
- dpi = 100
- height = rows / dpi * scale
- width = cols / dpi * scale
- if preview:
- # Let the UI backend do everything.
- import matplotlib.pyplot as plt
- fig = plt.figure(figsize=(width, height), dpi=dpi)
- else:
- from matplotlib.figure import Figure
- fig = Figure(figsize=(width, height), dpi=dpi)
- FigureCanvasBase(fig)
- ax = fig.add_axes([0, 0, 1, 1], aspect='auto',
- frameon=False, xticks=[], yticks=[])
- ax.imshow(im, aspect='auto', resample=True, interpolation=interpolation)
- fig.savefig(thumbfile, dpi=dpi)
- return fig
|