跳转至

10.1.架构

Introduction

Matplotlib 是一个 Python 2D绘图库,它是一个开源项目,得到 Python 科学计算社区的全力支持。该项目由生物学家 John Hunter 于2003年发起,最初的作者大部分都是在编程领域自学成才的科学家,现在项目由开源社区开发。

Matplotlib 现在已经被很多人用于分析和研究目的,甚至被 NASA 用于凤凰号火星探测器的数据可视化。值得一提的是,人类捕捉的第一张黑洞照片完全使用 Python 合成,其中就用到了 Matplotlib。

Origin of matplotlib

Matplotlib 的起源可以追溯到 John Hunter 试图把他自己和研究癫痫的同事从分析皮质脑电图(ECoG)的专有软件 (Proprietary software) 中解放出来。在当时,专有的数据可视化软件是一种有限的资源。John Hunter 工作的实验室只有一个软件许可证,各种各样的研究生、医学生、博士后、实习生和调查人员轮流分享硬件密钥。John Hunter 想找到一个可供整个团队使用的替代工具。在当时,MATLAB 已被广泛应用于在生物医学界的数据分析和可视化,因此 Hunter 开始着手基于 MATLAB 开发一个工具用来取代专用软件,并取得了一些成功。这个版本可以被多个研究人员使用和扩展。然而,癫痫手术患者的真实医疗数据具有多种数据模式 (CT、MRI、ECoG、EEG),其复杂性让 MATLAB 在数据管理方面受到挑战。由于不满意 MATLAB 对这项任务的适用性,Hunter 开始在用户界面工具包 GTK + 的基础上开发一个新的 Python 应用程序,在当时 GTK + 是领先的 Linux 桌面视窗系统。

GTK(原名GTK+)最初是GIMP的专用开发库(GIMP Toolkit),后来发展为Unix-like系统下开发图形界面的应用程序的主流开发工具之一。GTK是自由软件,并且是GNU计划的一部分。自2019年2月6日起,GTK+改名为GTK。GTK使用C语言开发,但是其设计者使用面向对象技术,也提供了其他语言(如Python)的接口。

Matplotlib 最初是为这个 GTK+ 应用程序开发的 EEG/ECoG 可视化工具,这个用例指导了它的原始架构。Matplotlib 最初的设计还有第二个目的:作为交互式命令驱动的图形生成工具的替代品,MATLAB 在这一点上做得非常好。MATLAB 的设计使加载数据文件和绘图的任务变得非常简单直接,然而完整的面向对象 API 在语法上过于繁重。因此 matplotlib 还提供了一个有状态的脚本接口,用于快速简单地生成类似于 MATLAB 的图形。

Overview of matplotlib Architecture

Matplotlib 中,包含和管理给定图形中所有元素的顶级 matplotlib 对象称为 Figure。当前 Matplotlib 体系结构围绕着用户创建、表示(渲染)和更新 Figure 对象所需的操作展开。从逻辑上讲,完成这一系列操作的体系结构分为三层,可以将其视为堆栈。这三层从下到上分别是:backend、artist 和scripting。上层知道如何与下面的层进行通信,但下层却不知道上层。


Fig 1
  1. Matplotlib 是一个绘图库,它依赖于某些后端来渲染图片。backend 层可以与不同的运行环境 (backend) 直接交互,为上层提供统一的接口。

  2. 一个图形需要由多个对象组成,这些对象应该被单独修改,并且修改的行为对整个图片的影响是正面的、可预测的。artist 层包含了一张图中每个可视组件的抽象。由于这层与可视化高度相关,该层被认为是创建视觉艺术的一般性概念,因此被称为artist layer。

  3. 最后,还需要支持以编程的方式与图片交互,为用户提供尽可能干净、直观的语法去操作图形。这层被称为脚本层。

Backend layer

matplotlib 的后端可以分为两种:

  1. User interface backends (interactive)
  2. Hardcopy backends (non-interactive)

使用User interface后端时,Figure 会被渲染到用户界面窗口,用户可以通过 User interface backend 的 GUI 事件(如键盘和鼠标输入) 交互地修改图片。使用Hardcopy backend时,Figure会被保存为图片文件。分别有如下的User interface backend 和 Hardcopy backend:

User interface backend Hardcopy backend
GTK 2.x and GTK 3.x PS
wxWidgets PDF
Tk SVG
Qt4 and Qt5 PNG
Mac OS X Cocoa

