I’m quite fascinated with the image that the Mandelbrot Set produces, but admittedly, I didn’t understand how to implement the function from the description on Wikipedia.
Thankfully, there are 2 really good videos that give an overview and an in depth explanation of the function, which you can watch at:
Mandelbrot back to basics:
https://youtu.be/FFftmWSzgmk
Mandelbrot Set described:
https://youtu.be/NGMRB4O922I
The formula, [katex] z _{n+1}=z_n^2+c [/katex] , involves complex numbers, which you will see used in my implementation.
In my implementation, we go through each x,y coordinate, checking if at that coordinate, whether or not the values returned are within the Mandelbrot Set … don’t forget to watch the above videos for what that means.
At each x,y coordinate, we have to scale those coordinates to be within a 2×2 box, which has it’s x coordinate centered at -0.5 and it’s y coordinate centered at 0.0… to then call out to my get_number_of_iterations
function, which iterates until the complex number… ‘explodes’, then we return that iteration count back to our calling function.
The resulting grayscale image definitely looks like a Mandelbrot:
Here is the code I created to make the above image:
#include <complex>
#include <cstdint>
#include <filesystem>
#include <iostream>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <vector>
/****************************************************************
* Utility template that helps us find the offset in a 1d vector
* when we are using a (x,y) coordinate.
*/
template <const uint32_t channel_count>
auto offset_in_interleaved_1d_vec(const uint32_t width, const uint32_t x, const uint32_t y, const uint32_t channel) -> size_t
{
const auto offset = (y * width + x) * channel_count + channel;
return offset;
}
/*****************************************************************/
auto get_number_of_iterations(const std::complex<double>& z0, const double size, const int max) -> int
{
auto z = z0;
for (auto i = 0; i < max; i++)
{
if (std::abs(z) > size)
return i;
z = (z * z) + z0;
}
// default to max
return max;
}
/****************************************************************
* From Wikipedia ( https://en.wikipedia.org/wiki/Mandelbrot_set#Formal_definition ):
* `The Mandelbrot set is the set of values of c in the complex plane for which the orbit of the critical point z = 0 under iteration of the quadratic map remains bounded.`
* z{n+1} = z^2{n} + c
*/
auto create_grayscale_mandelbrot_image(const double center_x, const double center_y, const double size, const int max_iterations, const int pixels_wide) -> std::vector<std::uint8_t>
{
// A vector to hold the grayscale image data, pixels_wide^2 in size, and initialized with zero
auto image = std::vector<std::uint8_t>(pixels_wide * pixels_wide, 0);
// Convenience lambda
auto get_scaled_coordinate = [&](const double center, const double xy) {
return (center - (size / 2) + ((size * xy) / pixels_wide));
};
for (auto y = 0; y < pixels_wide; y++)
{
for (auto x = 0; x < pixels_wide; x++)
{
// Scale the x/y coordinate to be within the size x size box
auto x0 = get_scaled_coordinate(center_x, x);
auto y0 = get_scaled_coordinate(center_y, y);
// Find out how many iterations (of the function) we can go through before the complex number becomes unstable
auto z = std::complex<double>(x0, y0);
auto gray = max_iterations - get_number_of_iterations(z, size, max_iterations);
// Get the offset, using the x,y coordinate, to our memory position in the 1D vector
auto offset = offset_in_interleaved_1d_vec<1>(pixels_wide, x, y, 1);
// Now save the grayscale value
image[offset] = gray;
}
}
return image;
}
/*****************************************************************
* Convenience function to create the full path to our output image
*/
auto get_output_file_path() -> std::string
{
const auto current_path = std::filesystem::current_path();
const auto path = current_path / std::filesystem::path("mandelbrot.jpg");
const auto full_path = std::filesystem::weakly_canonical(path);
return full_path.string();
}
/*****************************************************************/
int main(int, char**)
{
// The center x,y for the Mandelbrot box
const auto center_x = -0.5; // center x
const auto center_y = 0.0; // center y
// The size of the Mandelbrot box, which in this case, is a 2x2 box
const auto size = 2.0;
// The number of iterations we will allow for, before aborting
const auto max_iterations = 255;
const auto image_pixels_wide = 512 * 2;
// Create the image data
auto grayscale_image = create_grayscale_mandelbrot_image(center_x, center_y, size, max_iterations, image_pixels_wide);
// Convert the image data to an OpenCV Mat
auto output_gray = cv::Mat(image_pixels_wide, image_pixels_wide, CV_8UC1, grayscale_image.data());
// Write out the data to disk; first up, let's get a path to write to
const auto full_path = get_output_file_path();
const auto successful = cv::imwrite(full_path.c_str(), output_gray);
if (!successful)
std::cout << "Failed to write out file\n";
else
std::cout << "Success!\n";
}
I used CMake to create the project, and here is what the CMakeLists.txt file looks like:
cmake_minimum_required(VERSION 3.0.0)
project(mandelbrot VERSION 0.1.0)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20)
include(CTest)
enable_testing()
find_package( OpenCV REQUIRED )
include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable(mandelbrot main.cpp)
target_link_libraries( mandelbrot ${OpenCV_LIBS} )
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
So, there you have it. My attempt at drawing the Mandelbrot Set.