Windows版GIMP3でPython-Fuを試す。

コンピュータ

Windows版GIMP3.0をインストールしましたので、そちらでPython-Fuのプラグインスクリプトを動作させるべく、色々情報集めをし、試した結果をまとめて行きたいと思います。

はじめに

GIMPを使う目的はひとそれぞれ、写真の加工に使う人もいれば、広告やポスターなどのデザインツールとして使ったり、イラストなどアートツールとして使う人もいるでしょう。有償ソフトに匹敵するとは言い切れませんが、非常に多機能で多くの人に有用なソフトであることは間違いありません。

さて、筆者個人の使い方と言いますと、動画素材として既存の静止画から、不要な部分の消去(インペイント)だったり、パーツごとにレイヤー分けしたり、結果欠損部分の加筆を行ったりする様な用途に使っています。新しい物を作るというより、既存の作品を再編集することに近い作業になると思います。

そうなると比較的、特定の機能をきめられた順番で呼び出すことが多く、ショートカットキーなどを駆使することに成りますが、指がつりそうになります。また、たまにしか使わない機能は手順を忘れがちになります。

よく使う手順を登録しておき、呼び出して実行する機能がGIMPにも備わっており、その手順はpython言語で記述することが出来ます。
こちらの記事ではその方法を調べ、実践した結果をまとめた記事になる予定です。気が向いたら随時更新の予定です。

pythonコンソールの起動

メインメニューで
フィルター(R)→Development→Python-Fu→Python Console
image
python-fuはpythonでGIMPを構成するオブジェクトを操作することで、手順の自動化などを行うことが目的となります。
GIMP内のpythonを使い対話形式で実行するのが、こちらのコンソールとなります。こちらのコンソールからGIMPのオブジェクトを扱うことが出来るので、プロシージャブラウザで使えそうなプロシージャを検索し、こちらのコンソールで試すことが、開発の基本手順に成りそうです。

pythonコンソールでアクティブレイヤーの取得

GIMPのプロシージャ(関数)はImage(Drawable)やlayerオブジェクトを引数に取るケースが多いので、pythonコンソールで現在選択しているアクティブレイヤーを取得する方法を調べて見ました。

# いま開いている画像(先頭=通常は現在の画像)
img = Gimp.get_images()[0]

# 選択中レイヤー(複数選択に対応)
sel_layers = img.get_selected_layers()

# “従来の active_layer 相当”を 1 枚だけ欲しい場合
layer = sel_layers[0] if sel_layers else None
print(layer.get_name() if layer else "no layer")
# 背景←レイヤー名が表示されると成功

後はプロシージャブラウザでプロシージャを探し、コンソールで試す旅が始められます。

プロシージャブラウザの起動

メインメニューで
ヘルプ(H)→プロシージャブラウザ(B)

image

プロシージャブラウザで
gimp-edit-named-cut
の場合、Python-Fuでは

Gimp.edit_named_cut()

の様に最初のgimpがGimpオブジェクトに相当し、その後の-(ハイフン)は_(アンダーバー)に読み替える感じです。(曖昧)

2.10では多くのメソッドがpdbからアクセスしていましたが、3.0からは各クラスからアクセスする模様。

主なクラス
Gimp … GIMP全般
Gimp.Selection … 選択
Gimp.Layer … レイヤー
Gimp.Image … イメージ
Gegl.Color … 色

プロシージャブラウザの検索結果の単純な読み替え対応では難しそうです。以下のサイトでメソッドが検索できる模様

PyGObject API Reference

プラグインの登録

対話型コンソールでもpythonスクリプトを実行することが出来ますが、毎度コンソールを建て上げてコードを用意するのは面倒です。
定型的なスクリプトはプラグインとして登録しフィルターメニューから呼び出して実行出来ると便利です。

プラグインファイルを保存するディレクトリは設定のフォルダから確認出来ます。

筆者の環境では以下のパスに成ります。

