1. 先搞懂分形到底在画什么两条最值得上手的主线第一次用Python渲染出Mandelbrot集合那张图的时候我盯着屏幕上墨蓝与暗金交错的海岸线看了很久——代码生成的东西竟然能长出这么复杂的纹理。很多人以为算法绘图是专业图形学软件或生成艺术平台的专利但只要你掌握了分形的核心逻辑用NumPy加PIL就能画出同样惊艳甚至更自由的作品而且每一步都看得见、摸得着改一个参数就是一幅新画。分形有一个特别迷人的特性它的局部放大之后和整体保持着某种相似性。这句话听起来很玄落到代码层面其实非常具体——你只需要反复套用同一个数学规则让程序自己生长出细节。我用Python做分形艺术这两年绝大多数作品都从两条主线出发迭代复数方程逃逸时间算法和递归/L系统重写规则。前者适合渲染那种深邃的、像宇宙尘埃一样的数字风景后者适合画树、植物、雪花这类结构感很强的东西。先解释一下什么叫自相似。想象你正在看一段不规则的锯齿形海岸线把其中一公里放大到面前你会看到更细碎的凹凸再放大其中一米凹凸还在。分形艺术就是把这个无限放大仍有细节的性质用算法逼近出来。Python里常用两种手段逼近一种是反复迭代一个复数函数看像素点在迭代中是逃向无穷远还是被困在有限区域内这就是Mandelbrot集合和Julia集合的基本思路另一种是递归调用让一根树枝不断分裂出更小的树枝或者让一组字符规则不断生长成复杂的植物形态。这两种路线对新手都很友好因为它们不需要复杂的图形引擎核心不过是一个循环加一个判断。而且它们的视觉效果上限极高——迭代一次的规则极其简单但叠加几十万次之后画面复杂度远超手绘。1.1 自相似性大自然藏在数学里的套娃你如果观察过蕨类植物的叶片会发现每一片小叶子都像整片大叶子的缩小版观察西兰花一个花球切开之后每一朵小花蕾又长得像完整的花球。这种部分与整体结构相似的性质就是自相似。在Python里复现这件事最直观的方式是递归。拿最简单的分形树举例树干是一根线段顶端分出两根倾斜的分支每根分支再当作新的树干继续分……如果你只把分支长度乘以0.7、旋转角度固定这个规则重复执行就能生成一棵有模有样的树。我通常用turtle库来做这种项目因为turtle的笔尖方向和位置控制非常直观缩到最短代码也只有十几行import turtle t turtle.Turtle() t.speed(0) t.left(90) t.penup() t.goto(0, -300) t.pendown() def tree(length, depth): if depth 0: return t.forward(length) t.left(30) tree(length * 0.7, depth - 1) t.right(60) tree(length * 0.7, depth - 1) t.left(30) t.backward(length) tree(150, 8) turtle.done()跑一下你就明白了递归深度8层的时候已经能看出树的轮廓深度加到12层枝杈会细密许多。这里的核心思想是将同一操作施用于自身的结果而不是每次重新设计规则。分形艺术中几乎所有的复杂图案都从这个套娃原则派生出来区别只是套娃的规则长什么样、套多少层、颜色怎么分配。1.2 迭代公式与逃逸时间从一个复数方程开始的宇宙如果说递归分形像拓扑生长那Mandelbrot集合就完全是另一套路子它不画树枝而是画复平面上所有满足特定迭代条件点的集合。常用的公式只有一个z z² c其中c是复平面上某个像素点对应的复数z从0开始不断迭代。如果迭代很多次之后z的模长始终没有超过某个阈值通常取2我们就认为这个点属于Mandelbrot集合把它涂成黑色如果中途跑出去了就根据它跑了多少次才逃逸来分配颜色。后者就是我们常说的逃逸时间算法。你可能想问这么简单的式子凭什么能画出那么复杂的图形恰恰因为它简单却对初值极度敏感。c的微小变化会让迭代轨迹完全不同于是复平面上每个点都像一个小小的命运分支大量点汇聚之后就形成了那种布满螺旋、细丝和大陆块边缘的图案。复平面的坐标范围可以任选但经典视图一般取实数轴-2.5到1.5、虚数轴-1.5到1.5。这个范围能完整呈现Mandelbrot集合的主心形区域和两侧的芽胞结构。2. 第一幅作品用Python渲染Mandelbrot集合的完整流程看完了原理接着就是动手。我觉得最合理的节奏是先写一个最简版本让图像能出来然后在此基础上加配色、加平滑、提高清晰度。很多初学者一上来就想做4K高清大图结果被性能问题劝退其实完全没必要。第一版分辨率控制在800×600、迭代次数128就够观察了。2.1 从公式到像素坐标映射与迭代判断图像是一个二维网格每一格对应复平面上的一个坐标。要做的就是把像素坐标映射到复平面横向像素列号j映射到复平面实部x纵向像素行号i映射到复平面虚部y我习惯用NumPy的向量化写法避免在Python层面套无数个for循环。原因是NumPy底层把循环推给了C数组运算速度能快上百倍。下面是最简版本import numpy as np from PIL import Image width, height 800, 600 max_iter 128 xmin, xmax -2.5, 1.5 ymin, ymax -1.5, 1.5 xs np.linspace(xmin, xmax, width).reshape(1, -1) ys np.linspace(ymin, ymax, height).reshape(-1, 1) c xs 1j * ys z np.zeros_like(c) escaped_count np.zeros(c.shape, dtypenp.int32) for n in range(max_iter): mask escaped_count 0 z[mask] z[mask] ** 2 c[mask] escaped mask (np.abs(z) 2.0) escaped_count[escaped] n 1 image_array (255 - escaped_count * (255 // max_iter)).astype(np.uint8) img Image.fromarray(image_array, modeL) img.save(mandelbrot_basic.png)这段代码里escaped_count记录的是每个像素在第几次迭代时逃逸0表示一直没逃逸集合内部。黑白模式下逃逸早的点偏暗逃逸晚的点偏亮于是集合外部会形成一圈一圈的同心纹理。第一次跑通的时候你可能觉得黑白也很耐看但彩色会让作品跃升一个台阶。2.2 平滑着色与调色板把迭代次数变成色彩层次单纯用整数迭代次数上色色带会出现明显的断层因为相邻像素的迭代次数跳变很大。专业的分形渲染通常会用平滑迭代值来消除断层。常用公式是mu n 1 - log2(log(|z|))这个公式的核心思想是不仅看第几次逃逸还要看逃逸瞬间z的模长大到什么程度用对数把连续的模长差异折算成小数部分。这样一来原本相邻像素即使迭代次数相差1颜色的过渡也会柔和许多。拿到0到1之间连续变化的mu之后要做的就是把它映射成RGB。我个人的偏好是用HSV颜色空间再做转换因为Hue色相可以从0到360平滑变化做出来的渐变特别自然。举个简单的例子def color_map(mu, max_iter): hue (mu / max_iter * 240 200) % 360 sat 0.8 val 0.9 if mu max_iter else 0.0 # 用colorsys转RGB from colorsys import hsv_to_rgb r, g, b hsv_to_rgb(hue / 360, sat, val) return int(r * 255), int(g * 255), int(b * 255)我试过很多配色发现深蓝到青、紫红到金这两种组合在Mandelbrot上效果最稳定。前者很像深空星云后者很像熔岩海岸。配色方案其实没有标准答案建议把调色函数独立出来方便随时换我后面专门讲这个。2.3 当你放大某个细节局部坐标范围与高迭代次数Mandelbrot集合最迷人的地方在于放大之后依然复杂。你只需要把坐标范围缩小到集合边界附近同时提高迭代次数就能看到完全不同的结构。比如我把视图拉近到实轴-0.748、虚轴0.1附近那里的海马状螺旋和细丝纹理特别丰富。放大时有一个关键经验迭代次数必须跟着分辨率一起提高。如果坐标范围缩得很小但迭代上限还是128那么大量像素会糊成一片很多细节被误判为集合内部变成黑色。我一般根据观察尺度调整max_iter——整体视图用200左右放大到0.01尺度用800到1200再往里就可能需要2000以上了。放大坐标时要特别小心浮点精度复平面很小的偏移都会让整幅图构图全变。3. 换一种节奏递归树、L-system与植物生长模拟Mandelbrot满足那种想反复放大探索的欲求但没有一棵树的结构感。做植物形态的分形我用得最多的工具是turtle加L-system。L-systemLindenmayer系统核心是用字符串重写规则模拟生长过程先定义初始字符串公理再定义替换规则反复迭代后把最终字符串解释成绘图指令。3.1 递归分形树与海龟绘图最简单也最能练手感turtle画树的优点是能实时看到笔尖行动递归树的分支角度、长度衰减率、深度全部可视化调试起来非常顺手。前面代码里的递归函数tree本质就是画一段然后左右分支递归。三个核心参数的意义你要弄清楚分支长度衰减率0.6以下树形偏稀疏0.75以上树冠密实分支角度20到45度比较像自然树冠角度太大会显得机械递归深度8到12层适合演示超过14层turtle会非常慢我在初始代码里用了固定的30度和0.7衰减率。这里有个小技巧给每层增加随机扰动树会从几何树变成有生命感的树。比如把角度写成30 random.uniform(-8, 8)长度乘0.7改成length * random.uniform(0.65, 0.78)跑几次你会发现每一棵都长得不一样。随机扰动是分形艺术从数学标本走向艺术作品的重要一步。3.2 L-system的字符串重写逻辑与绘图解释L-system比硬编码递归更灵活因为规则本身是可定义的数据。一个经典的植物生长规则例子公理F规则F - F[F]F[-F]F这里F代表向前画一条线段代表左转-代表右转[和]分别代表压栈和出栈即保存当前状态再恢复。反复应用规则之后字符串会指数级增长。比如迭代6次之后字符串长度已经非常可观画出来就是浓密的灌木丛。代码大致是这样import turtle axiom F rules {F: F[F]F[-F]F} angle 25 stack [] def expand(axiom, rules, iterations): result axiom for _ in range(iterations): result .join(rules.get(ch, ch) for ch in result) return result def draw(commands): for ch in commands: if ch F: t.forward(10) elif ch : t.left(angle) elif ch -: t.right(angle) elif ch [: stack.append((t.pos(), t.heading())) elif ch ]: pos, heading stack.pop() t.penup() t.goto(pos) t.setheading(heading) t.pendown()用L-system的好处是你不需要改代码逻辑只要改规则字符串就能画出不同风格的植物、雪花、迷宫甚至类龙形曲线。典型的Koch雪花规则是F - FF--FF配上60度转角迭代几次后就是完整的雪花轮廓。建议把规则和角度做成配置参数把L-system当绘画编译器你会发现它能覆盖的分形类型远超预期。4. 真正拉开差距的细节颜色映射、分辨率与迭代策略实话说分形图案的结构由数学规则决定只要写对公式任何人都能画出对的图。但想画出好看的图拼的是颜色映射和参数搭配。这一节我把我常用的参数组合和配色策略完整列出来。4.1 给逃逸次数设计一套顺眼的配色方案颜色映射的本质是平滑迭代值mu归一化到0到1之间然后查颜色表。三种常用方案线性渐变从color1到color2直接插值适合极简风格三角渐变双色交替营造星环或波纹的效果多段渐变按mu值分段映射到2到4个颜色锚点层次最丰富我最常用多段渐变。锚点列表大概是[(0, 0.1, 0.3), (0.5, 0.7, 1.0), (1.0, 0.8, 0.2)]也就是深蓝到亮蓝再跳到金色。用NumPy的np.interp可以方便做分段插值。还有一个重要技巧颜色循环周期。mu值超过1之后取小数部分循环这样同样的渐变色就能反复出现形成一圈一圈的环状色带。代码示意def palette(t): # t: 0~1 的色位 if t 0.5: r 0 g int(0.4 t * 2 * 0.4) b 100 t * 2 * 120 else: r int((t - 0.5) * 2 * 255) g int(0.3 * 255) b 255 - r return (r, g, b)真正渲染大幅图像时逐像素调用Python函数还是有点慢。我一般预计算一个256长度的查询表LUT每个mu值直接查表映射渲染8K图的提速非常明显。4.2 性能瓶颈与分辨率策略多人画不出高清图的关键原因分形渲染的复杂度大头不在图像尺寸而在迭代计算。800×600×200次迭代就是9600万次复数运算听起来很多但NumPy向量化后其实只要几秒。真正的坑是大量像素落在集合内部——这些像素每轮迭代都要继续算直到达到max_iter才停。优化思路有三个层次先用低分辨率粗跑确定构图和配色满意后再渲染大图避免反复试错消耗时间用NumPy的数组掩码跳过已逃逸像素就像我在最简版代码里写的mask escaped_count 0。这样每轮参与计算的像素越来越少后段迭代速度显著提升把核心迭代函数用Numba或Cython加速这个优化对中等尺寸图收益不太明显但高清图很吃这套如果你做动画或批量生成系列图还有一个容易被忽略的技巧分辨率缩小4倍颜色参考大图映射。也就是先渲染一张低清图做色彩参考再正式渲染高清图能省掉很多调参时间。4.3 让作品有呼吸感的参数组合表我把经常用的参数组合整理成了一张表方便你直接抄作业。每套方案都是我在实际项目里验证过出图效果的方案名称公式/规则坐标范围迭代次数配色倾向备注海马港湾z²c实部-0.748到-0.743虚部0.095到0.11200蓝紫渐变放大细节极多熔岩之心z³c整体视图附近300金红渐变幂次提高结构更碎星云尘带z⁴c实部-1.35到-0.6虚部-0.4到0.2400紫青多段适合做桌面壁纸蕨叶生长L-system规则F[F]F[-F]F画布区域深度8绿色系适度随机扰动效果最好雪原晶体Koch规则FF--FF画布区域深度5白蓝渐变角度60度是核心这张表的重点是换参数就换风格。实际调参过程中我习惯每次只改一个变量比如只改幂次从2到3你会发现画面从经典款变成锯齿状礁石视觉差异巨大。这种微调是算法绘图最有意思的部分。5. 从能出图到出好图踩坑记录与参数探索路径最后写写实际操作中容易栽跟头的地方。很多问题看起来是代码错误本质上是对分形原理理解不够深。5.1 边缘锯齿、色带断层与半途而废的迭代第一个常见问题是集合边界出现明显的锯齿状噪声尤其当max_iter偏低时。解决方法和前面说的一致提高迭代上限同时用平滑迭代值替代整数迭代次数。如果用了平滑公式还有隐约条纹检查你在计算mu时用的对数是log2还是自然对数公式必须对应。第二个问题是图像整体偏黑仿佛什么都看不见。这通常发生在坐标范围选到了集合内部大片区域比如实轴-0.1到0.1、虚轴-0.1到0.1这一带大量的点属于集合内部逃逸像素太少自然没有层次。解决办法是回到经典视图重新选区域或者把最大迭代次数降低让更多点显示为外部。第三个问题很有代表性迭代过程中大量像素始终没有逃逸导致程序跑得极慢。我遇到过一次坐标缩放太深直接掉进了Mandelbrot集合内部深处几乎一半像素都是内部点每轮迭代都在空转。这时候不是优化代码能解决的换一块离集合边界更近的区域性能问题会立刻缓解。5.2 用参数脚本管理系列作品从一个画到一组画接触分形艺术三个月后我发现单张图再好看也有审美疲劳的临界点真正有玩味的是系列作品——同一个公式坐标范围微微偏移或者旋转角度逐张递进拼在一起就像在讲述空间探索的过程。我自己养成了一个习惯所有作品都有一个独立的绘制参数文件记录坐标范围、迭代次数、调色板锚点、随机种子。想创作系列时就把这些参数批量修改写一个循环逐帧渲染。这种方式特别适合做分形动画虽然需要不少渲染时间但效果是YouTube上那种无限缩放进分形视频的感觉。顺带分享一个能立刻提升观感的细节保存成PNG而不是JPG。分形图边缘极度锐利JPG的压缩伪影会在细丝部分产生明显的色斑。我早期很多作品发到社交平台上总觉得有点糊后来全部改存PNG边界清晰度立刻不一样。如果你想要更进一步可以尝试把多个公式混合——比如在Mandelbrot迭代中加入三角函数项z z**2 c 0.1 * np.sin(z)图案会变得非常戏剧性。但这是进阶话题了不建议新手一上来就玩混合公式等基础的那几条路线都画出过满意的作品再尝试会更好。
阅读完成 · 觉得有帮助?