Windows版GIMP-2.10でPython-Fuを試す。

python コンピュータ
Python-FuはGIMPの操作手順をスクリプト化することで自動実行するための仕組みです。Python-Fuの場合スクリプト言語はpythonで、pythonからGIMPをオブジェクトと見立てて操作する感じになります。
実行する方法は2つあり、GIMPで対話型コンソールを起動しPythonを実行する方法と、pythonのスクリプトファイルをplug_insディレクトリに保存しフィルターとして実行する方法があります。コンソールを使って関数などを試しながら最終的にフィルタスクリプトを作る感じになります。
フィルターとしてスクリプトを呼び出す場合、基本的に現在開いている画像ファイル(imageオブジェクト)とレイヤー(drawableオブジェクト)を引数として渡します。任意のパラメータを引数にすることも出来ますが、現在のマウスの座標などは引数にすることは出来ないようなので(筆者が知らないだけかも?)特定の領域を対象にする場合、レイヤー上の選択範囲を利用することに成ります。
Python-Fuとして呼び出せる関数は概ねGIMPのメニューなどから呼び出せる機能に対応します。よく行うメニューを押す手順などをスクリプト化すると作業効率が向上すると考えられます。また、レイヤーのピクセルを直接操作することが出来ますので、ブラシやペンを使った描画方法と合わせて、比較的自由に絵を描画することも出来ます。
ただ、GIMPに組み込まれているPythonを使う関係上、外部ライブラリに頼ることが難しく、基本的にGIMP内の機能を使うか、無い場合pythonで作成する必要があります。pythonでメモリ(ピクセル)をゴリゴリ書き換えるような処理は余り向いていないので、実用的なパフォーマンスが出ない場合があります。畳み込み演算をするような処理を試した所、その処理時間の長さに驚いた記憶があります。
Python-FuでGIMPには無い新しい機能の追加も出来なくは無いですが、既存の機能を組み合わせて使い勝手を向上させるような使い方が良いかもしれません。

Pythonのバージョン

2.7.17
現行のPythonのバージョンは3系ですので若干古いです。

デバック用にコンソールを表示させる起動オプション

PowerShellで実行(gimp-2.10.exeのパスはインストール環境に合わせて変更)

. "C:\Program Files\GIMP 2\bin\gimp-2.10.exe" --console-messages

スクリプト内で

gimp.message("文字列")

と記述するとコンソールに文字列が表示されます。スクリプト登録の確認用

フィルター用プラグイン(.pyスクリプト)の保存場所

メニュー→「編集(E)」→「設定(P)」→「フォルダー」→「プラグイン」
デフォルトでは以下のディレクトリ

%Userprofile%\AppData\Roaming\GIMP\2.10\plug-ins

こちらに保存されたフィルタスクリプトがプラグインとしてGIMPに登録され実行することが出来るようになります。

対話式スクリプト実行コンソールの起動

メニュー→「フィルター(R)」→「Python-Fu」→「コンソール(C)」
表示されるコンソールにスクリプトを直接記述(コピペ)すると最終行で「…」と表示されますので、さらにエンターキーを押すとスクリプトが実行されます。

一括処理スクリプト

画像フォーマット変換

pngファイルをGMIPの標準ファイル形式であるxcfに一括変換します。

対話式コンソールにコピー&ペーストで実行

import os, glob
path = "H:\\python-fu\\png"
outdir = os.path.join(path, "output")
if not os.path.exists(outdir):
    os.mkdir(outdir)

for src_file in glob.glob(os.path.join(path, "*.png")):
    img = pdb.gimp_file_load(src_file, src_file)
    disp = pdb.gimp_display_new(img)
    dst_file = os.path.join(outdir, (os.path.splitext(os.path.basename(src_file))[0]+'.xcf'))
    pdb.gimp_file_save(img, img.active_layer, dst_file, dst_file)
    pdb.gimp_display_delete(disp)

pngファイルを読み込んで拡張子をxcfに変更して保存しているだけに見えますが、これで画像フォーマットの変換がされているようです。

layerを追加してxcf保存

import os, glob
path = "D:\\"
layer_dir = "D:\\layer\\"
outdir = os.path.join(path, "xcf")
if not os.path.exists(outdir):
    os.mkdir(outdir)

