icespike-x.github.io

View on GitHub
第一篇博客 | 使用Python绘制Mandelbrot集的图像

返回

第一篇博客 | 使用Python绘制Mandelbrot集的图像

2019/5/8 星期三 16:37

先放上结果图: 结果图

1. 什么是Mandelbrot集?

曼德博集合(Mandelbrot set,或译为曼德布洛特复数集合)是一种在复平面上组成分形的点的集合,以数学家本华·曼德博的名字命名。

定义

摘自维基百科:

曼德博集合可以用复二次多项式来定义:

$ {\displaystyle f_{c}(z)=z^{2}+c\,} $, 其中$ {\displaystyle c} $是一个复数参数。

从$ {\displaystyle z=0} $开始对$ {\displaystyle f_{c}(z)} $进行迭代:

$ {\displaystyle z_{n+1}=z_{n}^{2}+c,n=0,1,2,…} $

$ {\displaystyle z_{0}=0\,} $

$ {\displaystyle z_{1}=z_{0}^{2}+c=c\,} $

$ {\displaystyle z_{2}=z_{1}^{2}+c=c^{2}+c\,} $

每次迭代的值依序如以下序列所示:

$ {\displaystyle (0,f_{c}(0),f_{c}(f_{c}(0)),f_{c}(f_{c}(f_{c}(0))),\ldots )} $

不同的参数$ {\displaystyle c} $可能使序列的绝对值逐渐发散到无限大,也可能收敛在有限的区域内。

曼德博集合$ {\displaystyle M} $就是使序列不延伸至无限大的所有复数$ {\displaystyle c} $的集合。

2.通过Python伪代码来实现Mandelbrot集的范例

def In_Mandelbrot(c: complex) -> bool:
    z = complex(0)
    
    for i in range(ITER_DEPTH):
        z = z*z + c
        if z.real ** 2 + z.imag ** 2 > 4:
            return False
    return True

在这段代码中,ITER_DEPTH表示迭代深度。

3. 实战

首先,加载数学库和用于输出图像的Pillow库:

import math
from PIL import Image, ImageDraw

然后,定义之后要设置的常量:

# 清晰度,坐标轴转换成像素的比例值。
SCALE = 256

# 迭代深度
ITER_DEPTH = 64

# 图像显示用,控制着色的参数,建议设置成 ITER_DEPTH / 16。
ITER_PER_COLOR = 4

加上创建和保存图像的代码:

# 图像背景为黑色。
im = Image.new('RGB', (4 * SCALE,) * 2, 0)
g = ImageDraw.Draw(im)

im.save("mandelbrot.jpg", format='JPEG')

这里4 * SCALE中4 = 2(正负坐标轴)*2(坐标范围为2, -2)

然后在创建图像和保存图像的代码中加上绘制M集的代码:

for i in range(SCALE * 4):
    for j in range(SCALE * 4):
        # 将像素位置转换成坐标轴上的位置。
        x = i / SCALE - 2.0
        y = j / SCALE - 2.0

        # 初始化迭代的参数

        c = complex(x, y)

        z = complex(0, 0)

        # 迭代,迭代终止时使用迭代的次数作色。

        for k in range(ITER_DEPTH):
            z = z ** 2
            z += c
            if z.real ** 2 + z.imag ** 2 > 4:
                # 着色,使用的公式可以让输出的图片变得更清晰
                g.point((i, j), (0, int((k // ITER_PER_COLOR) * ((256 - 1) / (ITER_DEPTH // ITER_PER_COLOR2))), 0))
                break

首先开头的循环

for i in range(SCALE * 4):
    for j in range(SCALE * 4):

遍历每一个像素,然后在初始化坐标的公式中

        x = i / SCALE - 2.0
        y = j / SCALE - 2.0

像素值在除与SCALE之后在[0, 4]的范围内,再减去二就是对应的坐标值。

        c = complex(x, y)

        z = complex(0, 0)

这段已经在之前有解释,不再赘述。

        for k in range(ITER_DEPTH):
            z = z ** 2
            z += c
            if z.real ** 2 + z.imag ** 2 > 4:
                # 着色,使用的公式可以让输出的图片变得更清晰
                g.point((i, j), (0, int((k // ITER_PER_COLOR) * ((256 - 1) / (ITER_DEPTH // ITER_PER_COLOR))), 0))
                break

迭代公式中其他已经在之前有解释,使用ImageDraw.ImageDraw.point函数来绘制对应的像素,将输出颜色的公式分离出来:

(
    0,  # Red
    int((k // 2) * ((256 - 1) / (ITER_DEPTH // ITER_PER_COLOR))),  # Green
    0  # Blue
)

使用绿色主要是为了显眼。

int((k // ITER_PER_COLOR) * ((256 - 1) / (ITER_DEPTH // ITER_PER_COLOR)))

在for循环中 k是迭代次数,在迭代的过程中发散时迭代次数越多颜色越亮,因此就有了开头那张图的效果。

最后附上完整代码(开始写博客才发现我这是有多水):

from PIL import Image, ImageDraw
from math import sqrt

# 清晰度,坐标轴转换成像素的比例值。
SCALE = 256

# 迭代深度
ITER_DEPTH = 64

# 控制着色的参数,建议设置成 ITER_DEPTH / 16
ITER_PER_COLOR = 4

im = Image.new('RGB', (4 * SCALE,) * 2, 0)

g = ImageDraw.Draw(im)

for i in range(SCALE * 4):
    for j in range(SCALE * 4):
        # 将像素位置转换成坐标轴上的位置。
        x = i / SCALE - 2.0
        y = j / SCALE - 2.0

        # 初始化迭代的参数

        c = complex(x, y)

        z = complex(0, 0)

        # 迭代,迭代终止时使用迭代的次数作色。

        for k in range(ITER_DEPTH):
            z = z ** 2
            z += c
            if z.real ** 2 + z.imag ** 2 > 4:
                # 着色,使用的公式可以让输出的图片变得更清晰
                g.point((i, j), (0, int((k // ITER_PER_COLOR) * ((256 - 1) / (ITER_DEPTH // ITER_PER_COLOR))), 0))
                break

im.save("mandelbrot.jpg", format='JPEG')

(完)