Color Schemes for Mandelbrot Sets

by Jeremy D. Frens on June 26, 2016
part of the Fractals in Elixir series


This is yet another episode in my attempt to write a (better) fractal-generating program in Elixir. The series so far:

I implemented a couple more coloring schemes. They’re a little more interesting that the black-on-white and white-on-black schemes from last week.

As always, my code is in my mandelbrot repo on Github tagged blog_2016_06_26.

Stark coloring schemes

Last week we saw pictures like these:

black on white

white on black

I now implemented a “gray” color scheme:

gray

I really like the effect that it produces. You can now see the connection to the little mini-Mandelbrot off to the left. The tendrils sticking out of the main set also stand out.

It takes just a little math1:

def gray(iterations) when escaped?(iterations), do: PPM.black
def gray(iterations) do
  factor = :math.sqrt(iterations / @maximum_iterations)
  intensity = round(@maximum_intensity * factor)
  PPM.ppm(intensity, intensity, intensity)
end

The inside of the Mandelbrot set is always colored black. The outside of the set gets a scaled gray color. factor scales the intensity so that points that escape fast (with a low number of iterations) are close to black. Points that take a while to escape (close to the set) are lighter colored.

Colors are weird, and our brains are weirder. A simple linear scale (without the square root) doesn’t work for our eyes and brain. It looks rather boring:

linear gray coloring

We need the intensity to get lighter (towards white) faster. Numbers really close to zero grow much larger when you take the square root:

gray scale plot

That’s iterations versus intensity.

Warp coloring scheme

“Warp” is a user on the povusers.org website2. Unfortunately, he (I think) doesn’t include any personal information (like his name), so I can’t give him proper credit. But he suggests a coloring scheme on his Mandelbrot set page.

This “Warp POV” coloring scheme uses two linear scales. For points that escape quickly, the scale is from black to bright red (or green or blue). For points that escape more slowly, the scale is from bright red (or green or blue) to white.

It works pretty well. Red:

red

Green:

green

Blue:

blue

To my eye, I get more detail from the gray-scale. These Warp-POV coloring schemes work very well with the burning ship fractal which we’ll be seeing pretty soon.

The coloring is based on computing two intensities:

def intensities(iterations) when escaped?(iterations), do: {0 , 0}
def intensities(iterations) do
  half_iterations = @maximum_iterations/2-1
  if iterations <= half_iterations do
    {scale(max(1, iterations)), 0}
  else
    {@maximum_intensity, scale(iterations - half_iterations)}
  end
end

def scale(i) do
  round(2.0 * (i - 1) / @maximum_iterations * @maximum_intensity)
end

The primary intensity is used for the color of the color scheme: red, green, or blue. The second intensity is for the other two color components. So for the red color scheme the intensities {128, 0} are turned into PPM.ppm(128, 0, 0). If it were the blue color scheme, the same intensities are turned into PPM.ppm(0, 0, 128).

Random coloring scheme

The challenge with a random coloring scheme is to keep the coloring consistent for one image. If two points escape after 58 iterations, they both need the same color. The colors can vary from run to run, but for one run they need to be consistent.

So somewhere, somehow, you have to keep state. My state is a list of random colors, and I keep it in a process because this is Elixir, of course.

Fractals.Colorizer.Random is a GenServer which is supervised by Fractals.ColorizerSupervisor. The supervisor always starts the Random server, even if another color scheme is selected. Since the process just holds 256 random colors, it doesn’t seem like something that really needs to be optimized.

When the server starts, Random generates a list of random colors:

defp make_colors do
  Enum.map(0..255, &random_color/1)
end
defp random_color(_) do
  PPM.ppm(random_hue, random_hue, random_hue)
end
defp random_hue do
  :random.uniform(255)
end

The result of make_colors is the state for the GenServer. A Random.at function returns the color at a particular index.

