gimp/app/tools/gimpiscissorstool.c

1774 lines
45 KiB
C

/* The GIMP -- an image manipulation program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* 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.
*/
/* This tool is based on a paper from SIGGRAPH '95:
* "Intelligent Scissors for Image Composition", Eric N. Mortensen and
* William A. Barrett, Brigham Young University.
*
* thanks to Professor D. Forsyth for prompting us to implement this tool. */
/* The history of this implementation is lonog and varied. It was
* orignally done by Spencer and Peter, and worked fine in the 0.54
* (motif only) release of the gimp. Later revisions (0.99.something
* until about 1.1.4) completely changed the algorithm used, until it
* bore little resemblance to the one described in the paper above.
* The 0.54 version of the algorithm was then forwards ported to 1.1.4
* by Austin Donnelly. */
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "appenv.h"
#include "draw_core.h"
#include "channel_pvt.h"
#include "drawable.h"
#include "errors.h"
#include "gdisplay.h"
#include "gimage_mask.h"
#include "interface.h"
#include "iscissors.h"
#include "edit_selection.h"
#include "paint_funcs.h"
#include "rect_select.h"
#include "selection_options.h"
#include "temp_buf.h"
#include "tools.h"
#include "bezier_selectP.h"
#include "scan_convert.h"
#include "libgimp/gimpintl.h"
#include "libgimp/gimpmath.h"
#ifdef DEBUG
#define TRC(x) printf x
#define D(x) x
#else
#define TRC(x)
#define D(x)
#endif
/* local structures */
typedef struct _ICurve ICurve;
struct _ICurve
{
int x1, y1;
int x2, y2;
GPtrArray *points;
};
/* The possible states... */
typedef enum {
NO_ACTION,
SEED_PLACEMENT,
SEED_ADJUSTMENT,
WAITING
} Iscissors_state;
/* The possible drawing states... */
typedef enum {
DRAW_NOTHING = 0x0,
DRAW_CURRENT_SEED = 0x1,
DRAW_CURVE = 0x2,
DRAW_ACTIVE_CURVE = 0x4
} Iscissors_draw;
#define DRAW_ALL (DRAW_CURRENT_SEED | DRAW_CURVE)
typedef struct _iscissors Iscissors;
struct _iscissors
{
DrawCore * core; /* Core select object */
int x, y; /* upper left hand coordinate */
int ix, iy; /* initial coordinates */
int nx, ny; /* new coordinates */
TempBuf * dp_buf; /* dynamic programming buffer */
ICurve * curve1; /* 1st curve connected to current point */
ICurve * curve2; /* 2nd curve connected to current point */
GSList * curves; /* the list of curves */
gboolean first_point; /* is this the first point? */
gboolean connected; /* is the region closed? */
Iscissors_state state; /* state of iscissors */
Iscissors_draw draw; /* items to draw on a draw request */
/* XXX might be useful */
Channel * mask; /* selection mask */
TileManager * gradient_map; /* lazily filled gradient map */
};
typedef struct _IScissorsOptions IScissorsOptions;
struct _IScissorsOptions
{
SelectionOptions selection_options;
};
/**********************************************/
/* Intelligent scissors selection apparatus */
/* Other defines... */
#define MAX_GRADIENT 179.606 /* == sqrt(127^2 + 127^2) */
#define GRADIENT_SEARCH 32 /* how far to look when snapping to an edge */
#define TARGET_HEIGHT 25
#define TARGET_WIDTH 25
#define POINT_WIDTH 8 /* size (in pixels) of seed handles */
#define POINT_HALFWIDTH (POINT_WIDTH / 2)
#define EXTEND_BY 0.2 /* proportion to expand cost map by */
#define FIXED 5 /* additional fixed size to expand cost map */
#define MIN_GRADIENT 63 /* gradients < this are directionless */
#define MAX_POINTS 2048
#ifdef USE_LAPLACIAN
# define COST_WIDTH 3 /* number of bytes for each pixel in cost map */
#else
# define COST_WIDTH 2 /* number of bytes for each pixel in cost map */
#endif
#define BLOCK_WIDTH 64
#define BLOCK_HEIGHT 64
#define CONV_WIDTH (BLOCK_WIDTH + 2)
#define CONV_HEIGHT (BLOCK_HEIGHT + 2)
/* weight to give between laplacian (_Z), gradient (_G) and direction (_D) */
#ifdef USE_LAPLACIAN
# define OMEGA_Z 0.16
# define OMEGA_D 0.42
# define OMEGA_G 0.42
#else
# define OMEGA_D 0.2
# define OMEGA_G 0.8
#endif
/* sentinel to mark seed point in ?cost? map */
#define SEED_POINT 9
/* Functional defines */
#define PIXEL_COST(x) (x >> 8)
#define PIXEL_DIR(x) (x & 0x000000ff)
/* static variables */
/* where to move on a given link direction */
static int move [8][2] =
{
{ 1, 0 },
{ 0, 1 },
{ -1, 1 },
{ 1, 1 },
{ -1, 0 },
{ 0, -1 },
{ 1, -1 },
{ -1, -1 },
};
/* IE:
* '---+---+---`
* | 7 | 5 | 6 |
* +---+---+---+
* | 4 | | 0 |
* +---+---+---+
* | 2 | 1 | 3 |
* `---+---+---'
*/
/* points for drawing curves */
static GdkPoint curve_points [MAX_POINTS];
/* temporary convolution buffers -- */
D(static unsigned int sent0 = 0xd0d0d0d0);
static unsigned char maxgrad_conv0 [TILE_WIDTH * TILE_HEIGHT * 4] = "";
D(static unsigned int sent1 = 0xd1d1d1d1);
static unsigned char maxgrad_conv1 [TILE_WIDTH * TILE_HEIGHT * 4] = "";
D(static unsigned int sent2 = 0xd2d2d2d2);
static unsigned char maxgrad_conv2 [TILE_WIDTH * TILE_HEIGHT * 4] = "";
D(static unsigned int sent3 = 0xd3d3d3d3);
static int horz_deriv [9] =
{
1, 0, -1,
2, 0, -2,
1, 0, -1,
};
static int vert_deriv [9] =
{
1, 2, 1,
0, 0, 0,
-1, -2, -1,
};
#ifdef USE_LAPLACIAN
static int laplacian [9] =
{
-1, -1, -1,
-1, 8, -1,
-1, -1, -1,
};
#endif
static int blur_32 [9] =
{
1, 1, 1,
1, 24, 1,
1, 1, 1,
};
static float distance_weights [GRADIENT_SEARCH * GRADIENT_SEARCH];
static int diagonal_weight [256];
static int direction_value [256][4];
static gboolean initialized = FALSE;
static Tile *cur_tile = NULL;
/***********************************************************************/
/* static variables */
static IScissorsOptions *iscissors_options = NULL;
/***********************************************************************/
/* Local function prototypes */
static void iscissors_button_press (Tool *, GdkEventButton *, gpointer);
static void iscissors_button_release (Tool *, GdkEventButton *, gpointer);
static void iscissors_motion (Tool *, GdkEventMotion *, gpointer);
static void iscissors_control (Tool *, ToolAction, gpointer);
static void iscissors_reset (Iscissors *);
static void iscissors_draw (Tool *);
static TileManager *gradient_map_new (GImage *gimage);
static void find_optimal_path (TileManager *, TempBuf *, int, int,
int, int, int, int);
static void find_max_gradient (Iscissors *, GImage *, int *, int *);
static void calculate_curve (Tool *, ICurve *);
static void iscissors_draw_curve (GDisplay *, Iscissors *, ICurve *);
static void iscissors_free_icurves (GSList *);
static void iscissors_free_buffers (Iscissors *);
static int clicked_on_vertex (Tool *);
static int clicked_on_curve (Tool *);
static void precalculate_arrays (void);
static GPtrArray *plot_pixels (Iscissors *, TempBuf *,
int, int, int, int, int, int);
static void
iscissors_options_reset (void)
{
IScissorsOptions *options = iscissors_options;
selection_options_reset ((SelectionOptions *) options);
}
static IScissorsOptions *
iscissors_options_new (void)
{
IScissorsOptions *options;
/* the new intelligent scissors tool options structure */
options = g_new (IScissorsOptions, 1);
selection_options_init ((SelectionOptions *) options,
ISCISSORS,
iscissors_options_reset);
return options;
}
Tool *
tools_new_iscissors (void)
{
Tool * tool;
Iscissors * private;
if (!iscissors_options)
{
iscissors_options = iscissors_options_new ();
tools_register (ISCISSORS, (ToolOptions *) iscissors_options);
}
tool = tools_new_tool (ISCISSORS);
private = g_new (Iscissors, 1);
private->core = draw_core_new (iscissors_draw);
private->curves = NULL;
private->dp_buf = NULL;
private->state = NO_ACTION;
private->mask = NULL;
private->gradient_map = NULL;
tool->auto_snap_to = FALSE; /* Don't snap to guides */
tool->private = (void *) private;
tool->button_press_func = iscissors_button_press;
tool->button_release_func = iscissors_button_release;
tool->motion_func = iscissors_motion;
tool->cursor_update_func = rect_select_cursor_update;
tool->control_func = iscissors_control;
iscissors_reset (private);
return tool;
}
void
tools_free_iscissors (Tool *tool)
{
Iscissors * iscissors;
iscissors = (Iscissors *) tool->private;
TRC (("tools_free_iscissors\n"));
/* XXX? Undraw curve */
/*iscissors->draw = DRAW_CURVE;*/
if (tool->state == ACTIVE)
draw_core_stop (iscissors->core, tool);
draw_core_free (iscissors->core);
iscissors_reset (iscissors);
g_free (iscissors);
}
/* Local functions */
static void
iscissors_button_press (Tool *tool,
GdkEventButton *bevent,
gpointer gdisp_ptr)
{
GDisplay *gdisp;
GimpDrawable *drawable;
Iscissors *iscissors;
int grab_pointer = 0;
int replace, op;
gdisp = (GDisplay *) gdisp_ptr;
iscissors = (Iscissors *) tool->private;
drawable = gimage_active_drawable (gdisp->gimage);
gdisplay_untransform_coords (gdisp, bevent->x, bevent->y,
&iscissors->x, &iscissors->y, FALSE, TRUE);
/* If the tool was being used in another image...reset it */
if (tool->state == ACTIVE && gdisp_ptr != tool->gdisp_ptr)
{
/*iscissors->draw = DRAW_CURVE; XXX? */
draw_core_stop (iscissors->core, tool);
iscissors_reset (iscissors);
}
tool->state = ACTIVE;
tool->gdisp_ptr = gdisp_ptr;
switch (iscissors->state)
{
case NO_ACTION :
#if 0
/* XXX what's this supposed to do? */
if (!(bevent->state & GDK_SHIFT_MASK) &&
!(bevent->state & GDK_CONTROL_MASK))
if (selection_point_inside (gdisp->select, gdisp_ptr, bevent->x, bevent->y))
{
init_edit_selection (tool, gdisp->select, gdisp_ptr, bevent->x, bevent->y);
return;
}
#endif
iscissors->state = SEED_PLACEMENT;
iscissors->draw = DRAW_CURRENT_SEED;
grab_pointer = 1;
if (! (bevent->state & GDK_SHIFT_MASK))
find_max_gradient (iscissors, gdisp->gimage,
&iscissors->x, &iscissors->y);
iscissors->x = BOUNDS (iscissors->x, 0, gdisp->gimage->width - 1);
iscissors->y = BOUNDS (iscissors->y, 0, gdisp->gimage->height - 1);
iscissors->ix = iscissors->x;
iscissors->iy = iscissors->y;
/* Initialize the selection core only on starting the tool */
draw_core_start (iscissors->core, gdisp->canvas->window, tool);
break;
default :
/* Check if the mouse click occured on a vertex or the curve itself */
if (clicked_on_vertex (tool))
{
iscissors->nx = iscissors->x;
iscissors->ny = iscissors->y;
iscissors->state = SEED_ADJUSTMENT;
iscissors->draw = DRAW_ACTIVE_CURVE;
draw_core_resume (iscissors->core, tool);
grab_pointer = 1;
}
/* If the iscissors is connected, check if the click was inside */
else if (iscissors->connected && iscissors->mask &&
channel_value (iscissors->mask, iscissors->x, iscissors->y))
{
/* Undraw the curve */
tool->state = INACTIVE;
iscissors->draw = DRAW_CURVE;
draw_core_stop (iscissors->core, tool);
replace = 0;
if (bevent->state & GDK_SHIFT_MASK)
op = ADD;
else if (bevent->state & GDK_CONTROL_MASK)
op = SUB;
else
{
op = ADD;
replace = 1;
}
if (replace)
gimage_mask_clear (gdisp->gimage);
else
gimage_mask_undo (gdisp->gimage);
if (((SelectionOptions *) iscissors_options)->feather)
channel_feather (iscissors->mask,
gimage_get_mask (gdisp->gimage),
((SelectionOptions *) iscissors_options)->feather_radius,
((SelectionOptions *) iscissors_options)->feather_radius,
op, 0, 0);
else
channel_combine_mask (gimage_get_mask (gdisp->gimage),
iscissors->mask, op, 0, 0);
iscissors_reset (iscissors);
selection_start (gdisp->select, TRUE);
}
/* if we're not connected, we're adding a new point */
else if (!iscissors->connected)
{
iscissors->state = SEED_PLACEMENT;
iscissors->draw = DRAW_CURRENT_SEED;
grab_pointer = 1;
draw_core_resume (iscissors->core, tool);
}
break;
}
if (grab_pointer)
gdk_pointer_grab (gdisp->canvas->window, FALSE,
GDK_POINTER_MOTION_HINT_MASK |
GDK_BUTTON1_MOTION_MASK |
GDK_BUTTON_RELEASE_MASK,
NULL, NULL, bevent->time);
}
static void
iscissors_convert (Iscissors *iscissors, gpointer gdisp_ptr)
{
GDisplay *gdisp = (GDisplay *) gdisp_ptr;
ScanConverter *sc;
ScanConvertPoint *pts;
guint npts;
GSList *list;
ICurve *icurve;
guint packed;
int i;
int index;
sc = scan_converter_new (gdisp->gimage->width, gdisp->gimage->height, 1);
/* go over the curves in reverse order, adding the points we have */
list = iscissors->curves;
index = g_slist_length (list);
while (index)
{
index--;
icurve = (ICurve *) g_slist_nth_data (list, index);
npts = icurve->points->len;
pts = g_new (ScanConvertPoint, npts);
for (i=0; i < npts; i ++)
{
packed = GPOINTER_TO_INT (g_ptr_array_index (icurve->points, i));
pts[i].x = packed & 0x0000ffff;
pts[i].y = packed >> 16;
}
scan_converter_add_points (sc, npts, pts);
g_free (pts);
}
if (iscissors->mask)
channel_delete (iscissors->mask);
iscissors->mask = scan_converter_to_channel (sc, gdisp->gimage);
scan_converter_free (sc);
channel_invalidate_bounds (iscissors->mask);
}
static void
iscissors_button_release (Tool *tool,
GdkEventButton *bevent,
gpointer gdisp_ptr)
{
Iscissors *iscissors;
GDisplay *gdisp;
ICurve *curve;
gdisp = (GDisplay *) gdisp_ptr;
iscissors = (Iscissors *) tool->private;
TRC (("iscissors_button_release\n"));
/* Make sure X didn't skip the button release event -- as it's known
* to do */
if (iscissors->state == WAITING)
return;
gdk_pointer_ungrab (bevent->time);
gdk_flush (); /* XXX why the flush? */
/* Undraw everything */
switch (iscissors->state)
{
case SEED_PLACEMENT :
iscissors->draw = DRAW_CURVE | DRAW_CURRENT_SEED;
break;
case SEED_ADJUSTMENT :
iscissors->draw = DRAW_CURVE | DRAW_ACTIVE_CURVE;
break;
default:
break;
}
draw_core_stop (iscissors->core, tool);
/* First take care of the case where the user "cancels" the action */
if (! (bevent->state & GDK_BUTTON3_MASK))
{
/* Progress to the next stage of intelligent selection */
switch (iscissors->state)
{
case SEED_PLACEMENT :
/* Add a new icurve */
if (!iscissors->first_point)
{
/* Determine if we're connecting to the first point */
if (iscissors->curves)
{
curve = (ICurve *) iscissors->curves->data;
if (abs (iscissors->x - curve->x1) < POINT_HALFWIDTH &&
abs (iscissors->y - curve->y1) < POINT_HALFWIDTH)
{
iscissors->x = curve->x1;
iscissors->y = curve->y1;
iscissors->connected = TRUE;
}
}
/* Create the new curve segment */
if (iscissors->ix != iscissors->x ||
iscissors->iy != iscissors->y)
{
curve = g_malloc (sizeof (ICurve));
curve->x1 = iscissors->ix;
curve->y1 = iscissors->iy;
iscissors->ix = curve->x2 = iscissors->x;
iscissors->iy = curve->y2 = iscissors->y;
curve->points = NULL;
TRC (("create new curve segment\n"));
iscissors->curves = g_slist_append (iscissors->curves,
(void *) curve);
TRC (("calculate curve\n"));
calculate_curve (tool, curve);
}
}
else /* this was our first point */
{
iscissors->first_point = FALSE;
}
break;
case SEED_ADJUSTMENT :
/* recalculate both curves */
if (iscissors->curve1)
{
iscissors->curve1->x1 = iscissors->nx;
iscissors->curve1->y1 = iscissors->ny;
calculate_curve (tool, iscissors->curve1);
}
if (iscissors->curve2)
{
iscissors->curve2->x2 = iscissors->nx;
iscissors->curve2->y2 = iscissors->ny;
calculate_curve (tool, iscissors->curve2);
}
break;
default:
break;
}
}
TRC (("button_release: draw core resume\n"));
/* Draw only the boundary */
iscissors->state = WAITING;
iscissors->draw = DRAW_CURVE;
draw_core_resume (iscissors->core, tool);
/* convert the curves into a region */
if (iscissors->connected)
iscissors_convert (iscissors, gdisp_ptr);
}
static void
iscissors_motion (Tool *tool,
GdkEventMotion *mevent,
gpointer gdisp_ptr)
{
Iscissors *iscissors;
GDisplay *gdisp;
gdisp = (GDisplay *) gdisp_ptr;
iscissors = (Iscissors *) tool->private;
if (tool->state != ACTIVE || iscissors->state == NO_ACTION)
return;
if (iscissors->state == SEED_PLACEMENT)
iscissors->draw = DRAW_CURRENT_SEED;
else if (iscissors->state == SEED_ADJUSTMENT)
iscissors->draw = DRAW_ACTIVE_CURVE;
draw_core_pause (iscissors->core, tool);
gdisplay_untransform_coords (gdisp, mevent->x, mevent->y,
&iscissors->x, &iscissors->y, FALSE, TRUE);
switch (iscissors->state)
{
case SEED_PLACEMENT :
/* Hold the shift key down to disable the auto-edge snap feature */
if (! (mevent->state & GDK_SHIFT_MASK))
find_max_gradient (iscissors, gdisp->gimage,
&iscissors->x, &iscissors->y);
iscissors->x = BOUNDS (iscissors->x, 0, gdisp->gimage->width - 1);
iscissors->y = BOUNDS (iscissors->y, 0, gdisp->gimage->height - 1);
if (iscissors->first_point)
{
iscissors->ix = iscissors->x;
iscissors->iy = iscissors->y;
}
break;
case SEED_ADJUSTMENT :
/* Move the current seed to the location of the cursor */
if (! (mevent->state & GDK_SHIFT_MASK))
find_max_gradient (iscissors, gdisp->gimage,
&iscissors->x, &iscissors->y);
iscissors->x = BOUNDS (iscissors->x, 0, gdisp->gimage->width - 1);
iscissors->y = BOUNDS (iscissors->y, 0, gdisp->gimage->height - 1);
iscissors->nx = iscissors->x;
iscissors->ny = iscissors->y;
break;
default :
break;
}
draw_core_resume (iscissors->core, tool);
}
static void
iscissors_draw (Tool *tool)
{
GDisplay *gdisp;
Iscissors *iscissors;
ICurve *curve;
GSList *list;
int tx1, ty1, tx2, ty2;
int txn, tyn;
TRC (("iscissors_draw\n"));
gdisp = (GDisplay *) tool->gdisp_ptr;
iscissors = (Iscissors *) tool->private;
gdisplay_transform_coords (gdisp, iscissors->ix, iscissors->iy, &tx1, &ty1,
FALSE);
/* Draw the crosshairs target if we're placing a seed */
if (iscissors->draw & DRAW_CURRENT_SEED)
{
gdisplay_transform_coords(gdisp, iscissors->x, iscissors->y, &tx2, &ty2,
FALSE);
gdk_draw_line (iscissors->core->win, iscissors->core->gc,
tx2 - (TARGET_WIDTH >> 1), ty2,
tx2 + (TARGET_WIDTH >> 1), ty2);
gdk_draw_line (iscissors->core->win, iscissors->core->gc,
tx2, ty2 - (TARGET_HEIGHT >> 1),
tx2, ty2 + (TARGET_HEIGHT >> 1));
if (!iscissors->first_point)
gdk_draw_line (iscissors->core->win, iscissors->core->gc,
tx1, ty1, tx2, ty2);
}
if ((iscissors->draw & DRAW_CURVE) && !iscissors->first_point)
{
/* Draw a point at the init point coordinates */
if (!iscissors->connected)
gdk_draw_arc (iscissors->core->win, iscissors->core->gc, 1,
tx1 - POINT_HALFWIDTH, ty1 - POINT_HALFWIDTH,
POINT_WIDTH, POINT_WIDTH, 0, 23040);
/* Go through the list of icurves, and render each one... */
list = iscissors->curves;
while (list)
{
curve = (ICurve *) list->data;
/* plot the curve */
iscissors_draw_curve (gdisp, iscissors, curve);
gdisplay_transform_coords (gdisp, curve->x1, curve->y1, &tx1, &ty1,
FALSE);
gdk_draw_arc (iscissors->core->win, iscissors->core->gc, 1,
tx1 - POINT_HALFWIDTH, ty1 - POINT_HALFWIDTH,
POINT_WIDTH, POINT_WIDTH, 0, 23040);
list = g_slist_next (list);
}
}
if (iscissors->draw & DRAW_ACTIVE_CURVE)
{
gdisplay_transform_coords (gdisp, iscissors->nx, iscissors->ny,
&txn, &tyn, FALSE);
/* plot both curves, and the control point between them */
if (iscissors->curve1)
{
gdisplay_transform_coords (gdisp, iscissors->curve1->x2,
iscissors->curve1->y2, &tx1, &ty1, FALSE);
gdk_draw_line (iscissors->core->win, iscissors->core->gc,
tx1, ty1, txn, tyn);
}
if (iscissors->curve2)
{
gdisplay_transform_coords (gdisp, iscissors->curve2->x1,
iscissors->curve2->y1, &tx2, &ty2, FALSE);
gdk_draw_line (iscissors->core->win, iscissors->core->gc,
tx2, ty2, txn, tyn);
}
gdk_draw_arc (iscissors->core->win, iscissors->core->gc, 1,
txn - POINT_HALFWIDTH, tyn - POINT_HALFWIDTH,
POINT_WIDTH, POINT_WIDTH, 0, 23040);
}
}
static void
iscissors_draw_curve (GDisplay *gdisp,
Iscissors *iscissors,
ICurve *curve)
{
gpointer *point;
guint len;
int tx, ty;
int npts;
guint32 coords;
/* Uh, this shouldn't happen, but it does. So we ignore it.
* Quality code, baby. */
if (!curve->points)
return;
npts = 0;
point = curve->points->pdata;
len = curve->points->len;
while (len--)
{
coords = GPOINTER_TO_INT (*point);
point++;
gdisplay_transform_coords (gdisp, (coords & 0x0000ffff),
(coords >> 16), &tx, &ty, FALSE);
if (npts < MAX_POINTS)
{
curve_points [npts].x = tx;
curve_points [npts].y = ty;
npts ++;
}
else
{
g_warning ("too many points in ICurve segment!");
return;
}
}
/* draw the curve */
gdk_draw_lines (iscissors->core->win, iscissors->core->gc,
curve_points, npts);
}
static void
iscissors_control (Tool *tool,
ToolAction action,
gpointer gdisp_ptr)
{
Iscissors * iscissors;
Iscissors_draw draw;
iscissors = (Iscissors *) tool->private;
switch (iscissors->state)
{
case SEED_PLACEMENT:
draw = DRAW_CURVE | DRAW_CURRENT_SEED;
break;
case SEED_ADJUSTMENT:
draw = DRAW_CURVE | DRAW_ACTIVE_CURVE;
break;
default:
draw = DRAW_CURVE;
break;
}
iscissors->draw = draw;
switch (action)
{
case PAUSE:
draw_core_pause (iscissors->core, tool);
break;
case RESUME:
draw_core_resume (iscissors->core, tool);
break;
case HALT:
draw_core_stop (iscissors->core, tool);
iscissors_reset (iscissors);
break;
default:
break;
}
}
static void
iscissors_reset (Iscissors *iscissors)
{
TRC( ("iscissors_reset\n"));
/* Free and reset the curve list */
if (iscissors->curves)
{
iscissors_free_icurves (iscissors->curves);
TRC (("g_slist_free (iscissors->curves);\n"));
g_slist_free (iscissors->curves);
iscissors->curves = NULL;
}
/* free mask */
if (iscissors->mask)
channel_delete (iscissors->mask);
iscissors->mask = NULL;
/* free the gradient map */
if (iscissors->gradient_map)
{
/* release any tile we were using */
if (cur_tile)
{
TRC (("tile_release\n"));
tile_release (cur_tile, FALSE);
}
cur_tile = NULL;
TRC (("tile_manager_destroy (iscissors->gradient_map);\n"));
tile_manager_destroy (iscissors->gradient_map);
iscissors->gradient_map = NULL;
}
iscissors->curve1 = NULL;
iscissors->curve2 = NULL;
iscissors->first_point = TRUE;
iscissors->connected = FALSE;
iscissors->state = NO_ACTION;
/* Reset the dp buffers */
iscissors_free_buffers (iscissors);
/* If they haven't already been initialized, precalculate the diagonal
* weight and direction value arrays
*/
if (!initialized)
{
precalculate_arrays ();
initialized = TRUE;
}
}
static void
iscissors_free_icurves (GSList *list)
{
ICurve * curve;
TRC (("iscissors_free_icurves\n"));
while (list)
{
curve = (ICurve *) list->data;
if (curve->points)
{
TRC (("g_ptr_array_free (curve->points);\n"));
g_ptr_array_free (curve->points, TRUE);
}
TRC (("g_free (curve);\n"));
g_free (curve);
list = g_slist_next (list);
}
}
static void
iscissors_free_buffers (Iscissors * iscissors)
{
if (iscissors->dp_buf)
temp_buf_free (iscissors->dp_buf);
iscissors->dp_buf = NULL;
}
/* XXX need some scan-conversion routines from somewhere. maybe. ? */
static int
clicked_on_vertex (Tool *tool)
{
Iscissors * iscissors;
GSList *list;
ICurve * curve;
int curves_found = 0;
iscissors = (Iscissors *) tool->private;
/* traverse through the list, returning non-zero if the current cursor
* position is on an existing curve vertex. Set the curve1 and curve2
* variables to the two curves containing the vertex in question
*/
iscissors->curve1 = iscissors->curve2 = NULL;
list = iscissors->curves;
while (list && curves_found < 2)
{
curve = (ICurve *) list->data;
if (abs (curve->x1 - iscissors->x) < POINT_HALFWIDTH &&
abs (curve->y1 - iscissors->y) < POINT_HALFWIDTH)
{
iscissors->curve1 = curve;
if (curves_found++)
return 1;
}
else if (abs (curve->x2 - iscissors->x) < POINT_HALFWIDTH &&
abs (curve->y2 - iscissors->y) < POINT_HALFWIDTH)
{
iscissors->curve2 = curve;
if (curves_found++)
return 1;
}
list = g_slist_next (list);
}
/* if only one curve was found, the curves are unconnected, and
* the user only wants to move either the first or last point
* disallow this for now.
*/
if (curves_found == 1)
{
return 0;
}
/* no vertices were found at the cursor click point. Now check whether
* the click occured on a curve. If so, create a new vertex there and
* two curve segments to replace what used to be just one...
*/
return clicked_on_curve (tool);
}
static int
clicked_on_curve (Tool *tool)
{
Iscissors * iscissors;
GSList *list, *new_link;
gpointer *pt;
int len;
ICurve * curve, * new_curve;
guint32 coords;
int tx, ty;
iscissors = (Iscissors *) tool->private;
/* traverse through the list, returning non-zero if the current cursor
* position is on a curve... If this occurs, replace the curve with two
* new curves, separated by the new vertex.
*/
list = iscissors->curves;
while (list)
{
curve = (ICurve *) list->data;
pt = curve->points->pdata;
len = curve->points->len;
while (len--)
{
coords = GPOINTER_TO_INT (*pt);
pt++;
tx = coords & 0x0000ffff;
ty = coords >> 16;
/* Is the specified point close enough to the curve? */
if (abs (tx - iscissors->x) < POINT_HALFWIDTH &&
abs (ty - iscissors->y) < POINT_HALFWIDTH)
{
/* Since we're modifying the curve, undraw the existing one */
iscissors->draw = DRAW_CURVE;
draw_core_pause (iscissors->core, tool);
/* Create the new curve */
new_curve = g_malloc (sizeof (ICurve));
new_curve->x2 = curve->x2;
new_curve->y2 = curve->y2;
new_curve->x1 = curve->x2 = iscissors->x;
new_curve->y1 = curve->y2 = iscissors->y;
new_curve->points = NULL;
/* Create the new link and supply the new curve as data */
new_link = g_slist_alloc ();
new_link->data = (void *) new_curve;
/* Insert the new link in the list */
new_link->next = list->next;
list->next = new_link;
iscissors->curve1 = new_curve;
iscissors->curve2 = curve;
/* Redraw the curve */
draw_core_resume (iscissors->core, tool);
return 1;
}
}
list = g_slist_next (list);
}
return 0;
}
static void
precalculate_arrays (void)
{
int i;
for (i = 0; i < 256; i++)
{
/* The diagonal weight array */
diagonal_weight [i] = (int) (i * G_SQRT2);
/* The direction value array */
direction_value [i][0] = (127 - abs (127 - i)) * 2;
direction_value [i][1] = abs (127 - i) * 2;
direction_value [i][2] = abs (191 - i) * 2;
direction_value [i][3] = abs (63 - i) * 2;
TRC (("i: %d, v0: %d, v1: %d, v2: %d, v3: %d\n", i,
direction_value [i][0],
direction_value [i][1],
direction_value [i][2],
direction_value [i][3]));
}
/* set the 256th index of the direction_values to the hightest cost */
direction_value [255][0] = 255;
direction_value [255][1] = 255;
direction_value [255][2] = 255;
direction_value [255][3] = 255;
}
static void
calculate_curve (Tool *tool, ICurve *curve)
{
GDisplay * gdisp;
Iscissors * iscissors;
int x, y, dir;
int xs, ys, xe, ye;
int x1, y1, x2, y2;
int width, height;
int ewidth, eheight;
TRC (("calculate_curve(%p, %p)\n", tool, curve));
/* Calculate the lowest cost path from one vertex to the next as specified
* by the parameter "curve".
* Here are the steps:
* 1) Calculate the appropriate working area for this operation
* 2) Allocate a temp buf for the dynamic programming array
* 3) Run the dynamic programming algorithm to find the optimal path
* 4) Translate the optimal path into pixels in the icurve data
* structure.
*/
gdisp = (GDisplay *) tool->gdisp_ptr;
iscissors = (Iscissors *) tool->private;
/* Get the bounding box */
xs = BOUNDS (curve->x1, 0, gdisp->gimage->width - 1);
ys = BOUNDS (curve->y1, 0, gdisp->gimage->height - 1);
xe = BOUNDS (curve->x2, 0, gdisp->gimage->width - 1);
ye = BOUNDS (curve->y2, 0, gdisp->gimage->height - 1);
x1 = MINIMUM (xs, xe);
y1 = MINIMUM (ys, ye);
x2 = MAXIMUM (xs, xe) + 1; /* +1 because if xe = 199 & xs = 0, x2 - x1, width = 200 */
y2 = MAXIMUM (ys, ye) + 1;
/* expand the boundaries past the ending points by
* some percentage of width and height. This serves the following purpose:
* It gives the algorithm more area to search so better solutions
* are found. This is particularly helpful in finding "bumps" which
* fall outside the bounding box represented by the start and end
* coordinates of the "curve".
*/
ewidth = (x2 - x1) * EXTEND_BY + FIXED;
eheight = (y2 - y1) * EXTEND_BY + FIXED;
if (xe >= xs)
x2 += BOUNDS (ewidth, 0, gdisp->gimage->width - x2);
else
x1 -= BOUNDS (ewidth, 0, x1);
if (ye >= ys)
y2 += BOUNDS (eheight, 0, gdisp->gimage->height - y2);
else
y1 -= BOUNDS (eheight, 0, y1);
/* blow away any previous points list we might have */
if (curve->points)
{
TRC (("1229: g_ptr_array_free (curve->points);\n"));
g_ptr_array_free (curve->points, TRUE);
curve->points = NULL;
}
/* If the bounding box has width and height... */
if ((x2 - x1) && (y2 - y1))
{
width = (x2 - x1);
height = (y2 - y1);
/* Initialise the gradient map tile manager for this image if we
* don't already have one. */
if (!iscissors->gradient_map)
iscissors->gradient_map = gradient_map_new (gdisp->gimage);
TRC (("dp buf resize\n"));
/* allocate the dynamic programming array */
iscissors->dp_buf =
temp_buf_resize (iscissors->dp_buf, 4, x1, y1, width, height);
TRC (("find_optimal_path\n"));
/* find the optimal path of pixels from (x1, y1) to (x2, y2) */
find_optimal_path (iscissors->gradient_map, iscissors->dp_buf,
x1, y1, x2, y2, xs, ys);
/* get a list of the pixels in the optimal path */
TRC (("plot_pixels\n"));
curve->points = plot_pixels (iscissors, iscissors->dp_buf,
x1, y1, xs, ys, xe, ye);
}
/* If the bounding box has no width */
else if ((x2 - x1) == 0)
{
/* plot a vertical line */
y = ys;
dir = (ys > ye) ? -1 : 1;
curve->points = g_ptr_array_new ();
while (y != ye)
{
g_ptr_array_add (curve->points, GINT_TO_POINTER ((y << 16) + xs));
y += dir;
}
}
/* If the bounding box has no height */
else if ((y2 - y1) == 0)
{
/* plot a horizontal line */
x = xs;
dir = (xs > xe) ? -1 : 1;
curve->points = g_ptr_array_new ();
while (x != xe)
{
g_ptr_array_add (curve->points, GINT_TO_POINTER ((ys << 16) + x));
x += dir;
}
}
}
/* badly need to get a replacement - this is _way_ too expensive */
static int
gradient_map_value (TileManager *map, int x, int y,
guint8 *grad, guint8 *dir)
{
static int cur_tilex;
static int cur_tiley;
guint8 *p;
if (!cur_tile ||
x / TILE_WIDTH != cur_tilex ||
y / TILE_HEIGHT != cur_tiley)
{
if (cur_tile)
tile_release (cur_tile, FALSE);
cur_tile = tile_manager_get_tile (map, x, y, TRUE, FALSE);
if (!cur_tile)
return 0;
cur_tilex = x / TILE_WIDTH;
cur_tiley = y / TILE_HEIGHT;
}
p = tile_data_pointer (cur_tile, x % TILE_WIDTH, y % TILE_HEIGHT);
*grad = p[0];
*dir = p[1];
return 1;
}
static int
calculate_link (TileManager *gradient_map,
int x, int y, guint32 pixel, int link)
{
int value = 0;
guint8 grad1, dir1, grad2, dir2;
if (!gradient_map_value (gradient_map, x, y, &grad1, &dir1))
{
grad1 = 0;
dir1 = 255;
}
/* Convert the gradient into a cost: large gradients are good, and
* so have low cost. */
grad1 = 255 - grad1;
/* calculate the contribution of the gradient magnitude */
if (link > 1)
value += diagonal_weight [grad1] * OMEGA_G;
else
value += grad1 * OMEGA_G;
/* calculate the contribution of the gradient direction */
x += (gint8)(pixel & 0xff);
y += (gint8)((pixel & 0xff00) >> 8);
if (!gradient_map_value (gradient_map, x, y, &grad2, &dir2))
{
grad2 = 0;
dir2 = 255;
}
value += (direction_value [dir1][link] + direction_value [dir2][link]) *
OMEGA_D;
return value;
}
static GPtrArray *
plot_pixels (Iscissors *iscissors, TempBuf *dp_buf,
int x1, int y1,
int xs, int ys,
int xe, int ye)
{
int x, y;
guint32 coords;
int link;
int width;
unsigned int * data;
GPtrArray *list;
width = dp_buf->width;
/* Start the data pointer at the correct location */
data = (unsigned int *)temp_buf_data(dp_buf) + (ye - y1) * width + (xe - x1);
x = xe;
y = ye;
list = g_ptr_array_new ();
while (1)
{
coords = (y << 16) + x;
g_ptr_array_add (list, GINT_TO_POINTER (coords));
link = PIXEL_DIR (*data);
if (link == SEED_POINT)
return list;
x += move [link][0];
y += move [link][1];
data += move [link][1] * width + move [link][0];
}
/* won't get here */
return NULL;
}
#define PACK(x, y) ((((y) & 0xff) << 8) | ((x) & 0xff))
#define OFFSET(pixel) ((gint8)((pixel) & 0xff) + \
((gint8)(((pixel) & 0xff00) >> 8)) * dp_buf->width)
static void
find_optimal_path (TileManager *gradient_map, TempBuf *dp_buf,
int x1, int y1, int x2, int y2,
int xs, int ys)
{
int i, j, k;
int x, y;
int link;
int linkdir;
int dirx, diry;
int min_cost;
int new_cost;
int offset;
int cum_cost [8];
int link_cost [8];
int pixel_cost [8];
guint32 pixel [8];
guint32 * data, *d;
TRC (("find_optimal_path (%p, %p, [%d,%d-%d,%d] %d, %d)\n",
gradient_map, dp_buf, x1, y1, x2, y2, xs, ys));
/* initialize the dynamic programming buffer */
data = (guint32 *) temp_buf_data (dp_buf);
for (i = 0; i < dp_buf->height; i++)
for (j = 0; j < dp_buf->width; j++)
*data++ = 0; /* 0 cumulative cost, 0 direction */
/* what directions are we filling the array in according to? */
dirx = (xs - x1 == 0) ? 1 : -1;
diry = (ys - y1 == 0) ? 1 : -1;
linkdir = (dirx * diry);
y = ys;
/* Start the data pointer at the correct location */
data = (guint32 *) temp_buf_data (dp_buf);
TRC (("find_optimal_path: mainloop\n"));
for (i = 0; i < dp_buf->height; i++)
{
x = xs;
d = data + (y-y1) * dp_buf->width + (x-x1);
for (j = 0; j < dp_buf->width; j++)
{
min_cost = G_MAXINT;
/* pixel[] array encodes how to get to a neigbour, if possible.
* 0 means no connection (eg edge).
* Rest packed as bottom two bytes: y offset then x offset.
* Initially, we assume we can't get anywhere. */
for (k = 0; k < 8; k++)
pixel [k] = 0;
/* Find the valid neighboring pixels */
/* the previous pixel */
if (j)
pixel [((dirx == 1) ? 4 : 0)] = PACK (-dirx, 0);
/* the previous row of pixels */
if (i)
{
pixel [((diry == 1) ? 5 : 1)] = PACK (0, -diry);
link = (linkdir == 1) ? 3 : 2;
if (j)
pixel [((diry == 1) ? (link + 4) : link)] = PACK(-dirx, -diry);
link = (linkdir == 1) ? 2 : 3;
if (j != dp_buf->width - 1)
pixel [((diry == 1) ? (link + 4) : link)] = PACK (dirx, -diry);
}
/* find the minimum cost of going through each neighbor to reach the
* seed point...
*/
link = -1;
for (k = 0; k < 8; k ++)
if (pixel [k])
{
link_cost [k] = calculate_link (gradient_map,
xs + j*dirx, ys + i*diry,
pixel [k],
((k > 3) ? k - 4 : k));
offset = OFFSET (pixel [k]);
pixel_cost [k] = PIXEL_COST (d [offset]);
cum_cost [k] = pixel_cost [k] + link_cost [k];
if (cum_cost [k] < min_cost)
{
min_cost = cum_cost [k];
link = k;
}
}
/* If anything can be done... */
if (link >= 0)
{
/* set the cumulative cost of this pixel and the new direction */
*d = (cum_cost [link] << 8) + link;
/* possibly change the links from the other pixels to this pixel...
* these changes occur if a neighboring pixel will receive a lower
* cumulative cost by going through this pixel.
*/
for (k = 0; k < 8; k ++)
if (pixel [k] && k != link)
{
/* if the cumulative cost at the neighbor is greater than
* the cost through the link to the current pixel, change the
* neighbor's link to point to the current pixel.
*/
new_cost = link_cost [k] + cum_cost [link];
if (pixel_cost [k] > new_cost)
{
/* reverse the link direction /-----------------------\ */
offset = OFFSET (pixel [k]);
d [offset] = (new_cost << 8) + ((k > 3) ? k - 4 : k + 4);
}
}
}
/* Set the seed point */
else if (!i && !j)
*d = SEED_POINT;
/* increment the data pointer and the x counter */
d += dirx;
x += dirx;
}
/* increment the y counter */
y += diry;
}
TRC (("done: find_optimal_path\n"));
}
/* Called to fill in a newly referenced tile in the gradient map */
static void
gradmap_tile_validate (TileManager *tm, Tile *tile)
{
static gboolean first_gradient = TRUE;
int x, y;
int dw, dh;
int sw, sh;
int i, j;
int b;
float gradient;
guint8 *gradmap;
guint8 *tiledata;
guint8 *datah, *datav;
gint8 hmax, vmax;
Tile *srctile;
PixelRegion srcPR, destPR;
GImage *gimage;
gimage = (GImage *) tile_manager_get_user_data (tm);
if (first_gradient)
{
int radius = GRADIENT_SEARCH >> 1;
/* compute the distance weights */
for (i = 0; i < GRADIENT_SEARCH; i++)
for (j = 0; j < GRADIENT_SEARCH; j++)
distance_weights [i * GRADIENT_SEARCH + j] =
1.0 / (1 + sqrt (SQR(i - radius) + SQR(j - radius)));
first_gradient = FALSE;
}
tile_manager_get_tile_coordinates (tm, tile, &x, &y);
dw = tile_ewidth (tile);
dh = tile_eheight (tile);
TRC (("fill req for tile %p @ (%d, %d)\n", tile, x, y));
/* get corresponding tile in the gimage */
srctile = tile_manager_get_tile (gimp_image_composite (gimage),
x, y, TRUE, FALSE);
if (!srctile)
{
g_warning ("bad tile coords?");
return;
}
sw = tile_ewidth (srctile);
sh = tile_eheight (srctile);
if (dw != sw || dh != sh)
g_warning ("dw:%d sw:%d dh:%d sh:%d\n", dw, sw, dh, sh);
srcPR.w = MIN (dw, sw);
srcPR.h = MIN (dh, sh);
srcPR.bytes = gimp_image_composite_bytes (gimage);
srcPR.data = tile_data_pointer (srctile, 0, 0);
srcPR.rowstride = srcPR.w * srcPR.bytes;
/* XXX tile edges? */
/* Blur the source to get rid of noise */
destPR.rowstride = TILE_WIDTH * 4;
destPR.data = maxgrad_conv0;
convolve_region (&srcPR, &destPR, blur_32, 3, 32, NORMAL_CONVOL);
/* Set the "src" temp buf up as the new source Pixel Region */
srcPR.rowstride = destPR.rowstride;
srcPR.data = destPR.data;
/* Get the horizontal derivative */
destPR.data = maxgrad_conv1;
convolve_region (&srcPR, &destPR, horz_deriv, 3, 1, NEGATIVE_CONVOL);
/* Get the vertical derivative */
destPR.data = maxgrad_conv2;
convolve_region (&srcPR, &destPR, vert_deriv, 3, 1, NEGATIVE_CONVOL);
/* calculate overall gradient */
tiledata = tile_data_pointer (tile, 0, 0);
for (i = 0; i < srcPR.h; i++)
{
datah = maxgrad_conv1 + srcPR.rowstride*i;
datav = maxgrad_conv2 + srcPR.rowstride*i;
gradmap = tiledata + tile_ewidth (tile) * COST_WIDTH * i;
for (j = 0; j < srcPR.w; j++)
{
hmax = datah[0] - 128;
vmax = datav[0] - 128;
for (b = 1; b < srcPR.bytes; b++)
{
if (abs (datah[b] - 128) > abs (hmax)) hmax = datah[b] - 128;
if (abs (datav[b] - 128) > abs (vmax)) vmax = datav[b] - 128;
}
if (i == 0 || j == 0 || i == srcPR.h-1 || j == srcPR.w-1)
{
gradmap[j*COST_WIDTH] = 0;
gradmap[j*COST_WIDTH + 1] = 255;
goto contin;
}
/* 1 byte absolute magitude first */
gradient = sqrt(SQR(hmax) + SQR(vmax));
gradmap[j*COST_WIDTH] = gradient * 255 / MAX_GRADIENT;
/* then 1 byte direction */
if (gradient > MIN_GRADIENT)
{
float direction;
if (!hmax)
direction = (vmax > 0) ? G_PI_2 : -G_PI_2;
else
direction = atan ((double) vmax / (double) hmax);
/* Scale the direction from between 0 and 254,
* corresponding to -PI/2, PI/2 255 is reserved for
* directionless pixels */
gradmap[j*COST_WIDTH + 1] =
(guint8) (254 * (direction + G_PI_2) / G_PI);
}
else
gradmap[j*COST_WIDTH + 1] = 255; /* reserved for weak gradient */
contin:
{
#ifdef DEBUG
int g = gradmap[j*COST_WIDTH];
int d = gradmap[j*COST_WIDTH + 1];
TRC (("%c%c", 'a' + (g * 25 / 255), '0' + (d / 25)));
#endif /* DEBUG */
}
datah += srcPR.bytes;
datav += srcPR.bytes;
}
TRC (("\n"));
}
TRC (("\n"));
tile_release (srctile, FALSE);
}
static TileManager *
gradient_map_new (GImage *gimage)
{
TileManager *tm;
tm = tile_manager_new (gimage->width, gimage->height,
sizeof(guint8) * COST_WIDTH);
tile_manager_set_user_data (tm, gimage);
tile_manager_set_validate_proc (tm, gradmap_tile_validate);
return tm;
}
static void
find_max_gradient (Iscissors *iscissors, GImage *gimage, int *x, int *y)
{
PixelRegion srcPR;
int radius;
int i, j;
int endx, endy;
int sx, sy, cx, cy;
int x1, y1, x2, y2;
void *pr;
guint8 *gradient;
float g, max_gradient;
TRC (("find_max_gradient(%d, %d)\n", *x, *y));
/* Initialise the gradient map tile manager for this image if we
* don't already have one. */
if (!iscissors->gradient_map)
iscissors->gradient_map = gradient_map_new (gimage);
radius = GRADIENT_SEARCH >> 1;
/* calculate the extent of the search */
cx = BOUNDS (*x, 0, gimage->width);
cy = BOUNDS (*y, 0, gimage->height);
sx = cx - radius;
sy = cy - radius;
x1 = BOUNDS (cx - radius, 0, gimage->width);
y1 = BOUNDS (cy - radius, 0, gimage->height);
x2 = BOUNDS (cx + radius, 0, gimage->width);
y2 = BOUNDS (cy + radius, 0, gimage->height);
/* calculate the factor to multiply the distance from the cursor by */
max_gradient = 0;
*x = cx;
*y = cy;
/* Find the point of max gradient */
pixel_region_init (&srcPR, iscissors->gradient_map,
x1, y1, x2 - x1, y2 - y1, FALSE);
/* this iterates over 1, 2 or 4 tiles only */
for (pr = pixel_regions_register (1, &srcPR);
pr != NULL;
pr = pixel_regions_process (pr))
{
endx = srcPR.x + srcPR.w;
endy = srcPR.y + srcPR.h;
for (i = srcPR.y; i < endy; i++)
{
gradient = srcPR.data + srcPR.rowstride * (i - srcPR.y);
for (j = srcPR.x; j < endx; j++)
{
g = *gradient;
gradient += COST_WIDTH;
g *= distance_weights [(i-y1) * GRADIENT_SEARCH + (j-x1)];
if (g > max_gradient)
{
max_gradient = g;
*x = j;
*y = i;
}
}
}
}
TRC (("done: find_max_gradient(%d, %d)\n", *x, *y));
}
/* End of iscissors.c */