高效的Matplotlib重绘

18

我正在使用 Matplotlib 允许用户使用鼠标点击选择感兴趣的数据点,使用与 此答案 非常相似的方法。

实际上,散点图显示在热图图像上方,鼠标点击可以添加或删除散点。

我的数据是使用 pcolormesh() 在后台绘制的,因此当我使用 axis.figure.canvas.draw() 更新画布时,散点和背景热图都会重新绘制。考虑到热图的大小,这对于可用界面来说太慢了。

是否有一种方法可以有选择性地只重新绘制散点,而不重新绘制背景?

示例代码:

points = []  # Holds a list of (x,y) scatter points 

def onclick(event):
    # Click event handler to add points
    points.append( (event.x, event.y) )
    ax.figure.canvas.draw()

fig = plt.figure()
ax = plt.figure()
# Plot the background
ax.pcolormesh(heatmap_data)

# Add the listener to handle clicks
cid = fig.canvas.mpl_connect("button_press_event", onclick)
plt.show()

2
你想要使用blitting(这意味着你需要使用基于Agg的后端之一)。请参见https://dev59.com/02ox5IYBdhLWcg3wzXel以了解更多信息。 - tacaswell
此外,如果这有所帮助,请查看这个gist。这是一个更全面的例子。https://gist.github.com/joferkington/6e5bdf8600be2cf4ac79如果Tom或其他人没有先回答,我会稍后尝试撰写完整的答案。 - Joe Kington
1个回答

29
当然可以!你需要的是blitting。如果你不是在编写GUI程序,可以使用matplotlib.animation简化其中的一些部分,但如果你想要交互式显示,就需要直接处理它。
在matplotlib中,你需要使用fig.canvas.copy_from_bbox组合,然后交替调用fig.canvas.restore_region(background),ax.draw_artist(what_you_want_to_draw)和fig.canvas.blit:
background = fig.canvas.copy_from_bbox(ax.bbox)

for x, y in user_interactions:
    fig.canvas.restore_region(background)
    points.append([x, y])
    scatter.set_offsets(points)
    ax.draw_artist(scatter)
    fig.canvas.blit(ax.bbox)

简单的位块传输示例:添加点

如果你只是要添加点,那么你可以跳过保存和恢复背景。但是,这样做会导致抗锯齿点被反复绘制在彼此之上,从而产生一些微妙的绘图变化。

无论如何,以下是你想要的最简单的示例。它仅涉及添加点,并跳过了保存和恢复背景:

import matplotlib.pyplot as plt
import numpy as np

def main():
    fig, ax = plt.subplots()
    ax.pcolormesh(np.random.random((100, 100)), cmap='gray')

    ClickToDrawPoints(ax).show()

class ClickToDrawPoints(object):
    def __init__(self, ax):
        self.ax = ax
        self.fig = ax.figure
        self.xy = []
        self.points = ax.scatter([], [], s=200, color='red', picker=20)
        self.fig.canvas.mpl_connect('button_press_event', self.on_click)

    def on_click(self, event):
        if event.inaxes is None:
            return
        self.xy.append([event.xdata, event.ydata])
        self.points.set_offsets(self.xy)
        self.ax.draw_artist(self.points)
        self.fig.canvas.blit(self.ax.bbox)

    def show(self):
        plt.show()

main()

有时候简单的东西过于简单
然而,假设我们想让右键单击删除一个点。
在这种情况下,我们需要能够恢复背景而不重新绘制它。
好的,一切都很好。我们将使用与我在答案开头提到的伪代码片段类似的东西。
但是,有一个注意事项:如果调整了图形大小,则需要更新背景。同样,如果交互式缩放/平移坐标轴,则需要更新背景。基本上,任何时候绘制图表都需要更新背景。
很快你就需要变得相当复杂。
更复杂:添加/拖动/删除点
这是一般示例的“脚手架”。这种方法有些低效,因为图会被画两次(例如平移将很慢)。有可能解决这个问题,但我会在另一个时间留下这些例子。
这实现了添加点、拖动点和删除点。要在交互式缩放/平移后添加/拖动点,请再次单击工具栏上的缩放/平移工具以禁用它们。
这是一个相当复杂的例子,但希望它能给人们一种构建交互式绘制/拖动/编辑/删除matplotlib艺术家的框架类型的感觉,而无需重新绘制整个图表。
import numpy as np
import matplotlib.pyplot as plt

