1Pの私的和訳に引き続き、How to write a GIMP plug-in 2Pも和訳してみた。訳出の意図、注意は前回同様です。
前回別ページを作ると書きましたが、やっぱり日記に直接埋め込む形に、また今回はコードと画像も一緒に載せています。コードの中でコメントの無いコメント行がありますが、これはtdiary上での制約ですので無視してください。
この和訳文書はcreative commons : 帰属 - 非営利 - 同一条件許諾 2.5でライセンスされてます。
原著者: Dave Neary(mailto:bolsh@NOSPAM.gimp.org)
この章では、GIMPのプラグインインターフェイスを構築する為に基礎的な要素を示す。そこで、プラグイン中で利用できる単純だが役立つアルゴリズムを作成する。
これから実装するアルゴリズムは単純なblur(ぼかし)である。GIMPには"Filters->Blur->Blur"で初期値と共に含まれている。
アルゴリズムは非常にシンプルです。画像の各ピクセル(画素)を近隣のピクセル達との平均値で置き換える。例えば、一番単純なケースである隣接する 3x3(図1参照)ピクセルを考えてみると、この場合5で中央の値として置き換えられる、これは9つのピクセルの平均値である。
前回何の役にも立たないrun()関数を書いたが、再度以下のrun()関数のプロトタイプを見てみよう。
static void run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals);
これはフィルター処理で見ることができる(例えば画像修正をするプラグイン)。最初の3つ引数は、それぞれrun-modeであり、次はImageの識別子であり、もう一つはアクティブなDrawableを示すものである(レイヤーかマスク)。
GIMPのImageは次のものを含む構造をしている、大きなものではガイド、レイヤー、レイヤーマスク、そのほかImageと関連したデータ。 "drawable"という単語はGIMPの内部構造にはよく出てくる。"drawable"とは、われわれが取得したり、修正したりできる生のデータであるオブジェクトのことである。したがって、レイヤー、レイヤーマスク、選択領域は全て"drawable"である。
GimpDrawable を識別子(以下のコードの"drawable_id")から取得するためには、gimp_drawable_get()関数を必要とする。
GimpDrawable *gimp_drawable_get(gint32 drawable_id);
この構造体からさらにGimpPixelRgn構造体を通してdrawableデータへアクセスする。さらに、drawableデータの形式(RGB, Gray)を確認することもできる。GimpDrawableで利用可能な関数全てのリストはAPIリストの中にある。
plug-inにとってとても重要な関数が二つあり、それはgimp_drawable_mask_bounds()と gimp_pixel_rgn_init()である。一つ目の関数はdrawableデータ内で選択できる領域を与えてくれる、二つ目の関数は、データへのアクセスに使うGimpPixelRgnを初期化する。
ちゃんと初期化されたGimpPixelRgnを取得したならすぐに、いくつかの異なった方法でimageデータにアクセスできる。それはピクセル単位だったり、矩形単位だったり、行または列単位にできる。どの方法が良いかは選択したアルゴリズムに依存するだろう。加えて、GIMPはタイルキャッシュ機構を持っている、これによりロード/アンロードといったコストの高い処理を必要な時以外は行わないようにしている。
imageデータの取得、配置を行う主な関数は以下の通りである。
void gimp_pixel_rgn_get_pixel (GimpPixelRgn *pr, guchar *buf, gint x, gint y); void gimp_pixel_rgn_get_row (GimpPixelRgn *pr, guchar *buf, gint x, gint y, gint width); void gimp_pixel_rgn_get_col (GimpPixelRgn *pr, guchar *buf, gint x, gint y, gint height); void gimp_pixel_rgn_get_rect (GimpPixelRgn *pr, guchar *buf, gint x, gint y, gint width, gint height); void gimp_pixel_rgn_set_pixel (GimpPixelRgn *pr, const guchar *buf, gint x, gint y); void gimp_pixel_rgn_set_row (GimpPixelRgn *pr, const guchar *buf, gint x, gint y, gint width); void gimp_pixel_rgn_set_col (GimpPixelRgn *pr, const guchar *buf, gint x, gint y, gint height); void gimp_pixel_rgn_set_rect (GimpPixelRgn *pr, const guchar *buf, gint x, gint y, gint width, gint height);
imageデータへのアクセス方法は他にもある(しかも頻繁に使われる)。その方法はタイルキャッシュレベルのデータ管理を行うことができる、これについては後ほど詳しく述べる。
最後に、drawableデータを修正したプラグインはコアへデータを送るためにflushしなければいけない、またアプリケーションに対して表示を更新する旨を伝えなければならない。これは次の関数を利用する。
gimp_displays_flush (); gimp_drawable_detach (drawable);
いくつかのやり方の異なる手法を試すべく、blur()関数に処理を委譲させるようにする。run()関数は以下の通りである。
static void run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals)
{
static GimpParam values[1];
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
GimpRunMode run_mode;
GimpDrawable *drawable;
//
/* Setting mandatory output values */
*nreturn_vals = 1;
*return_vals = values;
//
values[0].type = GIMP_PDB_STATUS;
values[0].data.d_status = status;
//
/* Getting run_mode - we won't display a dialog if
* we are in NONINTERACTIVE mode */
run_mode = param[0].data.d_int32;
//
/* Get the specified drawable */
drawable = gimp_drawable_get (param[2].data.d_drawable);
//
gimp_progress_init ("My Blur...");
//
/* Let's time blur - don't forget to include time.h */
/* {
* time_t before = time(NULL);
*/
blur (drawable);
/* printf("Blur() took %ld seconds.\n", time(NULL) - before);
* } */
//
gimp_displays_flush ();
gimp_drawable_detach (drawable);
//
return;
}
ここで説明するべき部分は少ししか無い。gimp_progress_init()の呼び出しはプラグインの進歩計測の初期化を行う。後の処理で、 gimp_progress_update(double percent)を呼び出した場合、入力引数として与えられたパーセンテージを視覚的に表現する。run_modeを確認すれば、グラフィカルインターフェイスを表示するかどうかがわかる。run_modeがとりうる値はGIMP_RUN_INTERACTIVE, GIMP_RUN_NONINTERACTIVEまたはGIMP_RUN_WITH_LAST_VALSである。 GIMP_RUN_WITH_LAST_VALSはGIMP上か、scriptか、フィルタの最適用をを実行することを表す。
最初のバージョンではblurアルゴリズムにgimp_pixel_rgn_(get|set)_pixel()を使う。このバージョンは以下に挙げる。詳細については後述する。
gimp_drawable_mask_bounds()はフィルタ効果の計算に制限を与える、これは有効な選択範囲内の領域以外を取り除くことで制限とする。この方法で処理を制限するのは、パフォーマンス改善のために重要なことである。
gimp_pixel_rgn_init()で入力引数としてdrawableや処理範囲、GimpPixelRgnの生成物の挙動を変更する二つの真偽値を扱う。一つめの真偽値は、"set"のオペレーションがshadowタイル上で行われる必要があることを示す。これは、変更された全てのデータがまとめられる時、gimp_drawable_merge_shadow()が呼び出されるまではオリジナルのデータをそのままにしておくためである。二つ目の真偽値は、まとめるためにコアに送られる時に変更したタイルに"dirty"タグをつけることを示す。たいていの場合、データ読み込みの場合、二つの入力引数には"FALSE"と"FALSE"を使い、データ書き込みの場合には、"TRUE"と"TRUE"を使う。それ以外の組合せはめったに使われない。
static void blur(GimpDrawable *drawable)
{
gint i, j, k, channels;
gint x1, y1, x2, y2;
GimpPixelRgn rgn_in, rgn_out;
guchar output[4];
//
/* Gets upper left and lower right coordinates,
* and layers number in the image */
gimp_drawable_mask_bounds (drawable->drawable_id, &x1, &y1, &x2, &y2);
channels = gimp_drawable_bpp(drawable->drawable_id);
//
/* Initialises two PixelRgns, one to read original data,
* and the other to write output data. That second one will
* be merged at the end by the call to
* gimp_drawable_merge_shadow() */
gimp_pixel_rgn_init (&rgn_in, drawable, x1, y1, (x2 - x1), (y2 - y1),
FALSE, FALSE);
gimp_pixel_rgn_init (&rgn_out, drawable, x1, y1, (x2 - x1), (y2 - y1),
TRUE, TRUE);
//
for (i = x1; i < x2; i++)
{
for (j = y1; j < y2; j++)
{
guchar pixel[9][4];
//
/* Get nine pixels */
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[0], MAX(i-1,x1),
MAX(j-1, y1));
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[1], MAX(i-1,x1), j);
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[2], MAX(i-1,x1),
MIN(j+1,y2-1));
//
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[3], i, MAX(j-1,y1));
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[4], i, j);
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[5], i, MIN(j+1,y2-1));
//
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[6], MIN(i+1,x2-1),
MAX(j-1,y1));
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[7], MIN(i+1,x2-1), j);
gimp_pixel_rgn_get_pixel(&rgn_in, pixel[8], MIN(i+1,x2-1),
MIN(j+1,y2-1));
//
/* For each layer, compute the average of the
* nine */
for (k = 0; k < channels; k++)
{
int tmp, sum = 0;
for (tmp = 0; tmp < 9; tmp ++)
{
sum += pixel[tmp][k];
}
output[k] = sum / 9;
}
//
gimp_pixel_rgn_set_pixel(&rgn_out, output, i, j);
}
//
if (i%10 == 0)
gimp_progress_update ((gdouble) (i - x1) / (gdouble) (x2 - x1));
}
//
/* Update the modified region */
gimp_drawable_flush (drawable);
gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
gimp_drawable_update (drawable->drawable_id, x1, y1, (x2 - x1),
(y2 - y1));
}
作成した関数にはパフォーマンス上不利なバグがある。300x300の範囲上で、時間計測処理を有効にさせると、K6-2 350MHzのPC上で12分かかっている。比較のために、同じ範囲に対してガウシアンblurをかけたら3秒かかった。
作成した関数を、gimp_pixel_rgn_(get|set)_row()を使って修正すれば結果は良くなるだろう。先ほど760秒かかっていた処理が、6秒になった。コードは以下の通りである。
static void blur(GimpDrawable *drawable)
{
gint i, j, k, channels;
gint x1, y1, x2, y2;
GimpPixelRgn rgn_in, rgn_out;
guchar *row1, *row2, *row3;
guchar *outrow;
//
gimp_drawable_mask_bounds (drawable->drawable_id, &x1, &y1, &x2, &y2);
channels = gimp_drawable_bpp(drawable->drawable_id);
//
gimp_pixel_rgn_init (&rgn_in, drawable, x1, y1, (x2 - x1), (y2 - y1),
FALSE, FALSE);
gimp_pixel_rgn_init (&rgn_out, drawable, x1, y1, (x2 - x1), (y2 - y1),
TRUE, TRUE);
//
/* Initialise enough memory for row1, row2, row3, outrow */
row1 = malloc (channels * (x2 - x1) * sizeof *row1);
row2 = malloc (channels * (x2 - x1) * sizeof *row2);
row3 = malloc (channels * (x2 - x1) * sizeof *row3);
outrow = malloc (channels * (x2 - x1) * sizeof *outrow);
//
for (i = y1; i < y2; i++)
{
/* Get row i-1, i, i+1 */
gimp_pixel_rgn_get_row (&rgn_in, row1, x1, MAX(y1, i-1), (x2 - x1));
gimp_pixel_rgn_get_row (&rgn_in, row2, x1, i, (x2 - x1));
gimp_pixel_rgn_get_row (&rgn_in, row3, x1, MIN(y2-1, i+1), (x2 - x1));
//
for (j = x1; j < x2; j++)
{
/* For each layer, compute the average of the nine
* pixels */
for (k = 0; k < channels; k++)
{
int sum = 0;
sum = row1[channels * MAX((j - 1 - x1), 0) + k]
+ row1[channels * (j - x1) + k]
+ row1[channels * MIN((j + 1 - x1), x2 - x1 - 1) + k]
+ row2[channels * MAX((j - 1 - x1), 0) + k]
+ row2[channels * (j - x1) + k]
+ row2[channels * MIN((j + 1 - x1), x2 - x1 - 1) + k]
+ row3[channels * MAX((j - 1 - x1), 0) + k]
+ row3[channels * (j - x1) + k]
+ row3[channels * MIN((j + 1 - x1), x2 - x1 - 1) + k];
outrow[channels * (j - x1) + k] = sum / 9;
}
//
}
//
gimp_pixel_rgn_set_row(&rgn_out, outrow, x1, i, (x2 - x1));
//
if (i%10 == 0)
gimp_progress_update ((gdouble) (i - y1) / (gdouble) (y2 - y1));
}
//
free (row1);
free (row2);
free (row3);
free (outrow);
//
gimp_drawable_flush (drawable);
gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
gimp_drawable_update (drawable->drawable_id, x1, y1, (x2 - x1),
(y2 - y1));
}
コードも入れたら量が倍になった、しかも画像の入れ方が微妙になっちまった。これで日記を読むレスポンスに影響してたら考えよう。見にくいんじゃゴルァとかあったら教えていただけると幸いです。3P目は近いうちにやる予定です。
う、他のエントリをつっこむと日記構造の意味的におかしくなってしまうなぁ。やっぱりまとめるか...。
実はXBOXを買おうか迷っていたんですが、俄然XBOX360の購入意欲が沸いてきました。問題は普通のテレビが無いからアップスキャンコンバータでも買わないとあかんかも、D-SubかDVIで接続できんのかなぁ...>360
Linux+Apache+Tomcat+Jboss+PostgresとHP-UX+WebLogic+Oracleの比較の項が興味深かった。パフォーマンスうんぬんではなく、それぞれの挙動についてとかそういう視点で見るのかと思えておもしろい。PDFかなんかで手に入らないかなぁ...。
さりあんさんのとこのエントリで知ったけど、NTTおかしいよ!(褒め言葉)
それぞれの略称が無理くりすぎるとか、ケロちゃんの綴りが「CERBERUS」なあたりがKerberosと混同しないようにしようとしてる(やることも似てるしね)ところとかがおもしろすぎる。
sourceforge.netから持ってこれるのはkernel patchとutility toolのようだ。テスト機使ってつっこんで見ようかしらね。