/* LIC 0.14 -- image filter plug-in for The Gimp program * Copyright (C) 1996 Tom Bech * * E-mail: tomb@gimp.org * You can contact the original The Gimp authors at gimp@xcf.berkeley.edu * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * In other words, you can't sue me for whatever happens while using this ;) * * Changes (post 0.10): * -> 0.11: Fixed a bug in the convolution kernels (Tom). * -> 0.12: Added Quartic's bilinear interpolation stuff (Tom). * -> 0.13 Changed some UI stuff causing trouble with the 0.60 release, added * the (GIMP) tags and changed random() calls to rand() (Tom) * -> 0.14 Ported to 0.99.11 (Tom) * * This plug-in implements the Line Integral Convolution (LIC) as described in * Cabral et al. "Imaging vector fields using line integral convolution" in the * Proceedings of ACM SIGGRAPH 93. Publ. by ACM, New York, NY, USA. p. 263-270. * Some of the code is based on code by Steinar Haugen (thanks!), the Perlin * noise function is practically ripped as is :) */ #include "config.h" #include #include #include #include #include #include #include "libgimp/stdplugins-intl.h" /************/ /* Typedefs */ /************/ #define CHECKBOUNDS(x,y) (x>=0 && y>=0 && xbpp == 4) { if (in_channels == 4) color.a = (gdouble) data[3] / 255.0; else color.a = 1.0; } else color.a = 1.0; return color; } static void poke (gint x, gint y, GimpRGB *color) { static guchar data[4]; data[0] = (guchar) (color->r * 255.0); data[1] = (guchar) (color->g * 255.0); data[2] = (guchar) (color->b * 255.0); data[3] = (guchar) (color->a * 255.0); gimp_pixel_rgn_set_pixel (&dest_region, data, x, y); } /****************************************/ /* Allocate memory for temporary images */ /****************************************/ static gint image_setup (GimpDrawable *drawable, gint interactive) { /* Get some useful info on the input drawable */ /* ========================================== */ input_drawable = drawable; output_drawable = drawable; gimp_drawable_mask_bounds (drawable->drawable_id, &border_x1, &border_y1, &border_x2, &border_y2); width = input_drawable->width; height = input_drawable->height; gimp_pixel_rgn_init (&source_region, input_drawable, 0, 0, width, height, FALSE, FALSE); maxcounter = (glong) width * (glong) height; /* Assume at least RGB */ /* =================== */ in_channels = 3; if (gimp_drawable_has_alpha (input_drawable->drawable_id)) in_channels++; if (interactive) { /* Allocate memory for temp. images */ /* ================================ */ } return TRUE; } static guchar peekmap (guchar *image, gint x, gint y) { glong index; index = (glong) x + (glong) width * (glong) y; return image[index] ; } /*************/ /* Main part */ /*************/ /***************************************************/ /* Compute the derivative in the x and y direction */ /* We use these convolution kernels: */ /* |1 0 -1| | 1 2 1| */ /* DX: |2 0 -2| DY: | 0 0 0| */ /* |1 0 -1| | -1 -2 -1| */ /* (It's a varation of the Sobel kernels, really) */ /***************************************************/ static gint gradx (guchar *image, gint x, gint y) { gint val=0; if (CHECKBOUNDS (x-1, y-1)) val = val + (gint) peekmap (image, x-1, y-1); if (CHECKBOUNDS (x+1, y-1)) val = val - (gint) peekmap (image, x+1, y-1); if (CHECKBOUNDS (x-1, y)) val = val + 2 * (gint) peekmap (image, x-1, y); if (CHECKBOUNDS (x+1, y)) val = val - 2 * (gint) peekmap (image, x+1, y); if (CHECKBOUNDS (x-1, y+1)) val = val + (gint) peekmap (image, x-1, y+1); if (CHECKBOUNDS (x+1, y+1)) val = val - (gint) peekmap (image, x+1, y+1); return val; } static gint grady (guchar *image, gint x, gint y) { gint val = 0; if (CHECKBOUNDS (x-1, y-1)) val = val + (gint) peekmap (image, x-1, y-1); if (CHECKBOUNDS (x, y-1)) val = val + 2 * (gint) peekmap (image, x, y-1); if (CHECKBOUNDS (x+1, y-1)) val = val + (gint) peekmap (image, x+1, y-1); if (CHECKBOUNDS (x-1, y+1)) val = val - (gint) peekmap (image, x-1, y+1); if (CHECKBOUNDS (x, y+1)) val = val - 2 * (gint) peekmap (image, x, y+1); if (CHECKBOUNDS (x+1, y+1)) val = val - (gint) peekmap (image, x+1, y+1); return val; } /************************************/ /* A nice 2nd order cubic spline :) */ /************************************/ static gdouble cubic (gdouble t) { gdouble at = fabs (t); return (at < 1.0) ? at * at * (2.0 * at - 3.0) + 1.0 : 0.0; } static gdouble omega (gdouble u, gdouble v, gint i, gint j) { while (i < 0) i += numx; while (j < 0) j += numy; i %= numx; j %= numy; return cubic (u) * cubic (v) * (G[i][j][0]*u + G[i][j][1]*v); } /*************************************************************/ /* The noise function (2D variant of Perlins noise function) */ /*************************************************************/ static gdouble noise (gdouble x, gdouble y) { gint i, sti = (gint) floor (x / dx); gint j, stj = (gint) floor (y / dy); gdouble sum = 0.0; /* Calculate the gdouble sum */ /* ======================== */ for (i = sti; i <= sti + 1; i++) { for (j = stj; j <= stj + 1; j++) sum += omega ((x - (gdouble) i * dx) / dx, (y - (gdouble) j * dy) / dy, i, j); } return sum; } /*************************************************/ /* Generates pseudo-random vectors with length 1 */ /*************************************************/ static void generatevectors (void) { gdouble alpha; gint i, j; GRand *gr; gr = g_rand_new(); for (i = 0; i < numx; i++) { for (j = 0; j < numy; j++) { alpha = g_rand_double_range (gr, 0, 2) * G_PI; G[i][j][0] = cos (alpha); G[i][j][1] = sin (alpha); } } g_rand_free (gr); } /* A simple triangle filter */ /* ======================== */ static gdouble filter (gdouble u) { gdouble f = 1.0 - fabs (u) / l; return (f < 0.0) ? 0.0 : f; } /******************************************************/ /* Compute the Line Integral Convolution (LIC) at x,y */ /******************************************************/ static gdouble lic_noise (gint x, gint y, gdouble vx, gdouble vy) { gdouble i = 0.0; gdouble f1 = 0.0, f2 = 0.0; gdouble u, step = 2.0 * l / isteps; gdouble xx = (gdouble) x, yy = (gdouble) y; gdouble c, s; /* Get vector at x,y */ /* ================= */ c = vx; s = vy; /* Calculate integral numerically */ /* ============================== */ f1 = filter (-l) * noise (xx + l * c , yy + l * s); for (u = -l + step; u <= l; u += step) { f2 = filter (u) * noise ( xx - u * c , yy - u * s); i += (f1 + f2) * 0.5 * step; f1 = f2; } i = (i - minv) / (maxv - minv); i = CLAMP (i, 0.0, 1.0); i = (i / 2.0) + 0.5; return i; } static void getpixel (GimpRGB *p, gdouble u, gdouble v) { register gint x1, y1, x2, y2; static GimpRGB pp[4]; x1 = (gint)u; y1 = (gint)v; if (x1 < 0) x1 = width - (-x1 % width); else x1 = x1 % width; if (y1 < 0) y1 = height - (-y1 % height); else y1 = y1 % height; x2 = (x1 + 1) % width; y2 = (y1 + 1) % height; pp[0] = peek (x1, y1); pp[1] = peek (x2, y1); pp[2] = peek (x1, y2); pp[3] = peek (x2, y2); *p = gimp_bilinear_rgb (u, v, pp); } static void lic_image (gint x, gint y, gdouble vx, gdouble vy, GimpRGB *color) { gdouble u, step = 2.0 * l / isteps; gdouble xx = (gdouble) x, yy = (gdouble) y; gdouble c, s; GimpRGB col, col1, col2, col3; /* Get vector at x,y */ /* ================= */ c = vx; s = vy; /* Calculate integral numerically */ /* ============================== */ col = black; getpixel (&col1, xx + l * c, yy + l * s); gimp_rgb_multiply (&col1, filter (-l)); for (u = -l + step; u <= l; u += step) { getpixel (&col2, xx - u * c, yy - u * s); gimp_rgb_multiply (&col2, filter (u)); col3 = col1; gimp_rgb_add (&col3, &col2); gimp_rgb_multiply (&col3, 0.5 * step); gimp_rgb_add (&col, &col3); col1 = col2; } gimp_rgb_multiply (&col, 1.0 / l); gimp_rgb_clamp (&col); *color = col; } static gdouble get_hue (const GimpRGB *col) { GimpHSL hsl; gimp_rgb_to_hsl (col, &hsl); return hsl.h; } static gdouble get_saturation (const GimpRGB *col) { GimpHSL hsl; gimp_rgb_to_hsl (col, &hsl); return hsl.s; } static gdouble get_brightness (const GimpRGB *col) { GimpHSL hsl; gimp_rgb_to_hsl (col, &hsl); return hsl.l; } static guchar* rgb_to_hsl (GimpDrawable *image, gdouble (*hsl_func)(const GimpRGB *col)) { guchar *themap, data[4]; gint w, h, x, y; GimpRGB color; gdouble val; glong maxc, index = 0; GimpPixelRgn region; w = image->width; h = image->height; maxc = (glong) w * (glong) h; gimp_pixel_rgn_init (®ion, image, 0, 0, w, h, FALSE, FALSE); themap = g_new (guchar, maxc); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { gimp_pixel_rgn_get_pixel (®ion, data, x, y); color.r = data[0]; color.g = data[1]; color.b = data[2]; val = hsl_func (&color); themap[index++] = (guchar) (val * 255.0); } } return themap; } static guchar* rgb_to_hue (GimpDrawable *image) { return rgb_to_hsl (image, get_hue); } static guchar* rgb_to_saturation (GimpDrawable *image) { return rgb_to_hsl (image, get_saturation); } static guchar* rgb_to_brightness (GimpDrawable *image) { return rgb_to_hsl (image, get_brightness); } static void compute_lic (gboolean rotate) { gint xcount, ycount; glong counter = 0; GimpRGB color; gdouble vx, vy, tmp; for (ycount = 0; ycount < height; ycount++) { for (xcount = 0; xcount < width; xcount++) { /* Get derivative at (x,y) and normalize it */ /* ============================================================== */ vx = gradx (scalarfield, xcount, ycount); vy = grady (scalarfield, xcount, ycount); /* Rotate if needed */ if (rotate) { tmp = vy; vy = -vx; vx = tmp; } tmp = sqrt (vx * vx + vy * vy); if (tmp != 0.0) { tmp = 1.0 / tmp; vx *= tmp; vy *= tmp; } /* Convolve with the LIC at (x,y) */ /* ============================== */ if (licvals.effect_convolve == 0) { color = peek (xcount, ycount); tmp = lic_noise (xcount, ycount, vx, vy); gimp_rgb_multiply (&color, tmp); } else lic_image (xcount, ycount, vx, vy, &color); poke (xcount, ycount, &color); counter++; if ((counter % width) == 0) gimp_progress_update ((gfloat) counter / (gfloat) maxcounter); } } } static void compute_image (void) { GimpDrawable *effect; gimp_pixel_rgn_init (&dest_region, output_drawable, 0, 0, width, height, TRUE, TRUE); gimp_progress_init (_("Van Gogh (LIC)...")); if (licvals.effect_convolve == 0) generatevectors (); l = (gdouble) licvals.filtlen; dx = dy = (gdouble) licvals.noisemag; minv = ((gdouble) licvals.minv) / 10.0; maxv = ((gdouble) licvals.maxv) / 10.0; isteps = (gdouble) licvals.intsteps; effect = gimp_drawable_get (licvals.effect_image_id); switch (licvals.effect_channel) { case 0: scalarfield = rgb_to_hue (effect); break; case 1: scalarfield = rgb_to_saturation (effect); break; case 2: scalarfield = rgb_to_brightness (effect); break; } compute_lic (licvals.effect_operator); g_free (scalarfield); /* Update image */ /* ============ */ gimp_drawable_flush (output_drawable); gimp_drawable_merge_shadow (output_drawable->drawable_id, TRUE); gimp_drawable_update (output_drawable->drawable_id, 0, 0, width, height); gimp_displays_flush (); } /**************************/ /* Below is only UI stuff */ /**************************/ static void ok_button_clicked (GtkWidget *widget, gpointer data) { gtk_widget_hide (GTK_WIDGET (data)); gdk_flush (); compute_image (); gtk_main_quit (); } static gint effect_image_constrain (gint32 image_id, gint32 drawable_id, gpointer data) { if (drawable_id == -1) return TRUE; return gimp_drawable_is_rgb (drawable_id); } static void effect_image_callback (gint32 id, gpointer data) { licvals.effect_image_id = id; } static void create_main_dialog (void) { GtkWidget *main_vbox; GtkWidget *vbox; GtkWidget *sep; GtkWidget *hbox; GtkWidget *frame; GtkWidget *table; GtkWidget *option_menu; GtkWidget *menu; GtkObject *scale_data; gint row; dialog = gimp_dialog_new (_("Van Gogh (LIC)"), "lic", gimp_standard_help_func, "filters/lic.html", GTK_WIN_POS_MOUSE, FALSE, TRUE, FALSE, GTK_STOCK_CANCEL, gtk_main_quit, NULL, NULL, NULL, FALSE, TRUE, GTK_STOCK_OK, ok_button_clicked, NULL, NULL, NULL, TRUE, FALSE, NULL); main_vbox = gtk_vbox_new (FALSE, 4); gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 6); gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), main_vbox); gtk_widget_show (main_vbox); hbox = gtk_hbox_new (FALSE, 6); gtk_box_pack_start (GTK_BOX (main_vbox), hbox, FALSE, FALSE, 0); gtk_widget_show (hbox); frame = gimp_radio_group_new2 (TRUE, _("Effect Channel"), G_CALLBACK (gimp_radio_button_update), &licvals.effect_channel, (gpointer) licvals.effect_channel, _("_Hue"), (gpointer) 0, NULL, _("_Saturation"), (gpointer) 1, NULL, _("_Brightness"), (gpointer) 2, NULL, NULL); gtk_container_add (GTK_CONTAINER (hbox), frame); gtk_widget_show (frame); frame = gimp_radio_group_new2 (TRUE, _("Effect Operator"), G_CALLBACK (gimp_radio_button_update), &licvals.effect_operator, (gpointer) licvals.effect_operator, _("_Derivative"), (gpointer) 0, NULL, _("_Gradient"), (gpointer) 1, NULL, NULL); gtk_container_add (GTK_CONTAINER (hbox), frame); gtk_widget_show (frame); frame = gimp_radio_group_new2 (TRUE, _("Convolve"), G_CALLBACK (gimp_radio_button_update), &licvals.effect_convolve, (gpointer) licvals.effect_convolve, _("_With White Noise"), (gpointer) 0, NULL, _("W_ith Source Image"), (gpointer) 1, NULL, NULL); gtk_container_add (GTK_CONTAINER (hbox), frame); gtk_widget_show (frame); frame = gtk_frame_new (_("Parameter Settings")); gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0); gtk_widget_show (frame); vbox = gtk_vbox_new (FALSE, 2); gtk_container_set_border_width (GTK_CONTAINER (vbox), 4); gtk_container_add (GTK_CONTAINER (frame), vbox); gtk_widget_show (vbox); /* Effect image menu */ table = gtk_table_new (1, 2, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 4); gtk_box_pack_start (GTK_BOX (vbox), table, FALSE, FALSE, 0); gtk_widget_show (table); option_menu = gtk_option_menu_new (); menu = gimp_drawable_menu_new (effect_image_constrain, effect_image_callback, NULL, licvals.effect_image_id); gtk_option_menu_set_menu (GTK_OPTION_MENU (option_menu), menu); gimp_table_attach_aligned (GTK_TABLE (table), 0, 0, _("_Effect Image:"), 1.0, 0.5, option_menu, 2, TRUE); sep = gtk_hseparator_new (); gtk_box_pack_start (GTK_BOX (vbox), sep, FALSE, FALSE, 0); gtk_widget_show (sep); table = gtk_table_new (5, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 4); gtk_table_set_row_spacings (GTK_TABLE (table), 2); gtk_box_pack_start (GTK_BOX (vbox), table, FALSE, FALSE, 0); gtk_widget_show (table); row = 0; scale_data = gimp_scale_entry_new (GTK_TABLE (table), 0, row++, _("_Filter Length:"), 0, 6, licvals.filtlen, 0, 64, 1.0, 8.0, 1, TRUE, 0, 0, NULL, NULL); g_signal_connect (scale_data, "value_changed", G_CALLBACK (gimp_double_adjustment_update), &licvals.filtlen); scale_data = gimp_scale_entry_new (GTK_TABLE (table), 0, row++, _("_Noise Magnitude:"), 0, 6, licvals.noisemag, 1, 5, 0.1, 1.0, 1, TRUE, 0, 0, NULL, NULL); g_signal_connect (scale_data, "value_changed", G_CALLBACK (gimp_double_adjustment_update), &licvals.noisemag); scale_data = gimp_scale_entry_new (GTK_TABLE (table), 0, row++, _("In_tegration Steps:"), 0, 6, licvals.intsteps, 1, 40, 1.0, 5.0, 1, TRUE, 0, 0, NULL, NULL); g_signal_connect (scale_data, "value_changed", G_CALLBACK (gimp_double_adjustment_update), &licvals.intsteps); scale_data = gimp_scale_entry_new (GTK_TABLE (table), 0, row++, _("_Minimum Value:"), 0, 6, licvals.minv, -100, 0, 1, 10, 1, TRUE, 0, 0, NULL, NULL); g_signal_connect (scale_data, "value_changed", G_CALLBACK (gimp_double_adjustment_update), &licvals.minv); scale_data = gimp_scale_entry_new (GTK_TABLE (table), 0, row++, _("M_aximum Value:"), 0, 6, licvals.maxv, 0, 100, 1, 10, 1, TRUE, 0, 0, NULL, NULL); g_signal_connect (scale_data, "value_changed", G_CALLBACK (gimp_double_adjustment_update), &licvals.maxv); gtk_widget_show (dialog); } /******************/ /* Implementation */ /******************/ static void lic_interactive (GimpDrawable *drawable); /*************************************/ /* Set parameters to standard values */ /*************************************/ static void set_default_settings (void) { licvals.filtlen = 5; licvals.noisemag = 2; licvals.intsteps = 25; licvals.minv = -25; licvals.maxv = 25; licvals.effect_channel = 2; licvals.effect_operator = 1; licvals.effect_convolve = 1; licvals.effect_image_id = 0; } static void query (void) { static GimpParamDef args[] = { { GIMP_PDB_INT32, "run_mode", "Interactive" }, { GIMP_PDB_IMAGE, "image", "Input image" }, { GIMP_PDB_DRAWABLE, "drawable", "Input drawable" } }; gimp_install_procedure ("plug_in_lic", "Creates a Van Gogh effect (Line Integral Convolution)", "No help yet", "Tom Bech & Federico Mena Quintero", "Tom Bech & Federico Mena Quintero", "Version 0.14, September 24 1997", N_("/Filters/Map/_Van Gogh (LIC)..."), "RGB", GIMP_PLUGIN, G_N_ELEMENTS (args), 0, args, NULL); } static void run (const gchar *name, gint nparams, const GimpParam *param, gint *nreturn_vals, GimpParam **return_vals) { static GimpParam values[1]; GimpDrawable *drawable; GimpRunMode run_mode; GimpPDBStatusType status = GIMP_PDB_SUCCESS; run_mode = param[0].data.d_int32; INIT_I18N (); *nreturn_vals = 1; *return_vals = values; values[0].type = GIMP_PDB_STATUS; values[0].data.d_status = status; /* Set default values */ /* ================== */ set_default_settings (); /* Possibly retrieve data */ /* ====================== */ gimp_get_data ("plug_in_lic", &licvals); /* Get the specified drawable */ /* ========================== */ drawable = gimp_drawable_get (param[2].data.d_drawable); if (status == GIMP_PDB_SUCCESS) { /* Make sure that the drawable is RGBA or RGB color */ /* ================================================ */ if (gimp_drawable_is_rgb (drawable->drawable_id)) { /* Set the tile cache size */ /* ======================= */ gimp_tile_cache_ntiles (TILE_CACHE_SIZE); switch (run_mode) { case GIMP_RUN_INTERACTIVE: lic_interactive (drawable); gimp_set_data ("plug_in_lic", &licvals, sizeof (LicValues)); break; case GIMP_RUN_WITH_LAST_VALS: image_setup (drawable, FALSE); compute_image (); break; default: break; } } else status = GIMP_PDB_EXECUTION_ERROR; } values[0].data.d_status = status; gimp_drawable_detach (drawable); } GimpPlugInInfo PLUG_IN_INFO = { NULL, /* init_proc */ NULL, /* quit_proc */ query, /* query_proc */ run, /* run_proc */ }; static void lic_interactive (GimpDrawable *drawable) { gimp_ui_init ("lic", TRUE); /* Create application window */ /* ========================= */ create_main_dialog (); /* Prepare images */ /* ============== */ image_setup (drawable, TRUE); /* Gtk main event loop */ /* =================== */ gtk_main (); gdk_flush (); } MAIN ()