Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bd42020

Browse files
authored
Merge pull request #18782 from jklymak/enh-image-postrgba-interpolate
ENH: allow image to interpolate post RGBA
2 parents c139b3d + c460dbc commit bd42020

File tree

8 files changed

+183
-30
lines changed

8 files changed

+183
-30
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Image interpolation now possible at RGBA stage
2+
----------------------------------------------
3+
4+
Images in Matplotlib created via `~.axes.Axes.imshow` are resampled to match
5+
the resolution of the current canvas. It is useful to apply an anto-aliasing
6+
filter when downsampling to reduce Moire effects. By default, interpolation
7+
is done on the data, a norm applied, and then the colormapping performed.
8+
9+
However, it is often desireable for the anti-aliasing interpolation to happen
10+
in RGBA space, where the colors are interpolated rather than the data. This
11+
usually leads to colors outside the colormap, but visually blends adjacent
12+
colors, and is what browsers and other image processing software does.
13+
14+
A new keyword argument *interpolation_stage* is provided for
15+
`~.axes.Axes.imshow` to set the stage at which the anti-aliasing interpolation
16+
happens. The default is the current behaviour of "data", with the alternative
17+
being "rgba" for the newly-available behavior.
18+
19+
For more details see the discussion of the new keyword argument in
20+
:doc:`/gallery/images_contours_and_fields/image_antialiasing`.
21+

examples/images_contours_and_fields/image_antialiasing.py

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@
55
66
Images are represented by discrete pixels, either on the screen or in an
77
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.
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).
911
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.
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 difference between these is
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.
1423
1524
Other anti-aliasing filters can be specified in `.Axes.imshow` using the
1625
*interpolation* keyword argument.
@@ -20,26 +29,55 @@
2029
import matplotlib.pyplot as plt
2130

2231
###############################################################################
23-
# First we generate a 500x500 px image with varying frequency content:
24-
x = np.arange(500) / 500 - 0.5
25-
y = np.arange(500) / 500 - 0.5
32+
# First we generate a 450x450 pixel image with varying frequency content:
33+
N = 450
34+
x = np.arange(N) / N - 0.5
35+
y = np.arange(N) / N - 0.5
36+
aa = np.ones((N, N))
37+
aa[::2, :] = -1
2638

2739
X, Y = np.meshgrid(x, y)
2840
R = np.sqrt(X**2 + Y**2)
29-
f0 = 10
30-
k = 250
41+
f0 = 5
42+
k = 100
3143
a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2))
32-
33-
44+
# make the left hand side of this
45+
a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1
46+
a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1
47+
aa[:, int(N / 3):] = a[:, int(N / 3):]
48+
a = aa
3449
###############################################################################
35-
# The following images are subsampled from 500 data pixels to 303 rendered
36-
# pixels. The Moire patterns in the 'nearest' interpolation are caused by the
37-
# high-frequency data being subsampled. The 'antialiased' image
50+
# The following images are subsampled from 450 data pixels to either
51+
# 125 pixels or 250 pixels (depending on your display).
52+
# The Moire patterns in the 'nearest' interpolation are caused by the
53+
# high-frequency data being subsampled. The 'antialiased' imaged
3854
# 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 ax, interp in zip(axs, ['nearest', 'antialiased']):
41-
ax.imshow(a, interpolation=interp, cmap='gray')
42-
ax.set_title(f"interpolation='{interp}'")
55+
#
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+
#
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), constrained_layout=True)
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,
79+
cmap='RdBu_r')
80+
ax.set_title(f"interpolation='{interp}'\nspace='{space}'")
4381
plt.show()
4482

4583
###############################################################################
@@ -63,7 +101,7 @@
63101
plt.show()
64102

65103
###############################################################################
66-
# Apart from the default 'hanning' antialiasing `~.Axes.imshow` supports a
104+
# Apart from the default 'hanning' antialiasing, `~.Axes.imshow` supports a
67105
# number of different interpolation algorithms, which may work better or
68106
# worse depending on the pattern.
69107
fig, axs = plt.subplots(1, 2, figsize=(7, 4), constrained_layout=True)
@@ -72,7 +110,6 @@
72110
ax.set_title(f"interpolation='{interp}'")
73111
plt.show()
74112

75-
76113
#############################################################################
77114
#
78115
# .. admonition:: References

lib/matplotlib/artist.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,7 @@ def aliased_name(self, s):
14501450
'matplotlib.image._ImageBase.set_filternorm',
14511451
'matplotlib.image._ImageBase.set_filterrad',
14521452
'matplotlib.image._ImageBase.set_interpolation',
1453+
'matplotlib.image._ImageBase.set_interpolation_stage',
14531454
'matplotlib.image._ImageBase.set_resample',
14541455
'matplotlib.text._AnnotationBase.set_annotation_clip',
14551456
}

