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

1""" 

2The image module supports basic image loading, rescaling and display 

3operations. 

4""" 

5 

6import math 

7import os 

8import logging 

9from pathlib import Path 

10import warnings 

11 

12import numpy as np 

13import PIL.PngImagePlugin 

14 

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) 

28 

29_log = logging.getLogger(__name__) 

30 

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} 

53 

54interpolations_names = set(_interpd_) 

55 

56 

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. 

61 

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. 

69 

70 renderer : `.RendererBase` 

71 

72 magnification : float, default: 1 

73 The additional magnification to apply for the renderer in use. 

74 

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 

85 

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]]])) 

96 

97 if len(parts) == 0: 

98 return np.empty((0, 0, 4), dtype=np.uint8), 0, 0 

99 

100 bbox = Bbox.union(bboxes) 

101 

102 output = np.zeros( 

103 (int(bbox.height), int(bbox.width), 4), dtype=np.uint8) 

104 

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) 

109 

110 return output, bbox.x0 / magnification, bbox.y0 / magnification 

111 

112 

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. 

118 

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) 

124 

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()) 

128 

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() 

136 

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[:] 

149 

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() 

158 

159 

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 

214 

215 

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 

228 

229 

230class _ImageBase(martist.Artist, cm.ScalarMappable): 

231 """ 

232 Base class for images. 

233 

234 interpolation and cmap default to their rc settings 

235 

236 cmap is a colors.Colormap instance 

237 norm is a colors.Normalize instance to map luminance to 0-1 

238 

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. 

242 

243 Additional kwargs are matplotlib.artist properties 

244 """ 

245 zorder = 0 

246 

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 

271 

272 self._imcache = None 

273 

274 self._internal_update(kwargs) 

275 

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__ 

282 

283 def __getstate__(self): 

284 # Save some space on the pickle by not saving the cache. 

285 return {**super().__getstate__(), "_imcache": None} 

286 

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') 

291 

292 return self._A.shape[:2] 

293 

294 def set_alpha(self, alpha): 

295 """ 

296 Set the alpha value used for blending - not supported on all backends. 

297 

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 

307 

308 def _get_scalar_alpha(self): 

309 """ 

310 Get a scalar alpha value to be applied to the artist as a whole. 

311 

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 

320 

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) 

327 

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. 

335 

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. 

339 

340 If *unsampled* is True, the image will not be scaled, but an 

341 appropriate affine transformation will be returned instead. 

