Рисуем фракталы Мандельброта с помощью языка GIMP Script-Fu

Автор оригинала: Cristiano L. Fontana
  • Перевод
  • Tutorial


Программа GNU Image Manipulation Program (GIMP) – моё решение проблемы редактирования изображений. Набор инструментов у этого редактора очень мощный и удобный, за исключением инструментов, чтобы генерировать фракталы, которые нелегко нарисовать вручную. Фракталы – увлекательные математические конструкции, обладающие свойством самоподобия. Другими словами, если их увеличить в любой области, они будут удивительно похожи на картину до увеличения. Помимо того, что они интересны, они также делают очень красивые картинки!



Сложные математические образы с помощью языка GIMP Script-Fu


Часть фрактала Мандельброта в палитре GIMP Coldfire

GIMP можно автоматизировать с помощью Script-Fu, чтобы выполнять пакетную обработку изображений  или создавать сложные процедуры, которые нецелесообразно выполнять вручную; рисование фракталов попадает в последнюю категорию. Из этого туториала вы узнаете, как при помощи GIMP и Script-Fu нарисовать представление фрактала Мандельброта.

Часть фрактала Мандельброта с помощью палитры Firecode GIMP.

Повёрнутый и увеличенный фрагмент множества Мандельброта в палитре Firecode.

В этом туториале мы напишем сценарий, который создаёт слой в изображении и рисует представление множество Мандельброта в окружении разных цветов.

Что такое множество Мандельброта?


Без паники! Я не буду вдаваться в подробности. Чтобы вы больше понимали, множество Мандельброта определяется как множество комплексных чисел a, для которых последовательность zn+1 = zn2 + a не расходится, начинаясь с z = 0.

На самом деле множество Мандельброта представляет собой причудливую чёрную кляксу на фотографиях; красивые цвета – не часть множества. Они показывают, сколько итераций требуется, чтобы величина последовательности чисел прошла пороговое значение.

Script-Fu в GIMP


Script-Fu – это встроенный в GIMP скриптовый язык, реализация языка программирования Scheme.

Если вы хотите поближе познакомиться со Scheme, документация GIMP предлагает  подробное руководство. Я также написал статью о  пакетной обработке изображений  с помощью Script-Fu. Наконец, меню «Help» предлагает обозреватель процедур с очень обширной документацией, в которой подробно описаны все функции Script-Fu.



Scheme – это Lisp-подобный язык, поэтому его основная характеристика – то, что он использует префиксную нотацию и много круглых скобок. Функции и операторы применяются к списку операндов через префиксы:

(function-name operand operand ...)

(+ 2 3)
 Returns 5

(list 1 2 3 5)
 Returns a list containing 1, 2, 3, and 5

Пишем сценарий


Можно написать свой первый скрипт и сохранить его в папке Scripts, которую можно найти в окне настроек, в разделе Folders Scripts. Мой находится в $HOME/.config/GIMP/2.10/scripts. Создайте файл с именем mandelbrot.scm:

; Complex numbers implementation
(define (make-rectangular x y) (cons x y))
(define (real-part z) (car z))
(define (imag-part z) (cdr z))

(define (magnitude z)
  (let ((x (real-part z))
        (y (imag-part z)))
    (sqrt (+ (* x x) (* y y)))))

(define (add-c a b)
  (make-rectangular (+ (real-part a) (real-part b))
                    (+ (imag-part a) (imag-part b))))

(define (mul-c a b)
  (let ((ax (real-part a))
        (ay (imag-part a))
        (bx (real-part b))
        (by (imag-part b)))
    (make-rectangular (- (* ax bx) (* ay by))
                      (+ (* ax by) (* ay bx)))))