lib/matplotlib/axes/_axes.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5289,8 +5289,9 @@ def fill_betweenx(self, y, x1, x2=0, where=None,
52895289
@_api.make_keyword_only("3.5", "aspect")
52905290
@_preprocess_data()
52915291
def imshow(self, X, cmap=None, norm=None, aspect=None,
5292-
interpolation=None, alpha=None, vmin=None, vmax=None,
5293-
origin=None, extent=None, *, filternorm=True, filterrad=4.0,
5292+
interpolation=None, alpha=None,
5293+
vmin=None, vmax=None, origin=None, extent=None, *,
5294+
interpolation_stage=None, filternorm=True, filterrad=4.0,
52945295
resample=None, url=None, **kwargs):
52955296
"""
52965297
Display data as an image, i.e., on a 2D regular raster.
@@ -5382,6 +5383,12 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,
53825383
which can be set by *filterrad*. Additionally, the antigrain image
53835384
resize filter is controlled by the parameter *filternorm*.
53845385
5386+
interpolation_stage : {'data', 'rgba'}, default: 'data'
5387+
If 'data', interpolation
5388+
is carried out on the data provided by the user. If 'rgba', the
5389+
interpolation is carried out after the colormapping has been
5390+
applied (visual interpolation).
5391+
53855392
alpha : float or array-like, optional
53865393
The alpha blending value, between 0 (transparent) and 1 (opaque).
53875394
If *alpha* is an array, the alpha blending values are applied pixel
@@ -5482,9 +5489,11 @@ def imshow(self, X, cmap=None, norm=None, aspect=None,
54825489
if aspect is None:
54835490
aspect = rcParams['image.aspect']
54845491
self.set_aspect(aspect)
5485-
im = mimage.AxesImage(self, cmap, norm, interpolation, origin, extent,
5486-
filternorm=filternorm, filterrad=filterrad,
5487-
resample=resample, **kwargs)
5492+
im = mimage.AxesImage(self, cmap, norm, interpolation,
5493+
origin, extent, filternorm=filternorm,
5494+
filterrad=filterrad, resample=resample,
5495+
interpolation_stage=interpolation_stage,
5496+
**kwargs)
54885497

54895498
im.set_data(X)
54905499
im.set_alpha(alpha)

lib/matplotlib/image.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ def __init__(self, ax,
238238
filternorm=True,
239239
filterrad=4.0,
240240
resample=False,
241+
*,
242+
interpolation_stage=None,
241243
**kwargs
242244
):
243245
martist.Artist.__init__(self)
@@ -249,6 +251,7 @@ def __init__(self, ax,
249251
self.set_filternorm(filternorm)
250252
self.set_filterrad(filterrad)
251253
self.set_interpolation(interpolation)
254+
self.set_interpolation_stage(interpolation_stage)
252255
self.set_resample(resample)
253256
self.axes = ax
254257

@@ -392,8 +395,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
392395
if not unsampled:
393396
if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)):
394397
raise ValueError(f"Invalid shape {A.shape} for image data")
395-
396-
if A.ndim == 2:
398+
if A.ndim == 2 and self._interpolation_stage != 'rgba':
397399
# if we are a 2D array, then we are running through the
398400
# norm + colormap transformation. However, in general the
399401
# input data is not going to match the size on the screen so we
@@ -541,6 +543,9 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
541543
):
542544
output = self.norm(resampled_masked)
543545
else:
546+
if A.ndim == 2: # _interpolation_stage == 'rgba'
547+
self.norm.autoscale_None(A)
548+
A = self.to_rgba(A)
544549
if A.shape[2] == 3:
545550
A = _rgb_to_rgba(A)
546551
alpha = self._get_scalar_alpha()
@@ -773,6 +778,22 @@ def set_interpolation(self, s):
773778
self._interpolation = s
774779
self.stale = True
775780

781+
def set_interpolation_stage(self, s):
782+
"""
783+
Set when interpolation happens during the transform to RGBA.
784+
785+
Parameters
786+
----------
787+
s : {'data', 'rgba'} or None
788+
Whether to apply up/downsampling interpolation in data or rgba
789+
space.
790+
"""
791+
if s is None:
792+
s = "data" # placeholder for maybe having rcParam
793+
_api.check_in_list(['data', 'rgba'])
794+
self._interpolation_stage = s
795+
self.stale = True
796+
776797
def can_composite(self):
777798
"""Return whether the image can be composited with its neighbors."""
778799
trans = self.get_transform()
@@ -854,6 +875,11 @@ class AxesImage(_ImageBase):
854875
'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite',
855876
'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell',
856877
'sinc', 'lanczos', 'blackman'.
878+
interpolation_stage : {'data', 'rgba'}, default: 'data'
879+
If 'data', interpolation
880+
is carried out on the data provided by the user. If 'rgba', the
881+
interpolation is carried out after the colormapping has been
882+
applied (visual interpolation).
857883
origin : {'upper', 'lower'}, default: :rc:`image.origin`
858884
Place the [0, 0] index of the array in the upper left or lower left
859885
corner of the axes. The convention 'upper' is typically used for
@@ -890,6 +916,8 @@ def __init__(self, ax,
890916
filternorm=True,
891917
filterrad=4.0,
892918
resample=False,
919+
*,
920+
interpolation_stage=None,
893921
**kwargs
894922
):
895923

@@ -904,6 +932,7 @@ def __init__(self, ax,
904932
filternorm=filternorm,
905933
filterrad=filterrad,
906934
resample=resample,
935+
interpolation_stage=interpolation_stage,
907936
**kwargs
908937
)
909938

lib/matplotlib/pyplot.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2864,12 +2864,13 @@ def hlines(
28642864
def imshow(
28652865
X, cmap=None, norm=None, aspect=None, interpolation=None,
28662866
alpha=None, vmin=None, vmax=None, origin=None, extent=None, *,
2867-
filternorm=True, filterrad=4.0, resample=None, url=None,
2868-
data=None, **kwargs):
2867+
interpolation_stage=None, filternorm=True, filterrad=4.0,
2868+
resample=None, url=None, data=None, **kwargs):
28692869
__ret = gca().imshow(
28702870
X, cmap=cmap, norm=norm, aspect=aspect,
28712871
interpolation=interpolation, alpha=alpha, vmin=vmin,
28722872
vmax=vmax, origin=origin, extent=extent,
2873+
interpolation_stage=interpolation_stage,
28732874
filternorm=filternorm, filterrad=filterrad, resample=resample,
28742875
url=url, **({"data": data} if data is not None else {}),
28752876
**kwargs)

lib/matplotlib/tests/test_image.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,3 +1328,58 @@ def test_nonuniform_and_pcolor():
13281328
ax.set_axis_off()
13291329
# NonUniformImage "leaks" out of extents, not PColorImage.
13301330
ax.set(xlim=(0, 10))
1331+
1332+
1333+
@image_comparison(["rgba_antialias.png"], style="mpl20",
1334+
remove_text=True)
1335+
def test_rgba_antialias():
1336+
fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False,
1337+
sharey=False, constrained_layout=True)
1338+
N = 250
1339+
aa = np.ones((N, N))
1340+
aa[::2, :] = -1
1341+
1342+
x = np.arange(N) / N - 0.5
1343+
y = np.arange(N) / N - 0.5
1344+
1345+
X, Y = np.meshgrid(x, y)
1346+
R = np.sqrt(X**2 + Y**2)
1347+
f0 = 10
1348+
k = 75
1349+
# aliased concentric circles
1350+
a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2))
1351+
1352+
# stripes on lhs
1353+
a[:int(N/2), :][R[:int(N/2), :] < 0.4] = -1
1354+
a[:int(N/2), :][R[:int(N/2), :] < 0.3] = 1
1355+
aa[:, int(N/2):] = a[:, int(N/2):]
1356+
1357+
# set some over/unders and NaNs
1358+
aa[20:50, 20:50] = np.NaN
1359+
aa[70:90, 70:90] = 1e6
1360+
aa[70:90, 20:30] = -1e6
1361+
aa[70:90, 195:215] = 1e6
1362+
aa[20:30, 195:215] = -1e6
1363+
1364+
cmap = copy(plt.cm.RdBu_r)
1365+
cmap.set_over('yellow')
1366+
cmap.set_under('cyan')
1367+
1368+
axs = axs.flatten()
1369+
# zoom in
1370+
axs[0].imshow(aa, interpolation='nearest', cmap=cmap, vmin=-1.2, vmax=1.2)
1371+
axs[0].set_xlim([N/2-25, N/2+25])
1372+
axs[0].set_ylim([N/2+50, N/2-10])
1373+
1374+
# no anti-alias
1375+
axs[1].imshow(aa, interpolation='nearest', cmap=cmap, vmin=-1.2, vmax=1.2)
1376+
1377+
# data antialias: Note no purples, and white in circle. Note
1378+
# that alternating red and blue stripes become white.
1379+
axs[2].imshow(aa, interpolation='antialiased', interpolation_stage='data',
1380+
cmap=cmap, vmin=-1.2, vmax=1.2)
1381+
1382+
# rgba antialias: Note purples at boundary with circle. Note that
1383+
# alternating red and blue stripes become purple
1384+
axs[3].imshow(aa, interpolation='antialiased', interpolation_stage='rgba',
1385+
cmap=cmap, vmin=-1.2, vmax=1.2)

0 commit comments

Comments
 (0)