在这个世界,有非常多奇妙的事情。分形,就是其中之一。
分形艺术(fractal art)由IBM研究室的数学家曼德布洛特(Benoit.Mandelbrot,1924-2010)提出。其维度并非整数的几何图形,而是在越来越细微的尺度上不断自我重复,是一项研究不规则性的科学。
图1 mandelbrot 分形图
图1 是 mandelbrot 分形图中的一部分。一个完整的 mandelbort 图如图 2 所示。
图2 mandelbrot 分形图
本文的目的,是使用 go 语言来绘制一幅这样的图。想想是不是觉得很酷?其实绘制这样的图非常简单,你只需要知道一点点数学知识即可。下面一步一步介绍。
1. Mandelbrot 集合
有以下迭代公式:
其中 z0=0 z 0 = 0 , c c 为任意复数(complex). 也有叫虚数,这个随你啦。
若 收敛,则 c∈M c ∈ M 。 M M 表示 Mandelbrot 集合。
从上面的定义可得知,Mandelbrot 就是所有那些使得 收敛的 c c 的集合。
1.1 Mandelbrot 集合特点
这里我只介绍对我们有用的。
- 特性1:如果 ,则
|c|≤2
|
c
|
≤
2
.
这个特性能很快帮我们排除掉一大波非 Mandelbrot 集合里的数。对于 |c|>2 | c | > 2 的那些数来说,它一定不属于 Mandelbrot 集合。
值得一提的是,上面的特性反过来是不一定成立的。也就是说如果 |c|≤2 | c | ≤ 2 ,则 c 不一定就属于 Mandelbrot 集合。
- 特性2:如果 c∈M c ∈ M ,则 |zn|≤2 | z n | ≤ 2 .
上面这句话的意思是说,对于 Mandelbrot 集合中的任意一个复数 c c ,都能使用 成立。反过来说,如果某个复数 c c 使得 ,则该数一定不属于 Mandelbrot 集合。
如果写程序的话,给定一个数 c c ,判断它是不是 mandelbrot 集合就非常简单了,我们只要判断 和 2 之间的大小就行了。
当然我们不可能在程序里判断每一个迭代中的 z z ,比如你只要计算 到 z199 z 199 这 200 个 z z ,如果这 200 个数都小于等于 2,则认为它属于 Mandelbrot 集合(我们只能说它大概率属于 Mandelbrot 集合)。如果计算出来某个 大于 2 了,那它一定不属于 Mandelbrot 集合。
当然你可以提高迭代精度,比如迭代 1000 次,10000 次甚至更多次。
1.2 绘图方法
1.2.1 坐标映射
我们只需要计算 |c|≤2 | c | ≤ 2 的那些复数就行了。
对于一个复数 c=x+yi c = x + y i ,我们只需要计算 −2≤x≤2 − 2 ≤ x ≤ 2 , −2≤y≤2 − 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 上,其中 −2≤x≤2 − 2 ≤ x ≤ 2 , −2≤y≤2 − 2 ≤ y ≤ 2 。映射公式如下:
x=(px1000−0.5)×4y=(py1000−0.5)×4 x = ( p x 1000 − 0.5 ) × 4 y = ( p y 1000 − 0.5 ) × 4这样我们就能把坐标 (px,py) ( p x , p y ) 映射到一个复数 c c 上啦。
最后的问题就可以转换为判断像素 是否属于 Mandelbrot 集合。
1.2.2 颜色确定
如果某个像素 p∈M p ∈ M ,则将其颜色设置为黑色。
如果某个像素 p∉M p ∉ M ,则如果确定颜色呢?前面已经讲了确定一个复数 c c 的方法,主要是计算每一个 的大小。
假设我们在计算第 n 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
。这两种复数类型精度分别对应float32
和float64
。下面的例子简单演示了声明复数的方法。
// 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 中的复数类型
练习:
- 重写 getColor 函数,生成彩色图像。图 2 是我提供的一个样例,getColor 定义见文末。
- 提升图像质量(如提升迭代次数,图像大小)
图 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] }