|
| 1 | +""" |
| 2 | +*origin* and *extent* in `~.Axes.imshow` |
| 3 | +======================================== |
| 4 | +
|
| 5 | +:meth:`~.Axes.imshow` allows you to render an image (either a 2D array |
| 6 | +which will be color-mapped (based on *norm* and *cmap*) or and 3D RGB(A) |
| 7 | +array which will be used as-is) to a rectangular region in dataspace. |
| 8 | +The orientation of the image in the final rendering is controlled by |
| 9 | +the *origin* and *extent* kwargs (and attributes on the resulting |
| 10 | +`~.AxesImage` instance) and the data limits of the axes. |
| 11 | +
|
| 12 | +The *extent* kwarg controls the bounding box in data coordinates that |
| 13 | +the image will fill specified as ``(left, right, bottom, top)`` in |
| 14 | +**data coordinates**, the *origin* kwarg controls how the image fills |
| 15 | +that bounding box, and the orientation in the final rendered image is |
| 16 | +also affected by the axes limits. |
| 17 | +
|
| 18 | +.. hint:: Most of the code below is used for adding labels and informative |
| 19 | + text to the plots. The described effects of *origin* and *extent* can be |
| 20 | + seen in the plots without the need to follow all code details. |
| 21 | +
|
| 22 | + For a quick understanding, you may want to skip the code details below and |
| 23 | + directly continue with the discussion of the results. |
| 24 | +""" |
| 25 | +import numpy as np |
| 26 | +import matplotlib.pyplot as plt |
| 27 | +from matplotlib.gridspec import GridSpec |
| 28 | + |
| 29 | + |
| 30 | +def index_to_coordinate(index, extent, origin): |
| 31 | + """Return the pixel center of an index.""" |
| 32 | + left, right, bottom, top = extent |
| 33 | + |
| 34 | + hshift = 0.5 * np.sign(right - left) |
| 35 | + left, right = left + hshift, right - hshift |
| 36 | + vshift = 0.5 * np.sign(top - bottom) |
| 37 | + bottom, top = bottom + vshift, top - vshift |
| 38 | + |
| 39 | + if origin == 'upper': |
| 40 | + bottom, top = top, bottom |
| 41 | + |
| 42 | + return { |
| 43 | + "[0, 0]": (left, bottom), |
| 44 | + "[M', 0]": (left, top), |
| 45 | + "[0, N']": (right, bottom), |
| 46 | + "[M', N']": (right, top), |
| 47 | + }[index] |
| 48 | + |
| 49 | + |
| 50 | +def get_index_label_pos(index, extent, origin, inverted_xindex): |
| 51 | + """ |
| 52 | + Return the desired position and horizontal alignment of an index label. |
| 53 | + """ |
| 54 | + if extent is None: |
| 55 | + extent = lookup_extent(origin) |
| 56 | + left, right, bottom, top = extent |
| 57 | + x, y = index_to_coordinate(index, extent, origin) |
| 58 | + |
| 59 | + is_x0 = index[-2:] == "0]" |
| 60 | + halign = 'left' if is_x0 ^ inverted_xindex else 'right' |
| 61 | + hshift = 0.5 * np.sign(left - right) |
| 62 | + x += hshift * (1 if is_x0 else -1) |
| 63 | + return x, y, halign |
| 64 | + |
| 65 | + |
| 66 | +def get_color(index, data, cmap): |
| 67 | + """Return the data color of an index.""" |
| 68 | + val = { |
| 69 | + "[0, 0]": data[0, 0], |
| 70 | + "[0, N']": data[0, -1], |
| 71 | + "[M', 0]": data[-1, 0], |
| 72 | + "[M', N']": data[-1, -1], |
| 73 | + }[index] |
| 74 | + return cmap(val / data.max()) |
| 75 | + |
| 76 | + |
| 77 | +def lookup_extent(origin): |
| 78 | + """Return extent for label positioning when not given explicitly.""" |
| 79 | + if origin == 'lower': |
| 80 | + return (-0.5, 6.5, -0.5, 5.5) |
| 81 | + else: |
| 82 | + return (-0.5, 6.5, 5.5, -0.5) |
| 83 | + |
| 84 | + |
| 85 | +def set_extent_None_text(ax): |
| 86 | + ax.text(3, 2.5, 'equals\nextent=None', size='large', |
| 87 | + ha='center', va='center', color='w') |
| 88 | + |
| 89 | + |
| 90 | +def plot_imshow_with_labels(ax, data, extent, origin, xlim, ylim): |
| 91 | + """Actually run ``imshow()`` and add extent and index labels.""" |
| 92 | + im = ax.imshow(data, origin=origin, extent=extent) |
| 93 | + |
| 94 | + # extent labels (left, right, bottom, top) |
| 95 | + left, right, bottom, top = im.get_extent() |
| 96 | + if xlim is None or top > bottom: |
| 97 | + upper_string, lower_string = 'top', 'bottom' |
| 98 | + else: |
| 99 | + upper_string, lower_string = 'bottom', 'top' |
| 100 | + if ylim is None or left < right: |
| 101 | + port_string, starboard_string = 'left', 'right' |
| 102 | + inverted_xindex = False |
| 103 | + else: |
| 104 | + port_string, starboard_string = 'right', 'left' |
| 105 | + inverted_xindex = True |
| 106 | + bbox_kwargs = {'fc': 'w', 'alpha': .75, 'boxstyle': "round4"} |
| 107 | + ann_kwargs = {'xycoords': 'axes fraction', |
| 108 | + 'textcoords': 'offset points', |
| 109 | + 'bbox': bbox_kwargs} |
| 110 | + ax.annotate(upper_string, xy=(.5, 1), xytext=(0, -1), |
| 111 | + ha='center', va='top', **ann_kwargs) |
| 112 | + ax.annotate(lower_string, xy=(.5, 0), xytext=(0, 1), |
| 113 | + ha='center', va='bottom', **ann_kwargs) |
| 114 | + ax.annotate(port_string, xy=(0, .5), xytext=(1, 0), |
| 115 | + ha='left', va='center', rotation=90, |
| 116 | + **ann_kwargs) |
| 117 | + ax.annotate(starboard_string, xy=(1, .5), xytext=(-1, 0), |
| 118 | + ha='right', va='center', rotation=-90, |
| 119 | + **ann_kwargs) |
| 120 | + ax.set_title('origin: {origin}'.format(origin=origin)) |
| 121 | + |
| 122 | + # index labels |
| 123 | + for index in ["[0, 0]", "[0, N']", "[M', 0]", "[M', N']"]: |
| 124 | + tx, ty, halign = get_index_label_pos(index, extent, origin, |
| 125 | + inverted_xindex) |
| 126 | + facecolor = get_color(index, data, im.get_cmap()) |
| 127 | + ax.text(tx, ty, index, color='white', ha=halign, va='center', |
| 128 | + bbox={'boxstyle': 'square', 'facecolor': facecolor}) |
| 129 | + if xlim: |
| 130 | + ax.set_xlim(*xlim) |
| 131 | + if ylim: |
| 132 | + ax.set_ylim(*ylim) |
| 133 | + |
| 134 | + |
| 135 | +def generate_imshow_demo_grid(extents, xlim=None, ylim=None): |
| 136 | + N = len(extents) |
| 137 | + fig = plt.figure(tight_layout=True) |
| 138 | + fig.set_size_inches(6, N * (11.25) / 5) |
| 139 | + gs = GridSpec(N, 5, figure=fig) |
| 140 | + |
| 141 | + columns = {'label': [fig.add_subplot(gs[j, 0]) for j in range(N)], |
| 142 | + 'upper': [fig.add_subplot(gs[j, 1:3]) for j in range(N)], |
| 143 | + 'lower': [fig.add_subplot(gs[j, 3:5]) for j in range(N)]} |
| 144 | + x, y = np.ogrid[0:6, 0:7] |
| 145 | + data = x + y |
| 146 | + |
| 147 | + for origin in ['upper', 'lower']: |
| 148 | + for ax, extent in zip(columns[origin], extents): |
| 149 | + plot_imshow_with_labels(ax, data, extent, origin, xlim, ylim) |
| 150 | + |
| 151 | + for ax, extent in zip(columns['label'], extents): |
| 152 | + text_kwargs = {'ha': 'right', |
| 153 | + 'va': 'center', |
| 154 | + 'xycoords': 'axes fraction', |
| 155 | + 'xy': (1, .5)} |
| 156 | + if extent is None: |
| 157 | + ax.annotate('None', **text_kwargs) |
| 158 | + ax.set_title('extent=') |
| 159 | + else: |
| 160 | + left, right, bottom, top = extent |
| 161 | + text = ('left: {left:0.1f}\nright: {right:0.1f}\n' + |
| 162 | + 'bottom: {bottom:0.1f}\ntop: {top:0.1f}\n').format( |
| 163 | + left=left, right=right, bottom=bottom, top=top) |
| 164 | + |
| 165 | + ax.annotate(text, **text_kwargs) |
| 166 | + ax.axis('off') |
| 167 | + return columns |
| 168 | + |
| 169 | + |
| 170 | +############################################################################### |
| 171 | +# |
| 172 | +# Default extent |
| 173 | +# -------------- |
| 174 | +# |
| 175 | +# First, let's have a look at the default `extent=None` |
| 176 | + |
| 177 | +generate_imshow_demo_grid(extents=[None]) |
| 178 | + |
| 179 | +############################################################################### |
| 180 | +# |
| 181 | +# Generally, for an array of shape (M, N), the first index runs along the |
| 182 | +# vertical, the second index runs along the horizontal. |
| 183 | +# The pixel centers are at integer positions ranging from 0 to ``N' = N - 1`` |
| 184 | +# horizontally and from 0 to ``M' = M - 1`` vertically. |
| 185 | +# *origin* determines how to the data is filled in the bounding box. |
| 186 | +# |
| 187 | +# For ``origin='lower'``: |
| 188 | +# |
| 189 | +# - [0, 0] is at (left, bottom) |
| 190 | +# - [M', 0] is at (left, top) |
| 191 | +# - [0, N'] is at (right, bottom) |
| 192 | +# - [M', N'] is at (right, top) |
| 193 | +# |
| 194 | +# ``origin='upper'`` reverses the vertical axes direction and filling: |
| 195 | +# |
| 196 | +# - [0, 0] is at (left, top) |
| 197 | +# - [M', 0] is at (left, bottom) |
| 198 | +# - [0, N'] is at (right, top) |
| 199 | +# - [M', N'] is at (right, bottom) |
| 200 | +# |
| 201 | +# In summary, the position of the [0, 0] index as well as the extent are |
| 202 | +# influenced by *origin*: |
| 203 | +# |
| 204 | +# ====== =============== ========================================== |
| 205 | +# origin [0, 0] position extent |
| 206 | +# ====== =============== ========================================== |
| 207 | +# upper top left ``(-0.5, numcols-0.5, numrows-0.5, -0.5)`` |
| 208 | +# lower bottom left ``(-0.5, numcols-0.5, -0.5, numrows-0.5)`` |
| 209 | +# ====== =============== ========================================== |
| 210 | +# |
| 211 | +# The default value of *origin* is set by :rc:`image.origin` which defaults |
| 212 | +# to ``'upper'`` to match the matrix indexing conventions in math and |
| 213 | +# computer graphics image indexing conventions. |
| 214 | +# |
| 215 | +# |
| 216 | +# Explicit extent |
| 217 | +# --------------- |
| 218 | +# |
| 219 | +# By setting *extent* we define the coordinates of the image area. The |
| 220 | +# underlying image data is interpolated/resampled to fill that area. |
| 221 | +# |
| 222 | +# If the axes is set to autoscale, then the view limits of the axes are set |
| 223 | +# to match the *extent* which ensures that the coordinate set by |
| 224 | +# ``(left, bottom)`` is at the bottom left of the axes! However, this |
| 225 | +# may invert the axis so they do not increase in the 'natural' direction. |
| 226 | +# |
| 227 | + |
| 228 | +extents = [(-0.5, 6.5, -0.5, 5.5), |
| 229 | + (-0.5, 6.5, 5.5, -0.5), |
| 230 | + (6.5, -0.5, -0.5, 5.5), |
| 231 | + (6.5, -0.5, 5.5, -0.5)] |
| 232 | + |
| 233 | +columns = generate_imshow_demo_grid(extents) |
| 234 | +set_extent_None_text(columns['upper'][1]) |
| 235 | +set_extent_None_text(columns['lower'][0]) |
| 236 | + |
| 237 | + |
| 238 | +############################################################################### |
| 239 | +# |
| 240 | +# Explicit extent and axes limits |
| 241 | +# ------------------------------- |
| 242 | +# |
| 243 | +# If we fix the axes limits by explicity setting `set_xlim` / `set_ylim`, we |
| 244 | +# force a certain size and orientation of the axes. |
| 245 | +# This can decouple the 'left-right' and 'top-bottom' sense of the image from |
| 246 | +# the orientation on the screen. |
| 247 | +# |
| 248 | +# In the example below we have chosen the limits slightly larger than the |
| 249 | +# extent (note the white areas within the Axes). |
| 250 | +# |
| 251 | +# While we keep the extents as in the examples before, the coordinate (0, 0) |
| 252 | +# is now explicitly put at the bottom left and values increase to up and to |
| 253 | +# the right (from the viewer point of view). |
| 254 | +# We can see that: |
| 255 | +# |
| 256 | +# - The coordinate ``(left, bottom)`` anchors the image which then fills the |
| 257 | +# box going towards the ``(right, top)`` point in data space. |
| 258 | +# - The first column is always closest to the 'left'. |
| 259 | +# - *origin* controls if the first row is closest to 'top' or 'bottom'. |
| 260 | +# - The image may be inverted along either direction. |
| 261 | +# - The 'left-right' and 'top-bottom' sense of the image may be uncoupled from |
| 262 | +# the orientation on the screen. |
| 263 | + |
| 264 | +generate_imshow_demo_grid(extents=[None] + extents, |
| 265 | + xlim=(-2, 8), ylim=(-1, 6)) |
0 commit comments