4.2 Transformations (坐标变换)


文档摘要

4.2 Transformations 坐标变换 本节摘要:坐标变换是 Matplotlib 把数据点送到屏幕上的那条管道。同一个点,在数据坐标里是一对原始数值,在轴坐标里是相对子图的百分比,在图形坐标里是相对整张画布的百分比,最后在显示坐标里才变成一个真正的像素。四套坐标系各有各的用途,变换对象负责在它们之间换算。本节先讲清这四套坐标系的区别,再讲 Affine2D 的平移、缩放、旋转、剪切怎么用,最后落到组合变换的顺序问题——这是最容易翻车的地方。 上手前先明确 阅读完本节,你应当能够: 说清数据坐标、轴坐标、图形坐标、显示坐标各自的基准与用途。 用 transData、transAxes、transFigure 把对象定位到正确的坐标系。

4.2 Transformations 坐标变换

本节摘要:坐标变换是 Matplotlib 把数据点送到屏幕上的那条管道。同一个点,在数据坐标里是一对原始数值,在轴坐标里是相对子图的百分比,在图形坐标里是相对整张画布的百分比,最后在显示坐标里才变成一个真正的像素。四套坐标系各有各的用途,变换对象负责在它们之间换算。本节先讲清这四套坐标系的区别,再讲 Affine2D 的平移、缩放、旋转、剪切怎么用,最后落到组合变换的顺序问题——这是最容易翻车的地方。

上手前先明确

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

  1. 说清数据坐标、轴坐标、图形坐标、显示坐标各自的基准与用途。
  2. 用 transData、transAxes、transFigure 把对象定位到正确的坐标系。
  3. 用 Affine2D 做平移、缩放、旋转,并解释各方法的意义。
  4. 说明组合变换中加号运算的顺序规则,以及顺序不同结果为何不同。
  5. 判断什么时候该用轴坐标或图形坐标定位,而不是死磕数据坐标。

一、一个点,四套坐标

画图时你给的 (x, y) 是数据坐标——它跟你数据的量纲一致,横轴是年份、纵轴是销售额,那 (2020, 300) 就是字面意思。但屏幕不认识"2020 年",屏幕只认识像素。从 (2020, 300) 到屏幕上的那个亮点,中间要经过不止一次换算。

Matplotlib 把这些中间态整理成四套坐标系。我用"地图"来类比:数据坐标是"经纬度",说的是真实世界的位置;轴坐标和图形坐标是"这张纸上的相对位置",说的是在纸的百分之几处;显示坐标是"印刷机的墨点",说的是最终落在哪个像素上。同一件事,四个说法。

先看一张变换链的全景图:

图:数据坐标到像素坐标的变换链

图:数据坐标到像素坐标的变换链

注意最后一步:图形坐标到显示坐标,不是简单的比例换算,还牵扯画布的物理尺寸和 DPI。你在一台高分屏上把图放大两倍,数据坐标一点没变,像素坐标全变了。这也是为什么"同一个脚本,两台电脑截图出来的点位置不一样"——显示的像素变了,但数据的语义没变。

二、四套坐标,各管一摊事

四套坐标系不是平起平坐的,各有各的主场。数据坐标用来放你的真实数据;轴坐标用来在子图内部做相对定位,比如把文字固定在子图的正中间;图形坐标用来在整张画布上定位,比如给整张图加一个总标题;显示坐标是渲染的终点,你几乎不需要直接碰它。

它们的区别,一张表说清:

坐标系 基准 范围 典型用途 对应变换
数据坐标 数据量纲 随数据和轴设置变化 放折线、散点的真实位置 transData
轴坐标 单个子图 左下 0,0 到右上 1,1 子图内相对定位注解 transAxes
图形坐标 整张画布 左下 0,0 到右上 1,1 总标题、跨子图元素 transFigure
显示坐标 像素 随尺寸与 DPI 变化 渲染终点,一般不手写 无常用入口

怎么用?给 textannotate 这些函数传一个 transform 参数,指定坐标属于哪套系:

ax.text(0.5, 0.5, '居中', transform=ax.transAxes) # 子图正中 fig.text(0.5, 0.02, '底部说明', transform=fig.transFigure) # 画布底部

第一条代码里,0.5, 0.5 不再是数据坐标,而是"子图横竖各一半的位置"。无论你的数据范围是 0 到 1 还是 0 到一百万,这行字都稳稳待在子图正中间。这就是变换的威力:它把"位置"从数据的束缚里解放出来

另外提一句:对数轴、极坐标这些"非线性"的东西,底层也是坐标变换。对数轴就是把数据坐标先取对数再映射,极坐标是把直角坐标换到角度和半径。你平时用 ax.set_xscale('log') 时,其实是在给 x 轴换一套变换。这也是为什么同一个 Affine2D 挂在不同 scale 的轴上,视觉结果会不一样——变换是叠在轴自己的缩放之上的。

