You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Manim傅里叶圆动画效果不符问题技术求助

傅里叶变换动画实现问题

我近期想要制作一个傅里叶变换动画场景,因此定义了新类FourierCircles,期望实现类似某YouTube视频51秒处的效果,但渲染以下代码后,动画结果与预期效果示意图不符。

渲染代码

from manim import *

from ObjectBase.FourierCircles.mobjects import PeriodicValueTracker, ArrayMobject, NestedPath
from ObjectBase.FourierCircles.utils import *

"""
FourierCircles 是一个用于动画展示傅里叶级数(具体为复数傅里叶级数)的 Mobject,
它基于给定图像的边缘(以有序复数集合表示)进行展示,
展示效果参考某YouTube视频51秒处的示例。

初始化流程:加载图像、提取边缘、执行FFT变换,
并创建包含箭头、圆和路径的Mobject。
start_rotate_in_steady_speed方法:让时间值以稳定速度递增。

输入参数:
- n: 使用的傅里叶分量数量,默认值为100
- time: 用于跟踪时间值的PeriodicValueTracker对象
- source_picture: 源图片的路径
- source_array: 可选参数,源图片也可以以numpy数组形式提供

--------------------------------------------------------------------------------
!!!!!!!!!!!!!!!!source_picture 和 source_array 二选一,不可同时提供!!!!!!!!!!!
--------------------------------------------------------------------------------

方法说明:
- start_rotate_in_steady_speed: 让时间值以稳定速度递增
    speed: 时间值递增的速度(弧度/秒)
- set_rotate_value: 将时间值设置为特定值
    value: 要设置的时间值(弧度)
- fade: 路径淡出的速率

--------------------------------------------------------------------------------
!!!!!!!!!!!!!!图片必须为SVG格式且包含路径数据!!!!!!!!!!!!!!!
------------------------------------------------------------------------------
"""
class FourierCircles(Group):
    def __init__(
            self,
            *,
            n: int = 100,
            min_value=0,
            max_value=2 * np.pi,
            source_picture: str = None,
            source_array: np.ndarray = None,
            fade = 0.5
            ):
        ##
        if source_array is None and source_picture is None:
            raise ValueError("必须提供source_picture或source_array其中之一")
        elif source_array is not None and source_picture is not None:
            raise ValueError("source_picture和source_array只能提供一个,二者互斥")

        super().__init__()

        edge_array = extract_svg_edges_vector(
            f"{source_picture}"
        )

        # 初始化设置
        points,_ = normalise(edge_array,return_factor=True)
        self.N = min(n, len(points))
        self.time = PeriodicValueTracker(0,min_value=min_value,max_value=max_value)
        self.fade = fade

        # 提取点的离散傅里叶分量
        amplitudes, frequencies, phases = fft(points, self.N)

        # 向VGroup添加箭头和圆
        arrows = [Arrow(ORIGIN, RIGHT) for _ in range(self.N)]
        circles = [
            Circle(
                radius=abs(amplitudes.tolist()[i]),
                color=TEAL,
                stroke_width=.2, stroke_opacity=.5
            )
            for i in range(self.N)
        ]

        self.add(*arrows, *circles)

        # 创建用于循环计算的数值和点数组
        values = ArrayMobject()
        cumulative = ArrayMobject()

        # 将值设置为 e^i(a + wt),并累加求和
        values.add_updater(
            lambda array, dt:
            array.set_data(
                np.array(
                    [0] + [a * np.exp(1j * (p + self.time.get_value() * f))
                    for a, f, p in
                    zip(amplitudes, frequencies, phases)])
            ),
            call_updater=True
        )
        cumulative.add_updater(
            lambda array, dt:
            array.become(
                values.sum()),
            call_updater=True
        )
        values.update(0)
        cumulative.update(0)

        # 在场景中绘制Mobject
        for i, (arrow, ring) in enumerate(zip(arrows, circles)):
            # 为每个对象分配索引,将圆放置在对应中心,箭头连接上一个点到下一个点
            arrow.idx = i
            ring.idx = i
            ring.add_updater(
                lambda self_ring:
                self_ring.move_to(
                    complex_to_R3(cumulative[self_ring.idx])
                )
            )
            arrow.add_updater(
                lambda self_arrow:
                self_arrow.become(
                    Arrow(
                        complex_to_R3(cumulative[self_arrow.idx]),
                        complex_to_R3(cumulative[self_arrow.idx + 1]),
                        buff=0,
                        max_tip_length_to_length_ratio=.2,
                        stroke_width=2,
                        stroke_opacity=.8
                    )
                )
            )
        # 将最后一个点添加到路径,并设置路径淡出效果
        path = NestedPath()
        path.set_points_as_corners([complex_to_R3(cumulative[-1])] * 2)
        path.add_updater(
            lambda self_path: self_path.updater(
            complex_to_R3(cumulative[-1]), fade)
        )
        self.add(*arrows, *circles,path,values,cumulative)


    # 让时间值以稳定速度递增的方法
    def start_rotate_in_steady_speed(
            self, *,
            speed: float = np.pi,
    ):
        self.time.add_updater(
            lambda mob, dt: mob.increment_value(dt * speed)
        )

    # 将时间值设置在0到2pi之间的指定值
    def set_rotate_value(self, value: float):
        self.time.set_value(value)

    # 示例用法:
    # 假设你有一个名为'example.svg'的SVG文件,包含路径数据。

    # 加载图像并转换为点的方法


