12
\$\begingroup\$

A recent Mandelbrot question (Hi @EBrown) in C# inspired me to build my own using the Java 8 parallel streams to implement the parallelism that's useful for computing each pixel value.

The intention is to incorporate this in to a graphical interface that allows you to navigate and zoom in on the Mandelbrot set.

To do this, I separated the functionality in to three components:

  1. a GUI for presenting the results
  2. an engine for computing the limits for each pixel's computation
  3. a tool that applies color to the results and produces an Image that the GUI can present.

In this question I have only part 2 and part 3 for review.

Note that the limit is used to limit both the number of iterations per pixel, and also to determine the number of colours used for the presentation.

When computing a view in to the set, the code works at a given zoom level, and then maps that zoomed information in to a matrix that represents the pixels in the display that show the zoomed data. This mapping is performed using a "window" that has a center, and a zoom degree. The entire Mandelbrot set is contained in the rectangle bounded by -2.5 and 1.0 on the x axis, and between -1.0 and 1.0 on the y axis.

A parallel stream is used to compute one row of pixels in each thread.

I am looking for ways to optimize this further, and identify other ways it can be improved, styled, or conform better to industry best practices.

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.stream.IntStream;

import javax.imageio.ImageIO;

@SuppressWarnings("javadoc")
public class Mandelbrot {

    public static final class Window {
        private final double centerX, centerY, zoom;

        /**
         * Create a window centered at the given logical location and zoom
         * level.
         * 
         * @param centerX
         *            The X location
         * @param centerY
         *            The Y location
         * @param zoom
         *            The zoom degree (1.0 to 1.79e308 or so)
         */
        public Window(double centerX, double centerY, double zoom) {
            if (zoom < 1.0) {
                throw new IllegalArgumentException("Illegal zoom " + zoom);
            }
            this.centerX = centerX;
            this.centerY = centerY;
            this.zoom = zoom;
        }

        public double getCenterX() {
            return centerX;
        }

        public double getCenterY() {
            return centerY;
        }

        public double getZoom() {
            return zoom;
        }

    }

    /**
     * Compute a matrix of iterations representing a window in to the Mandelbrot set.
     * 
     * @param pixWidth The width of the matrix to compute
     * @param pixHeight The height of the matrix to compute
     * @param limit The limit at which computations assume the coordinate is included in the set.
     * @param window The definition of the location and zoom degree in to the set.
     * @return A matrix containing the computational iterations
     */
    public static final int[][] mandelbrot(final int pixWidth, final int pixHeight,
            final int limit, final Window window) {

        final double mandWidth = 3.5 / window.getZoom();
        final double mandHeight = 2.0 / window.getZoom();
        final double xStep = mandWidth / pixWidth;
        final double yStep = mandHeight / pixHeight;

        final double left = window.getCenterX() - mandWidth / 2.0;
        final double bottom = window.getCenterY() - mandHeight / 2.0;

        final double[] scaleX = IntStream.range(0, pixWidth)
                .mapToDouble(x -> left + x * xStep).toArray();
        final double[] scaleY = IntStream.range(0, pixHeight)
                .mapToDouble(y -> bottom + y * yStep).toArray();

        return IntStream
                .range(0, pixHeight)
                .parallel()
                .mapToObj(
                        y -> IntStream.range(0, pixWidth)
                                .map(x -> countIterations(limit, scaleX[x], scaleY[y]))
                                .toArray()).toArray(s -> new int[s][]);

    }

    private static int countIterations(final int limit, final double x0, final double y0) {
        double x = 0.0;
        double y = 0.0;
        int iterations = 0;
        while (x * x + y * y < 4.0 && iterations < limit) {
            double xt = x * x - y * y + x0;
            y = 2 * x * y + y0;
            x = xt;
            iterations++;
        }
        return iterations;
    }

    /**
     * Create a buffered image mapping the iterations of the mandelbrot to the color palette
     * @param mand The matrics to map.
     * @param color The color to map for the matrix.
     * @return A BufferedImage containing the mapped mandelbrot.
     */
    public static BufferedImage mapMandelbrot(int[][] mand, final int[] color) {
        final int width = mand[0].length;
        final int height = mand.length;
        final BufferedImage image = new BufferedImage(width, height,
                BufferedImage.TYPE_INT_ARGB);
        int[] pixels = IntStream.range(0, width * height)
                .map(p -> color[mand[p / width][p % width] % color.length]).toArray();

        image.setRGB(0, 0, width, height, pixels, 0, width);
        return image;
    }

    /**
     * Produce an increasingly bright sequence of colors that spiral out through the colour wheel.
     * 
     * @param maxIndex the number of colors to produce (inclusive)
     * @return an array of aRGB values that represent the unique colors.
     */
    public static final int[] buildColors(final int maxIndex) {
        final float root = (float) Math.sqrt(maxIndex);
        return IntStream
                .rangeClosed(0, maxIndex)
                .map(c -> maxIndex - c)
                .mapToObj(
                        c -> Color
                                .getHSBColor((c % root) / root, 1.0f, (c / root) / root))
                .mapToInt(c -> c.getRGB()).toArray();
    }

