Coverage for /usr/lib/python3/dist-packages/matplotlib/image.py: 13%
752 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1"""
2The image module supports basic image loading, rescaling and display
3operations.
4"""
6import math
7import os
8import logging
9from pathlib import Path
10import warnings
12import numpy as np
13import PIL.PngImagePlugin
15import matplotlib as mpl
16from matplotlib import _api, cbook, cm
17# For clarity, names from _image are given explicitly in this module
18from matplotlib import _image
19# For user convenience, the names from _image are also imported into
20# the image namespace
21from matplotlib._image import *
22import matplotlib.artist as martist
23from matplotlib.backend_bases import FigureCanvasBase
24import matplotlib.colors as mcolors
25from matplotlib.transforms import (
26 Affine2D, BboxBase, Bbox, BboxTransform, BboxTransformTo,
27 IdentityTransform, TransformedBbox)
29_log = logging.getLogger(__name__)
31# map interpolation strings to module constants
32_interpd_ = {
33 'antialiased': _image.NEAREST, # this will use nearest or Hanning...
34 'none': _image.NEAREST, # fall back to nearest when not supported
35 'nearest': _image.NEAREST,
36 'bilinear': _image.BILINEAR,
37 'bicubic': _image.BICUBIC,
38 'spline16': _image.SPLINE16,
39 'spline36': _image.SPLINE36,
40 'hanning': _image.HANNING,
41 'hamming': _image.HAMMING,
42 'hermite': _image.HERMITE,
43 'kaiser': _image.KAISER,
44 'quadric': _image.QUADRIC,
45 'catrom': _image.CATROM,
46 'gaussian': _image.GAUSSIAN,
47 'bessel': _image.BESSEL,
48 'mitchell': _image.MITCHELL,
49 'sinc': _image.SINC,
50 'lanczos': _image.LANCZOS,
51 'blackman': _image.BLACKMAN,
52}
54interpolations_names = set(_interpd_)
57def composite_images(images, renderer, magnification=1.0):
58 """
59 Composite a number of RGBA images into one. The images are
60 composited in the order in which they appear in the *images* list.
62 Parameters
63 ----------
64 images : list of Images
65 Each must have a `make_image` method. For each image,
66 `can_composite` should return `True`, though this is not
67 enforced by this function. Each image must have a purely
68 affine transformation with no shear.
70 renderer : `.RendererBase`
72 magnification : float, default: 1
73 The additional magnification to apply for the renderer in use.
75 Returns
76 -------
77 image : uint8 array (M, N, 4)
78 The composited RGBA image.
79 offset_x, offset_y : float
80 The (left, bottom) offset where the composited image should be placed
81 in the output figure.
82 """
83 if len(images) == 0:
84 return np.empty((0, 0, 4), dtype=np.uint8), 0, 0
86 parts = []
87 bboxes = []
88 for image in images:
89 data, x, y, trans = image.make_image(renderer, magnification)
90 if data is not None:
91 x *= magnification
92 y *= magnification
93 parts.append((data, x, y, image._get_scalar_alpha()))
94 bboxes.append(
95 Bbox([[x, y], [x + data.shape[1], y + data.shape[0]]]))
97 if len(parts) == 0:
98 return np.empty((0, 0, 4), dtype=np.uint8), 0, 0
100 bbox = Bbox.union(bboxes)
102 output = np.zeros(
103 (int(bbox.height), int(bbox.width), 4), dtype=np.uint8)
105 for data, x, y, alpha in parts:
106 trans = Affine2D().translate(x - bbox.x0, y - bbox.y0)
107 _image.resample(data, output, trans, _image.NEAREST,
108 resample=False, alpha=alpha)
110 return output, bbox.x0 / magnification, bbox.y0 / magnification
113def _draw_list_compositing_images(
114 renderer, parent, artists, suppress_composite=None):
115 """
116 Draw a sorted list of artists, compositing images into a single
117 image where possible.
119 For internal Matplotlib use only: It is here to reduce duplication
120 between `Figure.draw` and `Axes.draw`, but otherwise should not be
121 generally useful.
122 """
123 has_images = any(isinstance(x, _ImageBase) for x in artists)
125 # override the renderer default if suppressComposite is not None
126 not_composite = (suppress_composite if suppress_composite is not None
127 else renderer.option_image_nocomposite())
129 if not_composite or not has_images:
130 for a in artists:
131 a.draw(renderer)
132 else:
133 # Composite any adjacent images together
134 image_group = []
135 mag = renderer.get_image_magnification()
137 def flush_images():
138 if len(image_group) == 1:
139 image_group[0].draw(renderer)
140 elif len(image_group) > 1:
141 data, l, b = composite_images(image_group, renderer, mag)
142 if data.size != 0:
143 gc = renderer.new_gc()
144 gc.set_clip_rectangle(parent.bbox)
145 gc.set_clip_path(parent.get_clip_path())
146 renderer.draw_image(gc, round(l), round(b), data)
147 gc.restore()
148 del image_group[:]
150 for a in artists:
151 if (isinstance(a, _ImageBase) and a.can_composite() and
152 a.get_clip_on() and not a.get_clip_path()):
153 image_group.append(a)
154 else:
155 flush_images()
156 a.draw(renderer)
157 flush_images()
160def _resample(
161 image_obj, data, out_shape, transform, *, resample=None, alpha=1):
162 """
163 Convenience wrapper around `._image.resample` to resample *data* to
164 *out_shape* (with a third dimension if *data* is RGBA) that takes care of
165 allocating the output array and fetching the relevant properties from the
166 Image object *image_obj*.
167 """
168 # AGG can only handle coordinates smaller than 24-bit signed integers,
169 # so raise errors if the input data is larger than _image.resample can
170 # handle.
171 msg = ('Data with more than {n} cannot be accurately displayed. '
172 'Downsampling to less than {n} before displaying. '
173 'To remove this warning, manually downsample your data.')
174 if data.shape[1] > 2**23:
175 warnings.warn(msg.format(n='2**23 columns'))
176 step = int(np.ceil(data.shape[1] / 2**23))
177 data = data[:, ::step]
178 transform = Affine2D().scale(step, 1) + transform
179 if data.shape[0] > 2**24:
180 warnings.warn(msg.format(n='2**24 rows'))
181 step = int(np.ceil(data.shape[0] / 2**24))
182 data = data[::step, :]
183 transform = Affine2D().scale(1, step) + transform
184 # decide if we need to apply anti-aliasing if the data is upsampled:
185 # compare the number of displayed pixels to the number of
186 # the data pixels.
187 interpolation = image_obj.get_interpolation()
188 if interpolation == 'antialiased':
189 # don't antialias if upsampling by an integer number or
190 # if zooming in more than a factor of 3
191 pos = np.array([[0, 0], [data.shape[1], data.shape[0]]])
192 disp = transform.transform(pos)
193 dispx = np.abs(np.diff(disp[:, 0]))
194 dispy = np.abs(np.diff(disp[:, 1]))
195 if ((dispx > 3 * data.shape[1] or
196 dispx == data.shape[1] or
197 dispx == 2 * data.shape[1]) and
198 (dispy > 3 * data.shape[0] or
199 dispy == data.shape[0] or
200 dispy == 2 * data.shape[0])):
201 interpolation = 'nearest'
202 else:
203 interpolation = 'hanning'
204 out = np.zeros(out_shape + data.shape[2:], data.dtype) # 2D->2D, 3D->3D.
205 if resample is None:
206 resample = image_obj.get_resample()
207 _image.resample(data, out, transform,
208 _interpd_[interpolation],
209 resample,
210 alpha,
211 image_obj.get_filternorm(),
212 image_obj.get_filterrad())
213 return out
216def _rgb_to_rgba(A):
217 """
218 Convert an RGB image to RGBA, as required by the image resample C++
219 extension.
220 """
221 rgba = np.zeros((A.shape[0], A.shape[1], 4), dtype=A.dtype)
222 rgba[:, :, :3] = A
223 if rgba.dtype == np.uint8:
224 rgba[:, :, 3] = 255
225 else:
226 rgba[:, :, 3] = 1.0
227 return rgba
230class _ImageBase(martist.Artist, cm.ScalarMappable):
231 """
232 Base class for images.
234 interpolation and cmap default to their rc settings
236 cmap is a colors.Colormap instance
237 norm is a colors.Normalize instance to map luminance to 0-1
239 extent is data axes (left, right, bottom, top) for making image plots
240 registered with data plots. Default is to label the pixel
241 centers with the zero-based row and column indices.
243 Additional kwargs are matplotlib.artist properties
244 """
245 zorder = 0
247 def __init__(self, ax,
248 cmap=None,
249 norm=None,
250 interpolation=None,
251 origin=None,
252 filternorm=True,
253 filterrad=4.0,
254 resample=False,
255 *,
256 interpolation_stage=None,
257 **kwargs
258 ):
259 martist.Artist.__init__(self)
260 cm.ScalarMappable.__init__(self, norm, cmap)
261 if origin is None:
262 origin = mpl.rcParams['image.origin']
263 _api.check_in_list(["upper", "lower"], origin=origin)
264 self.origin = origin
265 self.set_filternorm(filternorm)
266 self.set_filterrad(filterrad)
267 self.set_interpolation(interpolation)
268 self.set_interpolation_stage(interpolation_stage)
269 self.set_resample(resample)
270 self.axes = ax
272 self._imcache = None
274 self._internal_update(kwargs)
276 def __str__(self):
277 try:
278 size = self.get_size()
279 return f"{type(self).__name__}(size={size!r})"
280 except RuntimeError:
281 return type(self).__name__
283 def __getstate__(self):
284 # Save some space on the pickle by not saving the cache.
285 return {**super().__getstate__(), "_imcache": None}
287 def get_size(self):
288 """Return the size of the image as tuple (numrows, numcols)."""
289 if self._A is None:
290 raise RuntimeError('You must first set the image array')
292 return self._A.shape[:2]
294 def set_alpha(self, alpha):
295 """
296 Set the alpha value used for blending - not supported on all backends.
298 Parameters
299 ----------
300 alpha : float or 2D array-like or None
301 """
302 martist.Artist._set_alpha_for_array(self, alpha)
303 if np.ndim(alpha) not in (0, 2):
304 raise TypeError('alpha must be a float, two-dimensional '
305 'array, or None')
306 self._imcache = None
308 def _get_scalar_alpha(self):
309 """
310 Get a scalar alpha value to be applied to the artist as a whole.
312 If the alpha value is a matrix, the method returns 1.0 because pixels
313 have individual alpha values (see `~._ImageBase._make_image` for
314 details). If the alpha value is a scalar, the method returns said value
315 to be applied to the artist as a whole because pixels do not have
316 individual alpha values.
317 """
318 return 1.0 if self._alpha is None or np.ndim(self._alpha) > 0 \
319 else self._alpha
321 def changed(self):
322 """
323 Call this whenever the mappable is changed so observers can update.
324 """
325 self._imcache = None
326 cm.ScalarMappable.changed(self)
328 def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
329 unsampled=False, round_to_pixel_border=True):
330 """
331 Normalize, rescale, and colormap the image *A* from the given *in_bbox*
332 (in data space), to the given *out_bbox* (in pixel space) clipped to
333 the given *clip_bbox* (also in pixel space), and magnified by the
334 *magnification* factor.
336 *A* may be a greyscale image (M, N) with a dtype of float32, float64,
337 float128, uint16 or uint8, or an (M, N, 4) RGBA image with a dtype of
338 float32, float64, float128, or uint8.
340 If *unsampled* is True, the image will not be scaled, but an
341 appropriate affine transformation will be returned instead.
343 If *round_to_pixel_border* is True, the output image size will be
344 rounded to the nearest pixel boundary. This makes the images align
345 correctly with the axes. It should not be used if exact scaling is
346 needed, such as for `FigureImage`.
348 Returns
349 -------
350 image : (M, N, 4) uint8 array
351 The RGBA image, resampled unless *unsampled* is True.
352 x, y : float
353 The upper left corner where the image should be drawn, in pixel
354 space.
355 trans : Affine2D
356 The affine transformation from image to pixel space.
357 """
358 if A is None:
359 raise RuntimeError('You must first set the image '
360 'array or the image attribute')
361 if A.size == 0:
362 raise RuntimeError("_make_image must get a non-empty image. "
363 "Your Artist's draw method must filter before "
364 "this method is called.")
366 clipped_bbox = Bbox.intersection(out_bbox, clip_bbox)
368 if clipped_bbox is None:
369 return None, 0, 0, None
371 out_width_base = clipped_bbox.width * magnification
372 out_height_base = clipped_bbox.height * magnification
374 if out_width_base == 0 or out_height_base == 0:
375 return None, 0, 0, None
377 if self.origin == 'upper':
378 # Flip the input image using a transform. This avoids the
379 # problem with flipping the array, which results in a copy
380 # when it is converted to contiguous in the C wrapper
381 t0 = Affine2D().translate(0, -A.shape[0]).scale(1, -1)
382 else:
383 t0 = IdentityTransform()
385 t0 += (
386 Affine2D()
387 .scale(
388 in_bbox.width / A.shape[1],
389 in_bbox.height / A.shape[0])
390 .translate(in_bbox.x0, in_bbox.y0)
391 + self.get_transform())
393 t = (t0
394 + (Affine2D()
395 .translate(-clipped_bbox.x0, -clipped_bbox.y0)
396 .scale(magnification)))
398 # So that the image is aligned with the edge of the axes, we want to
399 # round up the output width to the next integer. This also means
400 # scaling the transform slightly to account for the extra subpixel.
401 if (t.is_affine and round_to_pixel_border and
402 (out_width_base % 1.0 != 0.0 or out_height_base % 1.0 != 0.0)):
403 out_width = math.ceil(out_width_base)
404 out_height = math.ceil(out_height_base)
405 extra_width = (out_width - out_width_base) / out_width_base
406 extra_height = (out_height - out_height_base) / out_height_base
407 t += Affine2D().scale(1.0 + extra_width, 1.0 + extra_height)
408 else:
409 out_width = int(out_width_base)
410 out_height = int(out_height_base)
411 out_shape = (out_height, out_width)
413 if not unsampled:
414 if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)):
415 raise ValueError(f"Invalid shape {A.shape} for image data")
416 if A.ndim == 2 and self._interpolation_stage != 'rgba':
417 # if we are a 2D array, then we are running through the
418 # norm + colormap transformation. However, in general the
419 # input data is not going to match the size on the screen so we
420 # have to resample to the correct number of pixels
422 # TODO slice input array first
423 a_min = A.min()
424 a_max = A.max()
425 if a_min is np.ma.masked: # All masked; values don't matter.
426 a_min, a_max = np.int32(0), np.int32(1)
427 if A.dtype.kind == 'f': # Float dtype: scale to same dtype.
428 scaled_dtype = np.dtype(
429 np.float64 if A.dtype.itemsize > 4 else np.float32)
430 if scaled_dtype.itemsize < A.dtype.itemsize:
431 _api.warn_external(f"Casting input data from {A.dtype}"
432 f" to {scaled_dtype} for imshow.")
433 else: # Int dtype, likely.
434 # Scale to appropriately sized float: use float32 if the
435 # dynamic range is small, to limit the memory footprint.
436 da = a_max.astype(np.float64) - a_min.astype(np.float64)
437 scaled_dtype = np.float64 if da > 1e8 else np.float32
439 # Scale the input data to [.1, .9]. The Agg interpolators clip
440 # to [0, 1] internally, and we use a smaller input scale to
441 # identify the interpolated points that need to be flagged as
442 # over/under. This may introduce numeric instabilities in very
443 # broadly scaled data.
445 # Always copy, and don't allow array subtypes.
446 A_scaled = np.array(A, dtype=scaled_dtype)
447 # Clip scaled data around norm if necessary. This is necessary
448 # for big numbers at the edge of float64's ability to represent
449 # changes. Applying a norm first would be good, but ruins the
450 # interpolation of over numbers.
451 self.norm.autoscale_None(A)
452 dv = np.float64(self.norm.vmax) - np.float64(self.norm.vmin)
453 vmid = np.float64(self.norm.vmin) + dv / 2
454 fact = 1e7 if scaled_dtype == np.float64 else 1e4
455 newmin = vmid - dv * fact
456 if newmin < a_min:
457 newmin = None
458 else:
459 a_min = np.float64(newmin)
460 newmax = vmid + dv * fact
461 if newmax > a_max:
462 newmax = None
463 else:
464 a_max = np.float64(newmax)
465 if newmax is not None or newmin is not None:
466 np.clip(A_scaled, newmin, newmax, out=A_scaled)
468 # Rescale the raw data to [offset, 1-offset] so that the
469 # resampling code will run cleanly. Using dyadic numbers here
470 # could reduce the error, but would not fully eliminate it and
471 # breaks a number of tests (due to the slightly different
472 # error bouncing some pixels across a boundary in the (very
473 # quantized) colormapping step).
474 offset = .1
475 frac = .8
476 # Run vmin/vmax through the same rescaling as the raw data;
477 # otherwise, data values close or equal to the boundaries can
478 # end up on the wrong side due to floating point error.
479 vmin, vmax = self.norm.vmin, self.norm.vmax
480 if vmin is np.ma.masked:
481 vmin, vmax = a_min, a_max
482 vrange = np.array([vmin, vmax], dtype=scaled_dtype)
484 A_scaled -= a_min
485 vrange -= a_min
486 # .item() handles a_min/a_max being ndarray subclasses.
487 a_min = a_min.astype(scaled_dtype).item()
488 a_max = a_max.astype(scaled_dtype).item()
490 if a_min != a_max:
491 A_scaled /= ((a_max - a_min) / frac)
492 vrange /= ((a_max - a_min) / frac)
493 A_scaled += offset
494 vrange += offset
495 # resample the input data to the correct resolution and shape
496 A_resampled = _resample(self, A_scaled, out_shape, t)
497 del A_scaled # Make sure we don't use A_scaled anymore!
498 # Un-scale the resampled data to approximately the original
499 # range. Things that interpolated to outside the original range
500 # will still be outside, but possibly clipped in the case of
501 # higher order interpolation + drastically changing data.
502 A_resampled -= offset
503 vrange -= offset
504 if a_min != a_max:
505 A_resampled *= ((a_max - a_min) / frac)
506 vrange *= ((a_max - a_min) / frac)
507 A_resampled += a_min
508 vrange += a_min
509 # if using NoNorm, cast back to the original datatype
510 if isinstance(self.norm, mcolors.NoNorm):
511 A_resampled = A_resampled.astype(A.dtype)
513 mask = (np.where(A.mask, np.float32(np.nan), np.float32(1))
514 if A.mask.shape == A.shape # nontrivial mask
515 else np.ones_like(A, np.float32))
516 # we always have to interpolate the mask to account for
517 # non-affine transformations
518 out_alpha = _resample(self, mask, out_shape, t, resample=True)
519 del mask # Make sure we don't use mask anymore!
520 # Agg updates out_alpha in place. If the pixel has no image
521 # data it will not be updated (and still be 0 as we initialized
522 # it), if input data that would go into that output pixel than
523 # it will be `nan`, if all the input data for a pixel is good
524 # it will be 1, and if there is _some_ good data in that output
525 # pixel it will be between [0, 1] (such as a rotated image).
526 out_mask = np.isnan(out_alpha)
527 out_alpha[out_mask] = 1
528 # Apply the pixel-by-pixel alpha values if present
529 alpha = self.get_alpha()
530 if alpha is not None and np.ndim(alpha) > 0:
531 out_alpha *= _resample(self, alpha, out_shape,
532 t, resample=True)
533 # mask and run through the norm
534 resampled_masked = np.ma.masked_array(A_resampled, out_mask)
535 # we have re-set the vmin/vmax to account for small errors
536 # that may have moved input values in/out of range
537 s_vmin, s_vmax = vrange
538 if isinstance(self.norm, mcolors.LogNorm) and s_vmin <= 0:
539 # Don't give 0 or negative values to LogNorm
540 s_vmin = np.finfo(scaled_dtype).eps
541 # Block the norm from sending an update signal during the
542 # temporary vmin/vmax change
543 with self.norm.callbacks.blocked(), \
544 cbook._setattr_cm(self.norm, vmin=s_vmin, vmax=s_vmax):
545 output = self.norm(resampled_masked)
546 else:
547 if A.ndim == 2: # _interpolation_stage == 'rgba'
548 self.norm.autoscale_None(A)
549 A = self.to_rgba(A)
550 if A.shape[2] == 3:
551 A = _rgb_to_rgba(A)
552 alpha = self._get_scalar_alpha()
553 output_alpha = _resample( # resample alpha channel
554 self, A[..., 3], out_shape, t, alpha=alpha)
555 output = _resample( # resample rgb channels
556 self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha)
557 output[..., 3] = output_alpha # recombine rgb and alpha
559 # output is now either a 2D array of normed (int or float) data
560 # or an RGBA array of re-sampled input
561 output = self.to_rgba(output, bytes=True, norm=False)
562 # output is now a correctly sized RGBA array of uint8
564 # Apply alpha *after* if the input was greyscale without a mask
565 if A.ndim == 2:
566 alpha = self._get_scalar_alpha()
567 alpha_channel = output[:, :, 3]
568 alpha_channel[:] = ( # Assignment will cast to uint8.
569 alpha_channel.astype(np.float32) * out_alpha * alpha)
571 else:
572 if self._imcache is None:
573 self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2))
574 output = self._imcache
576 # Subset the input image to only the part that will be displayed.
577 subset = TransformedBbox(clip_bbox, t0.inverted()).frozen()
578 output = output[
579 int(max(subset.ymin, 0)):
580 int(min(subset.ymax + 1, output.shape[0])),
581 int(max(subset.xmin, 0)):
582 int(min(subset.xmax + 1, output.shape[1]))]
584 t = Affine2D().translate(
585 int(max(subset.xmin, 0)), int(max(subset.ymin, 0))) + t
587 return output, clipped_bbox.x0, clipped_bbox.y0, t
589 def make_image(self, renderer, magnification=1.0, unsampled=False):
590 """
591 Normalize, rescale, and colormap this image's data for rendering using
592 *renderer*, with the given *magnification*.
594 If *unsampled* is True, the image will not be scaled, but an
595 appropriate affine transformation will be returned instead.
597 Returns
598 -------
599 image : (M, N, 4) uint8 array
600 The RGBA image, resampled unless *unsampled* is True.
601 x, y : float
602 The upper left corner where the image should be drawn, in pixel
603 space.
604 trans : Affine2D
605 The affine transformation from image to pixel space.
606 """
607 raise NotImplementedError('The make_image method must be overridden')
609 def _check_unsampled_image(self):
610 """
611 Return whether the image is better to be drawn unsampled.
613 The derived class needs to override it.
614 """
615 return False
617 @martist.allow_rasterization
618 def draw(self, renderer, *args, **kwargs):
619 # if not visible, declare victory and return
620 if not self.get_visible():
621 self.stale = False
622 return
623 # for empty images, there is nothing to draw!
624 if self.get_array().size == 0:
625 self.stale = False
626 return
627 # actually render the image.
628 gc = renderer.new_gc()
629 self._set_gc_clip(gc)
630 gc.set_alpha(self._get_scalar_alpha())
631 gc.set_url(self.get_url())
632 gc.set_gid(self.get_gid())
633 if (renderer.option_scale_image() # Renderer supports transform kwarg.
634 and self._check_unsampled_image()
635 and self.get_transform().is_affine):
636 im, l, b, trans = self.make_image(renderer, unsampled=True)
637 if im is not None:
638 trans = Affine2D().scale(im.shape[1], im.shape[0]) + trans
639 renderer.draw_image(gc, l, b, im, trans)
640 else:
641 im, l, b, trans = self.make_image(
642 renderer, renderer.get_image_magnification())
643 if im is not None:
644 renderer.draw_image(gc, l, b, im)
645 gc.restore()
646 self.stale = False
648 def contains(self, mouseevent):
649 """Test whether the mouse event occurred within the image."""
650 inside, info = self._default_contains(mouseevent)
651 if inside is not None:
652 return inside, info
653 # 1) This doesn't work for figimage; but figimage also needs a fix
654 # below (as the check cannot use x/ydata and extents).
655 # 2) As long as the check below uses x/ydata, we need to test axes
656 # identity instead of `self.axes.contains(event)` because even if
657 # axes overlap, x/ydata is only valid for event.inaxes anyways.
658 if self.axes is not mouseevent.inaxes:
659 return False, {}
660 # TODO: make sure this is consistent with patch and patch
661 # collection on nonlinear transformed coordinates.
662 # TODO: consider returning image coordinates (shouldn't
663 # be too difficult given that the image is rectilinear
664 trans = self.get_transform().inverted()
665 x, y = trans.transform([mouseevent.x, mouseevent.y])
666 xmin, xmax, ymin, ymax = self.get_extent()
667 if xmin > xmax:
668 xmin, xmax = xmax, xmin
669 if ymin > ymax:
670 ymin, ymax = ymax, ymin
672 if x is not None and y is not None:
673 inside = (xmin <= x <= xmax) and (ymin <= y <= ymax)
674 else:
675 inside = False
677 return inside, {}
679 def write_png(self, fname):
680 """Write the image to png file *fname*."""
681 im = self.to_rgba(self._A[::-1] if self.origin == 'lower' else self._A,
682 bytes=True, norm=True)
683 PIL.Image.fromarray(im).save(fname, format="png")
685 def set_data(self, A):
686 """
687 Set the image array.
689 Note that this function does *not* update the normalization used.
691 Parameters
692 ----------
693 A : array-like or `PIL.Image.Image`
694 """
695 if isinstance(A, PIL.Image.Image):
696 A = pil_to_array(A) # Needed e.g. to apply png palette.
697 self._A = cbook.safe_masked_invalid(A, copy=True)
699 if (self._A.dtype != np.uint8 and
700 not np.can_cast(self._A.dtype, float, "same_kind")):
701 raise TypeError("Image data of dtype {} cannot be converted to "
702 "float".format(self._A.dtype))
704 if self._A.ndim == 3 and self._A.shape[-1] == 1:
705 # If just one dimension assume scalar and apply colormap
706 self._A = self._A[:, :, 0]
708 if not (self._A.ndim == 2
709 or self._A.ndim == 3 and self._A.shape[-1] in [3, 4]):
710 raise TypeError("Invalid shape {} for image data"
711 .format(self._A.shape))
713 if self._A.ndim == 3:
714 # If the input data has values outside the valid range (after
715 # normalisation), we issue a warning and then clip X to the bounds
716 # - otherwise casting wraps extreme values, hiding outliers and
717 # making reliable interpretation impossible.
718 high = 255 if np.issubdtype(self._A.dtype, np.integer) else 1
719 if self._A.min() < 0 or high < self._A.max():
720 _log.warning(
721 'Clipping input data to the valid range for imshow with '
722 'RGB data ([0..1] for floats or [0..255] for integers).'
723 )
724 self._A = np.clip(self._A, 0, high)
725 # Cast unsupported integer types to uint8
726 if self._A.dtype != np.uint8 and np.issubdtype(self._A.dtype,
727 np.integer):
728 self._A = self._A.astype(np.uint8)
730 self._imcache = None
731 self.stale = True
733 def set_array(self, A):
734 """
735 Retained for backwards compatibility - use set_data instead.
737 Parameters
738 ----------
739 A : array-like
740 """
741 # This also needs to be here to override the inherited
742 # cm.ScalarMappable.set_array method so it is not invoked by mistake.
743 self.set_data(A)
745 def get_interpolation(self):
746 """
747 Return the interpolation method the image uses when resizing.
749 One of 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16',
750 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric',
751 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos',
752 or 'none'.
753 """
754 return self._interpolation
756 def set_interpolation(self, s):
757 """
758 Set the interpolation method the image uses when resizing.
760 If None, use :rc:`image.interpolation`. If 'none', the image is
761 shown as is without interpolating. 'none' is only supported in
762 agg, ps and pdf backends and will fall back to 'nearest' mode
763 for other backends.
765 Parameters
766 ----------
767 s : {'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', \
768'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', \
769'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'none'} or None
770 """
771 if s is None:
772 s = mpl.rcParams['image.interpolation']
773 s = s.lower()
774 _api.check_in_list(_interpd_, interpolation=s)
775 self._interpolation = s
776 self.stale = True
778 def set_interpolation_stage(self, s):
779 """
780 Set when interpolation happens during the transform to RGBA.
782 Parameters
783 ----------
784 s : {'data', 'rgba'} or None
785 Whether to apply up/downsampling interpolation in data or rgba
786 space.
787 """
788 if s is None:
789 s = "data" # placeholder for maybe having rcParam
790 _api.check_in_list(['data', 'rgba'], s=s)
791 self._interpolation_stage = s
792 self.stale = True
794 def can_composite(self):
795 """Return whether the image can be composited with its neighbors."""
796 trans = self.get_transform()
797 return (
798 self._interpolation != 'none' and
799 trans.is_affine and
800 trans.is_separable)
802 def set_resample(self, v):
803 """
804 Set whether image resampling is used.
806 Parameters
807 ----------
808 v : bool or None
809 If None, use :rc:`image.resample`.
810 """
811 if v is None:
812 v = mpl.rcParams['image.resample']
813 self._resample = v
814 self.stale = True
816 def get_resample(self):
817 """Return whether image resampling is used."""
818 return self._resample
820 def set_filternorm(self, filternorm):
821 """
822 Set whether the resize filter normalizes the weights.
824 See help for `~.Axes.imshow`.
826 Parameters
827 ----------
828 filternorm : bool
829 """
830 self._filternorm = bool(filternorm)
831 self.stale = True
833 def get_filternorm(self):
834 """Return whether the resize filter normalizes the weights."""
835 return self._filternorm
837 def set_filterrad(self, filterrad):
838 """
839 Set the resize filter radius only applicable to some
840 interpolation schemes -- see help for imshow
842 Parameters
843 ----------
844 filterrad : positive float
845 """
846 r = float(filterrad)
847 if r <= 0:
848 raise ValueError("The filter radius must be a positive number")
849 self._filterrad = r
850 self.stale = True
852 def get_filterrad(self):
853 """Return the filterrad setting."""
854 return self._filterrad
857class AxesImage(_ImageBase):
858 """
859 An image attached to an Axes.
861 Parameters
862 ----------
863 ax : `~.axes.Axes`
864 The axes the image will belong to.
865 cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap`
866 The Colormap instance or registered colormap name used to map scalar
867 data to colors.
868 norm : str or `~matplotlib.colors.Normalize`
869 Maps luminance to 0-1.
870 interpolation : str, default: :rc:`image.interpolation`
871 Supported values are 'none', 'antialiased', 'nearest', 'bilinear',
872 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite',
873 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell',
874 'sinc', 'lanczos', 'blackman'.
875 interpolation_stage : {'data', 'rgba'}, default: 'data'
876 If 'data', interpolation
877 is carried out on the data provided by the user. If 'rgba', the
878 interpolation is carried out after the colormapping has been
879 applied (visual interpolation).
880 origin : {'upper', 'lower'}, default: :rc:`image.origin`
881 Place the [0, 0] index of the array in the upper left or lower left
882 corner of the axes. The convention 'upper' is typically used for
883 matrices and images.
884 extent : tuple, optional
885 The data axes (left, right, bottom, top) for making image plots
886 registered with data plots. Default is to label the pixel
887 centers with the zero-based row and column indices.
888 filternorm : bool, default: True
889 A parameter for the antigrain image resize filter
890 (see the antigrain documentation).
891 If filternorm is set, the filter normalizes integer values and corrects
892 the rounding errors. It doesn't do anything with the source floating
893 point values, it corrects only integers according to the rule of 1.0
894 which means that any sum of pixel weights must be equal to 1.0. So,
895 the filter function must produce a graph of the proper shape.
896 filterrad : float > 0, default: 4
897 The filter radius for filters that have a radius parameter, i.e. when
898 interpolation is one of: 'sinc', 'lanczos' or 'blackman'.
899 resample : bool, default: False
900 When True, use a full resampling method. When False, only resample when
901 the output image is larger than the input image.
902 **kwargs : `.Artist` properties
903 """
905 @_api.make_keyword_only("3.6", name="cmap")
906 def __init__(self, ax,
907 cmap=None,
908 norm=None,
909 interpolation=None,
910 origin=None,
911 extent=None,
912 filternorm=True,
913 filterrad=4.0,
914 resample=False,
915 *,
916 interpolation_stage=None,
917 **kwargs
918 ):
920 self._extent = extent
922 super().__init__(
923 ax,
924 cmap=cmap,
925 norm=norm,
926 interpolation=interpolation,
927 origin=origin,
928 filternorm=filternorm,
929 filterrad=filterrad,
930 resample=resample,
931 interpolation_stage=interpolation_stage,
932 **kwargs
933 )
935 def get_window_extent(self, renderer=None):
936 x0, x1, y0, y1 = self._extent
937 bbox = Bbox.from_extents([x0, y0, x1, y1])
938 return bbox.transformed(self.axes.transData)
940 def make_image(self, renderer, magnification=1.0, unsampled=False):
941 # docstring inherited
942 trans = self.get_transform()
943 # image is created in the canvas coordinate.
944 x1, x2, y1, y2 = self.get_extent()
945 bbox = Bbox(np.array([[x1, y1], [x2, y2]]))
946 transformed_bbox = TransformedBbox(bbox, trans)
947 clip = ((self.get_clip_box() or self.axes.bbox) if self.get_clip_on()
948 else self.figure.bbox)
949 return self._make_image(self._A, bbox, transformed_bbox, clip,
950 magnification, unsampled=unsampled)
952 def _check_unsampled_image(self):
953 """Return whether the image would be better drawn unsampled."""
954 return self.get_interpolation() == "none"
956 def set_extent(self, extent):
957 """
958 Set the image extent.
960 Parameters
961 ----------
962 extent : 4-tuple of float
963 The position and size of the image as tuple
964 ``(left, right, bottom, top)`` in data coordinates.
966 Notes
967 -----
968 This updates ``ax.dataLim``, and, if autoscaling, sets ``ax.viewLim``
969 to tightly fit the image, regardless of ``dataLim``. Autoscaling
970 state is not changed, so following this with ``ax.autoscale_view()``
971 will redo the autoscaling in accord with ``dataLim``.
972 """
973 self._extent = xmin, xmax, ymin, ymax = extent
974 corners = (xmin, ymin), (xmax, ymax)
975 self.axes.update_datalim(corners)
976 self.sticky_edges.x[:] = [xmin, xmax]
977 self.sticky_edges.y[:] = [ymin, ymax]
978 if self.axes.get_autoscalex_on():
979 self.axes.set_xlim((xmin, xmax), auto=None)
980 if self.axes.get_autoscaley_on():
981 self.axes.set_ylim((ymin, ymax), auto=None)
982 self.stale = True
984 def get_extent(self):
985 """Return the image extent as tuple (left, right, bottom, top)."""
986 if self._extent is not None:
987 return self._extent
988 else:
989 sz = self.get_size()
990 numrows, numcols = sz
991 if self.origin == 'upper':
992 return (-0.5, numcols-0.5, numrows-0.5, -0.5)
993 else:
994 return (-0.5, numcols-0.5, -0.5, numrows-0.5)
996 def get_cursor_data(self, event):
997 """
998 Return the image value at the event position or *None* if the event is
999 outside the image.
1001 See Also
1002 --------
1003 matplotlib.artist.Artist.get_cursor_data
1004 """
1005 xmin, xmax, ymin, ymax = self.get_extent()
1006 if self.origin == 'upper':
1007 ymin, ymax = ymax, ymin
1008 arr = self.get_array()
1009 data_extent = Bbox([[xmin, ymin], [xmax, ymax]])
1010 array_extent = Bbox([[0, 0], [arr.shape[1], arr.shape[0]]])
1011 trans = self.get_transform().inverted()
1012 trans += BboxTransform(boxin=data_extent, boxout=array_extent)
1013 point = trans.transform([event.x, event.y])
1014 if any(np.isnan(point)):
1015 return None
1016 j, i = point.astype(int)
1017 # Clip the coordinates at array bounds
1018 if not (0 <= i < arr.shape[0]) or not (0 <= j < arr.shape[1]):
1019 return None
1020 else:
1021 return arr[i, j]
1024class NonUniformImage(AxesImage):
1025 mouseover = False # This class still needs its own get_cursor_data impl.
1027 def __init__(self, ax, *, interpolation='nearest', **kwargs):
1028 """
1029 Parameters
1030 ----------
1031 interpolation : {'nearest', 'bilinear'}, default: 'nearest'
1033 **kwargs
1034 All other keyword arguments are identical to those of `.AxesImage`.
1035 """
1036 super().__init__(ax, **kwargs)
1037 self.set_interpolation(interpolation)
1039 def _check_unsampled_image(self):
1040 """Return False. Do not use unsampled image."""
1041 return False
1043 def make_image(self, renderer, magnification=1.0, unsampled=False):
1044 # docstring inherited
1045 if self._A is None:
1046 raise RuntimeError('You must first set the image array')
1047 if unsampled:
1048 raise ValueError('unsampled not supported on NonUniformImage')
1049 A = self._A
1050 if A.ndim == 2:
1051 if A.dtype != np.uint8:
1052 A = self.to_rgba(A, bytes=True)
1053 else:
1054 A = np.repeat(A[:, :, np.newaxis], 4, 2)
1055 A[:, :, 3] = 255
1056 else:
1057 if A.dtype != np.uint8:
1058 A = (255*A).astype(np.uint8)
1059 if A.shape[2] == 3:
1060 B = np.zeros(tuple([*A.shape[0:2], 4]), np.uint8)
1061 B[:, :, 0:3] = A
1062 B[:, :, 3] = 255
1063 A = B
1064 vl = self.axes.viewLim
1065 l, b, r, t = self.axes.bbox.extents
1066 width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification)
1067 height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification)
1068 x_pix = np.linspace(vl.x0, vl.x1, width)
1069 y_pix = np.linspace(vl.y0, vl.y1, height)
1070 if self._interpolation == "nearest":
1071 x_mid = (self._Ax[:-1] + self._Ax[1:]) / 2
1072 y_mid = (self._Ay[:-1] + self._Ay[1:]) / 2
1073 x_int = x_mid.searchsorted(x_pix)
1074 y_int = y_mid.searchsorted(y_pix)
1075 # The following is equal to `A[y_int[:, None], x_int[None, :]]`,
1076 # but many times faster. Both casting to uint32 (to have an
1077 # effectively 1D array) and manual index flattening matter.
1078 im = (
1079 np.ascontiguousarray(A).view(np.uint32).ravel()[
1080 np.add.outer(y_int * A.shape[1], x_int)]
1081 .view(np.uint8).reshape((height, width, 4)))
1082 else: # self._interpolation == "bilinear"
1083 # Use np.interp to compute x_int/x_float has similar speed.
1084 x_int = np.clip(
1085 self._Ax.searchsorted(x_pix) - 1, 0, len(self._Ax) - 2)
1086 y_int = np.clip(
1087 self._Ay.searchsorted(y_pix) - 1, 0, len(self._Ay) - 2)
1088 idx_int = np.add.outer(y_int * A.shape[1], x_int)
1089 x_frac = np.clip(
1090 np.divide(x_pix - self._Ax[x_int], np.diff(self._Ax)[x_int],
1091 dtype=np.float32), # Downcasting helps with speed.
1092 0, 1)
1093 y_frac = np.clip(
1094 np.divide(y_pix - self._Ay[y_int], np.diff(self._Ay)[y_int],
1095 dtype=np.float32),
1096 0, 1)
1097 f00 = np.outer(1 - y_frac, 1 - x_frac)
1098 f10 = np.outer(y_frac, 1 - x_frac)
1099 f01 = np.outer(1 - y_frac, x_frac)
1100 f11 = np.outer(y_frac, x_frac)
1101 im = np.empty((height, width, 4), np.uint8)
1102 for chan in range(4):
1103 ac = A[:, :, chan].reshape(-1) # reshape(-1) avoids a copy.
1104 # Shifting the buffer start (`ac[offset:]`) avoids an array
1105 # addition (`ac[idx_int + offset]`).
1106 buf = f00 * ac[idx_int]
1107 buf += f10 * ac[A.shape[1]:][idx_int]
1108 buf += f01 * ac[1:][idx_int]
1109 buf += f11 * ac[A.shape[1] + 1:][idx_int]
1110 im[:, :, chan] = buf # Implicitly casts to uint8.
1111 return im, l, b, IdentityTransform()
1113 def set_data(self, x, y, A):
1114 """
1115 Set the grid for the pixel centers, and the pixel values.
1117 Parameters
1118 ----------
1119 x, y : 1D array-like
1120 Monotonic arrays of shapes (N,) and (M,), respectively, specifying
1121 pixel centers.
1122 A : array-like
1123 (M, N) ndarray or masked array of values to be colormapped, or
1124 (M, N, 3) RGB array, or (M, N, 4) RGBA array.
1125 """
1126 x = np.array(x, np.float32)
1127 y = np.array(y, np.float32)
1128 A = cbook.safe_masked_invalid(A, copy=True)
1129 if not (x.ndim == y.ndim == 1 and A.shape[0:2] == y.shape + x.shape):
1130 raise TypeError("Axes don't match array shape")
1131 if A.ndim not in [2, 3]:
1132 raise TypeError("Can only plot 2D or 3D data")
1133 if A.ndim == 3 and A.shape[2] not in [1, 3, 4]:
1134 raise TypeError("3D arrays must have three (RGB) "
1135 "or four (RGBA) color components")
1136 if A.ndim == 3 and A.shape[2] == 1:
1137 A = A.squeeze(axis=-1)
1138 self._A = A
1139 self._Ax = x
1140 self._Ay = y
1141 self._imcache = None
1143 self.stale = True
1145 def set_array(self, *args):
1146 raise NotImplementedError('Method not supported')
1148 def set_interpolation(self, s):
1149 """
1150 Parameters
1151 ----------
1152 s : {'nearest', 'bilinear'} or None
1153 If None, use :rc:`image.interpolation`.
1154 """
1155 if s is not None and s not in ('nearest', 'bilinear'):
1156 raise NotImplementedError('Only nearest neighbor and '
1157 'bilinear interpolations are supported')
1158 super().set_interpolation(s)
1160 def get_extent(self):
1161 if self._A is None:
1162 raise RuntimeError('Must set data first')
1163 return self._Ax[0], self._Ax[-1], self._Ay[0], self._Ay[-1]
1165 def set_filternorm(self, s):
1166 pass
1168 def set_filterrad(self, s):
1169 pass
1171 def set_norm(self, norm):
1172 if self._A is not None:
1173 raise RuntimeError('Cannot change colors after loading data')
1174 super().set_norm(norm)
1176 def set_cmap(self, cmap):
1177 if self._A is not None:
1178 raise RuntimeError('Cannot change colors after loading data')
1179 super().set_cmap(cmap)
1182class PcolorImage(AxesImage):
1183 """
1184 Make a pcolor-style plot with an irregular rectangular grid.
1186 This uses a variation of the original irregular image code,
1187 and it is used by pcolorfast for the corresponding grid type.
1188 """
1190 @_api.make_keyword_only("3.6", name="cmap")
1191 def __init__(self, ax,
1192 x=None,
1193 y=None,
1194 A=None,
1195 cmap=None,
1196 norm=None,
1197 **kwargs
1198 ):
1199 """
1200 Parameters
1201 ----------
1202 ax : `~.axes.Axes`
1203 The axes the image will belong to.
1204 x, y : 1D array-like, optional
1205 Monotonic arrays of length N+1 and M+1, respectively, specifying
1206 rectangle boundaries. If not given, will default to
1207 ``range(N + 1)`` and ``range(M + 1)``, respectively.
1208 A : array-like
1209 The data to be color-coded. The interpretation depends on the
1210 shape:
1212 - (M, N) ndarray or masked array: values to be colormapped
1213 - (M, N, 3): RGB array
1214 - (M, N, 4): RGBA array
1216 cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap`
1217 The Colormap instance or registered colormap name used to map
1218 scalar data to colors.
1219 norm : str or `~matplotlib.colors.Normalize`
1220 Maps luminance to 0-1.
1221 **kwargs : `.Artist` properties
1222 """
1223 super().__init__(ax, norm=norm, cmap=cmap)
1224 self._internal_update(kwargs)
1225 if A is not None:
1226 self.set_data(x, y, A)
1228 def make_image(self, renderer, magnification=1.0, unsampled=False):
1229 # docstring inherited
1230 if self._A is None:
1231 raise RuntimeError('You must first set the image array')
1232 if unsampled:
1233 raise ValueError('unsampled not supported on PColorImage')
1235 if self._imcache is None:
1236 A = self.to_rgba(self._A, bytes=True)
1237 self._imcache = np.pad(A, [(1, 1), (1, 1), (0, 0)], "constant")
1238 padded_A = self._imcache
1239 bg = mcolors.to_rgba(self.axes.patch.get_facecolor(), 0)
1240 bg = (np.array(bg) * 255).astype(np.uint8)
1241 if (padded_A[0, 0] != bg).all():
1242 padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg
1244 l, b, r, t = self.axes.bbox.extents
1245 width = (round(r) + 0.5) - (round(l) - 0.5)
1246 height = (round(t) + 0.5) - (round(b) - 0.5)
1247 width = int(round(width * magnification))
1248 height = int(round(height * magnification))
1249 vl = self.axes.viewLim
1251 x_pix = np.linspace(vl.x0, vl.x1, width)
1252 y_pix = np.linspace(vl.y0, vl.y1, height)
1253 x_int = self._Ax.searchsorted(x_pix)
1254 y_int = self._Ay.searchsorted(y_pix)
1255 im = ( # See comment in NonUniformImage.make_image re: performance.
1256 padded_A.view(np.uint32).ravel()[
1257 np.add.outer(y_int * padded_A.shape[1], x_int)]
1258 .view(np.uint8).reshape((height, width, 4)))
1259 return im, l, b, IdentityTransform()
1261 def _check_unsampled_image(self):
1262 return False
1264 def set_data(self, x, y, A):
1265 """
1266 Set the grid for the rectangle boundaries, and the data values.
1268 Parameters
1269 ----------
1270 x, y : 1D array-like, optional
1271 Monotonic arrays of length N+1 and M+1, respectively, specifying
1272 rectangle boundaries. If not given, will default to
1273 ``range(N + 1)`` and ``range(M + 1)``, respectively.
1274 A : array-like
1275 The data to be color-coded. The interpretation depends on the
1276 shape:
1278 - (M, N) ndarray or masked array: values to be colormapped
1279 - (M, N, 3): RGB array
1280 - (M, N, 4): RGBA array
1281 """
1282 A = cbook.safe_masked_invalid(A, copy=True)
1283 if x is None:
1284 x = np.arange(0, A.shape[1]+1, dtype=np.float64)
1285 else:
1286 x = np.array(x, np.float64).ravel()
1287 if y is None:
1288 y = np.arange(0, A.shape[0]+1, dtype=np.float64)
1289 else:
1290 y = np.array(y, np.float64).ravel()
1292 if A.shape[:2] != (y.size-1, x.size-1):
1293 raise ValueError(
1294 "Axes don't match array shape. Got %s, expected %s." %
1295 (A.shape[:2], (y.size - 1, x.size - 1)))
1296 if A.ndim not in [2, 3]:
1297 raise ValueError("A must be 2D or 3D")
1298 if A.ndim == 3:
1299 if A.shape[2] == 1:
1300 A = A.squeeze(axis=-1)
1301 elif A.shape[2] not in [3, 4]:
1302 raise ValueError("3D arrays must have RGB or RGBA as last dim")
1304 # For efficient cursor readout, ensure x and y are increasing.
1305 if x[-1] < x[0]:
1306 x = x[::-1]
1307 A = A[:, ::-1]
1308 if y[-1] < y[0]:
1309 y = y[::-1]
1310 A = A[::-1]
1312 self._A = A
1313 self._Ax = x
1314 self._Ay = y
1315 self._imcache = None
1316 self.stale = True
1318 def set_array(self, *args):
1319 raise NotImplementedError('Method not supported')
1321 def get_cursor_data(self, event):
1322 # docstring inherited
1323 x, y = event.xdata, event.ydata
1324 if (x < self._Ax[0] or x > self._Ax[-1] or
1325 y < self._Ay[0] or y > self._Ay[-1]):
1326 return None
1327 j = np.searchsorted(self._Ax, x) - 1
1328 i = np.searchsorted(self._Ay, y) - 1
1329 try:
1330 return self._A[i, j]
1331 except IndexError:
1332 return None
1335class FigureImage(_ImageBase):
1336 """An image attached to a figure."""
1338 zorder = 0
1340 _interpolation = 'nearest'
1342 @_api.make_keyword_only("3.6", name="cmap")
1343 def __init__(self, fig,
1344 cmap=None,
1345 norm=None,
1346 offsetx=0,
1347 offsety=0,
1348 origin=None,
1349 **kwargs
1350 ):
1351 """
1352 cmap is a colors.Colormap instance
1353 norm is a colors.Normalize instance to map luminance to 0-1
1355 kwargs are an optional list of Artist keyword args
1356 """
1357 super().__init__(
1358 None,
1359 norm=norm,
1360 cmap=cmap,
1361 origin=origin
1362 )
1363 self.figure = fig
1364 self.ox = offsetx
1365 self.oy = offsety
1366 self._internal_update(kwargs)
1367 self.magnification = 1.0
1369 def get_extent(self):
1370 """Return the image extent as tuple (left, right, bottom, top)."""
1371 numrows, numcols = self.get_size()
1372 return (-0.5 + self.ox, numcols-0.5 + self.ox,
1373 -0.5 + self.oy, numrows-0.5 + self.oy)
1375 def make_image(self, renderer, magnification=1.0, unsampled=False):
1376 # docstring inherited
1377 fac = renderer.dpi/self.figure.dpi
1378 # fac here is to account for pdf, eps, svg backends where
1379 # figure.dpi is set to 72. This means we need to scale the
1380 # image (using magnification) and offset it appropriately.
1381 bbox = Bbox([[self.ox/fac, self.oy/fac],
1382 [(self.ox/fac + self._A.shape[1]),
1383 (self.oy/fac + self._A.shape[0])]])
1384 width, height = self.figure.get_size_inches()
1385 width *= renderer.dpi
1386 height *= renderer.dpi
1387 clip = Bbox([[0, 0], [width, height]])
1388 return self._make_image(
1389 self._A, bbox, bbox, clip, magnification=magnification / fac,
1390 unsampled=unsampled, round_to_pixel_border=False)
1392 def set_data(self, A):
1393 """Set the image array."""
1394 cm.ScalarMappable.set_array(self, A)
1395 self.stale = True
1398class BboxImage(_ImageBase):
1399 """The Image class whose size is determined by the given bbox."""
1401 @_api.make_keyword_only("3.6", name="cmap")
1402 def __init__(self, bbox,
1403 cmap=None,
1404 norm=None,
1405 interpolation=None,
1406 origin=None,
1407 filternorm=True,
1408 filterrad=4.0,
1409 resample=False,
1410 **kwargs
1411 ):
1412 """
1413 cmap is a colors.Colormap instance
1414 norm is a colors.Normalize instance to map luminance to 0-1
1416 kwargs are an optional list of Artist keyword args
1417 """
1418 super().__init__(
1419 None,
1420 cmap=cmap,
1421 norm=norm,
1422 interpolation=interpolation,
1423 origin=origin,
1424 filternorm=filternorm,
1425 filterrad=filterrad,
1426 resample=resample,
1427 **kwargs
1428 )
1429 self.bbox = bbox
1431 def get_window_extent(self, renderer=None):
1432 if renderer is None:
1433 renderer = self.get_figure()._get_renderer()
1435 if isinstance(self.bbox, BboxBase):
1436 return self.bbox
1437 elif callable(self.bbox):
1438 return self.bbox(renderer)
1439 else:
1440 raise ValueError("Unknown type of bbox")
1442 def contains(self, mouseevent):
1443 """Test whether the mouse event occurred within the image."""
1444 inside, info = self._default_contains(mouseevent)
1445 if inside is not None:
1446 return inside, info
1448 if not self.get_visible(): # or self.get_figure()._renderer is None:
1449 return False, {}
1451 x, y = mouseevent.x, mouseevent.y
1452 inside = self.get_window_extent().contains(x, y)
1454 return inside, {}
1456 def make_image(self, renderer, magnification=1.0, unsampled=False):
1457 # docstring inherited
1458 width, height = renderer.get_canvas_width_height()
1459 bbox_in = self.get_window_extent(renderer).frozen()
1460 bbox_in._points /= [width, height]
1461 bbox_out = self.get_window_extent(renderer)
1462 clip = Bbox([[0, 0], [width, height]])
1463 self._transform = BboxTransformTo(clip)
1464 return self._make_image(
1465 self._A,
1466 bbox_in, bbox_out, clip, magnification, unsampled=unsampled)
1469def imread(fname, format=None):
1470 """
1471 Read an image from a file into an array.
1473 .. note::
1475 This function exists for historical reasons. It is recommended to
1476 use `PIL.Image.open` instead for loading images.
1478 Parameters
1479 ----------
1480 fname : str or file-like
1481 The image file to read: a filename, a URL or a file-like object opened
1482 in read-binary mode.
1484 Passing a URL is deprecated. Please open the URL
1485 for reading and pass the result to Pillow, e.g. with
1486 ``np.array(PIL.Image.open(urllib.request.urlopen(url)))``.
1487 format : str, optional
1488 The image file format assumed for reading the data. The image is
1489 loaded as a PNG file if *format* is set to "png", if *fname* is a path
1490 or opened file with a ".png" extension, or if it is a URL. In all
1491 other cases, *format* is ignored and the format is auto-detected by
1492 `PIL.Image.open`.
1494 Returns
1495 -------
1496 `numpy.array`
1497 The image data. The returned array has shape
1499 - (M, N) for grayscale images.
1500 - (M, N, 3) for RGB images.
1501 - (M, N, 4) for RGBA images.
1503 PNG images are returned as float arrays (0-1). All other formats are
1504 returned as int arrays, with a bit depth determined by the file's
1505 contents.
1506 """
1507 # hide imports to speed initial import on systems with slow linkers
1508 from urllib import parse
1510 if format is None:
1511 if isinstance(fname, str):
1512 parsed = parse.urlparse(fname)
1513 # If the string is a URL (Windows paths appear as if they have a
1514 # length-1 scheme), assume png.
1515 if len(parsed.scheme) > 1:
1516 ext = 'png'
1517 else:
1518 ext = Path(fname).suffix.lower()[1:]
1519 elif hasattr(fname, 'geturl'): # Returned by urlopen().
1520 # We could try to parse the url's path and use the extension, but
1521 # returning png is consistent with the block above. Note that this
1522 # if clause has to come before checking for fname.name as
1523 # urlopen("file:///...") also has a name attribute (with the fixed
1524 # value "<urllib response>").
1525 ext = 'png'
1526 elif hasattr(fname, 'name'):
1527 ext = Path(fname.name).suffix.lower()[1:]
1528 else:
1529 ext = 'png'
1530 else:
1531 ext = format
1532 img_open = (
1533 PIL.PngImagePlugin.PngImageFile if ext == 'png' else PIL.Image.open)
1534 if isinstance(fname, str) and len(parse.urlparse(fname).scheme) > 1:
1535 # Pillow doesn't handle URLs directly.
1536 raise ValueError(
1537 "Please open the URL for reading and pass the "
1538 "result to Pillow, e.g. with "
1539 "``np.array(PIL.Image.open(urllib.request.urlopen(url)))``."
1540 )
1541 with img_open(fname) as image:
1542 return (_pil_png_to_float_array(image)
1543 if isinstance(image, PIL.PngImagePlugin.PngImageFile) else
1544 pil_to_array(image))
1547def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None,
1548 origin=None, dpi=100, *, metadata=None, pil_kwargs=None):
1549 """
1550 Colormap and save an array as an image file.
1552 RGB(A) images are passed through. Single channel images will be
1553 colormapped according to *cmap* and *norm*.
1555 .. note::
1557 If you want to save a single channel image as gray scale please use an
1558 image I/O library (such as pillow, tifffile, or imageio) directly.
1560 Parameters
1561 ----------
1562 fname : str or path-like or file-like
1563 A path or a file-like object to store the image in.
1564 If *format* is not set, then the output format is inferred from the
1565 extension of *fname*, if any, and from :rc:`savefig.format` otherwise.
1566 If *format* is set, it determines the output format.
1567 arr : array-like
1568 The image data. The shape can be one of
1569 MxN (luminance), MxNx3 (RGB) or MxNx4 (RGBA).
1570 vmin, vmax : float, optional
1571 *vmin* and *vmax* set the color scaling for the image by fixing the
1572 values that map to the colormap color limits. If either *vmin*
1573 or *vmax* is None, that limit is determined from the *arr*
1574 min/max value.
1575 cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap`
1576 A Colormap instance or registered colormap name. The colormap
1577 maps scalar data to colors. It is ignored for RGB(A) data.
1578 format : str, optional
1579 The file format, e.g. 'png', 'pdf', 'svg', ... The behavior when this
1580 is unset is documented under *fname*.
1581 origin : {'upper', 'lower'}, default: :rc:`image.origin`
1582 Indicates whether the ``(0, 0)`` index of the array is in the upper
1583 left or lower left corner of the axes.
1584 dpi : float
1585 The DPI to store in the metadata of the file. This does not affect the
1586 resolution of the output image. Depending on file format, this may be
1587 rounded to the nearest integer.
1588 metadata : dict, optional
1589 Metadata in the image file. The supported keys depend on the output
1590 format, see the documentation of the respective backends for more
1591 information.
1592 pil_kwargs : dict, optional
1593 Keyword arguments passed to `PIL.Image.Image.save`. If the 'pnginfo'
1594 key is present, it completely overrides *metadata*, including the
1595 default 'Software' key.
1596 """
1597 from matplotlib.figure import Figure
1598 if isinstance(fname, os.PathLike):
1599 fname = os.fspath(fname)
1600 if format is None:
1601 format = (Path(fname).suffix[1:] if isinstance(fname, str)
1602 else mpl.rcParams["savefig.format"]).lower()
1603 if format in ["pdf", "ps", "eps", "svg"]:
1604 # Vector formats that are not handled by PIL.
1605 if pil_kwargs is not None:
1606 raise ValueError(
1607 f"Cannot use 'pil_kwargs' when saving to {format}")
1608 fig = Figure(dpi=dpi, frameon=False)
1609 fig.figimage(arr, cmap=cmap, vmin=vmin, vmax=vmax, origin=origin,
1610 resize=True)
1611 fig.savefig(fname, dpi=dpi, format=format, transparent=True,
1612 metadata=metadata)
1613 else:
1614 # Don't bother creating an image; this avoids rounding errors on the
1615 # size when dividing and then multiplying by dpi.
1616 if origin is None:
1617 origin = mpl.rcParams["image.origin"]
1618 if origin == "lower":
1619 arr = arr[::-1]
1620 if (isinstance(arr, memoryview) and arr.format == "B"
1621 and arr.ndim == 3 and arr.shape[-1] == 4):
1622 # Such an ``arr`` would also be handled fine by sm.to_rgba below
1623 # (after casting with asarray), but it is useful to special-case it
1624 # because that's what backend_agg passes, and can be in fact used
1625 # as is, saving a few operations.
1626 rgba = arr
1627 else:
1628 sm = cm.ScalarMappable(cmap=cmap)
1629 sm.set_clim(vmin, vmax)
1630 rgba = sm.to_rgba(arr, bytes=True)
1631 if pil_kwargs is None:
1632 pil_kwargs = {}
1633 else:
1634 # we modify this below, so make a copy (don't modify caller's dict)
1635 pil_kwargs = pil_kwargs.copy()
1636 pil_shape = (rgba.shape[1], rgba.shape[0])
1637 image = PIL.Image.frombuffer(
1638 "RGBA", pil_shape, rgba, "raw", "RGBA", 0, 1)
1639 if format == "png":
1640 # Only use the metadata kwarg if pnginfo is not set, because the
1641 # semantics of duplicate keys in pnginfo is unclear.
1642 if "pnginfo" in pil_kwargs:
1643 if metadata:
1644 _api.warn_external("'metadata' is overridden by the "
1645 "'pnginfo' entry in 'pil_kwargs'.")
1646 else:
1647 metadata = {
1648 "Software": (f"Matplotlib version{mpl.__version__}, "
1649 f"https://matplotlib.org/"),
1650 **(metadata if metadata is not None else {}),
1651 }
1652 pil_kwargs["pnginfo"] = pnginfo = PIL.PngImagePlugin.PngInfo()
1653 for k, v in metadata.items():
1654 if v is not None:
1655 pnginfo.add_text(k, v)
1656 if format in ["jpg", "jpeg"]:
1657 format = "jpeg" # Pillow doesn't recognize "jpg".
1658 facecolor = mpl.rcParams["savefig.facecolor"]
1659 if cbook._str_equal(facecolor, "auto"):
1660 facecolor = mpl.rcParams["figure.facecolor"]
1661 color = tuple(int(x * 255) for x in mcolors.to_rgb(facecolor))
1662 background = PIL.Image.new("RGB", pil_shape, color)
1663 background.paste(image, image)
1664 image = background
1665 pil_kwargs.setdefault("format", format)
1666 pil_kwargs.setdefault("dpi", (dpi, dpi))
1667 image.save(fname, **pil_kwargs)
1670def pil_to_array(pilImage):
1671 """
1672 Load a `PIL image`_ and return it as a numpy int array.
1674 .. _PIL image: https://pillow.readthedocs.io/en/latest/reference/Image.html
1676 Returns
1677 -------
1678 numpy.array
1680 The array shape depends on the image type:
1682 - (M, N) for grayscale images.
1683 - (M, N, 3) for RGB images.
1684 - (M, N, 4) for RGBA images.
1685 """
1686 if pilImage.mode in ['RGBA', 'RGBX', 'RGB', 'L']:
1687 # return MxNx4 RGBA, MxNx3 RBA, or MxN luminance array
1688 return np.asarray(pilImage)
1689 elif pilImage.mode.startswith('I;16'):
1690 # return MxN luminance array of uint16
1691 raw = pilImage.tobytes('raw', pilImage.mode)
1692 if pilImage.mode.endswith('B'):
1693 x = np.frombuffer(raw, '>u2')
1694 else:
1695 x = np.frombuffer(raw, '<u2')
1696 return x.reshape(pilImage.size[::-1]).astype('=u2')
1697 else: # try to convert to an rgba image
1698 try:
1699 pilImage = pilImage.convert('RGBA')
1700 except ValueError as err:
1701 raise RuntimeError('Unknown image mode') from err
1702 return np.asarray(pilImage) # return MxNx4 RGBA array
1705def _pil_png_to_float_array(pil_png):
1706 """Convert a PIL `PNGImageFile` to a 0-1 float array."""
1707 # Unlike pil_to_array this converts to 0-1 float32s for backcompat with the
1708 # old libpng-based loader.
1709 # The supported rawmodes are from PIL.PngImagePlugin._MODES. When
1710 # mode == "RGB(A)", the 16-bit raw data has already been coarsened to 8-bit
1711 # by Pillow.
1712 mode = pil_png.mode
1713 rawmode = pil_png.png.im_rawmode
1714 if rawmode == "1": # Grayscale.
1715 return np.asarray(pil_png).astype(np.float32)
1716 if rawmode == "L;2": # Grayscale.
1717 return np.divide(pil_png, 2**2 - 1, dtype=np.float32)
1718 if rawmode == "L;4": # Grayscale.
1719 return np.divide(pil_png, 2**4 - 1, dtype=np.float32)
1720 if rawmode == "L": # Grayscale.
1721 return np.divide(pil_png, 2**8 - 1, dtype=np.float32)
1722 if rawmode == "I;16B": # Grayscale.
1723 return np.divide(pil_png, 2**16 - 1, dtype=np.float32)
1724 if mode == "RGB": # RGB.
1725 return np.divide(pil_png, 2**8 - 1, dtype=np.float32)
1726 if mode == "P": # Palette.
1727 return np.divide(pil_png.convert("RGBA"), 2**8 - 1, dtype=np.float32)
1728 if mode == "LA": # Grayscale + alpha.
1729 return np.divide(pil_png.convert("RGBA"), 2**8 - 1, dtype=np.float32)
1730 if mode == "RGBA": # RGBA.
1731 return np.divide(pil_png, 2**8 - 1, dtype=np.float32)
1732 raise ValueError(f"Unknown PIL rawmode: {rawmode}")
1735def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear',
1736 preview=False):
1737 """
1738 Make a thumbnail of image in *infile* with output filename *thumbfile*.
1740 See :doc:`/gallery/misc/image_thumbnail_sgskip`.
1742 Parameters
1743 ----------
1744 infile : str or file-like
1745 The image file. Matplotlib relies on Pillow_ for image reading, and
1746 thus supports a wide range of file formats, including PNG, JPG, TIFF
1747 and others.
1749 .. _Pillow: https://python-pillow.org/
1751 thumbfile : str or file-like
1752 The thumbnail filename.
1754 scale : float, default: 0.1
1755 The scale factor for the thumbnail.
1757 interpolation : str, default: 'bilinear'
1758 The interpolation scheme used in the resampling. See the
1759 *interpolation* parameter of `~.Axes.imshow` for possible values.
1761 preview : bool, default: False
1762 If True, the default backend (presumably a user interface
1763 backend) will be used which will cause a figure to be raised if
1764 `~matplotlib.pyplot.show` is called. If it is False, the figure is
1765 created using `.FigureCanvasBase` and the drawing backend is selected
1766 as `.Figure.savefig` would normally do.
1768 Returns
1769 -------
1770 `.Figure`
1771 The figure instance containing the thumbnail.
1772 """
1774 im = imread(infile)
1775 rows, cols, depth = im.shape
1777 # This doesn't really matter (it cancels in the end) but the API needs it.
1778 dpi = 100
1780 height = rows / dpi * scale
1781 width = cols / dpi * scale
1783 if preview:
1784 # Let the UI backend do everything.
1785 import matplotlib.pyplot as plt
1786 fig = plt.figure(figsize=(width, height), dpi=dpi)
1787 else:
1788 from matplotlib.figure import Figure
1789 fig = Figure(figsize=(width, height), dpi=dpi)
1790 FigureCanvasBase(fig)
1792 ax = fig.add_axes([0, 0, 1, 1], aspect='auto',
1793 frameon=False, xticks=[], yticks=[])
1794 ax.imshow(im, aspect='auto', resample=True, interpolation=interpolation)
1795 fig.savefig(thumbfile, dpi=dpi)
1796 return fig