← 探索新知
ASTERXOR / 探索新知

这艘火箭
是用圆画出来的

没有任何一个圆知道火箭长什么样。它们只是按各自的节奏旋转,最后让笔尖走到了恰好的位置。

32个傅里叶旋转向量在某一时刻组成的火箭轮廓
32 次旋转叠加 · 一个笔尖
探索新知ASTERXOR / LEON LEUNG

圆只能绕圈,几个圆凑在一起,怎么就会拐弯了?

把笔固定在一根旋转的杆末端,得到的是一个圆。再在杆的末端接上一根旋转的杆,笔尖的轨迹就开始变得有趣。如果杆的长度、转速和初始方向配合得恰当,它们甚至能描出一艘火箭。

圆并没有学会画火箭 只是拿到了一份很好的说明书
THE IDEA

把一幅画,换成一段运动

先别管火箭,想象一个小光点沿着它的边缘走完一圈。每个时刻,光点都有一个横坐标和一个纵坐标。把这两个数随时间的变化记下来,就记录了整段旅程。

傅里叶分析换了一个问法:能不能把这段旅程拆成一些更简单的运动?这里的简单运动就是旋转。每根旋转的箭头都贡献一点水平和垂直位移,把它们加起来,末端的位置就落在画面中的某个点上。

这与把一个和弦分解成单独的音符有些相似。只不过这里分解的不是声音,而是一条路径。画还是那幅画,只是我们换了一种描述它的方法。

同一条轮廓,依次表示为采样点、旋转系数和重建路径。
FIG 01同一条轮廓,依次表示为采样点、旋转系数和重建路径。
THE MECHANISM

每个圆只需要三条指令

半径决定它贡献的大小。频率决定笔尖走完一圈时,它要转多少圈。初始角度决定它从哪个方向开始转。有些箭头顺时针转,有些则反过来。

箭头首尾相接,因此后一个圆的圆心,会跟着前一根箭头的末端一起移动。图中的金色光点就是整条链的末端,并不是一个偷偷沿着预设火箭线条行走的独立光点。

此外还有一个固定值,负责把整条链放到画面的合适位置。它是所有采样点的平均位置,不需要旋转。本文所说的“32个圆”,不包括这个固定中心。

32个旋转向量链的真实计算快照。蓝色圆表示各向量的运动范围,金色点是最终笔尖。
FIG 0232个旋转向量链的真实计算快照。蓝色圆表示各向量的运动范围,金色点是最终笔尖。
THE EXPERIMENT

八个圆像火箭,三十二个圆有了细节

这次从一条专门绘制的多边形火箭轮廓出发,并不是拿照片来处理。沿着轮廓长度等间隔取1,024个点,再通过快速傅里叶变换,也就是FFT,计算旋转分量。

这里不是只保留转得慢的圆,而是优先保留贡献幅度较大的分量。八个圆已经能让人认出火箭,但两侧的翼很圆润。增加到32个,轮廓的大部分特征都回来了。128个时,尖角也更接近原图。

有趣的不只是“圆越多,细节越多”。更值得留意的是,在细节还没完全回来之前,整体已经很像了。较大的旋转建立大致形状,许多较小的旋转则一起处理那些不好转弯的地方。

金色是重建路径,虚线是原始轮廓。三组都使用相同的1,024个采样点。
FIG 03金色是重建路径,虚线是原始轮廓。三组都使用相同的1,024个采样点。
1,024轮廓采样点
32旋转分量
0.232%平均偏差
THE DETAILS

真正费力的,是拐角

下图较亮的黄色和粉色区域,表示32个圆重建时较大的位置偏差。鼻尖、两侧的翼和尾部都很明显。直线边缘相对好跟随,突然改变方向的地方,会对有限数量的平滑旋转提出更高要求。

只看平均数,很容易忽略这种区别。32个圆的平均距离偏差,是原轮廓外接矩形对角线的0.232%,最大偏差则是1.314%。前者回答平均差多少,后者回答最差的地方差多少。

这次选定的三组结果逐步变好,但不能理解为每加一个圆,每个点、每种误差都会改善。保留幅度最大的傅里叶分量,可以在固定分量数下最小化采样点处的平方误差。平均距离与视觉观感则不是同一个指标。

左:32个圆重建的位置偏差。右:若干分量数量下的平均误差。颜色越亮,局部偏差越大。
FIG 04左:32个圆重建的位置偏差。右:若干分量数量下的平均误差。颜色越亮,局部偏差越大。
圆的数量平均偏差最大偏差
81.550%3.976%
320.232%1.314%
1280.025%0.275%

距离均以原轮廓外接矩形对角线的百分比表示,只在1,024个采样位置计算。固定中心另计。

THE CODE

图像背后,是一个很小的想法

把横坐标放进复数的实部,把纵坐标放进虚部。听起来像突然增加了难度,其实只是把两个坐标装在一起,方便计算。复指数恰好能简洁地表达旋转。

重建时,把各个时刻选中的旋转向量加起来,再加回固定中心。下面是计算的核心片段,并非完整绘图程序。

PYTHON / THE CORE IDEA
coefficients = np.fft.fft(points) / len(points)
frequencies = np.fft.fftfreq(len(points), d=1 / len(points))
selected = np.argsort(-np.abs(coefficients[1:]))[:32] + 1

rotations = coefficients[selected, None] * np.exp(
    2j * np.pi * frequencies[selected, None] * time[None, :]
)
path = coefficients[0] + rotations.sum(axis=0)
THE PERSPECTIVE

漂亮,但不是魔法

把它叫作“用32个圆压缩了一张图片”很诱人,但会漏掉不少前提。这里处理的是一条封闭轮廓,保存的是数值系数,也没有比较真实图片文件的体积。它不能替代JPEG、SVG或者AI图像模型。

有限采样也没有还原出一条无限精确的多边形。保留全部分量后,可以在数值精度范围内还原那1,024个采样点;这些点之间的轨迹,是平滑的周期插值。

我更喜欢这个结果所展示的另一件事:一眼看上去是“画”的东西,也可以理解为一组精心配合的运动。火箭从来没有藏在某一个圆里,而是藏在它们的配合里。

继续阅读

NumPy · Discrete Fourier Transform ↗NumPy · numpy.fft.fft ↗

NumPy文档说明了本文采用的变换约定和FFT。以上图像均由文中描述的计算生成。

留一个想法带走

复杂,有时只是许多简单的东西配合得很好