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;
}
}