342 

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`. 

347 

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.") 

365 

366 clipped_bbox = Bbox.intersection(out_bbox, clip_bbox) 

367 

368 if clipped_bbox is None: 

369 return None, 0, 0, None 

370 

371 out_width_base = clipped_bbox.width * magnification 

372 out_height_base = clipped_bbox.height * magnification 

373 

374 if out_width_base == 0 or out_height_base == 0: 

375 return None, 0, 0, None 

376 

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() 

384 

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()) 

392 

393 t = (t0 

394 + (Affine2D() 

395 .translate(-clipped_bbox.x0, -clipped_bbox.y0) 

396 .scale(magnification))) 

397 

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) 

412 

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 

421 

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 

438 

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. 

444 

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) 

467 

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) 

483 

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() 

489 

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) 

512 

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 

558 

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 

563 

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) 

570 

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 

575 

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]))] 

583 

584 t = Affine2D().translate( 

585 int(max(subset.xmin, 0)), int(max(subset.ymin, 0))) + t 

586 

587 return output, clipped_bbox.x0, clipped_bbox.y0, t 

588 

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*. 

593 

594 If *unsampled* is True, the image will not be scaled, but an 

595 appropriate affine transformation will be returned instead. 

596 

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') 

608 

609 def _check_unsampled_image(self): 

610 """ 

611 Return whether the image is better to be drawn unsampled. 

612 

613 The derived class needs to override it. 

614 """ 

615 return False 

616 

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 

647 

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 

671 

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 

676 

677 return inside, {} 

678 

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") 

684 

685 def set_data(self, A): 

686 """ 

687 Set the image array. 

688 

689 Note that this function does *not* update the normalization used. 

690 

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) 

698 

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)) 

703 

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] 

707 

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)) 

712 

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) 

729 

730 self._imcache = None 

731 self.stale = True 

732 

733 def set_array(self, A): 

734 """ 

735 Retained for backwards compatibility - use set_data instead. 

736 

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) 

744 

745 def get_interpolation(self): 

746 """ 

747 Return the interpolation method the image uses when resizing. 

748 

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 

755 

756 def set_interpolation(self, s): 

757 """ 

758 Set the interpolation method the image uses when resizing. 

759 

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. 

764 

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 

777 

778 def set_interpolation_stage(self, s): 

779 """ 

780 Set when interpolation happens during the transform to RGBA. 

781 

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 

793 

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) 

801 

802 def set_resample(self, v): 

803 """ 

804 Set whether image resampling is used. 

805 

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 

815 

816 def get_resample(self): 

817 """Return whether image resampling is used.""" 

818 return self._resample 

819 

820 def set_filternorm(self, filternorm): 

821 """ 

822 Set whether the resize filter normalizes the weights. 

823 

824 See help for `~.Axes.imshow`. 

825 

826 Parameters 

827 ---------- 

828 filternorm : bool 

829 """ 

830 self._filternorm = bool(filternorm) 

831 self.stale = True 

832 

833 def get_filternorm(self): 

834 """Return whether the resize filter normalizes the weights.""" 

835 return self._filternorm 

836 

837 def set_filterrad(self, filterrad): 

838 """ 

839 Set the resize filter radius only applicable to some 

840 interpolation schemes -- see help for imshow 

841 

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 

851 

852 def get_filterrad(self): 

853 """Return the filterrad setting.""" 

854 return self._filterrad 

855 

856 

857class AxesImage(_ImageBase): 

858 """ 

859 An image attached to an Axes. 

860 

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 """ 

904 

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 ): 

919 

920 self._extent = extent 

921 

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 ) 

934 

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) 

939 

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) 

951 

952 def _check_unsampled_image(self): 

953 """Return whether the image would be better drawn unsampled.""" 

954 return self.get_interpolation() == "none" 

955 

956 def set_extent(self, extent): 

957 """ 

958 Set the image extent. 

959 

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. 

965 

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 

983 

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) 

995 

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. 

1000 

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] 

1022 

1023 

1024class NonUniformImage(AxesImage): 

1025 mouseover = False # This class still needs its own get_cursor_data impl. 

1026 

1027 def __init__(self, ax, *, interpolation='nearest', **kwargs): 

1028 """ 

1029 Parameters 

1030 ---------- 

1031 interpolation : {'nearest', 'bilinear'}, default: 'nearest' 

1032 

1033 **kwargs 

1034 All other keyword arguments are identical to those of `.AxesImage`. 

1035 """ 

1036 super().__init__(ax, **kwargs) 

1037 self.set_interpolation(interpolation) 

1038 

1039 def _check_unsampled_image(self): 

1040 """Return False. Do not use unsampled image.""" 

1041 return False 

1042 

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() 

1112 

1113 def set_data(self, x, y, A): 

1114 """ 

1115 Set the grid for the pixel centers, and the pixel values. 

1116 

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 

1142 

1143 self.stale = True 

1144 

1145 def set_array(self, *args): 

1146 raise NotImplementedError('Method not supported') 

1147 

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) 

1159 

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] 

1164 

1165 def set_filternorm(self, s): 

1166 pass 

1167 

1168 def set_filterrad(self, s): 

1169 pass 

1170 

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) 

1175 

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) 

1180 

1181 

1182class PcolorImage(AxesImage): 

1183 """ 

1184 Make a pcolor-style plot with an irregular rectangular grid. 

1185 

1186 This uses a variation of the original irregular image code, 

1187 and it is used by pcolorfast for the corresponding grid type. 

1188 """ 

1189 

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: 

1211 

1212 - (M, N) ndarray or masked array: values to be colormapped 

1213 - (M, N, 3): RGB array 

1214 - (M, N, 4): RGBA array 

1215 

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) 

1227 

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') 

1234 

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 

1243 

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 

1250 

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() 

1260 

1261 def _check_unsampled_image(self): 

1262 return False 

1263 

1264 def set_data(self, x, y, A): 

1265 """ 

1266 Set the grid for the rectangle boundaries, and the data values. 

1267 

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: 

1277 

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() 

1291 

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") 

1303 

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] 

1311 

1312 self._A = A 

1313 self._Ax = x 

1314 self._Ay = y 

1315 self._imcache = None 

1316 self.stale = True 

1317 

1318 def set_array(self, *args): 

1319 raise NotImplementedError('Method not supported') 

1320 

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 

1333 

1334 

1335class FigureImage(_ImageBase): 

1336 """An image attached to a figure.""" 

1337 

1338 zorder = 0 

1339 

1340 _interpolation = 'nearest' 

1341 

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 

1354 

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 

1368 

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) 

1374 

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) 

1391 

1392 def set_data(self, A): 

1393 """Set the image array.""" 

1394 cm.ScalarMappable.set_array(self, A) 

1395 self.stale = True 

1396 

1397 

1398class BboxImage(_ImageBase): 

1399 """The Image class whose size is determined by the given bbox.""" 

1400 

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 

1415 

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 

1430 

1431 def get_window_extent(self, renderer=None): 

1432 if renderer is None: 

1433 renderer = self.get_figure()._get_renderer() 

1434 

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") 

1441 

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 

1447 

1448 if not self.get_visible(): # or self.get_figure()._renderer is None: 

1449 return False, {} 

1450 

1451 x, y = mouseevent.x, mouseevent.y 

1452 inside = self.get_window_extent().contains(x, y) 

1453 

1454 return inside, {} 

1455 

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) 

1467 

1468 

1469def imread(fname, format=None): 

1470 """ 

1471 Read an image from a file into an array. 

1472 

1473 .. note:: 

1474 

1475 This function exists for historical reasons. It is recommended to 

1476 use `PIL.Image.open` instead for loading images. 

1477 

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. 

1483 

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`. 

