Skip to content

Commit 3ceba40

Browse files
jklymaktacaswell
andcommitted
API: imshow make rgba the defaut stage when down-sampling
imshow used to default to interpolating in data space. That makes sense for up-sampled images, but fails in odd ways for down-sampled images. Here we introduce a new default value for *interpolation_stage* 'antialiased', which changes the interpolation stage to 'rgba' if the data is downsampled or upsampled less than a factor of three. Apply suggestions from code review Co-authored-by: Thomas A Caswell <tcaswell@gmail.com>
1 parent 0fd212d commit 3ceba40

File tree

12 files changed

+380
-88
lines changed

12 files changed

+380
-88
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
imshow *interpolation_stage* default changed to 'antialiased'
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The *interpolation_stage* keyword argument `~.Axes.imshow` has a new default
5+
value 'antialiased'. For images that are up-sampled less than a factor of
6+
three or down-sampled , image interpolation will occur in 'rgba' space. For images
7+
that are up-sampled by more than a factor of 3, then image interpolation occurs
8+
in 'data' space.
9+
10+
The previous default was 'data', so down-sampled images may change subtly with
11+
the new default. However, the new default also avoids floating point artifacts
12+
at sharp boundaries in a colormap when down-sampling.
13+
14+
The previous behavior can achieved by changing :rc:`image.interpolation_stage`.

galleries/examples/images_contours_and_fields/image_antialiasing.py

Lines changed: 209 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,29 @@
11
"""
2-
==================
3-
Image antialiasing
4-
==================
5-
6-
Images are represented by discrete pixels, either on the screen or in an
7-
image file. When data that makes up the image has a different resolution
8-
than its representation on the screen we will see aliasing effects. How
9-
noticeable these are depends on how much down-sampling takes place in
10-
the change of resolution (if any).
11-
12-
When subsampling data, aliasing is reduced by smoothing first and then
13-
subsampling the smoothed data. In Matplotlib, we can do that
14-
smoothing before mapping the data to colors, or we can do the smoothing
15-
on the RGB(A) data in the final image. The differences between these are
16-
shown below, and controlled with the *interpolation_stage* keyword argument.
17-
18-
The default image interpolation in Matplotlib is 'antialiased', and
19-
it is applied to the data. This uses a
20-
hanning interpolation on the data provided by the user for reduced aliasing
21-
in most situations. Only when there is upsampling by a factor of 1, 2 or
22-
>=3 is 'nearest' neighbor interpolation used.
23-
24-
Other anti-aliasing filters can be specified in `.Axes.imshow` using the
25-
*interpolation* keyword argument.
2+
================
3+
Image resampling
4+
================
5+
6+
Images are represented by discrete pixels assigned color values, either on the
7+
screen or in an image file. When a user calls `~.Axes.imshow` with a data
8+
array, it is rare that the size of the data array exactly matches the number of
9+
pixels allotted to the image in the figure, so Matplotlib resamples or `scales
10+
<https://en.wikipedia.org/wiki/Image_scaling>`_ the data or image to fit. If
11+
the data array is larger than the number of pixels allotted in the rendered figure,
12+
then the image will be "down-sampled" and image information will be lost.
13+
Conversely, if the data array is smaller than the number of output pixels then each
14+
data point will get multiple pixels, and the image is "up-sampled".
15+
16+
In the following figure, the first data array has size (450, 450), but is
17+
represented by far fewer pixels in the figure, and hence is down-sampled. The
18+
second data array has size (4, 4), and is represented by far more pixels, and
19+
hence is up-sampled.
2620
"""
2721

2822
import matplotlib.pyplot as plt
2923
import numpy as np
3024

