from contextlib import ExitStack import functools import io import os from pathlib import Path import platform import sys import urllib.request import numpy as np from numpy.testing import assert_allclose, assert_array_equal from PIL import Image import matplotlib as mpl from matplotlib import ( colors, image as mimage, patches, pyplot as plt, style, rcParams) from matplotlib.image import (AxesImage, BboxImage, FigureImage, NonUniformImage, PcolorImage) from matplotlib.patches import Rectangle from matplotlib.testing.decorators import check_figures_equal, image_comparison from matplotlib.transforms import Bbox, Affine2D, Transform, TransformedBbox import matplotlib.ticker as mticker import pytest @pytest.fixture def nonaffine_identity(): """Non-affine identity transform for compositing with any affine transform""" class NonAffineIdentityTransform(Transform): input_dims = 2 output_dims = 2 def inverted(self): return self return NonAffineIdentityTransform() @image_comparison(['interp_alpha.png'], remove_text=True, style='_classic_test') def test_alpha_interp(): """Test the interpolation of the alpha channel on RGBA images""" fig, (axl, axr) = plt.subplots(1, 2) # full green image img = np.zeros((5, 5, 4)) img[..., 1] = np.ones((5, 5)) # transparent under main diagonal img[..., 3] = np.tril(np.ones((5, 5), dtype=np.uint8)) axl.imshow(img, interpolation="none") axr.imshow(img, interpolation="bilinear") @image_comparison(['interp_nearest_vs_none'], tol=3.7, # For Ghostscript 10.06+. extensions=['pdf', 'svg'], remove_text=True, style='mpl20') def test_interp_nearest_vs_none(): """Test the effect of "nearest" and "none" interpolation""" # Setting dpi to something really small makes the difference very # visible. This works fine with pdf, since the dpi setting doesn't # affect anything but images, but the agg output becomes unusably # small. rcParams['savefig.dpi'] = 3 X = np.array([[[218, 165, 32], [122, 103, 238]], [[127, 255, 0], [255, 99, 71]]], dtype=np.uint8) fig, (ax1, ax2) = plt.subplots(1, 2) ax1.imshow(X, interpolation='none') ax1.set_title('interpolation none') ax2.imshow(X, interpolation='nearest') ax2.set_title('interpolation nearest') @pytest.mark.parametrize('suppressComposite', [False, True]) @image_comparison(['figimage'], extensions=['png', 'pdf'], style='_classic_test') def test_figimage(suppressComposite): fig = plt.figure(figsize=(2, 2), dpi=100) fig.suppressComposite = suppressComposite x, y = np.ix_(np.arange(100) / 100.0, np.arange(100) / 100) z = np.sin(x**2 + y**2 - x*y) c = np.sin(20*x**2 + 50*y**2) img = z + c/5 fig.figimage(img, xo=0, yo=0, origin='lower') fig.figimage(img[::-1, :], xo=0, yo=100, origin='lower') fig.figimage(img[:, ::-1], xo=100, yo=0, origin='lower') fig.figimage(img[::-1, ::-1], xo=100, yo=100, origin='lower') def test_image_python_io(): fig, ax = plt.subplots() ax.plot([1, 2, 3]) buffer = io.BytesIO() fig.savefig(buffer) buffer.seek(0) plt.imread(buffer) @pytest.mark.parametrize( "img_size, fig_size, interpolation", [(5, 2, "hanning"), # data larger than figure. (5, 5, "nearest"), # exact resample. (5, 10, "nearest"), # double sample. (3, 2.9, "hanning"), # <3 upsample. (3, 9.1, "nearest"), # >3 upsample. ]) @check_figures_equal() def test_imshow_antialiased(fig_test, fig_ref, img_size, fig_size, interpolation): np.random.seed(19680801) dpi = plt.rcParams["savefig.dpi"] A = np.random.rand(int(dpi * img_size), int(dpi * img_size)) for fig in [fig_test, fig_ref]: fig.set_size_inches(fig_size, fig_size) ax = fig_test.subplots() ax.set_position([0, 0, 1, 1]) ax.imshow(A, interpolation='auto') ax = fig_ref.subplots() ax.set_position([0, 0, 1, 1]) ax.imshow(A, interpolation=interpolation) @check_figures_equal() def test_imshow_zoom(fig_test, fig_ref): # should be less than 3 upsample, so should be nearest... np.random.seed(19680801) dpi = plt.rcParams["savefig.dpi"] A = np.random.rand(int(dpi * 3), int(dpi * 3)) for fig in [fig_test, fig_ref]: fig.set_size_inches(2.9, 2.9) ax = fig_test.subplots() ax.imshow(A, interpolation='auto') ax.set_xlim(10, 20) ax.set_ylim(10, 20) ax = fig_ref.subplots() ax.imshow(A, interpolation='nearest') ax.set_xlim(10, 20) ax.set_ylim(10, 20) @check_figures_equal() def test_imshow_pil(fig_test, fig_ref): style.use("default") png_path = Path(__file__).parent / "baseline_images/pngsuite/basn3p04.png" tiff_path = Path(__file__).parent / "baseline_images/test_image/uint16.tif" axs = fig_test.subplots(2) axs[0].imshow(Image.open(png_path)) axs[1].imshow(Image.open(tiff_path)) axs = fig_ref.subplots(2) axs[0].imshow(plt.imread(png_path)) axs[1].imshow(plt.imread(tiff_path)) def test_imread_pil_uint16(): img = plt.imread(os.path.join(os.path.dirname(__file__), 'baseline_images', 'test_image', 'uint16.tif')) assert img.dtype == np.uint16 assert np.sum(img) == 134184960 def test_imread_fspath(): img = plt.imread( Path(__file__).parent / 'baseline_images/test_image/uint16.tif') assert img.dtype == np.uint16 assert np.sum(img) == 134184960 @pytest.mark.parametrize("fmt", ["png", "jpg", "jpeg", "tiff"]) def test_imsave(fmt): has_alpha = fmt not in ["jpg", "jpeg"] # The goal here is that the user can specify an output logical DPI # for the image, but this will not actually add any extra pixels # to the image, it will merely be used for metadata purposes. # So we do the traditional case (dpi == 1), and the new case (dpi # == 100) and read the resulting PNG files back in and make sure # the data is 100% identical. np.random.seed(1) # The height of 1856 pixels was selected because going through creating an # actual dpi=100 figure to save the image to a Pillow-provided format would # cause a rounding error resulting in a final image of shape 1855. data = np.random.rand(1856, 2) buff_dpi1 = io.BytesIO() plt.imsave(buff_dpi1, data, format=fmt, dpi=1) buff_dpi100 = io.BytesIO() plt.imsave(buff_dpi100, data, format=fmt, dpi=100) buff_dpi1.seek(0) arr_dpi1 = plt.imread(buff_dpi1, format=fmt) buff_dpi100.seek(0) arr_dpi100 = plt.imread(buff_dpi100, format=fmt) assert arr_dpi1.shape == (1856, 2, 3 + has_alpha) assert arr_dpi100.shape == (1856, 2, 3 + has_alpha) assert_array_equal(arr_dpi1, arr_dpi100) def test_imsave_python_sequences(): # Tests saving an image with data passed using Python sequence types # such as lists or tuples. # RGB image: 3 rows × 2 columns, with float values in [0.0, 1.0] img_data = [ [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)], [(0.0, 0.0, 1.0), (1.0, 1.0, 0.0)], [(0.0, 1.0, 1.0), (1.0, 0.0, 1.0)], ] buff = io.BytesIO() plt.imsave(buff, img_data, format="png") buff.seek(0) read_img = plt.imread(buff) assert_array_equal( np.array(img_data), read_img[:, :, :3] # Drop alpha if present ) @pytest.mark.parametrize("origin", ["upper", "lower"]) def test_imsave_rgba_origin(origin): # test that imsave always passes c-contiguous arrays down to pillow buf = io.BytesIO() result = np.zeros((10, 10, 4), dtype='uint8') mimage.imsave(buf, arr=result, format="png", origin=origin) @pytest.mark.parametrize("fmt", ["png", "pdf", "ps", "eps", "svg"]) def test_imsave_fspath(fmt, tmp_path): plt.imsave(tmp_path / f'unused.{fmt}', np.array([[0, 1]]), format=fmt) def test_imsave_color_alpha(): # Test that imsave accept arrays with ndim=3 where the third dimension is # color and alpha without raising any exceptions, and that the data is # acceptably preserved through a save/read roundtrip. np.random.seed(1) for origin in ['lower', 'upper']: data = np.random.rand(16, 16, 4) buff = io.BytesIO() plt.imsave(buff, data, origin=origin, format="png") buff.seek(0) arr_buf = plt.imread(buff) # Recreate the float -> uint8 conversion of the data # We can only expect to be the same with 8 bits of precision, # since that's what the PNG file used. data = (255*data).astype('uint8') if origin == 'lower': data = data[::-1] arr_buf = (255*arr_buf).astype('uint8') assert_array_equal(data, arr_buf) def test_imsave_pil_kwargs_png(): from PIL.PngImagePlugin import PngInfo buf = io.BytesIO() pnginfo = PngInfo() pnginfo.add_text("Software", "test") plt.imsave(buf, [[0, 1], [2, 3]], format="png", pil_kwargs={"pnginfo": pnginfo}) im = Image.open(buf) assert im.info["Software"] == "test" def test_imsave_pil_kwargs_tiff(): from PIL.TiffTags import TAGS_V2 as TAGS buf = io.BytesIO() pil_kwargs = {"description": "test image"} plt.imsave(buf, [[0, 1], [2, 3]], format="tiff", pil_kwargs=pil_kwargs) assert len(pil_kwargs) == 1 im = Image.open(buf) tags = {TAGS[k].name: v for k, v in im.tag_v2.items()} assert tags["ImageDescription"] == "test image" @image_comparison(['image_alpha'], remove_text=True, style='_classic_test') def test_image_alpha(): np.random.seed(0) Z = np.random.rand(6, 6) fig, (ax1, ax2, ax3) = plt.subplots(1, 3) ax1.imshow(Z, alpha=1.0, interpolation='none') ax2.imshow(Z, alpha=0.5, interpolation='none') ax3.imshow(Z, alpha=0.5, interpolation='nearest') @mpl.style.context('mpl20') @check_figures_equal() def test_imshow_alpha(fig_test, fig_ref): np.random.seed(19680801) rgbf = np.random.rand(6, 6, 3).astype(np.float32) rgbu = np.uint8(rgbf * 255) ((ax0, ax1), (ax2, ax3)) = fig_test.subplots(2, 2) ax0.imshow(rgbf, alpha=0.5) ax1.imshow(rgbf, alpha=0.75) ax2.imshow(rgbu, alpha=127/255) ax3.imshow(rgbu, alpha=191/255) rgbaf = np.concatenate((rgbf, np.ones((6, 6, 1))), axis=2).astype(np.float32) rgbau = np.concatenate((rgbu, np.full((6, 6, 1), 255, np.uint8)), axis=2) ((ax0, ax1), (ax2, ax3)) = fig_ref.subplots(2, 2) rgbaf[:, :, 3] = 0.5 ax0.imshow(rgbaf) rgbaf[:, :, 3] = 0.75 ax1.imshow(rgbaf) rgbau[:, :, 3] = 127 ax2.imshow(rgbau) rgbau[:, :, 3] = 191 ax3.imshow(rgbau) @pytest.mark.parametrize('n_channels, is_int, alpha_arr, opaque', [(3, False, False, False), # RGB float (4, False, False, False), # RGBA float (4, False, True, False), # RGBA float with alpha array (4, False, False, True), # RGBA float with solid color (4, True, False, False)]) # RGBA unint8 def test_imshow_multi_draw(n_channels, is_int, alpha_arr, opaque): if is_int: array = np.random.randint(0, 256, (2, 2, n_channels)) else: array = np.random.random((2, 2, n_channels)) if opaque: array[:, :, 3] = 1 if alpha_arr: alpha = np.array([[0.3, 0.5], [1, 0.8]]) else: alpha = None fig, ax = plt.subplots() im = ax.imshow(array, alpha=alpha) fig.draw_without_rendering() # Draw should not modify original array np.testing.assert_array_equal(array, im._A) def test_cursor_data(): from matplotlib.backend_bases import MouseEvent fig, ax = plt.subplots() im = ax.imshow(np.arange(100).reshape(10, 10), origin='upper') x, y = 4, 4 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 44 # Now try for a point outside the image # Tests issue #4957 x, y = 10.1, 4 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) is None # Hmm, something is wrong here... I get 0, not None... # But, this works further down in the tests with extents flipped # x, y = 0.1, -0.1 # xdisp, ydisp = ax.transData.transform([x, y]) # event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) # z = im.get_cursor_data(event) # assert z is None, "Did not get None, got %d" % z ax.clear() # Now try with the extents flipped. im = ax.imshow(np.arange(100).reshape(10, 10), origin='lower') x, y = 4, 4 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 44 fig, ax = plt.subplots() im = ax.imshow(np.arange(100).reshape(10, 10), extent=[0, 0.5, 0, 0.5]) x, y = 0.25, 0.25 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 55 # Now try for a point outside the image # Tests issue #4957 x, y = 0.75, 0.25 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) is None x, y = 0.01, -0.01 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) is None # Now try with additional transform applied to the image artist trans = Affine2D().scale(2).rotate(0.5) im = ax.imshow(np.arange(100).reshape(10, 10), transform=trans + ax.transData) x, y = 3, 10 xdisp, ydisp = ax.transData.transform([x, y]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == 44 @pytest.mark.parametrize("xy, data", [ # x/y coords chosen to be 0.5 above boundaries so they lie within image pixels [[0.5, 0.5], 0 + 0], [[0.5, 1.5], 0 + 1], [[4.5, 0.5], 16 + 0], [[8.5, 0.5], 16 + 0], [[9.5, 2.5], 81 + 4], [[-1, 0.5], None], [[0.5, -1], None], ] ) def test_cursor_data_nonuniform(xy, data): from matplotlib.backend_bases import MouseEvent # Non-linear set of x-values x = np.array([0, 1, 4, 9, 16]) y = np.array([0, 1, 2, 3, 4]) z = x[np.newaxis, :]**2 + y[:, np.newaxis]**2 fig, ax = plt.subplots() im = NonUniformImage(ax, extent=(x.min(), x.max(), y.min(), y.max())) im.set_data(x, y, z) ax.add_image(im) # Set lower min lim so we can test cursor outside image ax.set_xlim(x.min() - 2, x.max()) ax.set_ylim(y.min() - 2, y.max()) xdisp, ydisp = ax.transData.transform(xy) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.get_cursor_data(event) == data, (im.get_cursor_data(event), data) @pytest.mark.parametrize( "data, text", [ ([[10001, 10000]], "[10001.000]"), ([[.123, .987]], "[0.123]"), ([[np.nan, 1, 2]], "[]"), ([[1, 1+1e-15]], "[1.0000000000000000]"), ([[-1, -1]], "[-1.0]"), ([[0, 0]], "[0.00]"), ]) def test_format_cursor_data(data, text): from matplotlib.backend_bases import MouseEvent fig, ax = plt.subplots() im = ax.imshow(data) xdisp, ydisp = ax.transData.transform([0, 0]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.format_cursor_data(im.get_cursor_data(event)) == text @pytest.mark.parametrize( "data, text", [ ([[[10001, 10000]], [[0, 0]]], "[10001.000, 0.000]"), ([[[.123, .987]], [[0.1, 0]]], "[0.123, 0.100]"), ([[[np.nan, 1, 2]], [[0, 0, 0]]], "[]"), ]) def test_format_cursor_data_multinorm(data, text): from matplotlib.backend_bases import MouseEvent fig, ax = plt.subplots() cmap_bivar = mpl.bivar_colormaps['BiOrangeBlue'] cmap_multivar = mpl.multivar_colormaps['2VarAddA'] # This is a test for ColorizingArtist._format_cursor_data_override() # with data with multiple channels. # It includes a workaround so that we can test this functionality # before the MultiVar/BiVariate colormaps and MultiNorm are exposed # via the top-level methods (ax.imshow()) # i.e. we here set the hidden variables _cmap and _norm # and use set_array() on the ColorizingArtist rather than the _ImageBase # but this workaround should be replaced by: # `ax.imshow(data, cmap=cmap_bivar, vmin=(0,0), vmax=(1,1))` # once the functionality is available. # see https://github.com/matplotlib/matplotlib/issues/14168 im = ax.imshow([[0, 1]]) im.colorizer._cmap = cmap_bivar im.colorizer._norm = colors.MultiNorm([im.norm, im.norm]) mpl.colorizer.ColorizingArtist.set_array(im, data) xdisp, ydisp = ax.transData.transform([0, 0]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.format_cursor_data(im.get_cursor_data(event)) == text im.colorizer._cmap = cmap_multivar event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) assert im.format_cursor_data(im.get_cursor_data(event)) == text @image_comparison(['image_clip'], style='mpl20') def test_image_clip(): d = [[1, 2], [3, 4]] fig, ax = plt.subplots() im = ax.imshow(d) patch = patches.Circle((0, 0), radius=1, transform=ax.transData) im.set_clip_path(patch) @image_comparison(['image_cliprect'], style='mpl20') def test_image_cliprect(): fig, ax = plt.subplots() d = [[1, 2], [3, 4]] im = ax.imshow(d, extent=(0, 5, 0, 5)) rect = patches.Rectangle( xy=(1, 1), width=2, height=2, transform=im.axes.transData) im.set_clip_path(rect) @check_figures_equal() def test_imshow_10_10_1(fig_test, fig_ref): # 10x10x1 should be the same as 10x10 arr = np.arange(100).reshape((10, 10, 1)) ax = fig_ref.subplots() ax.imshow(arr[:, :, 0], interpolation="bilinear", extent=(1, 2, 1, 2)) ax.set_xlim(0, 3) ax.set_ylim(0, 3) ax = fig_test.subplots() ax.imshow(arr, interpolation="bilinear", extent=(1, 2, 1, 2)) ax.set_xlim(0, 3) ax.set_ylim(0, 3) def test_imshow_10_10_2(): fig, ax = plt.subplots() arr = np.arange(200).reshape((10, 10, 2)) with pytest.raises(TypeError): ax.imshow(arr) def test_imshow_10_10_5(): fig, ax = plt.subplots() arr = np.arange(500).reshape((10, 10, 5)) with pytest.raises(TypeError): ax.imshow(arr) @image_comparison(['no_interpolation_origin'], remove_text=True, style='_classic_test') def test_no_interpolation_origin(): fig, axs = plt.subplots(2) axs[0].imshow(np.arange(100).reshape((2, 50)), origin="lower", interpolation='none') axs[1].imshow(np.arange(100).reshape((2, 50)), interpolation='none') @image_comparison(['image_shift'], extensions=['pdf', 'svg'], remove_text=True, style='_classic_test') def test_image_shift(): imgData = [[1 / x + 1 / y for x in range(1, 100)] for y in range(1, 100)] tMin = 734717.945208 tMax = 734717.946366 fig, ax = plt.subplots() ax.imshow(imgData, norm=colors.LogNorm(), interpolation='none', extent=(tMin, tMax, 1, 100)) ax.set_aspect('auto') def test_image_edges(): fig = plt.figure(figsize=[1, 1]) ax = fig.add_axes((0, 0, 1, 1), frameon=False) data = np.tile(np.arange(12), 15).reshape(20, 9) im = ax.imshow(data, origin='upper', extent=[-10, 10, -10, 10], interpolation='none', cmap='gray') x = y = 2 ax.set_xlim(-x, x) ax.set_ylim(-y, y) ax.set_xticks([]) ax.set_yticks([]) buf = io.BytesIO() fig.savefig(buf, facecolor=(0, 1, 0)) buf.seek(0) im = plt.imread(buf) r, g, b, a = sum(im[:, 0]) r, g, b, a = sum(im[:, -1]) assert g != 100, 'Expected a non-green edge - but sadly, it was.' @image_comparison(['image_composite_background'], remove_text=True, style='mpl20') def test_image_composite_background(): fig, ax = plt.subplots() arr = np.arange(12).reshape(4, 3) ax.imshow(arr, extent=[0, 2, 15, 0]) ax.imshow(arr, extent=[4, 6, 15, 0]) ax.set_facecolor((1, 0, 0, 0.5)) ax.set_xlim(0, 12) @image_comparison(['image_composite_alpha'], remove_text=True, style='_classic_test', tol=0.07) def test_image_composite_alpha(): """ Tests that the alpha value is recognized and correctly applied in the process of compositing images together. """ fig, ax = plt.subplots() arr = np.zeros((11, 21, 4)) arr[:, :, 0] = 1 arr[:, :, 3] = np.concatenate( (np.arange(0, 1.1, 0.1), np.arange(0, 1, 0.1)[::-1])) arr2 = np.zeros((21, 11, 4)) arr2[:, :, 0] = 1 arr2[:, :, 1] = 1 arr2[:, :, 3] = np.concatenate( (np.arange(0, 1.1, 0.1), np.arange(0, 1, 0.1)[::-1]))[:, np.newaxis] ax.imshow(arr, extent=[1, 2, 5, 0], alpha=0.3) ax.imshow(arr, extent=[2, 3, 5, 0], alpha=0.6) ax.imshow(arr, extent=[3, 4, 5, 0]) ax.imshow(arr2, extent=[0, 5, 1, 2]) ax.imshow(arr2, extent=[0, 5, 2, 3], alpha=0.6) ax.imshow(arr2, extent=[0, 5, 3, 4], alpha=0.3) ax.set_facecolor((0, 0.5, 0, 1)) ax.set_xlim(0, 5) ax.set_ylim(5, 0) @check_figures_equal(extensions=["pdf"]) def test_clip_path_disables_compositing(fig_test, fig_ref): t = np.arange(9).reshape((3, 3)) for fig in [fig_test, fig_ref]: ax = fig.add_subplot() ax.imshow(t, clip_path=(mpl.path.Path([(0, 0), (0, 1), (1, 0)]), ax.transData)) ax.imshow(t, clip_path=(mpl.path.Path([(1, 1), (1, 2), (2, 1)]), ax.transData)) fig_ref.suppressComposite = True @image_comparison(['rasterize_10dpi'], extensions=['pdf', 'svg'], remove_text=True, style='mpl20') def test_rasterize_dpi(): # This test should check rasterized rendering with high output resolution. # It plots a rasterized line and a normal image with imshow. So it will # catch when images end up in the wrong place in case of non-standard dpi # setting. Instead of high-res rasterization I use low-res. Therefore # the fact that the resolution is non-standard is easily checked by # image_comparison. img = np.asarray([[1, 2], [3, 4]]) fig, axs = plt.subplots(1, 3, figsize=(3, 1)) axs[0].imshow(img) axs[1].plot([0, 1], [0, 1], linewidth=20., rasterized=True) axs[1].set(xlim=(0, 1), ylim=(-1, 2)) axs[2].plot([0, 1], [0, 1], linewidth=20.) axs[2].set(xlim=(0, 1), ylim=(-1, 2)) # Low-dpi PDF rasterization errors prevent proper image comparison tests. # Hide detailed structures like the axes spines. for ax in axs: ax.set_xticks([]) ax.set_yticks([]) ax.spines[:].set_visible(False) rcParams['savefig.dpi'] = 10 @image_comparison(['bbox_image_inverted'], remove_text=True, style='mpl20') def test_bbox_image_inverted(): # This is just used to produce an image to feed to BboxImage image = np.arange(100).reshape((10, 10)) fig, ax = plt.subplots() bbox_im = BboxImage( TransformedBbox(Bbox([[100, 100], [0, 0]]), ax.transData), interpolation='nearest') bbox_im.set_data(image) bbox_im.set_clip_on(False) ax.set_xlim(0, 100) ax.set_ylim(0, 100) ax.add_artist(bbox_im) image = np.identity(10) bbox_im = BboxImage(TransformedBbox(Bbox([[0.1, 0.2], [0.3, 0.25]]), ax.get_figure().transFigure), interpolation='nearest') bbox_im.set_data(image) bbox_im.set_clip_on(False) ax.add_artist(bbox_im) def test_get_window_extent_for_AxisImage(): # Create a figure of known size (1000x1000 pixels), place an image # object at a given location and check that get_window_extent() # returns the correct bounding box values (in pixels). im = np.array([[0.25, 0.75, 1.0, 0.75], [0.1, 0.65, 0.5, 0.4], [0.6, 0.3, 0.0, 0.2], [0.7, 0.9, 0.4, 0.6]]) fig, ax = plt.subplots(figsize=(10, 10), dpi=100) ax.set_position([0, 0, 1, 1]) ax.set_xlim(0, 1) ax.set_ylim(0, 1) im_obj = ax.imshow( im, extent=[0.4, 0.7, 0.2, 0.9], interpolation='nearest') fig.canvas.draw() renderer = fig.canvas.renderer im_bbox = im_obj.get_window_extent(renderer) assert_array_equal(im_bbox.get_points(), [[400, 200], [700, 900]]) fig, ax = plt.subplots(figsize=(10, 10), dpi=100) ax.set_position([0, 0, 1, 1]) ax.set_xlim(1, 2) ax.set_ylim(0, 1) im_obj = ax.imshow( im, extent=[0.4, 0.7, 0.2, 0.9], interpolation='nearest', transform=ax.transAxes) fig.canvas.draw() renderer = fig.canvas.renderer im_bbox = im_obj.get_window_extent(renderer) assert_array_equal(im_bbox.get_points(), [[400, 200], [700, 900]]) @image_comparison(['zoom_and_clip_upper_origin.png'], remove_text=True, style='mpl20') def test_zoom_and_clip_upper_origin(): image = np.arange(100) image = image.reshape((10, 10)) fig, ax = plt.subplots() ax.imshow(image) ax.set_ylim(2.0, -0.5) ax.set_xlim(-0.5, 2.0) def test_nonuniformimage_setcmap(): ax = plt.gca() im = NonUniformImage(ax) im.set_cmap('Blues') def test_nonuniformimage_setnorm(): ax = plt.gca() im = NonUniformImage(ax) im.set_norm(plt.Normalize()) def test_jpeg_2d(): # smoke test that mode-L pillow images work. imd = np.ones((10, 10), dtype='uint8') for i in range(10): imd[i, :] = np.linspace(0.0, 1.0, 10) * 255 im = Image.new('L', (10, 10)) im.putdata(imd.flatten()) fig, ax = plt.subplots() ax.imshow(im) def test_jpeg_alpha(): plt.figure(figsize=(1, 1), dpi=300) # Create an image that is all black, with a gradient from 0-1 in # the alpha channel from left to right. im = np.zeros((300, 300, 4), dtype=float) im[..., 3] = np.linspace(0.0, 1.0, 300) plt.figimage(im) buff = io.BytesIO() plt.savefig(buff, facecolor="red", format='jpg', dpi=300) buff.seek(0) image = Image.open(buff) # If this fails, there will be only one color (all black). If this # is working, we should have all 256 shades of grey represented. num_colors = len(image.getcolors(256)) assert 175 <= num_colors <= 230 # The fully transparent part should be red. corner_pixel = image.getpixel((0, 0)) assert corner_pixel == (254, 0, 0) def test_axesimage_setdata(): ax = plt.gca() im = AxesImage(ax) z = np.arange(12, dtype=float).reshape((4, 3)) im.set_data(z) z[0, 0] = 9.9 assert im._A[0, 0] == 0, 'value changed' def test_figureimage_setdata(): fig = plt.gcf() im = FigureImage(fig) z = np.arange(12, dtype=float).reshape((4, 3)) im.set_data(z) z[0, 0] = 9.9 assert im._A[0, 0] == 0, 'value changed' @pytest.mark.parametrize( "image_cls,x,y,a", [ (NonUniformImage, np.arange(3.), np.arange(4.), np.arange(12.).reshape((4, 3))), (PcolorImage, np.arange(3.), np.arange(4.), np.arange(6.).reshape((3, 2))), ]) def test_setdata_xya(image_cls, x, y, a): ax = plt.gca() im = image_cls(ax) im.set_data(x, y, a) x[0] = y[0] = a[0, 0] = 9.9 assert im._A[0, 0] == im._Ax[0] == im._Ay[0] == 0, 'value changed' im.set_data(x, y, a.reshape((*a.shape, -1))) # Just a smoketest. def test_minimized_rasterized(): # This ensures that the rasterized content in the colorbars is # only as thick as the colorbar, and doesn't extend to other parts # of the image. See #5814. While the original bug exists only # in Postscript, the best way to detect it is to generate SVG # and then parse the output to make sure the two colorbar images # are the same size. from xml.etree import ElementTree np.random.seed(0) data = np.random.rand(10, 10) fig, ax = plt.subplots(1, 2) p1 = ax[0].pcolormesh(data) p2 = ax[1].pcolormesh(data) plt.colorbar(p1, ax=ax[0]) plt.colorbar(p2, ax=ax[1]) buff = io.BytesIO() plt.savefig(buff, format='svg') buff = io.BytesIO(buff.getvalue()) tree = ElementTree.parse(buff) width = None for image in tree.iter('image'): if width is None: width = image['width'] else: if image['width'] != width: assert False def test_load_from_url(): path = Path(__file__).parent / "baseline_images/pngsuite/basn3p04.png" url = ('file:' + ('///' if sys.platform == 'win32' else '') + path.resolve().as_posix()) with pytest.raises(ValueError, match="Please open the URL"): plt.imread(url) with urllib.request.urlopen(url) as file: plt.imread(file) @image_comparison(['log_scale_image'], remove_text=True, style='_classic_test') def test_log_scale_image(): Z = np.zeros((10, 10)) Z[::2] = 1 fig, ax = plt.subplots() ax.imshow(Z, extent=[1, 100, 1, 100], cmap='viridis', vmax=1, vmin=-1, aspect='auto') ax.set(yscale='log') @image_comparison(['rotate_image'], remove_text=True, style='_classic_test') def test_rotate_image(): delta = 0.25 x = y = np.arange(-3.0, 3.0, delta) X, Y = np.meshgrid(x, y) Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / (2 * np.pi * 0.5 * 1.5)) Z = Z2 - Z1 # difference of Gaussians fig, ax1 = plt.subplots(1, 1) im1 = ax1.imshow(Z, interpolation='none', cmap='viridis', origin='lower', extent=[-2, 4, -3, 2], clip_on=True) trans_data2 = Affine2D().rotate_deg(30) + ax1.transData im1.set_transform(trans_data2) # display intended extent of the image x1, x2, y1, y2 = im1.get_extent() ax1.plot([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], "r--", lw=3, transform=trans_data2) ax1.set_xlim(2, 5) ax1.set_ylim(0, 4) def test_image_preserve_size(): buff = io.BytesIO() im = np.zeros((481, 321)) plt.imsave(buff, im, format="png") buff.seek(0) img = plt.imread(buff) assert img.shape[:2] == im.shape def test_image_preserve_size2(): n = 7 data = np.identity(n, float) fig = plt.figure(figsize=(n, n), frameon=False) ax = fig.add_axes((0.0, 0.0, 1.0, 1.0)) ax.set_axis_off() ax.imshow(data, interpolation='nearest', origin='lower', aspect='auto') buff = io.BytesIO() fig.savefig(buff, dpi=1) buff.seek(0) img = plt.imread(buff) assert img.shape == (7, 7, 4) assert_array_equal(np.asarray(img[:, :, 0], bool), np.identity(n, bool)[::-1]) @image_comparison(['mask_image_over_under.png'], remove_text=True, style='_classic_test', tol=1.0) def test_mask_image_over_under(): delta = 0.025 x = y = np.arange(-3.0, 3.0, delta) X, Y = np.meshgrid(x, y) Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / (2 * np.pi * 0.5 * 1.5)) Z = 10*(Z2 - Z1) # difference of Gaussians palette = plt.colormaps["gray"].with_extremes(over='r', under='g', bad='b') Zm = np.ma.masked_where(Z > 1.2, Z) fig, (ax1, ax2) = plt.subplots(1, 2) im = ax1.imshow(Zm, interpolation='bilinear', cmap=palette, norm=colors.Normalize(vmin=-1.0, vmax=1.0, clip=False), origin='lower', extent=[-3, 3, -3, 3]) ax1.set_title('Green=low, Red=high, Blue=bad') fig.colorbar(im, extend='both', orientation='horizontal', ax=ax1, aspect=10) im = ax2.imshow(Zm, interpolation='nearest', cmap=palette, norm=colors.BoundaryNorm([-1, -0.5, -0.2, 0, 0.2, 0.5, 1], ncolors=256, clip=False), origin='lower', extent=[-3, 3, -3, 3]) ax2.set_title('With BoundaryNorm') fig.colorbar(im, extend='both', spacing='proportional', orientation='horizontal', ax=ax2, aspect=10) @image_comparison(['mask_image'], remove_text=True, style='_classic_test') def test_mask_image(): # Test mask image two ways: Using nans and using a masked array. fig, (ax1, ax2) = plt.subplots(1, 2) A = np.ones((5, 5)) A[1:2, 1:2] = np.nan ax1.imshow(A, interpolation='nearest') A = np.zeros((5, 5), dtype=bool) A[1:2, 1:2] = True A = np.ma.masked_array(np.ones((5, 5), dtype=np.uint16), A) ax2.imshow(A, interpolation='nearest') def test_mask_image_all(): # Test behavior with an image that is entirely masked does not warn data = np.full((2, 2), np.nan) fig, ax = plt.subplots() ax.imshow(data) fig.canvas.draw_idle() # would emit a warning @image_comparison(['imshow_endianess.png'], remove_text=True, style='_classic_test') def test_imshow_endianess(): x = np.arange(10) X, Y = np.meshgrid(x, x) Z = np.hypot(X - 5, Y - 5) fig, (ax1, ax2) = plt.subplots(1, 2) kwargs = dict(origin="lower", interpolation='nearest', cmap='viridis') ax1.imshow(Z.astype('f8'), **kwargs) @image_comparison(['imshow_masked_interpolation'], tol=0 if platform.machine() == 'x86_64' else 0.01, remove_text=True, style='mpl20') def test_imshow_masked_interpolation(): cmap = mpl.colormaps['viridis'].with_extremes(over='r', under='b', bad='k') N = 20 n = colors.Normalize(vmin=0, vmax=N*N-1) data = np.arange(N*N, dtype=float).reshape(N, N) data[5, 5] = -1 # This will cause crazy ringing for the higher-order # interpolations data[15, 5] = 1e5 # data[3, 3] = np.nan data[15, 15] = np.inf mask = np.zeros_like(data).astype('bool') mask[5, 15] = True data = np.ma.masked_array(data, mask) fig, ax_grid = plt.subplots(3, 6) interps = sorted(mimage._interpd_) interps.remove('auto') interps.remove('antialiased') for interp, ax in zip(interps, ax_grid.ravel()): ax.set_title(interp) ax.imshow(data, norm=n, cmap=cmap, interpolation=interp) ax.axis('off') def test_imshow_no_warn_invalid(): plt.imshow([[1, 2], [3, np.nan]]) # Check that no warning is emitted. @pytest.mark.parametrize( 'dtype', [np.dtype(s) for s in 'u2 u4 i2 i4 i8 f4 f8'.split()]) def test_imshow_clips_rgb_to_valid_range(dtype): arr = np.arange(300, dtype=dtype).reshape((10, 10, 3)) if dtype.kind != 'u': arr -= 10 too_low = arr < 0 too_high = arr > 255 if dtype.kind == 'f': arr = arr / 255 _, ax = plt.subplots() out = ax.imshow(arr).get_array() assert (out[too_low] == 0).all() if dtype.kind == 'f': assert (out[too_high] == 1).all() assert out.dtype.kind == 'f' else: assert (out[too_high] == 255).all() assert out.dtype == np.uint8 @image_comparison(['imshow_flatfield.png'], remove_text=True, style='mpl20') def test_imshow_flatfield(): fig, ax = plt.subplots() im = ax.imshow(np.ones((5, 5)), interpolation='nearest') im.set_clim(.5, 1.5) @image_comparison(['imshow_bignumbers.png'], remove_text=True, style='mpl20') def test_imshow_bignumbers(): rcParams['image.interpolation'] = 'nearest' # putting a big number in an array of integers shouldn't # ruin the dynamic range of the resolved bits. fig, ax = plt.subplots() img = np.array([[1, 2, 1e12], [3, 1, 4]], dtype=np.uint64) pc = ax.imshow(img) pc.set_clim(0, 5) @image_comparison(['imshow_bignumbers_real.png'], remove_text=True, style='mpl20') def test_imshow_bignumbers_real(): rcParams['image.interpolation'] = 'nearest' # putting a big number in an array of integers shouldn't # ruin the dynamic range of the resolved bits. fig, ax = plt.subplots() img = np.array([[2., 1., 1.e22], [4., 1., 3.]]) pc = ax.imshow(img) pc.set_clim(0, 5) @pytest.mark.parametrize( "make_norm", [colors.Normalize, colors.LogNorm, lambda: colors.SymLogNorm(1), lambda: colors.PowerNorm(1)]) def test_empty_imshow(make_norm): fig, ax = plt.subplots() with pytest.warns(UserWarning, match="Attempting to set identical low and high xlims"): im = ax.imshow([[]], norm=make_norm()) im.set_extent([-5, 5, -5, 5]) fig.canvas.draw() with pytest.raises(RuntimeError): im.make_image(fig.canvas.get_renderer()) def test_imshow_float16(): fig, ax = plt.subplots() ax.imshow(np.zeros((3, 3), dtype=np.float16)) # Ensure that drawing doesn't cause crash. fig.canvas.draw() def test_imshow_float128(): fig, ax = plt.subplots() ax.imshow(np.zeros((3, 3), dtype=np.longdouble)) with (ExitStack() if np.can_cast(np.longdouble, np.float64, "equiv") else pytest.warns(UserWarning)): # Ensure that drawing doesn't cause crash. fig.canvas.draw() def test_imshow_bool(): fig, ax = plt.subplots() ax.imshow(np.array([[True, False], [False, True]], dtype=bool)) def test_full_invalid(): fig, ax = plt.subplots() ax.imshow(np.full((10, 10), np.nan)) fig.canvas.draw() @pytest.mark.parametrize("fmt,counted", [("ps", b" colorimage"), ("svg", b" 1e20 data = np.full((5, 5), x, dtype=np.float64) data[0:2, :] = 1E20 ax = fig_test.subplots() ax.imshow(data, norm=colors.LogNorm(vmin=1, vmax=data.max()), interpolation='nearest', cmap='viridis') data = np.full((5, 5), x, dtype=np.float64) data[0:2, :] = 1000 ax = fig_ref.subplots() cmap = mpl.colormaps['viridis'].with_extremes(under='w') ax.imshow(data, norm=colors.Normalize(vmin=1, vmax=data.max()), interpolation='nearest', cmap=cmap) @check_figures_equal() def test_spy_box(fig_test, fig_ref): # setting up reference and test ax_test = fig_test.subplots(1, 3) ax_ref = fig_ref.subplots(1, 3) plot_data = ( [[1, 1], [1, 1]], [[0, 0], [0, 0]], [[0, 1], [1, 0]], ) plot_titles = ["ones", "zeros", "mixed"] for i, (z, title) in enumerate(zip(plot_data, plot_titles)): ax_test[i].set_title(title) ax_test[i].spy(z) ax_ref[i].set_title(title) ax_ref[i].imshow(z, interpolation='nearest', aspect='equal', origin='upper', cmap='Greys', vmin=0, vmax=1) ax_ref[i].set_xlim(-0.5, 1.5) ax_ref[i].set_ylim(1.5, -0.5) ax_ref[i].xaxis.tick_top() ax_ref[i].title.set_y(1.05) ax_ref[i].xaxis.set_ticks_position('both') ax_ref[i].xaxis.set_major_locator( mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True) ) ax_ref[i].yaxis.set_major_locator( mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True) ) @image_comparison(["nonuniform_and_pcolor.png"], style="mpl20") def test_nonuniform_and_pcolor(): axs = plt.figure(figsize=(3, 3)).subplots(3, sharex=True, sharey=True) for ax, interpolation in zip(axs, ["nearest", "bilinear"]): im = NonUniformImage(ax, interpolation=interpolation) im.set_data(np.arange(3) ** 2, np.arange(3) ** 2, np.arange(9).reshape((3, 3))) ax.add_image(im) axs[2].pcolorfast( # PcolorImage np.arange(4) ** 2, np.arange(4) ** 2, np.arange(9).reshape((3, 3))) for ax in axs: ax.set_axis_off() # NonUniformImage "leaks" out of extents, not PColorImage. ax.set(xlim=(0, 10)) @image_comparison(["nonuniform_logscale.png"], style="mpl20") def test_nonuniform_logscale(): _, axs = plt.subplots(ncols=3, nrows=1) for i in range(3): ax = axs[i] im = NonUniformImage(ax) im.set_data(np.arange(1, 4) ** 2, np.arange(1, 4) ** 2, np.arange(9).reshape((3, 3))) ax.set_xlim(1, 16) ax.set_ylim(1, 16) ax.set_box_aspect(1) if i == 1: ax.set_xscale("log", base=2) ax.set_yscale("log", base=2) if i == 2: ax.set_xscale("log", base=4) ax.set_yscale("log", base=4) ax.add_image(im) @image_comparison(['rgba_antialias.png'], style='mpl20', remove_text=True) def test_rgba_antialias(): fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False, sharey=False, constrained_layout=True) N = 250 aa = np.ones((N, N)) aa[::2, :] = -1 x = np.arange(N) / N - 0.5 y = np.arange(N) / N - 0.5 X, Y = np.meshgrid(x, y) R = np.sqrt(X**2 + Y**2) f0 = 10 k = 75 # aliased concentric circles a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2)) # stripes on lhs a[:int(N/2), :][R[:int(N/2), :] < 0.4] = -1 a[:int(N/2), :][R[:int(N/2), :] < 0.3] = 1 aa[:, int(N/2):] = a[:, int(N/2):] # set some over/unders and NaNs aa[20:50, 20:50] = np.nan aa[70:90, 70:90] = 1e6 aa[70:90, 20:30] = -1e6 aa[70:90, 195:215] = 1e6 aa[20:30, 195:215] = -1e6 cmap = plt.colormaps["RdBu_r"].with_extremes(over='yellow', under='cyan') axs = axs.flatten() # zoom in axs[0].imshow(aa, interpolation='nearest', cmap=cmap, vmin=-1.2, vmax=1.2) axs[0].set_xlim(N/2-25, N/2+25) axs[0].set_ylim(N/2+50, N/2-10) # no anti-alias axs[1].imshow(aa, interpolation='nearest', cmap=cmap, vmin=-1.2, vmax=1.2) # data antialias: Note no purples, and white in circle. Note # that alternating red and blue stripes become white. axs[2].imshow(aa, interpolation='auto', interpolation_stage='data', cmap=cmap, vmin=-1.2, vmax=1.2) # rgba antialias: Note purples at boundary with circle. Note that # alternating red and blue stripes become purple axs[3].imshow(aa, interpolation='auto', interpolation_stage='rgba', cmap=cmap, vmin=-1.2, vmax=1.2) @check_figures_equal() def test_upsample_interpolation_stage(fig_test, fig_ref): """ Show that interpolation_stage='auto' gives the same as 'data' for upsampling. """ # Fixing random state for reproducibility. This non-standard seed # gives red splotches for 'rgba'. np.random.seed(19680801+9) grid = np.random.rand(4, 4) ax = fig_ref.subplots() ax.imshow(grid, interpolation='bilinear', cmap='viridis', interpolation_stage='data') ax = fig_test.subplots() ax.imshow(grid, interpolation='bilinear', cmap='viridis', interpolation_stage='auto') @check_figures_equal() def test_downsample_interpolation_stage(fig_test, fig_ref): """ Show that interpolation_stage='auto' gives the same as 'rgba' for downsampling. """ # Fixing random state for reproducibility np.random.seed(19680801) grid = np.random.rand(400, 400) ax = fig_ref.subplots() ax.imshow(grid, interpolation='auto', cmap='viridis', interpolation_stage='rgba') ax = fig_test.subplots() ax.imshow(grid, interpolation='auto', cmap='viridis', interpolation_stage='auto') def test_rc_interpolation_stage(): for val in ["data", "rgba"]: with mpl.rc_context({"image.interpolation_stage": val}): assert plt.imshow([[1, 2]]).get_interpolation_stage() == val for val in ["DATA", "foo", None]: with pytest.raises(ValueError): mpl.rcParams["image.interpolation_stage"] = val # We check for the warning with a draw() in the test, but we also need to # filter the warning as it is emitted by the figure test decorator @pytest.mark.filterwarnings(r'ignore:Data with more than .* ' 'cannot be accurately displayed') @pytest.mark.parametrize('origin', ['upper', 'lower']) @pytest.mark.parametrize( 'dim, size, msg', [['row', 2**23, r'2\*\*23 columns'], ['col', 2**24, r'2\*\*24 rows']]) @check_figures_equal() def test_large_image(fig_test, fig_ref, dim, size, msg, origin, high_memory): # Check that Matplotlib downsamples images that are too big for AGG # See issue #19276. Currently the fix only works for png output but not # pdf or svg output. ax_test = fig_test.subplots() ax_ref = fig_ref.subplots() array = np.zeros((1, size + 2)) array[:, array.size // 2:] = 1 if dim == 'col': array = array.T im = ax_test.imshow(array, vmin=0, vmax=1, aspect='auto', extent=(0, 1, 0, 1), interpolation='none', origin=origin) with pytest.warns(UserWarning, match=f'Data with more than {msg} cannot be ' 'accurately displayed.'): with io.BytesIO() as buffer: # Write to a buffer to trigger the warning fig_test.savefig(buffer) array = np.zeros((1, size // 2 + 1)) array[:, array.size // 2:] = 1 if dim == 'col': array = array.T im = ax_ref.imshow(array, vmin=0, vmax=1, aspect='auto', extent=(0, 1, 0, 1), interpolation='none', origin=origin) @check_figures_equal() def test_str_norms(fig_test, fig_ref): t = np.random.rand(10, 10) * .8 + .1 # between 0 and 1 axts = fig_test.subplots(1, 5) axts[0].imshow(t, norm="log") axts[1].imshow(t, norm="log", vmin=.2) axts[2].imshow(t, norm="symlog") axts[3].imshow(t, norm="symlog", vmin=.3, vmax=.7) axts[4].imshow(t, norm="logit", vmin=.3, vmax=.7) axrs = fig_ref.subplots(1, 5) axrs[0].imshow(t, norm=colors.LogNorm()) axrs[1].imshow(t, norm=colors.LogNorm(vmin=.2)) # same linthresh as SymmetricalLogScale's default. axrs[2].imshow(t, norm=colors.SymLogNorm(linthresh=2)) axrs[3].imshow(t, norm=colors.SymLogNorm(linthresh=2, vmin=.3, vmax=.7)) axrs[4].imshow(t, norm="logit", clim=(.3, .7)) assert type(axts[0].images[0].norm) is colors.LogNorm # Exactly that class with pytest.raises(ValueError): axts[0].imshow(t, norm="foobar") def test__resample_valid_output(): resample = functools.partial(mpl._image.resample, transform=Affine2D()) with pytest.raises(TypeError, match="incompatible function arguments"): resample(np.zeros((9, 9)), None) with pytest.raises(ValueError, match="different dimensionalities"): resample(np.zeros((9, 9)), np.zeros((9, 9, 4))) with pytest.raises(ValueError, match="different dimensionalities"): resample(np.zeros((9, 9, 4)), np.zeros((9, 9))) with pytest.raises(ValueError, match="3D input array must be RGBA"): resample(np.zeros((9, 9, 3)), np.zeros((9, 9, 4))) with pytest.raises(ValueError, match="3D output array must be RGBA"): resample(np.zeros((9, 9, 4)), np.zeros((9, 9, 3))) with pytest.raises(ValueError, match="mismatched types"): resample(np.zeros((9, 9), np.uint8), np.zeros((9, 9))) with pytest.raises(ValueError, match="must be C-contiguous"): resample(np.zeros((9, 9)), np.zeros((9, 9)).T) out = np.zeros((9, 9)) out.flags.writeable = False with pytest.raises(ValueError, match="Output array must be writeable"): resample(np.zeros((9, 9)), out) @pytest.mark.parametrize("data, interpolation, expected", [(np.array([[0.1, 0.3, 0.2]]), mimage.NEAREST, np.array([[0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.3, 0.2, 0.2, 0.2]])), (np.array([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]]), mimage.NEAREST, np.array([[0.1, 0.2, 0.2, 0.3, 0.4, 0.4, 0.5, 0.6, 0.6]])), (np.array([[0.1, 0.3, 0.2]]), mimage.BILINEAR, np.array([[0.1, 0.1, 0.15, 0.21, 0.27, 0.285, 0.255, 0.225, 0.2, 0.2]])), (np.array([[0.1, 0.9]]), mimage.BILINEAR, np.array([[0.1, 0.1, 0.1, 0.1, 0.1, 0.14, 0.22, 0.3, 0.38, 0.46, 0.54, 0.62, 0.7, 0.78, 0.86, 0.9, 0.9, 0.9, 0.9, 0.9]])), (np.array([[0.1, 0.1]]), mimage.BILINEAR, np.full((1, 10), 0.1)), # Test at the subpixel level (np.array([[0.1, 0.9]]), mimage.NEAREST, np.concatenate([np.full(512, 0.1), np.full(512, 0.9)]).reshape(1, -1)), (np.array([[0.1, 0.9]]), mimage.BILINEAR, np.concatenate([np.full(256, 0.1), np.linspace(0.5, 256, 512).astype(int) / 256 * 0.8 + 0.1, np.full(256, 0.9)]).reshape(1, -1)), ] ) def test_resample_nonaffine(data, interpolation, expected, nonaffine_identity): # Test that both affine and nonaffine transforms resample to the correct answer # If the array is constant, the tolerance can be tight # Otherwise, the tolerance is limited by the subpixel approach in the agg backend atol = 0 if np.all(data == data.ravel()[0]) else 2e-3 # Create a simple affine transform for scaling the input array affine_transform = Affine2D().scale(sx=expected.shape[1] / data.shape[1], sy=1) affine_result = np.empty_like(expected) mimage.resample(data, affine_result, affine_transform, interpolation=interpolation) assert_allclose(affine_result, expected, atol=atol) # Create a nonaffine version of the same transform # by compositing with a nonaffine identity transform nonaffine_transform = nonaffine_identity + affine_transform nonaffine_result = np.empty_like(expected) mimage.resample(data, nonaffine_result, nonaffine_transform, interpolation=interpolation) assert_allclose(nonaffine_result, expected, atol=atol) def test_axesimage_get_shape(): # generate dummy image to test get_shape method ax = plt.gca() im = AxesImage(ax) with pytest.raises(RuntimeError, match="You must first set the image array"): im.get_shape() z = np.arange(12, dtype=float).reshape((4, 3)) im.set_data(z) assert im.get_shape() == (4, 3) assert im.get_size() == im.get_shape() def test_non_transdata_image_does_not_touch_aspect(): ax = plt.figure().add_subplot() im = np.arange(4).reshape((2, 2)) ax.imshow(im, transform=ax.transAxes) assert ax.get_aspect() == "auto" ax.imshow(im, transform=Affine2D().scale(2) + ax.transData) assert ax.get_aspect() == 1 ax.imshow(im, transform=ax.transAxes, aspect=2) assert ax.get_aspect() == 2 @image_comparison(['downsampling.png'], style='mpl20', remove_text=True, tol=0 if platform.machine() == 'x86_64' else 0.07) def test_downsampling(): N = 450 x = np.arange(N) / N - 0.5 y = np.arange(N) / N - 0.5 aa = np.ones((N, N)) aa[::2, :] = -1 X, Y = np.meshgrid(x, y) R = np.sqrt(X**2 + Y**2) f0 = 5 k = 100 a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2)) # make the left hand side of this a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1 a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1 aa[:, int(N / 3):] = a[:, int(N / 3):] a = aa fig, axs = plt.subplots(2, 3, figsize=(7, 6), layout='compressed') axs[0, 0].imshow(a, interpolation='nearest', interpolation_stage='rgba', cmap='RdBu_r') axs[0, 0].set_xlim(125, 175) axs[0, 0].set_ylim(250, 200) axs[0, 0].set_title('Zoom') for ax, interp, space in zip(axs.flat[1:], ['nearest', 'nearest', 'hanning', 'hanning', 'auto'], ['data', 'rgba', 'data', 'rgba', 'auto']): ax.imshow(a, interpolation=interp, interpolation_stage=space, cmap='RdBu_r') ax.set_title(f"interpolation='{interp}'\nspace='{space}'") @image_comparison(['downsampling_speckle.png'], style='mpl20', remove_text=True) def test_downsampling_speckle(): fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), sharex=True, sharey=True, layout="compressed") axs = axs.flatten() img = ((np.arange(1024).reshape(-1, 1) * np.ones(720)) // 50).T cm = plt.get_cmap("viridis").with_extremes(over="m") norm = colors.LogNorm(vmin=3, vmax=11) # old default cannot be tested because it creates over/under speckles # in the following that are machine dependent. axs[0].set_title("interpolation='auto', stage='rgba'") axs[0].imshow(np.triu(img), cmap=cm, norm=norm, interpolation_stage='rgba') # Should be same as previous axs[1].set_title("interpolation='auto', stage='auto'") axs[1].imshow(np.triu(img), cmap=cm, norm=norm) @image_comparison( ['upsampling.png'], style='mpl20', remove_text=True) def test_upsampling(): np.random.seed(19680801+9) # need this seed to get yellow next to blue a = np.random.rand(4, 4) fig, axs = plt.subplots(1, 3, figsize=(6.5, 3), layout='compressed') im = axs[0].imshow(a, cmap='viridis') axs[0].set_title( "interpolation='auto'\nstage='antialaised'\n(default for upsampling)") # probably what people want: axs[1].imshow(a, cmap='viridis', interpolation='sinc') axs[1].set_title( "interpolation='sinc'\nstage='auto'\n(default for upsampling)") # probably not what people want: axs[2].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='rgba') axs[2].set_title("interpolation='sinc'\nstage='rgba'") fig.colorbar(im, ax=axs, shrink=0.7, extend='both') @pytest.mark.parametrize( 'dtype', ('float64', 'float32', 'int16', 'uint16', 'int8', 'uint8'), ) @pytest.mark.parametrize('ndim', (2, 3)) def test_resample_dtypes(dtype, ndim): # Issue 28448, incorrect dtype comparisons in C++ image_resample can raise # ValueError: arrays must be of dtype byte, short, float32 or float64 rng = np.random.default_rng(4181) shape = (2, 2) if ndim == 2 else (2, 2, 3) data = rng.uniform(size=shape).astype(np.dtype(dtype, copy=True)) fig, ax = plt.subplots() axes_image = ax.imshow(data) # Before fix the following raises ValueError for some dtypes. axes_image.make_image(None)[0] @pytest.mark.parametrize('intp_stage', ('data', 'rgba')) @check_figures_equal(extensions=['png', 'pdf', 'svg']) def test_interpolation_stage_rgba_respects_alpha_param(fig_test, fig_ref, intp_stage): axs_tst = fig_test.subplots(2, 3) axs_ref = fig_ref.subplots(2, 3) ny, nx = 3, 3 scalar_alpha = 0.5 array_alpha = np.random.rand(ny, nx) # When the image does not have an alpha channel, alpha should be specified # by the user or default to 1.0 im_rgb = np.random.rand(ny, nx, 3) im_concat_default_a = np.ones((ny, nx, 1)) # alpha defaults to 1.0 im_rgba = np.concatenate( # combine rgb channels with array alpha (im_rgb, array_alpha.reshape((ny, nx, 1))), axis=-1 ) axs_tst[0][0].imshow(im_rgb) axs_ref[0][0].imshow(np.concatenate((im_rgb, im_concat_default_a), axis=-1)) axs_tst[0][1].imshow(im_rgb, interpolation_stage=intp_stage, alpha=scalar_alpha) axs_ref[0][1].imshow( np.concatenate( # combine rgb channels with broadcasted scalar alpha (im_rgb, scalar_alpha * im_concat_default_a), axis=-1 ), interpolation_stage=intp_stage ) axs_tst[0][2].imshow(im_rgb, interpolation_stage=intp_stage, alpha=array_alpha) axs_ref[0][2].imshow(im_rgba, interpolation_stage=intp_stage) # When the image already has an alpha channel, multiply it by the # alpha param (both scalar and array alpha multiply the existing alpha) axs_tst[1][0].imshow(im_rgba) axs_ref[1][0].imshow(im_rgb, alpha=array_alpha) axs_tst[1][1].imshow(im_rgba, interpolation_stage=intp_stage, alpha=scalar_alpha) axs_ref[1][1].imshow( np.concatenate( # combine rgb channels with scaled array alpha (im_rgb, scalar_alpha * array_alpha.reshape((ny, nx, 1))), axis=-1 ), interpolation_stage=intp_stage ) new_array_alpha = np.random.rand(ny, nx) axs_tst[1][2].imshow(im_rgba, interpolation_stage=intp_stage, alpha=new_array_alpha) axs_ref[1][2].imshow( np.concatenate( # combine rgb channels with multiplied array alpha (im_rgb, array_alpha.reshape((ny, nx, 1)) * new_array_alpha.reshape((ny, nx, 1))), axis=-1 ), interpolation_stage=intp_stage ) @image_comparison(['nn_pixel_alignment.png'], style='_classic_test') def test_nn_pixel_alignment(nonaffine_identity): fig, axs = plt.subplots(2, 3) for j, N in enumerate([3, 7, 11]): # In each column, the plots use the same data array data = np.arange(N**2).reshape((N, N)) % 4 seps = np.arange(-0.5, N) for i in range(2): if i == 0: # Top row uses an affine transform axs[i, j].imshow(data, cmap='Grays', interpolation='nearest') else: # Bottom row uses a non-affine transform axs[i, j].imshow(data, cmap='Grays', interpolation='nearest', transform=nonaffine_identity + axs[i, j].transData) axs[i, j].set_axis_off() axs[i, j].vlines(seps, -1, N, lw=0.5, color='red', ls='dashed') axs[i, j].hlines(seps, -1, N, lw=0.5, color='red', ls='dashed') @image_comparison(['alignment_half_display_pixels.png'], style='mpl20') def test_alignment_half_display_pixels(nonaffine_identity): # All values in this test are chosen carefully so that many display pixels are # aligned with an edge or a corner of an input pixel # Layout: # Top row is origin='upper', bottom row is origin='lower' # Column 1: affine transform, anchored at whole pixel # Column 2: affine transform, anchored at half pixel # Column 3: nonaffine transform, anchored at whole pixel # Column 4: nonaffine transform, anchored at half pixel # Column 5: affine transform, anchored at half pixel, interpolation='hanning' # Each axes patch is magenta, so seeing a magenta line at an edge of the image # means that the image is not filling the axes fig = plt.figure(figsize=(5, 2), dpi=100) fig.set_facecolor('g') corner_x = [0.01, 0.199, 0.41, 0.599, 0.809] corner_y = [0.05, 0.53] axs = [] for cy in corner_y: for ix, cx in enumerate(corner_x): my = cy + 0.0125 if ix in [1, 3, 4] else cy axs.append(fig.add_axes([cx, my, 0.17, 0.425], xticks=[], yticks=[])) # Verify that each axes has been created with the correct width/height and that all # four corners are on whole pixels (columns 1 and 3) or half pixels (columns 2, 4, # and 5) for i, ax in enumerate(axs): extents = ax.get_window_extent().extents assert_allclose(extents[2:4] - extents[0:2], 85, rtol=0, atol=1e-13) assert_allclose(extents % 1, 0.5 if i % 5 in [1, 3, 4] else 0, rtol=0, atol=1e-13) N = 10 data = np.arange(N**2).reshape((N, N)) % 9 seps = np.arange(-0.5, N) for i, ax in enumerate(axs): ax.set_facecolor('m') transform = nonaffine_identity + ax.transData if i % 4 >= 2 else ax.transData ax.imshow(data, cmap='Blues', interpolation='hanning' if i % 5 == 4 else 'nearest', origin='upper' if i >= 5 else 'lower', transform=transform) ax.vlines(seps, -0.5, N - 0.5, lw=0.5, color='red', ls=(0, (2, 4))) ax.hlines(seps, -0.5, N - 0.5, lw=0.5, color='red', ls=(0, (2, 4))) for spine in ax.spines: ax.spines[spine].set_linestyle((0, (5, 10))) @image_comparison(['image_bounds_handling.png'], style='_classic_test', tol=0.006) def test_image_bounds_handling(nonaffine_identity): # TODO: The second and third panels in the bottom row show that the handling of # image bounds is bugged for non-affine transforms and non-nearest-neighbor # interpolation. If this bug gets fixed, the baseline image should be updated. fig, axs = plt.subplots(2, 3) N = 11 for j, interpolation in enumerate(['nearest', 'hanning', 'bilinear']): data = np.arange(N**2).reshape((N, N)) data = data / N**2 + (data % 4) / 6 rotation = Affine2D().rotate_around(N/2-0.5, N/2-0.5, 1) for i in range(2): transform = rotation + axs[i, j].transData if i == 1: # Bottom row uses a non-affine transform transform = nonaffine_identity + transform axs[i, j].imshow(data, cmap='Grays', interpolation=interpolation, transform=transform) axs[i, j].set_axis_off() box = Rectangle((-0.5, -0.5), N, N, edgecolor='red', facecolor='none', lw=0.5, ls='dashed', transform=rotation + axs[i, j].transData) axs[i, j].add_artist(box) @image_comparison(['rgba_clean_edges.png'], style='_classic_test', tol=0.003) def test_rgba_clean_edges(): np.random.seed(19680801+9) # same as in test_upsampling() data = np.random.rand(8, 8) data = np.stack([data, data]) data[1, 2:4, 2:4] = np.nan rotation = Affine2D().rotate_around(3.5, 3.5, 1) fig, axs = plt.subplots(1, 2) for i in range(2): # Add background patches to check the fading to non-white colors black = Rectangle((3.75, 2), 5, 5, color='black', zorder=0) gray = Rectangle((0, -2), 3.75, 4, color='gray', zorder=0) partly_black = Rectangle((3.75, -2), 5, 4, fc='black', ec='none', alpha=0.5, zorder=0) axs[i].add_patch(black) axs[i].add_patch(gray) axs[i].add_patch(partly_black) axs[i].imshow(data[i, ...], interpolation='bilinear', interpolation_stage='rgba', transform=rotation + axs[i].transData) axs[i].set_axis_off() axs[i].set_xlim(-2.5, 9.5) axs[i].set_ylim(-2.5, 9.5) @image_comparison(['affine_fill_to_edges.png'], style='_classic_test') def test_affine_fill_to_edges(): # The two rows show the two settings of origin # The three columns show the original and the two mirror flips fig, axs = plt.subplots(2, 3) N = 7 data = np.arange(N**2).reshape((N, N)) % 3 transform = [Affine2D(), Affine2D().translate(0, -N + 1).scale(1, -1), Affine2D().translate(-N + 1, 0).scale(-1, 1)] for j in range(3): for i in range(2): origin = 'upper' if i == 0 else 'lower' axs[i, j].imshow(data, cmap='Grays', interpolation='hanning', origin=origin, transform=transform[j] + axs[i, j].transData) axs[i, j].set_axis_off() axs[i, j].vlines([-0.5, N - 0.5], -1, 2, lw=0.5, color='red') axs[i, j].vlines([-0.5, N - 0.5], N - 3, N, lw=0.5, color='red') axs[i, j].hlines([-0.5, N - 0.5], -1, 2, lw=0.5, color='red') axs[i, j].hlines([-0.5, N - 0.5], N - 3, N, lw=0.5, color='red')