0 位图传输技术与快速渲染
Blitting,即位图传输、块传输技术是栅格图形化中的标准技术。在Matplotlib的上下文中,该技术可用于(大幅度)提高交互式图形的性能。例如,动画和小部件模块在内部使用位图传输。在这里,我们将演示如何在这些类之外实现自己的blitting。
位图传输技术通过将所有不变的图形元素渲染到背景图像中来加速重复绘图。然后,对于每次绘图,只需要将不断变化的元素绘制到此背景上。例如,如果一个Axes的限制没有改变,我们可以渲染一次空轴,包括所有刻度和标签,然后只绘制变化的数据。
这种策略是:
- 准备常量背景:
- 绘制图形,但通过将所有要动画化的artists对象标记为动画来排除它们(参看Artist.set_animated);
- 保存一份RBGA缓存的备份。
- 渲染单个图像:
- 还原RBGA缓冲区的副本;
- 使用Axes.draw_artist/Figure.draw_artist重新绘制动画Artist;
- 在屏幕上显示生成的图像。
此过程的一个结果是,你的动态artists始终绘制在静态artists之上。
并非所有的后端都支持传输。你可以通过以下的方式检查给定的画布是否支持这样做:FigureCanvasBase.supports_blit
属性。
警告
此代码不适用于OSX后端(但适用于Mac上的其他GUI后端)。
1 最小示例
我们可以将FigureCanvasAgg
方法copy_from_bbox
和restore_region
结合在artists上设置animated=True
来实现一个使用位图传输来加速渲染的最小示例。
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
fig, ax = plt.subplots()
# animated=True 告诉 matplotlib 只在我们显式请求时才绘制
# 某个artist
(ln,) = ax.plot(x, np.sin(x), animated=True)
# 确保窗口已经显示,但脚本继续运行
plt.show(block=False)
# 停止欣赏空荡荡的窗口,确保至少渲染一次
#
# 我们需要在屏幕上完整绘制这个图形直到最终的大小
# 在我们继续之前 :
# a) 我们有正确的大小和绘制的背景可以抓取
# b) 我们可以渲染缓存,从而 ``ax.draw_artist`` 可以工作
# 所以我们旋转事件循环,让后端处理任何待处理的操作
plt.pause(0.1)
# 获取整张图像的副本(所有fig.bbox中的内容)
bg = fig.canvas.copy_from_bbox(fig.bbox)
# 绘制动画artist, 这里使用了一个缓存渲染
ax.draw_artist(ln)
# 将结果显示到屏幕上,这将RGBA缓存更新从渲染器中推送到GUI框架上
# 从而让你能看得见它
fig.canvas.blit(fig.bbox)
for j in range(100):
# 将背景重置为画布状态,屏幕不变
fig.canvas.restore_region(bg)
# 更新artist, 画布状态和屏幕都保持不变
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
# 重新渲染artist, 更新画布状态, 但不更新屏幕
ax.draw_artist(ln)
# 将图像复制到GUI状态,但屏幕可能尚未改变
fig.canvas.blit(fig.bbox)
# 刷新任何挂起的 GUI 事件,如果需要,重新绘制屏幕
fig.canvas.flush_events()
# 你可以旋转一个暂停在此如果你想要让整个过程降速
# plt.pause(.1)
这个示例有效并显示了一个简单的动画,但是因为我们只抓取了一次背景,如果图形的像素大小发生变化(由于图形的大小或者dpi发生变化),背景就失效,并导致错误发生(但有时看起来会很酷,还有一个全局变量和相当多的样板,这表明我们应该将其包装在一个类中)。
2 基于类的示例
我们可以使用一个类来封装恢复背景、绘制artist,然后将结果传输到屏幕上的样板逻辑和状态。此外,每当发生完全重新绘制以正确处理调整大小时,我们使用"draw_event"回调来捕获新的背景。
class BlitManager:
def __init__(self, canvas, animated_artists=()):
"""
Parameters
----------
canvas : FigureCanvasAgg
The canvas to work with, this only works for subclasses of the Agg
canvas which have the `~FigureCanvasAgg.copy_from_bbox` and
`~FigureCanvasAgg.restore_region` methods.
animated_artists : Iterable[Artist]
List of the artists to manage
"""
self.canvas = canvas
self._bg = None
self._artists = []
for a in animated_artists:
self.add_artist(a)
# 在每次绘图上抓取背景
self.cid = canvas.mpl_connect("draw_event", self.on_draw)
def on_draw(self, event):
"""Callback to register with 'draw_event'."""
cv = self.canvas
if event is not None:
if event.canvas != cv:
raise RuntimeError
self._bg = cv.copy_from_bbox(cv.figure.bbox)
self._draw_animated()
def add_artist(self, art):
"""
Add an artist to be managed.
Parameters
----------
art : Artist
The artist to be added. Will be set to 'animated' (just
to be safe). *art* must be in the figure associated with
the canvas this class is managing.
"""
if art.figure != self.canvas.figure:
raise RuntimeError
art.set_animated(True)
self._artists.append(art)
def _draw_animated(self):
"""Draw all of the animated artists."""
fig = self.canvas.figure
for a in self._artists:
fig.draw_artist(a)
def update(self):
"""Update the screen with animated artists."""
cv = self.canvas
fig = cv.figure
# 如果错过了绘制事件,则什么也不画
if self._bg is None:
self.on_draw(None)
else:
# 还原背景
cv.restore_region(self._bg)
# 绘制所有的动画artists
self._draw_animated()
# 更新GUI状态
cv.blit(fig.bbox)
# 让GUI事件循环处理所有它该干的事儿
cv.flush_events()
以下是我们将如何使用我们的类。这是一个比第一种情况稍微复杂一些的例子,因为我们是学添加了一个文本框架计数器。
# 创建一个新的绘图
fig, ax = plt.subplots()
# 添加一条线
(ln,) = ax.plot(x, np.sin(x), animated=True)
# 添加帧序号
fr_number = ax.annotate(
"0",
(0, 1),
xycoords="axes fraction",
xytext=(10, -10),
textcoords="offset points",
ha="left",
va="top",
animated=True,
)
bm = BlitManager(fig.canvas, [ln, fr_number])
# 确保你的窗口在屏幕上,绘图
plt.show(block=False)
plt.pause(.1)
for j in range(100):
#更新artists
ln.set_ydata(np.sin(x + (j / 100) * np.pi))
fr_number.set_text(f"frame: {j}")
# 告诉位图传输管理器来更新
bm.update()
此类不依赖于pyplot
,适合嵌入到更大的GUI应用程序中。