4.3 Path 和 Patch 对象


文档摘要

4.3 Path 和 Patch 对象 本节摘要:Path 和 Patch 是 Matplotlib 图形的最底层。Path 只描述形状的骨架——一串顶点配合一串代码,顶点交代"经过哪些点",代码交代"点与点之间怎么连";Patch 在 Path 之上叠加填充、描边、透明度这些视觉属性,把骨架变成看得见的图形。内置的 Rectangle、Circle、Polygon 都是 Path 的封装。掌握了 Path 和 Patch,你就能画出任何内置函数画不出的形状,也能真正理解散点、柱体这些常见图在底层是怎么拼出来的。 本节导读 阅读完本节,你应当能够: 说清 Path 由顶点和代码两部分组成,以及各自的作用。

4.3 Path 和 Patch 对象

本节摘要:Path 和 Patch 是 Matplotlib 图形的最底层。Path 只描述形状的骨架——一串顶点配合一串代码,顶点交代"经过哪些点",代码交代"点与点之间怎么连";Patch 在 Path 之上叠加填充、描边、透明度这些视觉属性,把骨架变成看得见的图形。内置的 Rectangle、Circle、Polygon 都是 Path 的封装。掌握了 Path 和 Patch,你就能画出任何内置函数画不出的形状,也能真正理解散点、柱体这些常见图在底层是怎么拼出来的。

本节导读

阅读完本节,你应当能够:

  1. 说清 Path 由顶点和代码两部分组成,以及各自的作用。
  2. 区分 MOVETO、LINETO、CURVE3、CURVE4、CLOSEPOLY 这五种代码的行为。
  3. 用顶点加代码构造一个自定义闭合形状,再包成 PathPatch 画出来。
  4. 说出 Patch 与 Path 的关系,以及 PathPatch 相比内置 Patch 的灵活之处。
  5. 判断什么时候该用 PathPatch 自绘,而不是找内置形状硬凑。

一、把一张图拆到最底

画一根折线,你会写 ax.plot(x, y);画一个矩形,你会写 ax.add_patch(Rectangle(...))。但再往下拆一层,这两个东西其实是同一种结构的两种包装:一串点,加上"怎么连这些点"的指令。

这就是 Path 的模型。它只有两样东西:

  • 顶点:一组坐标,交代路径要经过哪些位置。
  • 代码:一组指令,交代从一个顶点到下一个顶点,是画直线、画曲线,还是抬起笔不画。

我用"连点成图"来类比:顶点是图钉,钉在画布上;代码是你手里的笔,决定从这颗图钉到下一颗,是落笔划过去,还是抬笔跳过去。图钉摆对位置,笔法选对,形状就出来了。

还有个事实能帮你把整个知识串起来:连 ax.plot 画的折线,底层也走 Path。Line2D 内部持有自己的路径,渲染时同样是把顶点和代码交给渲染器。所以 Path 不是"画自定义形状"的冷门工具,它是 Matplotlib 一切线条和形状的共同底座。你画折线时看不到它,只是因为它被 Line2D 藏在了幕后。

先看一张全景图,理解"顶点 + 代码 → Path → Patch"这条链路:

图:Path 与 Patch 的组成

图:Path 与 Patch 的组成

这张图里,中间的方形骨架是 Path——它只有轮廓和顶点,颜色信息为零。右边同一个方形,填了色、描了边,才成了我们眼睛认得的"图形"。那个从"骨架"到"图形"的飞跃,靠的就是 Patch 往 Path 上叠视觉属性。

二、五种代码,笔怎么走

Path 的代码是一组整数常量,决定笔怎么走。常用的五种,行为各不相同,一张表说清:

代码 数值 行为
MOVETO 1 抬笔移动到该顶点,不画线
LINETO 2 从当前位置画直线到该顶点
CURVE3 3 用二次贝塞尔曲线画到该顶点,需一个控制点
CURVE4 4 用三次贝塞尔曲线画到该顶点,需两个控制点
CLOSEPOLY 79 从当前位置画直线回到子路径起点,闭合

注意一个细节:代码和顶点是一一对应的。每个顶点配一个代码,数组长度必须一样,否则 Matplotlib 直接报错。MOVETO 是路径的"起笔",它只把笔移到起点,不产生任何线条;LINETO 才真正落笔画直线。

先画一个最朴素的三角形,体会"顶点配代码"的节奏:

import matplotlib.pyplot as plt import matplotlib.path as mpath import matplotlib.patches as mpatches verts = [(0, 0), (1, 0), (0.5, 1), (0, 0)] codes = [mpath.Path.MOVETO, mpath.Path.LINETO, mpath.Path.LINETO, mpath.Path.CLOSEPOLY] path = mpath.Path(verts, codes) patch = mpatches.PathPatch(path, facecolor='orange', edgecolor='black', lw=2) fig, ax = plt.subplots() ax.add_patch(patch) ax.set_xlim(-0.2, 1.2) ax.set_ylim(-0.2, 1.2) ax.set_aspect('equal')

