Fourier animation

Drawing with Fourier series

click Go to view the effect

theoretical basis

Fourier series:

Those who have learned Fourier series, Fourier transform and other knowledge know that the [periodic function] satisfying the [Dirichlet condition] can be fitted with Fourier series.

Dirichlet condition:

  1. x(t) shall be absolutely integrable in any cycle; In any finite interval, x(t) can only take finite maximum or minimum values;
  2. On any finite interval, x(t) can only have finite numbers Type I discontinuity.

Fourier series expression:

\[f(t)=\sum_{k=-∞}^{+∞} a_ke^{jk\frac{2\pi}{T}t} \]

So, another way of thinking, can we also use Fourier series to draw all kinds of lines and combine them into all kinds of graphics? The answer is yes

Principle understanding

Transformation between plane point and f(t)

Two dimensional images are composed of some points. The position of the point is determined by the (x, y) coordinates [take the xy plane as an example], so the coordinates of a point can be represented by a complex number:

\[z=x+jy \]

f(t) can be complex. Adding a series of points can get f(t) at a certain time, so it can be expanded by Fourier series.

Relationship between complex number and circle

Students who have studied complex variable functions know that a complex number can be converted as follows:

\[e^{j\theta} = cos(\theta)+jsin(\theta) \]

Therefore:

\[re^{j\theta} = r[cos(\theta)+jsin(\theta)] \]

The complex number can represent either a point or a vector, so r can be expressed as the length of the vector, θ Can be expressed as the angle of the vector, when θ When it changes within the range of $[0,2 \ PI] $, it means that the vector is rotating around the origin to form a circle.

Get back to the point

Look at this equation again:

\[f(t)=\sum_{k=-∞}^{+∞} a_ke^{jk\omega t}=\sum_{k=-∞}^{+∞} a_ke^{jk\frac{2\pi}{T}t} \]

f(t) represents a point of the graph we want to draw at time t, and the complex index \ (a_ke^{jk\omega t} \) on the right represents that at a certain time t, the vector length is \ (a_k \) and the angle is \ (jk\omega t \), ω That is, the rotation speed of the vector.

Therefore, the right side represents the addition of countless vectors. With the continuous increase of time t and the continuous rotation of vectors, we can continuously calculate all points represented by \ (f(t) \), and connect these points to form the graph we want.

Note: it is necessary to ensure that a figure can be drawn in one stroke, that is, a simply connected figure, such as a square;

Multiple graphs can use \ (f_1(t) and f respectively_ 2(t),f_3(t) \) and so on.

The (x, y) coordinates of a graph are determined by us in advance, that is, we have to determine what graph we want to draw in advance, and then extract the (x, y) coordinates of its contour. Later, we will introduce how to extract the image contour through python

If you determine the (x,y) coordinates, that is, you determine f(t), because f(t) can be a complex number

Now we mainly solve how to expand \ (f(t) \) into Fourier series, that is, determine

\[a_ke^{jk\frac{2\pi}{T}t},(k=...-2,-1,0,1,2...) \]

  • t is a time scale, which changes with time and does not need us to find it;

  • T is the period of f(t), which is determined by the graph we draw, that is, how often we draw the graph, usually the number of points of the graph (outline).

    The unit of T should be seconds. Why is it points? In fact, all points N of the contour are used to represent the number of points drawn in a cycle, so that the graph is drawn just once;

  • j is understood to mean that this is a plural, regardless;

  • \(a_k \) is the only parameter we require, that is, the length value of each vector;

When learning Fourier series, we all know that the coefficient \ (a_k \) is solved as follows:

\[a_k= \frac{1}{T} \int_{-T/2}^{T/2} f(t) e^{-jk\frac{2\pi}{T}t}dt \]

This is an expression under the condition that t is continuous. We can turn it into a discrete case (t changes to \ (\ Delta t \) each time):

\[a_k=\frac{1}{T}\sum_{t=-T/2}^{T/2}[f(t)e^{-jk\frac{2\pi}{T}(t)} \Delta t] \]

The next step is how to calculate \ (a_k \) through code

