C# 版曼德尔布罗集






4.82/5 (25投票s)
C# 程序,用于生成和探索曼德勃罗集。
- 下载源代码 - 9.6 KB
- 下载 mandelbrotsetwithc__project.zip - 93.7 KB
- 下载 mandelbrotsetwithc__application.zip - 93.1 KB
引言
在本文中,我将展示大约两个月工作成果。我带着对分形图案的兴趣开始,最终完成了一个能在大约 2 秒内生成曼德勃罗集的程序。这个项目极大地帮助了我学习编程,尤其是在优化和效率方面。
背景
首先,我们需要使用复数 **Z**,每个复数都有一个“实部”和一个“虚部”。可以写成
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。此像素的颜色将使用曼德勃罗方程进行计算。
方程是
**c** 是绘制给定像素的坐标,坐标为 **c = x + i * y**。
**n** 是迭代次数,我们从 **n = 0** 开始,并且 **Z0=0+i0**。所以在 **n = 0** 时,
**Zn+1 = Z1 = Z0** **2+c = c**。所以 **Z1 = c**。
现在我们可以将这个 **Z** 值代入方程得到 **Z2**。在 **n = 1** 时,
Zn+1 = Z2= Z12+c
我们继续这样迭代,在每次迭代中生成 **Zn+1**。**但是我们什么时候应该停止迭代呢?**
每次得到一个新的 **Zn+1** 值时,我们还会计算一个称为复数模的值,它写为
| Zn+1 |
它使用勾股定理计算三角形斜边长度
**| Zn+1 | = **√**(x2 + y2)
曼德勃罗告诉我们,如果 **| Zn+1 |** ≤ 2 则停止迭代,否则继续迭代。
我们计算在方程收敛(停止迭代)之前完成的迭代次数 **n**。**n** 值用于表示 **c = x + i * y** 处像素的颜色。
集合的某些部分 **n** 似乎是无限的,或者至少变得非常大,以至于无法继续迭代。我们设定一个最大迭代次数 nMax,并在 **n** 达到 nMax 时总是停止迭代。
事实证明 nMax 非常重要!较高的 nMax 计算时间更长,但可以显示曼德勃罗方程收敛(彩色部分)和发散(白色)区域边界附近的非常精细的细节。但是较低的 nMax 也能产生不错的图像,因为颜色比例可以很好地工作。
请注意:在程序中,**k** 用作迭代计数,**kMax** 用于最大迭代次数。
特点
我决定让我的程序能够做更多的事情,而不仅仅是生成主要的曼德勃罗集,它还有其他一些功能。
我添加了一个缩放功能,您可以设置缩放多少,然后单击鼠标即可渲染新图像。当然,缩放得越远,您就需要更多的迭代才能支持高细节。
您可以在“收藏夹”功能中保存曼德勃罗集的某个区域。这会将您图像的所有参数保存到文本文件中,该文件可以被读取以加载收藏夹。
每次渲染新图像时,它都会将用于创建图像的参数保存到文本文件中。这样,您可以使用“撤销”功能回溯您的图像。
除了一个显示上次渲染花费时间的计时器外,我认为能够保存您的图像也是一个不错的功能。图像以 PNG 格式保存,您可以为其选择一个文件名。
所有保存的文件都存储在 C:\Users\%username%\mandelbrot_config。
使用代码
该程序是一个 Windows 窗体应用程序。可以从 Microsoft Visual Studio 环境中启动。或者,该可执行文件可以作为普通的 Windows 应用程序运行。
用户界面不言自明,控件允许设置绘图的 x、y 坐标范围、允许的最大迭代次数、分辨率(像素步长)和缩放控件。此外,还有一些在本文“功能”部分提到的其他控件。
我们使用了四个 C# 类,它们的使用方式如下:
ComplexPoint.cs
用于封装单个复数点 (Z = x + i * y),其中 x 和 y 分别是实部和虚部。该类包含一些复数算术函数,用于执行曼德勃罗方程背后的数学运算。
ScreenPixelManage.cs
处理数学坐标和物理屏幕坐标(像素坐标)之间的转换。底层的数学坐标独立于屏幕分辨率和大小,而像素坐标适用于运行时屏幕尺寸。
Prompt.cs
自定义提示,在本例中,用于用户输入他们新的收藏夹的名称(参见“功能”)
Mandelbrot.cs
这是项目中的主类,它扩展了 .NET Form 类。用于渲染曼德勃罗集,其中包含允许用户修改要绘制的曼德勃罗集部分、像素步长(分辨率)以及本文“功能”部分提到的其他一些控件。
下面可以看到用于绘制曼德勃罗集的主要代码块。
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 从其最大值开始。
关注点
性能优化
如引言所述,我学会了如何优化代码。在写这篇文章之前,用我的程序渲染曼德勃罗集需要花费很长时间:10 分钟。我上传的这个版本大约需要 2 秒(AMD A8),而一台快速的 PC 可以在 1 秒内完成(AMD FX-8350)。
一个关键的性能改进来自于图像的渲染方式。在这个程序的第一版中,我使用了 System.Drawing,将曼德勃罗集中的每个像素绘制为一个椭圆。这非常消耗 CPU。此外,在以高分辨率(小像素步长)绘制时,使用 System.Drawing 绘制的每个椭圆都会重叠相邻的像素,导致图像略微模糊。我上传的这个版本的程序则使用 bitmap,这解决了这些问题,并且允许在窗体隐藏或最小化时保留图像。
颜色映射
颜色映射用于将我们的迭代值 **n** 转换为像素颜色。许多人投入时间研究不同的颜色映射,大多使用某种查找表,并且经常使用某种插值算法。
我的解决方案非常简单但也非常有效——结果图像,在我看来,和任何其他图像一样好。它的工作原理如下:
对于迭代次数 N,计算
色相 = (n/nMax)α
其中**α** 是一个很小的数字,目前设置为 0.2。色相很容易使用固定的饱和度 (s) 和亮度 (l) 值转换为标准的 RGB,即我们转换
n→ HSL 颜色→RGB 颜色。
实时计算 (n/nMax)α 会非常消耗 CPU,因此我在曼德勃罗计算开始时生成一个 RGB 颜色查找表。
文件 I/O
我还学会了文件 I/O,这对于未来的其他项目可能很有用。如功能所述,我使用文件 I/O 将用于绘制图像的参数保存在文本文件中。此文本文件还用于读取以检索这些参数。