根据是否支持raster graphics和vector graphics,Hardcopy backend 又可以分为:

  1. 仅支持raster graphics
  2. 仅支持vector graphics
  3. 支持以上两者

交互式后端和非交互后端建立在一些核心的抽象之上,这些基类如下。对于不同的后端,这些基类有不同的具体实现。

  • FigureCanvasBase and FigureManagerBase
  • RendererBase and GraphicsContextBase
  • Event, ShowBase, and Timer

FigureCanvas

FigureCanvasBase是 User interface backend 和 Hardcopy backend 都使用的基类,它表示用于呈现图形的画布。其职责包括:

  1. 持有Figure的引用

  2. 使用Figure更新画布

  3. 定义运行时将被注册的事件方法

  4. 将原生backend的事件转换为 matplotlib 事件抽象框架

  5. 定义绘制方法用于渲染图形

  6. 启动和停止非 GUI 事件循环的方法

使用 Hardcopy backend 时,FigureCanvasBase 可以注册后端支持的图片文件类型。当使用interface backends时,FigureCanvasBase 提供将画布插入backend 窗口的方法。

在 pyplot 模式下运行时,matplotlib 会使用 FigureManagerBase 类。该类包装了FigureCanvasBase 类以及各种 GUI backend 的方法,以便更轻松地呈现图形和接口。

Renderer

在 matplotlib 中,renderer 的工作是提供一个底层的绘图接口,用于在画布上添加墨水。

如上所述,原始 matplotlib 应用程序是 GTK+ 应用程序中的 ECoG 查看器,因此,原始设计的大部分灵感来自当时可用的 gdk/GTK+ API。原始的 renderer API 是受 GDK Drawable 接口的启发,它实现了绘制点、绘制线、绘制矩形、绘制图像、绘制多边形和绘制字形等基本方法。一开始,Matplotlib 为每个额外的后端 (最早的是 PostScript 后端和 GD 后端) 都实现了 GDK Drawable API,并将它们转换为与后端相关的本地绘图命令。这复杂化了新后端的实现,后面这个 API 随后被简化了,这使得将 matplotlib 移植到一个新的用户界面工具包或文件规范的过程更加简单。通过这种抽象,Matplotlib 可以在不同的平台渲染图像或输出图像文件。

此外,许多渲染操作都交给了一个额外的抽象——GraphicsContextBase。此抽象为处理颜色、线条样式、剖面线样式、混合属性和反锯色选项等的代码提供了干净的分隔。

Matplotlib 的一个很好的设计决策是使用C++ 模板库 Anti Grain Geometry 或 agg 的基于核心像素的 renderer,从而实现不同平台之间图像的一致性。这是一个用于渲染抗锯齿的2D 图形的高性能库,可以生成有吸引力的图像。Matplotlib 支持将 agg 后端呈现的像素缓冲插入到其所支持的每个用户界面工具包中,这样就可以跨 UI 和操作系统获得像素精确的图形。因为输出PNG时,Matplotlib 也使用 agg 渲染器,所以硬拷贝和屏幕显示的效果是一样的,因此你所看到的就是从用户界面、操作系统和 PNG 输出中获得的图像。

Event

Matplotlib 后端还有一些逻辑与事件、事件循环和计时有关。这些逻辑分别通过三个基类(及其派生类)实现:

  1. Event:这是 DrawEventMouseEventKeyEvent 等的基类
  2. ShowBase:这是 GUI 后端的模块级别的子类
  3. TimerBase:这是TimerQTTimerGTK3TimerWx的基类

Matplotlib Event 框架将诸如 key-press-Event 或 mouse-motion-Event 之类的 UI 事件映射到 matplotlib 类 KeyEventMouseEvent。用户可以将这些事件连接到回调函数,并与图形和数据进行交互(例如,在Event发生后,自动选择一个数据点或一组点,或操作图形某部分)。

Matplotlib Event框架对各中底层 UI 工具包的事件框架进行了抽象,这允许 matplotlib 开发人员和最终用户以 “write once run everywhere” 的方式编写 UI 事件处理代码。例如,图形在所有用户界面工具包中的交互式平移和缩放,都可以在 matplotlib 事件框架中实现。


Fig 2

Artist layer

matplotlib 大部分代码都位于artist layer之中,实际上,大多数繁琐的工作都在此层完成。位于 artist layer 的 Artist 对象知道如何使用 renderer 在画布 FigureCanvas 上绘制图形。