Note that the range of K \ (k =... - 2, - 1,0,1,2... \) k represents the number of circles. It is determined by yourself. The greater the K, the better the approximation effect. However, the greater the K, the higher the computational complexity and higher requirements for computer performance. It is recommended that the maximum number of points should not exceed N (i.e. all points of f(t))

After learning digital signal processing, you can see the formula of DFT

\[{X}(k)=\sum_{n=0}^{N-1} {x}(n) e^{-j\frac{2\pi}{N}kn} \\ {x}(n)=\sum_{k=0}^{N-1} {X}(k)e^{j\frac{2\pi}{N}kn} \]

Where \ (X(k) \) is actually \ (a_k \), \ (x(n) \) is \ (f(t) \)

code implementation

Find the coefficients of Fourier series

Programming language Javascript

First realize the addition and multiplication of complex numbers, and convert the complex index into a general index

In the code, z=[a, b] is used to represent a complex number z=a+jb. In fact, the representation does not matter. As long as a complex number can be determined, it is important to enable it to realize some basic operations of complex numbers

//Complex multiplication
//z1 = a1 + jb1, denoted by [a1, b1]
//z2 = a2 + jb2, represented by [a2, b2]
function complexMul(z1, z2) {
    return [z1[0] * z2[0] - z1[1] * z2[1], z1[0] * z2[1] + z2[0] * z1[1]];
}

// Complex addition
function complexAdd(z1, z2) {
    return [z1[0] + z2[0], z1[1] + z2[1]];
}

// Complex exponential to complex, z=r*(e^j) θ)= r*cos( θ)+ r*jsin( θ)  among θ Is plural
function r_exp(r, theta) {
    return [r * Math.cos(theta), r * Math.sin(theta)];
}

Then find \ (a_k \), (represented by "Cn" in the code), and carefully figure out the formula when writing:

\[a_k=\frac{1}{T}\sum_{t=-T/2}^{T/2}[f(t)e^{-jk\frac{2\pi}{T}(t)} \Delta t] \]

// K=[0, -1,1, -2,2, -3,3 ...]
function get_K(circleCounts){
    var K = [];  //length = circleCount
    for (var i = 0; i < circleCounts; i++) {
        K[i] = (1 + i >> 1) * (i & 1 ? -1 : 1); //K = [0, -1,1, -2,2, -3,3, -4,4,...]
    }
    return K;
}
//Parameter xn, that is, the coordinate set of f(t),
//circleCounts: that is, the size of k
//L is not required for solving Fourier series. It is used to represent the magnification of Cn, that is, the magnification of vector length, which can enlarge the last drawn figure
function get_Cn(xn, circleCounts, L=1){
    xn_len = xn.length;
    Cn = []       //N is the number of circles
    K = get_K(circleCounts);
    for(var k=0; k<circleCounts; k++){
        Cn[k] = [0, 0];
        for(var n=0; n<xn_len;n++){
            Cn[k] = complexAdd(Cn[k], complexMul(xn[n], r_exp(1, -2*PI*K[k]*n/xn_len)));
        }
        Cn[k][0] /= (xn_len/L);
        Cn[k][1] /= (xn_len/L);
    }
    return Cn;
}

Find Fourier series

Then find \ (f(t) \) according to \ (a_k \), that is, add up the vectors

\[f(t)=\sum_{k=-∞}^{+∞} a_ke^{jk\omega t}=\sum_{k=-∞}^{+∞} a_ke^{jk\frac{2\pi}{T}t} \]