The one drawback to using a GenServer is that it’s a bottleneck. ColorizerWorker spawns Tasks to get chunks of the image colored, and each Task concurrently asks Random for colors. Nevertheless, as far as I can tell, the random coloring doesn’t run appreciable slower than another other scheme.

Dispatching

One of my concerns with my old Elixir app was the way I dispatched on the different fractal options. I thought the way I handled the color schemes was okay:

# OLD VERSION!
def color_function(options) do
  case options.color do
    :black_on_white -> &Simple.black_on_white/1
    :white_on_black -> &Simple.white_on_black/1
    :gray           -> &Simple.gray/1
    :blue           -> &WarpPov.blue/1
    :green          -> &WarpPov.green/1
    :red            -> &WarpPov.red/1
    :random         -> Random.build_random
  end
end

The result of color_function was stored as part of the fractal options, before the actual processing of the fractal even got started.

In my reboot, the coloring scheme is picked as late as possible:

def color_point({_zn, iterations}, %Options{color: color}) do
  case color do
    :black_on_white -> black_on_white(iterations)
    :white_on_black -> white_on_black(iterations)
    :gray           -> gray(iterations)
    :red            -> WarpPov.red(iterations)
    :green          -> WarpPov.green(iterations)
    :blue           -> WarpPov.blue(iterations)
    :random         -> Random.at(Random, iterations)
  end
end

I like how this code is explicit about what and how it’s calling the functions for the different coloring schemes. I don’t have to come up with abstractions like I did for color_function.

The dispatch to the different color scheme looks the same, done through a case expression. The difference is that color_point is given the result of the escape-time algorithm along with the fractal options, after the escape-time algorithm has been run. While color_function is called once, color_point is called on every grid point.

I’m having a hard time getting over the extra cost this incurs. Pick the function for the color scheme for every point? Why not pick it once and be done with it? And yet, as I pointed out in the original “Rebooted” article, if I were writing this in an Object-Oriented language like Ruby, I’d use a class for each color scheme. There would be a color_point method with a common interface (can’t get around this in an OO language), and the method would have to be looked up on the color-scheme object for every point. I don’t even flinch at that scenario because that’s OOP. I’m not sure why the analogous thing in FP makes me anxious.

Ideas for future work

The Warp-POV color scheme does not have to stick to the three primary colors. With a bit of tweaking, yellow, magenta, and cyan shouldn’t be too hard to do since they’re the equal blending of two primary colors. The real accomplishment would be to specify any color to put at the halfway point in the color scheme.

I recently discovered a continuous, smooth scaling (wikipedia, stackoverflow, “Renormalizing the Mandelbrot Escape”). I’ll have to spend some time absorbing the math a bit first.

The random coloring scheme needs to be consist for one image. If I tweak my app to generate multiple images at the same time, I might want different random colorings for different images. My current architecture works on only one image, so it’s premature to work on this any time soon.

I’d also like to run some timing experiments. What is the overhead of picking the function for the color scheme on every point? Just how much does the bottleneck of Random affect performance?

One last thing

I also created a Mix task for generating images. I had used a Rakefile for the previous Elixir attempt since I could crib it from the one I had written for my Haskell version.

I decided that I should learn a bit more about Mix, and so I wrote this Mix task.

I’ll talk about about this task in a future article once I’ve worked on it a bit more. Right now it works pretty well, but it’s a bit hacky (e.g., it invokes the fractals executable through the shell).

Next week

I think I’ll tackle the other fractals this week and report on them next time. I already worked out the basic algorithms in my previous Elixir app, so it should just be a matter of adapting them for my new system. Just.

Footnotes

  1. As I mentioned in a previous episode, I write the output image in the PPM format. It’s a simple RGB format written in ASCII. See my PPM module and its specs for more details.

  2. POV—the Persistence of Vision Raytracer—is a pretty impressive, open source raytracer. Or at least it was. It’s been a while since I played with it.

elixir fractals