    /**
     * For a given width, return a height that matches the ratio of a Mandelbrot
     * image. A complete Mandelbrot is contained within -2.5 to 1.0 and -1.0 to
     * 1.0 which gives a useful ratio of height to width of 3.5/2.0.
     * <p>
     * The returned height will always be odd which allows the center of the
     * image to be on an exact row.
     * 
     * @param width
     *            the width to compute a height for.
     * @return the corresponding height.
     */
    public static final int getAppropriateHeight(final int width) {
        int height = (int) ((width / 3.5) * 2.0);
        // an odd-numbered height is useful for presentation - especially at
        // zoom 1.0
        return height % 2 == 0 ? height + 1 : height;
    }

    /**
     * Convenience method for testing the mandelbrot function.
     * <p>
     * This method coordinates a complete computation for a mandelbrot of given dimensions.
     * 
     * @param width The resulting image width
     * @param height The resulting image height
     * @param limit The limit of iteration testing for the mandelbrot inclusion
     * @param window The position and scale within the mandelbrot set to compute.
     * @param outdir The location in which to store the resulting image.
     * @throws IOException if the image could not be stored.
     */
    public static final void buildBrot(int width, int height, int limit, Window window,
            File outdir) throws IOException {
        int[] colormap = buildColors(limit);

        long start = System.nanoTime();
        final int[][] mand = mandelbrot(width, height, limit, window);

        long built = System.nanoTime();
        BufferedImage image = mapMandelbrot(mand, colormap);

        long rendered = System.nanoTime();

        File outfile = new File(outdir, String.format(
                "mandelbrot_w%d_x%d_c%.3fms_r%.3fms.png", width, limit,
                (built - start) / 1000000.0, (rendered - built) / 1000000.0));
        ImageIO.write(image, "png", outfile);
        System.out.println("Created " + outfile);
    }

    public static void main(String[] args) throws IOException {
        File outdir = new File("output");
        outdir.mkdirs();

        // simple window centered on the basic set, and at basic zoom
        Window window = new Window(-2.5 + 3.5 / 2, 0.0, 1.0);

        buildBrot(2048, getAppropriateHeight(2048), 1000, window, outdir);
    }

}

A run of the above code will produce an image like:

enter image description here

\$\endgroup\$
12
\$\begingroup\$

Say what you mean and mean what you say

You named your class "Window" when it should really be something more like "Viewport". There are no "Windows" (in the GUI sense) in your code. If you decide to incorporate this into an application that does contain Windows in the GUI sense, this will be hopelessly confusing.

Don't omit essential documentation from the code

Above the code, you've posted an explanation, but that isn't present in the code. If you felt the need to write it in order to explain the code to us, shouldn't it also be part of the code?

Use more descriptive comments

The algorithm for computing a point in the Mandelbrot set is ubitiquous and easily comprehensible to anybody skimming the Wikipedia page, but once you get into hard stuff (like zooming) better comments are required. Compare Ebrown's comments with yours:

// Next, consider `xCenter` and `yCenter` which represent what pixel 
// is `(0,0)` in the specified image size.
Point center = new Point(imageSize.Width / 2, imageSize.Height / 2);

// And we'll scale the size so the brot sits within [-2,2], 
SizeF scaleSize = new SizeF(center.X / 2, center.Y);

Yours:

/**
 * Create a window centered at the given logical location and zoom
 * level.
 * 
 * @param centerX
 *            The X location
 * @param centerY
 *            The Y location
 * @param zoom
 *            The zoom degree (1.0 to 1.79e308 or so)
 */

Which do you think is more indicative of what the code is actually doing?

Here's another example, you describe "limit" as the following:

Note that the limit is used to limit both the number of iterations per pixel, and also to determine the number of colours used for the presentation.

Then in the code:

/**
 * Compute a matrix of iterations representing a window in to the Mandelbrot set.
 * 
 * @param pixWidth The width of the matrix to compute
 * @param pixHeight The height of the matrix to compute
 * @param limit The limit at which computations assume the coordinate is included in the set.
 * @param window The definition of the location and zoom degree in to the set.
 * @return A matrix containing the computational iterations
 */

Limits, computations, Mandelbrot... we're doing something math related so you're going to confuse people into thinking you mean limit in the mathematical sense. Maybe MaxIterations is a better name.

Magic numbers

What is 1.79e308, why does it matter?

Why -2.5 + 3.5 / 2?

Why getAppropriateHeight() etc. (similarly you use 3.5 and 2.0 in mandelbrot without explanation)

Are these pixels? Complex numbers?

All Mandelbrot viewers handle zooming a different way. Don't assume yours is intuitive just because you start off with a nice number (1.0).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.