class DrawDragPoints(object):
    """
    Demonstrates a basic example of the "scaffolding" you need to efficiently
    blit drawable/draggable/deleteable artists on top of a background.
    """
    def __init__(self):
        self.fig, self.ax = self.setup_axes()
        self.xy = []
        self.tolerance = 10
        self._num_clicks = 0

        # The artist we'll be modifying...
        self.points = self.ax.scatter([], [], s=200, color='red',
                                      picker=self.tolerance, animated=True)

        connect = self.fig.canvas.mpl_connect
        connect('button_press_event', self.on_click)
        self.draw_cid = connect('draw_event', self.grab_background)

    def setup_axes(self):
        """Setup the figure/axes and plot any background artists."""
        fig, ax = plt.subplots()

        # imshow would be _much_ faster in this case, but let's deliberately
        # use something slow...
        ax.pcolormesh(np.random.random((1000, 1000)), cmap='gray')

        ax.set_title('Left click to add/drag a point\nRight-click to delete')
        return fig, ax

    def on_click(self, event):
        """Decide whether to add, delete, or drag a point."""
        # If we're using a tool on the toolbar, don't add/draw a point...
        if self.fig.canvas.toolbar._active is not None:
            return

        contains, info = self.points.contains(event)
        if contains:
            i = info['ind'][0]
            if event.button == 1:
                self.start_drag(i)
            elif event.button == 3:
                self.delete_point(i)
        else:
            self.add_point(event)

    def update(self):
        """Update the artist for any changes to self.xy."""
        self.points.set_offsets(self.xy)
        self.blit()

    def add_point(self, event):
        self.xy.append([event.xdata, event.ydata])
        self.update()

    def delete_point(self, i):
        self.xy.pop(i)
        self.update()

    def start_drag(self, i):
        """Bind mouse motion to updating a particular point."""
        self.drag_i = i
        connect = self.fig.canvas.mpl_connect
        cid1 = connect('motion_notify_event', self.drag_update)
        cid2 = connect('button_release_event', self.end_drag)
        self.drag_cids = [cid1, cid2]

    def drag_update(self, event):
        """Update a point that's being moved interactively."""
        self.xy[self.drag_i] = [event.xdata, event.ydata]
        self.update()

    def end_drag(self, event):
        """End the binding of mouse motion to a particular point."""
        for cid in self.drag_cids:
            self.fig.canvas.mpl_disconnect(cid)

    def safe_draw(self):
        """Temporarily disconnect the draw_event callback to avoid recursion"""
        canvas = self.fig.canvas
        canvas.mpl_disconnect(self.draw_cid)
        canvas.draw()
        self.draw_cid = canvas.mpl_connect('draw_event', self.grab_background)

    def grab_background(self, event=None):
        """
        When the figure is resized, hide the points, draw everything,
        and update the background.
        """
        self.points.set_visible(False)
        self.safe_draw()

        # With most backends (e.g. TkAgg), we could grab (and refresh, in
        # self.blit) self.ax.bbox instead of self.fig.bbox, but Qt4Agg, and
        # some others, requires us to update the _full_ canvas, instead.
        self.background = self.fig.canvas.copy_from_bbox(self.fig.bbox)

        self.points.set_visible(True)
        self.blit()

    def blit(self):
        """
        Efficiently update the figure, without needing to redraw the
        "background" artists.
        """
        self.fig.canvas.restore_region(self.background)
        self.ax.draw_artist(self.points)
        self.fig.canvas.blit(self.fig.bbox)

    def show(self):
        plt.show()

DrawDragPoints().show()

太棒了,谢谢!我可以简要地问一下,为什么在这里更喜欢使用imshow而不是pcolormesh - Jordan
@Jordan - 渲染速度更快(10倍至1000倍),并且可以更好地扩展到大数组。 只要您的网格单元是均匀间隔和矩形的,imshow(具有使其等效于pcolor的选项)是更好的选择。 选择pcolormesh的主要原因是a)您希望为每个单元绘制边缘线,b)您在轴上使用非线性缩放,或c)您具有非矩形或非规则单元。 - Joe Kington
2
我在方法safe_draw中遇到了问题。运行时出现错误:QWidget::repaint: Recursive repaint detected。当我注释掉这一行self.draw_cid = canvas.mpl_connect('draw_event, self.grab_background)时,程序似乎可以正常工作,只要我不改变大小。有什么解决办法吗? - Bruno Vermeulen
非常感谢您!这是一个非常好的、清晰的例子,帮助我加快了我的注释速度! - raphael
1
只是提一下,现在matplotlib-doc中还有非常好的示例,可以教你如何实现正确的blitting! https://matplotlib.org/stable/tutorials/advanced/blitting.html#class-based-example - raphael

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接