Pull to refresh

GIMP Script-fu: быстрое изучение и написание простых скриптов на Scheme (+ пакетная обработка бесплатно)

Programming *Lisp *Image processing *
Tutorial

Вступление


В статье будет рассказано о том, как в кратчайшие сроки познакомиться с основами скриптинга в GIMP на языке Scheme и приступить непосредственно к решению простых практических задач. Данный материал предназначен лишь для тех, кто собирается автоматизировать рутинную обработку здесь и сейчас, не сильно вдаваясь в тонкости и не жертвуя драгоценным временем. Также, статью не рекомендуется использовать в качестве пособия по Scheme отдельно от Script-fu. Связано это с упрощённым стилем программирования в данном материале и отсутствием освещения других немаловажных фактов, которые нас сейчас волнуют гораздо меньше, чем скорость освоения.

Содержание:
  1. Что нам понадобится?
  2. Коротко о синтаксисе
  3. Переменные
  4. Функции
  5. Списки
  6. Регистрация скрипта в GIMP
  7. Написание кода
  8. Заключение

Что нам понадобится?


Английский интерфейс: для этого достаточно создать переменную окружения «LANG» со значением «en». Зачем это нужно? Во-первых, так будет проще искать соответствие процедур объектам интерфейса. Во-вторых, мне не придется приводить команды на двух языках. В-третьих, на английском языке в интернете больше всего информации.
Консоль Script-fu: Filters → Script-fu → Console. Здесь мы сможем тестировать небольшие кусочки кода, — то, что нужно при освоении языка.
Обозреватель процедур: Help → Procedure Browser. Здесь можно без особого труда найти функцию, выполняющую требуемое действие и прочитать ее полное описание (всё хорошо задокументировано).
Редактор кода с подсветкой и/или подсчётом парных скобок. Оставлю на ваш вкус. Мне хватило Notepad++. Но учтите, скобок будет много!

В нескольких последующих секциях содержатся выдержки из первых четырех страниц документации Script-fu и немного отсебятины. Настоятельно рекомендуется попробовать запустить приведенные ниже примеры в консоли.

Коротко о синтаксисе

  • Все выражения в Scheme должны быть окружены скобками.
  • Имя функции всегда идёт первым в скобках, а затем идут её параметры.
  • Математические операторы также являются функциями.
Самое время привести пример:
(* (+ 1 2) (sqrt (- 13 4)) 10)

Последним будет посчитан результат умножения. Как видно, функции умножения передаётся три аргумента: результат сложения, результат извлечения корня из разности и число. Обратите внимание на количество скобок: они здесь везде обязательны. Это может мешать, но зато всегда понятно, что́ за чем вычисляется.
  • Функция и каждый из аргументов должны быть отделены друг от друга пробелами.
Пример: «(+ 1 2)» — корректный код, «(+1 2)» — не является таковым.
  • Всё, что идет за символом «;», является комментарием и игнорируется.

Переменные


Переменные в Scheme определяются с помощью конструкции let*. Общий вид:
(let*
 (
  (переменная значение)
  ...
  (переменная значение)
 )
 (выражение)
 ...
 (выражение)
)

Если сравнивать с императивными языками, то это что-то вроде объявления локальных переменных. Иными словами, после скобки, закрывающей конструкцию let*, переменные перестают существовать.
Пример:
(let*
 (
  (a 1)
  (b (+ a 2))
 )
 (+ a b)
)

Ещё пример:
(let*
 ( (x 9) )
 (sqrt x)
)

Обратите внимание, что даже в том случае, когда мы определяем только одну переменную, внешние скобки для списка переменных не опускаются!

Новое значение переменной можно присвоить с помощью конструкции set!:
(set! переменная значение)

Пример:
(let*
 ( (a 42) (b 21) (x 0) )
 (set! x (/ a b))
)

Функции


Свои функции можно определить с помощью конструкции define:
(define (имя_функции аргументы) код_функции)

Значением функции будет результат выполнения последней команды в коде функции.
Реализуем функцию вычисления разницы модулей (). Это можно сделать с помощью abs, но мы сделаем чуть сложнее:
(define (difference x y)
 (if (< x 0) (set! x (- x)))
 (if (< y 0) (set! y (- y)))
 (if (> x y) (- x y) (- y x))
)

Здесь мы использовали конструкцию if, которая проверяет истинность первого своего аргумента и в зависимости от этого выполняет либо второй, либо третий аргумент (причём последний, как можно видеть, необязателен).
Обратите внимание, что функция может обращаться со своими аргументами как с переменными, но изменяет она лишь их копии. В этом можно убедиться следующим образом:
(let* ((a 3) (b -4)) (list (difference a b) a b))

(Функция list здесь используется для вывода нескольких результатов — значения функции, переменной a и переменной b, — а подробнее о списках мы поговорим ниже). Запустите в консоли и проверьте, что значения переменных не изменились.

Списки


Чтобы определить список, достаточно написать (никаких запятых):
'(0 1 1 2 3 5 8 13)