# 测试渲染代码
if __name__ == "__main__":
    class test(Scene):
        def construct(self):
            fc = FourierCircles(
                n=1000,
                source_picture="/Users/niuyingkai/PycharmProjects/ManimTestStation/FourierCircles/test_image_violin.svg",
            )
            self.add(fc)
    with tempconfig({"quality": "high_quality","preview": True}):
        scene = test()
        scene.render()

自定义VMobjects代码

from __future__ import annotations
from manim import *
import numpy as np


# 用于存储数组的可更新对象
class ArrayMobject(Mobject):
    def __init__(self, array : np.ndarray = None, data: np.ndarray = np.array([])):
        super().__init__()
        # 存储数据
        self.__data = data
        self.set_data(array)

    # 数据获取器
    @property
    def get_data(self) -> np.ndarray:
        return self.__data

    # 数据设置器
    def set_data(self, data : np.ndarray):
        self.__data = data

    def sum(self) -> ArrayMobject:
        # 累加数据并返回新的Mobject
        return ArrayMobject(np.add.accumulate(self.get_data))

    # 简化索引操作
    def __getitem__(self, idx: int) -> float:
        data_list  = self.get_data.tolist()
        return data_list[idx]

    def become(self, new_obj: ArrayMobject, **kwargs):
        # 不创建新对象,仅更新数据
        self.set_data(new_obj.get_data)
        return self


class NestedPath(VMobject):
    """
    一个可以通过添加新点并淡出旧路径来更新的VMobject。
    """
    def updater(self, point : np.ndarray, fade: float):
        # 将旧路径保存为子对象
        previous_path = NestedPath()
        self.add(previous_path)

        # 添加新点到路径,并设置淡出效果
        previous_path.set_points_as_corners(self.points.copy())
        previous_path.add_updater(lambda path: path.fade(
            fade) if path.get_stroke_opacity() > 2e-2 else path.clear_updaters())

        # 添加新点并截断路径点数量
        self.add_points_as_corners([point])
        self.set_points_as_corners(self.points[-4:])

        return self

"""
PeriodicValueTracker 继承自 ValueTracker,行为类似,
但会在指定范围 [min_value, max_value] 内循环取值。

输入参数:
- min_value: 范围最小值
- max_value: 范围最大值

方法:
- set_value: 设置值,超出范围时自动循环
"""
class PeriodicValueTracker(ValueTracker):
    def __init__(self,value=0,*, min_value: float=0, max_value: float=1, **kwargs):
        self.value = value
        self.min_value = min_value
        self.max_value = max_value
        self.range = max_value - min_value
        super().__init__(min_value, **kwargs)

    def set_value(self, value: float):
        # 将值限制在指定范围内循环
        wrapped_value = ((value - self.min_value) % self.range) + self.min_value
        super().set_value(wrapped_value)

内容的提问来源于stack exchange,提问作者YingKai Niu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 04:09:50