029- 使用 go 绘制 Mandelbrot 分形图

在这个世界,有非常多奇妙的事情。分形,就是其中之一。

分形艺术(fractal art)由IBM研究室的数学家曼德布洛特(Benoit.Mandelbrot,1924-2010)提出。其维度并非整数的几何图形,而是在越来越细微的尺度上不断自我重复,是一项研究不规则性的科学。


这里写图片描述
图1 mandelbrot 分形图

图1 是 mandelbrot 分形图中的一部分。一个完整的 mandelbort 图如图 2 所示。


这里写图片描述
图2 mandelbrot 分形图

本文的目的,是使用 go 语言来绘制一幅这样的图。想想是不是觉得很酷?其实绘制这样的图非常简单,你只需要知道一点点数学知识即可。下面一步一步介绍。

1. Mandelbrot 集合

有以下迭代公式:

zn=zn1+c z n = z n − 1 + c

其中 z0=0 z 0 = 0 c c 为任意复数(complex). 也有叫虚数,这个随你啦。

limn+zn 收敛,则 cM c ∈ M M M 表示 Mandelbrot 集合。

从上面的定义可得知,Mandelbrot 就是所有那些使得 limn+zn 收敛的 c c 的集合。

1.1 Mandelbrot 集合特点

这里我只介绍对我们有用的。

  • 特性1:如果 cM,则 |c|2 | c | ≤ 2 .

    这个特性能很快帮我们排除掉一大波非 Mandelbrot 集合里的数。对于 |c|>2 | c | > 2 的那些数来说,它一定不属于 Mandelbrot 集合。

    值得一提的是,上面的特性反过来是不一定成立的。也就是说如果 |c|2 | c | ≤ 2 ,则 c 不一定就属于 Mandelbrot 集合。

    • 特性2:如果 cM c ∈ M ,则 |zn|2 | z n | ≤ 2 .

    上面这句话的意思是说,对于 Mandelbrot 集合中的任意一个复数 c c ,都能使用 |zn|2 成立。反过来说,如果某个复数 c c 使得 |zn|>2,则该数一定不属于 Mandelbrot 集合。

    如果写程序的话,给定一个数 c c ,判断它是不是 mandelbrot 集合就非常简单了,我们只要判断 zn 和 2 之间的大小就行了。

    当然我们不可能在程序里判断每一个迭代中的 z z ,比如你只要计算 z0 z199 z 199 这 200 个 z z ,如果这 200 个数都小于等于 2,则认为它属于 Mandelbrot 集合(我们只能说它大概率属于 Mandelbrot 集合)。如果计算出来某个 z 大于 2 了,那它一定不属于 Mandelbrot 集合。

    当然你可以提高迭代精度,比如迭代 1000 次,10000 次甚至更多次。

    1.2 绘图方法

    1.2.1 坐标映射

    我们只需要计算 |c|2 | c | ≤ 2 的那些复数就行了。

    对于一个复数 c=x+yi c = x + y i ,我们只需要计算 2x2 − 2 ≤ x ≤ 2 2y2 − 2 ≤ y ≤ 2 这个范围内的复数就行了。

    假设我们有一幅大小为 1000×1000 1000 × 1000 的图,左上角第一个像素坐标为 (0,0) ( 0 , 0 ) ,右下角为 (999,999) ( 999 , 999 ) 。我们需要把每一个像素 (px,py) ( p x , p y ) 映射到一个复数 c=x+yi c = x + y i 上,其中 2x2 − 2 ≤ x ≤ 2 2y2 − 2 ≤ y ≤ 2 。映射公式如下:

    x=(px10000.5)×4y=(py10000.5)×4 x = ( p x 1000 − 0.5 ) × 4 y = ( p y 1000 − 0.5 ) × 4

    这样我们就能把坐标 (px,py) ( p x , p y ) 映射到一个复数 c c 上啦。

    最后的问题就可以转换为判断像素 (px,py) 是否属于 Mandelbrot 集合。

    1.2.2 颜色确定

    如果某个像素 pM p ∈ M ,则将其颜色设置为黑色。

    如果某个像素 pM p ∉ M ,则如果确定颜色呢?前面已经讲了确定一个复数 c c 的方法,主要是计算每一个 z 的大小。

    假设我们在计算第 n n 个数 zn 的时候,发现 |zn|>2 | z n | > 2 ,则我们把对应的像素点颜色设置为 f(n) f ( n ) . f(n) f ( n ) 是一种把迭代次数转换为特定颜色的方法。举个例子,假设我们生成的是灰度图,我们可以设置:

    f(n)=n f ( n ) = n

    即把迭代次数设置为像素值。如果 n>255 n > 255 怎么办?没关系,你可以对 255 取模,这样就可以把像素值控制到 255 以内了。

    一切理论都准备好了,就差实践了。但是好像还差点什么。没错,复数在 go 语言里难道还需要自己构造一个吗?不用了!go 语言已经对复数提供了支持。

    2. go 语言中的复数类型

    go 语言原生支持复数类型。

    在 go 里,有两种复数类型,一种是 complex64,另一种是 complex128。这两种复数类型精度分别对应 float32float64

    下面的例子简单演示了声明复数的方法。

    // demo01.go
    package main
    
    import "fmt"
    import "math/cmplx"
    
    func main() {
        var x complex64 = complex(3, 4) // 使用 complex 内建函数
        var y complex64 = complex(6, 8)
        var z complex128 = complex(1, 2)
        fmt.Println(x)
        fmt.Printf("%v\n", y)
        // 在 go 里,复数可以直接做四则运行
        fmt.Println(x + y)
        fmt.Println(x * y)
        fmt.Println(x / y)
    
        fmt.Println()
    
        a := 1 + 2i // 也可以使用字面量。如果不指定类型,则推导类型是 complex128
        b := 2 + 3i
        c := cmplx.Sqrt(-4.41) // 对负数开根号,也可以得到复数。
        fmt.Println(a * b)
        fmt.Println(c)
        fmt.Println(real(a)) // 计算实部
        fmt.Println(imag(a)) // 计算虚部
    }

    3. 使用 go 绘制 mandelbrot 分形图

    // demo02.go
    package main
    
    import (
        "image"
        "image/color"
        "image/png"
        "io"
        "math/cmplx"
        "net/http"
    )
    
    func handle(w http.ResponseWriter, r *http.Request) {
        draw(w)
    }
    
    func draw(w io.Writer) {
        const size = 1000
        rec := image.Rect(0, 0, size, size)
        img := image.NewRGBA(rec)
    
        for y := 0; y < size; y++ {
            yy := 4 * (float64(y)/size - 0.5) // [-2, 2]
            for x := 0; x < size; x++ {
                xx := 4 * (float64(x)/size - 0.5) // [-2, 2]
                c := complex(xx, yy)
    
                img.Set(x, y, mandelbrot(c))
            }
        }
    
        png.Encode(w, img)
    }
    
    // z := z^2 + c
    // 特点,如果 c in M,则 |c| <= 2; 反过来不一定成立
    // 如果  c in M,则 |z| <= 2. 这个特性可以用来发现 c 是否属于 M
    func mandelbrot(c complex128) color.Color {
        var z complex128
        const iterator = 254
    
        // 如果迭代 200 次发现 z 还是小于 2,则认为 c 属于 M
        for i := uint8(0); i < iterator; i++ {
            if cmplx.Abs(z) > 2 {
                return getColor(i)
            }
            z = z*z + c
        }
    
        return color.Black
    }
    
    // 根据迭代次数计算一个合适的像素值
    func getColor(n uint8) color.Color {
        // 这里乘以 15 是为了提高颜色的区分度,即对比度
        return color.Gray{n * 15}
    }
    
    func main() {
        http.HandleFunc("/", handle)
        http.ListenAndServe(":8080", nil)
    }

    最后的结果如下:


    这里写图片描述
    图3 mandelbrot 分形图

    4. 总结

    • 掌握 go 中的复数类型

    练习:

    1. 重写 getColor 函数,生成彩色图像。图 2 是我提供的一个样例,getColor 定义见文末。
    2. 提升图像质量(如提升迭代次数,图像大小)

    图 2 中的 getColor函数定义如下:

    func getColor(n uint8) color.Color {
        paletted := [16]color.Color{
            color.RGBA{66, 30, 15, 255},    // # brown 3
            color.RGBA{25, 7, 26, 255},     // # dark violett
            color.RGBA{9, 1, 47, 255},      //# darkest blue
            color.RGBA{4, 4, 73, 255},      //# blue 5
            color.RGBA{0, 7, 100, 255},     //# blue 4
            color.RGBA{12, 44, 138, 255},   //# blue 3
            color.RGBA{24, 82, 177, 255},   //# blue 2
            color.RGBA{57, 125, 209, 255},  //# blue 1
            color.RGBA{134, 181, 229, 255}, // # blue 0
            color.RGBA{211, 236, 248, 255}, // # lightest blue
            color.RGBA{241, 233, 191, 255}, // # lightest yellow
            color.RGBA{248, 201, 95, 255},  // # light yellow
            color.RGBA{255, 170, 0, 255},   // # dirty yellow
            color.RGBA{204, 128, 0, 255},   // # brown 0
            color.RGBA{153, 87, 0, 255},    // # brown 1
            color.RGBA{106, 52, 3, 255},    // # brown 2
        }
        return paletted[n%16]
    }
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值