31-
# %%
25+
fig, axs = plt.subplots(1, 2, figsize=(4, 2))
26+
3227
# First we generate a 450x450 pixel image with varying frequency content:
3328
N = 450
3429
x = np.arange(N) / N - 0.5
@@ -45,71 +40,214 @@
4540
a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1
4641
a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1
4742
aa[:, int(N / 3):] = a[:, int(N / 3):]
48-
a = aa
43+
alarge = aa
44+
45+
axs[0].imshow(alarge, cmap='RdBu_r')
46+
axs[0].set_title('(450, 450) Down-sampled', fontsize='medium')
47+
48+
np.random.seed(19680801+9)
49+
asmall = np.random.rand(4, 4)
50+
axs[1].imshow(asmall, cmap='viridis')
51+
axs[1].set_title('(4, 4) Up-sampled', fontsize='medium')
52+
4953
# %%
50-
# The following images are subsampled from 450 data pixels to either
51-
# 125 pixels or 250 pixels (depending on your display).
52-
# The Moiré patterns in the 'nearest' interpolation are caused by the
53-
# high-frequency data being subsampled. The 'antialiased' imaged
54-
# still has some Moiré patterns as well, but they are greatly reduced.
54+
# Matplotlib's `~.Axes.imshow` method has two keyword arguments to allow the user
55+
# to control how resampling is done. The *interpolation* keyword argument allows
56+
# a choice of the kernel that is used for resampling, allowing either `anti-alias
57+
# <https://en.wikipedia.org/wiki/Anti-aliasing_filter>`_ filtering if
58+
# down-sampling, or smoothing of pixels if up-sampling. The
59+
# *interpolation_stage* keyword argument, determines if this smoothing kernel is
60+
# applied to the underlying data, or if the kernel is applied to the RGBA pixels.
5561
#
56-
# There are substantial differences between the 'data' interpolation and
57-
# the 'rgba' interpolation. The alternating bands of red and blue on the
58-
# left third of the image are subsampled. By interpolating in 'data' space
59-
# (the default) the antialiasing filter makes the stripes close to white,
60-
# because the average of -1 and +1 is zero, and zero is white in this
61-
# colormap.
62+
# ``interpolation_stage='rgba'``: Data -> Normalize -> RGBA -> Interpolate/Resample
6263
#
63-
# Conversely, when the anti-aliasing occurs in 'rgba' space, the red and
64-
# blue are combined visually to make purple. This behaviour is more like a
65-
# typical image processing package, but note that purple is not in the
66-
# original colormap, so it is no longer possible to invert individual
67-
# pixels back to their data value.
68-
69-
fig, axs = plt.subplots(2, 2, figsize=(5, 6), layout='constrained')
70-
axs[0, 0].imshow(a, interpolation='nearest', cmap='RdBu_r')
71-
axs[0, 0].set_xlim(100, 200)
72-
axs[0, 0].set_ylim(275, 175)
73-
axs[0, 0].set_title('Zoom')
74-
75-
for ax, interp, space in zip(axs.flat[1:],
76-
['nearest', 'antialiased', 'antialiased'],
77-
['data', 'data', 'rgba']):
78-
ax.imshow(a, interpolation=interp, interpolation_stage=space,
64+
# ``interpolation_stage='data'``: Data -> Interpolate/Resample -> Normalize -> RGBA
65+
#
66+
# For both keyword arguments, Matplotlib has a default "antialiased", that is
67+
# recommended for most situations, and is described below. Note that this
68+
# default behaves differently if the image is being down- or up-sampled, as
69+
# described below.
70+
#
71+
# Down-sampling and modest up-sampling
72+
# ====================================
73+
#
74+
# When down-sampling data, we usually want to remove aliasing by smoothing the
75+
# image first and then sub-sampling it. In Matplotlib, we can do that smoothing
76+
# before mapping the data to colors, or we can do the smoothing on the RGB(A)
77+
# image pixels. The differences between these are shown below, and controlled
78+
# with the *interpolation_stage* keyword argument.
79+
#
80+
# The following images are down-sampled from 450 data pixels to approximately
81+
# 125 pixels or 250 pixels (depending on your display).
82+
# The underlying image has alternating +1, -1 stripes on the left side, and
83+
# a varying wavelength (`chirp <https://en.wikipedia.org/wiki/Chirp>`_) pattern
84+
# in the rest of the image. If we zoom, we can see this detail without any
85+
# down-sampling:
86+
87+
fig, ax = plt.subplots(figsize=(4, 4), layout='compressed')
88+
ax.imshow(alarge, interpolation='nearest', cmap='RdBu_r')
89+
ax.set_xlim(100, 200)
90+
ax.set_ylim(275, 175)
91+
ax.set_title('Zoom')
92+
93+
# %%
94+
# If we down-sample, the simplest algorithm is to decimate the data using
95+
# `nearest-neighbor interpolation
96+
# <https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation>`_. We can
97+
# do this in either data space or RGBA space:
98+
99+
fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='compressed')
100+
for ax, interp, space in zip(axs.flat, ['nearest', 'nearest'],
101+
['data', 'rgba']):
102+
ax.imshow(alarge, interpolation=interp, interpolation_stage=space,
79103
cmap='RdBu_r')
80-
ax.set_title(f"interpolation='{interp}'\nspace='{space}'")
104+
ax.set_title(f"interpolation='{interp}'\nstage='{space}'")
105+
106+
# %%
107+
# Nearest interpolation is identical in data and RGBA space, and both exhibit
108+
# `Moiré <https://en.wikipedia.org/wiki/Moiré_pattern>`_ patterns because the
109+
# high-frequency data is being down-sampled and shows up as lower frequency
110+
# patterns. We can reduce the Moiré patterns by applying an anti-aliasing filter
111+
# to the image before rendering:
112+
113+
fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='compressed')
114+
for ax, interp, space in zip(axs.flat, ['hanning', 'hanning'],
115+
['data', 'rgba']):
116+
ax.imshow(alarge, interpolation=interp, interpolation_stage=space,
117+
cmap='RdBu_r')
118+
ax.set_title(f"interpolation='{interp}'\nstage='{space}'")
81119
plt.show()
82120

