以下の記事はGIMP-2.10のPython-Fuの内容になります。GIMP-3.0では利用できない可能性が高いので予めご了承ください。
フィルターとしてスクリプトを呼び出す場合、基本的に現在開いている画像ファイル(imageオブジェクト)とレイヤー(drawableオブジェクト)を引数として渡します。任意のパラメータを引数にすることも出来ますが、現在のマウスの座標などは引数にすることは出来ないようなので(筆者が知らないだけかも?)特定の領域を対象にする場合、レイヤー上の選択範囲を利用することに成ります。
ただ、GIMPに組み込まれているPythonを使う関係上、外部ライブラリに頼ることが難しく、基本的にGIMP内の機能を使うか、無い場合pythonで作成する必要があります。pythonでメモリ(ピクセル)をゴリゴリ書き換えるような処理は余り向いていないので、実用的なパフォーマンスが出ない場合があります。畳み込み演算をするような処理を試した所、その処理時間の長さに驚いた記憶があります。
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)」
表示されるコンソールにスクリプトを直接記述(コピペ)すると最終行で「…」と表示されますので、さらにエンターキーを押すとスクリプトが実行されます。
一括処理スクリプト
PowerShellから実行する場合は以下の記事を参照
画像フォーマット変換
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()
範囲選択した状態で実行すると、レイヤーが新規に追加され、選択範囲が元レイヤーから切り取られ、新しいレイヤーに貼り付けられます。
開発手順
情報に関してもGIMPのプロシージャブラウザを調べて、デバッグの情報はgimp.message()でエラーコンソールに文字列を表示するぐらいしかありません。
対話型コンソールを使うことを思いつくとスクリプト作成スピードが上がります。プロシージャブラウザの関数をコンソールで直に実行することが出来るので成功した結果をスクリプトに反映する方法が便利で良いです。
エラーコンソールの表示
メニュー「ウィンドウ(W)」→「ドッキング可能なダイアログ(D)」→「エラーコンソール(N)」
gimp.message("文字列")
で文字列がコンソールに出力されます。
現在のイメージとアクティブレイヤーの取得
# 現在のイメージを取得
image = gimp.image_list()[0]
# アクティブレイヤー
layer = image.active_layer
対話式コンソールなどでPython-Fuを試す場合、まず現在開いているイメージオブジェクトとアクティブレイヤーを取得する必要があります。gimpで用意されている関数の多くは、この2つを引数に取る場合が多いので、とりあえず抑えておきたいと思います。
メンバーの一覧を取得
dir(メンバーを調べたい変数)
ピクセル単位のアクセス
指定座標のピクセル情報を取得
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)
最後に
アプリケーションに与まれたスクリプト言語は究極のカスタマイズツールとして機能します。
自分がよく使う機能をプラグインという形で作成することでGIMPの使い勝手が向上が見込めます。
学習コストに見合ったパフォーマンスが得られるかどうかは人それぞれといった感じですが・・・
コメント
[…] Windows版GIMP-2.10でPython-Fuを試す。 | 迷惑堂本舗 […]