001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009package Ch21_Geometric_Operations; 010 011import ij.IJ; 012import ij.ImagePlus; 013import ij.gui.GenericDialog; 014import ij.gui.ImageCanvas; 015import ij.gui.ImageWindow; 016import ij.gui.Overlay; 017import ij.plugin.filter.PlugInFilter; 018import ij.process.ImageProcessor; 019import imagingbook.common.color.sets.BasicAwtColor; 020import imagingbook.common.geometry.basic.Pnt2d; 021import imagingbook.common.geometry.basic.Pnt2d.PntInt; 022import imagingbook.common.geometry.mappings.linear.AffineMapping2D; 023import imagingbook.common.ij.DialogUtils; 024import imagingbook.common.ij.IjUtils; 025import imagingbook.common.ij.overlay.ColoredStroke; 026import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 027import imagingbook.core.jdoc.JavaDocHelp; 028import imagingbook.sampleimages.GeneralSampleImage; 029 030import java.awt.Component; 031import java.awt.event.InputEvent; 032import java.awt.event.KeyAdapter; 033import java.awt.event.KeyEvent; 034import java.awt.event.KeyListener; 035import java.awt.event.MouseAdapter; 036import java.awt.event.MouseEvent; 037import java.awt.event.MouseListener; 038import java.awt.event.MouseMotionListener; 039import java.awt.event.WindowAdapter; 040import java.awt.event.WindowEvent; 041import java.awt.geom.Ellipse2D; 042import java.awt.geom.Path2D; 043 044/** 045 * <p> 046 * ImageJ plugin, performs piecewise affine transformation by triangulation of the input image, as described in Sec. 047 * 21.1.8 (see Fig. 21.13) of [1]. 048 * </p> 049 * <p> 050 * The plugin projects a triangular grid onto the image which is mouse-editable, i.e., grid points can be selected and 051 * dragged. Note that the outer grid points (along the image border) cannot be moved. The grid is drawn as a graphic 052 * overlay. In each repaint iteration, pixel values are re-calculated by 053 * </p> 054 * <ol> 055 * <li>finding the containing (distorted) triangle of the pixel,</li> 056 * <li>calculating the affine mapping w.r.t. the original (undistorted) 057 * triangle,</li> 058 * <li>apply target-to-source mapping of pixel coordinates to the original (source) image 059 * and</li> 060 * <li>retrieving the interpolated pixel values from the original image.</li> 061 * </ol> 062 * <p> 063 * The following mouse and keypoint events are observed: 064 * </p> 065 * <ul> 066 * <li>left mouse botton: select and drag grid points,</li> 067 * <li>right mouse botton: reset the grid,</li> 068 * <li>ctrl/+ key: zoom in,</li> 069 * <li>ctrl/- key; zoom out,</li> 070 * <li>enter or escape key: finish editing.</li> 071 * </ul> 072 * <p> 073 * Note that this is a simplistic implementation which leaves much room for improvements 074 * and increased efficiency. 075 * </p> 076 * <p> 077 * [1] W. Burger, M.J. Burge, <em>Digital Image Processing – An 078 * Algorithmic Introduction</em>, 3rd ed, Springer (2022). 079 * </p> 080 * 081 * @author WB 082 * @version 2022/11/25 083 */ 084public class Mesh_Warp_Interactive implements PlugInFilter, JavaDocHelp { 085 086 private static final String PropertyKey = Mesh_Warp_Interactive.class.getName(); 087 private static final String EditString = " (editing)"; 088 089 private static int Rows = 10; // number of grid rows 090 private static int Cols = 10; // number of grid columns 091 092 private static BasicAwtColor StrokeColorChoice = BasicAwtColor.Blue; 093 private static BasicAwtColor HighlightColorChoice = BasicAwtColor.Green; 094 095 private static double StrokeWidth = 0.25; 096 private static double CatchRadius = 3.0; 097 private static boolean ShowTriangles = true; 098 private static boolean HighlightSelection = true; 099 private static boolean RemoveOverlayWhenDone = true; 100 101 // event handling variables: 102 private KeyListener[] windowKeyListeners = null; 103 private MouseListener[] canvasMouseListeners = null; 104 private KeyListener[] canvasKeyListeners = null; 105 private MouseMotionListener[] canvasMouseMotionListeners = null; 106 107 // ---- data structures representing the grid and mesh -------------- 108 109 private Pnt2d[][] gridOrig; // grid points positions 110 private Pnt2d[][] gridWarped; // gridWarped[row][col][x/y] 111 private PntInt nodeSelected = null; // the selected grid node (x = row, y = column), inner node only! 112 113 // triangles: 114 private Triangle[][][] trianglesOrig; 115 private Triangle[][][] trianglesWarped; // trianglesWarped[row][col][0/1] 116 private TriangleGroup trianglesSelected = null; 117 118 // ------------------------------------------------------------------ 119 120 private ImageWindow win; 121 private ImageCanvas canvas; 122 private ImagePlus im; 123 private ImageProcessor ipOrig = null; 124 private String title; 125 126 /** 127 * Constructor, asks to open a predefined sample image if no other image 128 * is currently open. 129 */ 130 public Mesh_Warp_Interactive() { 131 if (IjUtils.noCurrentImage()) { 132 DialogUtils.askForSampleImage(GeneralSampleImage.WartburgSmall); 133 } 134 } 135 136 @Override 137 public int setup(String arg, ImagePlus im) { 138 this.im = im; 139 return DOES_ALL; 140 } 141 142 @Override 143 public void run(ImageProcessor ip) { 144 if (im.getProperty(PropertyKey) != null) { 145 IJ.error("Plugin is already running, finish first!"); 146 return; 147 } 148 149 if (!runDialog()) { 150 return; 151 } 152 153 this.win = this.im.getWindow(); 154 this.canvas = this.win.getCanvas(); 155 this.title = this.im.getTitle(); 156 157 ipOrig = ip.duplicate(); // keep a copy of the original image 158 ipOrig.setInterpolate(true); 159 ipOrig.setInterpolationMethod(ImageProcessor.BICUBIC); 160 161 im.setProperty(PropertyKey, "running"); 162 im.setTitle(title + EditString); 163 setupListeners(); 164 165 reset(); 166 redraw(); 167 IJ.wait(100); 168 } 169 170 // --------------------------------------------------------------- 171 172 private void reset() { 173 initGridAndTriangles(im.getWidth(), im.getHeight()); 174 nodeSelected = null; 175 trianglesSelected = null; 176 } 177 178 private void finish() { 179 revertListeners(); 180 im.setTitle(title); 181 nodeSelected = null; 182 trianglesSelected = null; 183 if (RemoveOverlayWhenDone) { 184 im.setOverlay(null); 185 } 186 ipOrig = null; 187 im.setProperty(PropertyKey, null); 188 im.updateAndDraw(); 189 } 190 191 // --------------------------------------------------------------- 192 193 private void redraw() { 194 im.setOverlay(makeGridOverlay(gridWarped)); 195 im.updateAndDraw(); 196 } 197 198 private void initGridAndTriangles(int w, int h) { 199 this.gridOrig = new Pnt2d[Rows][Cols]; 200 this.gridWarped = new Pnt2d[Rows][Cols]; 201 202 // insert equally spaced points over image width/height 203 for (int r = 0; r < Rows; r++) { 204 double y = (double) r * (h - 1) / (Rows - 1); 205 for (int c = 0; c < Cols; c++) { 206 double x = (double) c * (w - 1) / (Cols - 1); 207 gridOrig[r][c] = gridWarped[r][c] = Pnt2d.from(x, y); 208 } 209 } 210 211 trianglesOrig = new Triangle[Rows - 1][Cols - 1][2]; 212 trianglesWarped = new Triangle[Rows - 1][Cols - 1][2]; 213 updateTrianglesAll(trianglesOrig, gridOrig); 214 updateTrianglesAll(trianglesWarped, gridWarped); 215 } 216 217 private Overlay makeGridOverlay(Pnt2d[][] pnts) { 218 ShapeOverlayAdapter ola = new ShapeOverlayAdapter(); 219 ColoredStroke pathstroke = new ColoredStroke(StrokeWidth, StrokeColorChoice.getColor()); 220 ColoredStroke polystroke = new ColoredStroke(StrokeWidth, HighlightColorChoice.getColor()); 221 ColoredStroke highlightstroke = 222 new ColoredStroke(StrokeWidth, HighlightColorChoice.getColor(), HighlightColorChoice.getColor()); 223 224 // draw the complete grid 225 Path2D.Double gridPath = new Path2D.Double(); 226 227 // draw horizontal grid lines 228 for (int r = 0; r < pnts.length; r++) { 229 gridPath.moveTo(pnts[r][0].getX(), pnts[r][0].getY()); 230 for (int c = 1; c < pnts[r].length; c++) { 231 gridPath.lineTo(pnts[r][c].getX(), pnts[r][c].getY()); 232 } 233 } 234 // draw vertical grid lines 235 for (int r = 0; r < pnts[0].length; r++) { 236 gridPath.moveTo(pnts[0][r].getX(), pnts[0][r].getY()); 237 for (int c = 1; c < pnts.length; c++) { 238 gridPath.lineTo(pnts[c][r].getX(), pnts[c][r].getY()); 239 } 240 } 241 // draw diagonal grid lines 242 if (ShowTriangles) { 243 for (int r = 0; r < pnts.length - 1; r++) { 244 for (int c = 0; c < pnts[0].length - 1; c++) { 245 gridPath.moveTo(pnts[r][c].getX(), pnts[r][c].getY()); 246 gridPath.lineTo(pnts[r + 1][c + 1].getX(), pnts[r + 1][c + 1].getY()); 247 } 248 } 249 } 250 ola.addShape(gridPath, pathstroke); 251 252 // draw the vertices 253 double rad = CatchRadius; 254 for (int r = 0; r < pnts.length; r++) { 255 for (int c = 0; c < pnts[r].length; c++) { 256 double x = pnts[r][c].getX(); 257 double y = pnts[r][c].getY(); 258 Ellipse2D.Double circle = new Ellipse2D.Double(x - rad, y - rad, 2 * rad, 2 * rad); 259 ola.addShape(circle, polystroke); 260 } 261 } 262 263 // mark the selected grid point 264 if (nodeSelected != null && HighlightSelection) { 265 Pnt2d ps = gridWarped[nodeSelected.x][nodeSelected.y]; 266 double xs = ps.getX(); 267 double ys = ps.getY(); 268 Ellipse2D.Double circle = new Ellipse2D.Double(xs - rad, ys - rad, 2 * rad, 2 * rad); 269 ola.addShape(circle, highlightstroke); 270 } 271 272 // mark the enclosing polygon (if exists) 273 if (trianglesSelected != null && HighlightSelection) { 274 for (Triangle t : trianglesSelected.trgls) { 275 ola.addShape(t, polystroke); 276 } 277 } 278 279 return ola.getOverlay(); 280 } 281 282 // move the currently selected grid point to new position 283 private void moveSelectedGridPoint(int xNew, int yNew) { 284 if (nodeSelected != null) { // && (ns.x >= 0) && (ns.x < Rows) && (ns.y >= 0) && (ns.y < Cols) 285 gridWarped[nodeSelected.x][nodeSelected.y] = Pnt2d.from(xNew, yNew); 286 } 287 } 288 289 private PntInt findGridPoint(PntInt xyClick) { 290 // only inner grid points may be selected, not the ones at the border! 291 for (int r = 1; r < gridWarped.length - 1; r++) { 292 for (int c = 1; c < gridWarped[r].length - 1; c++) { 293 double dist = xyClick.distance(gridWarped[r][c]); 294 if (dist <= CatchRadius) { 295 return PntInt.from(r, c); 296 } 297 } 298 } 299 return null; 300 } 301 302 private void updateGridSelection(PntInt xy) { 303 nodeSelected = findGridPoint(xy); 304 trianglesSelected = (nodeSelected == null) ? null : new TriangleGroup(nodeSelected); 305 } 306 307 // ----------------------------------------------------------------------------- 308 309 private void remapImage() { 310 updateTrianglesAll(trianglesWarped, gridWarped); 311 updateTrianglesSelected(); 312 updateAffineMappings(); 313 warpImage(); 314 } 315 316 private void warpImage() { 317 ImageProcessor ip = im.getProcessor(); 318 int width = ip.getWidth(); 319 int height = ip.getHeight(); 320 321 // iterate over all pixels: 322 for (int u = 0; u < width; u++) { 323 for (int v = 0; v < height; v++) { 324 Triangle tWarp = null; 325 326 // if there is a selection we only remap pixels inside 327 if (trianglesSelected != null) { 328 tWarp = trianglesSelected.findTriangle(u, v); // search only over enclosing polygon triangles 329 if (tWarp == null) 330 continue; 331 } 332 333 // otherwise (if no selection), we remap all pixels 334 else { 335 tWarp = findEnclosingGridTriangle(u, v); 336 } 337 338 // if enclosing triangle was found, remap pixel (u,v) 339 if (tWarp != null) { // containing triangle found 340 AffineMapping2D am = tWarp.getMapping(); 341 Pnt2d xy = am.applyTo(PntInt.from(u, v)); // source image position 342 int val = ipOrig.getPixelInterpolated(xy.getX(), xy.getY()); 343 ip.set(u, v, val); 344 } 345 } 346 } 347 } 348 349 private Triangle findEnclosingGridTriangle(double x, double y) { 350 for (int r = 0; r < Rows - 1; r++) { 351 for (int c = 0; c < Cols - 1; c++) { 352 for (int i = 0; i < 2; i++) { 353 Triangle t = trianglesWarped[r][c][i]; 354 if (t.contains(x, y)) { 355 return t; 356 } 357 } 358 } 359 } 360 return null; // no enclosing triangle found 361 } 362 363 private void updateTrianglesAll(Triangle[][][] theTriangles, Pnt2d[][] theGrid) { 364 for (int r = 0; r < Rows - 1; r++) { 365 for (int c = 0; c < Cols - 1; c++) { 366 Pnt2d p0 = theGrid[r][c]; 367 Pnt2d p1 = theGrid[r+1][c]; 368 Pnt2d p2 = theGrid[r+1][c+1]; 369 Pnt2d p3 = theGrid[r][c+1]; 370 theTriangles[r][c][0] = new Triangle(p0, p1, p2, r, c, 0); // triangle A 371 theTriangles[r][c][1] = new Triangle(p0, p2, p3, r, c, 1); // triangle B 372 } 373 } 374 } 375 376 private void updateTrianglesSelected() { 377 if (trianglesSelected != null) { 378 trianglesSelected.updateTriangles(); 379 } 380 } 381 382 private void updateAffineMappings() { 383 for (int r = 0; r < Rows - 1; r++) { 384 for (int c = 0; c < Cols - 1; c++) { 385 for (int i = 0; i < 2; i++) { 386 Triangle tOrig = trianglesOrig[r][c][i]; 387 Triangle tWarp = trianglesWarped[r][c][i]; 388 AffineMapping2D am = getAffineMapping(tWarp, tOrig); 389 tWarp.setMapping(am); 390 } 391 } 392 } 393 } 394 395 private AffineMapping2D getAffineMapping(Triangle tP, Triangle tQ) { 396 Pnt2d[] P = {tP.pa, tP.pb, tP.pc}; 397 Pnt2d[] Q = {tQ.pa, tQ.pb, tQ.pc}; 398 return AffineMapping2D.fromPoints(P, Q); 399 } 400 401 // ------------------------------------------------------------ 402 // EVENT HANDLING: 403 // ---------------------------------------------------------------- 404 405 // anonymous sub-class of MouseAdapter 406 private final MouseAdapter MA = new MouseAdapter() { 407 @Override 408 public void mousePressed(MouseEvent e) { 409 try { // right mouse button -> reset 410 if ((e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0) { 411 reset(); 412 } 413 else { // otherwise update the grid 414 updateGridSelection( 415 PntInt.from(canvas.offScreenX(e.getX()), canvas.offScreenY(e.getY()))); 416 } 417 redraw(); 418 e.consume(); 419 } catch (Exception ex) { 420 IJ.handleException(ex); 421 } 422 } 423 424 @Override 425 public void mouseReleased(MouseEvent e) { 426 try { 427 remapImage(); 428 redraw(); 429 if (e.getClickCount() == 2 && !e.isConsumed()) { 430 e.consume(); 431 } 432 } catch (Exception ex) { 433 IJ.handleException(ex); 434 } 435 } 436 437 @Override 438 public void mouseDragged(MouseEvent e) { 439 try { 440 int x = canvas.offScreenX(e.getX()); 441 int y = canvas.offScreenY(e.getY()); 442 if ((nodeSelected != null) && (trianglesSelected != null) && (trianglesSelected.contains(x, y))) { 443 moveSelectedGridPoint(x, y); 444 redraw(); 445 } 446 } catch (Exception ex) { 447 IJ.handleException(ex); 448 } 449 } 450 451 }; 452 453 // anonymous sub-class of KeyAdapter 454 private final KeyAdapter KA = new KeyAdapter() { 455 @Override 456 public void keyPressed(KeyEvent e) { 457 int keyCode = e.getKeyCode(); 458 // escape -> finish 459 if(keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_ENTER) { 460 finish(); 461 } 462 // ctrl/+ zoom in 463 else if(keyCode == KeyEvent.VK_PLUS && e.isControlDown()) { 464 canvas.zoomIn(im.getWidth()/2, im.getHeight()/2); 465 } 466 // ctrl/- zoom out 467 else if(keyCode == KeyEvent.VK_MINUS && e.isControlDown()) { 468 canvas.zoomOut(im.getWidth()/2, im.getHeight()/2); 469 } 470 } 471 }; 472 473 private void setupListeners() { 474 // remove current listeners and keep for later re-install 475 windowKeyListeners = removeKeyListeners(win); 476 canvasKeyListeners = removeKeyListeners(canvas); 477 canvasMouseListeners = removeMouseListeners(canvas); 478 canvasMouseMotionListeners = removeMouseMotionListeners(canvas); 479 480 canvas.addKeyListener(KA); 481 canvas.addMouseListener(MA); 482 canvas.addMouseMotionListener(MA); 483 484 win.addWindowListener(new WindowAdapter() { 485 @Override 486 public void windowClosing(WindowEvent e) { 487 finish(); 488 } 489 }); 490 491 canvas.requestFocus(); // important, otherwise key events have no effect!! 492 } 493 494 private void revertListeners() { 495 if (win != null) { 496 // remove this plugin's listener(s) 497 removeKeyListeners(win); 498 // install original listener(s) 499 addKeyListeners(win, windowKeyListeners); 500 } 501 502 if (canvas != null) { 503 // remove this plugin's listener(s) 504 removeKeyListeners(canvas); 505 removeMouseListeners(canvas); 506 removeMouseMotionListeners(canvas); 507 // install original listener(s) 508 addKeyListeners(canvas, canvasKeyListeners); 509 addMouseListeners(canvas, canvasMouseListeners); 510 addMouseMotionListeners(canvas, canvasMouseMotionListeners); 511 } 512 } 513 514 // ------------------------------------------------------ 515 516 private KeyListener[] removeKeyListeners(Component comp) { 517 KeyListener[] listeners = comp.getKeyListeners(); 518 for (KeyListener kl : comp.getKeyListeners()) { 519 comp.removeKeyListener(kl); 520 } 521 return listeners; 522 } 523 524 private MouseListener[] removeMouseListeners(Component comp) { 525 MouseListener[] listeners = comp.getMouseListeners(); 526 for (MouseListener ml : listeners) { 527 comp.removeMouseListener(ml); 528 } 529 return listeners; 530 } 531 532 private MouseMotionListener[] removeMouseMotionListeners(Component comp) { 533 MouseMotionListener[] listeners = comp.getMouseMotionListeners(); 534 for (MouseMotionListener ml : listeners) { 535 comp.removeMouseMotionListener(ml); 536 } 537 return listeners; 538 } 539 540 // ---------------- 541 542 private void addKeyListeners(Component comp, KeyListener[] listeners) { 543 if (comp == null || listeners == null) return; 544 for (KeyListener kl : listeners) { 545 comp.addKeyListener(kl); 546 } 547 } 548 549 private void addMouseListeners(Component comp, MouseListener[] listeners) { 550 if (comp == null || listeners == null) return; 551 for (MouseListener ml : listeners) { 552 comp.addMouseListener(ml); 553 } 554 } 555 556 private void addMouseMotionListeners(Component comp, MouseMotionListener[] listeners) { 557 if (comp == null || listeners == null) return; 558 for (MouseMotionListener ml : listeners) { 559 comp.addMouseMotionListener(ml); 560 } 561 } 562 563 // ------------------------------------------------------------------ 564 565 /** 566 * Represents a 2D triangle, used for point inclusion testing and 567 * affine mapping. 568 * @author WB 569 */ 570 @SuppressWarnings("serial") 571 class Triangle extends Path2D.Double { 572 573 final int row, col, id; 574 final Pnt2d pa, pb, pc; 575 576 private AffineMapping2D mapping = null; 577 578 AffineMapping2D getMapping() { 579 return this.mapping; 580 } 581 582 void setMapping(AffineMapping2D mapping) { 583 this.mapping = mapping; 584 } 585 586 Triangle(Pnt2d pa, Pnt2d pb, Pnt2d pc, int row, int col, int id) { 587 this.row = row; 588 this.col = col; 589 this.id = id; // = 0,1 590 this.pa = pa; 591 this.pb = pb; 592 this.pc = pc; 593 // make triangle path: 594 this.moveTo(pa.getX(), pa.getY()); 595 this.lineTo(pb.getX(), pb.getY()); 596 this.lineTo(pc.getX(), pc.getY()); 597 this.closePath(); 598 } 599 600// @Override 601// public String toString() { 602// return (String.format("Triangle[row=%d col=%d id=%d", this.row, this.col, this.id)); 603// } 604 } 605 606 /** 607 * This is the group of triangles associated with a particular grid point. 608 */ 609 class TriangleGroup { 610 final int r, c; // the grid point 611 private final Triangle[] trgls; 612 613 TriangleGroup(PntInt rc) { 614 this.r = rc.x; 615 this.c = rc.y; 616 this.trgls = new Triangle[6]; 617 updateTriangles(); 618 } 619 620 private void updateTriangles() { 621 trgls[0] = trianglesWarped[r-1][c-1][0]; 622 trgls[1] = trianglesWarped[r-1][c-1][1]; 623 trgls[2] = trianglesWarped[r-1][c][0]; 624 trgls[3] = trianglesWarped[r][c-1][1]; 625 trgls[4] = trianglesWarped[r][c][0]; 626 trgls[5] = trianglesWarped[r][c][1]; 627 } 628 629 private Triangle findTriangle(double x, double y) { 630 for (Triangle t : this.trgls) { 631 if (t.contains(x, y)) { 632 return t; 633 } 634 } 635 return null; 636 } 637 638 private boolean contains(double x, double y) { 639 return (findTriangle(x, y) != null); 640 } 641 } 642 643 // ------------------------------------------------------------------- 644 645 private boolean runDialog() { 646 GenericDialog gd = new GenericDialog(this.getClass().getSimpleName()); 647 gd.addHelp(getJavaDocUrl()); 648 gd.addMessage(DialogUtils.makeLineSeparatedString( 649 "How to use:", 650 " left mouse: select and drag grid points", 651 " right mouse: reset the grid", 652 " ctrl/+ key: zoom in\n", 653 " ctrl/- key: zoom out\n", 654 " enter or escape key: finish editing")); 655 656 gd.addNumericField("Number of grid rows", Rows, 0); 657 gd.addNumericField("Number of grid columns", Cols, 0); 658 gd.addEnumChoice("Grid stroke color", StrokeColorChoice); 659 gd.addEnumChoice("Point highlight color", HighlightColorChoice); 660 gd.addNumericField("Grid stroke width", StrokeWidth, 1); 661 gd.addNumericField("Catch radius", CatchRadius, 1); 662 gd.addCheckbox("Show grid triangles", ShowTriangles); 663 gd.addCheckbox("Highlight selection", HighlightSelection); 664 gd.addCheckbox("Remove overlay when done", RemoveOverlayWhenDone); 665 666 gd.showDialog(); 667 if (gd.wasCanceled()) 668 return false; 669 670 Rows = (int) gd.getNextNumber(); 671 Cols = (int) gd.getNextNumber(); 672 StrokeColorChoice = gd.getNextEnumChoice(BasicAwtColor.class); 673 HighlightColorChoice = gd.getNextEnumChoice(BasicAwtColor.class); 674 StrokeWidth = gd.getNextNumber(); 675 CatchRadius = gd.getNextNumber(); 676 ShowTriangles = gd.getNextBoolean(); 677 HighlightSelection = gd.getNextBoolean(); 678 RemoveOverlayWhenDone = gd.getNextBoolean(); 679 680 return true; 681 } 682}