拆开看这段代码:第一个顶点 (0, 0) 配 MOVETO,笔抬起来落到原点;第二个顶点 (1, 0) 配 LINETO,画一条线到右下角;第三个顶点 (0.5, 1) 再配 LINETO,画线到顶部;最后一个顶点 (0, 0) 配 CLOSEPOLY——它其实不怎么看这个坐标,而是直接从当前点画线回到子路径起点,把三角形封口。

⚠️ 常见坑:CLOSEPOLY 后面不要再接 LINETO。CLOSEPOLY 已经把当前子路径收口了,你接着画 LINETO 等于在一个已经闭合的路径上再往外拉线,画出来常常是一条多余的、让人摸不着头脑的线段。闭合就停在闭合处。

还有一个容易被忽略的能力:一个 Path 里可以有多个"子路径"。你在中途再放一个 MOVETO,就等于抬笔另起一段,前面画的是一段,后面画的是另一段,两段互不连接,但属于同一个 Path。画"环中环"或者"两个分离的三角形"时,这招很省事——一个 Path 就能装下多个独立轮廓,不用创建一堆 Path 再逐个 add。

调试 Path 有个土办法:先把顶点用散点标出来,再叠加一个只描边不填充的 PathPatch,一眼就能看出顶点位置和连线方向对不对。多数"画出来形状很怪"的问题,都是某个顶点坐标写错、或者某条代码类型配错,用这个土办法一标就现形。

三、曲线代码:CURVE3 和 CURVE4

直线好懂,曲线靠贝塞尔曲线。CURVE3 是二次贝塞尔,需要"起点 + 一个控制点 + 终点"三个点;CURVE4 是三次贝塞尔,需要"起点 + 两个控制点 + 终点"四个点。控制点的作用是"把线往自己这边拽",它不在最终曲线上,却决定了曲线的弯度。

verts = [(0, 0), (0, 1), (1, 1), (1, 0)] codes = [mpath.Path.MOVETO, mpath.Path.CURVE3, mpath.Path.CURVE4, mpath.Path.LINETO]

这段代码的读法是:MOVETO 落到 (0, 0);CURVE3 用 (0, 1) 作为控制点,把曲线从起点拽到终点 (1, 1)——注意 CURVE3 吃掉两个顶点,一个控制点一个终点;接着 CURVE4 需要两个控制点加一个终点,但这里顶点已经不够了,所以实际写代码时,顶点数量必须跟着代码类型走。二次贝塞尔吃 2 个顶点,三次贝塞尔吃 3 个顶点,直线只吃 1 个。这是 Path 最容易数错的地方:别只数顶点,要按代码的类型去数每条指令吃几个顶点

💡 关键直觉:把控制点想象成"磁铁"。控制点离直线越远、越偏向某侧,曲线就往那侧鼓得越厉害。两个控制点的三次贝塞尔,比一个控制点的二次贝塞尔多了一块"磁铁",所以它能捏出更复杂、更饱满的弯。

画一个"水滴"感受一下控制点的拽力:一个闭合轮廓,左右两边各用一条三次贝塞尔捏出弧线。

verts = [(0, 0), (0, 1), (0.5, 1.2), (1, 1), (1, 0), (0, 0)] codes = [mpath.Path.MOVETO, mpath.Path.CURVE4, mpath.Path.LINETO, mpath.Path.CURVE4, mpath.Path.CLOSEPOLY]

这里 MOVETO 落到左下角,第一个 CURVE4 吃掉三个顶点(两个控制点加终点),把左半边轮廓鼓成弧线;LINETO 拉到右边;第二个 CURVE4 再把右半边收成弧形;最后 CLOSEPOLY 封口。控制点越往外拽,曲线越饱满;把控制点挪到贴近直线,曲线就几乎退化成直线。想捏出顺眼的形状,秘诀就是反复调控制点的位置。

四、Patch 家族:内置形状都是 Path 的马甲

理解了 Path,再回头看那些内置 Patch,你会豁然开朗:Rectangle、Circle、Ellipse、Polygon、Wedge、Arc,本质上都是"预先把 Path 造好、再包一层常用属性"的便捷类。你平时不用自己数顶点,是因为它们替你把顶点和代码都算好了。

用起来也确实方便——一句话就能画个圆:

import matplotlib.patches as mpatches circle = mpatches.Circle((0.5, 0.5), 0.2, facecolor='lightcoral', edgecolor='blue', lw=2) ax.add_patch(circle)

但内置形状有边界:你能画矩形、圆、多边形,却画不了一个五角星、一片枫叶、一个不规则的心形。这些"内置没有的形状",就轮到 PathPatch 上场。它不预设形状,你把任意 Path 塞给它,它就能画:

star = mpatches.PathPatch(mpath.Path.unit_regular_star(5), facecolor='gold', edgecolor='black') ax.add_patch(star)

unit_regular_starunit_circle 这类现成的 Path 是很好的起点,画出来再配合坐标变换做缩放平移(上一节的 Affine2D),就能把单位形状放到任意位置、任意大小。

还有个进阶点值得一提:当一个 Path 有多个子路径、且路径自交时,填充哪个区域由"填充规则"决定——默认的奇偶规则数交点,nonzero 规则看方向。多数简单形状两种规则结果一样,但画环形、自交多边形时,切换填充规则能显著改变填充结果。知道有这么个开关就行,用到时再深挖。

