diff --git a/doc/api/next_api_changes/behavior/28061-JMK.rst b/doc/api/next_api_changes/behavior/28061-JMK.rst new file mode 100644 index 000000000000..e3ffb783b394 --- /dev/null +++ b/doc/api/next_api_changes/behavior/28061-JMK.rst @@ -0,0 +1,23 @@ +``imshow`` *interpolation_stage* default changed to 'auto' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *interpolation_stage* parameter of `~.Axes.imshow` has a new default +value 'auto'. For images that are up-sampled less than a factor of +three or down-sampled, image interpolation will occur in 'rgba' space. For images +that are up-sampled by a factor of 3 or more, then image interpolation occurs +in 'data' space. + +The previous default was 'data', so down-sampled images may change subtly with +the new default. However, the new default also avoids floating point artifacts +at sharp boundaries in a colormap when down-sampling. + +The previous behavior can achieved by setting the *interpolation_stage* parameter +or :rc:`image.interpolation_stage` to 'data'. + +imshow default *interpolation* changed to 'auto' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *interpolation* parameter of `~.Axes.imshow` has a new default +value 'auto', changed from 'antialiased', for consistency with *interpolation_stage* +and because the interpolation is only anti-aliasing during down-sampling. Passing +'antialiased' still works, and behaves exactly the same as 'auto', but is discouraged. diff --git a/galleries/examples/images_contours_and_fields/image_antialiasing.py b/galleries/examples/images_contours_and_fields/image_antialiasing.py index 18be4f282b67..7f223f6998f2 100644 --- a/galleries/examples/images_contours_and_fields/image_antialiasing.py +++ b/galleries/examples/images_contours_and_fields/image_antialiasing.py @@ -1,34 +1,29 @@ """ -================== -Image antialiasing -================== - -Images are represented by discrete pixels, either on the screen or in an -image file. When data that makes up the image has a different resolution -than its representation on the screen we will see aliasing effects. How -noticeable these are depends on how much down-sampling takes place in -the change of resolution (if any). - -When subsampling data, aliasing is reduced by smoothing first and then -subsampling the smoothed data. In Matplotlib, we can do that -smoothing before mapping the data to colors, or we can do the smoothing -on the RGB(A) data in the final image. The differences between these are -shown below, and controlled with the *interpolation_stage* keyword argument. - -The default image interpolation in Matplotlib is 'antialiased', and -it is applied to the data. This uses a -hanning interpolation on the data provided by the user for reduced aliasing -in most situations. Only when there is upsampling by a factor of 1, 2 or ->=3 is 'nearest' neighbor interpolation used. - -Other anti-aliasing filters can be specified in `.Axes.imshow` using the -*interpolation* keyword argument. +================ +Image resampling +================ + +Images are represented by discrete pixels assigned color values, either on the +screen or in an image file. When a user calls `~.Axes.imshow` with a data +array, it is rare that the size of the data array exactly matches the number of +pixels allotted to the image in the figure, so Matplotlib resamples or `scales +`_ the data or image to fit. If +the data array is larger than the number of pixels allotted in the rendered figure, +then the image will be "down-sampled" and image information will be lost. +Conversely, if the data array is smaller than the number of output pixels then each +data point will get multiple pixels, and the image is "up-sampled". + +In the following figure, the first data array has size (450, 450), but is +represented by far fewer pixels in the figure, and hence is down-sampled. The +second data array has size (4, 4), and is represented by far more pixels, and +hence is up-sampled. """ import matplotlib.pyplot as plt import numpy as np -# %% +fig, axs = plt.subplots(1, 2, figsize=(4, 2)) + # First we generate a 450x450 pixel image with varying frequency content: N = 450 x = np.arange(N) / N - 0.5 @@ -45,71 +40,214 @@ 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 +alarge = aa + +axs[0].imshow(alarge, cmap='RdBu_r') +axs[0].set_title('(450, 450) Down-sampled', fontsize='medium') + +np.random.seed(19680801+9) +asmall = np.random.rand(4, 4) +axs[1].imshow(asmall, cmap='viridis') +axs[1].set_title('(4, 4) Up-sampled', fontsize='medium') + # %% -# The following images are subsampled from 450 data pixels to either -# 125 pixels or 250 pixels (depending on your display). -# The Moiré patterns in the 'nearest' interpolation are caused by the -# high-frequency data being subsampled. The 'antialiased' imaged -# still has some Moiré patterns as well, but they are greatly reduced. +# Matplotlib's `~.Axes.imshow` method has two keyword arguments to allow the user +# to control how resampling is done. The *interpolation* keyword argument allows +# a choice of the kernel that is used for resampling, allowing either `anti-alias +# `_ filtering if +# down-sampling, or smoothing of pixels if up-sampling. The +# *interpolation_stage* keyword argument, determines if this smoothing kernel is +# applied to the underlying data, or if the kernel is applied to the RGBA pixels. # -# There are substantial differences between the 'data' interpolation and -# the 'rgba' interpolation. The alternating bands of red and blue on the -# left third of the image are subsampled. By interpolating in 'data' space -# (the default) the antialiasing filter makes the stripes close to white, -# because the average of -1 and +1 is zero, and zero is white in this -# colormap. +# ``interpolation_stage='rgba'``: Data -> Normalize -> RGBA -> Interpolate/Resample # -# Conversely, when the anti-aliasing occurs in 'rgba' space, the red and -# blue are combined visually to make purple. This behaviour is more like a -# typical image processing package, but note that purple is not in the -# original colormap, so it is no longer possible to invert individual -# pixels back to their data value. - -fig, axs = plt.subplots(2, 2, figsize=(5, 6), layout='constrained') -axs[0, 0].imshow(a, interpolation='nearest', cmap='RdBu_r') -axs[0, 0].set_xlim(100, 200) -axs[0, 0].set_ylim(275, 175) -axs[0, 0].set_title('Zoom') - -for ax, interp, space in zip(axs.flat[1:], - ['nearest', 'antialiased', 'antialiased'], - ['data', 'data', 'rgba']): - ax.imshow(a, interpolation=interp, interpolation_stage=space, +# ``interpolation_stage='data'``: Data -> Interpolate/Resample -> Normalize -> RGBA +# +# For both keyword arguments, Matplotlib has a default "antialiased", that is +# recommended for most situations, and is described below. Note that this +# default behaves differently if the image is being down- or up-sampled, as +# described below. +# +# Down-sampling and modest up-sampling +# ==================================== +# +# When down-sampling data, we usually want to remove aliasing by smoothing the +# image first and then sub-sampling it. In Matplotlib, we can do that smoothing +# before mapping the data to colors, or we can do the smoothing on the RGB(A) +# image pixels. The differences between these are shown below, and controlled +# with the *interpolation_stage* keyword argument. +# +# The following images are down-sampled from 450 data pixels to approximately +# 125 pixels or 250 pixels (depending on your display). +# The underlying image has alternating +1, -1 stripes on the left side, and +# a varying wavelength (`chirp `_) pattern +# in the rest of the image. If we zoom, we can see this detail without any +# down-sampling: + +fig, ax = plt.subplots(figsize=(4, 4), layout='compressed') +ax.imshow(alarge, interpolation='nearest', cmap='RdBu_r') +ax.set_xlim(100, 200) +ax.set_ylim(275, 175) +ax.set_title('Zoom') + +# %% +# If we down-sample, the simplest algorithm is to decimate the data using +# `nearest-neighbor interpolation +# `_. We can +# do this in either data space or RGBA space: + +fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='compressed') +for ax, interp, space in zip(axs.flat, ['nearest', 'nearest'], + ['data', 'rgba']): + ax.imshow(alarge, interpolation=interp, interpolation_stage=space, cmap='RdBu_r') - ax.set_title(f"interpolation='{interp}'\nspace='{space}'") + ax.set_title(f"interpolation='{interp}'\nstage='{space}'") + +# %% +# Nearest interpolation is identical in data and RGBA space, and both exhibit +# `Moiré `_ patterns because the +# high-frequency data is being down-sampled and shows up as lower frequency +# patterns. We can reduce the Moiré patterns by applying an anti-aliasing filter +# to the image before rendering: + +fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='compressed') +for ax, interp, space in zip(axs.flat, ['hanning', 'hanning'], + ['data', 'rgba']): + ax.imshow(alarge, interpolation=interp, interpolation_stage=space, + cmap='RdBu_r') + ax.set_title(f"interpolation='{interp}'\nstage='{space}'") plt.show() # %% -# Even up-sampling an image with 'nearest' interpolation will lead to Moiré -# patterns when the upsampling factor is not integer. The following image -# upsamples 500 data pixels to 530 rendered pixels. You may note a grid of -# 30 line-like artifacts which stem from the 524 - 500 = 24 extra pixels that -# had to be made up. Since interpolation is 'nearest' they are the same as a -# neighboring line of pixels and thus stretch the image locally so that it -# looks distorted. +# The `Hanning `_ filter smooths +# the underlying data so that each new pixel is a weighted average of the +# original underlying pixels. This greatly reduces the Moiré patterns. +# However, when the *interpolation_stage* is set to 'data', it also introduces +# white regions to the image that are not in the original data, both in the +# alternating bands on the left hand side of the image, and in the boundary +# between the red and blue of the large circles in the middle of the image. +# The interpolation at the 'rgba' stage has a different artifact, with the alternating +# bands coming out a shade of purple; even though purple is not in the original +# colormap, it is what we perceive when a blue and red stripe are close to each +# other. +# +# The default for the *interpolation* keyword argument is 'auto' which +# will choose a Hanning filter if the image is being down-sampled or up-sampled +# by less than a factor of three. The default *interpolation_stage* keyword +# argument is also 'auto', and for images that are down-sampled or +# up-sampled by less than a factor of three it defaults to 'rgba' +# interpolation. +# +# Anti-aliasing filtering is needed, even when up-sampling. The following image +# up-samples 450 data pixels to 530 rendered pixels. You may note a grid of +# line-like artifacts which stem from the extra pixels that had to be made up. +# Since interpolation is 'nearest' they are the same as a neighboring line of +# pixels and thus stretch the image locally so that it looks distorted. + fig, ax = plt.subplots(figsize=(6.8, 6.8)) -ax.imshow(a, interpolation='nearest', cmap='gray') -ax.set_title("upsampled by factor a 1.048, interpolation='nearest'") -plt.show() +ax.imshow(alarge, interpolation='nearest', cmap='grey') +ax.set_title("up-sampled by factor a 1.17, interpolation='nearest'") # %% -# Better antialiasing algorithms can reduce this effect: +# Better anti-aliasing algorithms can reduce this effect: fig, ax = plt.subplots(figsize=(6.8, 6.8)) -ax.imshow(a, interpolation='antialiased', cmap='gray') -ax.set_title("upsampled by factor a 1.048, interpolation='antialiased'") -plt.show() +ax.imshow(alarge, interpolation='auto', cmap='grey') +ax.set_title("up-sampled by factor a 1.17, interpolation='auto'") # %% -# Apart from the default 'hanning' antialiasing, `~.Axes.imshow` supports a +# Apart from the default 'hanning' anti-aliasing, `~.Axes.imshow` supports a # number of different interpolation algorithms, which may work better or -# worse depending on the pattern. +# worse depending on the underlying data. fig, axs = plt.subplots(1, 2, figsize=(7, 4), layout='constrained') for ax, interp in zip(axs, ['hanning', 'lanczos']): - ax.imshow(a, interpolation=interp, cmap='gray') + ax.imshow(alarge, interpolation=interp, cmap='gray') ax.set_title(f"interpolation='{interp}'") + +# %% +# A final example shows the desirability of performing the anti-aliasing at the +# RGBA stage when using non-trivial interpolation kernels. In the following, +# the data in the upper 100 rows is exactly 0.0, and data in the inner circle +# is exactly 2.0. If we perform the *interpolation_stage* in 'data' space and +# use an anti-aliasing filter (first panel), then floating point imprecision +# makes some of the data values just a bit less than zero or a bit more than +# 2.0, and they get assigned the under- or over- colors. This can be avoided if +# you do not use an anti-aliasing filter (*interpolation* set set to +# 'nearest'), however, that makes the part of the data susceptible to Moiré +# patterns much worse (second panel). Therefore, we recommend the default +# *interpolation* of 'hanning'/'auto', and *interpolation_stage* of +# 'rgba'/'auto' for most down-sampling situations (last panel). + +a = alarge + 1 +cmap = plt.get_cmap('RdBu_r') +cmap.set_under('yellow') +cmap.set_over('limegreen') + +fig, axs = plt.subplots(1, 3, figsize=(7, 3), layout='constrained') +for ax, interp, space in zip(axs.flat, + ['hanning', 'nearest', 'hanning', ], + ['data', 'data', 'rgba']): + im = ax.imshow(a, interpolation=interp, interpolation_stage=space, + cmap=cmap, vmin=0, vmax=2) + title = f"interpolation='{interp}'\nstage='{space}'" + if ax == axs[2]: + title += '\nDefault' + ax.set_title(title, fontsize='medium') +fig.colorbar(im, ax=axs, extend='both', shrink=0.8) + +# %% +# Up-sampling +# =========== +# +# If we up-sample, then we can represent a data pixel by many image or screen pixels. +# In the following example, we greatly over-sample the small data matrix. + +np.random.seed(19680801+9) +a = np.random.rand(4, 4) + +fig, axs = plt.subplots(1, 2, figsize=(6.5, 4), layout='compressed') +axs[0].imshow(asmall, cmap='viridis') +axs[0].set_title("interpolation='auto'\nstage='auto'") +axs[1].imshow(asmall, cmap='viridis', interpolation="nearest", + interpolation_stage="data") +axs[1].set_title("interpolation='nearest'\nstage='data'") plt.show() +# %% +# The *interpolation* keyword argument can be used to smooth the pixels if desired. +# However, that almost always is better done in data space, rather than in RGBA space +# where the filters can cause colors that are not in the colormap to be the result of +# the interpolation. In the following example, note that when the interpolation is +# 'rgba' there are red colors as interpolation artifacts. Therefore, the default +# 'auto' choice for *interpolation_stage* is set to be the same as 'data' +# when up-sampling is greater than a factor of three: + +fig, axs = plt.subplots(1, 2, figsize=(6.5, 4), layout='compressed') +im = axs[0].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='data') +axs[0].set_title("interpolation='sinc'\nstage='data'\n(default for upsampling)") +axs[1].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='rgba') +axs[1].set_title("interpolation='sinc'\nstage='rgba'") +fig.colorbar(im, ax=axs, shrink=0.7, extend='both') + +# %% +# Avoiding resampling +# =================== +# +# It is possible to avoid resampling data when making an image. One method is +# to simply save to a vector backend (pdf, eps, svg) and use +# ``interpolation='none'``. Vector backends allow embedded images, however be +# aware that some vector image viewers may smooth image pixels. +# +# The second method is to exactly match the size of your axes to the size of +# your data. The following figure is exactly 2 inches by 2 inches, and +# if the dpi is 200, then the 400x400 data is not resampled at all. If you download +# this image and zoom in an image viewer you should see the individual stripes +# on the left hand side (note that if you have a non hiDPI or "retina" screen, the html +# may serve a 100x100 version of the image, which will be downsampled.) + +fig = plt.figure(figsize=(2, 2)) +ax = fig.add_axes([0, 0, 1, 1]) +ax.imshow(aa[:400, :400], cmap='RdBu_r', interpolation='nearest') +plt.show() # %% # # .. admonition:: References diff --git a/galleries/examples/images_contours_and_fields/interpolation_methods.py b/galleries/examples/images_contours_and_fields/interpolation_methods.py index 496b39c56b9f..dea1b474801c 100644 --- a/galleries/examples/images_contours_and_fields/interpolation_methods.py +++ b/galleries/examples/images_contours_and_fields/interpolation_methods.py @@ -8,14 +8,14 @@ If *interpolation* is None, it defaults to the :rc:`image.interpolation`. If the interpolation is ``'none'``, then no interpolation is performed for the -Agg, ps and pdf backends. Other backends will default to ``'antialiased'``. +Agg, ps and pdf backends. Other backends will default to ``'auto'``. For the Agg, ps and pdf backends, ``interpolation='none'`` works well when a big image is scaled down, while ``interpolation='nearest'`` works well when a small image is scaled up. See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for a -discussion on the default ``interpolation='antialiased'`` option. +discussion on the default ``interpolation='auto'`` option. """ import matplotlib.pyplot as plt diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 5e69bcb57d7f..5d5248951314 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5799,7 +5799,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, interpolation : str, default: :rc:`image.interpolation` The interpolation method used. - Supported values are 'none', 'antialiased', 'nearest', 'bilinear', + Supported values are 'none', 'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman'. @@ -5814,7 +5814,7 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, pdf, and svg viewers may display these raw pixels differently. On other backends, 'none' is the same as 'nearest'. - If *interpolation* is the default 'antialiased', then 'nearest' + If *interpolation* is the default 'auto', then 'nearest' interpolation is used if the image is upsampled by more than a factor of three (i.e. the number of display pixels is at least three times the size of the data array). If the upsampling rate is @@ -5832,11 +5832,20 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None, which can be set by *filterrad*. Additionally, the antigrain image resize filter is controlled by the parameter *filternorm*. - interpolation_stage : {'data', 'rgba'}, default: 'data' - If 'data', interpolation - is carried out on the data provided by the user. If 'rgba', the - interpolation is carried out after the colormapping has been - applied (visual interpolation). + interpolation_stage : {'auto', 'data', 'rgba'}, default: 'auto' + Supported values: + + - 'data': Interpolation is carried out on the data provided by the user + This is useful if interpolating between pixels during upsampling. + - 'rgba': The interpolation is carried out in RGBA-space after the + color-mapping has been applied. This is useful if downsampling and + combining pixels visually. + - 'auto': Select a suitable interpolation stage automatically. This uses + 'rgba' when downsampling, or upsampling at a rate less than 3, and + 'data' when upsampling at a higher rate. + + See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for + a discussion of image antialiasing. alpha : float or array-like, optional The alpha blending value, between 0 (transparent) and 1 (opaque). diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index d04e3ad99ddc..732134850c2b 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -492,7 +492,7 @@ class Axes(_AxesBase): vmax: float | None = ..., origin: Literal["upper", "lower"] | None = ..., extent: tuple[float, float, float, float] | None = ..., - interpolation_stage: Literal["data", "rgba"] | None = ..., + interpolation_stage: Literal["data", "rgba", "auto"] | None = ..., filternorm: bool = ..., filterrad: float = ..., resample: bool | None = ..., diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 61b22cf519c7..366b680e5393 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -31,7 +31,7 @@ # map interpolation strings to module constants _interpd_ = { - 'antialiased': _image.NEAREST, # this will use nearest or Hanning... + 'auto': _image.NEAREST, # this will use nearest or Hanning... 'none': _image.NEAREST, # fall back to nearest when not supported 'nearest': _image.NEAREST, 'bilinear': _image.BILINEAR, @@ -50,6 +50,7 @@ 'sinc': _image.SINC, 'lanczos': _image.LANCZOS, 'blackman': _image.BLACKMAN, + 'antialiased': _image.NEAREST, # this will use nearest or Hanning... } interpolations_names = set(_interpd_) @@ -186,7 +187,7 @@ def _resample( # compare the number of displayed pixels to the number of # the data pixels. interpolation = image_obj.get_interpolation() - if interpolation == 'antialiased': + if interpolation in ['antialiased', 'auto']: # don't antialias if upsampling by an integer number or # if zooming in more than a factor of 3 pos = np.array([[0, 0], [data.shape[1], data.shape[0]]]) @@ -421,7 +422,21 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if not unsampled: if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)): raise ValueError(f"Invalid shape {A.shape} for image data") - if A.ndim == 2 and self._interpolation_stage != 'rgba': + + # if antialiased, this needs to change as window sizes + # change: + interpolation_stage = self._interpolation_stage + if interpolation_stage in ['antialiased', 'auto']: + pos = np.array([[0, 0], [A.shape[1], A.shape[0]]]) + disp = t.transform(pos) + dispx = np.abs(np.diff(disp[:, 0])) / A.shape[1] + dispy = np.abs(np.diff(disp[:, 1])) / A.shape[0] + if (dispx < 3) or (dispy < 3): + interpolation_stage = 'rgba' + else: + interpolation_stage = 'data' + + if A.ndim == 2 and interpolation_stage == 'data': # if we are a 2D array, then we are running through the # norm + colormap transformation. However, in general the # input data is not going to match the size on the screen so we @@ -550,7 +565,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, cbook._setattr_cm(self.norm, vmin=s_vmin, vmax=s_vmax): output = self.norm(resampled_masked) else: - if A.ndim == 2: # _interpolation_stage == 'rgba' + if A.ndim == 2: # interpolation_stage = 'rgba' self.norm.autoscale_None(A) A = self.to_rgba(A) alpha = self._get_scalar_alpha() @@ -744,9 +759,9 @@ def get_interpolation(self): """ Return the interpolation method the image uses when resizing. - One of 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', - 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', - 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', + One of 'auto', 'antialiased', 'nearest', 'bilinear', 'bicubic', + 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', + 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', or 'none'. """ return self._interpolation @@ -762,7 +777,7 @@ def set_interpolation(self, s): Parameters ---------- - s : {'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', \ + s : {'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', \ 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', \ 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'none'} or None """ @@ -785,12 +800,14 @@ def set_interpolation_stage(self, s): Parameters ---------- - s : {'data', 'rgba'} or None + s : {'data', 'rgba', 'auto'} or None Whether to apply up/downsampling interpolation in data or RGBA space. If None, use :rc:`image.interpolation_stage`. + If 'auto' we will check upsampling rate and if less + than 3 then use 'rgba', otherwise use 'data'. """ s = mpl._val_or_rc(s, 'image.interpolation_stage') - _api.check_in_list(['data', 'rgba'], s=s) + _api.check_in_list(['data', 'rgba', 'auto'], s=s) self._interpolation_stage = s self.stale = True @@ -870,7 +887,7 @@ class AxesImage(_ImageBase): norm : str or `~matplotlib.colors.Normalize` Maps luminance to 0-1. interpolation : str, default: :rc:`image.interpolation` - Supported values are 'none', 'antialiased', 'nearest', 'bilinear', + Supported values are 'none', 'auto', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman'. diff --git a/lib/matplotlib/image.pyi b/lib/matplotlib/image.pyi index 4b684f693845..f4a90ed94386 100644 --- a/lib/matplotlib/image.pyi +++ b/lib/matplotlib/image.pyi @@ -73,7 +73,7 @@ class _ImageBase(martist.Artist, cm.ScalarMappable): filterrad: float = ..., resample: bool | None = ..., *, - interpolation_stage: Literal["data", "rgba"] | None = ..., + interpolation_stage: Literal["data", "rgba", "auto"] | None = ..., **kwargs ) -> None: ... def get_size(self) -> tuple[int, int]: ... @@ -89,8 +89,8 @@ class _ImageBase(martist.Artist, cm.ScalarMappable): def get_shape(self) -> tuple[int, int, int]: ... def get_interpolation(self) -> str: ... def set_interpolation(self, s: str | None) -> None: ... - def get_interpolation_stage(self) -> Literal["data", "rgba"]: ... - def set_interpolation_stage(self, s: Literal["data", "rgba"]) -> None: ... + def get_interpolation_stage(self) -> Literal["data", "rgba", "auto"]: ... + def set_interpolation_stage(self, s: Literal["data", "rgba", "auto"]) -> None: ... def can_composite(self) -> bool: ... def set_resample(self, v: bool | None) -> None: ... def get_resample(self) -> bool: ... @@ -112,7 +112,7 @@ class AxesImage(_ImageBase): filternorm: bool = ..., filterrad: float = ..., resample: bool = ..., - interpolation_stage: Literal["data", "rgba"] | None = ..., + interpolation_stage: Literal["data", "rgba", "auto"] | None = ..., **kwargs ) -> None: ... def get_window_extent(self, renderer: RendererBase | None = ...) -> Bbox: ... diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 29ffb20f4280..411e14387b6e 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -602,8 +602,8 @@ ## * IMAGES * ## *************************************************************************** #image.aspect: equal # {equal, auto} or a number -#image.interpolation: antialiased # see help(imshow) for options -#image.interpolation_stage: data # see help(imshow) for options +#image.interpolation: auto # see help(imshow) for options +#image.interpolation_stage: auto # see help(imshow) for options #image.cmap: viridis # A colormap name (plasma, magma, etc.) #image.lut: 256 # the size of the colormap lookup table #image.origin: upper # {lower, upper} @@ -671,7 +671,7 @@ # to the nearest pixel when certain criteria are met. # When False, paths will never be snapped. #path.sketch: None # May be None, or a tuple of the form: - # path.sketch: (scale, length, randomness) + # path.sketch: (scale, length, randomness) # - *scale* is the amplitude of the wiggle # perpendicular to the line (in pixels). # - *length* is the length of the wiggle along the diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 441af598dbc6..e6242271d113 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3565,7 +3565,7 @@ def imshow( vmax: float | None = None, origin: Literal["upper", "lower"] | None = None, extent: tuple[float, float, float, float] | None = None, - interpolation_stage: Literal["data", "rgba"] | None = None, + interpolation_stage: Literal["data", "rgba", "auto"] | None = None, filternorm: bool = True, filterrad: float = 4.0, resample: bool | None = None, diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b0cd22098489..b617261fb9cd 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1053,7 +1053,7 @@ def _convert_validator_spec(key, conv): "image.aspect": validate_aspect, # equal, auto, a number "image.interpolation": validate_string, - "image.interpolation_stage": ["data", "rgba"], + "image.interpolation_stage": ["auto", "data", "rgba"], "image.cmap": _validate_cmap, # gray, jet, etc. "image.lut": validate_int, # lookup table "image.origin": ["upper", "lower"], diff --git a/lib/matplotlib/tests/baseline_images/test_image/downsampling.png b/lib/matplotlib/tests/baseline_images/test_image/downsampling.png new file mode 100644 index 000000000000..4e68e52d787b Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/downsampling.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/downsampling_speckle.png b/lib/matplotlib/tests/baseline_images/test_image/downsampling_speckle.png new file mode 100644 index 000000000000..eb8b3ce13013 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/downsampling_speckle.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png index f416faa96d5f..5ff776ad3de5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png and b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/upsampling.png b/lib/matplotlib/tests/baseline_images/test_image/upsampling.png new file mode 100644 index 000000000000..281dae56a30e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/upsampling.png differ diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 8d7970078efa..4340be96a38b 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -98,7 +98,7 @@ def test_imshow_antialiased(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='antialiased') + ax.imshow(A, interpolation='auto') ax = fig_ref.subplots() ax.set_position([0, 0, 1, 1]) ax.imshow(A, interpolation=interpolation) @@ -113,7 +113,7 @@ def test_imshow_zoom(fig_test, fig_ref): for fig in [fig_test, fig_ref]: fig.set_size_inches(2.9, 2.9) ax = fig_test.subplots() - ax.imshow(A, interpolation='antialiased') + ax.imshow(A, interpolation='auto') ax.set_xlim([10, 20]) ax.set_ylim([10, 20]) ax = fig_ref.subplots() @@ -853,8 +853,6 @@ def test_image_preserve_size2(): @image_comparison(['mask_image_over_under.png'], remove_text=True, tol=1.0) def test_mask_image_over_under(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False delta = 0.025 x = y = np.arange(-3.0, 3.0, delta) @@ -953,6 +951,7 @@ def test_imshow_masked_interpolation(): 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()): @@ -1406,8 +1405,7 @@ def test_nonuniform_logscale(): @image_comparison( - ['rgba_antialias.png'], style='mpl20', remove_text=True, - tol=0 if platform.machine() == 'x86_64' else 0.007) + ['rgba_antialias.png'], style='mpl20', remove_text=True, tol=0.01) def test_rgba_antialias(): fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False, sharey=False, constrained_layout=True) @@ -1452,15 +1450,54 @@ def test_rgba_antialias(): # data antialias: Note no purples, and white in circle. Note # that alternating red and blue stripes become white. - axs[2].imshow(aa, interpolation='antialiased', interpolation_stage='data', + 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='antialiased', interpolation_stage='rgba', + axs[3].imshow(aa, interpolation='auto', interpolation_stage='rgba', cmap=cmap, vmin=-1.2, vmax=1.2) +@check_figures_equal(extensions=('png', )) +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(extensions=('png', )) +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(4000, 4000) + 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}): @@ -1578,6 +1615,87 @@ def test_non_transdata_image_does_not_touch_aspect(): assert ax.get_aspect() == 2 +@image_comparison( + ['downsampling.png'], style='mpl20', remove_text=True, tol=0.09) +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, tol=0.09) +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") + cm.set_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'), diff --git a/lib/matplotlib/tests/test_png.py b/lib/matplotlib/tests/test_png.py index 066eb01c3ae6..aa7591508a67 100644 --- a/lib/matplotlib/tests/test_png.py +++ b/lib/matplotlib/tests/test_png.py @@ -20,7 +20,10 @@ def test_pngsuite(): if data.ndim == 2: # keep grayscale images gray cmap = cm.gray - plt.imshow(data, extent=(i, i + 1, 0, 1), cmap=cmap) + # Using the old default data interpolation stage lets us + # continue to use the existing reference image + plt.imshow(data, extent=(i, i + 1, 0, 1), cmap=cmap, + interpolation_stage='data') plt.gca().patch.set_facecolor("#ddffff") plt.gca().set_xlim(0, len(files))