File size: 5,998 Bytes
c77c587
573c3f1
 
70542da
47201a2
1ed08a3
47201a2
573c3f1
 
47201a2
d46ecd3
1c3330f
573c3f1
 
 
 
cad3317
573c3f1
 
c77c587
573c3f1
cad3317
47201a2
 
573c3f1
47201a2
 
 
 
 
 
 
 
573c3f1
 
 
cbcb276
 
c77c587
cbcb276
573c3f1
 
 
c8a65fb
 
c77c587
573c3f1
c77c587
c8a65fb
 
209d481
4e597da
c8a65fb
c77c587
573c3f1
 
c77c587
 
1c3330f
 
c77c587
bdd2388
 
c77c587
 
573c3f1
4e597da
a1ae37d
 
87d3bd4
 
 
 
 
 
 
 
c77c587
573c3f1
 
c77c587
573c3f1
47201a2
3ea9a92
87d3bd4
c77c587
 
20a6f95
87d3bd4
 
 
 
 
 
 
 
 
 
 
 
62895e8
1a3150c
 
 
87d3bd4
 
47201a2
9d0d208
87d3bd4
47201a2
87d3bd4
 
47201a2
b32455d
 
038be2c
 
 
92ce2d5
c77c587
573c3f1
c77c587
 
 
6ee4209
bdd2388
cad3317
573c3f1
 
 
 
 
c77c587
7c848ad
c77c587
573c3f1
c77c587
 
 
b32455d
46b8412
b9d04ad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from PIL import Image
import io
import os
import cv2
from math import tau
import gradio as gr
from concurrent.futures import ThreadPoolExecutor
import tempfile

def fourier_transform_drawing(input_image, frames, coefficients, img_size, blur_kernel_size, desired_range, num_points, theta_points):
    # Convert PIL to OpenCV image
    img = cv2.cvtColor(np.array(input_image), cv2.COLOR_RGB2BGR)
    img = cv2.resize(img, (img_size, img_size), interpolation=cv2.INTER_AREA)
    imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(imgray, (blur_kernel_size, blur_kernel_size), 0)
    _, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
    contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    # find the contour with the largest area
    largest_contour_idx = np.argmax([cv2.contourArea(c) for c in contours])
    largest_contour = contours[largest_contour_idx]
    
    # def combine_all_contours(contours):
    #     combined_contour = np.array([], dtype=np.int32).reshape(0, 1, 2)
    #     for contour in contours:
    #         combined_contour = np.vstack((combined_contour, contour))
    #     return combined_contour

    # largest_contour = combine_all_contours(contours)
    verts = [tuple(coord) for coord in largest_contour.squeeze()]
    xs, ys = np.asarray(list(zip(*verts)))
    x_range, y_range = np.max(xs) - np.min(xs), np.max(ys) - np.min(ys)
    scale_x, scale_y = desired_range / x_range, desired_range / y_range
    xs = (xs - np.mean(xs)) * scale_x
    ys = (-ys + np.mean(ys)) * scale_y

    t_list = np.linspace(0, tau, len(xs))
    t_values = np.linspace(0, tau, num_points)
    f_precomputed = np.interp(t_values, t_list, xs + 1j * ys)

    def compute_cn(f_exp, n, t_values):
        coef = np.trapz(f_exp * np.exp(-n * t_values * 1j), t_values) / tau
        return coef

    N = coefficients
    indices = [0] + [j for i in range(1, N + 1) for j in (i, -i)]

    with ThreadPoolExecutor(max_workers=8) as executor:
        coefs = list(executor.map(lambda n: (compute_cn(f_precomputed, n, t_values), n), indices))

    fig, ax = plt.subplots()
    circles = [ax.plot([], [], 'b-')[0] for _ in range(-N, N + 1)]
    circle_lines = [ax.plot([], [], 'g-')[0] for _ in range(-N, N + 1)]
    drawing, = ax.plot([], [], 'r-', linewidth=2)

    ax.set_xlim(-desired_range, desired_range)
    ax.set_ylim(-desired_range, desired_range)
    ax.set_axis_off()
    ax.set_aspect('equal')
    fig.set_size_inches(15, 15)

    draw_x, draw_y = [], []
    theta = np.linspace(0, tau, theta_points)
    coefs_static = [(np.linalg.norm(c), fr) for c, fr in coefs]
    last_image = None 
    
    # Initialize the background
    fig.canvas.draw()
    background = fig.canvas.copy_from_bbox(ax.bbox)
    
    def animate(i, coefs, time, fig, ax, background, circles, circle_lines, drawing, draw_x, draw_y, coefs_static, theta):
        # Restore the background to erase old frames
        fig.canvas.restore_region(background)
        
        center = (0, 0)
        for idx, (r, fr) in enumerate(coefs_static):
            c_dynamic = coefs[idx][0] * np.exp(1j * (fr * tau * time[i]))
            x, y = center[0] + r * np.cos(theta), center[1] + r * np.sin(theta)
            circle_lines[idx].set_data([center[0], center[0] + np.real(c_dynamic)], [center[1], center[1] + np.imag(c_dynamic)])
            circles[idx].set_data(x, y)
            center = (center[0] + np.real(c_dynamic), center[1] + np.imag(c_dynamic))
    
        draw_x.append(center[0])
        draw_y.append(center[1])
        drawing.set_data(draw_x[:i+1], draw_y[:i+1])
    
        # Draw only the updated elements
        for circle in circles:
            ax.draw_artist(circle)
        for line in circle_lines:
            ax.draw_artist(line)
        ax.draw_artist(drawing)
    
        # Blit only the updated area
        fig.canvas.blit(ax.bbox)
    
        # Capture the current canvas state as a PIL Image
        canvas = fig.canvas
        w, h = canvas.get_width_height()
        buf = np.frombuffer(canvas.buffer_rgba(), dtype=np.uint8)
        image = Image.fromarray(buf.reshape(h, w, 4), 'RGBA').convert('RGB')
        
        return (image, None)

    # Generate and yield images for each frame
    time = np.linspace(0, 1, num=frames)
    for frame in range(frames):
        yield from animate(frame, coefs, )
        yield from animate(frame, coefs, time, fig, ax, background, circles, circle_lines, drawing, draw_x, draw_y, coefs_static, theta)

    # Generate final animation
    with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file:
        anim = animation.FuncAnimation(fig, animate, frames=frames, interval=5, fargs=(coefs, np.linspace(0, 1, num=frames)))
        anim.save(temp_file.name, fps=15)

    yield (last_image, temp_file.name)

# Gradio interface setup
interface = gr.Interface(
    fn=fourier_transform_drawing,
    inputs=[
        gr.Image(label="Input Image", sources=['upload'], type="pil"),
        gr.Slider(minimum=5, maximum=500, value=100, label="Number of Frames"),
        gr.Slider(minimum=1, maximum=500, value=50, label="Number of Coefficients"),
        gr.Number(value=224, label="Image Size (px)", precision=0),
        gr.Slider(minimum=3, maximum=11, step=2, value=5, label="Blur Kernel Size (odd number)"),
        gr.Number(value=400, label="Desired Range for Scaling", precision=0),
        gr.Number(value=1000, label="Number of Points for Integration", precision=0),
        gr.Slider(minimum=50, maximum=500, value=80, label="Theta Points for Animation")
    ],
    outputs=["image", gr.Video()],
    title="Fourier Transform Drawing",
    description="Upload an image and generate a Fourier Transform drawing animation.",
)

if __name__ == "__main__":
    # define queue - required for generators
    interface.queue()
    interface.launch()