C:\Users\ユーザー名\AppData\Roaming\GIMP\3.0

そちらにプラグイン用のサブディレクトリを作成し、そちらにスクリプトを保存します。

プラグイン用のスクリプトは結構、メニューの登録やオプションダイアログなど、お作法的な部分も多いですが、とりあえず動けば良いという個人向けであれば、動作実績のある簡単なスクリプトを参考にすれば、色々なプラグインが作れると思います。

メモ帳を起動するプラグイン

pythonのsubprocessが使えるようなので外部コマンドの起動のサンプルとしてメモ帳(notepad.exe)を起動するプラグインを作成してみました。

#!/usr/bin/env python3
# GIMP 3 (Python-Fu) : notepad.exe を起動する
import sys, gi, subprocess
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
PROC = "python-fu-run-notepad"
def run(proc, run_mode, image, drawables, config, data):
    try:
        # Windows のメモ帳を起動(PATH 上にある想定)
        subprocess.run(["notepad.exe"])
        Gimp.message("notepad.exe を起動しました。")
        return proc.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
    except Exception as e:
        Gimp.message(f"起動失敗: {e}")
        return proc.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, None)
class RunNotepad(Gimp.PlugIn):
    def do_query_procedures(self):
        return [PROC]
    def do_create_procedure(self, name):
        if name != PROC:
            return None
        p = Gimp.ImageProcedure.new(self, name, Gimp.PDBProcType.PLUGIN, run, None)
        p.set_menu_label("Run Notepad")
        p.add_menu_path("<Image>/Filters/My")   # 画像を開いている時に表示
        p.set_documentation(
            "Launch notepad.exe via subprocess",
            "外部の notepad.exe を起動するサンプル(ImageProcedure版)",
            "run_notepad.py"
        )
        p.set_attribution("Your Name", "Public Domain", "2025")
        # 画像が必要なメニュー配下なので image types を指定
        p.set_image_types("*")
        # 描画対象が選べる状態で有効化(最低限の感度)
        p.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.DRAWABLE)
        return p
Gimp.main(RunNotepad.__gtype__, sys.argv)

メモ帳の終了を待つプログラムになっています。画像の加工プログラムを自作して外部プロセスとして呼び出す目処が立ちました。

Python-Fuのコンソールから現在開いている画像をエクスポート

注意、アクティブレイヤーではなく現在見えている状態がそのままPNG形式でエクスポートされます。

# 現在開いている0番目の画像を取得
img = Gimp.get_images()[0]
# 出力先の画像ファイルのパスオブジェクトの生成
out = Gio.File.new_for_path("h:/out.png")
# 画像の保存(エクスポート)
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, img, out, None)

PNGファイルをインポートしレイヤーとして画像に追加

# 追加先の画像(先頭のXCF)
img = Gimp.get_images()[0]
# 読み込みたいファイル
f = Gio.File.new_for_path("H:/out.png")
# レイヤーとして読み込む
new_layer = Gimp.file_load_layer(Gimp.RunMode.NONINTERACTIVE, img, f)
# 画像に挿入(インデックス0で一番上に追加)
img.insert_layer(new_layer, None, 0)

外部コマンドをフィルターとして実行

プラグインディレクトリに保存
ファイル名:my-toline\my-toline.py

