ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python分形绘图实战:曼德勃罗集、L系统与IFS算法全解析

Python分形绘图实战:曼德勃罗集、L系统与IFS算法全解析 不用装什么重型软件也不用非得懂图形学一台装了Python的电脑就够了。分形与算法绘图这个方向我玩了三四年从最开始照着教程抄代码到后来自己调迭代次数、改颜色映射、做L系统参数化最大的感受是它比普通的数据可视化更接近创作而Python恰好把这个门槛压到了最低。这篇文章我打算把从0到1的路子完整走一遍适合三类人看刚学完Python基础语法、想找个有趣方向练手的已经把matplotlib用得挺熟、但觉得画折线图柱状图太无聊的还有纯粹被分形图吸引、想搞明白背后原理的。文章里不会绕弯子讲一堆数学推导我会把核心公式、代码实现、参数调优和踩坑记录全部摊开保证你照着抄也能画出第一张属于自己的分形图。1. 为什么用Python做分形与算法绘图先说个直觉层面的感受。分形图形最大的特点是自相似——你放大一片海岸线局部跟整体长得差不多再放大还是差不多。这种性质用传统作图工具很难手工画出来因为它的精细程度已经超出了一笔一笔画的上限必须靠算法生成。而算法生成恰恰是Python的舒适区语法直观、生态丰富、调试方便写分形代码就像在写一套图形生长规则而不是苦哈哈地操作像素点。1.1 Python生态凭什么适合干这件事很多做视觉创作的人一上来就想到Processing或者openFrameworks这些工具确实强但Python有自己的独特优势尤其适合边学边玩、边跑边改的工作流。数值计算开箱即用分形的核心几乎是清一色的复数迭代运算NumPy对复数数组的支持非常完善可以直接对整块画布做向量化运算一行代码处理几十万个像素点。可视化方案极灵活matplotlib负责快速出图PIL负责像素级精细控制turtle适合看动态生长过程三个工具覆盖了从看效果到做成品的所有环节。参数调整成本低改一个变量、重新跑一次脚本整个过程就是几秒钟的事。对于需要反复试验迭代次数、颜色映射、缩放范围的分形绘图来说这个重试成本至关重要。资料量庞大从Stack Overflow到各种开源项目分形相关的Python代码随手就能找到一堆。入门时可以参考进阶时可以对比不同实现方式的性能差异。我个人比较推荐把NumPy matplotlib作为主力组合碰到需要离线渲染大批量图片的场景再引入PIL或者imageio。这套组合的稳定性我实测下来非常可靠至少我在三个不同版本的Python环境里跑过同一套分形代码没出过兼容性问题。1.2 分形绘图到底在画什么很多人以为分形绘图是在画形状其实准确说是在画公式。我们定义一个迭代规则然后让计算机把这个规则反复执行几十万次、几百万次每一次执行的结果都对应画布上的一个像素点或者一个图形片段。规则简单但重复次数足够多之后涌现出来的复杂结构就是分形。举个生活化的类比想象你告诉一个人往前走一步然后向右转90度他走完一圈会画出一个正方形。如果再把规则改成往前走一步每次转弯角度递减1度他多走几圈就会画出一条螺旋线。分形就是把这个思路推向极致——规则可以包含分支、递归、缩放、随机性执行次数可以到达上百万次最后呈现的图案自然远超手工绘制的复杂度。分形维数这个概念是理解这类图形的钥匙。普通直线是一维平面是二维而科赫雪花这类曲线虽然画在平面上它的维数大约是1.26——比线更蓬松又没完全填满平面。这个数值不是玄学它直接决定了图案的精细程度和占空间的方式。在后面的算法解读和参数调整里我们不需要手动计算分形维数但理解这个概念后你就明白了为什么有些图案放大总觉得糊有些图案却越放大越有细节——那是无限精细在起作用。2. 核心算法与原理解析分形算法有非常多的流派但真正在Python绘图中经常碰面的主要是三条路线复平面迭代类、递归替换类、随机迭代类。每条路线的数学基础完全不同适用场景和视觉效果也大相径庭。我把它们一个个拆开讲清楚。2.1 曼德勃罗集与逃逸时间算法曼德勃罗集是分形界最出名的图案之一那个像烧焦的仙人掌一样的黑色图形周围环绕着五彩斑斓的火花。它的数学定义非常简洁对每个复数 c从 z 0 开始反复迭代 z z² c。如果迭代值始终不趋于无穷那么 c 就属于曼德勃罗集如果迭代值最终飞出去了c 就不属于集合。关键在于趋于无穷怎么判断。计算机不可能无限迭代下去所以这里用了一个经典技巧——逃逸时间算法检查每一次迭代后的复数模长也就是复数在复平面上到原点的距离一旦模长超过某个阈值通常取2就可以认定这个点迟早会发散于是记录下它在第几步逃逸然后停止对这个点的迭代。为什么阈值恰好选2当 |z| 2 时下一轮的 |z² c| 至少会超过 |z|² - |c|而曼德勃罗集讨论的范围内 |c| 不超过2所以一旦模长破了2这个门槛后续迭代只会越来越大就再也不会回头了。选择2不是拍脑袋而是保证判断严格的数学边界。逃逸时间的核心价值在于它把是否属于集合这种二值判断变成了第几步逃逸这种可以映射成颜色的连续数值。逃逸快的点颜色偏暖逃逸慢的点颜色偏冷图上那些细腻的色彩过渡就是这么来的。2.2 L系统与递归分形树L系统Lindenmayer系统最初是生物学家用来模拟植物生长形态的思路非常有意思用字符串表示当前状态用替换规则生成下一代字符串最后把字符串逐字符翻译成绘图指令。举个最常见的例子分形树公理初始字符串F表示向前画一笔替换规则F替换为F[F][-F]中括号表示保存当前状态画完分支后回来加号表示左转减号表示右转第一代是F第二代变成F[F][-F]第三代F[F][-F][F[F][-F]][-F[F][-F]]——字符串长度呈指数增长画出来的结构也越来越接近真实的树冠形态。L系统的魅力在于规则的微小变化会带来形态的巨大差异。分支角度从25度改成15度树的疏密程度就完全不一样在规则里加一个叶子的符号整棵树就带上了层次感。这种规则驱动形态的思维和直接画图完全不是一个路子它能帮你建立最直观的算法思维。2.3 IFS迭代函数系统IFS全称是Iterated Function System中文叫迭代函数系统它走的是随机路线。核心思想是预先定义好几组仿射变换平移、旋转、缩放、错切的组合每次迭代随机选一组变换施加到当前坐标点上画出来的点云最终会收敛到一个固定的分形吸引子。最经典的栗子是巴恩斯利蕨叶Barnsley Fern。它定义了四组变换系数每一组分别负责画出蕨叶的不同部位茎、下部叶片、左侧叶片、右侧叶片。每次迭代时四组变换被选中的概率不同比如茎的概率很低0.01下部叶片概率最高0.85这样点云的疏密程度刚好模拟出真实蕨叶的质感。IFS的好处是极其容易扩展换一组系数画出来的可能就是一棵树、一片雪花、甚至一座山。很多人在网上看到的几行代码生成一张纹理图多数就是IFS或者它变体的功劳。3. 实操过程与核心代码实现原理讲完马上进入动手环节。这一部分我按环境准备 → 曼德勃罗集 → L系统分形树 → IFS蕨叶 → 颜色方案的顺序依次展开每一段代码我都在Python 3.9及以上版本里实际跑过numpy版本2.x、matplotlib版本3.8以上都兼容。3.1 环境准备与依赖安装最省事的方式是通过pip安装三个核心库pip install numpy matplotlib pillow如果之前没装过Python建议直接去官网下载3.10以上的版本安装时记得勾选Add Python to PATH。装完后在命令行输入python --version能正常输出版本号就说明环境通了。提示如果你用的是Linux系统可能需要用pip3命令替代pipmacOS用户如果遇到权限报错可以在命令后加--user参数。开发工具方面我没用集成度特别高的IDE一个VS Code配上Python插件就够了。函数签名提示、代码补全、直接运行脚本这些核心功能都有跑分形代码完全够用。VSCode里按CtrlShiftP打开命令面板输入Python: Select Interpreter选对Python解释器版本后面就顺了。3.2 曼德勃罗集完整实现很多人第一次写曼德勃罗集用的是双循环遍历每个像素点一个800x600的画布就要跑48万个点每点最多256次迭代纯用Python循环至少得等十几秒体验非常差。我建议直接上NumPy向量化把整个画布当成一个复数矩阵来算十几秒能压缩到一两秒。先看完整代码import numpy as np import matplotlib.pyplot as plt def mandelbrot(width800, height600, x_min-2.5, x_max1.0, y_min-1.2, y_max1.2, max_iter256): # 生成实轴和虚轴坐标 x np.linspace(x_min, x_max, width) y np.linspace(y_min, y_max, height) # 用广播机制构建复数矩阵 c x[np.newaxis, :] 1j * y[:, np.newaxis] z np.zeros_like(c) # escape记录每个点的逃逸迭代次数默认设为max_iter表示未逃逸 escape np.full(c.shape, max_iter, dtypenp.int32) for i in range(max_iter): z z * z c # 找到模长超过2且还没被记录逃逸的点 mask (np.abs(z) 2) (escape max_iter) escape[mask] i # 已逃逸的点固定为一个安全值避免后续重复计算 z[mask] 2 return escape def plot_fractal(escape, cmaphot, figsize(10, 8)): plt.figure(figsizefigsize) plt.imshow(escape, cmapcmap, extent[-2.5, 1.0, -1.2, 1.2]) plt.colorbar(labelescape iteration) plt.title(Mandelbrot Set) plt.tight_layout() plt.show() if __name__ __main__: result mandelbrot() plot_fractal(result)这段代码里有几个关键点值得展开说复数矩阵的构建x[np.newaxis, :]把x数组变成形状为(1, 800)的行向量y[:, np.newaxis]把y变成(600, 1)的列向量两者相加时NumPy自动广播成(600, 800)的二维复数矩阵。这一步是向量化的精髓避免了任何显式循环。逃逸记录的技巧escape数组初始值全部是max_iter一旦某点满足逃逸条件就把它对应的迭代次数记下来同时把z的该位置设为2避免这个点在后续迭代中无限变大导致数值溢出。这个标记隔离的处理手法是这套代码能跑得又快又稳的核心。坐标范围的选择x_min-2.5, x_max1.0这个区间覆盖了曼德勃罗集的主要可见区域其中集本身主体大概在[-2, 0.6]范围内左右留一点空白能让整体构图更平衡。跑完代码你会看到经典的心形主集加四周的细碎触角结构。这时候可以手动改一下四个坐标边界比如把范围缩到x_min-0.8, x_max-0.7, y_min0.1, y_max0.2你会发现在这个很小的区域内依然有完整的、和整体结构相似的花纹——这就是分形自相似性的直观体验。3.3 L系统分形树实现L系统用turtle库实现最直观因为它本身就是一只画笔forward画线、left和right转弯跟L系统指令的对应关系非常自然。先写L系统的通用解析器import turtle def generate_lsystem(axiom, rules, steps): 根据公理、规则和迭代次数生成L系统字符串 seq axiom for _ in range(steps): seq .join(rules.get(ch, ch) for ch in seq) return seq def draw_lsystem(t, seq, angle, length): 解析L系统字符串并绘图 stack [] for ch in seq: if ch F: t.forward(length) elif ch : t.left(angle) elif ch -: t.right(angle) elif ch [: # 保存当前状态位置和朝向 stack.append((t.position(), t.heading())) elif ch ]: # 恢复之前保存的状态 pos, heading stack.pop() t.penup() t.goto(pos) t.setheading(heading) t.pendown()然后是分形树的具体配置def fractal_tree(): t turtle.Turtle() t.speed(0) t.left(90) # 初始朝上 t.penup() t.goto(0, -300) t.pendown() axiom F rules {F: F[F][-F]} steps 6 angle 25 seq generate_lsystem(axiom, rules, steps) draw_lsystem(t, seq, angle, length4) t.hideturtle() turtle.done() if __name__ __main__: fractal_tree()这段代码里最值得体会的是**[和]的组合使用**。遇到[时把当前坐标和朝向存进栈里遇到]时从栈里弹出来恢复状态。这个栈结构模拟了植物生长的分支点记忆画完一个分支后能准确回到分叉处继续画下一个分支。很多初学者第一次想不通这个逻辑建议画两步手动推演一遍马上就能明白为什么分形树能长得这么自然。改几个参数试试steps8树枝层级翻倍画面更茂密但运行时间也会指数级增加建议先从6开始。angle15树形更收拢偏针叶树的形态angle45则更张开接近热带阔叶树。length6配合steps5枝干更粗壮适合单独的老树造型。turtle绘图的速度上限比较低追求高清成品图的话可以把L系统字符串解析成坐标点列表再用matplotlib的plot函数批量绘制速度能快上一个数量级。3.4 IFS蕨叶实现IFS的代码比前面两个都要短核心就是一张变换表和一个随机数。巴恩斯利蕨叶的四组变换系数非常经典我直接给出完整实现import numpy as np import matplotlib.pyplot as plt def barnsley_fern(n_points50000): 通过IFS算法生成巴恩斯利蕨叶点云 points np.zeros((n_points, 2)) x, y 0.0, 0.0 for i in range(n_points): points[i] (x, y) r np.random.random() if r 0.01: # 茎概率1% x, y 0.0, 0.16 * y elif r 0.86: # 下部大叶片概率85% x, y 0.85 * x 0.04 * y, -0.04 * x 0.85 * y 1.6 elif r 0.93: # 左侧小叶片概率7% x, y 0.20 * x - 0.26 * y, 0.23 * x 0.22 * y 1.6 else: # 右侧小叶片概率7% x, y -0.15 * x 0.28 * y, 0.26 * x 0.24 * y 0.44 return points def plot_fern(points): plt.figure(figsize(6, 10)) plt.scatter(points[:, 0], points[:, 1], s0.1, cgreen, alpha0.6) plt.axis(equal) plt.axis(off) plt.show() if __name__ __main__: pts barnsley_fern() plot_fern(pts)这段代码有几个细节容易踩坑概率分配为什么如此悬殊。茎只占1%的点却在视觉上撑起了整个蕨叶的中轴线85%的点都落在大叶片上保证了主体区域的密集度。如果擅自把茎的概率调大到5%画面会变得杂乱无章分形结构就不清晰了。初始化坐标要选(0,0)。IFS的吸引子与初始点无关最终都会收敛到同一形状但在n_points有限的情况下从(0,0)开始可以让点云更快进入稳定状态避免开头出现零星噪点。绘制时用alpha0.6半透明。50万个点全不透明的话大片叶片区域会变成死黑色看不出纹理层次。半透明点叠加才有可能展现出叶片由密到疏的梯度变化。3.5 颜色映射与调色板设计很多人画分形算法没问题、图像形状也对但整体就是不好看。问题十有八九出在颜色映射上。分形图的颜色本质上是把逃逸迭代次数这个数值映射到RGB颜色空间不同的映射方案能产生风格迥异的视觉效果。我常用的三种方案方案做法适合风格matplotlib内置colormap直接指定cmap参数如hot、twilight、turbo快速出图颜色均衡对数归一化对逃逸次数取log后再归一化突出细节区域避免大面积同色自定义HSV渐变把迭代次数映射到色相固定饱和度和亮度极浓烈的艺术效果举一个自定义调色板的例子把逃逸次数通过正弦函数映射到RGB可以生成非常柔和的渐变import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap def custom_colormap(escape, max_iter256): # 将逃逸次数归一化到[0, 1] t escape / max_iter # 用正弦波生成三通道颜色分量相位错开形成彩虹渐变 r (np.sin(2 * np.pi * t) * 0.5 0.5) ** 1.5 g (np.sin(2 * np.pi * t 2.094) * 0.5 0.5) ** 1.5 b (np.sin(2 * np.pi * t 4.188) * 0.5 0.5) ** 1.5 rgb np.stack([r, g, b], axis-1) return np.clip(rgb, 0, 1)这个函数里**1.5是gamma校正可以让中间调的亮度更柔和。每次改变相位偏移量2.094和4.188整体色调就会明显不同——这两个数是120度和240度的弧度制表示正好把色相三等分。实际出图时还可以把逃逸次数的对数结果作为颜色输入效果通常比线性映射好。因为分形边缘的逃逸次数变化非常剧烈对数能压缩极端值让亮部细节保留得更完整。我最常用的组合是对数归一化 twilight colormap出来的图既有冷色的深邃背景又有暖色的边缘光晕非常有质感。4. 常见问题与排查技巧实录我在折腾分形绘图的几年里踩过的坑比画出来的图多多了。这里挑几个各阶段都会遇到的典型问题做一个速查表你们对照着查就行。症状原因解决方案图片全是黑的/全是白的坐标范围设置不当正好落在集合外部或内部深处检查四个坐标边界先用大范围预览整体再逐级缩放运行速度极慢纯Python双循环遍历像素点改用NumPy矩阵向量化见3.2节代码图像边缘锯齿感强烈图像分辨率太低调高width和height参数至少到1200x900放大之后糊成一团迭代次数不够细节没有充分展开把max_iter从256提高到1024同时调大分辨率turtle画树太慢turtle本身是单线程逐笔绘制减少steps层数或者改用matplotlib批量绘制numpy报内存错误分辨率矩阵太大复数数组占用内存过高用np.complex64替代默认的complex128内存减半输出图片颜色刺眼颜色映射用的纯色对比过强改用matplotlib的turbo或自定义HSV渐变避免默认viridis这里单拎出两个后期高频问题详细说。放大局部后画面糊。这几乎全是max_iter没跟上导致的。曼德勃罗集边缘区域的点需要迭代几十上百步才能确认逃逸迭代次数不够时这些点全部被判成未逃逸整个区域就糊成一片单色。解决办法很粗暴把max_iter翻倍再跑。代价是渲染时间增加但画面细节的提升非常明显。我做过一次实测1200x900分辨率下max_iter256大概1.5秒max_iter2048约7秒还在接受范围内。绘制大图时内存爆掉。默认的复数矩阵是complex128类型每个元素占16字节。一个4000x3000的画布光复数矩阵就占4000x3000x16约183MB加上其他临时数组很容易逼近内存上限。我的做法是显式指定数据类型c x[np.newaxis, :].astype(np.float32) 1j * y[:, np.newaxis].astype(np.float32)这样构建出来的复数矩阵是complex64内存直接减半。精度对于视觉输出来说完全够用分形图又不做科学计算不需要双精度。5. 实操心得与进阶方向前面把代码和问题都过了遍最后说点我自己的体会。分形与算法绘图这个方向真正重要的不是死记代码而是建立用规则描述形状的思维。我曾经花了整整一个晚上调整L系统里两个参数——分支角度和迭代层级——只是想看看树能从松树变到柳树的临界值到底在哪。这种试错过程比看十篇教程都管用。几个小的经验总结从曼德勃罗集入门最合适因为它只要一个公式和一个判断条件却能输出无比丰富的图形。等你熟练了逃逸时间算法理解Julia集、Newton分形就顺理成章了——它们的核心逻辑完全一样只是迭代函数和初始条件不同。调色方案决定作品上限。同样的分形结构用hot和twilight能呈现出截然不同的气质。建议把matplotlib所有内置colormap都试一遍然后自己动手写几个渐变函数你会发现颜色映射本身也是一门学问。保存图片用矢量格式。matplotlib里plt.savefig(output.svg)存成矢量图无限放大不模糊。分形图最适合做成超大幅面的装饰画矢量格式放大到60寸也不会失真。我试过用3000x2000的矩阵渲染原图再转PNG清晰度一般但输出SVG之后再转PNG细节保留程度明显更好。进阶方向很多选一条深挖即可Julia集同一个c值对应一套图案可以做成动画连续变化。三维分形用Marching Cubes算法渲染三维曼德勃罗集视觉效果更加震撼。分形地形生成用Perlin噪声叠加不同频率的细节生成假山、岛屿、云层。音乐可视化把分形迭代次数映射成音符频率做出会唱歌的分形。最后分享一个小技巧把曼德勃罗集的迭代次数数组保存下来每次只改变颜色映射函数就能批量生成大量风格完全不同的作品。我后来做装饰画壁纸就是用这一招从同一个分形里提取出了十几种配色方案。你不需要每次都重新跑一遍计算存数组、换调色、出图三步走一件作品就诞生了。
返回列表