Пустой список можно задать как через «'()», так и через «()». Списки могут содержать как атомарные значения, так и другие списки:
(let*
 (
  (x
   '("GIMP" (1 2 3) ("is" ("great" () ) ) )
  )
 )
 x
)

Поскольку один апостроф мы уже написали, внутренние списки предварять им необязательно.
Чтобы добавить к началу списка еще один элемент, нужно воспользоваться функцией конкатенации cons:
(cons 1 '(2 3 4) )

Она одинаково хорошо работает и с пустыми списками («(cons 1 () )» даст список из одного элемента).
Для создания списка, содержащего значения ранее объявленных переменных, потребуется функция list:
(let* ( (a 1) (b 2) (c 3) )
 (list a b c 4 5)
)

Чтобы понять разницу с определением списка через апостроф, замените «(list a b c 4 5)» на «'(a b c 4 5)» и сравните вывод.
Это всё хорошо, а как же получить содержимое списка? Для этого есть две функции. Первая, car, возвращает голову списка, то есть первый элемент. Вторая, cdr, возвращает хвост списка, то есть список, содержащий все элементы кроме первого. Обе функции предполагают, что список непуст. Примеры:
(car '(1 2 3 4) )
(cdr '(1 2 3 4) )
(car '(1) )
(cdr '(1) )

Вместо последовательного вызова car и cdr бывает полезно воспользоваться функциями типа caadr, cddr и т.п. Например, чтобы получить второй элемент списка, напишем следующее:
(cadr '("first" "second") )
что эквивалентно
(car (cdr '("first" "second") ) )

В следующем примере попробуйте добраться до элемента 3, используя только два вызова таких функций:
(let* ( (
 x  '( (1 2 (3 4 5) 6)  7  8  (9 10) )
 ) )
 ; здесь ваш код
)

Если у вас получилось, значит, вы уже почти готовы написать свой первый скрипт.

Регистрация скрипта в GIMP


Прежде чем садиться писать код, обеспечим себе для этого удобные условия.

Для скриптов пользователя GIMP создает в домашней директории папку .gimp-2.6/scripts. Чтобы скрипт подцепился, достаточно поместить в неё scm-файл и в меню GIMP выбрать Filters → Script-fu → Refresh Scripts (это если GIMP уже запущен, а иначе он сам всё загрузит при запуске).

В файл, очевидно, надо поместить написанные нами функции. В нём могут содержаться сколько угодно функций, но неплохо бы логически разные функции разнести по разным файлам, а файлы назвать в честь содержимого. Ещё одна рекомендация, даже соглашение: созданные нами функции должны именоваться по типу script-fu-functionname.

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

Пусть мы хотим написать функцию, улучшающую качество текста на снимке с неравномерной освещённостью (на самом деле, я её уже написал, но это не мешает нам сделать это ещё раз). Вот её определение:
(define (script-fu-readability inImage inLayer inRadius inHigh-input))

Знаю-знаю, здесь только объявление функции, и она ничего не делает. Полезный код будет чуть позже. Сейчас нам и этого вполне достаточно. Регистрация же происходит вот так:
(script-fu-register
 "script-fu-readability"
 "Readability"
 "Improves text readability on the photos. It's needed only when there is a non-uniform illumination"
 "Dragonizer"
 "Copyleft, use it at your own sweet will"
 "January 7, 2011"
 "RGB* GRAY* INDEXED*"
 SF-IMAGE      "The image"     0
 SF-DRAWABLE   "The layer"     0
 SF-ADJUSTMENT "Median blur: radius" '(15 1 20 1 5 0 SF-SLIDER)
 SF-ADJUSTMENT "Levels: intensity of highest input" '(235 0 255 1 10 0 SF-SPINNER)
)
(script-fu-menu-register "script-fu-readability" "<Image>/Filters/User's scripts")

В первую функцию передается следующее. Первый аргумент — имя нашей функции, второй — отображаемое имя, третий — описание, четвертый — автор, пятый — сведения о копирайте, шестой — дата создания. Седьмой — типы поддерживаемых изображений (RGB, RGBA, GRAY, GRAYA, INDEXED, INDEXEDA).

Последующие аргументы являются необязательными. Они (кроме SF-IMAGE и SF-DRAWABLE) позволяют создать в окошке скрипта виджеты, такие как строчки, галки, слайдеры, спиннеры, выбор цвета, шрифта и многое другое, чтобы передать выбор пользователя в функцию. Упомянутый же SF-IMAGE передаст нам ссылку на текущее открытое изображение, а SF-DRAWABLE — на выбранный слой. Я не буду описывать все эти SF-*, их параметры вы можете посмотреть в таблицах здесь (остальное читать не нужно, ибо кратко изложено в этой статье). А ещё советую поглядеть вот эту картинку, чтобы понять, что же вам из этого понадобится (взял отсюда).

Окошко готово, осталось добавить его вызов в меню GIMP, что и делает вторая функция кода выше. Два аргумента: опять же, имя функции и путь к меню. Путь начинается с <Image>, если каких-то веток ранее не существовало, GIMP их добавит.

Ещё пример: если бы мы захотели написать скрипт, создающий изображение с заданными свойствами, мы бы убрали параметры SF-IMAGE и SF-DRAWABLE из первой функции, вместо "RGB* GRAY* INDEXED*" использовали бы пустую строку "" (нам ведь не нужно открытое изображение, мы его создадим), а во второй функции изменили бы путь на что-то вроде "<Image>/File/Create/Something".

Чтобы полюбоваться результатом, сохраним наше творчество в «script-fu-readability.scm» и обновим скрипты. Теперь откроем/создадим какое-нибудь изображение и выберем из меню наш скрипт.

Написание кода


Вот он, вожделенный момент! Но поспешу расстроить: ничего сложного тут нет. Совсем. Функции вы уже писать умеете. А всё, что вам может понадобиться от редактора, легко найти в обозревателе процедур. Нужна какая-то операция со слоями? Ищите по запросу «layer». Инвертировать изображение? Вам нужно что-то, содержащее «invert». И так далее.

Я сделаю только два замечания:
  • Очень неплохо бы заключить все действия, выполняемые скриптом, между функциями gimp-image-undo-group-start и gimp-image-undo-group-end, как это сделано ниже, чтобы пользователю не пришлось отменять каждое действие по-отдельности.
  • Все функции GIMP возвращают результатом списки, независимо от количества данных в результате. Легко проколоться, ожидая, например, layer, а получая (layer). Так что не забывайте делать car в таких случаях.
А теперь пример рабочего кода. Алгоритм я позаимствовал отсюда (спасибо Killy).
(define (script-fu-readability inImage inLayer inRadius inHigh-input)
 (let* (
   (layer2 0)
  )
  (gimp-image-undo-group-start inImage)
  (if (not (= (car (gimp-image-base-type inImage)) GRAY)) (gimp-image-convert-grayscale inImage))
  (set! layer2 (car (gimp-layer-copy inLayer FALSE)))
  (gimp-image-add-layer inImage layer2 -1)
  (plug-in-despeckle RUN-NONINTERACTIVE inImage layer2 inRadius 0 -1 256)
  (gimp-layer-set-mode layer2 DIFFERENCE-MODE)
  (set! inLayer (car (gimp-image-flatten inImage)))
  (gimp-invert inLayer)
  (gimp-levels inLayer HISTOGRAM-VALUE 0 inHigh-input 0.1 0 255)
  (gimp-image-undo-group-end inImage)
 )
)

Имея под рукой браузер процедур, разобраться здесь несложно, если интересно.

Пакетная обработка


Куда-куда? Это ещё не всё. Думаете, мы столько проделали, чтобы написать какой-то несчастный скриптик, обрабатывающий одну картинку? Да руками было бы быстрее! Так что давайте заставим GIMP открыть все файлы из заданной папки, обработать, и сохранить в другую папку.

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

Код частично позаимствован из этого топика (спасибо Apostol), но там он сохраняет файлы, затирая исходные. Функция morph-filename взята отсюда.
(define (morph-filename orig-name new-extension)
 (let* ((buffer (vector "" "" "")))
  (if (re-match "^(.*)[.]([^.]+)$" orig-name buffer)
   (string-append (substring orig-name 0 (car (vector-ref buffer 2))) new-extension)
  )
 )
)

(define (script-fu-batch-readability inInFolder inOutFolder inRadius inHigh-input)
  (let* ((filelist (cadr (file-glob (string-append inInFolder DIR-SEPARATOR "*") 1))))
    (while (not (null? filelist))
      (let* ((filename (car filelist))
          (image (car (gimp-file-load RUN-NONINTERACTIVE filename filename)))
          (layer (car (gimp-image-get-active-layer image)))
        )
        (script-fu-readability image layer inRadius inHigh-input)
        (set! layer (car (gimp-image-get-active-layer image)))
        (set! filename (string-append inOutFolder DIR-SEPARATOR
          (morph-filename (car (gimp-image-get-name image)) "png")))
        (file-png-save2 RUN-NONINTERACTIVE image layer filename filename 0 9 0 0 0 1 0 0 0)
        (gimp-image-delete image)
      )
      (set! filelist (cdr filelist))
    )
  )
)


Заключение


Скрипт Readability вместе с пакетной версией можно скачать здесь (зеркало). Код прокомментирован, даже несколько излишне.

Ещё раз напомню, что статья не претендует на полноту в широком смысле и рассчитана лишь на то, чтобы читатель мог сесть, вдумчиво прочитать, параллельно практикуясь, и начать создавать скрипты, решающие его задачи. Так, чтобы на это не ушло столько же времени, сколько требуется на более-менее качественное изучение языка.

Если вы прочитали статью до конца, то теперь вы умеете Script-fu не хуже меня.
Tags: gimpscript-fuпакетная обработкаtutorial
Hubs: Programming Lisp Image processing
Total votes 56: ↑56 and ↓0 +56
Comments 21
Comments Comments 21

Popular right now