package ij.plugin.filter;
import ij.*;
import ij.plugin.*;
import ij.process.*;
import ij.gui.*;
/**
* This plugin implements the Euclidean Distance Map (EDM), Watershed,
* Ultimate Eroded Points and Voronoi commands in the Process/Binary submenu.
*
* - Euclidean Distance Map: The value of each pixel is the distance to the nearest
* background pixel (for background pixels, the EDM is 0)
* - Ultimate Eroded Points (UEPs) are maxima of the EDM. In the output, the points
* are assigned the EDM value, which is equal to the radius of the largest circle
* that fits into the particle, with the UEP as the center.
* - Watershed segmentation of the EDM splits particles at "necks"; starting at
* maxima of the EDM.
* - 'Voronoi' splits the image by lines of points having equal distance to the
* borders of the two nearest particles. Thus, the Voronoi cell of each particle
* includes all points that are nearer to this particle than any other particle.
* For the case of the priticles being single points, this is a Voronoi tessellation
* (also known as Dirichlet tessellation).
* In the output, the value inside the Voronoi cells is zero; the pixel values
* of the dividing lines between the cells are equal to the distance to the two
* nearest particles. This is similar to a medial axis transform of the background,
* but there are no lines in inner holes of particles.
*
* Watershed, Ultimate Eroded Points and Voronoi are handled by the MaximumFinder
* plugin applied to the EDM
* Note: These functions do not take ROIs into account.
* Setup is called with argument "" (empty string) for EDM,
* "watershed" for watershed segmentation, "points" for ultimate eroded points and
* "voronoi" for Voronoi segmentation of the background
*
* The EDM algorithm is similar to the 8SSEDT in
* F. Leymarie, M. D. Levine, in: CVGIP Image Understanding, vol. 55 (1992), pp 84-94
* http://dx.doi.org/10.1016/1049-9660(92)90008-Q
*
* The algorithm provides a fast approximation of the EDM, with the deviation from a
* full calculation being between -0.09 and 0. The algorithm is exact for distances<13.
* For d>=13, deviations from the true result can occur, but are very rare: typically
* the fraction of pixels deviating from the exact result is in the 10^-5 range, with
* most deviations between -0.03 and -0.04.
*
* Limitations:
* Maximum image diagonal for EDM: 46340 pixels (sqrt(2^31)); if the particles are
* dense enough it also works for width, height <=65534.
*
* Version 30-Apr-2008 Michael Schmid: more accurate EDM algorithm,
* 16-bit and float output possible,
* parallel processing for stacks
* Voronoi output added
* @see Adjustable Watershed plugin
*/
public class EDM implements ExtendedPlugInFilter {
/** Output type: overwrite current 8-bit image */
public static final int BYTE_OVERWRITE = 0;
/** Output type: new 8-bit image */
public static final int BYTE = 1;
/** Output type: new 16-bit image */
public static final int SHORT = 2;
/** Output type: new 32-bit image */
public static final int FLOAT = 3;
/** Unit in old make16bitEDM: this pixel value corresponds to a distance of one pixel.
* For compatibility only. */
public static final int ONE = 41;
/** In old make16bitEDM this pixel value corresponds to a pixel distance of sqrt(2) */
public static final int SQRT2 = 58; // ~ 41 * sqrt(2)
/** In old make16bitEDM this pixel value corresponds to a pixel distance of sqrt(5) */
public static final int SQRT5 = 92; // ~ 41 * sqrt(5)
private ImagePlus imp; //input
private ImagePlus outImp; //output if a new window is desired
private PlugInFilterRunner pfr; //needed to extract the stack slice if needed
private String command; //for showing status
private int outImageType; //output type; BYTE_OVERWRITE, BYTE, SHORT or FLOAT
private ImageStack outStack; //in case output should be a new stack
private int processType; //can be EDM, WATERSHED, UEP, VORONOI
private MaximumFinder maxFinder = new MaximumFinder(); //we use only one MaximumFinder (nice progress bar)
private double progressDone; //for progress bar, fraction of work done so far
private int nPasses; //for progress bar, how many images to process (sequentially or parallel threads)
private boolean interrupted; //whether watershed segmentation has been interrrupted by the user
private boolean background255; //whether background for EDM is 255, not zero
private int flags = DOES_8G | PARALLELIZE_STACKS | FINAL_PROCESSING;
//processType can be:
private static final int EDM = 0, WATERSHED = 1, UEP = 2, VORONOI = 3;
//whether MaximumFinder is needed for processType:
private static final boolean[] USES_MAX_FINDER = new boolean[] {
false, true, true, true };
//whether watershed segmentation is needed for processType:
private static final boolean[] USES_WATERSHED = new boolean[] {
false, true, false, true };
//prefixes for titles of separate output images; for each processType:
private static final String[] TITLE_PREFIX = new String[] {
"EDM of ", null, "UEPs of ", "Voronoi of "};
private static final int NO_POINT = -1; //no nearest point in array of nearest points
private static final double MAXFINDER_TOLERANCE = 0.5; //reasonable values are 0.3 ... 0.8;
//segmentation is more aggressive with smaller values
/** Output type (BYTE_OVERWRITE, BYTE, SHORT or FLOAT) */
private static int outputType = BYTE_OVERWRITE;
/** Prepare for processing; also called at the very end with argument 'final'
* to show any newly created output image.
*/
public int setup (String arg, ImagePlus imp) {
if (arg.equals("final")) {
showOutput();
return DONE;
}
this.imp = imp;
//'arg' is processing type; default is 'EDM' (0)
if (arg.equals("watershed")) {
processType = WATERSHED;
flags += KEEP_THRESHOLD;
} else if (arg.equals("points"))
processType = UEP;
else if (arg.equals("voronoi"))
processType = VORONOI;
//output type
if (processType != WATERSHED) //Watershed always has output BYTE_OVERWRITE=0
outImageType = outputType; //otherwise use the static variable from setOutputType
if (outImageType != BYTE_OVERWRITE)
flags |= NO_CHANGES;
//check image and prepare
if (imp != null) {
ImageProcessor ip = imp.getProcessor();
if (!ip.isBinary()) {
IJ.error("8-bit binary image (0 and 255) required.");
return DONE;
}
ip.resetRoi();
//processing routines assume background=0; image may be otherwise
boolean invertedLut = imp.isInvertedLut();
background255 = (invertedLut && Prefs.blackBackground) || (!invertedLut && !Prefs.blackBackground);
}
return flags;
} //public int setup
/** Called by the PlugInFilterRunner after setup.
* Asks the user in case of a stack and prepares a separate ouptut stack if required
*/
public int showDialog (ImagePlus imp, String command, PlugInFilterRunner pfr) {
this.pfr = pfr;
int width = imp.getWidth();
int height= imp.getHeight();
//ask whether to process all slices of stack & prepare stack
//(if required) for writing into it in parallel threads
flags = IJ.setupDialog(imp, flags);
if ((flags&DOES_STACKS)!=0 && outImageType!=BYTE_OVERWRITE) {
outStack = new ImageStack(width, height, imp.getStackSize());
maxFinder.setNPasses(imp.getStackSize());
}
return flags;
} //public int showDialog
/** Called by the PlugInFilterRunner to process the image or one frame of a stack */
public void run (ImageProcessor ip) {
if (interrupted) return;
int width = ip.getWidth();
int height = ip.getHeight();
int backgroundValue = (processType==VORONOI) ?
(background255 ? 0 : (byte)255) : //Voronoi needs EDM of the background
(background255 ? (byte)255 : 0); //all others do EDM of the foreground
if (USES_WATERSHED[processType]) nPasses = 0; //watershed has its own progress bar
FloatProcessor floatEdm = makeFloatEDM(ip, backgroundValue, false);
ByteProcessor maxIp = null;
if (USES_MAX_FINDER[processType]) {
if (processType == VORONOI) floatEdm.multiply(-1); //Voronoi starts from minima of EDM
int maxOutputType = USES_WATERSHED[processType] ? MaximumFinder.SEGMENTED : MaximumFinder.SINGLE_POINTS;
boolean isEDM = processType!=VORONOI;
maxIp = maxFinder.findMaxima(floatEdm, MAXFINDER_TOLERANCE,
ImageProcessor.NO_THRESHOLD, maxOutputType, false, isEDM);
if (maxIp == null) { //segmentation cancelled by user?
interrupted = true;
return;
} else if (processType != WATERSHED) {
if (processType == VORONOI) floatEdm.multiply(-1);
resetMasked(floatEdm, maxIp, processType == VORONOI ? -1 : 0);
}
}
ImageProcessor outIp = null;
if (processType==WATERSHED) {
if (background255) maxIp.invert();
ip.copyBits(maxIp, 0, 0, Blitter.COPY);
ip.setBinaryThreshold();
} else switch (outImageType) { //for all these, output contains the values of the EDM
case FLOAT:
outIp = floatEdm;
break;
case SHORT:
floatEdm.setMinAndMax(0., 65535.);
outIp = floatEdm.convertToShort(true);
break;
case BYTE:
floatEdm.setMinAndMax(0., 255.);
outIp = floatEdm.convertToByte(true);
break;
case BYTE_OVERWRITE:
ip.setPixels(0, floatEdm);
if (floatEdm.getMax() > 255.)
ip.resetMinAndMax(); //otherwise we have max of floatEdm
}
if (outImageType != BYTE_OVERWRITE) { //new output image
if (outStack==null) {
outImp = new ImagePlus(TITLE_PREFIX[processType]+imp.getShortTitle(), outIp);
} else
outStack.setPixels(outIp.getPixels(), pfr.getSliceNumber());
}
} //public void run
/** Prepare the progress bar.
* Without calling it or if nPasses=0, no progress bar will be shown.
* @param nPasses Number of images that this EDM will process.
*/
public void setNPasses (int nPasses) {
this.nPasses = nPasses;
progressDone = 0;
if (USES_MAX_FINDER[processType]) maxFinder.setNPasses(nPasses);
}
/** Converts a binary image into a 8-bit grayscale Euclidean Distance Map
* (EDM). Each foreground (nonzero) pixel in the binary image is
* assigned a value equal to its distance from the nearest
* background (zero) pixel.
*/
public void toEDM (ImageProcessor ip) {
ip.setPixels(0, makeFloatEDM(ip, 0, false));
ip.resetMinAndMax();
}
/** Do watershed segmentation based on the EDM of the
* foreground objects (nonzero pixels) in an 8-bit image.
* Particles are segmented by their shape; segmentation
* lines added are background pixels (value = 0);
*/
public void toWatershed (ImageProcessor ip) {
FloatProcessor floatEdm = makeFloatEDM(ip, 0, false);
ByteProcessor maxIp = maxFinder.findMaxima(floatEdm, MAXFINDER_TOLERANCE,
ImageProcessor.NO_THRESHOLD, MaximumFinder.SEGMENTED, false, true);
if (maxIp != null) ip.copyBits(maxIp, 0, 0, Blitter.AND);
}
/** Calculates a 16-bit grayscale Euclidean Distance Map for a binary 8-bit image.
* Each foreground (nonzero) pixel in the binary image is assigned a value equal to
* its distance from the nearest background (zero) pixel, multiplied by EDM.ONE.
* For compatibility with previous versions of ImageJ only.
*/
public ShortProcessor make16bitEDM (ImageProcessor ip) {
FloatProcessor floatEdm = makeFloatEDM(ip, 0, false);
floatEdm.setMinAndMax(0, 65535./ONE);
return (ShortProcessor)floatEdm.convertToShort(true);
}
/**
* Creates the Euclidian Distance Map of a (binary) byte image.
* @param ip The input image, not modified; must be a ByteProcessor.
* @param backgroundValue Pixels in the input with this value are interpreted as background.
* Note: for pixel value 255, write either -1 or (byte)255.
* @param edgesAreBackground Whether out-of-image pixels are considered background
* @return The EDM, containing the distances to the nearest background pixel.
* Returns null if the thread is interrupted.
*/
public FloatProcessor makeFloatEDM (ImageProcessor ip, int backgroundValue, boolean edgesAreBackground) {
int width = ip.getWidth();
int height = ip.getHeight();
FloatProcessor fp = new FloatProcessor(width, height);
byte[] bPixels = (byte[])ip.getPixels();
float[] fPixels = (float[])fp.getPixels();
final int progressInterval = 100;
int nProgressUpdates = height/progressInterval; //how often the progress bar is updated when passing once through y range
double progressAddendum = (nProgressUpdates>0) ? 0.5/nProgressUpdates : 0;
for (int i=0; i=0; y--) {
if (edgesAreBackground) yDist = height-y;
edmLine(bPixels, fPixels, pointBufs, width, y*width, y, backgroundValue, yDist);
if (y%progressInterval == 0) {
if (Thread.currentThread().isInterrupted()) return null;
addProgress(progressAddendum);
}
}
fp.sqrt();
return fp;
} //public FloatProcessor makeFloatEDM
// Handle a line; two passes: left-to-right and right-to-left
private void edmLine(byte[] bPixels, float[] fPixels, int[][] pointBufs, int width,
int offset, int y, int backgroundValue, int yDist) {
int[] points = pointBufs[0]; // the buffer for the left-to-right pass
int pPrev = NO_POINT;
int pDiag = NO_POINT; // point at (-/+1, -/+1) to current one (-1,-1 in the first pass)
int pNextDiag;
boolean edgesAreBackground = yDist != Integer.MAX_VALUE;
int distSqr = Integer.MAX_VALUE; // this value is used only if edges are not background
for (int x=0; x dist2) fPixels[offset] = dist2;
}
pPrev = points[x];
pDiag = pNextDiag;
}
offset--; //now points to the last pixel in the line
points = pointBufs[1]; // the buffer for the right-to-left pass. Low short contains x, high short y
pPrev = NO_POINT;
pDiag = NO_POINT;
for (int x=width-1; x>=0; x--, offset--) {
pNextDiag = points[x];
if (bPixels[offset] == backgroundValue) {
points[x] = x | y<<16; // remember coordinates as a candidate for nearest background point
} else { // foreground pixel:
if (edgesAreBackground)
distSqr = (width-x < yDist) ? (width-x)*(width-x) : yDist*yDist;
float dist2 = minDist2(points, pPrev, pDiag, x, y, distSqr);
if (fPixels[offset] > dist2) fPixels[offset] = dist2;
}
pPrev = points[x];
pDiag = pNextDiag;
}
} //private void edmLine
// Calculates minimum distance^2 of x,y from the following three points:
// - points[x] (nearest point found for previous line, same x)
// - pPrev (nearest point found for same line, previous x), and
// - pDiag (nearest point found for diagonal, i.e., previous line, previous x)
// Sets array element points[x] to the coordinates of the point having the minimum distance to x,y
// If the distSqr parameter is lower than the distance^2, then distSqr is used
// Returns to the minimum distance^2 obtained
private float minDist2 (int[] points, int pPrev, int pDiag, int x, int y, int distSqr) {
int p0 = points[x]; // the nearest background point for the same x in the previous line
int nearestPoint = p0;
if (p0 != NO_POINT) {
int x0 = p0& 0xffff; int y0 = (p0>>16)&0xffff;
int dist1Sqr = (x-x0)*(x-x0)+(y-y0)*(y-y0);
if (dist1Sqr < distSqr)
distSqr = dist1Sqr;
}
if (pDiag!=p0 && pDiag!=NO_POINT) {
int x1 = pDiag&0xffff; int y1 = (pDiag>>16)&0xffff;
int dist1Sqr = (x-x1)*(x-x1)+(y-y1)*(y-y1);
if (dist1Sqr < distSqr) {
nearestPoint = pDiag;
distSqr = dist1Sqr;
}
}
if (pPrev!=pDiag && pPrev!=NO_POINT) {
int x1 = pPrev& 0xffff; int y1 = (pPrev>>16)&0xffff;
int dist1Sqr = (x-x1)*(x-x1)+(y-y1)*(y-y1);
if (dist1Sqr < distSqr) {
nearestPoint = pPrev;
distSqr = dist1Sqr;
}
}
points[x] = nearestPoint;
return (float)distSqr;
} //private float minDist2
// overwrite ip with floatEdm converted to bytes
private void byteFromFloat(ImageProcessor ip, FloatProcessor floatEdm) {
int width = ip.getWidth();
int height = ip.getHeight();
byte[] bPixels = (byte[])ip.getPixels();
float[] fPixels = (float[])floatEdm.getPixels();
for (int i=0; iFLOAT)
throw new IllegalArgumentException("Invalid type: "+type);
outputType = type;
}
/** Returns the current output type (BYTE_OVERWRITE, BYTE, SHORT or FLOAT) */
public static int getOutputType() {
return outputType;
}
}