for src_file in glob.glob(os.path.join(path, "*.png")):
    img = pdb.gimp_file_load(src_file, src_file)
    disp = pdb.gimp_display_new(img)
    pdb.gimp_layer_add_alpha(img.active_layer)
    layer_path = os.path.join(layer_dir, os.path.basename(src_file))
    gimp.message(layer_path)
    layer = pdb.gimp_file_load_layer(img,layer_path)
    pdb.gimp_layer_add_alpha(layer)
    gimp.message("aaa")
    img.add_layer(layer, 1)
    dst_file = os.path.join(outdir, (os.path.splitext(os.path.basename(src_file))[0]+'.xcf'))
    pdb.gimp_file_save(img, img.active_layer, dst_file, dst_file)
    pdb.gimp_display_delete(disp)

pngファイルを読み込みlayer_dirディレクトリにある同名のpngファイルをlayerとして読み込みます。各レイヤーにアルファチャンネルを追加しxcfで保存します。

選択ガウスぼかし

import os, glob
path = "D:\\python-fu\\png"
outdir = os.path.join(path, "output")
if not os.path.exists(outdir):
    os.mkdir(outdir)

for src_file in glob.glob(os.path.join(path, "*.png")):
    img = pdb.gimp_file_load(src_file, src_file)
    disp = pdb.gimp_display_new(img)
    pdb.plug_in_sel_gauss(img, img.active_drawable, 5.0, 51)
    dst_file = os.path.join(outdir, (os.path.splitext(os.path.basename(src_file))[0]+'.png'))
    pdb.gimp_file_save(img, img.active_layer, dst_file, dst_file)
    pdb.gimp_display_delete(disp)

pdb.plug_in_sel_gaussで選択ガウスぼかしを実行しています。引数でぼかしを調整します。

拡大縮小で画像の高さを揃える

import os, glob, math
from gimpfu import *
path = "D:\\python-fu\\x2"
outdir = os.path.join(path, "output")
if not os.path.exists(outdir):
    os.mkdir(outdir)

for src_file in glob.glob(os.path.join(path, "*.png")):
    img = pdb.gimp_file_load(src_file, src_file)
    sw = img.width
    sh = img.height
    dh = 1080 * 2
    dw = round(float(sw) * (float(dh) / float(sh)))
    disp = pdb.gimp_display_new(img)
    interpolation = 3 # NoHalo
    if (dh < sh):
        interpolation = 4 # LoHalo
    gimp.message("dw:{} dh:{} interpolation:{}".format(dw, dh, interpolation))
    pdb.gimp_image_scale_full(img, dw, dh, interpolation)
    dst_file = os.path.join(outdir, (os.path.splitext(os.path.basename(src_file))[0]+'.png'))
    pdb.gimp_file_save(img, img.active_layer, dst_file, dst_file)
    pdb.gimp_display_delete(disp)

揃えたい高さを変数dhにセット。
補完方法は拡大の場合NoHalo縮小の場合LoHalo。

xcfファイルをpng形式一括エクスポート

import os, glob
path = "F:\\"
for s in glob.glob(os.path.join(path, "*.xcf")):
    img = pdb.gimp_file_load(s, s)
    disp = pdb.gimp_display_new(img)
    d = os.path.join(path, (os.path.splitext(os.path.basename(s))[0]+'.png'))
    pdb.gimp_file_save(img, img.active_layer, d, d)
    pdb.gimp_display_delete(disp)

plug-insスクリプト

plug-insディレクトリに保存しフィルターとして実行するタイプのスクリプト。

選択範囲の隣の範囲をコピーするフィルタスクリプト

#!/usr/bin/env python
# coding: utf8



from gimpfu import *
from array import array

def get_offset(x1, y1, x2, y2, width, height):
    
    sw = x2 - x1 + 1
    sh = y2 - y1 + 1
    
    offset_x = 0
    offset_y = 0
    
    if ((sw > sh) and (y1 - sh) > 0 and (y2 - sh) > 0):
        # Up
        offset_y = sh * -1
    elif ((sw > sh) and (y1 + sh) <= height and (y2 + sh) <= height):
        # Down
        offset_y = sh
    elif ((x1 - sw) > 0 and (x2 - sw) > 0):
        # Left
        offset_x = sw * -1
    elif ((x1 + sw) <= width and (x2 + sw) <= width):
        # Rihgt
        offset_x = sw
    
    
    return (offset_x, offset_y)



