使用 Python 和 Tkinter 在傅里叶 Series/Transform 中错位圆

Missalgined circles in Fourier Series/Transform using Python and Tkinter

我制作了一个 Fourier Series/Transform Tkinter 应用程序,到目前为止,一切都按我想要的方式运行,只是我遇到了圆圈未对齐的问题。 这是一张解释我的问题的图片(事后添加了绿色和粉红色以更好地解释问题):

我已将问题缩小到线条的开头,因为它们似乎在正确的位置结束,圆圈也在正确的位置。 正确位置和线条开始位置之间的距离似乎在拉长,但实际上与圆圈旋转的速度成正比,因为圆圈旋转的幅度越大,因此越快。

代码如下:

from tkinter import *
import time
import math
import random
root = Tk()
myCanvas = Canvas(root, width=1300, height=750)
myCanvas.pack()
myCanvas.configure(bg="#0A2239")

global x,y, lines, xList, yList


NumOfCircles = 4

rList = [200]
n=3
for i in range(0, NumOfCircles):
    rList.append(rList[0]/n)
    n=n+2
print(rList)

num = 250/sum(rList)

for i in range(0, NumOfCircles):
    rList[i] = rList[i]*num



x=0
y=0
lines = []
circles = []

centerXList = [300]
for i in range(0,NumOfCircles):
    centerXList.append(0)
    
centerYList = [300]
for i in range(0,NumOfCircles):
    centerYList.append(0)
    
xList = [0]*NumOfCircles
yList = [0]*NumOfCircles

waveLines = []
wavePoints = []
con=0



endCoord = []
for i in range(0, NumOfCircles):
    endCoord.append([0,0])

lastX = 0
lastY = 0

count = 0

randlist = []
n=1
for i in range(0, NumOfCircles):
    randlist.append(200/n)
    n=n+2

def createCircle(x, y, r, canvasName):
    x0 = x - r
    y0 = y - r
    x1 = x + r
    y1 = y + r
    return canvasName.create_oval(x0, y0, x1, y1, width=r/50, outline="#094F9A")

def updateCircle(i):
    newX = endCoord[i-1][0]
    newY = endCoord[i-1][1]
    centerXList[i] = newX
    centerYList[i] = newY
    x0 = newX - rList[i]
    y0 = newY - rList[i]
    x1 = newX + rList[i]
    y1 = newY + rList[i]
    myCanvas.coords(circles[i], x0, y0, x1, y1)
    

def circleWithLine(i):
    global line, lines
    circle = createCircle(centerXList[i], centerYList[i], rList[i], myCanvas)
    circles.append(circle)
    line = myCanvas.create_line(centerXList[i], centerYList[i], centerXList[i], centerYList[i], width=2, fill="#1581B7")
    lines.append(line)


def update(i, x, y):
    endCoord[i][0] = x+(rList[i]*math.cos(xList[i]))
    endCoord[i][1] = y+(rList[i]*math.sin(yList[i]))
    myCanvas.coords(lines[i], x, y, endCoord[i][0], endCoord[i][1])
    xList[i] += (math.pi/randlist[i])
    yList[i] += (math.pi/randlist[i])

def lineBetweenTwoPoints(x, y, x2, y2):
     line = myCanvas.create_line(x, y, x2, y2, fill="white")
     return line

def lineForWave(y1, y2, y3, y4, con):
    l = myCanvas.create_line(700+con, y1, 702+con, y2, 704+con, y3, 706+con, y4, smooth=1, fill="white")
    waveLines.append(l)

for i in range(0,NumOfCircles):
    circleWithLine(i)   

myCanvas.create_line(700, 20, 700, 620, fill="black", width = 3)
myCanvas.create_line(700, 300, 1250, 300, fill="red")

myCanvas.create_line(0, 300, 600, 300, fill="red", width = 0.5)
myCanvas.create_line(300, 0, 300, 600, fill="red", width = 0.5)

