Login  Register

Re: magic wand

Posted by Michael Schmid on May 27, 2009; 7:46am
URL: http://imagej.273.s1.nabble.com/magic-wand-tp3692147p3692148.html

Hi all,

below is quickly written proof-of-concept wand tool that does  
Photoshop-like selections and also has the possibility to stop the  
selection based on the gradient.

It does not support the ALT and SHIFT keys so far, and has a few  
other restrictions. Also, the code needs polishing.

Note that the mailer may break up long lines, which may sometimes  
cause problems.

Michael
________________________________________________________________
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.plugin.*;
import ij.plugin.filter.ThresholdToSelection;
import ij.plugin.frame.Recorder;
import java.awt.*;

/** An ImageJ magic wand with selectable tolerance and gradient  
detection.
   * This PlugIn works on the gray value for RGB images
   *
   * Parameters
   * ==========
   *
   * Gray Value Tolerance:
   * The selection is expanded to all image points as long as the  
difference
   * between the gray level (brightness) of the point clicked and the
   * image point is less than the Gray Value Tolerance.
   *
   * Gradient Tolerance:
   * Irrespective of Gray Value Tolerance, the selection is not expanded
   * if the gray level gradient is larger than the Gradient Tolerance.
   *
   * Usage
   * =====
   * - When called from the plugins menu, the tool is installed in  
the ImageJ Toolbar
   *   This replaces all other custom tools, e..g those from the  
startup macro.
   * - Put the following into your ImageJ/macros/StartupMacros.txt file
   *   to have this Wand Tool as a standard tool:
         macro 'Wand Tool-Cf00Lee55O2233' {
             getCursorLoc(x, y, z, flags);
             call('Wand_Tool.mousePressed', x, y, flags);
         }
         macro 'Wand Tool Options' {
             call('Wand_Tool.showDialog');
         }"
   * Left-click the tool icon for selecting the tool
   * Right-click or double-click the tool icon for the options menu
   * where the tolerance can be selected
   *
   * To do:
   * - shift-click, alt-click
   * - include holes option
   * - fix exception on cancel dialog
   * - units for calibrated images, spatial calibration for gradient
   * - save roi
   * - preview (with last seed) in dialog
   *
   * Michael Schmid, 2009-May-25
   */

public class Wand_Tool implements PlugIn {

     static double toleranceGrayVal = 20;
     static double toleranceGrayGrad = 5;

     int width, height;
     int[] dirOffset, dirXoffset, dirYoffset;    // offsets of  
neighbor pixels for addressing
     ByteProcessor maskIp;
     int[] coordinates;
     int coordinateMask;


     public void run(String arg) {
         if (IJ.versionLessThan("1.39a")) return;
         if (IJ.isMacro()) {
             showDialog();
             return;
         }
         String macro =
             "macro 'Wand Tool-Cf00Lee55O2233' {\n" +
             "  getCursorLoc(x, y, z, flags);\n" +
             "  call('Wand_Tool.mousePressed', x, y, flags);\n"+
             "}\n" +
             "macro 'Wand Tool Options' {\n" +
             "  call('Wand_Tool.showDialog');\n"+
             "}";
         new MacroInstaller().install(macro);
     }

     public static void mousePressed(String xString, String yString,  
String flagString) {
         int x = Integer.parseInt(xString);
         int y = Integer.parseInt(yString);
         int flags = Integer.parseInt(flagString);
         ImagePlus imp = WindowManager.getCurrentImage();
         if (imp==null) return;
         imp.getWindow().setCursor(Cursor.CROSSHAIR_CURSOR);
         new Wand_Tool().doWand(imp, x, y, flags);
     }

     public static void showDialog() {
         boolean save = Recorder.recordInMacros;
         if (Recorder.record) {
             Recorder.recordInMacros = true;
             Recorder.setCommand("Wand Tool");
         }
         GenericDialog gd = new GenericDialog("Wand Tool Options");
         gd.addNumericField("Gray Value Tolerance", toleranceGrayVal,  
2);
         gd.addNumericField("Gradient Tolerance", toleranceGrayGrad, 2);
         gd.showDialog();
         if (!gd.wasCanceled()) {
             toleranceGrayVal = gd.getNextNumber();
             toleranceGrayGrad = gd.getNextNumber();
             if (Recorder.record) Recorder.saveCommand();
         }
         Recorder.recordInMacros = save;
     }