def plugin_main(image, layer):
    w = layer.width
    h = layer.height
    
    # gimp.message("A")
    
    (non_empty,x1,y1,x2,y2) = pdb.gimp_selection_bounds(image)
    
    if (non_empty == 0):
        return
    
    pdb.gimp_layer_add_alpha(layer)
    
    # gimp.message("B")
    
    offset_x = 0
    offset_y = 0
    
    (offset_x, offset_y) = get_offset(x1, y1, x2, y2, w, h)
    
    
    dst_rgn = layer.get_pixel_rgn(0, 0, w, h, False, True)
    src_rgn = layer.get_pixel_rgn(0, 0, w, h, False, False)
    
    # gimp.message("C")
    
    for i in range(x1,x2):
        for j in range(y1, y2):
            r = pdb.gimp_selection_value(image, i, j)
            if (r != 0):
                
                
                # gimp.message("V")
                p = map(ord, src_rgn[i+offset_x, j+offset_y])
                red = p[0]
                green = p[1]
                blue = p[2]
                alpha = p[3]
                dst_rgn[i, j] = array("B", [red, green, blue, alpha]).tostring()
                
    # gimp.message("S")
    
    layer.flush()
    layer.merge_shadow()
    layer.update(0,0,w,h)


register("mycpla", "", "", "", "", "",
    "mycpla", 
    "RGB*",
    [
    (PF_IMAGE, "image", "Input image", None),
    (PF_DRAWABLE, "drawable", "Drawable", None)
    ],
    [],
    plugin_main,
    menu = "<Image>/Filters")

main()

ファイル名をmycpla.pyでプラグインディレクトリに保存し、GMIPを再起動するとメニュー→「フィルター(R)」の下の方にmycplaが追加されます。失敗すると追加されないわけですが、エラーも表示されません。以下のデバック用コンソールを駆使して調査することになります。

フィルタの機能は範囲選択した状態で実行すると選択範囲の上部又は左側のピクセルを選択範囲にコピーするスクリプトになります。
ピクセルを一点一点読み込んでは貼り付ける作業をしていますので非常に時間がかかります。コピースタンプ機能を自動化しようと思いましたが、自分のプログラミング能力ではこの辺りが限界でした。

明るい色(白)を透明に暗い色(黒)を不透明にするスクリプト

#!/usr/bin/env python
# coding: utf8

from gimpfu import *
from array import array


def plugin_main(image, layer):
    w = layer.width
    h = layer.height
    
    # gimp.message("A")

    pdb.gimp_layer_add_alpha(layer)

    
    (non_empty,x1,y1,x2,y2) = pdb.gimp_selection_bounds(image)
    
    if (non_empty == 0):
        return
    
    # gimp.message("B")
        
    
    dst_rgn = layer.get_pixel_rgn(0, 0, w, h, False, True)
    src_rgn = layer.get_pixel_rgn(0, 0, w, h, False, False)
    
    # gimp.message("C")
    
    for i in range(x1,x2):
        for j in range(y1, y2):
            r = pdb.gimp_selection_value(image, i, j)
            if (r != 0):
                
                
                # gimp.message("V")
                p = map(ord, src_rgn[i, j])
                red = p[0]
                green = p[1]
                blue = p[2]
                alpha = 255 - int((p[0] + p[1] + p[2])/3)
                dst_rgn[i, j] = array("B", [red, green, blue, alpha]).tostring()
                
    # gimp.message("S")
    
    layer.flush()
    layer.merge_shadow()
    layer.update(0,0,w,h)
    # gimp.message("Q")


register("sctone", "", "", "", "", "",
    "sctone", 
    "RGB*",
    [
    (PF_IMAGE, "image", "Input image", None),
    (PF_DRAWABLE, "drawable", "Drawable", None)
    ],
    [],
    plugin_main,
    menu = "<Image>/Filters")

main()

選択範囲で切り取り新規レイヤーにコピー

#!/usr/bin/env python
# coding: utf8

from gimpfu import *
from array import array

# 選択範囲で切り取り新規レイヤーにコピー