83121
# %%
84-
# Even up-sampling an image with 'nearest' interpolation will lead to Moiré
85-
# patterns when the upsampling factor is not integer. The following image
86-
# upsamples 500 data pixels to 530 rendered pixels. You may note a grid of
87-
# 30 line-like artifacts which stem from the 524 - 500 = 24 extra pixels that
88-
# had to be made up. Since interpolation is 'nearest' they are the same as a
89-
# neighboring line of pixels and thus stretch the image locally so that it
90-
# looks distorted.
122+
# The `Hanning <https://en.wikipedia.org/wiki/Hann_function>`_ filter smooths
123+
# the underlying data so that each new pixel is a weighted average of the
124+
# original underlying pixels. This greatly reduces the Moiré patterns.
125+
# However, when the *interpolation_stage* is set to 'data', it also introduces
126+
# white regions to the image that are not in the original data, both in the
127+
# alternating bands on the left hand side of the image, and in the boundary
128+
# between the red and blue of the large circles in the middle of the image.
129+
# The interpolation at the 'rgba' stage has a different artifact, with the alternating
130+
# bands coming out a shade of purple; even though purple is not in the original
131+
# colormap, it is what we perceive when a blue and red stripe are close to each
132+
# other.
133+
#
134+
# The default for the *interpolation* keyword argument is 'antialiased' which
135+
# will choose a Hanning filter if the image is being down-sampled or up-sampled
136+
# by less than a factor of three. The default *interpolation_stage* keyword
137+
# argument is also 'antialiased', and for images that are down-sampled or
138+
# up-sampled by less than a factor of three it defaults to 'rgba'
139+
# interpolation.
140+
#
141+
# Anti-aliasing filtering is needed, even when up-sampling. The following image
142+
# up-samples 450 data pixels to 530 rendered pixels. You may note a grid of
143+
# line-like artifacts which stem from the extra pixels that had to be made up.
144+
# Since interpolation is 'nearest' they are the same as a neighboring line of
145+
# pixels and thus stretch the image locally so that it looks distorted.
146+
91147
fig, ax = plt.subplots(figsize=(6.8, 6.8))
92-
ax.imshow(a, interpolation='nearest', cmap='gray')
93-
ax.set_title("upsampled by factor a 1.048, interpolation='nearest'")
94-
plt.show()
148+
ax.imshow(alarge, interpolation='nearest', cmap='grey')
149+
ax.set_title("up-sampled by factor a 1.17, interpolation='nearest'")
95150

