Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6ad31ab

Browse filesBrowse files
committed
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.
1 parent 2360c59 commit 6ad31ab
Copy full SHA for 6ad31ab

File tree

12 files changed

+375
-86
lines changed
Filter options

12 files changed

+375
-86
lines changed
+14Lines 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 down-sampled or up-sampled less than
6+
a factor of three, 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

Copy file name to clipboardExpand all lines: galleries/examples/images_contours_and_fields/image_antialiasing.py
+206-69Lines changed: 206 additions & 69 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 image file,
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 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,213 @@
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 wavenumber (`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+
# a lot off white colors to the image that are not in the original data, both
127+
# in the alternating bands on the left hand side of the image, and in the
128+
# boundary between the red and blue of the large circles in the middle of the
129+
# image. The interpolation at the 'rgba' stage is more natural, with the
130+
# alternating bands coming out a shade of purple; even though purple is not
131+
# in the original colormap, it is what we perceive when a blue and red stripe
132+
# are close to each other.
133+
#
134+
# The default for the *interpolation* keyword argument is 'antialiased'
135+
# which will choose a Hanning filter if the image is being down-sampled
136+
# or up-sampled by less than a factor of three. The default
137+
# *interploation_stage* keyword argument is also 'antialiased', and for
138+
# images that are down-sampled or up-sampled by less than a factor of three
139+
# it defaults to 'rgba' 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("upsampled by factor a 1.17, interpolation='nearest'")
95150

96151
# %%
97152
# Better antialiasing 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("upsampled by factor a 1.17, interpolation='antialiased'")
102156

103157
# %%
104158
# Apart from the default 'hanning' antialiasing, `~.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
168+
# the RGBA stage. In the following, the data in the upper 100 rows is exactly
169+
# 0.0, and data in the inner circle is exactly 2.0. If we perform the
170+
# *interpolation_stage* in 'data' space and use an anti-aliasing filter (first
171+
# panel), then floating point imprecision makes some of the data values just a
172+
# bit less than zero or a bit more than 2.0, and they get assigned the under-
173+
# or over- colors. This can be avoided if you don't use an anti-aliasing filter
174+
# (*interpolation* set set to 'nearest'), however, that makes the part of the
175+
# data susceptible to Moiré patterns much worse (second panel). Therefore, we
176+
# recommend the default *interpolation* of 'hanning'/'antialiased', and
177+
# *interpolation_stage* of 'rgba'/'antialiased' for most down-sampling
178+
# 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 upsample, 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. in the following, the figure is exactly 2 inches by 2 inches, and
242+
# the dpi is 200, so 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.
245+
246+
fig = plt.figure(figsize=(2, 2))
247+
ax = fig.add_axes([0, 0, 1, 1])
248+
ax.imshow(aa[:400, :400], cmap='RdBu_r', interpolation='nearest')
249+
plt.show()
113250
# %%
114251
#
115252
# .. admonition:: References

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+8-5Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5791,11 +5791,14 @@ def imshow(self, X, cmap=None, norm=None, *, aspect=None,
57915791
which can be set by *filterrad*. Additionally, the antigrain image
57925792
resize filter is controlled by the parameter *filternorm*.
57935793
5794-
interpolation_stage : {'data', 'rgba'}, default: 'data'
5795-
If 'data', interpolation
5796-
is carried out on the data provided by the user. If 'rgba', the
5797-
interpolation is carried out after the colormapping has been
5798-
applied (visual interpolation).
5794+
interpolation_stage : {'antialiased', 'data', 'rgba'}, default: 'antialiased'
5795+
If 'data', interpolation is carried out on the data provided by the user.
5796+
If 'rgba', the interpolation is carried out in RGBA-space after the
5797+
color-mapping has been applied (visual interpolation). If 'antialiased',
5798+
then 'rgba' is used if downsampling, or upsampling at a rate less than 3.
5799+
If upsampling at a higher rate, then 'data' is used.
5800+
See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for
5801+
a discussion of image antialiasing.
57995802
58005803
alpha : float or array-like, optional
58015804
The alpha blending value, between 0 (transparent) and 1 (opaque).

‎lib/matplotlib/image.py

Copy file name to clipboardExpand all lines: lib/matplotlib/image.py
+20-4Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,21 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
421421
if not unsampled:
422422
if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)):
423423
raise ValueError(f"Invalid shape {A.shape} for image data")
424-
if A.ndim == 2 and self._interpolation_stage != 'rgba':
424+
425+
# if antialiased, this needs to change as window sizes
426+
# change:
427+
interpolation_stage = self._interpolation_stage
428+
if interpolation_stage == 'antialiased':
429+
pos = np.array([[0, 0], [A.shape[1], A.shape[0]]])
430+
disp = t.transform(pos)
431+
dispx = np.abs(np.diff(disp[:, 0])) / A.shape[1]
432+
dispy = np.abs(np.diff(disp[:, 1])) / A.shape[0]
433+
if (dispx < 3) or (dispy < 3):
434+
interpolation_stage = 'rgba'
435+
else:
436+
interpolation_stage = 'data'
437+
438+
if A.ndim == 2 and interpolation_stage == 'data':
425439
# if we are a 2D array, then we are running through the
426440
# norm + colormap transformation. However, in general the
427441
# input data is not going to match the size on the screen so we
@@ -552,7 +566,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
552566
cbook._setattr_cm(self.norm, vmin=s_vmin, vmax=s_vmax):
553567
output = self.norm(resampled_masked)
554568
else:
555-
if A.ndim == 2: # _interpolation_stage == 'rgba'
569+
if A.ndim == 2: # interpolation_stage == 'rgba'
556570
self.norm.autoscale_None(A)
557571
A = self.to_rgba(A)
558572
alpha = self._get_scalar_alpha()
@@ -787,12 +801,14 @@ def set_interpolation_stage(self, s):
787801
788802
Parameters
789803
----------
790-
s : {'data', 'rgba'} or None
804+
s : {'data', 'rgba', 'antialiased'} or None
791805
Whether to apply up/downsampling interpolation in data or RGBA
792806
space. If None, use :rc:`image.interpolation_stage`.
807+
If 'antialiased' we will check upsampling rate and if less
808+
than 3 then use 'rgba', otherwise use 'data'.
793809
"""
794810
s = mpl._val_or_rc(s, 'image.interpolation_stage')
795-
_api.check_in_list(['data', 'rgba'], s=s)
811+
_api.check_in_list(['data', 'rgba', 'antialiased'], s=s)
796812
self._interpolation_stage = s
797813
self.stale = True
798814

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.