Matplotlib 全面使用指南 -- 使用 blitting 实现更快速的渲染 Faster rendering by using blitting

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Matplotlib

使用 blitting 实现更快速的渲染

Blitting 是栅格图形中的一种标准技术,在 Matplotlib 中可用于(显著)提升交互式图形的性能。例如,animationwidgets 模块内部就使用了 blitting。下面我们将演示如何在这些类之外自行实现 blitting。

Blitting 通过一次性把所有不变化的图形元素渲染成一张背景图,从而加速重复绘制。之后每次绘图时,只需把变化了的元素绘制到这张背景上即可。例如,如果 Axes 的坐标范围没有改变,我们可以先把不含数据的空 Axes(包括所有刻度与标签)渲染一次,后续只需绘制变化的数据。

具体策略如下:

  • 准备不变的背景:
    • 绘制 figure,但把需要动画的 artist 通过Artist.set_animated 标记为 animated,从而排除在背景之外。
    • 保存一份 RGBA 缓冲区的拷贝。
  • 渲染单张图像:
    • 还原 RGBA 缓冲区的拷贝。
    • 使用 Axes.draw_artist / Figure.draw_artist 重绘那些 animated 的 artist。
    • 把最终图像显示在屏幕上。

该过程的一个结果是:你的 animated artist 永远绘制在静态 artist 之上。

并非所有后端都支持 blitting。你可以通过 FigureCanvasBase.supports_blit 属性检查给定 canvas 是否支持。

警告:以下代码在 macosx 后端上无法工作(但在 Mac 上的其他 GUI 后端可以运行)。

最小示例

我们可以结合 FigureCanvasAggcopy_from_bboxrestore_region 方法,并把 artist 设为 animated=True,来实现一个使用 blitting 加速渲染的最小示例:

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)

# 先停一下,欣赏空坐标轴,并确保至少渲染了一次。
#
# 我们需要先让 figure 以最终尺寸完整绘制到屏幕上,
# 再继续后续步骤,这样:
#  a) 我们可以抓取正确尺寸且已绘制好的背景
#  b) 我们拥有缓存 renderer,使得 ``ax.draw_artist`` 可以工作
# 因此我们先启动事件循环,让后端处理所有挂起操作
plt.pause(0.1)

# 抓取整个 figure(位于 fig.bbox 内)的拷贝,不包含 animated artist
bg = fig.canvas.copy_from_bbox(fig.bbox)
# 绘制 animated artist,这里会使用缓存的 renderer
ax.draw_artist(ln)
# 把结果输出到屏幕,把更新后的 RGBA 缓冲区推送到 GUI 框架
fig.canvas.blit(fig.bbox)

for j in range(100):
    # 重置背景到 canvas 状态,但屏幕不变
    fig.canvas.restore_region(bg)
    # 更新 artist,此时 canvas 状态和屏幕都未改变
    ln.set_ydata(np.sin(x + (j / 100) * np.pi))
    # 重新渲染 artist,更新 canvas 状态,但屏幕不变
    ax.draw_artist(ln)
    # 把图像拷贝到 GUI 状态,但屏幕可能尚未刷新
    fig.canvas.blit(fig.bbox)
    # 刷新所有挂起的 GUI 事件,按需要重绘屏幕
    fig.canvas.flush_events()
    # 如想放慢速度,可插入暂停
    # plt.pause(.1)

img

这个示例可以工作并展示了一个简单动画,但由于我们只抓取了一次背景,如果 figure 的像素尺寸发生变化(由于 figure 尺寸或 dpi 改变),背景就会失效,导致图像错误(但有时看起来还挺酷!)。示例里还出现了全局变量和大量样板代码,提示我们应该把它封装成类。

基于类的示例

我们可以用一个类来封装这些样板逻辑和状态:
恢复背景、绘制 artist、把结果 blit 到屏幕。
此外,可以利用 'draw_event' 回调,在发生完整重绘时重新抓取背景,
从而正确处理窗口大小变化。

class BlitManager:
    def __init__(self, canvas, animated_artists=()):
        """
        参数
        ----------
        canvas : FigureCanvasAgg
            要操作的 canvas,仅适用于拥有 `~FigureCanvasAgg.copy_from_bbox`
            与 `~FigureCanvasAgg.restore_region` 方法的 Agg 子类。

        animated_artists : Iterable[Artist]
            要管理的 artist 列表
        """
        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):
        """与 '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):
        """
        添加要管理的 artist。

        参数
        ----------
        art : Artist
            要添加的 artist。会被设为 'animated'(保险起见)。
            *art* 必须位于此类所管理 canvas 对应的 figure 中。
        """
        if art.figure != self.canvas.figure:
            raise RuntimeError
        art.set_animated(True)
        self._artists.append(art)

    def _draw_animated(self):
        """绘制所有 animated artist。"""
        fig = self.canvas.figure
        for a in self._artists:
            fig.draw_artist(a)

    def update(self):
        """用 animated artist 更新屏幕。"""
        cv = self.canvas
        fig = cv.figure
        # 以防万一我们错过了 draw 事件
        if self._bg is None:
            self.on_draw(None)
        else:
            # 还原背景
            cv.restore_region(self._bg)
            # 绘制所有 animated artist
            self._draw_animated()
            # 更新 GUI 状态
            cv.blit(fig.bbox)
        # 让 GUI 事件循环处理所有事务
        cv.flush_events()

以下展示如何使用这个类。这个例子比前面的稍复杂,因为我们还添加了一个文本帧计数器。

# 创建新 figure
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):
    # 更新 artist
    ln.set_ydata(np.sin(x + (j / 100) * np.pi))
    fr_number.set_text(f"frame: {j}")
    # 通知 blitting manager 进行更新
    bm.update()

img

这个类不依赖 pyplot,可嵌入更大的 GUI 应用程序。

Download Jupyter notebook: blitting.ipynb

Download Python source code: blitting.py

Download zipped: blitting.zip

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值