淘先锋技术网

首页 1 2 3 4 5 6 7

1 方波的圆周分解

在学习傅里叶变换的时候,有一个经典的示例是方波的分解。我们知道,方波可以分解为无数个正弦波的叠加。而正弦波,又可以看作是圆周运动在一条直线上的投影。当时为了理解这个事情,恐怕大家也花了不少时间。

学习了matplotlib之后,出于学以致用的考虑,我们能不能绘制出动画,来描述上述分解,便于我们来理解呢?

先上动图:
在这里插入图片描述

前面学习中已经掌握了matplotlib如何制作动画,以及如何绘制子图。在这个例子中我们将看到以下内容的实战:

  • 分割画布为子图
  • 绘制圆和波形图
  • 调整图像的轴比例
  • 隐藏图像的刻度轴
  • 设置线型和颜色
  • 生成和保存动画

2. 绘图源码

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

# 把半径放大3倍 画大一些
radius = 4 / np.pi * 3
t = np.deg2rad(list(range(0, 360, 1)))
tc = [tt + 8 for tt in t]

# 一级圆
x = np.cos(t) * radius
y = np.sin(t) * radius

# 二级圆
x1 = np.cos(3*t) * radius / 3
y1 = np.sin(3*t) * radius / 3

# 三级圆
x2 = np.cos(5*t) * radius / 5
y2 = np.sin(5*t) * radius / 5

# 四级圆
x3 = np.cos(7*t) * radius / 7
y3 = np.sin(7*t) * radius / 7

# 子图的切割
fig, ax = plt.subplots(4, 1)
fig.set_size_inches(5, 8)
fig.tight_layout()
ax[0].set(xlim=(-2, 10), ylim=(-6, 6))

#设置横纵坐标等比例且根据轴范围自适应
ax[0].set_aspect("equal", adjustable='datalim')
ax[0].set_axis_off() # 隐藏轴刻度线
# 绘制固定的那个圆
ax[0].plot(x, y, 'b-', linewidth=1)

ax[1].set(xlim=(-2, 10), ylim=(-6, 6))
ax[1].set_aspect("equal", adjustable='datalim')
ax[1].set_axis_off()
ax[1].plot(x, y, 'b-', linewidth=1)

ax[2].set(xlim=(-2, 10), ylim=(-6, 6))
ax[2].set_aspect("equal", adjustable='datalim')
ax[2].set_axis_off()
ax[2].plot(x, y, 'b-', linewidth=1)

ax[3].set(xlim=(-2, 10), ylim=(-6, 6))
ax[3].set_aspect("equal", adjustable='datalim')
ax[3].set_axis_off()
ax[3].plot(x, y, 'b-', linewidth=1)

