Ansel Adam's Zones for Gimp (commented script-fu)

This is a script-fu for GIMP to generate the zones of the Ansel Adam’s Zone System. It is heavily commented to know what’s happening behind the scenes.

It generates 11 zones, including Zone 0 (pure black) and Zone X (pure white). They are generated by dividing the histogram in equal parts, based on luminosity.

To be honest, they are not purely equal, and not pure zones, though:

  • there’s a little trick explained in the code to prevent Zone 0 to be included in Zone I, and Zone X to be included in Zone IX
  • a Gaussian blur is used to merge a little the boundaries of the zones. It can be controlled with the radius of the Gaussian blur, and you can set it to 0.0 at the start of the script, if you want no merge at all

When I was searching for a script to do this, I found the Zone System Separator (http://registry.gimp.org/node/28651), but to me it’s overly complicated for the task, and not really suitable for beginners to learn script-fu. So I have written this one from the ground up, hopping it’s easier to understand. In no way I’m saying my script is better than the other one (they do mostly the same), but I’ve done my best to add the necessary comments to explain every line of code.

It has been tested and works under GIMP 2.10.1, and uses non-deprecated functions.

It has methods to:

  • create and use vectors
  • naming layers and find them later on
  • delete layers selectively
  • turn on/off visibility of layers
  • copy a layer and paste its contents into the layer mask of another layer (same thing you do in GIMP in one second, but a little more complicated in script-fu)
  • get the GIMP internal ID of all layers of the image, and use them

; If you're new to script-fu, you may first want to have a look at this basic script:
; https://discuss.pixls.us/t/script-fu-example-batch-script-explained-for-beginners/7341

; Note that everything is enclosed in brackets, and nested brackets and nested-nested
; brackets (ad infinitum).

; Bear in mind that in scheme everything is a list: there may be lists with multiple
; elements, single element lists and even empty (null) lists.

; You MUST pay attention to closing the corresponding opened bracket, or you will most
; likely get an undefined error. Even GIMP not being able to load the script.

; Every function will return a list.
; Most functions will return single element lists (the value we need) and to get access
; to that value, we will have to grab it with "car" "(car (function))".
; THIS LAST SENTENCE IS VERY IMPORTANT to understand most of the scripts you will find.

; A "drawable" in GIMP is roughly anything that could be modified: layers, channels, masks.
; You can set a variable with the contents of a drawable, and access it later on.
; In an already opened image, when launching a script, the drawable is the active layer.

; You must define a variable prior to change its contents with "set!": this command can't
; create the variable, it is only able to change the contents.


(script-fu-register
                  ; Register the script in the GIMP's procedural database
                  ; If we compare a script with a recipe, we will find out that we need
                  ; some fixed ingredients (salt, water, flour, ...), plus some others,
                  ; specific for your own recipe
                  ; Here, we must set a few required parameters (common to every script)
                  ; and some others specific to our own script

                  ; The REQUIRED PARAMETERS are:

     "script-fu-adams-zones"  ; the name of the script
     "Adam's Zones"           ; the name of the script shown in the submenu
     "An approximation to Ansel Adams Zone System"
                              ; a description of the script shown as a tooltip
     "Javier Bartol"          ; author
     "CC-BY-SA-3.0"           ; copyright
     "June 30, 2018"          ; creation date
     "*"                      ; types of images the script is intended to work with

                  ; And then, OUR OWN PARAMETERS (the ones needed by our function)

      SF-IMAGE    "Image"     -1
                              ; this parameter tells GIMP to automatically get the opened
                              ; image ID and assign it to "Image" (MUST BE the first
                              ; parameter in our function)
      SF-DRAWABLE "Drawable"  -1
                              ; now we get the active layer ID of the opened image and
                              ; assign it to "Drawable" (MUST BE the second parameter of
                              ; our function)

                  ; now the rest of the parameters defined in the main function, those
                  ; that will become our input variables

      SF-VALUE    "Gaussian Blur radius"  "0.3"
                              ; the third parameter is the desired blur radius present in
                              ; each mask and applied with the Gaussian Blur filter
)

(script-fu-menu-register "script-fu-adams-zones" "<Image>/Filters/Generic")
                  ; Register the script in the menus (inside Filters>>Generic)


(define (script-fu-adams-zones Image Drawable ask-gauss-radius)
                  ; Declaration of the main function (the part that does the work)

    (let*         ; Here we start the block of function procedures. The special form
                  ; "let*" gives us the opportunity to define variables that could be
                  ; linked between them: that is, if you define several variables, the
                  ; first variable can be used by the second one, and the first and
                  ; second variables can be used by the third one (and so on)
                  ; (http://www.gnu.org/software/mit-scheme/documentation/mit-scheme-ref/Lexical-Binding.html)

                  ; Definition of variables

        (
            (background-layer (car (gimp-image-get-active-drawable Image)))
                              ; gets the id of the drawable, grabs the result with "car",
                              ; and assigns it to "background-layer"
            (desaturated-layer (car (gimp-layer-new-from-drawable background-layer Image)))
                              ; copies the background layer into a new layer
            (magenta '(255 0 255))
                              ; defines the color "magenta" with its RGB values
            (magentalayer 0)  ; defines the variable "magentalayer", that will contain
                              ; a new layer entirely filled with "magenta" color
            (numberofzones 11); sets the number of zones (including pure black and pure
                              ; white) as defined by Ansel Adams
            (zone-number 0)   ; defines the variable that will rule on which zone number
                              ; layer we are working on
            (zone-names (vector "Zone 0" "Zone I" "Zone II" "Zone III" "Zone IV" "Zone V" "Zone VI" "Zone VII" "Zone VIII" "Zone IX" "Zone X"))
                              ; defines a vector that contains the strings needed to give
                              ; name to each zone layer (a vector could be written as "#()")
            (unfocused-layer 0)
                              ; defines a variable that will contain a copy of our desaturated
                              ; (B&W) layer
            (zonesthresholds (vector 0 1 11 22 33 44 55 66 77 88 99 100))
                              ; defines a vector that contains the thresholds needed to
                              ; set boundaries to each zone layer. To prevent Zone 1 to
                              ; include Zone 0, the thresholds for Zone 0 go from 0 to 1.
                              ; Same applies for Zone X: its ranges go from 99 to 100 to
                              ; isolate it from Zone IX
            (zone-layer 0)    ; defines a variable that will contain a copy of our background layer
            (LOWthreshold 0)  ; defines the variable that will contain the low threshold value
            (HIGHthreshold 0) ; defines the variable that will contain the high threshold value
            (mask 0)          ; defines a variable to contain a layer mask
            (floating-sel 0)  ; defines a variable to contain a pasted layer, that will
                              ; need to be attached to a layer
            (IDlayers 0)      ; defines the variable that will contain the internal ID of each layer
            (numberoflayers 0); defines the variable that will contain the total amount of layers

        )
                  ; End of setting variables

                  ; Starting the actual work

        (gimp-image-undo-disable Image)
                              ; disables GIMP's undo cache

        (gimp-item-set-name Drawable "Reference Layer")
                              ; changes the name of our background layer
        (gimp-image-insert-layer Image desaturated-layer 0 -1)
                              ; inserts the desaturated layer on top of the background
        (gimp-drawable-desaturate desaturated-layer DESATURATE-LUMINANCE)
                              ; finally desaturating the layer
        (gimp-item-set-name desaturated-layer "Desaturated Layer")
                              ; changes the name of the desaturated copy of our background
        (set! magentalayer (car (gimp-layer-new-from-drawable background-layer Image)))
                              ; creates a copy of the background in a new layer that will
                              ; be filled with color. This way the new layer will have the
                              ; same settings as the reference layer, without having to get
                              ; all of its parameters and setting each of them in this new
                              ; layer
        (gimp-context-set-foreground magenta)
                              ; sets the foreground color as our "magenta"
        (gimp-drawable-fill magentalayer FILL-FOREGROUND)
                              ; fills the new layer with "magenta" (the foreground color)
        (gimp-layer-set-opacity magentalayer 65)
                              ; sets the opacity of the new layer to 65, so the underlying
                              ; layer could be partially seen
        (gimp-image-insert-layer Image magentalayer 0 -1)
                              ; finally inserts the filled layer on top of the other layers
        (gimp-item-set-name magentalayer "Highlight")
                              ; changes the name of the layer

                  ; Starting a conditional loop

        (while (<= zone-number (- numberofzones 1))
                              ; the total number of zones are 11, but the naming of
                              ; each layer goes from "0" to "X" (or 10), so "zone-number"
                              ; has been defined to start with "0", and has to go up
                              ; to "10"

              (set! unfocused-layer (car (gimp-layer-new-from-drawable desaturated-layer Image)))
                              ; creates a clean copy of the B&W layer on each loop
              (gimp-item-set-name unfocused-layer "TEMP layer")
                              ; gives a name to the copied layer, so we will be able
                              ; to find it and delete it when needed
              (set! zone-layer (car (gimp-layer-copy background-layer FALSE)))
                              ; generates a new copy of our background layer that will
                              ; become one of our zone layers. The trick here is that if
                              ; you define (or set) this new layer outside of the loop,
                              ; as soon as you had inserted it into your image, GIMP
                              ; won't let you add it again as a new layer, because the
                              ; internal ID of the layer would be already present in
                              ; the stack of layers (you would have created a new copy
                              ; of the background only once, and would have pasted
                              ; it already)
              (set! LOWthreshold (/ (vector-ref zonesthresholds zone-number) 100))
                              ; sets the low threshold as the value of the item in our
                              ; vector of thresholds ("zonesthresholds") that has the same
                              ; position as the current zone layer (defined as "zone-number")
                              ; If we are in the fourth iteration of the loop, "zone-number"
                              ; will contain number 3, so the position 3 will be the low
                              ; threshold chosen. Remember that on the first iteration
                              ; (the first time the loop is played), "zone-number" is 0.
                              ; Any threshold value must be within 0.0 and 1.0, so we
                              ; need to divide the picked values by 100
              (set! HIGHthreshold (/ (vector-ref zonesthresholds (+ zone-number 1)) 100))
                              ; sets the high threshold as the value of the item in our
                              ; vector of thresholds ("zonesthresholds") that has one
                              ; position higher than the current zone layer
                              ; (zone 3, item position 4)
              (set! mask (car (gimp-layer-create-mask zone-layer ADD-MASK-BLACK)))
                              ; defining a mask linked to our zone layer
              (gimp-image-insert-layer Image unfocused-layer 0 -1)
                              ; inserts the layer that will be unfocused next
              (gimp-drawable-threshold unfocused-layer HISTOGRAM-LUMINANCE LOWthreshold HIGHthreshold)
                              ; applies the given thresholds to the unfocused-layer
                              ; (the layer will become a mosaic of black and white areas).
                              ; When working with masks in GIMP, white means "opaque"
                              ; and black means "transparent". "Opaque" will show the
                              ; pixels in the associated layer. "Transparent" will show
                              ; anything below the associated layer.
                              ; We have just created and modified a layer where every
                              ; pixel inside a zone is painted white, and the remaining
                              ; pixels, those that pertain to other zones, have been
                              ; painted black. Thus, everything inside a zone will be
                              ; visible, and the rest will be hidden
              (plug-in-gauss-rle RUN-NONINTERACTIVE Image unfocused-layer ask-gauss-radius TRUE TRUE)
                              ; applies the blur with the filter radius asked when
                              ; launching the script. White zones become slightly larger,
                              ; therefore making the zone not "pure", as it merges slightly
                              ; with other zones. You can get rid of this, with a radius
                              ; equal to "0.0", instead of "0.3"
              (gimp-image-insert-layer Image zone-layer 0 -1)
                              ; adds a new top layer to the image, that will be linked to
                              ; the mask needed to isolate our zone
              (gimp-item-set-name zone-layer (vector-ref zone-names zone-number))
                              ; renames the newly created layer as the current zone number.
                              ; We use a vector again: on the sixth iteration (the sixth
                              ; time the loop is played), zone-number will be "5", and
                              ; the string used will be "Zone V"
              (gimp-layer-add-mask zone-layer mask)
                              ; adds a mask in the current zone layer
              (gimp-layer-set-edit-mask zone-layer TRUE)
                              ; makes the mask active for editing
              (set! mask (car (gimp-layer-get-mask zone-layer)))
                              ; makes the variable "mask" to contain the ID of the layer's mask
              (gimp-edit-copy unfocused-layer)
                              ; copies the contents of the unfocused-layer into the
                              ; internal GIMP buffer
              (gimp-floating-sel-anchor (car (gimp-edit-paste mask TRUE)))
                              ; first the content of the buffer is pasted into the
                              ; layer's mask, creating a floating selection. Then that
                              ; floating selection is anchored into the layer's mask.
                              ; When creating the floating selection, the destination
                              ; drawable (in this case the layer's mask) is set. Then
                              ; the floating selection is anchored (actually pasted)
                              ; into the drawable
              (gimp-image-remove-layer Image (car(gimp-image-get-layer-by-name Image "TEMP layer")))
                              ; finds the layer named "TEMP layer" (the name given to
                              ; the unfocused-layer), the same name we can see in GIMP's
                              ; user interface. "car" gets the ID of that layer (the
                              ; value returned by "gimp-image-get-layer-by-name").
                              ; Then removes that layer from the image

              (set! zone-number (+ zone-number 1))
                              ; increases the value of the variable "zone-number", so
                              ; the loop can generate the next zone layer
        )

                  ; Ends the conditional loop

        (set! IDlayers (cadr (gimp-image-get-layers Image)))
                              ; "gimp-image-get-layers" creates a list with two items:
                              ; the first item is the number of layers present in the
                              ; image. The second item is a vector that contains the
                              ; internal ID of every layer (not the string we see in the
                              ; GIMP's interface). Those IDs are sorted from topmost
                              ; layer to bottommost layer, so the first ID will be from
                              ; "Zone X" layer, and the last ID will be from our original,
                              ; "Reference Layer"
                              ; "cadr" gets the content of the second item of the list
                              ; (that is, the actual vector): "cdr" removes the first
                              ; item (the number), and gives a list with just one item
                              ; (but a list, anyway); "car" gets the actual vector from
                              ; that list. "cadr" means "car" after "cdr".
                              ; Now, after all this, "item 0" of the vector "IDlayers" will
                              ; be "Zone X" ID, "item 1" will be "Zone IX" ID, "item 2"
                              ; will be "Zone VIII", and so on
        (set! numberoflayers (car (gimp-image-get-layers Image)))
                              ; sets this variable with the number of layers present in
                              ; the image

                  ; Starts a new conditional loop

        (while (> numberoflayers 0)
                              ; in this loop the intention is to hide all layers, and
                              ; later on show some specific layers to analyse the image
              (gimp-item-set-visible (vector-ref IDlayers (- numberoflayers 1)) FALSE)
                              ; sets the visibility of the layer to hidden.
                              ; The layer ID is an item inside the vector "IDlayers",
                              ; and vectors start counting items from "0", so the first
                              ; layer is vector item "0", and layer 14 is vector item "13"
              (set! numberoflayers (- numberoflayers 1))
        )

                  ; Ends the conditional loop

        (gimp-item-set-visible (vector-ref IDlayers 0) TRUE)
        (gimp-item-set-visible (vector-ref IDlayers 1) TRUE)
        (gimp-item-set-visible (vector-ref IDlayers 2) TRUE)
        (gimp-item-set-visible (vector-ref IDlayers 11) TRUE)
        (gimp-item-set-visible (vector-ref IDlayers 12) TRUE)
                              ; to me, the most interesting layers are the top 3 ("Zone X",
                              ; "Zone IX", and "Zone VIII"), so I need to make visible
                              ; the vector items "0", "1", and "2". Then I need the
                              ; "Highlight" layer (layer 12 from top, item 11) to be visible
                              ; so I can see the areas in that zones. And finally, the
                              ; "Desaturated Layer" (layer 13, item 12), to see the zones
                              ; in context.
                              ; I could have made a list and another loop, but instead
                              ; I've decided to go layer by layer, so I could pick each
                              ; one straight away

        (gimp-image-undo-enable Image)
                              ; enables GIMP's undo cache

      (gimp-displays-flush)
                              ; makes sure everything is updated into the image
    )
                  ; End of the block of function procedures
)
                  ; End of the main function (and the script)

1 Like

Does a great job of separating zones and without any seams. Thanks for sharing. As for using this to convert to greyscale, that still requires you to know how to process and blend each zone right. :slight_smile:

hi @XavAL, it would be nice to see a sample before-and-after pair of images.

@lylejk You’re welcome!

@RawConvert Well, this script does nothing to an image. You can use it to visualize the different zones in your image, as defined in Ansel Adam’s Zone System, to analyse where your darker and lighter areas are, and if you have to change your post-processing. Obviously you can do that eyeballing your image.

And if it’s of no use to somebody, maybe it is yet useful to learn how to create scripts in script-fu (looking into the code).

Anyway, here is an image taken from https://pixabay.com/en/dovetail-butterfly-nature-insect-3495224/

And after running the script, you will see in GIMP this masked result:

dovetail-result

and these layers:

dovetail-layers

As explained in the code, there are five layers visible: Zones VIII (8) to X (10), what I’ve called “Highlight” (a layer filled with magenta), and a desaturated version of the original image.

Anything not being a shade of magenta is a part of the image included in any of the 3 top zones.

You can see next to each zone layer a B&W mask giving you a rough idea of which zones have the most pixels in them (the white patches).

Zones VIII, IX and X are interesting because they have lots of light, but few details, so maybe there are areas in your image where you want details, and don’t have to be in those zones. E.g.: maybe it wouldn’t be a good idea to place the yellow part of the flowers (the petals) in zone VIII or zone IX.

Hope this helps

2 Likes