#!/usr/bin/env python3
# GIMP 3 (Python-Fu) : toLine.exeを使って線画抽出
import sys, gi, subprocess
import datetime, os, time
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp, GObject, Gio
PROC = "python-fu-toline"
def run(proc, run_mode, image, drawables, config, data):
    now = datetime.datetime.now()
    filename = now.strftime('%Y%m%d%H%M%S') + ".png"
    filename2 = now.strftime('%Y%m%d%H%M%S') + "_line.png"
    temp_dir = 'j:/temp'
    temp_in_file = os.path.join(temp_dir, filename)
    temp_out_file = os.path.join(temp_dir, filename2)

    try:
        # 出力先の画像ファイルのパスオブジェクトの生成
        out = Gio.File.new_for_path(temp_in_file)
        # 画像の保存(エクスポート)
        Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, out, None)
        time.sleep(1)
        subprocess.run(["c:/Users/karet/bin/ToLine.exe", "-i", temp_in_file, "-o", temp_out_file])
        # 読み込みたいファイル
        f = Gio.File.new_for_path(temp_out_file)
        # レイヤーとして読み込む
        new_layer = Gimp.file_load_layer(Gimp.RunMode.NONINTERACTIVE, image, f)
        # 画像に挿入(インデックス0で一番上に追加)
        image.insert_layer(new_layer, None, 0)
        Gimp.message("toline.exe を起動しました。")
        return proc.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
    except Exception as e:
        Gimp.message(f"起動失敗: {e}")
        return proc.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, None)
class RunNotepad(Gimp.PlugIn):
    def do_query_procedures(self):
        return [PROC]
    def do_create_procedure(self, name):
        if name != PROC:
            return None
        p = Gimp.ImageProcedure.new(self, name, Gimp.PDBProcType.PLUGIN, run, None)
        p.set_menu_label("Run ToLine")
        p.add_menu_path("<Image>/Filters/My")   # 画像を開いている時に表示
        p.set_documentation(
            "Launch notepad.exe via subprocess",
            "外部の toLine.exe ",
            "my-toline.py"
        )
        p.set_attribution("Your Name", "Public Domain", "2025")
        # 画像が必要なメニュー配下なので image types を指定
        p.set_image_types("*")
        # 描画対象が選べる状態で有効化(最低限の感度)
        p.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.DRAWABLE)
        return p
Gimp.main(RunNotepad.__gtype__, sys.argv)

レイヤーをトップへ移動するプラグイン

自分が必要な機能をプラグインで作るわけですが、GIMPの標準機能に既にあるものを作る場合があり、こちらの移動プラグインはその典型。
作ってしまったので残しておきます。
ファイル名:my-raise_layer_top/my-raise_layer_top.py

#!/usr/bin/env python3
# レイヤーをトップへ移動
import sys, gi, subprocess
import datetime, os, time
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp, GObject, Gio
PROC = "python-fu-raise-layer-top"
def run(proc, run_mode, image, drawables, config, data):
    # アクティブレイヤーの取得
    sel_layers = image.get_selected_layers()
    layer = sel_layers[0] if sel_layers else None
    if (layer is None):
        return proc.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, None)
    image.raise_item_to_top(layer)
    Gimp.displays_flush()
    return proc.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
class RunRaiseLayerTop(Gimp.PlugIn):
    def do_query_procedures(self):
        return [PROC]
    def do_create_procedure(self, name):
        if name != PROC:
            return None
        p = Gimp.ImageProcedure.new(self, name, Gimp.PDBProcType.PLUGIN, run, None)
        p.set_menu_label("Raise Layer Top")
        p.add_menu_path("<Image>/Filters/My")   # 画像を開いている時に表示
        p.set_documentation(
            "Raise Layer Top",
            "レイヤーをトップへ移動",
            "my-raise_layer_top.py"
        )
        p.set_attribution("Your Name", "Public Domain", "2025")
        # 画像が必要なメニュー配下なので image types を指定
        p.set_image_types("*")
        # 描画対象が選べる状態で有効化(最低限の感度)
        p.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.DRAWABLE)
        return p
Gimp.main(RunRaiseLayerTop.__gtype__, sys.argv)

レイヤーをボトムへ移動するプラグイン

ファイル名:my-lower_item_to_bottom/my-lower_item_to_bottom.py