# 动画准备
artists = []
for i in range(0, len(t)):
	# container用来存储每帧要绘制的内容,通过+=叠加
    container = []
    container += ax[0].plot([0, x[i]], [0, y[i]], 'b-', linewidth=1)
    yy = list(y[i:len(y)])
    yy.extend(y[0:i])
    container += ax[0].plot(tc, yy, color='blue', linewidth=1)
    container += ax[0].plot([x[i], tc[0]], [y[i], yy[0]], 'b--', linewidth=1)

    container += ax[1].plot([0, x[i]], [0, y[i]], 'b-', linewidth=1)
    container += ax[1].plot([x1i + x[i] for x1i in x1], [y1i + y[i] for y1i in y1], 'r-', linewidth=1)

    y_y1 = y+y1
    yy1 = list(y_y1[i:len(y_y1)])
    yy1.extend(y_y1[0:i])
    container += ax[1].plot([x[i], x[i]+x1[i]], [y[i], y[i]+y1[i]], 'r-', linewidth=1)
    container += ax[1].plot(tc, yy1, color='red', linewidth=1)
    container += ax[1].plot([x[i]+x1[i], tc[0]], [y[i]+y1[i], yy1[0]], 'r--', linewidth=1)
  
    container += ax[2].plot([0, x[i]], [0, y[i]], 'b-', linewidth=1)
    container += ax[2].plot([x1i + x[i] for x1i in x1], [y1i + y[i] for y1i in y1], 'r-', linewidth=1)
    container += ax[2].plot([x[i], x[i] + x1[i]], [y[i], y[i] + y1[i]], 'r-', linewidth=1)
    container += ax[2].plot([x2i + x1[i] + x[i] for x2i in x2], [y2i + y1[i] + y[i] for y2i in y2], 'g-', linewidth=1)
    container += ax[2].plot([x[i] + x1[i], x[i] + x1[i] + x2[i]], [y[i] + y1[i], y[i] + y1[i] + y2[i]], 'g-',
                            linewidth=1)

    y_y2 = y + y1 + y2
    yy2 = list(y_y2[i:len(y_y2)])
    yy2.extend(y_y2[0:i])
    container += ax[2].plot(tc, yy2, color='green', linewidth=1)
    container += ax[2].plot([x[i] + x1[i] + x2[i], tc[0]], [y[i] + y1[i] + y2[i], yy2[0]], 'g--',
                            linewidth=1)

    container += ax[3].plot([0, x[i]], [0, y[i]], 'b-', linewidth=1)
    container += ax[3].plot([x1i + x[i] for x1i in x1], [y1i + y[i] for y1i in y1], 'r-', linewidth=1)
    container += ax[3].plot([x[i], x[i] + x1[i]], [y[i], y[i] + y1[i]], 'r-', linewidth=1)
    container += ax[3].plot([x2i + x1[i] + x[i] for x2i in x2], [y2i + y1[i] + y[i] for y2i in y2], 'g-', linewidth=1)
    container += ax[3].plot([x[i] + x1[i], x[i] + x1[i] + x2[i]], [y[i] + y1[i], y[i] + y1[i] + y2[i]], 'g-',
                            linewidth=1)
    container += ax[3].plot([x3i + x2[i] + x1[i] + x[i] for x3i in x3], [y3i + y2[i] + y1[i] + y[i] for y3i in y3],
                            'c-', linewidth=1)
    container += ax[3].plot([x[i] + x1[i] + x2[i], x[i] + x1[i] + x2[i] + x3[i]],
                            [y[i] + y1[i] + y2[i], y[i] + y1[i] + y2[i] + y3[i]], 'c-', linewidth=1)

    y_y3 = y + y1 + y2 + y3
    yy3 = list(y_y3[i:len(y_y3)])
    yy3.extend(y_y3[0:i])
    container += ax[3].plot(tc, yy3, color='cyan', linewidth=1)
    container += ax[3].plot([x[i] + x1[i] + x2[i] + x3[i], tc[0]], [y[i] + y1[i] + y2[i] + y3[i], yy3[0]],
                            'c--', linewidth=1)
    artists.append(container)

# 生成并保存动图
ani = animation.ArtistAnimation(fig=fig, artists=artists, interval=40)
ani.save(filename="c:/users/admin/desktop/fourier.gif", writer="pillow")
plt.show()


3 源码分部解析

3.1 导入包

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

在这个程序里我们要用到matplotlibpyplot模块、animation模块,前者是绘图模块,后者是动画模块。

再者就是需要numpy模块了,绘图通常都需要的,不做过多解释。

3.2 圆的数据准备

# 把半径放大3倍 画大一些
radius = 4 / np.pi * 3
t = np.deg2rad(list(range(0, 360, 1)))
tc = [tt + 8 for tt in t]

# 一级圆
x = np.cos(t) * radius
y = np.sin(t) * radius

# 二级圆
x1 = np.cos(3*t) * radius / 3
y1 = np.sin(3*t) * radius / 3

# 三级圆
x2 = np.cos(5*t) * radius / 5
y2 = np.sin(5*t) * radius / 5

# 四级圆
x3 = np.cos(7*t) * radius / 7
y3 = np.sin(7*t) * radius / 7

我们共打算绘制四级圆,这里使用了圆的坐标分解公式。注意角度和弧度的切换。
同时为了令图画得更大些,我们将圆的半径放大了3倍。
另外注意tc这个变量,这个变量是用来绘制右侧的波形图的,它和t保持同相位变化,但为了在同一个axes中绘制图像,方便连接圆和波形,因此刻意地将其向右平移固定距离,这里设置为8,当然也可以调整。

3.3 子图的切割和不动部分的绘制