while True:
    for i in range(0, len(lines)):
        update(i, centerXList[i], centerYList[i])
    for i in range(1, len(lines)):
        updateCircle(i)
    if count >= 8:
        lineBetweenTwoPoints(lastX, lastY, endCoord[i][0], endCoord[i][1])
        if count % 6 == 0 and con<550:
            lineForWave(wavePoints[-7],wavePoints[-5],wavePoints[-3],wavePoints[-1], con)
            con += 6
    wavePoints.append(endCoord[i][1])
    myCanvas.update()
      


    lastX = endCoord[i][0]
    lastY = endCoord[i][1]
    
    if count != 108:
        count += 1
    else:
        count = 8
        
    time.sleep(0.01)
    
    
    
root.mainloop()

我知道这不是实现我想要实现的目标的最佳方法,因为使用 类 会好得多。我打算这样做,以防万一没人能找到解决方案,并希望重写时,这个问题不会持续存在。

您面临的主要问题是您从计算中收到浮点数,但您只能对像素使用整数。下面我将向您展示您失败的地方以及解决问题的最快方法。

首先你的目标是连接线,你在这里计算点数:

def update(i, x, y):
    endCoord[i][0] = x+(rList[i]*math.cos(xList[i]))
    endCoord[i][1] = y+(rList[i]*math.sin(yList[i]))
    myCanvas.coords(lines[i], x, y, endCoord[i][0], endCoord[i][1])
    xList[i] += (math.pi/randlist[i])
    yList[i] += (math.pi/randlist[i])

当您将以下代码添加到此函数中时,您会发现它在那里失败了。

if i != 0:
    print(i,x,y)
    print(i,endCoord[i-1][0], endCoord[i-1][1])

因为 xy 应该始终与最后一点(上一行的末尾)匹配,即 endCoord[i-1][0]endCoord[i-1][1].

为了解决您的问题,我只是跳过了 后续行 起点的匹配,并使用以下替代函数获取了前一行的坐标:

def update(i, x, y):
    endCoord[i][0] = x+(rList[i]*math.cos(xList[i]))
    endCoord[i][1] = y+(rList[i]*math.sin(yList[i]))
    if i == 0:
        points = x, y, endCoord[i][0], endCoord[i][1]
    else:
        points = endCoord[i-1][0], endCoord[i-1][1], endCoord[i][0], endCoord[i][1]
    myCanvas.coords(lines[i], *points)
    xList[i] += (math.pi/randlist[i])
    yList[i] += (math.pi/randlist[i])

其他建议是

  • 不要使用通配符导入
  • 只导入您在代码中真正使用的内容random您的示例中没有使用
  • global 命名空间中使用 global 是没有用的
  • 创建函数以避免重复代码

def listinpt_times_circles(inpt):
    return [inpt]*CIRCLES

x_list = listinpt_times_circles(0)
y_list = listinpt_times_circles(0)
center_x_list = listinpt_times_circles(0)
center_x_list.insert(0,300)
center_y_list = listinpt_times_circles(0)
center_y_list.insert(0,300)
  • 使用 .after(ms,func,*args) 而不是中断 while 循环和阻塞调用 time.sleep

def animate():
    global count,con,lastX,lastY
    for i in range(0, len(lines)):
        update(i, centerXList[i], centerYList[i])
    for i in range(1, len(lines)):
        updateCircle(i)
    if count >= 8:
        lineBetweenTwoPoints(lastX, lastY, endCoord[i][0], endCoord[i][1])
        if count % 6 == 0 and con<550:
            lineForWave(wavePoints[-7],wavePoints[-5],wavePoints[-3],wavePoints[-1], con)
            con += 6
    wavePoints.append(endCoord[i][1])
    myCanvas.update_idletasks()
      
    lastX = endCoord[i][0]
    lastY = endCoord[i][1]
    
    if count != 108:
        count += 1
    else:
        count = 8

    root.after(10,animate)
    
animate() 
root.mainloop()

list_of_radii = [200] #instead of rList

  • 如前所述,像素将用整数表示,而不是浮点数

myCanvas.create_line(0, 300, 600, 300, fill="red", width = 1) #0.5 has no effect compare 0.1 to 1

  • 如果您想显示更多循环,对每个动画使用 类 和 canvas 会很方便

正如@Thingamabobs 所说,未对齐的主要原因是像素坐标使用整数值。我对您的项目感到兴奋,并决定使用 matplotlib 制作一个示例,这样我就不必使用坐标的整数值。该示例适用于任何函数,我使用正弦波、方波和锯齿波函数实现了示例。

