029- 使用 go 绘制 Mandelbrot 分形图

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

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


029- 使用 go 绘制 Mandelbrot 分形图_第1张图片
图1 mandelbrot 分形图

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


029- 使用 go 绘制 Mandelbrot 分形图_第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 lim n → + ∞ z n 收敛,则 cM c ∈ M M M 表示 Mandelbrot 集合。

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

1.1 Mandelbrot 集合特点

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

  • 特性1:如果 cM c ∈ M ,则 |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 | z n | ≤ 2 成立。反过来说,如果某个复数 c c 使得 |zn|>2 | z n | > 2 ,则该数一定不属于 Mandelbrot 集合。

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

当然我们不可能在程序里判断每一个迭代中的 z z ,比如你只要计算 z0 z 0 z199 z 199 这 200 个 z z ,如果这 200 个数都小于等于 2,则认为它属于 Mandelbrot 集合(我们只能说它大概率属于 Mandelbrot 集合)。如果计算出来某个 z 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) ( p x , p y ) 是否属于 Mandelbrot 集合

1.2.2 颜色确定

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

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

假设我们在计算第 n n 个数 zn z n 的时候,发现 |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)
}

最后的结果如下:


029- 使用 go 绘制 Mandelbrot 分形图_第3张图片
图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]
}

你可能感兴趣的:(Go,语言学习笔记(更新中...),Go,语言修炼指南)