// CX and CY are used to determine where to draw the image, that is, positioning
// n, that is, circleCounts, the greater the approximation effect, the better, but it will also lead to higher performance consumption
// imgIndex indicates which image to draw, because you want to draw several images at the same time, such as f1(t), f2(t), f3(t)
// Speed is used to indicate the drawing speed
function DrawPath(cx, cy, n = 5, imgIndex=0, speed=1) {
    let p = [cx, cy];
    //Sum vectors
    for (var k = 0; k < n; k++) {
        var W = r_exp(1, 2 * PI * (time_n * speed) * K[k] / imgxn[imgIndex].length);   // W-factor
        p = complexAdd(p, complexMul([imgCn[imgIndex][k][0], imgCn[imgIndex][k][1]], W));
    }
    var x = p[0];
    var y = p[1];
    valuePointer[imgIndex] = valuePointer[imgIndex]+1;
    values_x[imgIndex][valuePointer[imgIndex] & pointCount[imgIndex]] = x;
    values_y[imgIndex][valuePointer[imgIndex] & pointCount[imgIndex]] = y;
    context.beginPath();
    context.lineWidth = 2
    context.strokeStyle = "rgba(255,100,200,1)";
    context.moveTo(x, y);
    for (var i = 1; i <= pointCount[imgIndex]; ++i) {
        context.lineTo(values_x[imgIndex][(valuePointer[imgIndex] - i) & pointCount[imgIndex]], values_y[imgIndex][(valuePointer[imgIndex] - i) & pointCount[imgIndex]]);
    }
    context.stroke();
}

Draw circles and vectors:

function DrawCircles(cx, cy, n = 5, imgIndex=0, speed=1) {
    let p = [cx, cy];  //Center of the first circle
    for (var k = 0; k < n; k++) {
        context.beginPath();
        var r = Math.hypot(imgCn[imgIndex][k][0], imgCn[imgIndex][k][1]);
        context.arc(p[0], p[1], r, 0, 2 * PI);
        context.lineWidth = 1;
        context.strokeStyle = "rgba(100,150,60,1.0)";
        if (k != 0) {
            context.stroke();   //The first circle is not drawn
        }
        var W = r_exp(1, 2 * PI * (time_n * speed) * K[k] / imgxn[imgIndex].length);   // W-factor
        p = complexAdd(p, complexMul([imgCn[imgIndex][k][0], imgCn[imgIndex][k][1]], W));
    }
}
function DrawLines(cx, cy, n=5, imgIndex=0, speed=1) {
    context.beginPath();
    let p = [cx, cy];  //The center of the first circle, used to locate the entire drawing
    for (var k = 0; k < n; k++) {

        context.moveTo(p[0], p[1]); //The first line is not drawn
        var W = r_exp(1, 2 * PI * (time_n * speed) * K[k] / imgxn[imgIndex].length);   // W-factor
        p = complexAdd(p, complexMul(imgCn[imgIndex][k], W));
        if(k==0) continue;
        context.lineTo(p[0], p[1]);
    }
    context.lineWidth = 1;
    context.strokeStyle = "rgba(255,255,255,0.8)";
    // context.strokeStyle = "rgba(" + randomx(255) + "," + randomx(255) + "," + randomx(255) + ",0.5)";

    context.stroke();
}

How to extract image contour through python

These are the basic codes required for drawing graphics, but there is another important problem: how to obtain the outline of graphics, that is, obtain \ (f(t) \) [xn in the code]. Only by obtaining \ (f(t) \) first can we obtain \ (a_k \) [Cn in the code]

Method 1

The image contour can be easily obtained through OpenCV. Python 3 calls the opencv library to extract the image contour:

import cv2

img = cv2.imread("../images/kuaile.png")  #Read picture
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # Convert to grayscale

img_gray = cv2.GaussianBlur(img_gray, (1,1), 0, 0) # Gaussian blur

#Turn to binary graph, and those less than the threshold will turn to pure black
ret, img_bin = cv2.threshold(img_gray, 100, 255,cv2.THRESH_BINARY)  
#Extract contour
img_canny = cv2.Canny(img_bin, 0, 1, 1)

cv2.imshow("bin", img_bin)
cv2.imshow("canny", img_canny)

