探索分形:Mandelbrot集的生成与优化

在本文中,将展示大约两个月的工作成果。最初对分形感兴趣,最终完成了一个能够在大约2秒内生成Mandelbrot集的程序。这个项目极大地帮助了学习编程,尤其是在优化和效率方面。

首先,需要使用复数,每个复数都有一个“实部”和“虚部”。这可以写成:

Z = x + i * y

其中x是实部,y是虚部。i对应于虚数中的“i”。由于复数有两个部分(x,y),可以在x-y图上绘制它们。在案例中,将它们绘制在图像上。每一行像素对应一组复数Z,它们都有相同的y值,每个x像素的增量代表x值的一步。

现在,将计算某个x,y值范围内的集合。然后为每个x,y坐标分配一个像素,例如x = 0.3,y = 0.2。这个像素的颜色将使用Mandelbrot方程计算。

Mandelbrot方程

方程是:

Z_(n+1) = Z_n^2 + c

其中c是绘制给定像素的坐标,坐标c = x + i * y。n是迭代次数,从n = 0开始,也从Z_0 = 0 + i * 0开始。所以,在n = 0时,Z_(n+1) = Z_1 = Z_0^2 + c = c。所以Z_1 = c。

现在可以将这个Z值放入方程中得到Z_2。在n = 1时,Z_(n+1) = Z_2 = Z_1^2 + c。继续这样迭代,每次迭代生成Z_(n+1)。

但是应该在什么时候停止迭代呢?每次得到一个新的Z_(n+1)值,也会计算一个叫做复模的东西,写成:

| Z_(n+1) |

它是使用勾股定理计算三角形斜边的长度:

| Z_(n+1) | = √(x^2 + y^2)

Mandelbrot告诉,如果| Z_(n+1) | ≤ 2,就停止迭代,否则继续迭代。计算在方程收敛(停止迭代)之前完成的迭代次数n。n值用于表示c = x + i * y处像素的颜色。

集合中有一些部分的n看起来是无限的,或者至少变得如此之大,以至于不可能继续迭代。设置一个最大迭代次数nMax,并总是停止迭代如果n达到nMax。

事实证明nMax相当重要!一个较大的nMax需要更长的时间来计算,但可以显示非常精细的细节,接近Mandelbrot方程收敛(彩色部分)和发散(白色)的地方之间的边界。然而,一个低nMax也可以产生很好的图像,因为颜色尺度可以很好地工作。

请注意:在程序中,k用作迭代计数,kMax用作最大迭代次数。

特性

决定让程序不仅仅是生成主Mandelbrot集,所以它有一些其他特性。添加了一个缩放功能,可以通过点击鼠标来设置想要缩放的量,并渲染新图像。当然,放大得越多,就需要更多的迭代来支持高细节。

能够在“收藏夹”功能中保存Mandelbrot集的一个区域。这会将图像的所有参数保存到一个文本文件中,可以从中读取以加载收藏夹。

每次渲染新图像时,它都会将用于创建图像的参数保存到一个文本文件中。这样,可以使用“撤销”功能返回到图像。

除了显示上次渲染所花费的时间的计时器外,认为保存图像的功能也很不错。图像以PNG格式保存,可以选择一个文件名。

所有保存的文件都存储在C:\Users\%username%\mandelbrot_config中。

使用代码

该程序是一个Windows表单应用程序。它可以在Microsoft Visual Studio环境中启动。或者,可执行文件可以作为普通的Windows应用程序运行。

用户界面是自解释的,控制允许绘制x,y坐标的范围,允许的最大迭代次数,分辨率(像素步长)和缩放控制。此外,还有其他在本文的“特性”部分提到的控制。

有四个C#类,用途如下:

  • ComplexPoint.cs:用于封装一个复数点(Z = x + i * y),其中x和y分别是实部和虚部。类中包含了一些复数算术函数,用于执行Mandelbrot方程背后的数学。
  • ScreenPixelManage.cs:处理数学和物理屏幕坐标(像素坐标)之间的转换。底层的数学坐标与屏幕分辨率和大小无关,而像素坐标适用于运行时屏幕尺寸。
  • Prompt.cs:自定义提示,这种情况下,用于用户输入他们新收藏夹的名称(见“特性”)。
  • Mandelbrot.cs:这是项目中的主要类,扩展了.NET表单类。它用于渲染Mandelbrot集,控制允许用户修改要绘制的Mandelbrot集的部分,像素步长(分辨率)以及其他在“特性”部分提到的内容。

用于绘制Mandelbrot集的主要代码块如下:

for (double y = yMin; y < yMax; y += xyStep.y) { int xPix = 0; for (double x = xMin; x < xMax; x += xyStep.x) { ComplexPoint c = new ComplexPoint(x, y); ComplexPoint zk = new ComplexPoint(0, 0); int k = 0; do { zk = zk.doCmplxSqPlusConst(c); modulusSquared = zk.doMoulusSq(); k++; } while ((modulusSquared <= 4.0) && (k < kMax)); if (k < kMax) { if (k == kLast) { color = colorLast; } else { color = colourTable.GetColour(k); colorLast = color; } if (xyPixelStep == 1) { if ((xPix < myBitmap.Width) && (yPix >= 0)) { myBitmap.SetPixel(xPix, yPix, color); } } else { for (int pX = 0; pX < xyPixelStep; pX++) { for (int pY = 0; pY < xyPixelStep; pY++) { if (((xPix + pX) < myBitmap.Width) && ((yPix - pY) >= 0)) { myBitmap.SetPixel(xPix + pX, yPix - pY, color); } } } } } xPix += xyPixelStep; } yPix -= xyPixelStep; }

上面的if...else...语句用于处理分辨率。如果像素步长大于1,分辨率将降低,以便在绘制最终图像时没有空白。如果像素步长为1,则图像将正常绘制,即以尽可能高的最高分辨率绘制。

请注意,上面的循环中y像素计数是递减的,而x是递增的。这是因为绘图区域的原点(x,y = 0, 0)在屏幕的左上角。希望从左下角开始绘制图像,然后向右上角工作。这意味着y从其最大值开始。

优化性能

正如在引言中提到的,学会了如何优化代码。在写这篇文章之前,用程序渲染Mandelbrot集需要相当长的时间:10分钟。上传的这个版本大约需要2秒(AMD A8),而一台快速的PC可以在不到1秒的时间内完成(AMD FX-8350)。

一个关键的性能改进来自于图像的渲染方式。在这个程序的第一个版本中,使用了System.Drawing,每个Mandelbrot集中的像素都被绘制成一个椭圆。这非常消耗CPU。此外,在高分辨率(小像素步长)下绘制时,每个使用System.Drawing绘制的椭圆都与相邻像素重叠,导致图像略微模糊。上传的这个程序版本使用了bitmap,它克服了这些问题,并且还允许在表单隐藏或最小化时保留图像。

颜色映射

颜色映射用于将迭代值n转换为像素颜色。许多人投入了时间研究不同的颜色映射,大多数使用某种查找表,通常还使用某种插值算法。

解决方案非常简单但也非常有效 - 认为生成的图像和任何其他图像一样好。它的工作原理如下:

for iterationCountN, calculate: hue = (n/nMax) * α

其中α是一个小数,目前设置为0.2。色调很容易转换为标准RGB,使用固定的饱和度(s)和亮度(l)值,即转换:

n → hsl colour → rgb colour.
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485