В одной из статей, посвященных D, я уже описывал множество Мандельброта, которое выглядит очень впечатляюще, но я очень хотел повторить эксперимент еще раз. Такое желание возникло из-за того, что меня не очень устроили результаты прошлого раза: множество получилось перевернутым и к тому же не раскрашенным.
По этой причине, я решил вернуться еще раз к построению данного множества, но уже без помощи DGui.
Начнем с того, что в качестве графической библиотеки мы возьмем dlib, что позволит практически сразу получить файл с картинкой вне зависимости от того, под какой операционной системой (Windows или Linux) будет работать приложение. Также, сам проект будет собираться с помощью универсальной системы сборки для D — dub.
Создав пустой проект и указав в качестве зависимости dlib, получим обычную заглушку для проекта. В полученный в ходе процедуры инициализации файл app.d в папке source проекта необходимо будет поместить уже рассматривавшийся нами код загрузки палитр Matlab в проект с dlib. Также предварительно нужно будет скачать сами палитры в формате СSV (comma-separated value), ссылка на загрузку которых приведена в этой статье.
Код, который необходимо добавить выглядит так:
import std.complex; import std.algorithm; import std.conv; import std.math; import std.range; import std.stdio; import std.string; import std.file; import dlib.image; Color4f[] getPalette(string filename) { Color4f[] palette; Color4f extractField(string triplet) { Color4f color; auto content = triplet.split(";"); color.r = parse!float(content[0]) / 255.0f; color.g = parse!float(content[1]) / 255.0f; color.b = parse!float(content[2]) / 255.0f; return color; } palette = (cast(string)(read(filename))) .splitLines .map!(a => extractField(a)) .array; return palette; }
После описания функции загрузки палитр, опишем само построение множества Мандельброта в виде итеративного процесса (количества итераций мы можем регулировать так, как считаем нужным, но я поставил уже некоторую «константу»). Построение множества очень просто и уже описывалось ранее, но на всякий случай повторюсь: будем вести расчеты с комплексными числами и возьмем стартовое значение 0+0i, а затем каждый раз будем возводить в квадрат это значение и прибавлять некоторую константу. Константа будет менять последовательно свои действительные и мнимые значения в промежутке от -2 до 2, а стартовое значение после процедуры с возведением во вторую степень и прибавлением числа, будет заменяться на новое. В ходе такого процесса, который для каждого значения константы выглядит по новому, мы будем проверять, не уходит ли в бесконечность в ходе конечного количества итераций полученное стартовое значение: для этого мы используем доказанный математический факт о том, что если модуль нового стартового значения больше 2, то произошел «уход в бесконечность» и точка явно не принадлежит множеству.
В этот раз, мы немного оптимизирум код, который почти аналогичен тому, что приводился в предыдущей статье про множество Мандельброта, но вместо вычисления модуля, я решил воспользоваться просто вычислением суммы квадратов действительной и мнимой части (до модуля недостает только извлечения полученного значения квадратного корня) и сравнением полученной суммы с 4 (т.е с 2 в квадрате). Такой подход сэкономит приличную часть времени на вычисления, а их тут будет действительно немало.
Для экономии времени на построения, я также решил немного реогранизовать старый код, выделив проверку на принадлежность точки к множеству в отдельную функцию, а поскольку я слабо помню типы комплексных чисел из std.complex, то для передачи аргумента в эту функцию, был сделанный сомнительный шаг — тип аргумента определен через typeof.
Выделение проверки в отдельный вычислительный блок позволило также реализовать следующую идею: допустим, мы имеем загруженную палитру из 256 цветов (все палитры в файле с палитрами из статьи, которую я упоминал, именно столько цветов и содержат), тогда, если стартовое значение в ходе некоторого количества итераций ушла в бесконечность (т.е точка не попала во множество), то количество итераций может быть использовано в качестве индекса для указания номера цвета в палитре. Именно так можно осуществить раскраску множества Мандельброта: введем дополнительную переменную вне цикла итераций для хранения цвета, и если точка принадлежит множеству, то покрасим ее в цвет минимального значения (это цвет с индексом 0 в палитре), и если не принадлежит — то в цвет, индекс которого в палитре равен количеству итераций.
И вот на этом моменте нам надо предусмотреть следующий момент: если количество итераций будет установлено больше, чем 255, то быть беде — произойдет выход за границы массива, в котором и хранятся цвета палитры. Чтобы этого не произошло воспользуемся замечательным шаблоном сlamp из std.complex, который переводит некотрое значение из стороннего диапазона (диапазон в смысле области значений) в некоторый новый. Данный шаблон принимает число, которое необходимо поместить в новую область значений, минимальный элемент и максимальный элемент нового диапазона значений, и возвращает уже пересчитанный результат.
Дальше все достаточно просто: задаем масштабирующие коэффициенты по абсциссе и ординате таким образом, чтобы начальная точка отрисовки перешла из верхнего левого угла в новую область, которая будет значительно ближе к центру изображения. В этом случае, мы еще и пропорционально растягиваем полученный геометрический образ (именно для этого используется умножение результирующих координат), поскольку если этого не сделать получим сильно уменьшенную картинку, на которой вряд ли что-то можно будет рассмотреть.
Вдобавок ко всему этому, делается приведение икса и игрека к целочисленному значению, поскольку именно значениями такого типа описываются координаты пикселя на изображении в dlib.
Полный код, при испытании которого не забудьте подставить свой путь к интересующей палитре и получаемому изображению:
import std.complex; import std.algorithm; import std.conv; import std.math; import std.range; import std.stdio; import std.string; import std.file; import dlib.image; Color4f[] getPalette(string filename) { Color4f[] palette; Color4f extractField(string triplet) { Color4f color; auto content = triplet.split(";"); color.r = parse!float(content[0]) / 255.0f; color.g = parse!float(content[1]) / 255.0f; color.b = parse!float(content[2]) / 255.0f; return color; } palette = (cast(string)(read(filename))) .splitLines .map!(a => extractField(a)) .array; return palette; } auto drawMandelbrotSet(SuperImage superImage, Color4f[] palette, float step) { SuperImage newImage = image(superImage.width, superImage.height); for (float i = -2.0f; i < 2.0f; i += step) { for (float j = -2.0f; j < 2.0f; j += step) { enum NUMBER_OF_ITERATION = 1024; bool isMandelbrotPoint = true; auto c = complex(i,j); auto z0 = complex(0.0f, 0.0f); Color4f color; bool isBelongToSet(typeof(c) zn) { if (((zn.re ^^ 2) + (zn.im ^^ 2)) > 4) { return false; } else { return true; } } for (size_t k = 0; k < NUMBER_OF_ITERATION; k++) { auto zn = (z0 * z0) + c; if (isBelongToSet(zn)) { z0 = zn; color = palette[0]; } else { isMandelbrotPoint = false; size_t index = clamp(k, 0, 255); color = palette[index]; break; } } auto X = cast(int) (1024 + 512 * i); auto Y = cast(int) (1024 + 512 * j); newImage[X, Y] = color; } } return newImage; } void main(string[] args) { auto img = image(2048, 2048); auto jetPalette = getPalette("/home/aquareji/Загрузки/Palette/jet.csv"); img .drawMandelbrotSet(jetPalette, .0005f) .savePNG("mandelbrot.png"); }
А теперь наглядный результат:
Красивое и занимательное зрелище, которое вы можете сделать еще интереснее, просто поиграв с параметрами шага отрисовки, количеством итераций и координатами стартовой точки (см. определение переменной z0).
А еще, мне при написании этой статьи пришла в голову мысль: а что будет если тоже самое попробовать не в комплексных числах, а скажем в дуальных, а ? Но об этом я может быть расскажу в следующий раз…