8000 ENH: anti-alias down-sampled images · djdt/matplotlib@7e0efe5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7e0efe5

Browse files
committed
ENH: anti-alias down-sampled images
1 parent 80d7a86 commit 7e0efe5

File tree

13 files changed

+289
-23
lines changed

13 files changed

+289
-23
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ per-file-ignores =
9494
examples/images_contours_and_fields/contourf_hatching.py: E402
9595
examples/images_contours_and_fields/contourf_log.py: E402
9696
examples/images_contours_and_fields/demo_bboximage.py: E402
97+
examples/images_contours_and_fields/image_antialiasing.py: E402
9798
examples/images_contours_and_fields/image_clip_path.py: E402
9899
examples/images_contours_and_fields/image_demo.py: E402
99100
examples/images_contours_and_fields/image_masked.py: E402
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Default interpolation for `image` is new "antialiased" option
2+
-------------------------------------------------------------
3+
4+
Images displayed in Matplotlib previously used nearest-neighbor
5+
interpolation, leading to aliasing effects for downscaling and non-integer
6+
upscaling.
7+
8+
New default for :rc:`image.interpolation` is the new option "antialiased".
9+
`imshow(A, interpolation='antialiased')` will apply a Hanning filter when
10+
resampling the data in A for display (or saving to file) *if* the upsample
11+
rate is less than a factor of three, and not an integer; downsampled data is
12+
always smoothed at resampling.
13+
14+
To get the old behavior, set :rc:`interpolation` to the old default "nearest"
15+
(or specify the ``interpolation`` kwarg of `.Axes.imshow`)
16+
17+
To always get the anti-aliasing behavior, no matter what the up/down sample
18+
rate, set :rc:`interpolation` to "hanning" (or one of the other filters
19+
available.
20+
21+
Note that the "hanning" filter was chosen because it has only a modest
22+
performance penalty. Anti-aliasing can be improved with other filters.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
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.
9+
10+
The default image interpolation in Matplotlib is 'antialiased'. This uses a
11+
hanning interpolation for reduced aliasing in most situations. Only when there
12+
is upsampling by a factor of 1, 2 or >=3 is 'nearest' neighbor interpolation
13+
used.
14+
15+
Other anti-aliasing filters can be specified in `.Axes.imshow` using the
16+
*interpolation* kwarg.
17+
"""
18+
19+
import numpy as np
20+
import matplotlib.pyplot as plt
21+
22+
###############################################################################
23+
# First we generate an image with varying frequency content:
24+
x = np.arange(500) / 500 - 0.5
25+
y = np.arange(500) / 500 - 0.5
26+
27+
X, Y = np.meshgrid(x, y)
28+
R = np.sqrt(X**2 + Y**2)
29+
f0 = 10
30+
k = 250
31+
a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2))
32+
33+
34+
###############################################################################
35+
# The following images are subsampled from 1000 data pixels to 604 rendered
36+
# pixels. The Moire patterns in the "nearest" interpolation are caused by the
37+
# high-frequency data being subsampled. The "antialiased" image
38+
# still has some Moire patterns as well, but they are greatly reduced.
39+
fig, axs = plt.subplots(1, 2, figsize=(7, 4), constrained_layout=True)
40+
for n, interp in enumerate(['nearest', 'antialiased']):
41+
im = axs[n].imshow(a, interpolation=interp, cmap='gray')
42+
axs[n].set_title(interp)
43+
plt.show()
44+
45+
###############################################################################
46+
# Even up-sampling an image will lead to Moire patterns unless the upsample
47+
# is an integer number of pixels.
48+
fig, ax = plt.subplots(1, 1, figsize=(5.3, 5.3))
49+
ax.set_position([0, 0, 1, 1])
50+
im = ax.imshow(a, interpolation='nearest', cmap='gray')
51+
plt.show()
52+
53+
###############################################################################
54+
# The patterns aren't as bad, but still benefit from anti-aliasing
55+
fig, ax = plt.subplots(1, 1, figsize=(5.3, 5.3))
56+
ax.set_position([0, 0, 1, 1])
57+
im = ax.imshow(a, interpolation='antialiased', cmap='gray')
58+
plt.show()
59+
60+
###############################################################################
61+
# If the small Moire patterns in the default "hanning" antialiasing are
62+
# still undesireable, then we can use other filters.
63+
fig, axs = plt.subplots(1, 2, figsize=(7, 4), constrained_layout=True)
64+
for n, interp in enumerate(['hanning', 'lanczos']):
65+
im = axs[n].imshow(a, interpolation=interp, cmap='gray')
66+
axs[n].set_title(interp)
67+
plt.show()
68+
69+
70+
#############################################################################
71+
#
72+
# ------------
73+
#
74+
# References
75+
# """"""""""
76+
#
77+
# The use of the following functions and methods is shown
78+
# in this example:
79+
80+
import matplotlib
81+
matplotlib.axes.Axes.imshow

examples/images_contours_and_fields/interpolation_methods.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
If `interpolation` is None, it defaults to the :rc:`image.interpolation`
1010
(default: ``'nearest'``). If the interpolation is ``'none'``, then no
1111
interpolation is performed for the Agg, ps and pdf backends. Other backends
12-
will default to ``'nearest'``.
12+
will default to ``'antialiased'``.
1313
1414
For the Agg, ps and pdf backends, ``interpolation = 'none'`` works well when a
1515
big image is scaled down, while ``interpolation = 'nearest'`` works well when
1616
a small image is scaled up.
17+
18+
See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for a
19+
discussion on the default `interpolation="antialiased"` option.
1720
"""
1821

1922
import matplotlib.pyplot as plt

lib/matplotlib/axes/_axes.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5488,20 +5488,30 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,
54885488
The interpolation method used. If *None*
54895489
:rc:`image.interpolation` is used, which defaults to 'nearest'.
54905490
5491-
Supported values are 'none', 'nearest', 'bilinear', 'bicubic',
5492-
'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser',
5493-
'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc',
5494-
'lanczos'.
5491+
Supported values are 'none', 'antialiased', 'nearest', 'bilinear',
5492+
'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite',
5493+
'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell',
5494+
'sinc', 'lanczos'.
54955495
54965496
If *interpolation* is 'none', then no interpolation is performed
54975497
on the Agg, ps, pdf and svg backends. Other backends will fall back
54985498
to 'nearest'. Note that most SVG renders perform interpolation at
54995499
rendering and that the default interpolation method they implement
55005500
may differ.
55015501
5502+
If *interpolation* is the default 'antialiased', then 'nearest'
5503+
interpolation is used if the image is upsampled by more than a
5504+
factor of three (i.e. the number of display pixels is at least
5505+
three times the size of the data array). If the upsampling rate is
5506+
smaller than 3, or the image is downsampled, then 'hanning'
5507+
interpolation is used to act as an anti-aliasing filter, unless the
5508+
image happens to be upsampled by exactly a factor of two or one.
5509+
55025510
See
55035511
:doc:`/gallery/images_contours_and_fields/interpolation_methods`
5504-
for an overview of the supported interpolation methods.
5512+
for an overview of the supported interpolation methods, and
5513+
:doc:`/gallery/images_contours_and_fields/image_antialiasing` for
5514+
a discussion of image antialiasing.
55055515
55065516
Some interpolation methods require an additional radius parameter,
55075517
which can be set by *filterrad*. Additionally, the antigrain image

lib/matplotlib/image.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
# map interpolation strings to module constants
3535
_interpd_ = {
36+
'antialiased': _image.NEAREST, # this will use nearest or Hanning...
3637
'none': _image.NEAREST, # fall back to nearest when not supported
3738
'nearest': _image.NEAREST,
3839
'bilinear': _image.BILINEAR,
@@ -168,11 +169,34 @@ def _resample(
168169
allocating the output array and fetching the relevant properties from the
169170
Image object *image_obj*.
170171
"""
172+
173+
# decide if we need to apply anti-aliasing if the data is upsampled:
174+
# compare the number of displayed pixels to the number of
175+
# the data pixels.
176+
interpolation = image_obj.get_interpolation()
177+
if interpolation == 'antialiased':
178+
# don't antialias if upsampling by an integer number or
179+
# if zooming in more than a factor of 3
179B 180+
shape = list(data.shape)
181+
if image_obj.origin == 'upper':
182+
shape[0] = 0
183+
dispx, dispy = transform.transform([shape[1], shape[0]])
184+
185+
if ((dispx > 3 * data.shape[1] or
186+
dispx == data.shape[1] or
187+
dispx == 2 * data.shape[1]) and
188+
(dispy > 3 * data.shape[0] or
189+
dispy == data.shape[0] or
190+
dispy == 2 * data.shape[0])):
191+
interpolation = 'nearest'
192+
else:
193+
interpolation = 'hanning'
194+
171195
out = np.zeros(out_shape + data.shape[2:], data.dtype) # 2D->2D, 3D->3D.
172196
if resample is None:
173197
resample = image_obj.get_resample()
174198
_image.resample(data, out, transform,
175-
_interpd_[image_obj.get_interpolation()],
199+
_interpd_[interpolation],
176200
resample,
177201
alpha,
178202
image_obj.get_filternorm(),
@@ -432,7 +456,6 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
432456
A_scaled += 0.1
433457
# resample the input data to the correct resolution and shape
434458
A_resampled = _resample(self, A_scaled, out_shape, t)
435-
436459
# done with A_scaled now, remove from namespace to be sure!
437460
del A_scaled
438461
# un-scale the resampled data to approximately the
@@ -690,9 +713,10 @@ def get_interpolation(self):
690713
"""
691714
Return the interpolation method the image uses when resizing.
692715
693-
One of 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36',
694-
'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom',
695-
'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', or 'none'.
716+
One of 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16',
717+
'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric',
718+
'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos',
719+
or 'none'.
696720
697721
"""
698722
return self._interpolation 10000
@@ -708,9 +732,9 @@ def set_interpolation(self, s):
708732
709733
Parameters
710734
----------
711-
s : {'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', \
712-
'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', \
713-
'bessel', 'mitchell', 'sinc', 'lanczos', 'none'}
735+
s : {'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16',
736+
'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', \
737+
'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'none'}
714738
715739
"""
716740
if s is None:

lib/matplotlib/rcsetup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,7 @@ def _validate_linestyle(ls):
11001100
'mathtext.fallback_to_cm': [True, validate_bool],
11011101

11021102
'image.aspect': ['equal', validate_aspect], # equal, auto, a number
1103-
'image.interpolation': ['nearest', validate_string],
1103+
'image.interpolation': ['antialiased', validate_string],
11041104
'image.cmap': ['viridis', validate_string], # gray, jet, etc.
11051105
'image.lut': [256, validate_int], # lookup table
11061106
'image.origin': ['upper',

lib/matplotlib/tests/test_axes.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,8 @@ def test_nonfinite_limits():
915915

916916
@image_comparison(['imshow', 'imshow'], remove_text=True, style='mpl20')
917917
def test_imshow():
918+
# use former defaults to match existing baseline image
919+
matplotlib.rcParams['image.interpolation'] = 'nearest'
918920
# Create a NxN image
919921
N = 100
920922
(x, y) = np.indices((N, N))
@@ -936,6 +938,8 @@ def test_imshow():
936938
@image_comparison(['imshow_clip'], style='mpl20')
937939
def test_imshow_clip():
938940
# As originally reported by Gellule Xg <gellule.xg@free.fr>
941+
# use former defaults to match existing baseline image
942+
matplotlib.rcParams['image.interpolation'] = 'nearest'
939943

940944
# Create a NxN image
941945
N = 100
@@ -3732,6 +3736,10 @@ def test_subplot_key_hash():
37323736
remove_text=True, tol=0.07, style='default')
37333737
def test_specgram_freqs():
37343738
'''test axes.specgram in default (psd) mode with sinusoidal stimuli'''
3739+
3740+
# use former defaults to match existing baseline image
3741+
matplotlib.rcParams['image.interpolation'] = 'nearest'
3742+
37353743
n = 1000
37363744
Fs = 10.
37373745

@@ -3784,6 +3792,10 @@ def test_specgram_freqs():
37843792
remove_text=True, tol=0.01, style='default')
37853793
def test_specgram_noise():
37863794
'''test axes.specgram in default (psd) mode with noise stimuli'''
3795+
3796+
# use former defaults to match existing baseline image
3797+
matplotlib.rcParams['image.interpolation'] = 'nearest'
3798+
37873799
np.random.seed(0)
37883800

37893801
n = 1000
@@ -3831,6 +3843,10 @@ def test_specgram_noise():
38313843
remove_text=True, tol=0.07, style='default')
38323844
def test_specgram_magnitude_freqs():
38333845
'''test axes.specgram in magnitude mode with sinusoidal stimuli'''
3846+
3847+
# use former defaults to match existing baseline image
3848+
matplotlib.rcParams['image.interpolation'] = 'nearest'
3849+
38343850
n = 1000
38353851
Fs = 10.
38363852

@@ -3886,6 +3902,10 @@ def test_specgram_magnitude_freqs():
38863902
remove_text=True, style='default')
38873903
def test_specgram_magnitude_noise():
38883904
'''test axes.specgram in magnitude mode with noise stimuli'''
3905+
3906+
# use former defaults to match existing baseline image
3907+
matplotlib.rcParams['image.interpolation'] = 'nearest'
3908+
38893909
np.random.seed(0)
38903910

38913911
n = 1000
@@ -3932,6 +3952,10 @@ def test_specgram_magnitude_noise():
39323952
remove_text=True, tol=0.007, style='default')
39333953
def test_specgram_angle_freqs():
39343954
'''test axes.specgram in angle mode with sinusoidal stimuli'''
3955+
3956+
# use former defaults to match existing baseline image
3957+
matplotlib.rcParams['image.interpolation'] = 'nearest'
3958+
39353959
n = 1000
39363960
Fs = 10.
39373961

@@ -3986,6 +4010,10 @@ def test_specgram_angle_freqs():
39864010
remove_text=True, style='default')
39874011
def test_specgram_noise_angle():
39884012
'''test axes.specgram in angle mode with noise stimuli'''
4013+
4014+
# use former defaults to match existing baseline image
4015+
matplotlib.rcParams['image.interpolation'] = 'nearest'
4016+
39894017
np.random.seed(0)
39904018

39914019
n = 1000
@@ -4032,6 +4060,9 @@ def test_specgram_noise_angle():
40324060
remove_text=True, style='default')
40334061
def test_specgram_freqs_phase():
40344062
'''test axes.specgram in phase mode with sinusoidal stimuli'''
4063+
4064+
# use former defaults to match existing baseline image
4065+
matplotlib.rcParams['image.interpolation'] = 'nearest'
40354066
n = 1000
40364067
Fs = 10.
40374068

@@ -4086,6 +4117,9 @@ def test_specgram_freqs_phase():
40864117
remove_text=True, style='default')
40874118
def test_specgram_noise_phase():
40884119
'''test axes.specgram in phase mode with noise stimuli'''
4120+
4121+
# use former defaults to match existing baseline image
4122+
matplotlib.rcParams['image.interpolation'] = 'nearest'
40894123
np.random.seed(0)
40904124

40914125
n = 1000

0 commit comments

Comments
 (0)
0