Artist 对象和 backend layer 之间的耦合发生在 draw 方法中。例如,下面创建了一个模拟类 SomeArtist,它是 Artist 的子类,SomeArtist 必须实现的基本方法是 drawdraw方法需要一个参数 renderer,该参数由 backend layer 传过来。SomeArtist不知道渲染器将在后端 (PDF、 SVG、 GTK+ DrawingArea, etc.) 绘制图像,但它知道渲染器的 API,并会调用适当的API (draw_textdraw_path)。由于渲染器有一个指向画布的指针,并且知道如何在画布上绘制,所以 draw方法将 Artist的抽象表示转换为像素缓冲区中的颜色、 SVG 文件中的路径或者其他任何具体表示。

class SomeArtist(Artist):
    'An example Artist that implements the draw method'

    def draw(self, renderer):
        """Call the appropriate renderer methods to paint self onto canvas"""
        if not self.get_visible():  return

        # create some objects and use renderer to draw self here
        renderer.draw_path(graphics_context, path, transform)

Figure中所有可视的组件(例如 lines、shapes、axes、text等等)都是 Artist 派生类的实例。图3是一个Figure的示意图。

Artist 基类包含了所有 artist 共享的属性:

  • canvas 和 artist 坐标系统之间的转换
  • 可视化
  • 定义 artist 可绘制区域的剪辑框
  • 标签 (Labels)
  • 用于处理用户交互事件的回调注册实例(比如,鼠标在 artist 上点击、挑选某些 artist 等等)

Primitives & Containers

Artist 子类可分为两种:Primitives 和 Containers。 Primitives 是基本的图形对象,Containers 由多个 Primitives 对象组成。它们分别包括但不限于以下内容:

Primitives Containers (Composite artists)
Line2D Figure
Shape (patch) class,e.g. Rectangle, Polygon, Ellipse, Circle, ArcText, Annotation, TextPath XAxis and YAxis
AxesImage and FigureImage Axes, PolarAxes, HammerAxes, MollweideAxes, and LambertAxes
Subplot

Fig 3: A Figure

各个 Aritist 对象从属关系


Fig 4: The hierarchy of artist instances used to draw

通常来说,实例化一个 Figure 对象并将其用于创建一个或多个 AxesSubplot 实例。AxeSubplot 将根据需要创建 Primitive 对象,因此用户不必手动跟踪 Primitive 对象的创建,并将其储存到相应的容器中。

在所有 Containers 中,Axes类是最重要的类之一,因为 Axes实例是大多数 matplotlib 对象的目的地(Primitives 和其他 Containers),并且包含许多辅助方法用于创建 Primitive Artist 并将它们添加到 Axes 实例之中。

除了创建 Primitive 之外,AxesSubplot 还包含了准备 Primitive 所需数据的方法,这些数据在创建Primitive时使用。AxesSubplot 会将这些数据储存到对应的容器中。此外,Axes 对象为 Figure 设置了坐标系,并跟踪可以连接到xlim_changedylim_changed事件的回调。回调在调用时将 Axes实例作为参数。

Collections

artist layer 的另一个组件是 Collections。使用 Collections 类可以高效地绘制大量类似对象。例如,需要创建数万的 circles, polygons, lines等对象。在大多数情况下,如果将这些圆圈、多边形、线等放入 Collections 中,可以获得更好的性能。可用的类包括但不限于PathCollectionCircleCollectionPolyCollectionEllipseCollectionLineCollectionEventCollection


Fig 5

您可能会注意到,基类可能自相矛盾地包含父类。这真的只是对在创建基类时通常创建的父类的引用。在调查 matplotlib 内部时,请记住这一点非常有用。 与逻辑后端图一样,matplotlib 内部不是要全面的。然而,它的目的是为视觉导向的视觉导向,当思考如何结合位在一起的概念帮助。 有了这个,我们被带到了 matplotlib 架构的最后一层。

Scripting layer

对于程序员来说,在编写 web 应用服务器、UI 应用程序或者与其他开发人员共享的脚本时,backend layer 和 artist layer 的 API 是合适编程范型。但对于日常用途,特别是对于实验室科学家、数据科学家(非专业程序员)的交互式探索性工作来说,这些 API 在语法上有点繁重。大多数用于数据分析和可视化的特殊用途语言都提供了一个更轻便的脚本接口,以简化常见任务。Matplotlib 在其 matplotlib.pyplot 接口中也做到了这一点。

