Processing Mandelbrot improvements
Related to: notes / Mandelbrot set Processing implementation
Todo:
- Deal with floating point precision errors
- Improve the way iteration counts are mapped to colors
Using the double type #
This code fixes the floating point errors by using the double type.
Using a cumulative distribution function to map iterations to colors #
It improves the way that iteration counts are mapped to colors with a Cumulative distribution function:
Cumulative distribution function: the probability that some distribution will have a value equal or less than some value.
I’m doing this in three steps:
Create a histogram of how many numbers in the range (0, maxIter) have each iteration count:
int[] histogram = new int[maxIter + 1];
for (int i = 0; i < iterations.length; i++) {
for (int j = 0; j < iterations[i].length; j++) {
if (iterations[i][j] > 0) histogram[iterations[i][j]]++;
}
}
Then creating a cumulative array (how many numbers have i iterations or fewer):
cumulative = new int[maxIter + 1];
cumulative[0] = histogram[0];
for (int i = 1; i <= maxIter; i++) {
cumulative[i] = cumulative[i-1] + histogram[i];
}
totalEscaped = cumulative[maxIter];
totalEscaped is then a count of the total number of values that are accounted for in the
cumulative array. It’s used to normalize the values into a probability:
void draw() {
for (int i = 0; i < iterations.length; i++) { // i indexes the imaginary axis
for (int j = 0; j < iterations[i].length; j++) { // j indexes the real axis
if (iterations[i][j] == 0) {
stroke(0, 0, 0);
} else { // not in set
int iter = iterations[i][j];
// normalized is the probability
float normalized = (float)cumulative[iter] / totalEscaped;
float hue = map(normalized, 0, 1, 40, 360);
stroke(hue, 74, 55);
}
point(j, i);
}
}
The result is that more common values (the lower iterations) are now assigned more distinct values
by the map function. For example, this will ideally make it possible to distinguish the dividing
line between bands of points that escaped after 3 iterations and bands of points that escaped after
4 iterations.
Improved code #
int rows = 1000;
int cols = 1000;
int maxIters = 10000; // adjust, especially for zoomed in areas
int maxIter = 0;
int totalEscaped;
int[][] iterations = new int[rows][cols];
int[] cumulative;
double aspect = 0.00000160 * 2.0;
double halfAspect = aspect / 2;
double centerReal = -1.769110375463767385;
double centerImag = 0.009020388228023440;
double startReal = centerReal - halfAspect;
double endReal = centerReal + halfAspect;
double startImag = centerImag + halfAspect;
double endImag = centerImag - halfAspect;
double[] imaginaryComponents = linspace(startImag, endImag, rows, "imaginary");
double[] realComponents = linspace(startReal, endReal, cols, "real");
void setup() {
size(1000, 1000); // cols, rows
colorMode(HSB, 360, 100, 100);
iterations = fillArray(imaginaryComponents, realComponents);
for (int i = 0; i < iterations.length; i++) {
for (int j = 0; j < iterations[i].length; j++) {
if (iterations[i][j] > maxIter) {
maxIter = iterations[i][j];
}
}
}
println("maxIter", maxIter);
int[] histogram = new int[maxIter + 1];
for (int i = 0; i < iterations.length; i++) {
for (int j = 0; j < iterations[i].length; j++) {
if (iterations[i][j] > 0) histogram[iterations[i][j]]++;
}
}
cumulative = new int[maxIter + 1];
cumulative[0] = histogram[0];
for (int i = 1; i <= maxIter; i++) {
cumulative[i] = cumulative[i-1] + histogram[i];
}
totalEscaped = cumulative[maxIter];
}
void draw() {
for (int i = 0; i < iterations.length; i++) { // i indexes the imaginary axis
for (int j = 0; j < iterations[i].length; j++) { // j indexes the real axis
if (iterations[i][j] == 0) {
// 0 means "(probably) in the Mandelbrot set"; these points iterated maxIters times without diverging
stroke(0, 0, 0);
} else { // not in set
int iter = iterations[i][j];
float normalized = (float)cumulative[iter] / totalEscaped;
float hue = map(normalized, 0, 1, 40, 360);
stroke(hue, 74, 55);
}
point(j, i);
}
}
String centerStr = String.format("%.8f", centerReal) + "_" + String.format("%.8f", centerImag) + "_" + String.format("%.8f", aspect);
save("mandelbrot_" + centerStr + ".png");
noLoop();
}
int[][] fillArray(double[] imagVals, double[] realVals) {
int[][] result = new int[rows][cols];
for (int i = 0; i < imagVals.length; i++) {
for (int j = 0; j < realVals.length; j++) {
Complex c = new Complex(realVals[j], imagVals[i]);
Complex z = new Complex(0, 0);
boolean diverged = false;
for (int iter = 0; iter < maxIters; iter++) {
if (z.magnitude() >= 2) {
result[i][j] = iter;
diverged = true;
break;
} else {
z = z.mult(z).add(c);
}
}
if (!diverged) {
result[i][j] = 0;
}
}
}
return result;
}
double[] linspace(double start, double end, int num, String plane) {
println("start (" + plane + "):", start);
println("end: (" + plane + "):", end);
println("range:", end - start);
double[] result = new double[num];
if (num == 1) {
result[0] = start;
return result;
}
double step = (end - start) / (num - 1);
println("step:", step);
for (int i = 0; i < num; i++) {
result[i] = start + i * step;
}
return result;
}
class Complex {
double re, im;
Complex(double re, double im) {
this.re = re;
this.im = im;
}
Complex add(Complex other) {
return new Complex(re + other.re, im + other.im);
}
Complex mult(Complex other) {
return new Complex(
re * other.re - im * other.im,
re * other.im + im * other.re
);
}
double magnitude() {
return Math.sqrt(re*re + im*im);
}
}
Images #
The filenames in the images below map to:
- center real
- center imaginary
- range
All images are 1000x1000 pixels (or 1000x1000 points on the complex plane)
start (imaginary): 0.0035000001080334187
end: (imaginary): -0.0035000001080334187
range: -0.007000000216066837
step: -7.007007223290127E-6
start (real): -1.9442999516613781
end: (real): -1.9372999514453113
range: 0.007000000216066837
step: 7.007007223290127E-6
maxIter 9213
start (imaginary): 7.812500552972779E-5
end: (imaginary): -7.812500552972779E-5
range: -1.5625001105945557E-4
step: -1.564064174769325E-7
start (real): -1.9426480960901245
end: (real): -1.942491846079065
range: 1.5625001105945557E-4
step: 1.564064174769325E-7
maxIter 9882
start (imaginary): 1.1718750556610757E-6
end: (imaginary): -1.1718750556610757E-6
range: -2.3437501113221515E-6
step: -2.3460962075296812E-9
start (real): -1.9427046573639473
end: (real): -1.942702313613836
range: 2.3437501113221515E-6
step: 2.3460962075296812E-9
maxIter 19796
start (imaginary): 0.7399999871850014
end: (imaginary): 0.5399999842047691
range: -0.20000000298023224
step: -2.0020020318341566E-4
start (real): -0.3500000014901161
end: (real): -0.14999999850988388
range: 0.20000000298023224
step: 2.0020020318341566E-4
maxIter 9961
start (imaginary): 0.6649999860674143
end: (imaginary): 0.6149999853223562
range: -0.05000000074505806
step: -5.0050050795853916E-5
start (real): -0.27500000037252903
end: (real): -0.22499999962747097
range: 0.05000000074505806
step: 5.0050050795853916E-5
maxIter 99627
References #
Wikipedia contributors. “Cumulative distribution function.” Accessed on: January 29, 2026. https://en.wikipedia.org/wiki/Cumulative_distribution_function .