96151
# %%
97-
# Better antialiasing algorithms can reduce this effect:
152+
# Better anti-aliasing algorithms can reduce this effect:
98153
fig, ax = plt.subplots(figsize=(6.8, 6.8))
99-
ax.imshow(a, interpolation='antialiased', cmap='gray')
100-
ax.set_title("upsampled by factor a 1.048, interpolation='antialiased'")
101-
plt.show()
154+
ax.imshow(alarge, interpolation='antialiased', cmap='grey')
155+
ax.set_title("up-sampled by factor a 1.17, interpolation='antialiased'")
102156

103157
# %%
104-
# Apart from the default 'hanning' antialiasing, `~.Axes.imshow` supports a
158+
# Apart from the default 'hanning' anti-aliasing, `~.Axes.imshow` supports a
105159
# number of different interpolation algorithms, which may work better or
106-
# worse depending on the pattern.
160+
# worse depending on the underlying data.
107161
fig, axs = plt.subplots(1, 2, figsize=(7, 4), layout='constrained')
108162
for ax, interp in zip(axs, ['hanning', 'lanczos']):
109-
ax.imshow(a, interpolation=interp, cmap='gray')
163+
ax.imshow(alarge, interpolation=interp, cmap='gray')
110164
ax.set_title(f"interpolation='{interp}'")
165+
166+
# %%
167+
# A final example shows the desirability of performing the anti-aliasing at the
168+
# RGBA stage when using non-trivial interpolation kernels. In the following,
169+
# the data in the upper 100 rows is exactly 0.0, and data in the inner circle
170+
# is exactly 2.0. If we perform the *interpolation_stage* in 'data' space and
171+
# use an anti-aliasing filter (first panel), then floating point imprecision
172+
# makes some of the data values just a bit less than zero or a bit more than
173+
# 2.0, and they get assigned the under- or over- colors. This can be avoided if
174+
# you do not use an anti-aliasing filter (*interpolation* set set to
175+
# 'nearest'), however, that makes the part of the data susceptible to Moiré
176+
# patterns much worse (second panel). Therefore, we recommend the default
177+
# *interpolation* of 'hanning'/'antialiased', and *interpolation_stage* of
178+
# 'rgba'/'antialiased' for most down-sampling situations (last panel).
179+
180+
a = alarge + 1
181+
cmap = plt.get_cmap('RdBu_r')
182+
cmap.set_under('yellow')
183+
cmap.set_over('limegreen')
184+
185+
fig, axs = plt.subplots(1, 3, figsize=(7, 3), layout='constrained')
186+
for ax, interp, space in zip(axs.flat,
187+
['hanning', 'nearest', 'hanning', ],
188+
['data', 'data', 'rgba']):
189+
im = ax.imshow(a, interpolation=interp, interpolation_stage=space,
190+
cmap=cmap, vmin=0, vmax=2)
191+
title = f"interpolation='{interp}'\nstage='{space}'"
192+
if ax == axs[2]:
193+
title += '\nDefault'
194+
ax.set_title(title, fontsize='medium')
195+
fig.colorbar(im, ax=axs, extend='both', shrink=0.8)
196+
197+
# %%
198+
# Up-sampling
199+
# ===========
200+
#
201+
# If we up-sample, then we can represent a data pixel by many image or screen pixels.
202+
# In the following example, we greatly over-sample the small data matrix.
203+
204+
np.random.seed(19680801+9)
205+
a = np.random.rand(4, 4)
206+
207+
fig, axs = plt.subplots(1, 2, figsize=(6.5, 4), layout='compressed')
208+
axs[0].imshow(asmall, cmap='viridis')
209+
axs[0].set_title("interpolation='antialiased'\nstage='antialiased'")
210+
axs[1].imshow(asmall, cmap='viridis', interpolation="nearest",
211+
interpolation_stage="data")
212+
axs[1].set_title("interpolation='nearest'\nstage='data'")
111213
plt.show()
112214

