package ij.gui;
import ij.*;
import ij.process.*;
import java.awt.*;
/** This class implements ImageJ's wand (tracing) tool.
* The wand selects pixels of equal or similar value or thresholded pixels
* forming a contiguous area.
* The wand creates selections that have only one boundary line (inner holes
* are not excluded from the selection). There may be holes at the boundary,
* however, if the boundary line touches the same vertex twice (both in
* 4-connected and 8-connected mode).
*
* Version 2009-06-01 (code refurbished; tolerance, 4- & 8-connected options added)
*/
public class Wand {
/** Wand operation type: trace outline of 4-connected pixels */
public final static int FOUR_CONNECTED = 4;
/** Wand operation type: trace outline of 8-connected pixels */
public final static int EIGHT_CONNECTED = 8;
/** Wand operation type similar to that of ImageJ 1.42p and before; for backwards
* compatibility.
* In this mode, no checking is done whether the foreground or the background
* gets selected; four- or 8-connected behaviour depends on foreground/background
* and (if no selection) on whether the initial pixel is on a 1-pixel wide line. */
public final static int LEGACY_MODE = 1;
/** The number of points in the generated outline. */
public int npoints;
private int maxPoints = 1000; // will be increased if necessary
/** The x-coordinates of the points in the outline.
A vertical boundary at x separates the pixels at x-1 and x. */
public int[] xpoints = new int[maxPoints];
/** The y-coordinates of the points in the outline.
A horizontal boundary at y separates the pixels at y-1 and y. */
public int[] ypoints = new int[maxPoints];
private final static int THRESHOLDED_MODE = 256; //work on threshold
private ImageProcessor ip;
private byte[] bpixels;
private int[] cpixels;
private short[] spixels;
private float[] fpixels;
private int width, height;
private float lowerThreshold, upperThreshold;
private int xmin; //of selection created
private boolean exactPixelValue; //For color, match RGB, not gray value
private static boolean allPoints;
/** Constructs a Wand object from an ImageProcessor. */
public Wand(ImageProcessor ip) {
this.ip = ip;
Object pixels = ip.getPixels();
if (pixels instanceof byte[])
bpixels = (byte[])pixels;
else if (pixels instanceof int[])
cpixels = (int[])pixels;
else if (pixels instanceof short[])
spixels = (short[])pixels;
else if (pixels instanceof float[])
fpixels = (float[])pixels;
width = ip.getWidth();
height = ip.getHeight();
}
/** Traces an object defined by lower and upper threshold values.
* 'mode' can be FOUR_CONNECTED or EIGHT_CONNECTED.
* ('LEGACY_MODE' is also supported and may result in selection of
* interior holes instead of the thresholded area if one clicks left
* of an interior hole).
* The start coordinates must be inside the area or left of it.
* When successful, npoints>0 and the boundary points can be accessed
* in the public xpoints and ypoints fields. */
public void autoOutline(int startX, int startY, double lower, double upper, int mode) {
lowerThreshold = (float)lower;
upperThreshold = (float)upper;
autoOutline(startX, startY, 0.0, mode|THRESHOLDED_MODE);
}
/** Traces an object defined by lower and upper threshold values or an
* interior hole; whatever is found first ('legacy mode').
* For compatibility with previous versions of ImageJ.
* The start coordinates must be inside the area or left of it.
* When successful, npoints>0 and the boundary points can be accessed
* in the public xpoints and ypoints fields. */
public void autoOutline(int startX, int startY, double lower, double upper) {
autoOutline(startX, startY, lower, upper, THRESHOLDED_MODE|LEGACY_MODE);
}
/** This is a variation of legacy autoOutline that uses int threshold arguments. */
public void autoOutline(int startX, int startY, int lower, int upper) {
autoOutline(startX, startY, (double)lower, (double)upper, THRESHOLDED_MODE|LEGACY_MODE);
}
/** Traces the boundary of an area of uniform color, where
* 'startX' and 'startY' are somewhere inside the area.
* When successful, npoints>0 and the boundary points can be accessed
* in the public xpoints and ypoints fields.
* For compatibility with previous versions of ImageJ only; otherwise
* use the reliable method specifying 4-connected or 8-connected mode
* and the tolerance. */
public void autoOutline(int startX, int startY) {
autoOutline(startX, startY, 0.0, LEGACY_MODE);
}
/** Traces the boundary of the area with pixel values within
* 'tolerance' of the value of the pixel at the starting location.
* 'tolerance' is in uncalibrated units.
* 'mode' can be FOUR_CONNECTED or EIGHT_CONNECTED.
* Mode LEGACY_MODE is for compatibility with previous versions of ImageJ;
* ignored if tolerance > 0.
* Mode bit THRESHOLDED_MODE for internal use only; it is set by autoOutline
* with 'upper' and 'lower' arguments.
* When successful, npoints>0 and the boundary points can be accessed
* in the public xpoints and ypoints fields. */
public void autoOutline(int startX, int startY, double tolerance, int mode) {
if (startX<0 || startX>=width || startY<0 || startY>=height) return;
if (fpixels!=null && Float.isNaN(getPixel(startX, startY))) return;
exactPixelValue = tolerance==0;
boolean thresholdMode = (mode & THRESHOLDED_MODE) != 0;
boolean legacyMode = (mode & LEGACY_MODE) != 0 && tolerance == 0;
if (!thresholdMode) {
double startValue = getPixel(startX, startY);
lowerThreshold = (float)(startValue - tolerance);
upperThreshold = (float)(startValue + tolerance);
}
int x = startX;
int y = startY;
int seedX; // the first inside pixel
if (inside(x,y)) { // find a border when coming from inside
seedX = x; // (seedX, startY) is an inside pixel
do {x++;} while (inside(x,y));
} else { // find a border when coming from outside (thresholded only)
do {
x++;
if (x>=width) return; // no border found
} while (!inside(x,y));
seedX = x;
}
boolean fourConnected;
if (legacyMode)
fourConnected = !thresholdMode && !(isLine(x, y));
else
fourConnected = (mode & FOUR_CONNECTED) != 0;
//now, we have a border between (x-1, y) and (x,y)
boolean first = true;
while (true) { // loop until we have not traced an inner hole
boolean insideSelected = traceEdge(x, y, fourConnected);
if (legacyMode) return; // in legacy mode, don't care what we have got
if (insideSelected) { // not an inner hole
if (first) return; // started at seed, so we got it (sucessful)
if (xmin<=seedX) { // possibly the correct particle
Polygon poly = new Polygon(xpoints, ypoints, npoints);
if (poly.contains(seedX, startY))
return; // successful, particle contains seed
}
}
first = false;
// we have traced an inner hole or the wrong particle
if (!inside(x,y)) do {
x++; // traverse the hole
if (x>width) throw new RuntimeException("Wand Malfunction"); //should never happen
} while (!inside(x,y));
do {x++;} while (inside(x,y)); //retry here; maybe no inner hole any more
}
}
/* Trace the outline, starting at a point (startX, startY).
* Pixel (startX-1, startY) must be outside, (startX, startY) must be inside,
* or reverse. Otherwise an endless loop will occur (and eat up all memory).
* Traces 8-connected inside pixels unless fourConnected is true.
* Returns whether the selection created encloses an 'inside' area
* and not an inner hole.
*/
private boolean traceEdge(int startX, int startY, boolean fourConnected) {
// Let us name the crossings between 4 pixels vertices, then the
// vertex (x,y) marked with '+', is between pixels (x-1, y-1) and (x,y):
//
// pixel x-1 x
// y-1 |
// ----+----
// y |
//
// The four principal directions are numbered such that the direction
// number * 90 degrees gives the angle in the mathematical sense; and
// the directions to the adjacent pixels (for inside(x,y,direction) are
// at (number * 90 - 45) degrees:
// walking pixel
// directions: 1 directions: 2 | 1
// 2 + 0 ----+----
// 3 3 | 0
//
// Directions, like angles, are cyclic; direction -1 = direction 3, etc.
//
// The algorithm: We walk along the border, from one vertex to the next,
// with the outside pixels always being at the left-hand side.
// For 8-connected tracing, we always trying to turn left as much as
// possible, to encompass an area as large as possible.
// Thus, when walking in direction 1 (up, -y), we start looking
// at the pixel in direction 2; if it is inside, we proceed in this
// direction (left); otherwise we try with direction 1 (up); if pixel 1
// is not inside, we must proceed in direction 0 (right).
//
// 2 | 1 (i=inside, o=outside)
// direction 2 < ---+---- > direction 0
// o | i
// ^ direction 1 = up = starting direction
//
// For 4-connected pixels, we try to go right as much as possible:
// First try with pixel 1; if it is outside we go in direction 0 (right).
// Otherwise, we examine pixel 2; if it is outside, we go in
// direction 1 (up); otherwise in direction 2 (left).
//
// When moving a closed loop, 'direction' gets incremented or decremented
// by a total of 360 degrees (i.e., 4) for counterclockwise and clockwise
// loops respectively. As the inside pixels are at the right side, we have
// got an outline of inner pixels after a cw loop (direction decremented
// by 4).
//
npoints = 0;
xmin = width;
final int startDirection;
if (inside(startX,startY)) // inside at left, outside right
startDirection = 1; // starting in direction 1 = up
else {
startDirection = 3; // starting in direction 3 = down
startY++; // continue after the boundary that has direction 3
}
int x = startX;
int y = startY;
int direction = startDirection;
do {
int newDirection;
if (fourConnected) {
newDirection = direction;
do {
if (!inside(x, y, newDirection)) break;
newDirection++;
} while (newDirection < direction+2);
newDirection--;
} else { // 8-connected
newDirection = direction + 1;
do {
if (inside(x, y, newDirection)) break;
newDirection--;
} while (newDirection >= direction);
}
if (allPoints || newDirection!=direction)
addPoint(x,y); // a corner point of the outline polygon: add to list
switch (newDirection & 3) { // '& 3' is remainder modulo 4
case 0: x++; break;
case 1: y--; break;
case 2: x--; break;
case 3: y++; break;
}
direction = newDirection;
} while (x!=startX || y!=startY || (direction&3)!=startDirection);
if (allPoints || xpoints[0]!=x) // if the start point = end point is a corner: add to list
addPoint(x, y);
return (direction <= 0); // if we have done a clockwise loop, inside pixels are enclosed
}
// add a point x,y to the outline polygon
private void addPoint (int x, int y) {
if (npoints==maxPoints) {
int[] xtemp = new int[maxPoints*2];
int[] ytemp = new int[maxPoints*2];
System.arraycopy(xpoints, 0, xtemp, 0, maxPoints);
System.arraycopy(ypoints, 0, ytemp, 0, maxPoints);
xpoints = xtemp;
ypoints = ytemp;
maxPoints *= 2;
}
xpoints[npoints] = x;
ypoints[npoints] = y;
npoints++;
if (xmin > x) xmin = x;
}
// check pixel at (x,y), whether it is inside traced area
private boolean inside(int x, int y) {
if (x<0 || x>=width || y<0 || y>=height)
return false;
float value = getPixel(x, y);
return value>=lowerThreshold && value<=upperThreshold;
}
// check pixel in a given direction from vertex (x,y)
private boolean inside(int x, int y, int direction) {
switch(direction & 3) { // '& 3' is remainder modulo 4
case 0: return inside(x, y);
case 1: return inside(x, y-1);
case 2: return inside(x-1, y-1);
case 3: return inside(x-1, y);
}
return false; //will never occur, needed for the compiler
}
// get a pixel value; returns Float.NaN if outside the field.
private float getPixel(int x, int y) {
if (x<0 || x>=width || y<0 || y>=height)
return Float.NaN;
if (bpixels!=null)
return bpixels[y*width + x] & 0xff;
else if (spixels!=null)
return spixels[y*width + x] & 0xffff;
else if (fpixels!=null)
return fpixels[y*width + x];
else if (exactPixelValue) //RGB for exact match
return cpixels[y*width + x] & 0xffffff; //don't care for upper byte
else //gray value of RGB
return ip.getPixelValue(x,y);
}
/* Are we tracing a one pixel wide line? Makes Legacy mode 8-connected instead of 4-connected */
private boolean isLine(int xs, int ys) {
int r = 5;
int xmin=xs;
int xmax=xs+2*r;
if (xmax>=width) xmax=width-1;
int ymin=ys-r;
if (ymin<0) ymin=0;
int ymax=ys+r;
if (ymax>=height) ymax=height-1;
int area = 0;
int insideCount = 0;
for (int x=xmin; (x<=xmax); x++)
for (int y=ymin; y<=ymax; y++) {
area++;
if (inside(x,y))
insideCount++;
}
if (IJ.debugMode)
IJ.log((((double)insideCount)/area<0.25?"line ":"blob ")+insideCount+" "+area+" "+IJ.d2s(((double)insideCount)/area));
return ((double)insideCount)/area<0.25;
}
public static void setAllPoints(boolean b) {
allPoints = b;
}
public static boolean allPoints() {
return allPoints;
}
}