如何编写GIMP插件(三)
舜哥翻译,原作者 Dave Neary
源自Gimp开发者网站,(一)、(二)已经有人翻译了。
原文: https://developer.gimp.org/writing-a-plug-in/3/index.html
.
在第二部分我们已经讲了以像素pixel和行row的方式来处理图像数据。现在我们将深入学习以标题title的方式来处理,以提高插件性能。我将扩展已有算法,并用图形界面来修改算法的参数:半径。
简介
现有的简单算法: 每一图层的每个像素都以公式 (2r+1)x(2r+1)找其邻域像素,取这些邻域像素的均值来替换中心点像素。实现起来要复杂一丢丢,比如要考虑边界问题,幸好算法的雾化效果通常还不错。
以前用3X3邻域,现在该升级为可变尺寸的领域鸟。
先说哈啥子是titles.
Tile 管理
一个标题title就是一个大小为64X64的图像数据块(译者注:原来就是个Block块块,取个高大上的名字:标题,I 服了U). 通常这些图像块块根据需要以共享内存的方式一个个排队送到插件里(枪毙?),当然这个过程要尽量避免巨耗资源。
通常不用特别的缓存cache,每个title块块召之即来、用了就丢free;但我们也可在函数调用时告诉插件缓存一下吧,免得来来回回太亏牲口了。
gimp_tile_cache_ntiles (gulong ntiles);
在本教程(二)中我们没用任何缓冲直接调用函数gimp_pixel_rgn_get_row() 和 gimp_pixel_rgn_set_row() 。
缓冲数量的计算: 图层宽度整除块块宽度+1,考虑到影子内存,加倍。公式代码如下:
gimp_tile_cache_ntiles (2 * (drawable->width /
gimp_tile_width () + 1));
用了缓冲cache,应该变快了,以前300X300的图像3秒干完,而2000X1500的要142秒(译者注: 这是10多年前的烂机器干的)。
现在仅加了以上那句缓冲,11秒就干完,有部分时间损失是边界处理。用4倍缓冲代替2倍可以降到10秒以下,但缓冲越大,硬盘访问越多,边际收益减小,划不来了。
算法归一化
修改算法,加入一个参数:半径r。r=3时,邻域大小为7X7,而旧算法的邻域3X3,r仅仅为1。
算法描述:
分配 2r+1个块块行
初始化这些行,小心处理边界
for each tile row
for each 像素pixel 每个 tile row
算邻域像素均值,也要小心边界
继续取下一行循环
这是一个复杂度为O(r²) 的算法(幸好 r 不会太大)。
算法如下:
复杂的都在函数 process_row()里, init_mem()/shuffle()用来简化核心函数blur()的。
static void blur (GimpDrawable *drawable);
static void init_mem (guchar ***row,
guchar **outrow,
gint num_bytes);
static void process_row (guchar **row,
guchar *outrow,
gint x1,
gint y1,
gint width,
gint height,
gint channels,
gint i);
static void shuffle (GimpPixelRgn *rgn_in,
guchar **row,
gint x1,
gint y1,
gint width,
gint height,
gint ypos);
/* The radius is still a constant, we'll change that when the
* graphical interface will be built. */
static gint radius = 3;
...
static void
blur (GimpDrawable *drawable)
{
gint i, ii, channels;
gint x1, y1, x2, y2;
GimpPixelRgn rgn_in, rgn_out;
guchar **row;
guchar *outrow;
gint width, height;
gimp_progress_init ("My Blur...");
/* Gets upper left and lower right coordinates,
* and layers number in the image */
gimp_drawable_mask_bounds (drawable->drawable_id,
&x1, &y1,
&x2, &y2);
width = x2 - x1;
height = y2 - y1;
channels = gimp_drawable_bpp (drawable->drawable_id);
/* Allocate a big enough tile cache */
gimp_tile_cache_ntiles (2 * (drawable->width /
gimp_tile_width () + 1));
/* 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,
width, height,
FALSE, FALSE);
gimp_pixel_rgn_init (&rgn_out,
drawable,
x1, y1,
width, height,
TRUE, TRUE);
/* Allocate memory for input and output tile rows */
init_mem (&row, &outrow, width * channels);
for (ii = -radius; ii <= radius; ii++)
{
gimp_pixel_rgn_get_row (&rgn_in,
row[radius + ii],
x1, y1 + CLAMP (ii, 0, height - 1),
width);
}
for (i = 0; i < height; i++)
{
/* To be done for each tile row */
process_row (row,
outrow,
x1, y1,
width, height,
channels,
i);
gimp_pixel_rgn_set_row (&rgn_out,
outrow,
x1, i + y1,
width);
/* shift tile rows to insert the new one at the end */
shuffle (&rgn_in,
row,
x1, y1,
width, height,
i);
if (i % 10 == 0)
gimp_progress_update ((gdouble) i / (gdouble) height);
}
/* We could also put that in a separate function but it's
* rather simple */
for (ii = 0; ii < 2 * radius + 1; ii++)
g_free (row[ii]);
g_free (row);
g_free (outrow);
/* Update the modified region */
gimp_drawable_flush (drawable);
gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
gimp_drawable_update (drawable->drawable_id,
x1, y1,
width, height);
}
static void
init_mem (guchar ***row,
guchar **outrow,
gint num_bytes)
{
gint i;
/* Allocate enough memory for row and outrow */
*row = g_new (char *, (2 * radius + 1));
for (i = -radius; i <= radius; i++)
(*row)[i + radius] = g_new (guchar, num_bytes);
*outrow = g_new (guchar, num_bytes);
}
static void
process_row (guchar **row,
guchar *outrow,
gint x1,
gint y1,
gint width,
gint height,
gint channels,
gint i)
{
gint j;
for (j = 0; j < width; j++)
{
gint k, ii, jj;
gint left = (j - radius),
right = (j + radius);
/* For each layer, compute the average of the
* (2r+1)x(2r+1) pixels */
for (k = 0; k < channels; k++)
{
gint sum = 0;
for (ii = 0; ii < 2 * radius + 1; ii++)
for (jj = left; jj <= right; jj++)
sum += row[ii][channels * CLAMP (jj, 0, width - 1) + k];
outrow[channels * j + k] =
sum / (4 * radius * radius + 4 * radius + 1);
}
}
}
static void
shuffle (GimpPixelRgn *rgn_in,
guchar **row,
gint x1,
gint y1,
gint width,
gint height,
gint ypos)
{
gint i;
guchar *tmp_row;
/* Get tile row (i + radius + 1) into row[0] */
gimp_pixel_rgn_get_row (rgn_in,
row[0],
x1, MIN (ypos + radius + y1, y1 + height - 1),
width);
/* Permute row[i] with row[i-1] and row[0] with row[2r] */
tmp_row = row[0];
for (i = 1; i < 2 * radius + 1; i++)
row[i - 1] = row[i];
row[2 * radius] = tmp_row;
}
增加图形界面接口和保存参数
为了让用户修改半径,或用脚本修改半径,我们需要回头处理插件的 run()函数。
按惯例要做个结构体,哪怕只有一个参数.
typedef struct
{
gint radius;
} MyBlurVals;
/* Set up default values for options */
static MyBlurVals bvals =
{
3 /* radius */
};
下一步就是修改 run()函数,确定执行模式: 交互模式和重复上一次模式。用函数 gimp_get_data() 取最后的参数值。
最后,交互模式下,加几行代码创建图形界面。
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);
switch (run_mode)
{
case GIMP_RUN_INTERACTIVE:
/* Get options last values if needed */
gimp_get_data ("plug-in-myblur", &bvals);
/* Display the dialog */
if (! blur_dialog (drawable))
return;
break;
case GIMP_RUN_NONINTERACTIVE:
if (nparams != 4)
status = GIMP_PDB_CALLING_ERROR;
if (status == GIMP_PDB_SUCCESS)
bvals.radius = param[3].data.d_int32;
break;
case GIMP_RUN_WITH_LAST_VALS:
/* Get options last values if needed */
gimp_get_data ("plug-in-myblur", &bvals);
break;
default:
break;
}
blur (drawable);
gimp_displays_flush ();
gimp_drawable_detach (drawable);
/* Finally, set options in the core */
if (run_mode == GIMP_RUN_INTERACTIVE)
gimp_set_data ("plug-in-myblur", &bvals, sizeof (MyBlurVals));
return;
}
图形界面
我不如其他地方清楚 GTK+图形编程在此幕后干了啥子,好在此处简单,仅需GIMP提供的如对话框、按钮之类的工具。
为show其简,我将在对话框中增加参数效果实时预览,牛逼吧?
最终GTK的控件树如下(用Glade画的):
GIMP 2.2, 提供了许多窗口 widgets 方便我们绑定参数,包括预览。复杂的自己查手册,我们先看简单的对话框:
对话框代码如下:
static gboolean
blur_dialog (GimpDrawable *drawable)
{
GtkWidget *dialog;
GtkWidget *main_vbox;
GtkWidget *main_hbox;
GtkWidget *frame;
GtkWidget *radius_label;
GtkWidget *alignment;
GtkWidget *spinbutton;
GtkObject *spinbutton_adj;
GtkWidget *frame_label;
gboolean run;
gimp_ui_init ("myblur", FALSE);
dialog = gimp_dialog_new ("My blur", "myblur",
NULL, 0,
gimp_standard_help_func, "plug-in-myblur",
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_OK, GTK_RESPONSE_OK,
NULL);
main_vbox = gtk_vbox_new (FALSE, 6);
gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), main_vbox);
gtk_widget_show (main_vbox);
frame = gtk_frame_new (NULL);
gtk_widget_show (frame);
gtk_box_pack_start (GTK_BOX (main_vbox), frame, TRUE, TRUE, 0);
gtk_container_set_border_width (GTK_CONTAINER (frame), 6);
alignment = gtk_alignment_new (0.5, 0.5, 1, 1);
gtk_widget_show (alignment);
gtk_container_add (GTK_CONTAINER (frame), alignment);
gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 6, 6, 6);
main_hbox = gtk_hbox_new (FALSE, 0);
gtk_widget_show (main_hbox);
gtk_container_add (GTK_CONTAINER (alignment), main_hbox);
radius_label = gtk_label_new_with_mnemonic ("_Radius:");
gtk_widget_show (radius_label);
gtk_box_pack_start (GTK_BOX (main_hbox), radius_label, FALSE, FALSE, 6);
gtk_label_set_justify (GTK_LABEL (radius_label), GTK_JUSTIFY_RIGHT);
spinbutton_adj = gtk_adjustment_new (3, 1, 16, 1, 5, 5);
spinbutton = gtk_spin_button_new (GTK_ADJUSTMENT (spinbutton_adj), 1, 0);
gtk_widget_show (spinbutton);
gtk_box_pack_start (GTK_BOX (main_hbox), spinbutton, FALSE, FALSE, 6);
gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spinbutton), TRUE);
frame_label = gtk_label_new ("Modify radius");
gtk_widget_show (frame_label);
gtk_frame_set_label_widget (GTK_FRAME (frame), frame_label);
gtk_label_set_use_markup (GTK_LABEL (frame_label), TRUE);
g_signal_connect (spinbutton_adj, "value_changed",
G_CALLBACK (gimp_int_adjustment_update),
&bvals.radius);
gtk_widget_show (dialog);
run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);
gtk_widget_destroy (dialog);
return run;
}
增加预览
预览太简单了,第一步用函数gimp_drawable_preview_new()创建一个窗口, 再附一个独立的信号signgl给它,由此达成调用 blur()来更新预览。增加第二个参数MyBlurVals 来记住预览的活动状态。
在blur()里加个preview参数,不为NULL就预览,因此 run() 函数内就设为NULL。为了限定预览范围,用函数gimp_preview_get_position() 、 gimp_preview_get_size(), 只show要预览的部分图像。
还要加点代码,预览时不更新进度条,初始化GimpPixelRgn时不需要返回块块给内核。
最后,显示和刷新调用函数 gimp_drawable_preview_draw_region() ,用对话框来看实时效果。另外,感谢GIMP,我们的插件功能早已提供。
最终版的两个函数如下:
static void
blur (GimpDrawable *drawable,
GimpPreview *preview)
{
gint i, ii, channels;
gint x1, y1, x2, y2;
GimpPixelRgn rgn_in, rgn_out;
guchar **row;
guchar *outrow;
gint width, height;
if (!preview)
gimp_progress_init ("My Blur...");
/* Gets upper left and lower right coordinates,
* and layers number in the image */
if (preview)
{
gimp_preview_get_position (preview, &x1, &y1);
gimp_preview_get_size (preview, &width, &height);
x2 = x1 + width;
y2 = y1 + height;
}
else
{
gimp_drawable_mask_bounds (drawable->drawable_id,
&x1, &y1,
&x2, &y2);
width = x2 - x1;
height = y2 - y1;
}
channels = gimp_drawable_bpp (drawable->drawable_id);
/* Allocate a big enough tile cache */
gimp_tile_cache_ntiles (2 * (drawable->width /
gimp_tile_width () + 1));
/* 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,
width, height,
FALSE, FALSE);
gimp_pixel_rgn_init (&rgn_out,
drawable,
x1, y1,
width, height,
preview == NULL, TRUE);
/* Allocate memory for input and output tile rows */
init_mem (&row, &outrow, width * channels);
for (ii = -bvals.radius; ii <= bvals.radius; ii++)
{
gimp_pixel_rgn_get_row (&rgn_in,
row[bvals.radius + ii],
x1, y1 + CLAMP (ii, 0, height - 1),
width);
}
for (i = 0; i < height; i++)
{
/* To be done for each tile row */
process_row (row,
outrow,
x1, y1,
width, height,
channels,
i);
gimp_pixel_rgn_set_row (&rgn_out,
outrow,
x1, i + y1,
width);
/* shift tile rows to insert the new one at the end */
shuffle (&rgn_in,
row,
x1, y1,
width, height,
i);
if (i % 10 == 0 && !preview)
gimp_progress_update ((gdouble) i / (gdouble) height);
}
for (ii = 0; ii < 2 * bvals.radius + 1; ii++)
g_free (row[ii]);
g_free (row);
g_free (outrow);
/* Update the modified region */
if (preview)
{
gimp_drawable_preview_draw_region (GIMP_DRAWABLE_PREVIEW (preview),
&rgn_out);
}
else
{
gimp_drawable_flush (drawable);
gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
gimp_drawable_update (drawable->drawable_id,
x1, y1,
width, height);
}
}
static gboolean
blur_dialog (GimpDrawable *drawable)
{
GtkWidget *dialog;
GtkWidget *main_vbox;
GtkWidget *main_hbox;
GtkWidget *preview;
GtkWidget *frame;
GtkWidget *radius_label;
GtkWidget *alignment;
GtkWidget *spinbutton;
GtkObject *spinbutton_adj;
GtkWidget *frame_label;
gboolean run;
gimp_ui_init ("myblur", FALSE);
dialog = gimp_dialog_new ("My blur", "myblur",
NULL, 0,
gimp_standard_help_func, "plug-in-myblur",
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_OK, GTK_RESPONSE_OK,
NULL);
main_vbox = gtk_vbox_new (FALSE, 6);
gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), main_vbox);
gtk_widget_show (main_vbox);
preview = gimp_drawable_preview_new (drawable, &bvals.preview);
gtk_box_pack_start (GTK_BOX (main_vbox), preview, TRUE, TRUE, 0);
gtk_widget_show (preview);
frame = gimp_frame_new ("Blur radius");
gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0);
gtk_widget_show (frame);
alignment = gtk_alignment_new (0.5, 0.5, 1, 1);
gtk_widget_show (alignment);
gtk_container_add (GTK_CONTAINER (frame), alignment);
gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 6, 6, 6);
main_hbox = gtk_hbox_new (FALSE, 12);
gtk_container_set_border_width (GTK_CONTAINER (main_hbox), 12);
gtk_widget_show (main_hbox);
gtk_container_add (GTK_CONTAINER (alignment), main_hbox);
radius_label = gtk_label_new_with_mnemonic ("_Radius:");
gtk_widget_show (radius_label);
gtk_box_pack_start (GTK_BOX (main_hbox), radius_label, FALSE, FALSE, 6);
gtk_label_set_justify (GTK_LABEL (radius_label), GTK_JUSTIFY_RIGHT);
spinbutton = gimp_spin_button_new (&spinbutton_adj, bvals.radius,
1, 32, 1, 1, 1, 5, 0);
gtk_box_pack_start (GTK_BOX (main_hbox), spinbutton, FALSE, FALSE, 0);
gtk_widget_show (spinbutton);
g_signal_connect_swapped (preview, "invalidated",
G_CALLBACK (blur),
drawable);
g_signal_connect_swapped (spinbutton_adj, "value_changed",
G_CALLBACK (gimp_preview_invalidate),
preview);
blur (drawable, GIMP_PREVIEW (preview));
g_signal_connect (spinbutton_adj, "value_changed",
G_CALLBACK (gimp_int_adjustment_update),
&bvals.radius);
gtk_widget_show (dialog);
run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);
gtk_widget_destroy (dialog);
return run;
}
看一下完整代码更舒坦哦!
总结
这几篇文章,我们从几个角度学了GIMP插件的基本概念;用一个简单的算法搞花了图像,顺便说了用缓冲减少了性能损失;最后归一化算法,增加了参数,还用了GIMP提供的图形界面控件整了个漂亮的接口界面。
致谢
感谢我的婆娘,感谢牛逼的大胃蛋定(David Odin)对本文的帮助.
版权声明
不准抄袭老子的内容和代码拿去骗科研经费!!!
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License.