215+
# %%
216+
# The *interpolation* keyword argument can be used to smooth the pixels if desired.
217+
# However, that almost always is better done in data space, rather than in RGBA space
218+
# where the filters can cause colors that are not in the colormap to be the result of
219+
# the interpolation. In the following example, note that when the interpolation is
220+
# 'rgba' there are red colors as interpolation artifacts. Therefore, the default
221+
# 'antialiased' choice for *interpolation_stage* is set to be the same as 'data'
222+
# when up-sampling is greater than a factor of three:
223+
224+
fig, axs = plt.subplots(1, 2, figsize=(6.5, 4), layout='compressed')
225+
im = axs[0].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='data')
226+
axs[0].set_title("interpolation='sinc'\nstage='data'\n(default for upsampling)")
227+
axs[1].imshow(a, cmap='viridis', interpolation='sinc', interpolation_stage='rgba')
228+
axs[1].set_title("interpolation='sinc'\nstage='rgba'")
229+
fig.colorbar(im, ax=axs, shrink=0.7, extend='both')
230+
231+
# %%
232+
# Avoiding resampling
233+
# ===================
234+
#
235+
# It is possible to avoid resampling data when making an image. One method is
236+
# to simply save to a vector backend (pdf, eps, svg) and use
237+
# ``interpolation='none'``. Vector backends allow embedded images, however be
238+
# aware that some vector image viewers may smooth image pixels.
239+
#
240+
# The second method is to exactly match the size of your axes to the size of
241+
# your data. The following figure is exactly 2 inches by 2 inches, and
242+
# if the dpi is 200, then the 400x400 data is not resampled at all. If you download
243+
# this image and zoom in an image viewer you should see the individual stripes
244+
# on the left hand side (note that if you have a non hiDPI or "retina" screen, the html
245+
# may serve a 100x100 version of the image, which will be downsampled.)
246+
247+
fig = plt.figure(figsize=(2, 2))
248+
ax = fig.add_axes([0, 0, 1, 1])
249+
ax.imshow(aa[:400, :400], cmap='RdBu_r', interpolation='nearest')
250+
plt.show()
113251
# %%
114252
#
115253
# .. admonition:: References

lib/matplotlib/axes/_axes.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5832,11 +5832,16 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None,
58325832
which can be set by *filterrad*. Additionally, the antigrain image
58335833
resize filter is controlled by the parameter *filternorm*.
58345834
5835-
interpolation_stage : {'data', 'rgba'}, default: 'data'
5836-
If 'data', interpolation
5837-
is carried out on the data provided by the user. If 'rgba', the
5838-
interpolation is carried out after the colormapping has been
5839-
applied (visual interpolation).
5835+
interpolation_stage : {'antialiased', 'data', 'rgba'}, default: 'antialiased'
5836+
If 'data', interpolation is carried out on the data provided by the user,
5837+
useful if interpolating between pixels during upsampling.
5838+
If 'rgba', the interpolation is carried out in RGBA-space after the
5839+
color-mapping has been applied, useful if downsampling and combining
5840+
pixels visually. The default 'antialiased' is appropriate for most
5841+
applications where 'rgba' is used when downsampling, or upsampling at a
5842+
rate less than 3, and 'data' is used when upsampling at a higher rate.
5843+
See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for
5844+
a discussion of image antialiasing.
58405845
58415846
alpha : float or array-like, optional
58425847
The alpha blending value, between 0 (transparent) and 1 (opaque).

0 commit comments

Comments
 (0)