pyplot 是一个有状态的接口,它处理大部分样板文件,用于创建图形和轴,并将它们连接到您选择的后端,并维护表示当前图形和轴的模块级内部数据结构,以指导绘图命令。

  • pyplot模块被导入时,它将解析本地配置文件。用户在配置文件中指明默认的后端以及其他设置。如果是像 QtAgg 这样的用户界面后端,pyplot 脚本将导入 GUI 框架并启动一个可嵌入绘图的 Qt 窗口。如果是像 Agg 的纯图像后端,pyplot脚本将生成 hard-copy 然后退出。
  • 选择后端后,pyplot 调用一个 setup 函数执行以下操作:

    • 创建 figure 管理工厂函数,调用该函数时将创建适合所选后端的新 figure 管理器

    • 准备与所选后端匹配的绘图函数

      e.g. plot, gca, savefig, etc. 这些是通过pyploy接口直接调用的函数,具体可以调用什么函数需要考虑是交互式后端还是非交互后端,这取决于 setup 函数返回的结果。

    • 标识与后端 mainloop 函数集成的可调用函数

    • 为所选后端提供模块

    • 当执行绘图命令时(e.g. plot.plot(), plt.hist(), plt.title(), etc.),pyplot 将检查其内部数据结构,以查看当前是否存在 Figure 实例。如果存在,它将提取当前的 Axes 实例,并调用 Axes.plotAxes.hist 之类的API进行绘图。如果当前不存在 Figure 实例,它将创建一个 Figure 实例 和 Axes 实例,并将它们设置为 current,然后调用 Axes 的API。
    • 当执行 plt.show() 命令时,这将强制渲染 Figure,如果用户在配置文件中指定了默认的 GUI 后端,将启动 GUI mainloop,并将所有图形创建到屏幕。

下面显示了 pyplot经常使用的 Line 绘制函数 matplotlib.pyplot.plot 的简化版本,这直观地说明 matplotlib.pyplot.plot 函数如何在 matplotlib 中封装对象和功能。所有其他 pyplot脚本接口功能都遵循相同的设计。

@autogen_docstring(Axes.plot)
def plot(*args, **kwargs):
    ax = gca()

    ret = ax.plot(*args, **kwargs)
    draw_if_interactive()

    return ret

Fig 6

Math Text

由于 Matplotlib 的用户通常具有科学背景,所以直接在图上放置格式丰富的数学表达式是很有用的。来自计算机科学家 Donald Knuth 教授的 $\TeX$ 排版系统可能是最流行的数学排版系统,因此 Matplotlib 也使用 $\TeX$ 语法来输入公式。

Matplotlib 提供了两种渲染数学公式的方法。第一个是 usetex,它使用用户计算机中的完整 TeX 引擎来渲染数学公式。TeX 以其原始的 DVI (与设备无关)格式输出公式中字符和行的位置。然后 Matplotlib 解析 DVI 文件并将其转换为(用于输出的)后端的一组绘图命令,然后直接渲染在图片上。这种方式可以处理大量晦涩的数学公式语法。但是,它要求用户完整安装一个可用的 Tex 系统。因此,Matplotlib 也提供它内部的数学渲染引擎,称为 mathtextmathtext 是 TeX 数学渲染引擎的直接接口,由一个更简单的解析器提供。该解析器使用 pyparsing [McG07] 解析框架编写。

这个接口是基于 TeX 源代码 [Knu86] 编写的。这个简单的解析器构建一个 box 和 glue,然后由布局引擎布局。虽然包含了完整的 TeX 数学渲染引擎,但是没有包含大量的第三方 TeX 和 LaTeX 数学库。mathtext 根据需要移植这些数学库的特性,首要目标是移植经常使用的,但非特定学科的特性。这是一种非常好的、轻量级的方法用来呈现大部分数学公式。

References

  1. Matplotlib - The Python 2D Plotting Library
  2. The Architecture of Open Source Applications Volume Il. Structure Scale and a Few More Fearless Hacks
  3. Hello Matplotlib!
  4. Matplotlib 3-tiered architecture ( Class Diagram (UML))
  5. Convert Markdown to HTML
  6. 脑科学博士申请指南
  7. REST IN PEACE: JOHN HUNTER, MATPLOTLIB AUTHOR, FATHER HAS PASSED AWAY.
  8. History
  9. eht-imaging
  10. CASE STUDY: THE FIRST IMAGE OF A BLACK HOLE
  11. Matplotlib Tutorial