def plugin_main(image, layer):
    pdb.gimp_layer_add_alpha(layer)

    w = layer.width
    h = layer.height
    
    # gimp.message("A")
    
    (non_empty,x1,y1,x2,y2) = pdb.gimp_selection_bounds(image)
    
    # 選択範囲がない場合終了
    if (non_empty == 0):
        return
    
    # gimp.message("B")
    
    
    # 新しいレイヤーの追加
    name = "new"
    width   = image.width
    height  = image.height
    type    = RGB_IMAGE
    opacity = 100
    mode    = NORMAL_MODE
    new_layer = gimp.Layer(image, name, width, height, type, opacity, mode)

    # 最前列にレイヤーを追加
    position = 0
    image.add_layer(new_layer, position)

    # アルファチャンネルの追加
    pdb.gimp_layer_add_alpha(new_layer)
    pdb.gimp_edit_clear(new_layer)
    
    # 透明色で塗りつぶす
    new_layer.fill(TRANSPARENT_FILL)
    
    # 選択範囲を切り取り
    pdb.gimp_edit_cut(layer)
    
    # 新しいレイヤーに貼り付け
    floating_sel = pdb.gimp_edit_paste(new_layer, 0)
    # 貼り付けを固定
    pdb.gimp_floating_sel_anchor(floating_sel)
    
    # 元レイヤーをアクティブに
    pdb.gimp_image_set_active_layer(image, layer)
    
register("CutToNewLayer", "", "", "", "", "",
    "CutToNewLayer", 
    "RGB*",
    [
    (PF_IMAGE, "image", "Input image", None),
    (PF_DRAWABLE, "drawable", "Drawable", None)
    ],
    [],
    plugin_main,
    menu = "<Image>/Filters")

main()

範囲選択した状態で実行すると、レイヤーが新規に追加され、選択範囲が元レイヤーから切り取られ、新しいレイヤーに貼り付けられます。

エラーコンソールの表示

メニュー「ウィンドウ(W)」→「ドッキング可能なダイアログ(D)」→「エラーコンソール(N)」

gimp.message("文字列")

で文字列がコンソールに出力されます。

現在のイメージとアクティブレイヤーの取得

# 現在のイメージを取得
image = gimp.image_list()[0]

# アクティブレイヤー
layer = image.active_layer

対話式コンソールなどでPython-Fuを試す場合、まず現在開いているイメージオブジェクトとアクティブレイヤーを取得する必要があります。gimpで用意されている関数の多くは、この2つを引数に取る場合が多いので、とりあえず抑えておきたいと思います。

ピクセル単位のアクセス

指定座標のピクセル情報を取得
ch, pixel = pdb.gimp_drawable_get_pixel(layer, x, y)
指定座標のピクセル情報をセット
pdb.gimp_drawable_set_pixel(layer, x, y, ch, pixel)
ch…チャンネル数
pixel…[Red(0-255),Green(0-255),Blue(0-255),Alpha(0-255)]
上記スクリプトとの方法とは異なる機能。速度差は感じられない。
(両方とも遅い。実用的な速度が出れば色々なフィルターが作れるのに…)

スクリプトのエンコーディング

# coding: utf8
指定しておかないと日本語のコメントでスクリプトが停止しました。(Python2だから?)

プログレスバー

フィルターの実行で時間がかかる場合など進捗状況を表示する。

プログレスバーの初期化

gimp.progress_init("文字列")

最初に文字列にフィルター名などをセット。

プログレスバーの更新

gimp.progress_update(0.0~1.0)

引数はfloat型で0.0(0%)~1.0(100%)をとる。フィルターの進捗割合を計算してセット。
割り算で値を計算する場合、計算する値はfloat型にキャストしてから計算すること。

プログレスバーの終了

gimp.progress_end()

フィルター終了時に実行

上手く動作しない場合確認する場所

エラーメッセージが表示されていない様でエラーが発生する直前までスクリプトが実行されてしまうので異常が気づきにくい。
・シンタックスエラーなどはGIMPのPython-Fuではなく通常のpython環境で確認する。
・インデントの誤り。空行を削除するとトラブルが少なくなる。

Undo

開始

pdb.gimp_image_undo_group_start(image)


終了

pdb.gimp_image_undo_group_end(image)


最後に

Python-Fuは多機能なGIMPに組み込まれたマクロ的な代物なので、魅力的だと思うのですが意外と日本語の情報が少なくて残念です。
個人的にはOpenCVあたりと組み合わせると楽しいことが出来そうだと思ったのですが、私にはちょっと難しそうです。
開発環境、特にフィルタを作って試す為に何度もGIMPの起動と終了を繰り返すことになり中々大変です。まず自分の環境できちんと動作するフィルタ登録部分の雛形スクリプトを用意するところから始めると良さそうです。

 

コメント

  1. […] Windows版GIMP-2.10でPython-Fuを試す。 | 迷惑堂本舗 […]