数学科普

自绘Mandelbrot集合(三)

作者:安迁

六、Java程序代码

本节给出Java程序代码(JavaScript版本见下节)。目前这个程序运行后只能绘出一个全红的800*600的png格式图像,并存在c:盘的temp文件夹下,名为”image.png”。如果你的c:盘下还没有temp文件夹,就请先创建一下。

package mandelbrot;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class Mandelbrot {
    // (1)可调参数
    double ESCAPERADIUS = 4.0;
    int MAXITERNUMBER = 1000;

    // -0.743030 + 0.126433i @ 0.016110 /0.75
    double OX = -0.743030;
    double OY = 0.126433;
    double WIDTH = 0.016110;
    double RATIO = 0.75;

    int IMAGEWIDTH = 800;

    // (2)计算所得参数
    int IMAGEHEIGHT = (int) (IMAGEWIDTH * RATIO);
    double PIXELSIZE = WIDTH / IMAGEWIDTH;
    double COFFSET = IMAGEWIDTH % 2 == 0 ? (IMAGEWIDTH / 2) - 0.5 : (IMAGEWIDTH / 2);
    double ROFFSET = IMAGEHEIGHT % 2 == 0 ? (IMAGEHEIGHT / 2) - 0.5 : (IMAGEHEIGHT / 2);

    // (3)图象缓存
    BufferedImage image = new BufferedImage(IMAGEWIDTH, IMAGEHEIGHT, BufferedImage.TYPE_INT_RGB);

    // (4)程序入口
    public static void main(String[] args) {
        new Mandelbrot().draw();
    }

    // (5)主程序
    void draw() {
        for (int row = 0; row < IMAGEHEIGHT; row++) {
            for (int col = 0; col < IMAGEWIDTH; col++) {
                int color = calcColor(col, row);
                drawColor(col, row, color);
            }
        }

        saveImage();
    }

    // (6)计算颜色
    int calcColor(int col, int row) {
        double cx = (col - COFFSET) * PIXELSIZE + OX;
        double cy = (row - ROFFSET) * PIXELSIZE + OY;
        double d = iter(cx, cy);
        return getColor(d);
    }

    // (7)迭代计算
    double iter(double cx, double cy) {
        return 0;
    }

    // (8)调色盘
    int getColor(double d) {
        return 0xFF0000;
    }

    // (9)在图像像素(col, row)处画上颜色rgb
    void drawColor(int col, int row, int rgb) {
        image.setRGB(col, IMAGEHEIGHT - row - 1, rgb);
    }

    // (10)保存图像
    void saveImage() {
        try {
            ImageIO.write(image, "png", new File("c:\\temp\\image.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

七、JavaScript程序代码

要使用HTML内嵌JavaScript程序的朋友则请将下列代码拷贝粘贴到纯文本编辑器中(比如Windows下的Notepad),并存为html文件,可将其命名为“mandelbrot.html”,然后用网络浏览器打开。你应该能看到显示有一个红色的长方形的页面。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mandelbrot</title>
<script>
// (4)程序入口
window.onload = function() {
    // (1)可调参数
    var ESCAPERADIUS = 4.0;
    var MAXITERNUMBER = 1000;

    // -0.743030 + 0.126433i @ 0.016110 /0.75
    var OX = -0.743030;
    var OY = 0.126433;
    var WIDTH = 0.016110;
    var RATIO = 0.75;

    var IMAGEWIDTH = 800;

    // (2)计算所得参数
    var IMAGEHEIGHT = IMAGEWIDTH * RATIO;
    var PIXELSIZE = WIDTH / IMAGEWIDTH;
    var COFFSET = IMAGEWIDTH % 2 == 0 ? (IMAGEWIDTH / 2) - 0.5 : (IMAGEWIDTH / 2);
    var ROFFSET = IMAGEHEIGHT % 2 == 0 ? (IMAGEHEIGHT / 2) - 0.5 : (IMAGEHEIGHT / 2);

    // (3)图象缓存
    var canvas = document.getElementById("image");
    var context = canvas.getContext("2d");
    canvas.width = IMAGEWIDTH;
    canvas.height = IMAGEHEIGHT;
    var imagedata = context.createImageData(IMAGEWIDTH, IMAGEHEIGHT);

    // (5)主程序
    for (row = 0; row < IMAGEHEIGHT; row++) {
        for (col = 0; col < IMAGEWIDTH; col++) {
            var color = calcColor(col, row);
            drawColor(col, row, color);
        }
    }

    saveImage();

    // (6)计算颜色
    function calcColor(col, row) {
        var cx = (col - COFFSET) * PIXELSIZE + OX;
        var cy = (row - ROFFSET) * PIXELSIZE + OY;
        var d = iter(cx, cy);
        return getColor(d);
    }

    // (7)迭代计算
    function iter(cx, cy) {
        return 0;
    }

    // (8)调色盘
    function getColor(d) {
        return 0xFF0000;
    }

    // (9)在图像像素(col, row)处画上颜色rgb
    function drawColor(col, row, rgb) {
        var pindex = ((IMAGEHEIGHT - row - 1) * IMAGEWIDTH + col) * 4;
        imagedata.data[pindex] = (rgb>>16) & 0xFF;
        imagedata.data[pindex+1] = (rgb>>8) & 0xFF;
        imagedata.data[pindex+2] = rgb & 0xFF;
        imagedata.data[pindex+3] = 255;
    }

       // (10)保存图像
    function saveImage() {
        context.putImageData(imagedata, 0, 0);
    }
};

</script>
</head>
<body>
<canvas id="image" width="0" height="0"></canvas>
</body>
</html>

八、语言相关部分

首先我们解决掉和所使用的计算机语言密切相关,所以在不同语言之间写法很不同的部分。这些部分和Mandelbrot集合的绘制算法其实没什么关系,属于相应语言中的特定知识,我在此只稍微提一下,不作详细解释。

首先是特定的声明,包括Java程序中“// (1)可调参数”前面的那些行,以及HTML文件中不包含在<script></script>之间段落内(即JavaScript程序部分)的那些行,大多属于“如果懂这门语言就必定懂为什么这么写”的内容,不多做解释。除了HTML中的这句

<canvas id="image" width="0" height="0"></canvas>

这是可以看作是创建了一块画布挂在网页上,画布的标识号“id”为“image”。我们将要画出来的图像,就是要显示在这块画布上。

然后是两个版本也都有的“(4)程序入口”部分,程序开始运行时就从这里进入,注意到JavaScript版本的入口在JavaScript程序部分的最上方,而Java版本则是从main()函数开始。

和具体绘图相关的则是第(3)、(9)、(10)部分。

“(3)图象缓存”部分都是定义了一个图像缓存。两种语言的绘图方式都必须先在缓存中进行,绘完以后才一次性地用“(10)保存图像”中的saveImage()函数表现出来。Java版本的saveImage()函数中的语句

    ImageIO.write(image, "png", new File("c:\\temp\\image.png"));

是将图像保存在“c:\temp”文件夹下的image.png文件中,如果把上面语句中的两个“png”都改成“jpg”,那么存成的文件就是jpeg格式的。大家可以按自己喜好决定。而JavaScript版本的saveImage()是将图像缓存内容表现在刚才说到的标识号为“image”的画布上,从而让我们在浏览器上看到画出的图像。

“(9)在图像像素(col, row)处画上颜色rgb”则就是在第四节中提到的点彩画法,呼叫drawColor(col, row, rgb)函数就能在图像缓存的第col列第row行那个像素上点上颜色代码为rgb的颜色。关于以整数表示的RGB颜色代码,我们在谈论调色盘时会作较具体的介绍。特别值得提一句的是,如果仔细看程序代码的话,在两个版本中,当我们宣称是在第row行画点时,我们其实都是在图像缓存的第IMAGEHEIGHT - row - 1行画点。

    image.setRGB(col, IMAGEHEIGHT - row - 1, rgb);

这是因为在数学习惯上,Y轴的方向朝上,越往上纵坐标越大;而在计算机绘图中,Y轴的方向通常则朝下。所以如果我们真的是在图像缓存的第row行画点的话,画出来的Mandelbrot集合图像会是上下颠倒的。

上述两个版本的特定声明部分,程序入口的第(4)部分以及和具体绘图相关的第(3)、(9)、(10)部分一直到本文结束都不会再变动,我也不再加解释。

九、绘图算法部分

剩下的(1)、(2)、(5)、(6)、(7)、(8)部分则是和Mandelbrot集合的绘制具体相关部分,如果对比两个版本,可以发现它们很相似。只是由于两种语言的对数据类型的处理方式不同,在Java语言中必须显式地说明每个变量,每个函数的参数以及返回值的数据类型(intdouble等),而在JavaScript语言中则只须作varfunction声明。相信有一定的计算机编程经验,但没有学过这两种语言的读者也容易看懂。

在“ (1)可调参数”部分定义了7个参数,以Java版本为例:

    // (1)可调参数
    var ESCAPERADIUS = 4.0;
    var MAXITERNUMBER = 1000;

    // -0.743030 + 0.126433i @ 0.016110 /0.75
    double OX = -0.743030;
    double OY = 0.126433;
    double WIDTH = 0.016110;
    double RATIO = 0.75;

    var IMAGEWIDTH = 800;

前两个参数先略过,接下去4个参数定义了一个区域坐标,而最后的IMAGEWIDTH则定义了要绘制的图像的宽度即横向像素数。读者可以在任何时候尝试改变这些参数,尤其是试用本文许多插图下的区域坐标定义,来验证是否能绘出和插图一致的区域的图像。

“ (2)计算所得参数”中则计算了一些通过在(1)中的参数所计算得到的参数,比如所绘图像高度,每个像素所代表的在复平面上的正方形的边长等等。COFFSET和ROFFSET被用在下面要介绍的“(6)计算颜色”函数中,它们的计算公式也许对不熟悉这样写法的读者来说有点奇怪:

double COFFSET = IMAGEWIDTH % 2 == 0 ? (IMAGEWIDTH / 2) - 0.5 : (IMAGEWIDTH / 2);

可其实这无非是在说,如果IMAGEWIDTH是偶数(IMAGEWIDTH % 2等于0的话),那么COFFSET的值就是(IMAGEWIDTH / 2) - 0.5;如果IMAGEWIDTH是奇数,那么COFFSET的值就是IMAGEWIDTH / 2,但因为这是整数除法,要去掉余数,所以结果其实和(IMAGEWIDTH-1) / 2相同。

“(5)主程序”部分就是第四节中介绍的点彩绘图原理。两个针对行和列的循环遍历图片上的每个像素,循环体则是两句无比简单的呼叫:

    int color = calcColor(col, row);
    drawColor(col, row, color);

第一句用我们将介绍的(6)中的calcColor()函数来计算每个像素的颜色,再用前面介绍的(9)drawColor()方法在图像缓存的对应像素上画出计算出来的颜色。两个循环执行后,所有像素都绘制完毕,最后呼叫(10)saveImage()来表现图像。

上面提到的(1)、(2)、(5)部分,除了会变动可调参数的数值外,一直到本文结束也不再改变。

本文中我们将要重大修改的则是剩下的(6)、(7)、(8)部分,当然也是关键。不过目前看起来它们也简单得很。仍以Java版本为例。

    // (6)计算颜色
    int calcColor(int col, int row) {
        double cx = (col - COFFSET) * PIXELSIZE + OX;
        double cy = (row - ROFFSET) * PIXELSIZE + OY;
        double d = iter(cx, cy);
        return getColor(d);
    }

首先计算了两个值cx和cy,它们其实就是第col列第row行那个像素所代表的复平面上的正方形的中心点的坐标,计算中用到了前面计算的参数COFFSET、ROFFSET和PIXELSIZE。“有WIDTH长度的线段,其中心点坐标为OX。线段被均匀分成IMAGEWIDTH段,试问第col段的中心点cx是多少?”这是个非常简单的数学问题,我不作详细推导了,只需注意“第n段”的n是从0开始算的,最左边的是第0段。

计算出像素中心点在复平面上的坐标后,我们将用这个坐标代表的复数进行Mandelbrot集合定义中的迭代运算,即呼叫(7)中的iter()函数,然后调用(8)中的调色盘函数来取得对应的颜色。也就是说,这个像素就被它的中心点代表了。

不过目前(7)和(8)中没什么具体的东西,无论cx和cy是什么,最终(6)总是返回十六进制数FF0000,这是红色的颜色代码,所以绘出的图是一片通红。

枯燥的准备工作终于做完,下面我们将逐步填充和修改(6)、(7)、(8)部分,以达到我们绘制Mandelbrot集合图像的目的。注意到上面两个版本的程序的长度都大约是80行,为达到最终程序只有150行的目标,我们只能再填充70行程序。

<(二)
(四)>