五、什么时候自己造 Path

不是每个形状都值得手搓。我的判断标准很简单:先查内置 Patch,再查能不能用现成 Path 拼,最后才自己数顶点写代码

该自己造 Path 的典型场景:自定义标记(画一个内置没有的特殊符号)、不规则区域的填充(地图上的一块区域)、流程图里的特殊节点。这些场景里,内置形状要么不存在,要么拼出来很别扭。

不该自己造的场景:矩形、圆、标准多边形——内置都有现成的,别重复造轮子。手写 Path 的额外成本是"顶点和代码要配对",多一个少一个都会报错或画出诡异形状,这个维护负担,能省则省。

自己造 Path 还有一个隐蔽的好处:它是"形状"和"外观"的彻底分离。同一个 Path,你可以包成实心填充的 Patch,也可以只描边不填充,还可以换色、加透明度、套路径特效。形状定义一次,外观随意变——这比每次重新画一遍省事得多,也更不容易出不一致。

还有一个高频需求:自定义散点标记。scatter 的 marker 参数除了内置符号,还能接受一个 Path 或 MarkerStyle 对象,把你自己的形状拿去做散点的标记:

from matplotlib.markers import MarkerStyle star_marker = MarkerStyle(Path.unit_regular_star(5)) ax.scatter(x, y, marker=star_marker, s=200)

这等于把"造形状"和"画散点"两条路打通了——你定义一次 Path,既能当 Patch 填充,也能当散点标记用。形状的复用价值,在这里体现得最直接。

六、路径特效:给形状再加一层

Patch 之上还能再叠特效。set_path_effects 能给一个形状加阴影、加描边光晕,让它从背景里跳出来。典型用法是给矩形加个投影,再描一圈亮边:

import matplotlib.patheffects as pe rect = mpatches.Rectangle((0.2, 0.2), 0.6, 0.4, facecolor='skyblue', edgecolor='black', lw=2) rect.set_path_effects([pe.SimpleShadow(), pe.Stroke(linewidth=3, foreground='darkblue')]) ax.add_patch(rect)

SimpleShadow 在图形下面垫一层阴影,Stroke 沿着轮廓再描一圈指定颜色的粗边。顺序有讲究:列表前面的特效画在下面,后面的画在上面。这里阴影在前、描边在后,出来的效果是"阴影垫底、亮边在最上层"。特效不是必需品,但做海报式图表、强调某个重点区域时,它能省掉一堆手动补画的工作。这也再次印证了 Path 和 Patch 的分层设计——形状、外观、特效三层各管各的,互不掺和。

七、几个高频疑问

顶点数和代码数对不上会怎样?
直接报错。Path 要求两者长度严格一致,而且曲线代码实际吃掉的顶点比一个多——CURVE3 吃两个、CURVE4 吃三个。所以别只盯着数组长度,还得数每条曲线指令到底消费了几个顶点。

CLOSEPOLY 的顶点坐标要写回起点吗?
可以写,也可以随便填一个数,因为 CLOSEPOLY 会无视这个顶点坐标,直接从当前点连回子路径起点。但为了可读性,我习惯还是把起点坐标再写一遍,提醒自己这里是在收口。

PathPatch 和内置 Patch 该选哪个?
形状内置有就用内置,没有才用 PathPatch。内置的优点是参数友好、不易出错;PathPatch 的优点是形状自由。判断标准就一条:你要的形状,内置类能不能一句话画出来。

散点和柱体也是 Path 吗?
散点、柱体这类批量图元,底层用的是 Collection,它内部管理着大量 Path 或顶点,一次性交给渲染器,所以比逐个画 Patch 快得多。这也是性能优化那章会反复提到的"批量优于循环"的源头。

Path 的坐标是数据坐标吗?
是。Path 里的顶点默认按数据坐标解释,最终经由 Patch 的 transform 换算到屏幕。所以同一个 Path,换一个 transform,就能出现在数据空间里的不同位置和大小。这也是为什么造形状时我习惯在原点附近、单位尺寸下造,再用变换去放——形状和位置彻底解耦。

要点速记

  • Path 两要素:顶点交代经过哪些点,代码交代点与点怎么连。
  • 五种代码:MOVETO 落点、LINETO 直线、CURVE3 与 CURVE4 曲线、CLOSEPOLY 闭合。
  • 顶点代码一一对应:长度必须一致,且曲线代码吃的顶点数更多。
  • Path 只管形状:没有颜色,是图形的骨架。
  • Patch 叠外观:填充、描边、透明度都在这一层,把骨架变成图形。
  • PathPatch 最灵活:内置形状没有的,用 PathPatch 加任意 Path 画。
  • 先查内置再自造:矩形圆多边形有现成的,别为省事反而多写代码。

至此,第 4 章前半部分的三块拼图齐了:Artist 是对象的地图,坐标变换是位置的换算,Path 和 Patch 是形状的原料。合在一起,你就能从"会用 Matplotlib"迈到"看得懂它、也改得动它"。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U