65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 版曼德尔布罗集

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (25投票s)

2017 年 3 月 22 日

CPOL

7分钟阅读

viewsIcon

54189

downloadIcon

5627

C# 程序,用于生成和探索曼德勃罗集。

引言

在本文中,我将展示大约两个月工作成果。我带着对分形图案的兴趣开始,最终完成了一个能在大约 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。此像素的颜色将使用曼德勃罗方程进行计算。

方程是

{\displaystyle z_{n+1}=z_{n}^{2}+c}

**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 将用于绘制图像的参数保存在文本文件中。此文本文件还用于读取以检索这些参数。

参考文献

维基百科上的曼德勃罗集

HSL 和 RGB 颜色解释

复数入门

© . All rights reserved.