ARTICLE DETAIL

资讯详情

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

Python分形绘图入门:从递归到曼德勃罗集,用代码生成艺术

Python分形绘图入门:从递归到曼德勃罗集,用代码生成艺术 1. 从一条曲线到一片森林为什么分形值得用Python亲手画如果你刷到过那种“一棵树不断分叉、每个分叉又继续分叉最终长成一整片丛林”的动图大概率就是分形。而Python恰好是把这套数学可视化做得最舒服的语言——不需要什么高端显卡不用装几十个G的专业绘图软件一个解释器加两个库就能把分形公式变成肉眼可见的漂亮图案。“用Python生成艺术分形与算法绘图”这个需求背后实际要解决三件事第一理解分形到底是什么否则你只会复制代码改一个参数就懵第二掌握最核心的绘图套路递归、迭代、坐标变换翻来覆去就那几招第三踩掉那些能让人瞬间失去耐心的坑窗口闪退、图像卡死、颜色瞎糊一脸。这篇文章就是按这条线走的用我实际跑过的代码、调过的参数、翻过的车给你一条能直接照抄的路径。适合谁看刚学Python想找点正反馈的初学者、想用程序生成纹理或图形素材的设计师、以及单纯觉得数学图像很酷的爱好者。不需要高数基础会写print就能开始。2. 先搞懂分形再动手画三种最常用的生成思路很多人一上来就搜“分形代码”其实分形不是某个固定算法而是一类“自相似”的几何结构——局部放大后和整体长得差不多。理解到这个层面你才能自己改参数、组合玩法。在实际编码里生成分形基本跑不出下面三种路子。2.1 递归分形最直观的“套娃”逻辑典型代表是科赫雪花Koch Snowflake和分形树。核心思想是把一条线段替换成某种折线然后对折线上的每一段再做同样的替换直到替换次数达到设定值。用代码表达大概长这样import turtle def koch(t, length, depth): if depth 0: t.forward(length) else: for angle in [60, -120, 60, 0]: t.left(angle) koch(t, length / 3, depth - 1) t turtle.Turtle() t.speed(0) t.penup(); t.goto(-200, 100); t.pendown() t.color(steelblue) for _ in range(3): koch(t, 400, 3) t.right(120) turtle.done()这段代码的关键在于“递归深度”。depth每减一层线段长度就除以3但你调用的次数是4的幂次增长——深度3时一共要画4^364段深度6就要画4096段。所以你看到网上那些高精度雪花图其实是在用递归深度换细节代价是计算量指数级上涨。我第一次跑深度6的时候龟图turtle慢得像爬后来直接把t.speed(0)改成了tracer(0, 0)画完再一次性刷新速度立刻起飞。还有一个我踩过的坑科赫雪花最容易出现的问题是depth设为0以后会多画一笔。原因在于递归终止条件里forward(length)之后for循环里的0度角做了个无意义的转向。这种问题不致命但会影响到闭合图形首尾是否完全衔接。2.2 迭代函数系统IFS用随机做控制IFS的思路和递归完全相反。它不去显式描述“怎么不断细分”而是定义一组仿射变换拉伸、旋转、平移的组合然后从任意起点开始随机挑一个变换作用上去得到新点再挑变换再作用……迭代几万次后所有点布满屏幕形成分形图案。最经典的例子是“巴恩斯利蕨”Barnsley Fern四组系数对应叶柄、左侧小叶、右侧小叶和整体上长趋势。简单实现大概是import random import matplotlib.pyplot as plt def fern(n50000): x y 0 xs, ys [], [] for _ in range(n): r random.random() if r 0.01: x, y 0, 0.16 * y elif r 0.86: x, y 0.85 * x 0.04 * y, -0.04 * x 0.85 * y 1.6 elif r 0.93: x, y 0.2 * x - 0.26 * y, 0.23 * x 0.22 * y 1.6 else: x, y -0.15 * x 0.28 * y, 0.26 * x 0.24 * y 0.44 xs.append(x) ys.append(y) plt.figure(figsize(4, 8)) plt.scatter(xs, ys, s0.1, colorgreen) plt.axis(off) plt.show() fern()注意这里每组概率不是随便拍的它决定每个变换被选中的次数占比。如果把第一个0.01改成0.5画出来的东西会变成一坨乱点因为茎干部分的变换被过度使用。IFS的好处是代码量少、视觉冲击强而且天然适合做彩色渐变——你可以在循环里根据迭代次数改变点的颜色从深绿过渡到浅黄。2.3 逃逸时间算法用复数迭代画曼德勃罗集想画出你脑海里那个“黑桃形外围一圈圈彩色光晕”的图像基本都逃不开曼德勃罗集Mandelbrot Set。它其实是复平面上一个简单迭代的“收敛结果”可视化z z² c其中z从0开始c对应每个像素点在复平面上的坐标。如果迭代某一次数后z的模长超过2就判定为“逃逸”这个像素点根据逃逸速度上色如果一直没超过就涂成黑色。这里有个容易误解的点不是直接画迭代轨迹而是每个像素点遍历一遍公式把“逃逸的快慢”映射成颜色。我用numpy向量化写过一版核心代码import numpy as np import matplotlib.pyplot as plt width, height 800, 800 xmin, xmax, ymin, ymax -2.5, 1.0, -1.25, 1.25 max_iter 256 x np.linspace(xmin, xmax, width) y np.linspace(ymin, ymax, height) X, Y np.meshgrid(x, y) C X 1j * Y Z np.zeros_like(C) divtime np.full(C.shape, max_iter) for i in range(max_iter): Z Z**2 C diverge np.abs(Z) 2 now_div diverge (divtime max_iter) divtime[now_div] i Z[diverge] 2 # 防止后续溢出 plt.figure(figsize(8, 8)) plt.imshow(divtime.T, cmaptwilight_shifted, extent(xmin, xmax, ymin, ymax)) plt.axis(off) plt.show()这段代码最大的价值是“三行冲垮CPU”np.meshgrid直接把整个平面算成矩阵迭代全部在数组层面发生800x800像素做256次迭代在我笔记本上大约1秒出图。如果你用纯Python双重循环逐像素跑同样的规模得等半分钟。所以想画大幅分形务必优先考虑numpy向量化这是我从菜鸟到能玩复杂图案的分水岭。3. 从零搭建绘画环境Python、编辑器与绘图库的一次性配置标题里的热词里有一堆“安装教程”可见环境配置本身就是第一道坎。但其实不用想复杂我这套配置已经用了三年覆盖从画分形到日常脚本绘图所有场景一套走完基本不会再碰环境问题。3.1 Python安装选版本和解压即用如果你是Windows用户直接去官网下载Python 3.10的64位安装包。安装时有一个极其关键的勾选——“Add Python to PATH”一定要打勾。如果不勾后续在命令行敲python会提示“not found”这就是热搜里“python was not found; run without arguments to install from the Microsoft Store”这个报错的直接原因。我见过太多人卡在这一步要么没勾PATH要么运行了Microsoft Store的假Python。记住装官方Python时把安装方式选为“Customize installation”把“Add Python to environment variables”点亮这才是正路。macOS用户其实不用专门装系统Python直接用brew install python或者去官网下载安装包都行。装完在终端输入python3 --version验证。Linux用户则看发行版Ubuntu里可能自带Python 3.10但很多时候缺pip你得顺手补一下sudo apt install python3-pip。3.2 编辑器选择VSCode比IDE更适合绘图调试我推荐VSCode而不是PyCharm原因是PyCharm默认窗口太重而分形绘图经常要弹出图形窗口、反复改参数轻量编辑器配合快捷键更顺手。VSCode装好中文插件和Python插件后在项目目录里新建.py文件右下角选择解释器就能直接按F5运行。有一点容易忽略图形窗口阻塞会让界面看上去“卡死”比如turtle.done()或plt.show()执行后程序会一直等待窗口关闭。这时候VSCode的调试按钮会显示“运行中”不是真死机。如果嫌烦可以把plt.show()改成plt.show(blockFalse)或者画完图直接plt.savefig(output.png)不再调用show()脚本跑完自动退出特别适合批量出图。3.3 安装核心库matplotlib、numpy与Pillow绘图分形最常用的就是matplotlib和numpy。在命令行执行pip install matplotlib numpy pillow如果pip报“不是内部或外部命令”说明Python的Scripts目录没在PATH里重装Python并勾选PATH是最省事的解法。国内用户如果下载慢可以在pip命令后加-i https://pypi.tuna.tsinghua.edu.cn/simple换镜像源。Pillow主要用于把多张分形图拼成gif或批量处理图片初学可以暂时不装但后续做动画基本离不开。4. 实操一把用Python画一个彩色分形树从参数到成品前面说的都是原理和配置这一节我们完整走一遍“分形树”的实操流程。分值树是最适合新手的项目——代码量少、视觉反馈明显、参数调节空间大而且你能亲眼看到“递归展开”的过程。4.1 设计一颗会生长的树递归函数怎么写我们选择用turtle来实现因为它的画笔逻辑能直观展示分形的分叉过程。核心函数这样定义import turtle def draw_branch(t, length, depth, angle): if depth 0: return t.forward(length) pos t.position() heading t.heading() t.left(angle) draw_branch(t, length * 0.7, depth - 1, angle) t.penup() t.goto(pos) t.setheading(heading) t.pendown() t.right(angle) draw_branch(t, length * 0.7, depth - 1, angle)这里的关键是画完左分支后要“还原位置和朝向”否则右分支的方向会乱掉。pos记录当前位置heading记录当前朝向这就是递归回溯的标准操作。length * 0.7是树枝缩短因子改大一点0.8树会更密改小0.5树会显得更稀疏有点梅花鹿角的意思。主程序里再加点颜色渐变树就活起来了t turtle.Turtle() t.speed(0) t.tracer(0, 0) # 关闭逐帧刷新 t.left(90) t.penup(); t.goto(0, -300); t.pendown() t.pensize(10) def draw_colored_branch(t, length, depth, angle): if depth 0: return t.pensize(depth * 2) r int(255 * depth / 10) g int(160 - depth * 12) b 80 t.color((r % 255, g % 255, b)) t.forward(length) pos t.position() heading t.heading() t.left(angle) draw_colored_branch(t, length * 0.7, depth - 1, angle) t.penup(); t.goto(pos); t.setheading(heading); t.pendown() t.right(angle) draw_colored_branch(t, length * 0.7, depth - 1, angle) t.color(brown) draw_colored_branch(t, 200, 8, 25) turtle.done()我调参数时发现tracer(0, 0)配合done()有个小坑如果你先关掉刷新又直接调done()窗口会停在空白状态。解决办法是在调tracer(0, 0)之后、done()之前调用一次t.update()强制刷新或者在递归结束后补一句t.update() turtle.done()4.2 参数调优心得长度、角度、深度和颜色这些参数决定了树的“性格”我实际试过的几个组合直接给你参考参数数值范围效果观察我的建议深度 depth6~12深度小于5太光秃大于10会因线段过多显得杂乱新手从8开始跑通后再试10缩短因子 length × 0.5~0.80.7值越小树越瘦高越大越蓬松0.65~0.72比较自然分支角度 angle15~45度角度越大树冠越展开太小像电线杆25度是安全值画笔粗细随深度递减粗到细模拟真实植物的枝干感当前深度×2但别小于1颜色棕色树干暖色枝叶用RGB三元组注意turtle颜色需要0~255或0~1的值使用0~1的浮点更方便控制一个最常犯的错在turtle.color()里直接传(255, 0, 0)会报“bad color sequence”错误。因为turtle默认采用0~1浮点范围你需要用turtle.colormode(255)切换模式或者把255改成1.0。这个坑我得拿出来单独讲因为新手用RGB取值时十有八九踩到。4.3 让静态分形动起来渐变、风摆和贴图分形树画完后可以加点“动起来”的小效果。最常见的是用time.sleep控制重复绘制不同角度模拟风吹的效果——其实就是在不同帧里改变angle参数import time for frame in range(30): angle 20 frame * 0.5 # 从20度逐渐增长 turtle.tracer(0, 0) t.clear() t.penup(); t.goto(0, -300); t.pendown() t.setheading(90) draw_colored_branch(t, 200, 7, angle) t.update() time.sleep(0.05)这里帧率就是靠t.clear()逐帧清屏重画参数连续变化就形成了动画。做成gif的话把每帧t.getcanvas().postscript(filefframe_{frame}.eps)转成图片再用Pillow合并成品效果相当惊艳很多屏保动画就是这么出来的。5. 玩点高级的Julia集、三维分形与随机分形当你把曼德勃罗集画顺手后会发现分形领域其实还有一整个宇宙等着探索。这里列出三个我玩过且认为最有意思的进阶方向每个都附注意事项。5.1 Julia集在同一个画面里换常数看变化Julia集和曼德勃罗集的迭代公式一样都是z z² c但角色区别在于曼德勃罗集里c是每个像素自己的坐标而Julia集里c是一个固定复数每个像素从自己的坐标出发迭代。也就是说同一张图size下你只要改c的实部和虚部画出来的图案就千变万化。代码改动极小把曼德勃罗集里的C从X 1j*Y改成固定值例如C complex(-0.4, 0.6)即可。我试过几个好用的参数c -0.7 0.27j碎花纹理c -0.8 0.156j螺旋星系状c 0.285 0.01j九连环形Julia集给了我一个非常深的感受看似只有一个常数变了整个图案气质完全不同这就是非线性系统的魅力——对初始条件极端敏感也是“混沌”这个词最直观的视觉化。5.2 三维分形用Meshgrid做高度图严格来说分形可以扩展到三维常见做法是给二维分形图像加上“迭代次数”作为高度值然后用matplotlib的plot_surface或Axes3D展示。比如把曼德勃罗集的divtime矩阵当作Z轴坐标颜色映射用cmapterrain就能看到一座起伏的“分形山脉”。需要注意的点是三维渲染非常吃显存网格数量控制在400x400以内否则旋转视角时会卡成PPT。我一般先用plt.imshow确认二维效果满意了再上三维免得反复等渲染。5.3 随机分形与噪声艺术Perlin噪声的简单实现除了确定性分形还有一类基于随机过程的分形典型代表是Perlin噪声常用于生成云纹、地形、大理石纹理。原理是用多层不同频率和振幅的噪声叠加每层“细节”不同但整体保持自相似性。Python里没有内置Perlin噪声你可以用numpy徒手写一个简化版核心步骤生成一个大致的随机梯度网格对每个点按距离插值。然后叠加多个尺度的噪声通常4~5层即可得到逼真的分形噪声。如果你不想自己造轮子装一个noise库pip install noise然后from noise import pnoise2 import numpy as np import matplotlib.pyplot as plt shape (512, 512) scale 100.0 octaves 6 persistence 0.5 lacunarity 2.0 world np.zeros(shape) for i in range(shape[0]): for j in range(shape[1]): world[i][j] pnoise2(i / scale, j / scale, octavesoctaves, persistencepersistence, lacunaritylacunarity) plt.imshow(world, cmapbone) plt.axis(off) plt.show()这段代码虽然是双重循环512x512像素跑起来也要好几秒但效果非常值——那种山脉纹理的细腻度直接能当桌面背景。如果嫌慢用numpy的矢量化插值能提速十倍这里不展开有兴趣的搜“numpy Perlin noise”能找到成熟模板。6. 从报错到崩溃我替你们踩过的绘图大坑这部分是全篇最“干货”的地方。我最初画分形时几乎每一步都在跟报错搏斗把高频问题整理成一张速查表你照着对号入座就行。现象根本原因解决办法窗口一闪而过看不清图程序运行完自动退出图形窗口也被关闭在脚本末尾加plt.show()或turtle.done()python提示“not found”Python没加入PATH重装并勾选“Add Python to PATH”matplotlib 中文显示成方块系统缺中文字体用plt.rcParams[font.sans-serif] [SimHei]或改用英文标签turtle画的线非常慢龟图逐帧刷新turtle.tracer(0, 0)画完turtle.update()matplotlib图像卡死无响应计算量太大迭代次数爆表缩小max_iter用numpy向量化代替纯循环hue颜色偏色或意外cmap名称拼错或数据范围不对检查imshow的cmap参数数据用vmin/vmax限制范围plt.savefig一张空白图保存前窗口已被关闭画布不存在在savefig之前不要调用plt.show()分形图像出现白噪点或噪边数据溢出numpy返回inf或nan在迭代中把np.abs(Z)2的点强制设为2防止后续计算膨胀颜色报错bad color sequenceturtle默认颜色范围0~1你用了0~255在脚本开头加turtle.colormode(255)或把RGB各分量除以255除了表里的这些我想单独提一个大家容易忽略的心态问题当你把max_iter设到1000以上可能导致迭代时间翻倍还不明显。实际上画曼德勃罗集256次迭代已经能覆盖绝大多数细节再高只对极度放大的局部有意义。画面出现“毛刺”往往是分辨率不足而不是迭代不足这时候应该增加像素尺寸而不是迭代次数一定要分清楚。还有一个真实翻车案例我为了追求高分辨率把图幅设成2000x2000max_iter2000跑了一分多钟不说最终因为内存占用过大直接卡死。原因是Z矩阵里存的是complex128类型2000x2000的复数组有2000200016字节≈64MB加上中间产生的其他数组内存轻松突破几百MB。所以合理的策略是先640x640小尺寸试参数满意后再放大输出同时适当降低迭代数。7. 比画图更值钱的几个让分形真正“用起来”的思路分形如果只停留在“好看”玩几天你就腻了。我后来把分形项目延伸成了几个实际能用的小工具这里分享出来也许能给你灵感。7.1 生成无缝纹理素材把Perlin噪声或分形的局部裁剪成无缝纹理tileable texture可以用于游戏背景、网页设计或布料印花。无缝的核心是“保证边缘像素值相同”做法是在生成噪声时把输入坐标做周期化处理比如对坐标取模world[i][j] pnoise2(i / scale, j / scale, base(0, 0))但实际上要无缝必须让pnoise2的输入在边界上连续。开源的noise库提供了pnoise2的周期性参数在调用时加上tile_size即可。这个方法我亲测有效画出来的纹理四边能首尾相接铺开没有接缝做游戏地砖特别合适。7.2 把分形变成动态屏保和网页动画用Python生成的png序列再用PIL合成gif或者转成moviepy的mp4就能变成会动的分形屏保。进阶玩法是导出为SVG矢量图然后在网页里用JavaScript的requestAnimationFrame渲染出动画。不过Python初学者不用做到那一步先用matplotlib.animation.FuncAnimation就能本地生成视频代码很简短from matplotlib.animation import FuncAnimation fig, ax plt.subplots() data np.random.rand(100, 100) im ax.imshow(data, cmaphot) def update(frame): # 这里改成你的分形迭代结果 data np.random.rand(100, 100) im.set_array(data) return im ani FuncAnimation(fig, update, frames30, interval50) ani.save(fractal.gif, writerpillow)7.3 把分形图作为数据可视化的底图分形图像的视觉密度很适合作“装饰性底图”叠加折线、散点或热力信息可以让汇报图表更有层次感。我试过在曼德勃罗集的背景上用scatter叠加随机点效果类似“星空中的信号”信息层级鲜明。只要注意不要让背景的分形干扰前景数据通常把背景调暗并降低透明度即可。做法是在imshow里设置alpha0.25再把前景图层设置成亮色。8. 我的几点体会与延伸玩法建议画分形这件事越玩越觉得它不只是“写代码出图”更像是在数学和视觉之间搭桥。我在反复调参的过程里逐渐理解了迭代深度、逃逸时间、噪声叠加这些概念它们不仅在做图上有用在很多算法领域比如地形生成、纹理合成、信号处理也都扎着根。有一点我深有体会不要一开始就追求复刻网上的高分复杂图从最简单的曼德勃罗集、分形树开始把每一步参数改动的视觉效果记录下来建立自己的“参数直觉表”比如“缩短因子调大后树冠更茂盛”“Julia集常数虚部改变会让图案旋转”。这种积累比抄十个复杂代码都有用因为它帮你把随机试错变成了有方向的设计。再分享一个已经验证很实用的技巧当你发现图像中某个局部特别精彩想放大欣赏时不要直接在原图上裁剪放大而是用同样的公式重算对应坐标范围。比如曼德勃罗集左上角x∈[-1.5,-1.0], y∈[0,0.5]这个区域重新设置坐标范围渲染一次细节会丰富到一个新世界——这才是分形最迷人的地方任何局部都隐藏着无限细节。如果后续想继续深入建议依次尝试曼德勃罗集的缩放动画、基于IFS的彩色复叶图、用numpy向量化的Julia集批量生成、最后挑战一下三维分形山体。每跨一步你对分形生成和Python绘图的掌握都会上一个台阶。写代码累了就随机生成几张分形图做壁纸也算是最有成就感的放松方式了。
返回列表