|
1 | 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. 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. |
26 | 20 | """
|
27 | 21 |
|
28 | 22 | import matplotlib.pyplot as plt
|
29 | 23 | import numpy as np
|
30 | 24 |
|
31 |
| -# %% |
| 25 | +fig, axs = plt.subplots(1, 2, figsize=(4, 2)) |
| 26 | + |
32 | 27 | # First we generate a 450x450 pixel image with varying frequency content:
|
33 | 28 | N = 450
|
34 | 29 | x = np.arange(N) / N - 0.5
|
|
45 | 40 | a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1
|
46 | 41 | a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1
|
47 | 42 | 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 | + |
49 | 53 | # %%
|
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. |
55 | 61 | #
|
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 |
62 | 63 | #
|
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, |
79 | 103 | 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}'") |
81 | 119 | plt.show()
|
82 | 120 |
|
83 | 121 | # %%
|
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 | + |
91 | 147 | 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'") |
95 | 150 |
|
96 | 151 | # %%
|
97 | 152 | # Better antialiasing algorithms can reduce this effect:
|
98 | 153 | 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'") |
102 | 156 |
|
103 | 157 | # %%
|
104 | 158 | # Apart from the default 'hanning' antialiasing, `~.Axes.imshow` supports a
|
105 | 159 | # number of different interpolation algorithms, which may work better or
|
106 |
| -# worse depending on the pattern. |
| 160 | +# worse depending on the underlying data. |
107 | 161 | fig, axs = plt.subplots(1, 2, figsize=(7, 4), layout='constrained')
|
108 | 162 | for ax, interp in zip(axs, ['hanning', 'lanczos']):
|
109 |
| - ax.imshow(a, interpolation=interp, cmap='gray') |
| 163 | + ax.imshow(alarge, interpolation=interp, cmap='gray') |
110 | 164 | 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'") |
111 | 213 | plt.show()
|
112 | 214 |
|
| 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() |
113 | 250 | # %%
|
114 | 251 | #
|
115 | 252 | # .. admonition:: References
|
|
0 commit comments