三、Affine2D:最常用的变换工具

坐标变换的底层,多数是仿射变换。它干的事很朴素:把点平移、缩放、旋转、剪切,而且变换前后,直线还是直线、平行线还是平行线。对应到 Matplotlib,就是 Affine2D 这个类。它有四个工厂方法,正好对应四种基本操作:

先看平移。它把一个图形整体挪动一段距离:

import matplotlib.transforms as mtransforms trans = mtransforms.Affine2D().translate(2, 3)

translate(2, 3) 的意思是:横轴方向挪 2 个单位,纵轴方向挪 3 个单位。这个"单位"是数据坐标的单位——因为平移通常作用在 transData 之上,2 就是横轴的 2 个数据单位。

缩放、旋转、剪切,写法同样直白:

scale = mtransforms.Affine2D().scale(1.5, 0.8) # 横放大 1.5 倍,纵缩到 0.8 倍 rotate = mtransforms.Affine2D().rotate_deg(30) # 逆时针转 30 度 shear = mtransforms.Affine2D().shear_x(0.3) # 横轴方向剪切 0.3 弧度

真正用到图上,要把这些变换"挂"到数据坐标上。写法是让自定义变换和 ax.transData 相加:

line, = ax.plot(x, y) line.set_transform(scale + ax.transData)

这里有个容易误会的点:scale + ax.transData 里,先执行的是靠右的 ax.transData,再执行靠左的 scale。也就是说,数据点先被换算成轴坐标,再在这个基础上做缩放。顺序千万别反——我们下一节专门讲这个。

为什么这四个操作能共用 Affine2D 一个类?因为它们本质上是同一种东西——一个三乘三矩阵在二维平面上的作用。平移、缩放、旋转、剪切,只是这个矩阵的不同取值。矩阵的左上四格管线性部分,缩放和旋转都藏在这里,靠边两格管平移。你不需要手写矩阵,translaterotate 这些工厂方法已经把矩阵填好了;但知道背后是矩阵,你就明白为什么组合变换其实就是矩阵乘法、为什么顺序不能随便换——矩阵乘法本来就不满足交换律。

⚠️ 常见坑:旋转一个矩形时忘了设 ax.set_aspect('equal')。x 轴和 y 轴刻度比例不一致时,旋转 90 度的正方形会变成歪斜的平行四边形。这不是变换错了,是坐标轴的"尺子"横竖不一样长,旋转自然把形状带歪。

四、组合变换:顺序是命门

单个变换不够用,就要组合。Matplotlib 用加号 + 把变换串起来,规则很反直觉:写在左边的变换后执行,写在右边的变换先执行。这个顺序和数学里函数复合的写法一致,却跟很多人"从左到右"的直觉相反。

看一个具体的差别。假设我们要对一个图形"先平移再旋转",和"先旋转再平移",结果是两个完全不同的位置:

t = mtransforms.Affine2D().translate(3, 0) r = mtransforms.Affine2D().rotate_deg(90) combo_a = r + t # 先平移,再旋转 combo_b = t + r # 先旋转,再平移

combo_a 里,靠右的 t 先执行——图形先向右平移 3 个单位,然后整体绕原点旋转 90 度,结果落到了纵轴方向。在 combo_b 里,靠右的 r 先执行——图形先旋转 90 度(已经转到纵轴),再向右平移,结果跑到横轴负方向去了。同样的两个操作,换个顺序,终点差着十万八千里。

为什么要设计成"从右到左"?因为它和函数复合的写法 f(g(x)) 对齐——g 先作用,f 后作用。Matplotlib 内部把变换串成一串时,也是这么读的。你只要记住一句话:加号右边的先动手,就不会踩坑。

旋转还有个常被忽略的参数:绕哪个点转。默认绕原点转,图形在原点附近无所谓,但图形在 (10, 10) 时,绕原点旋转会把整个图形甩出视野。要绕图形自身中心转,得先平移把中心移到原点、再旋转、再平移回去。这个"平移-旋转-平移"的三段式,是旋转任意图形的标准套路,写一次就记住。

💡 关键直觉:把变换串想象成一条流水线,数据点从最右边进,往左依次经过每道工序。a + b 就是"先过 b 这道工序,再过 a 这道工序"。想清楚数据点先经历什么,顺序自然就对了。

五、什么时候该上坐标变换

不是所有定位都要自己手写变换。多数时候,你只要在 textannotatescatter 里传对 transform 参数就够了。真正需要 Affine2D 的场景,通常是这三类:

第一类,给现有对象叠加偏移。比如让一条线整体上移几个单位,或者给散点加一点抖动避免重叠。第二类,画内置函数画不出的几何图案,比如旋转的文字、倾斜的矩形、分形图案。第三类,做动画——每一帧改一点旋转角度或平移量,图形就动起来了。