我也尝试遵循一些命名、类型注释等方面的良好做法,希望对您有所帮助

from numbers import Complex
from typing import Callable, Iterable, List

import matplotlib.pyplot as plt
import numpy as np


def fourier_series_coeff_numpy(f: Callable, T: float, N: int) -> List[Complex]:
    """Get the coefficients of the Fourier series of a function.

    Args:
        f (Callable): function to get the Fourier series coefficients of.
        T (float): period of the function.
        N (int): number of coefficients to get.

    Returns:
        List[Complex]: list of coefficients of the Fourier series.
    """
    f_sample = 2 * N

    t, dt = np.linspace(0, T, f_sample + 2, endpoint=False, retstep=True)

    y = np.fft.fft(f(t)) / t.size

    return y


def evaluate_fourier_series(coeffs: List[Complex], ang: float, period: float) -> List[Complex]:
    """Evaluate a Fourier series at a given angle.

    Args:
        coeffs (List[Complex]): list of coefficients of the Fourier series.
        ang (float): angle to evaluate the Fourier series at.
        period (float): period of the Fourier series.

    Returns:
        List[Complex]: list of complex numbers representing the Fourier series.
    """
    N = np.fft.fftfreq(len(coeffs), d=1/len(coeffs))
    N = filter(lambda x: x >= 0, N)

    y = 0
    radius = []
    for n, c in zip(N, coeffs):
        r = 2 * c * np.exp(1j * n * ang / period)
        y += r

        radius.append(r)

    return radius


def square_function_factory(period: float):
    """Builds a square function with given period.

    Args:
        period (float): period of the square function.
    """
    def f(t):
        if isinstance(t, Iterable):
            return [1.0 if x % period < period / 2 else -1.0 for x in t]
        elif isinstance(t, float):
            return 1.0 if t % period < period / 2 else -1.0

    return f


def saw_tooth_function_factory(period: float):
    """Builds a saw-tooth function with given period.
    
    Args:
        period (float): period of the saw-tooth function.
    """
    def f(t):
        if isinstance(t, Iterable):
            return [1.0 - 2 * (x % period / period) for x in t]
        elif isinstance(t, float):
            return 1.0 - 2 * (t % period / period)

    return f


def main():
    PERIOD = 1
    GRAPH_RANGE = 3.0
    N_COEFFS = 30

    f = square_function_factory(PERIOD)
    # f = lambda t: np.sin(2 * np.pi * t / PERIOD)
    # f = saw_tooth_function_factory(PERIOD)

    coeffs = fourier_series_coeff_numpy(f, 1, N_COEFFS)
    radius = evaluate_fourier_series(coeffs, 0, 1)

    fig, axs = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(10, 5))

    ang_cum = []
    amp_cum = []

    for ang in np.linspace(0, 2*np.pi * PERIOD * 3, 200):
        radius = evaluate_fourier_series(coeffs, ang, 1)

        x = np.cumsum([x.imag for x in radius])
        y = np.cumsum([x.real for x in radius])

        x = np.insert(x, 0, 0)
        y = np.insert(y, 0, 0)

        axs[0].plot(x, y)
        axs[0].set_ylim(-GRAPH_RANGE, GRAPH_RANGE)
        axs[0].set_xlim(-GRAPH_RANGE, GRAPH_RANGE)

        ang_cum.append(ang)
        amp_cum.append(y[-1])

        axs[1].plot(ang_cum, amp_cum)

        axs[0].axhline(y=y[-1],
                       xmin=x[-1] / (2 * GRAPH_RANGE) + 0.5,
                       xmax=1.2,
                       c="black",
                       linewidth=1,
                       zorder=0,
                       clip_on=False)

        min_x, max_x = axs[1].get_xlim()
        line_end_x = (ang - min_x) / (max_x - min_x)

        axs[1].axhline(y=y[-1],
                       xmin=-0.2,
                       xmax=line_end_x,
                       c="black",
                       linewidth=1,
                       zorder=0,
                       clip_on=False)

        plt.pause(0.01)

        axs[0].clear()
        axs[1].clear()


if __name__ == '__main__':
    main()