_, contours, hierarchy = cv2.findContours(img_canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cv2.drawContours(img, contours, -1, (255, 0, 255), 1)  #The rightmost parameter is the line thickness
cv2.imshow("result", img)

# Write contour xy coordinates to path Txt in the format pathArr[]=[x1, y1, x2, y2,...]
with open("./path.txt", 'w') as f:
    f.write("pathArr[]=[")
    for i in contours:
        for j in i:
            f.write(str(j[0][0]))
            f.write(",")
            f.write(str(j[0][1]))
            f.write(",")

    f.write("];")

cv2.waitKey(5000)
cv2.destroyAllWindows()


This is the generated path Txt

Method 2

If you are familiar with svg, you can make it into svg vector graph (draw the outline of the graph you need to draw through tools such as photoshop, and then save it as svg picture), or you can extract path

import numpy as np
from svg.path import parse_path

path = """
M53,368H41V354l1-4,1-4,1-2,1-2,1-1,1-2,1-1,2-2,2-2,1-1,2-1,1-1,2-1,2-1,2-1,3-1,4-1H87l4,1,3,1,2,1,2,1,3,2,2,2,2,2,2,2,2,3,1,2,1,2,1,3,1,4v11l-1,5-1,4-1,2-3,4-2,4-3,2-2,3-4,2-4,4-1,1-2,2-1,1H86v1l-2,2-2,2-2,1-1,1-2,1-1,1-2,1-2,2-2,1-2,1-2,2H96l4-1,1-3,2-2,1-6h9v8l-1,6-1,5-2,6-1,5h-8v-5H40v-8l8-6,6-4,4-3,4-4,3-3,3-3,3-2,3-3,5-4,5-4,3-3,1-3,1-2,1-1,1-2,1-2,1-3,1-4v-7l-1-4-1-2-1-3-3-3-4-3-4-1H67l-3,1-3,3-3,2-1,3-1,1-1,2-2,3v0l2-3,1-2,1-1,1-3,3-2,3-3,3-1H80l4,1,4,3,3,3,1,3,1,2,1,4v7l-1,4-1,3-1,2-1,2-1,1-1,2-1,3-3,3-5,4-5,4-3,3-3,2-3,3-3,3-4,4-4,3-6,4-8,6v8h60v5h8l1-5,2-6,1-5,1-6v-8h28l-3-5-1-6V363l1-4,2-5,1-3,1-3,1-4,2-3,2-3,2-2,3-2,3-2,4-3,4-2,6-1h16l8,3,7,5,5,7,5,9,1,6,2,8v31l-2,8-4,6-2,4-3,4-4,4-6,4-4,2-7,2H168l-9-3-8-7-5-5-4-7-1-6h16l6,10,6,5h13l8-7,3-4,1-5,1-4,1-4,1-12,1-1v-4l-1-9v-4l-2-5-1-3-3-7-3-5-6-4-4-1h-7l-5,2-5,6-3,5-2,4-1,8-1,9-1,2v11l1,1v8l1,4,2,4H113
"""

ps = parse_path(path)
xy = []
for p in ps:
    if p.length() == 0:
        continue
    xy.append([p.start.real, p.start.imag])


with open("./path.txt", 'w') as f:
    f.write("pathArr[]=[")
    for i in xy:
        f.write(str(int(i[0])))
        f.write(",")
        f.write(str(int(i[1])))
        f.write(",")
    f.write("];")

The two methods have their own advantages and disadvantages, which can be selected according to their familiarity

Code usage

The complete code has been put into Git Code and Gitee of CSDN. If you need to use it, go to download it yourself

CSDN-GitCode

Gitee

Note: Method 1 is used to obtain the path, and method 2 is similar to that of method 1

  1. First, prepare a picture (the outline of the picture can be drawn in one stroke, preferably black and white and obvious), such as:

  2. Modify imgProcess/get_path.py picture path

  3. Open imgprocess / path Txt, copy all the data (there will be more) to myft / JS / main JS is located as follows

  4. initialization

    //The parameters are:
    //The first pathArr, here is 0
    //The number of circles (400 by default). Select 500 here
    //The path storage capacity (11 by default, 2 * * 11 after internal calculation) is not modified here
    //Magnification (default 1), no magnification here
    initImg(0, 500, 11, 1);
    
  5. mapping

    Call the DrawImg function, and the parameters are [x coordinate (0), y coordinate (50), the drawing number (0 here), and drawing speed (1 here)]

    Note: the xy coordinate here is only the approximate position

  6. After modification, open myft / index HTML to view the effect

Reference tutorial Coding Abas - teach you how to write Fourier animation (the author's writing is very good. I just read what he wrote and understood it. Then I copied it myself. I can learn from it.)

Added by Riseykins on Wed, 05 Jan 2022 00:06:58 +0200