# 子图的切割
fig, ax = plt.subplots(4, 1)
fig.set_size_inches(5, 8)
fig.tight_layout()
ax[0].set(xlim=(-2, 10), ylim=(-6, 6))

#设置横纵坐标等比例且根据轴范围自适应
ax[0].set_aspect("equal", adjustable='datalim')
ax[0].set_axis_off() # 隐藏轴刻度线
# 绘制固定的那个圆
ax[0].plot(x, y, 'b-', linewidth=1)

ax[1].set(xlim=(-2, 10), ylim=(-6, 6))
ax[1].set_aspect("equal", adjustable='datalim')
ax[1].set_axis_off()
ax[1].plot(x, y, 'b-', linewidth=1)

ax[2].set(xlim=(-2, 10), ylim=(-6, 6))
ax[2].set_aspect("equal", adjustable='datalim')
ax[2].set_axis_off()
ax[2].plot(x, y, 'b-', linewidth=1)

ax[3].set(xlim=(-2, 10), ylim=(-6, 6))
ax[3].set_aspect("equal", adjustable='datalim')
ax[3].set_axis_off()
ax[3].plot(x, y, 'b-', linewidth=1)

我们首先生成一个4×1的子图阵列,然后为了稍微好看一些,将figure的整体大小设置为宽5英寸、高8英寸。

每一个子图的坐标范围都限定为同样的大小,再通过
axes.set_aspect("equal", adjustable='datalim')函数即可使得每一个子图绘制出来的大小一致。

通过set_axis_off()函数隐去边框轴刻度线。

把每帧图像都会有的四个一级圆绘制出来,这样的好处是不用在每一帧对象中重复绘制,有利于减小最终生成的动图的大小。

3.4 动图帧中的技巧

接下来就到了最关键的动图帧,我们注意到,对于一级圆,在动的部分是半径线、波形图和投影线。

    container += ax[0].plot([0, x[i]], [0, y[i]], 'b-', linewidth=1)
    yy = list(y[i:len(y)])
    yy.extend(y[0:i])
    container += ax[0].plot(tc, yy, color='blue', linewidth=1)
    container += ax[0].plot([x[i], tc[0]], [y[i], yy[0]], 'b--', linewidth=1)

绘制半径很简单,对于第i帧的当前点,即(x[i], y[i]),只需要绘制一条从原点连接至它的线段即可。

接下来的yy是一个技巧:因为波形图是需要利用一个周期内的数据进行循环处理的,即将第i个点到最后一个点放到第0点到第i-1点之前。注意这里的切片、列表转换、list.extend连接函数的使用。

接下来使用tc和yy来绘制第i帧的波形图。
最后将当前点和波形点投影连线到一起。

对于二级以后的圆,则是另一套逻辑。

	container += ax[1].plot([0, x[i]], [0, y[i]], 'b-', linewidth=1)
    container += ax[1].plot([x1i + x[i] for x1i in x1], [y1i + y[i] for y1i in y1], 'r-', linewidth=1)

    y_y1 = y+y1
    yy1 = list(y_y1[i:len(y_y1)])
    yy1.extend(y_y1[0:i])
    container += ax[1].plot([x[i], x[i]+x1[i]], [y[i], y[i]+y1[i]], 'r-', linewidth=1)
    container += ax[1].plot(tc, yy1, color='red', linewidth=1)
    container += ax[1].plot([x[i]+x1[i], tc[0]], [y[i]+y1[i], yy1[0]], 'r--', linewidth=1)

首先绘制一级圆的半径。不赘述。
i帧的二级圆,是以一级圆的当前轨迹点为圆心来绘制的,这里做了一步转换。
之后还是那个波形图的技巧,注意此时波形是两级正弦波的叠加。
再接下来是二级圆的半径绘制。圆心为一级圆的轨迹点(x[i], y[i])。二级圆的轨迹点位置就是(x[i]+x1[i]], y[i]+y1[i]])
后面三级圆和四级圆也是一样依此类推,只是每次都要加上之前的那个圆。

画上波形图和投影线。

这里还有一些优化空间,例如还没有为这幅图加上文字说明(涉及 LaTeX \LaTeX LATEX输入),以及也没有绘制出方波本身。作为一个练习,小白觉得主体的内容已经足够,留待后续优化。

在这里插入图片描述