1493 

1494 Returns 

1495 ------- 

1496 `numpy.array` 

1497 The image data. The returned array has shape 

1498 

1499 - (M, N) for grayscale images. 

1500 - (M, N, 3) for RGB images. 

1501 - (M, N, 4) for RGBA images. 

1502 

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 

1509 

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)) 

1545 

1546 

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. 

1551 

1552 RGB(A) images are passed through. Single channel images will be 

1553 colormapped according to *cmap* and *norm*. 

1554 

1555 .. note:: 

1556 

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. 

1559 

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) 

1668 

1669 

1670def pil_to_array(pilImage): 

1671 """ 

1672 Load a `PIL image`_ and return it as a numpy int array. 

1673 

1674 .. _PIL image: https://pillow.readthedocs.io/en/latest/reference/Image.html 

1675 

1676 Returns 

1677 ------- 

1678 numpy.array 

1679 

1680 The array shape depends on the image type: 

1681 

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 

1703 

1704 

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}") 

1733 

1734 

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*. 

1739 

1740 See :doc:`/gallery/misc/image_thumbnail_sgskip`. 

1741 

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. 

1748 

1749 .. _Pillow: https://python-pillow.org/ 

1750 

1751 thumbfile : str or file-like 

1752 The thumbnail filename. 

1753 

1754 scale : float, default: 0.1 

1755 The scale factor for the thumbnail. 

1756 

1757 interpolation : str, default: 'bilinear' 

1758 The interpolation scheme used in the resampling. See the 

1759 *interpolation* parameter of `~.Axes.imshow` for possible values. 

1760 

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. 

1767 

1768 Returns 

1769 ------- 

1770 `.Figure` 

1771 The figure instance containing the thumbnail. 

1772 """ 

1773 

1774 im = imread(infile) 

1775 rows, cols, depth = im.shape 

1776 

1777 # This doesn't really matter (it cancels in the end) but the API needs it. 

1778 dpi = 100 

1779 

1780 height = rows / dpi * scale 

1781 width = cols / dpi * scale 

1782 

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) 

1791 

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