Библиотека функций к Script-fu
Для отрисовки некоторого изображения в произвольном месте холста изображения это место надо как то определить. Определим рамку в которой мы будем отображать наши контуры или изображения как три вектора, вектор начала координат, вектор обозначающий горизонтальное направление(ось X), вектор обозначающий вертикальное направление(ось Y).
(struct rect (origin horiz vert))
Так же определим несколько операций над двумерными векторами, которые определяются также как точки на плоскости.
(define (vect+ p1 p2)
(p! (+ (p-x p1) (p-x p2))
(+ (p-y p1) (p-y p2))))
(define (vect- p1 p2)
(p! (- (p-x p1) (p-x p2))
(- (p-y p1) (p-y p2))))
(define (vect-len p)
(dist-xy (p-x p) (p-y p)))
(define (vect-angle p)
(angle-xy (p-x p) (p-y p)))
Для дальнейшего рисования и работы с рамками надо разработать несколько простых функций работающих с рамками:
(define (make-rect-by-point o x y) ;;создать рамку по трём точкам
(rect! o (vect- x o) (vect- y o)))
(define (make-rect-by-vect o h v) ;;создать рамку по трём векторам
(rect! o h v))
(define (make-rect-base h v) ;;создать рамку базирующуюся в начале координат
(rect! (p! 0 0) (p! h 0) (p! 0 v)))
(define (rect-tr2d r tr) ;;выполнить преобразование для рамки.
(make-rect-by-point (p-tr2d (rect-origin r) tr)
(p-tr2d (rect-horiz r) tr)
(p-tr2d (rect-vert r) tr)))
Наиболее интересная это функция rect-tr2d
позволяющая выполнять двумерные преобразования над рамкой.
Нахождение трансформации переводящей исходное изображение в рамку.
Рамка может представлять собой произвольный параллелограм размещённый на холсте. Для того чтобы разместить в ней изображение, необходимо сформировать некую трансформацию переводящую изображение находящееся в начале координат(а именно туда происходит вставка при добавлении одного изображения в другое) в эту рамку. Таким образом наша задача, получить трансформацию имея рамку и размеры исходного изображения.
Для получения трансформации преобразующую исходное изображение в начале координат в рамку, я написал функцию:
(define (make-tr2d-from-rect1 r width height)
(let* ((o (rect-origin r))
(h (rect-horiz r))
(v (rect-vert r))
(horiz-angle (rad2gr (vect-angle h)))
(vert-angle (rad2gr (vect-angle v)))
(hl (vect-len h))
(vl (vect-len v))
(shear-y (- (+ horiz-angle 90) vert-angle))
(vl-shear (* vl (cos (gr2rad shear-y)))))
(comb-tr2d
(make-tr2d-scale (/ hl width) (/ vl-shear height))
(make-tr2d-shear-y shear-y)
(make-tr2d-rot horiz-angle)
(make-tr2d-move (p-x o) (p-y o))))
)
строящую необходимое преобразование из объединения простых трансформаций: масштабирования, сдвига вдоль оси Y, вращения и перемещения.
Для возможности сравнения трансформаций, напишем функцию сравнения.
(define (tr2d- tr1 tr2)
(tr2d! (- (tr2d-m11 tr1) (tr2d-m11 tr2))
(- (tr2d-m12 tr1) (tr2d-m12 tr2))
(- (tr2d-m21 tr1) (tr2d-m21 tr2))
(- (tr2d-m22 tr1) (tr2d-m22 tr2))
(- (tr2d-dx tr1) (tr2d-dx tr2))
(- (tr2d-dy tr1) (tr2d-dy tr2))))
(define (tr2d* tr1 n)
(tr2d! (* (tr2d-m11 tr1) (tr2d-m11 tr2))
(* (tr2d-m12 tr1) (tr2d-m12 tr2))
(* (tr2d-m21 tr1) (tr2d-m21 tr2))
(* (tr2d-m22 tr1) (tr2d-m22 tr2))
(* (tr2d-dx tr1) (tr2d-dx tr2))
(* (tr2d-dy tr1) (tr2d-dy tr2))))
(define (tr2d-eq? tr1 tr2)
(let ((delta 0.00001))
(and (> delta (abs (- (tr2d-m11 tr1) (tr2d-m11 tr2))))
(> delta (abs (- (tr2d-m12 tr1) (tr2d-m12 tr2))))
(> delta (abs (- (tr2d-m21 tr1) (tr2d-m21 tr2))))
(> delta (abs (- (tr2d-m22 tr1) (tr2d-m22 tr2))))
(> delta (abs (- (tr2d-dx tr1) (tr2d-dx tr2))))
(> delta (abs (- (tr2d-dy tr1) (tr2d-dy tr2)))))))
Теперь можно протестировать правильность работы функции создающей трансформацию по рамке и размеру изображения.
Подготовимся к работе с изображениями:
;;(define path-home "D:") ;;для виндовс
(define path-home (getenv "HOME"))
(define path-lib (string-append path-home "/work/gimp/lib/"))
(define path-work (string-append path-home "/work/gimp/"))
(load (string-append path-lib "util.scm"))
(load (string-append path-lib "defun.scm"))
(load (string-append path-lib "struct.scm"))
(load (string-append path-lib "point.scm"))
(load (string-append path-lib "tr2d.scm"))
(load (string-append path-lib "contour.scm"))
(load (string-append path-lib "img.scm"))
;;(load (string-append path-lib "img2.6.scm"))
(load (string-append path-lib "rect.scm"))
(load (string-append path-lib "vect.scm"))
(define i1 (create-1-layer-img 640 480)) ;;подготовим полотно для рисования.
(define isource4 (car (file-png-load 1
(string-append path-work "t4.png") "t4")))
(define width (car (gimp-image-width isource4)))
(define height (car (gimp-image-height isource4)))
(define r0 (make-rect-base width height)) ;;#( rect #( p 0 0 ) #( p 100 0 ) #( p 0 200 ) )
Изображение которое будет отображаться в рамках.
Создадим несколько трансформаций и преобразуем нашу базовую рамку с их помощью.
(define tr1 (make-tr2d-move 100 100))
(define tr2 (comb-tr2d
(make-tr2d-rot 20)
(make-tr2d-move 300 200)))
(define tr3 (comb-tr2d
(make-tr2d-scale 1 3)
(make-tr2d-rot 45)
(make-tr2d-move 300 200)))
(define tr4 (comb-tr2d
(make-tr2d-scale 2 1.5)
(make-tr2d-shear-x -20)
(make-tr2d-shear-y 10)
(make-tr2d-rot -45)
(make-tr2d-move 65 335)))
(define r1 (rect-tr2d r0 tr1)) ;;#( rect #( p 100 100 ) #( p 100 0 ) #( p 0 200 ) )
(define r2 (rect-tr2d r0 tr2)) ;;#( rect #( p 300 200 ) #( p 93.969 34.202 ) #( p -68.404 187.9 ) )
(define r3 (rect-tr2d r0 tr3)) ;;#( rect #( p 300 200 ) #( p 70.71 70.710 ) #( p -424.26 424.26 ) )
(define r4 (rect-tr2d r0 tr4)) ;;#( rect #( p 65 335 ) #( p 80.872 -183.8 ) #( p 249.5 174.7 ) )
Теперь получим преобразования из имеющихся рамок и сравним их с исходными.
(define tr1x (make-tr2d-from-rect1 r1 width height))
(define tr2x (make-tr2d-from-rect1 r2 width height))
(define tr3x (make-tr2d-from-rect1 r3 width height))
(define tr4x (make-tr2d-from-rect1 r4 width height))
(tr2d-eq? tr1 tr1x) ;;#t
(tr2d-eq? tr2 tr2x) ;;#t
(tr2d-eq? tr3 tr2x) ;;#f - ну а почему бы и не проверить? а вдруг? но нет, сравнение работает.
(tr2d-eq? tr3 tr3x) ;;#t
(tr2d-eq? tr4 tr4x) ;;#t
Правильное построение функции нахождения трансформации.
Та функция, которую я продемонстрировал в предыдущем параграфе написана на основе школьных представлений о математике. И поэтому там используются множество тригонометрических преобразований. Что соответственно, усложняет и замедляет её работу. Правильное построение такой функции должно основываться на линейной алгебре. Но работать с такими вычислениями в ручную крайне муторно и поэтому как правило никто с ними не работает. Чтобы преодолеть этот психологический барьер я использую инструменты облегчающие решение различных математических задач. Очень удобным инструментом решения таких задач является программа Maxima.
В программе Maxima строим набор линейных равенств. Где x,y,z это координаты исходной точки. xr,yr,zr это координаты результирующей точки. mXX - это коэффициенты матрицы преобразования. Для нас именно они являются неизвестными.
Координаты исходных точек и результирующих мы возьмём исходя из того, что начальные точки это углы изображения находящегося в начале координат, а координаты результирующих точек, это координаты точек рамки. У нас есть по 3 точки исходных и результирующих, и для каждой точки мы имеем по 2 действительных равенства. Решаем систему линейных уравнений с помощью функции solve и получаем немного громоздкие уравнения для коэффициэнтов матрицы. Чтобы их упростить, представляем что координаты третьей исходной точки, это координаты начала координат, и подставляем этот факт в уравнения. А также подставляем тот факт, что две другие исходные точки лежат на осях координат, что сильно упрощает результирующие уравнения.
Исходя из этих уравнений создаем новую функцию вычисляющую матрицу двумерного преобразования по размерам изображения и результирующей рамки.
(define (make-tr2d-from-rect2 r width height)
(let* ((o (rect-origin r))
(h (vect+ o (rect-horiz r)))
(v (vect+ o (rect-vert r)))
(wh (* width height))
(m11 (/ (- (* height (p-x h)) (* height (p-x o))) wh))
(m12 (/ (- (* height (p-y h)) (* height (p-y o))) wh))
(m21 (/ (- (p-x v) (p-x o)) height))
(m22 (/ (- (p-y v) (p-y o)) height))
(m13 (p-x o))
(m23 (p-y o)))
(tr2d! m11 m12 m21 m22 m13 m23)
))
Небольшая ремарка: коэффициенты в функции не соответствуют коэффициентам в формулах, в силу того что у меня представление матрицы повёрнуто(транспонировано).
Протестируем работу новой функции.
(define tr1m (make-tr2d-from-rect2 r1 width height))
(define tr2m (make-tr2d-from-rect2 r2 width height))
(define tr3m (make-tr2d-from-rect2 r3 width height))
(define tr4m (make-tr2d-from-rect2 r4 width height))
(tr2d-eq? tr1 tr1m) ;;#t
(tr2d-eq? tr2 tr2m) ;;#t
(tr2d-eq? tr3 tr2m) ;;#f
(tr2d-eq? tr3 tr3m) ;;#t
(tr2d-eq? tr4 tr4m) ;;#t
Как можно убедиться результаты она даёт аналогичные предыдущей функции, но не содержит внутри никаких вызовов тригонометрических функций, а вместо 7 вызовов конструкторов преобразований(4 простых трансформации объединятся с помощью 3 конструкторов) всего один.
Давайте продемонстрируем возможное использование полученной функции.
Для начала определим функцию рисующую в заданной рамке:
(define (draw-from-image-trans dest src tr)
(let* ((dw2 (car (gimp-layer-new-from-drawable
(car (gimp-image-get-active-layer src)) dest)))
(m11 (tr2d-m11 tr))
(m12 (tr2d-m12 tr))
(m21 (tr2d-m21 tr))
(m22 (tr2d-m22 tr))
(mx (tr2d-dx tr))
(my (tr2d-dy tr)))
;;(print h)
;;(gimp-image-undo-disable dest)
(gimp-image-add-layer dest dw2 0)
(gimp-item-transform-matrix dw2
m11 m21 mx
m12 m22 my
0 0 1)
;;(gimp-image-undo-enable dest)
(gimp-image-merge-visible-layers dest CLIP-TO-IMAGE)
))
;;или
(load (string-append path-lib "brush.scm"))
(load (string-append path-lib "fig.scm"))
;;2.6
;;(load (string-append path-lib "brush-2.6.scm"))
;;(load (string-append path-lib "fig2.6.scm"))
(define (draw-from-image-rect img src r)
(let* ((width (car (gimp-image-width src)))
(height (car (gimp-image-height src)))
(tr (make-tr2d-from-rect r
width
height)))
;;(gimp-context-push)
(draw-from-image-trans img src tr)
;;(gimp-context-pop)
))
Построим несколько рамок по выбранным точкам:
(define r2 (make-rect-by-vect (p! 50 200) (p! 100 0) (p! 0 200)))
(define r3 (make-rect-by-point (p! 140 65) (p! 107 107) (p! 220 170)))
(define r4 (make-rect-by-point (p! 300 430) (p! 380 230) (p! 200 310)))
(define r5 (make-rect-by-point (p! 450 110) (p! 480 35) (p! 500 260)))
(draw-from-image-rect i1 isource4 r0)
(draw-from-image-rect i1 isource4 r2)
(draw-from-image-rect i1 isource4 r3)
(draw-from-image-rect i1 isource4 r4)
(draw-from-image-rect i1 isource4 r5)
Посмотрим на результат:
Полезное использование.
Итак, возьмём две картикни, одну используем как базу, а вторую как декорацию. Выберем рамку, где расположим вторую картинку и убрав белый цвет фона картинки нанесём располагающийся в ней текст в выбранную рамку, с некоторым коэффициэнтом прозрачности. Фоном выберем построенные ранее рамки. А вот эта картинка будет выступать в качестве декорации:
Предполагаемый алгоритм действий такой:
Загрузить базовое изображение и создать для него окно.
Сделать невидимым его слои(это нужно будет в последствии, когда потребуется установить прозрачность)
Загрузить изображение декорации(окно для него можно не создавать).
Выделить в изображении декорации белый слой.
Сделать его прозрачным.
Определить нужную рамку.
Получившееся изображение отобразить в базовое изображение.
Установить прозрачность для активного слоя в базовом изображении(это и есть изображение в рамке).
Сделать видимым слои базового изображения.
Объединить видимые слои базового изображение.
Сохранить получившуюся картинку.
код:
;;вспомогательные функции работы со слоями.
(define (get-visible-layers img)
(let ((layers (cadr (gimp-image-get-layers img)))
(rez '()))
(for-vect (i layers)
(when (= (car (gimp-drawable-get-visible (vector-ref layers i))) 1)
(set! rez (cons (vector-ref layers i) rez))))
rez))
(define (change-visible-layers layers visible)
(for-list (el layers)
(gimp-drawable-set-visible el visible)))
;;загрузим "подложку" базовое изображение
(define i1 (img-load (string-append path-work "doc/pic/pic38.png")))
(gimp-display-new i1)
(define viz-layers (get-visible-layers i1))
(change-visible-layers viz-layers 0)
;;загрузим накладываемое изображение.
(define isource5 (img-load (string-append path-work "/doc/pic/hello-world.png")))
;;выделим белый цвет
;;(gimp-image-select-color isource5 CHANNEL-OP-ADD (car (gimp-image-get-active-drawable isource5)) '(#xff #xff #xff))
(gimp-by-color-select (car (gimp-image-active-drawable isource5)) '(255 255 255)
0 CHANNEL-OP-ADD 1 0 0 0)
;;сделаем прозрачным выделение(в 2.6 не работает).
(gimp-edit-fill (car (gimp-image-active-drawable isource5)) TRANSPARENT-FILL)
;;определим рамку на базовом изображении
(define r6 (make-rect-by-point (p! 130 100) (p! 566 76) (p! 80 450)))
;;перенесём декорацию на базовое изображение в указанную рамку.
(draw-from-image-rect i1 isource5 r6)
;;сделаем прозрачным получившийся слой.
(gimp-layer-set-opacity (car (gimp-image-active-drawable i1)) 70) ;;установим ещё и прозрачность
;;отобразим скрытый слой.
(change-visible-layers viz-layers 1)
;;объединим видимые слои.
(gimp-image-merge-visible-layers i1 CLIP-TO-IMAGE) ;;объединим слои
;;сохраним результат.
(gimp-file-save 1 i1 (car (gimp-image-active-drawable i1))
(string-append path-work "doc/pic/pic38add-hello.png") "pic38add.png" )
И вот результат:
И всё бы ничего, и не важно что этот вариант не работает для версии GIMP 2.6, НО он не всегда работает и для версии 2.10. Функция gimp-edit-fill
с параметром TRANSPARENT-FILL
просто не заполняет прозрачным слоем выделенное изображение. Даже в версии 2.10 написано, что работает только для режимов заполнения цветом заполнения фронта или фона, а для других режимов её использовать НЕЛЬЗЯ, вернее не нужно, но суть не в этом. Иногда она работает, для простых выделения, а иногда нет, для сложных выделений, таких например как для текста с формулами из Maxima, приведёнными выше. Ну ладно баги и недоработки GIMPа мы оставим его разработчикам, вопрос в другом: А нам то как быть? Слава богу в GIMP есть функция позволяющая обойти эту безобразную недоработку, воспользуемся функцией gimp-selection-float
позволяющей создать слой из выделения.
Наш алгоритм получения изображения немного изменился:
Загрузить базовое изображение и создать для него окно.
Сделать невидимым его слои(это нужно будет в последствии, когда потребуется установить прозрачность)
Загрузить изображение декорации(окно для него можно не создавать).
Выделить в изображении декорации белый слой.
Инвертировать выделение.(т.е выделить всё что не белое)
Создать плавающий слой в декорации используя имеющееся выделение.(и он будет активным слоем в изображении)
Определить нужную рамку.
Получившееся изображение отобразить в базовое изображение.
Установить прозрачность для активного слоя в базовом изображении(это и есть изображение в рамке).
Сделать видимым слои базового изображения.
Объединить видимые слои базового изображение.
Сохранить получившуюся картинку.
Скрытый текст
;;загрузим "подложку" базовое изображение
(define i1 (img-load (string-append path-work "doc/pic/pic38.png")))
(gimp-display-new i1)
(define viz-layers (get-visible-layers i1))
(change-visible-layers viz-layers 0)
;;загрузим накладываемое изображение.
(define isource5 (img-load (string-append path-work "/doc/pic/pic36.png")))
;;для отладки можно выполнить, будет окно с изображением..
;;(gimp-display-new isource5)
;;выделим белый цвет
(gimp-image-select-color isource5 CHANNEL-OP-ADD (car (gimp-image-get-active-drawable isource5)) '(#xff #xff #xff))
;;выделили всё что не белое.
(gimp-selection-invert isource5)
;;создадим по выделению плавающий слой
(define ls1 (car (gimp-selection-float (car (gimp-image-get-active-drawable isource5)) 0 0)))
;;определим рамку
(define r6 (make-rect-by-point (p! 130 100) (p! 566 76) (p! 80 450)))
;;перенесём декорацию на базовое изображение в указанную рамку.
(draw-from-image-rect i1 isource5 r6)
;;установим ещё и прозрачность
(gimp-layer-set-opacity (car (gimp-image-active-drawable i1)) 70)
;;отобразим скрытый слой.
(change-visible-layers viz-layers 1)
;;объединим слои
(gimp-image-merge-visible-layers i1 CLIP-TO-IMAGE)
;;сохраним результат
(gimp-file-save 1 i1 (car (gimp-image-active-drawable i1))
(string-append path-work "doc/pic/pic38add.png") "pic38add.png" )
И вот результат:
Итог.
Если раньше мы брали изображение и трансформацию, и получали какой то результат на изображении. То теперь мы научились, определять место, где мы хотим получить изображение и отображать рисунки строго в заданной рамке. А построение трансформации, с помощью которой мы можем в указанном месте отобразить загружаемое изображение, происходит в автоматическом режиме и скрыто от пользователя.