举一个完整的例子:把 Matplotlib 预置的单位五角星,放到数据坐标的 (3, 4) 位置,并放大 1.5 倍。

from matplotlib.path import Path from matplotlib.patches import PathPatch import matplotlib.transforms as mtransforms star = PathPatch(Path.unit_regular_star(5), facecolor='gold', edgecolor='black') trans = mtransforms.Affine2D().scale(1.5).translate(3, 4) star.set_transform(trans + ax.transData) ax.add_patch(star)

注意变换的写法:.scale(1.5).translate(3, 4) 是链式调用,靠右的 translate 会先执行。单位五角星默认在原点附近、半径大约为 1,先放大 1.5 倍,再平移到 (3, 4),就得到一个位置尺寸都合心意的星星。这个"先缩放后平移"的固定套路,比直接写顶点坐标快得多,也更不容易数错点。

其余时候,别硬上变换。能用 transAxes 定位就定位,能调 xlim 就调 xlim。变换是精密工具,不是万能钥匙,过度使用只会让代码更难读。

六、逆向变换与混合坐标

正向变换把数据送到屏幕,逆向变换反过来,把屏幕上的点送回数据坐标。什么时候用?交互拾取——用户鼠标点了一下,你只知道像素位置,要把它换算成数据坐标,才能知道点中了哪个数据点。

inv = ax.transData.inverted() x_data, y_data = inv.transform((x_pixel, y_pixel))

inverted() 拿到的是反向变换,把像素坐标变回数据坐标。交互式缩放、十字光标读取数值,底层都是这一招。

还有一种"混合坐标",一半用数据坐标、一半用轴坐标。比如你想让一条竖线"横坐标固定在数据里的某一天,纵坐标永远撑满子图高度",就用 blended_transform_factory

from matplotlib.transforms import blended_transform_factory trans = blended_transform_factory(ax.transData, ax.transAxes) ax.plot([2020, 2020], [0, 1], transform=trans)

这条线的横坐标走数据坐标(2020 这个位置),纵坐标走轴坐标(0 到 1,正好撑满子图)。单个 transData 或 transAxes 都做不到"两套坐标混用",混合变换就是为这种需求生的。它的两个参数,第一个管横轴,第二个管纵轴,各用各的坐标系,互不干扰。

七、几个高频疑问

为什么我传的 transform 没生效?
多半是顺序问题。my_transform + ax.transData 是对的,写反成 ax.transData + my_transform,语义就变了——先做你的变换,再换算到屏幕,多半不是你要的效果。记住加号右边先执行。

transAxes 和 transFigure 长得像,怎么选?
看你要"相对谁"定位。相对单个子图,用 transAxes;相对整张画布、要跨多个子图对齐,用 transFigure。同一个 0.5,0.5,前者是子图中心,后者是画布中心。

Affine2D 能做非线性变换吗?
不能。仿射变换本质是线性加平移,直线还是直线、平行线还是平行线。要扭曲、弯曲、非线性映射,得用更底层的复合变换或自定义变换函数,但那是很少用到的进阶场景,多数需求用 Affine2D 就够。

数据坐标和轴坐标,我该信哪个?
放数据本身信数据坐标,做布局定位信轴坐标。二者不要混用:你想把注解放在"销售额等于 500 的点"旁边,用数据坐标;你想把注解固定在"子图右上角"不动,用轴坐标。混用是坐标变换最常见的翻车来源。

transform 和 transData 是什么关系?
transData 是 Axes 已经算好的一个变换对象,把数据坐标映射到显示坐标。你给 plot 传的 transform 参数,默认就是它。自己叠变换时,通常是在它前面再加一层,所以写成"我的变换 + ax.transData"。可以把 transData 想成最后一站,你的自定义变换是中间站,数据点先过中间站、再到最后一站。

温故知新

  • 四套坐标系:数据、轴、图形、显示,基准和用途各不相同。
  • 变换链:数据坐标经 transData、transAxes、transFigure,最终到像素。
  • 数据坐标:放真实数据,范围和量纲随数据走。
  • 轴坐标与图形坐标:都是 0 到 1 的相对坐标,一个对子图、一个对画布。
  • Affine2D:平移 translate、缩放 scale、旋转 rotate、剪切 shear 四种基本操作。
  • 变换要挂上坐标:用 自定义变换 + ax.transData 的方式作用到数据。
  • 组合顺序反直觉:加号右边的先执行,先平移再旋转和先旋转再平移是两码事。

下一节我们下到最底层:一个形状到底由什么拼出来。你会发现,无论是内置的矩形圆,还是你自己画的任意图形,都逃不出"顶点加代码"这套规则——那就是 Path 和 Patch。


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