使用 GIMP 脚本绘制 Mandelbrot 分形

使用 GIMP 的 Script-Fu 语言创建复杂的数学图像。
79 位读者喜欢这篇文章。
Painting art on a computer screen

Opensource.com

GNU 图像处理程序 (GIMP) 是我进行图像编辑的首选解决方案。它的工具集非常强大且方便,但绘制分形除外,因为分形是您无法轻易手工绘制的东西。这些是引人入胜的数学结构,具有自相似性的特征。换句话说,如果它们在某些区域被放大,它们看起来会与未放大的图片非常相似。除了有趣之外,它们还能制作出非常漂亮的图片!

Portion of a Mandelbrot fractal using GIMPs Coldfire palette

使用 Firecode 旋转和放大的 Mandelbrot 集部分。(Cristiano Fontana,CC BY-SA 4.0

GIMP 可以通过 Script-Fu 自动化,以进行图像的批量处理或创建手工操作不切实际的复杂程序;绘制分形属于后一类。本教程将展示如何使用 GIMP 和 Script-Fu 绘制 Mandelbrot 分形的表示。

Mandelbrot set drawn using GIMP's Firecode palette

使用 GIMP 的 Firecode 调色板绘制的彩色环境 Mandelbrot 集。(Cristiano Fontana,CC BY-SA 4.0

Rotated and magnified portion of the Mandelbrot set using Firecode.

使用 Firecode 旋转和放大的 Mandelbrot 集部分。(Cristiano Fontana,CC BY-SA 4.0

在本教程中,您将编写一个脚本,该脚本在图像中创建一个图层,并绘制一个 Mandelbrot 集的表示,并在其周围绘制彩色环境。

什么是 Mandelbrot 集?

不要惊慌!我不会在这里深入太多细节。对于更精通数学的人来说,Mandelbrot 集被定义为复数 a 的集合,对于这些复数,序列

zn+1 = zn2 + a

z₀ = 0 开始时不会发散。

实际上,Mandelbrot 集是图片中看起来花哨的黑色斑点;漂亮的颜色在集合之外。它们表示序列的幅度超过阈值需要多少次迭代。换句话说,颜色刻度显示了序列超过上限值需要多少步。

GIMP 的 Script-Fu

Script-Fu 是内置于 GIMP 中的脚本语言。它是 Scheme 编程语言 的一种实现。

如果您想更熟悉 Scheme,GIMP 的文档提供了一个深入的教程。我还写了一篇关于使用 Script-Fu 进行批量处理图像的文章。最后,“帮助”菜单提供了一个“过程浏览器”,其中包含非常全面的文档,详细描述了 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 文件夹中。我的路径是 $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,停止迭代的 threshold 值,标识图像边界的 domain-widthdomain-height,以及将图像居中于所需特征的 offset-xoffset-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,则它使用黑色,因为这意味着序列可能是有界的,并且该像素位于 Mandelbrot 集中

; 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-xreal-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)

与用户界面交互

要在图形用户界面 (GUI) 中使用 script-fu-mandelbrot 函数,脚本需要通知 GIMP。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 将新函数放在“图层”菜单中,标签为“创建 Mandelbrot 图层”

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

注册函数后,您可以在过程浏览器中可视化它。

script-fu-mandelbrot function

带有 script-fu-mandelbrot 函数的过程浏览器。(Cristiano Fontana,CC BY-SA 4.0

运行脚本

现在函数已准备就绪并已注册,您可以绘制 Mandelbrot 分形了!首先,创建一个正方形图像,然后从“图层”菜单运行脚本。

默认值是获得以下图像的良好起始设置。第一次运行脚本时,创建一个非常小的图像(例如,60x60 像素),因为此实现速度很慢!我的计算机花了几个小时才创建了以下 1920x1920 像素的全尺寸图像。正如我之前提到的,这不是最优化算法;相反,它是我最容易理解的算法。

Mandelbrot set drawn using GIMP's Firecode palette

使用 GIMP 的 Firecode 调色板绘制的彩色环境 Mandelbrot 集。(Cristiano Fontana,CC BY-SA 4.0

了解更多

本教程展示了如何使用 GIMP 的内置脚本功能绘制使用算法创建的图像。这些图像展示了 GIMP 强大的工具集,可用于艺术应用和数学图像。

如果您想继续前进,我建议您查看官方文档及其教程。作为练习,尝试修改此脚本以绘制一个 Julia 集,并将结果图像分享在评论中。

接下来阅读什么
User profile image.
Cristiano L. Fontana 曾是意大利帕多瓦大学“伽利略·伽利雷”物理与天文系的 researchers,现在正在体验其他新的经历。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.