#!/usr/bin/env python3
# レイヤーをボトムへ移動
import sys, gi, subprocess
import datetime, os, time
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp, GObject, Gio
PROC = "python-fu-lower-item-bottom"
def run(proc, run_mode, image, drawables, config, data):
    # アクティブレイヤーの取得
    sel_layers = image.get_selected_layers()
    layer = sel_layers[0] if sel_layers else None
    if (layer is None):
        return proc.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, None)
    image.lower_item_to_bottom(layer)
    Gimp.displays_flush()
    return proc.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
class RunLowerLayerBottom(Gimp.PlugIn):
    def do_query_procedures(self):
        return [PROC]
    def do_create_procedure(self, name):
        if name != PROC:
            return None
        p = Gimp.ImageProcedure.new(self, name, Gimp.PDBProcType.PLUGIN, run, None)
        p.set_menu_label("Lower Layer Bottom")
        p.add_menu_path("<Image>/Filters/My")   # 画像を開いている時に表示
        p.set_documentation(
            "Lower Layer Bottom",
            "レイヤーをボトムへ移動",
            "my-lower_item_to_bottom.py"
        )
        p.set_attribution("Your Name", "Public Domain", "2025")
        # 画像が必要なメニュー配下なので image types を指定
        p.set_image_types("*")
        # 描画対象が選べる状態で有効化(最低限の感度)
        p.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.DRAWABLE)
        return p
Gimp.main(RunLowerLayerBottom.__gtype__, sys.argv)

mask用に黒色のレイヤーを新規作成するプラグイン

白色で選択範囲を描画するため前景色を白にセット。また、半透明にすることで元レイヤーが見えるようにしています。

ファイル名:my-add_mask_layer/my-add_mask_layer.py

#!/usr/bin/env python3
# マスク用の黒色レイヤーを追加
import sys, gi, subprocess
import datetime, os, time
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp, GObject, Gio, Gegl
PROC = "python-fu-add-mask-laye"
def run(proc, run_mode, image, drawables, config, data):
    # 前景色・背景色をセット
    bk = Gegl.Color.new("#000000")
    wh = Gegl.Color.new("#FFFFFF")
    Gimp.context_set_foreground(bk)
    Gimp.context_set_background(wh)
    # 現在のレイヤーを取得
    sel_layers = image.get_selected_layers()
    layer = sel_layers[0] if sel_layers else None
    if (layer is None):
        return proc.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, None)
    image.raise_item_to_top(layer)
    # 選択中のレイヤーから新しいレイヤーを生成
    new_layer = Gimp.Layer.new_from_drawable(layer, image)
    # レイヤーの挿入
    Gimp.Image.insert_layer(image, new_layer, None, 0)
    # 前景色で塗りつぶし
    Gimp.Drawable.edit_fill(new_layer, 0)
    # レイヤー名をmaskに変更
    new_layer.set_name("mask")
    # レイヤーの不透明度を50%にセット
    Gimp.Layer.set_opacity(new_layer, 50.0)
    # 前景色・背景色をセット
    Gimp.context_set_foreground(wh)
    Gimp.context_set_background(bk)
    Gimp.displays_flush()
    return proc.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
class RunAddMaskLayer(Gimp.PlugIn):
    def do_query_procedures(self):
        return [PROC]
    def do_create_procedure(self, name):
        if name != PROC:
            return None
        p = Gimp.ImageProcedure.new(self, name, Gimp.PDBProcType.PLUGIN, run, None)
        p.set_menu_label("Add Mask Layer")
        p.add_menu_path("<Image>/Filters/My")   # 画像を開いている時に表示
        p.set_documentation(
            "Add Mask Layer",
            "マスクレイヤーの追加",
            "my-add_mask_layer.py"
        )
        p.set_attribution("Your Name", "Public Domain", "2025")
        # 画像が必要なメニュー配下なので image types を指定
        p.set_image_types("*")
        # 描画対象が選べる状態で有効化(最低限の感度)
        p.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.DRAWABLE)
        return p
Gimp.main(RunAddMaskLayer.__gtype__, sys.argv)

コメント