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
相关产品推荐
相关产品推荐