     void doWand(ImagePlus imp, int x0, int y0, int flags) {
         ImageProcessor ip = imp.getProcessor();
         prepare(ip);
         boolean useGradient = toleranceGrayGrad<toleranceGrayVal;
         float toleranceGrayGrad2 = (float)
(toleranceGrayGrad*toleranceGrayGrad);
         float grayRef = ip.getPixelValue(x0, y0);
         byte[] mPixels = (byte[])maskIp.getPixels();
         //simple flood fill algorithm
         int lastCoord = 0;
         int offset0 = x0 + y0*width;
         mPixels[offset0] = -1;
         coordinates[0] = offset0;
         for (int iCoord = 0; iCoord<=lastCoord; iCoord++) {
             int offset = coordinates[iCoord & coordinateMask];
             int x = offset % width;
             int y = offset / width;
             boolean isInner = (x!=0 && y!=0 && x!=(width-1) && y!=
(height-1));
             float v = ip.getPixelValue(x,y);
             boolean largeGradient = false;
             float xGradient=0, yGradient=0;
             if (useGradient) {
                 if (isInner) {
                     float vpp=ip.getPixelValue(x+1, y+1);
                     float vpm=ip.getPixelValue(x+1, y-1);
                     float vmp=ip.getPixelValue(x-1, y+1);
                     float vmm=ip.getPixelValue(x-1, y-1);

                     xGradient = 0.125f*(    //Sobel-filter like  
gradient
                             2f*(ip.getPixelValue(x+1, y)-
ip.getPixelValue(x-1, y))
                             + vpp-vmm +(vpm-vmp)); // v(x+1) - v(x-1)
                     yGradient = 0.125f*(
                             2f*(ip.getPixelValue(x, y+1)-
ip.getPixelValue(x, y-1))
                             + vpp-vmm -(vpm-vmp)); // v(y+1) - v(y-1)
                 } else {
                     int xCount=0, yCount=0;
                     for (int d=0; d<8; d++) if (isWithin(ip, x, y,  
d)) {
                         int x2 = x+dirXoffset[d];
                         int y2 = y+dirYoffset[d];
                         float v2 = ip.getPixelValue(x2, y2);
                         int weight = (2-(d&0x1));   //2 for  
straight, 1 for diag
                         xGradient += dirXoffset[d] * (v2-v) * weight;
                         xCount += weight * (dirXoffset[d]!=0 ? 1 : 0);
                         yGradient += dirYoffset[d] * (v2-v) * weight;
                         yCount += weight * (dirYoffset[d]!=0 ? 1 : 0);
                     }
                     xGradient /= xCount;
                     yGradient /= yCount;
                 }
                 largeGradient = xGradient*xGradient +  
yGradient*yGradient > toleranceGrayGrad2;
             }
             for (int d=0; d<8; d++) {           //analyze all  
neighbors (in 8 directions)
                 int offset2 = offset+dirOffset[d];
                 if ((isInner || isWithin(ip, x, y, d)) && mPixels
[offset2] == 0) {
                     int x2 = x+dirXoffset[d];
                     int y2 = y+dirYoffset[d];
                     float v2 = ip.getPixelValue(x2, y2);
                     if (v2>grayRef+toleranceGrayVal || v2<grayRef-
toleranceGrayVal) {
                         mPixels[offset2] = 1;   //out-of-bounds,  
don't analyze any more
                     } else if (!largeGradient || (v2-v)*
(xGradient*dirXoffset[d]+yGradient*dirYoffset[d])<=0) {
                         mPixels[offset2] = -1;  //add new point
                         if (lastCoord-iCoord > coordinateMask)
                             expandCoordinateArray();
                         lastCoord++;
                         coordinates[lastCoord & coordinateMask] =  
offset2;
                     }
                 }
             } //for direction d
             if ((iCoord&0xfff)==1) IJ.showProgress(iCoord/(double)
(width*height));
         } //for iCoord


         //convert mask to selection
         maskIp.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
         ThresholdToSelection tts = new ThresholdToSelection();
         tts.setup("", imp);
         tts.run(maskIp);
         IJ.showProgress(1.0);
         if (Recorder.record)
             Recorder.record("call","Wand_Tool.mousePressed\",\""+x0
+"\",\""+y0+"\",\""+flags);
     }

     /** Create static class variables:
      *  A mask, arrays of offsets within a pixel array for  
directions in clockwise order: 0=(x,y-1), 1=(x+1,y-1), ... 7=(x-1,y)
      */
     void prepare(ImageProcessor ip) {
         int width = ip.getWidth();
         int height = ip.getHeight();
         dirXoffset = new int[] {    0,      1,     1,     1,        
0,     -1,      -1,    -1    };
         dirYoffset = new int[] {   -1,     -1,     0,     1,        
1,      1,       0,    -1,   };
         dirOffset  = new int[] {-width, -width+1, +1, +width+1,  
+width, +width-1,   -1, -width-1 };
         maskIp = new ByteProcessor(width, height);
         coordinateMask = 0xfff;
         coordinates = new int[coordinateMask+1];
         this.height = height;
         this.width = width;
     }

     void expandCoordinateArray() {
         int newSize = 2*(coordinateMask+1);
         int newMask = newSize - 1;
         int[] newCoordinates = new int[newSize];
         System.arraycopy(coordinates, 0, newCoordinates, 0,  
coordinateMask+1);
         System.arraycopy(coordinates, 0, newCoordinates,  
coordinateMask+1, coordinateMask+1);
         coordinates = newCoordinates;
         coordinateMask = newMask;
     }

     /** returns whether the neighbor in a given direction is within  
the image
      * NOTE: it is assumed that the pixel x,y itself is within the  
image!
      * Uses class variables width, height: dimensions of the image
      * @param x         x-coordinate of the pixel that has a  
neighbor in the given direction
      * @param y         y-coordinate of the pixel that has a  
neighbor in the given direction
      * @param direction the direction from the pixel towards the  
neighbor (see makeDirectionOffsets)
      * @return          true if the neighbor is within the image  
(provided that x, y is within)
      */
     boolean isWithin(ImageProcessor ip, int x, int y, int direction) {
         int width = ip.getWidth();
         int height = ip.getHeight();
         int xmax = width - 1;
         int ymax = height -1;
         switch(direction) {
             case 0:
                 return (y>0);
             case 1:
                 return (x<xmax && y>0);
             case 2:
                 return (x<xmax);
             case 3:
                 return (x<xmax && y<ymax);
             case 4:
                 return (y<ymax);
             case 5:
                 return (x>0 && y<ymax);
             case 6:
                 return (x>0);
             case 7:
                 return (x>0 && y>0);
         }
         return false;   //to make the compiler happy :-)
     } // isWithin

     void clear (ByteProcessor maskIp) {
         byte[] pixels = (byte[])maskIp.getPixels();
         for (int i=0; i<pixels.length; i++)
             pixels[i] = 0;
     }
}