我正在研究一些matplotlib图,需要有一个缩放的插图。使用zoomed_inset_axes
工具包中的axes_grid1
可以实现这一点。请参阅示例here:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
import numpy as np
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3,4,-4,3)
fig, ax = plt.subplots(figsize=[5,4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
# extent = [-3, 4, -4, 3]
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6
axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
这将得到所需的结果:
http://matplotlib.org/1.3.1/_images/inset_locator_demo21.png
但是正如您在代码中看到的那样,数据必须绘制两次 - 一次针对主轴(ax.imshow...
),一次针对插入轴(axins.imshow...
)。
有没有办法在主色情图完成后添加缩放的 ,没有需要在新轴上再次绘制所有内容?
请注意:我不是在寻找一个用函数封装绘图调用的解决方案,让函数绘制ax
和axins
(参见下面的示例),但是(如果存在)a利用ax
中现有数据的原生解决方案。有谁知道这种解决方案是否存在?
这是包装解决方案:
def plot_with_zoom(*args, **kwargs):
ax.imshow(*args, **kwargs)
axins.imshow(*args, **kwargs)
它有效,但感觉有点像黑客,因为如果我只想放大我现有情节的区域,为什么还需要再次绘制所有数据。
在ed-smith的回答之后做了一些额外的澄清:
上面的例子当然只是最小的例子。图中可能有许多不同的数据集(并且数据集我指的是通过imshow
或plot
等绘制的事物)。想象一下,例如一个包含10个点阵列的散点图,所有点都与普通x相对。
正如我上面所写,最直接的方法就是在所有实例中绘制数据。但我正在寻找的是一种方式(如果它存在)从最终的ax
对象(不是单独的绘图命令)开始,并以某种方式创建缩放的插入。
答案 0 :(得分:2)
我认为以下是您想要的。请注意,您将返回的句柄用于第一个imshow
并将其添加到插入的轴。你需要制作一份副本,这样每个人物都有一个单独的句柄,
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
import numpy as np
import copy
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3,4,-4,3)
fig, ax = plt.subplots(figsize=[5,4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
# extent = [-3, 4, -4, 3]
im = ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
#Without copy, image is shown in insert only
imcopy = copy.copy(im)
axins = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6
axins.add_artist(imcopy)
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
对于你的包装函数,这就像是,
def plot_with_zoom(*args, **kwargs):
im = ax.imshow(*args, **kwargs)
imcopy = copy.copy(im)
axins.add_artist(imcopy)
但是,由于imshow
只是将数组Z
中存储的数据显示为图像,我认为此解决方案实际上比对imshow
的两次单独调用要慢。对于花费更多时间的情节,例如一个contour
情节或pcolormesh
,这种方法可能是明智的......
修改强>
超越单个imshow
,以及多个不同类型的情节。绘图函数都返回不同的句柄(例如,绘图返回行列表,imshow返回matplotlib.image.AxesImage等)。您可以在绘制时将这些句柄添加到列表(或dict)中(如果它们足够相似,则使用collection)。然后你可以编写一个通用函数,使用add_artist或来自缩放轴的add_patch方法将它们添加到轴中,可能使用if类型检查来处理绘图中使用的各种类型。一个更简单的方法可能是循环ax.get_children()
并重用任何不是轴本身元素的东西。
另一种选择可能是研究blitting技术,rasterization或其他用于加速动画的技术,例如使用fig.canvas.copy_from_bbox
或fig.canvas.tostring_rgb
将整个图形复制为图像(见why is plotting with Matplotlib so slow?低)。您还可以绘制图形,将其保存为非矢量图形(使用savefig
或StringIO buffer),然后读回并绘制放大版本。
答案 1 :(得分:1)
我最近正在使用我正在编写的软件来解决此问题,并决定在此处共享它,以防万一仍然有人在处理此问题。此解决方案不需要重新绘制,只需使用自定义缩放轴类而不是默认的缩放轴类即可。它使用自定义渲染器工作,该渲染器充当matplotlib Artists与实际渲染器之间的中间人。然后,可以使用自定义渲染器(而不是提供的原始渲染器)简单地绘制艺术家。下面是实现:
from matplotlib.path import Path
from matplotlib.axes import Axes
from matplotlib.axes._axes import _make_inset_locator
from matplotlib.transforms import Bbox, Transform, IdentityTransform, Affine2D
from matplotlib.backend_bases import RendererBase
import matplotlib._image as _image
import numpy as np
class TransformRenderer(RendererBase):
"""
A matplotlib renderer which performs transforms to change the final location of plotted
elements, and then defers drawing work to the original renderer.
"""
def __init__(self, base_renderer: RendererBase, mock_transform: Transform, transform: Transform,
bounding_axes: Axes):
"""
Constructs a new TransformRender.
:param base_renderer: The renderer to use for finally drawing objects.
:param mock_transform: The transform or coordinate space which all passed paths/triangles/images will be
converted to before being placed back into display coordinates by the main transform.
For example if the parent axes transData is passed, all objects will be converted to
the parent axes data coordinate space before being transformed via the main transform
back into coordinate space.
:param transform: The main transform to be used for plotting all objects once converted into the mock_transform
coordinate space. Typically this is the child axes data coordinate space (transData).
:param bounding_axes: The axes to plot everything within. Everything outside of this axes will be clipped.
"""
super().__init__()
self.__renderer = base_renderer
self.__mock_trans = mock_transform
self.__core_trans = transform
self.__bounding_axes = bounding_axes
def _get_axes_display_box(self) -> Bbox:
"""
Private method, get the bounding box of the child axes in display coordinates.
"""
return self.__bounding_axes.patch.get_bbox().transformed(self.__bounding_axes.transAxes)
def _get_transfer_transform(self, orig_transform):
"""
Private method, returns the transform which translates and scales coordinates as if they were originally
plotted on the child axes instead of the parent axes.
:param orig_transform: The transform that was going to be originally used by the object/path/text/image.
:return: A matplotlib transform which goes from original point data -> display coordinates if the data was
originally plotted on the child axes instead of the parent axes.
"""
# We apply the original transform to go to display coordinates, then apply the parent data transform inverted
# to go to the parent axes coordinate space (data space), then apply the child axes data transform to
# go back into display space, but as if we originally plotted the artist on the child axes....
return orig_transform + self.__mock_trans.inverted() + self.__core_trans
# We copy all of the properties of the renderer we are mocking, so that artists plot themselves as if they were
# placed on the original renderer.
@property
def height(self):
return self.__renderer.get_canvas_width_height()[1]
@property
def width(self):
return self.__renderer.get_canvas_width_height()[0]
def get_text_width_height_descent(self, s, prop, ismath):
return self.__renderer.get_text_width_height_descent(s, prop, ismath)
def get_canvas_width_height(self):
return self.__renderer.get_canvas_width_height()
def get_texmanager(self):
return self.__renderer.get_texmanager()
def get_image_magnification(self):
return self.__renderer.get_image_magnification()
def _get_text_path_transform(self, x, y, s, prop, angle, ismath):
return self.__renderer._get_text_path_transform(x, y, s, prop, angle, ismath)
def option_scale_image(self):
return False
def points_to_pixels(self, points):
return self.__renderer.points_to_pixels(points)
def flipy(self):
return self.__renderer.flipy()
# Actual drawing methods below:
def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None):
# Convert the path to display coordinates, but if it was originally drawn on the child axes.
path = path.deepcopy()
path.vertices = self._get_transfer_transform(transform).transform(path.vertices)
bbox = self._get_axes_display_box()
# We check if the path intersects the axes box at all, if not don't waste time drawing it.
if(not path.intersects_bbox(bbox, True)):
return
# Change the clip to the sub-axes box
gc.set_clip_rectangle(bbox)
self.__renderer.draw_path(gc, path, IdentityTransform(), rgbFace)
def _draw_text_as_path(self, gc, x, y, s: str, prop, angle, ismath):
# If the text field is empty, don't even try rendering it...
if((s is None) or (s.strip() == "")):
return
# Call the super class instance, which works for all cases except one checked above... (Above case causes error)
super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
def draw_gouraud_triangle(self, gc, points, colors, transform):
# Pretty much identical to draw_path, transform the points and adjust clip to the child axes bounding box.
points = self._get_transfer_transform(transform).transform(points)
path = Path(points, closed=True)
bbox = self._get_axes_display_box()
if(not path.intersects_bbox(bbox, True)):
return
gc.set_clip_rectangle(bbox)
self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, IdentityTransform())
# Images prove to be especially messy to deal with...
def draw_image(self, gc, x, y, im, transform=None):
mag = self.get_image_magnification()
shift_data_transform = self._get_transfer_transform(IdentityTransform())
axes_bbox = self._get_axes_display_box()
# Compute the image bounding box in display coordinates.... Image arrives pre-magnified.
img_bbox_disp = Bbox.from_bounds(x, y, im.shape[1], im.shape[0])
# Now compute the output location, clipping it with the final axes patch.
out_box = img_bbox_disp.transformed(shift_data_transform)
clipped_out_box = Bbox.intersection(out_box, axes_bbox)
if(clipped_out_box is None):
return
# We compute what the dimensions of the final output image within the sub-axes are going to be.
x, y, out_w, out_h = clipped_out_box.bounds
out_w, out_h = int(np.ceil(out_w * mag)), int(np.ceil(out_h * mag))
if((out_w <= 0) or (out_h <= 0)):
return
# We can now construct the transform which converts between the original image (a 2D numpy array which starts
# at the origin) to the final zoomed image (also a 2D numpy array which starts at the origin).
img_trans = (
Affine2D().scale(1/mag, 1/mag).translate(img_bbox_disp.x0, img_bbox_disp.y0)
+ shift_data_transform
+ Affine2D().translate(-clipped_out_box.x0, -clipped_out_box.y0).scale(mag, mag)
)
# We resize and zoom the original image onto the out_arr.
out_arr = np.zeros((out_h, out_w, im.shape[2]), dtype=im.dtype)
_image.resample(im, out_arr, img_trans, _image.NEAREST, alpha=1)
_image.resample(im[:, :, 3], out_arr[:, :, 3], img_trans, _image.NEAREST, alpha=1)
gc.set_clip_rectangle(clipped_out_box)
x, y = clipped_out_box.x0, clipped_out_box.y0
if(self.option_scale_image()):
self.__renderer.draw_image(gc, x, y, out_arr, None)
else:
self.__renderer.draw_image(gc, x, y, out_arr)
class ZoomViewAxes(Axes):
"""
A zoom axes which automatically displays all of the elements it is currently zoomed in on. Does not require
Artists to be plotted twice.
"""
def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform = None, zorder = 5, **kwargs):
"""
Construct a new zoom axes.
:param axes_of_zoom: The axes to zoom in on which this axes will be nested inside.
:param rect: The bounding box to place this axes in, within the parent axes.
:param transform: The transform to use when placing this axes in the parent axes. Defaults to
'axes_of_zoom.transData'.
:param zorder: An integer, the z-order of the axes. Defaults to 5, which means it is drawn on top of most
object in the plot.
:param kwargs: Any other keyword arguments which the Axes class accepts.
"""
if(transform is None):
transform = axes_of_zoom.transData
inset_loc = _make_inset_locator(rect.bounds, transform, axes_of_zoom)
bb = inset_loc(None, None)
super().__init__(axes_of_zoom.figure, bb.bounds, zorder=zorder, **kwargs)
self.__zoom_axes = axes_of_zoom
self.set_axes_locator(inset_loc)
axes_of_zoom.add_child_axes(self)
def draw(self, renderer=None, inframe=False):
super().draw(renderer, inframe)
if(not self.get_visible()):
return
axes_children = [
*self.__zoom_axes.collections,
*self.__zoom_axes.patches,
*self.__zoom_axes.lines,
*self.__zoom_axes.texts,
*self.__zoom_axes.artists,
*self.__zoom_axes.images
]
img_boxes = []
# We need to temporarily disable the clip boxes of all of the images, in order to allow us to continue
# rendering them it even if it is outside of the parent axes (they might still be visible in this zoom axes).
for img in self.__zoom_axes.images:
img_boxes.append(img.get_clip_box())
img.set_clip_box(img.get_window_extent(renderer))
# Sort all rendered item by their z-order so the render in layers correctly...
axes_children.sort(key=lambda obj: obj.get_zorder())
# Construct mock renderer and draw all artists to it.
mock_renderer = TransformRenderer(renderer, self.__zoom_axes.transData, self.transData, self)
for artist in axes_children:
if(artist is not self):
artist.draw(mock_renderer)
# Reset all of the image clip boxes...
for img, box in zip(self.__zoom_axes.images, img_boxes):
img.set_clip_box(box)
# We need to redraw the splines if enabled, as we have finally drawn everything... This avoids other objects
# being drawn over the splines
if(self.axison and self._frameon):
for spine in self.spines.values():
spine.draw(renderer)
使用自定义缩放轴完成的示例:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from zoomaxes import ZoomViewAxes
from matplotlib.transforms import Bbox
import numpy as np
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3, 4, -4, 3)
fig, ax = plt.subplots(figsize=[5, 4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30 + ny, 30:30 + nx] = Z
# extent = [-3, 4, -4, 3]
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins = ZoomViewAxes(ax, Bbox.from_bounds(0.6, 0.6, 0.35, 0.35), ax.transAxes) # Use the new zoom axes...
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
所显示的剧情的图像几乎相同: