8000 API: imshow make rgba the defaut stage when down-sampling · matplotlib/matplotlib@2679f98 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2679f98

Browse 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 2679f98

File tree

12 files changed

+375
-86
lines changed

12 files changed

+375
-86
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 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

Lines 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+
# 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 is more natural, 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("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 do not 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 F438 +
# 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

Lines 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

Lines 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