; Definition of the function creating the layer and drawing the fractal
(define (script-fu-mandelbrot image palette-name threshold domain-width domain-height offset-x offset-y)
  (define num-colors (car (gimp-palette-get-info palette-name)))
  (define colors (cadr (gimp-palette-get-colors palette-name)))

  (define width (car (gimp-image-width image)))
  (define height (car (gimp-image-height image)))

  (define new-layer (car (gimp-layer-new image
                                         width height
                                         RGB-IMAGE
                                         "Mandelbrot layer"
                                         100
                                         LAYER-MODE-NORMAL)))

  (gimp-image-add-layer image new-layer 0)
  (define drawable new-layer)
  (define bytes-per-pixel (car (gimp-drawable-bpp drawable)))

  ; Fractal drawing section.
  ; Code from: https://rosettacode.org/wiki/Mandelbrot_set#Racket
  (define (iterations a z i)
    (let ((z (add-c (mul-c z z) a)))
       (if (or (= i num-colors) (> (magnitude z) threshold))
          i
          (iterations a z (+ i 1)))))

  (define (iter->color i)
    (if (>= i num-colors)
        (list->vector '(0 0 0))
        (list->vector (vector-ref colors i))))

  (define z0 (make-rectangular 0 0))

  (define (loop x end-x y end-y)
    (let* ((real-x (- (* domain-width (/ x width)) offset-x))
           (real-y (- (* domain-height (/ y height)) offset-y))
           (a (make-rectangular real-x real-y))
           (i (iterations a z0 0))
           (color (iter->color i)))
      (cond ((and (< x end-x) (< y end-y)) (gimp-drawable-set-pixel drawable x y bytes-per-pixel color)
                                           (loop (+ x 1) end-x y end-y))
            ((and (>= x end-x) (< y end-y)) (gimp-progress-update (/ y end-y))
                                            (loop 0 end-x (+ y 1) end-y)))))
  (loop 0 width 0 height)

  ; These functions refresh the GIMP UI, otherwise the modified pixels would be evident
  (gimp-drawable-update drawable 0 0 width height)
  (gimp-displays-flush)
)

(script-fu-register
  "script-fu-mandelbrot"          ; Function name
  "Create a Mandelbrot layer"     ; Menu label
                                  ; Description
  "Draws a Mandelbrot fractal on a new layer. For the coloring it uses the palette identified by the name provided as a string. The image boundaries are defined by its domain width and height, which correspond to the image width and height respectively. Finally the image is offset in order to center the desired feature."
  "Cristiano Fontana"             ; Author
  "2021, C.Fontana. GNU GPL v. 3" ; Copyright
  "27th Jan. 2021"                ; Creation date
  "RGB"                           ; Image type that the script works on
  ;Parameter    Displayed            Default
  ;type         label                values
  SF-IMAGE      "Image"              0
  SF-STRING     "Color palette name" "Firecode"
  SF-ADJUSTMENT "Threshold value"    '(4 0 10 0.01 0.1 2 0)
  SF-ADJUSTMENT "Domain width"       '(3 0 10 0.1 1 4 0)
  SF-ADJUSTMENT "Domain height"      '(3 0 10 0.1 1 4 0)
  SF-ADJUSTMENT "X offset"           '(2.25 -20 20 0.1 1 4 0)
  SF-ADJUSTMENT "Y offset"           '(1.50 -20 20 0.1 1 4 0)
)
(script-fu-menu-register "script-fu-mandelbrot" "<Image>/Layer/")

Я пройдусь по сценарию, чтобы показать, что он делает.

Приготовьтесь нарисовать фрактал


Поскольку изображение касается прежде всего комплексных чисел, я написал их быструю, грязную реализацию в Script-Fu. Я определил комплексные числа как пары действительных чисел. Затем добавил несколько функций, необходимых для сценария и воспользовался документацией Racket, чтобы разобраться с именам функций и ролями:

(define (make-rectangular x y) (cons x y))
(define (real-part z) (car z))
(define (imag-part z) (cdr z))

(define (magnitude z)
  (let ((x (real-part z))
        (y (imag-part z)))
    (sqrt (+ (* x x) (* y y)))))

(define (add-c a b)
  (make-rectangular (+ (real-part a) (real-part b))
                    (+ (imag-part a) (imag-part b))))

(define (mul-c a b)
  (let ((ax (real-part a))
        (ay (imag-part a))
        (bx (real-part b))
        (by (imag-part b)))
    (make-rectangular (- (* ax bx) (* ay by))
                      (+ (* ax by) (* ay bx)))))

Рисуем фрактал


Новая функция называется script-fu-mandelbrot. Лучшая практика – называть функцию script-fu-something, чтобы её можно было легко идентифицировать в браузере процедур. Функции требуется несколько параметров: image, к которому она добавит слой с фракталом, palette-name, определяющее используемую цветовую палитру, пороговое значение (thresold), чтобы остановить итерации, domain-width и domain-height, которые определяют границы изображения, а также offset-x, offset-y, чтобы центрировать изображение по желаемому объекту. Сценарию также нужны некоторые другие параметры, которые он может вывести из интерфейса GIMP:

(define (script-fu-mandelbrot image palette-name threshold domain-width domain-height offset-x offset-y)
  (define num-colors (car (gimp-palette-get-info palette-name)))
  (define colors (cadr (gimp-palette-get-colors palette-name)))

  (define width (car (gimp-image-width image)))
  (define height (car (gimp-image-height image)))

  ...

Затем он создаёт новый слой и определяет его как drawable. «Drawable» – элемент, на котором мы будем рисовать:

(define new-layer (car (gimp-layer-new image
                                       width height
                                       RGB-IMAGE
                                       "Mandelbrot layer"
                                       100
                                       LAYER-MODE-NORMAL)))

(gimp-image-add-layer image new-layer 0)
(define drawable new-layer)
(define bytes-per-pixel (car (gimp-drawable-bpp drawable)))

Чтобы определить код цвета пикселя, я воспользовался примером Racket на сайте Rosetta Code. Это не самый оптимальный алгоритм, но его легко понять. Даже такой не математик, как я, смог разобраться с алгоритмом. Функция iterations определяет, сколько шагов последовательности требуется, чтобы дойти до порогового значения. Для завершения итерации я использую количество цветов в палитре. Другими словами, если порог слишком высок или последовательность не растёт, вычисление останавливается на значении num-colors. Функция iter->color преобразует количество итераций в цвет с помощью предоставленной палитры. Если число итераций равно num-colors, используется чёрный цвет, поскольку он означает, что последовательность, вероятно, достигла границы и что пиксель находится во множестве Мандельброта:

; Fractal drawing section.
; Code from: https://rosettacode.org/wiki/Mandelbrot_set#Racket
(define (iterations a z i)
  (let ((z (add-c (mul-c z z) a)))
     (if (or (= i num-colors) (> (magnitude z) threshold))
        i
        (iterations a z (+ i 1)))))

(define (iter->color i)
  (if (>= i num-colors)
      (list->vector '(0 0 0))
      (list->vector (vector-ref colors i))))


У меня есть ощущение, что пользователи Scheme не любят использовать циклы, я реализовал функцию прохода по пикселям рекурсивно. Функция loop считывает начальные координаты и их верхние границы. В каждом пикселе она определяет некоторые временные переменные с помощью функции let*: real-x и real-y – это реальные координаты пикселя на комплексной плоскости, в соответствии с параметрами; переменная a – отправная точка последовательности; i – это количество итераций; и, наконец, color – это цвет пикселя. Каждый пиксель окрашивается с помощью функции gimp-drawable-set-pixel, которая является внутренней процедурой GIMP. Особенность её в том, что она необратима и она не является триггером обновления изображения. Поэтому изображение не обновляется во время операции. Для удобства пользователя в конце каждой строки пикселей вызывается функция gimp-progress-update, обновляющая индикатор выполнения в пользовательском интерфейсе:

(define z0 (make-rectangular 0 0))

(define (loop x end-x y end-y)
  (let* ((real-x (- (* domain-width (/ x width)) offset-x))
         (real-y (- (* domain-height (/ y height)) offset-y))
         (a (make-rectangular real-x real-y))
         (i (iterations a z0 0))
         (color (iter->color i)))
    (cond ((and (< x end-x) (< y end-y)) (gimp-drawable-set-pixel drawable x y bytes-per-pixel color)
                                         (loop (+ x 1) end-x y end-y))
          ((and (>= x end-x) (< y end-y)) (gimp-progress-update (/ y end-y))
                                          (loop 0 end-x (+ y 1) end-y)))))
(loop 0 width 0 height)

В конце расчёта функция должна сообщить GIMP, что она изменила drawable, и также должна обновить интерфейс, потому что изображение не обновляется «автоматически» во время выполнения скрипта:

(gimp-drawable-update drawable 0 0 width height)
(gimp-displays-flush)

Взаимодействие с помощью интерфейса


Чтобы мы могли использовать функцию script-fu-mandelbrot в графическом интерфейсе пользователя (GUI), сценарий должен проинформировать об этом редактор. Функция script-fu-register сообщает GIMP о параметрах, требуемых скриптом, и предоставляет некоторую документацию:

(script-fu-register
  "script-fu-mandelbrot"          ; Function name
  "Create a Mandelbrot layer"     ; Menu label
                                  ; Description
  "Draws a Mandelbrot fractal on a new layer. For the coloring it uses the palette identified by the name provided as a string. The image boundaries are defined by its domain width and height, which correspond to the image width and height respectively. Finally the image is offset in order to center the desired feature."
  "Cristiano Fontana"             ; Author
  "2021, C.Fontana. GNU GPL v. 3" ; Copyright
  "27th Jan. 2021"                ; Creation date
  "RGB"                           ; Image type that the script works on
  ;Parameter    Displayed            Default
  ;type         label                values
  SF-IMAGE      "Image"              0
  SF-STRING     "Color palette name" "Firecode"
  SF-ADJUSTMENT "Threshold value"    '(4 0 10 0.01 0.1 2 0)
  SF-ADJUSTMENT "Domain width"       '(3 0 10 0.1 1 4 0)
  SF-ADJUSTMENT "Domain height"      '(3 0 10 0.1 1 4 0)
  SF-ADJUSTMENT "X offset"           '(2.25 -20 20 0.1 1 4 0)
  SF-ADJUSTMENT "Y offset"           '(1.50 -20 20 0.1 1 4 0)
)

Затем скрипт просит GIMP поместить новую функцию в меню слоя, у функции в меню будет надпись «Create a Mandelbrot layer»:

(script-fu-menu-register "script-fu-mandelbrot" "<Image>/Layer/")

Зарегистрировав функцию, вы можете визуализировать её в браузере процедур.



Запускаем скрипт


Теперь, когда функция готова и зарегистрирована, вы можете нарисовать фрактал Мандельброта! Сначала создайте квадратное изображение и запустите скрипт из меню Layers (Слои).



Значения по умолчанию – хороший старт, чтобы получить следующее изображение. При первом запуске скрипта создайте очень маленькое изображение (например, 60х60 пикселей), поскольку эта реализация медленная! Моему компьютеру потребовалось несколько часов, чтобы создать изображение ниже в полном разрешении 1920х1920 пикселей. Как я уже упоминал ранее, это не самый оптимальный алгоритм; скорее его проще понять.

Часть фрактала Мандельброта в палитре Firecode GIMP.

Узнать больше


В этом туториале я показал, как использовать скриптинг GIMP, чтобы нарисовать изображение при помощи алгоритма. Сгенерированные алгоритмом изображения демонстрируют мощный набор инструментов GIMP, которые могут использоваться в искусстве и для того, чтобы создавать математические изображения.

Если вы хотите продвинуться дальше, предлагаю вам ознакомиться с официальной документацией и туториалом. В качестве упражнения попробуйте изменить этот скрипт так, чтобы нарисовать множество Жюлиа.

image
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение:

SkillFactory
Школа Computer Science. Скидка 10% по коду HABR

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое