package ij.plugin.filter; import ij.plugin.filter.*; import ij.*; import ij.gui.*; import ij.measure.*; import ij.process.*; import java.awt.*; import java.util.*; /** This ImageJ plug-in filter finds the maxima (or minima) of an image. * It can create a mask where the local maxima of the current image are * marked (255; unmarked pixels 0). * The plug-in can also create watershed-segmented particles: Assume a * landscape of inverted heights, i.e., maxima of the image are now water sinks. * For each point in the image, the sink that the water goes to determines which * particle it belongs to. * When finding maxima (not minima), pixels with a level below the lower threshold * can be left unprocessed. * * Except for segmentation, this plugin works with ROIs, including non-rectangular ROIs. * Since this plug-in creates a separate output image it processes * only single images or slices, no stacks. * * Notes: * - When using one instance of MaximumFinder for more than one image in parallel threads, * all must images have the same width and height. * * version 09-Nov-2006 Michael Schmid * version 21-Nov-2006 Wayne Rasband. Adds "Display Point Selection" option and "Count" output type. * version 28-May-2007 Michael Schmid. Preview added, bugfix: minima of calibrated images, uses Arrays.sort * version 07-Aug-2007 Fixed a bug that could delete particles when doing watershed segmentation of an EDM. * version 21-Apr-2007 Adapted for float instead of 16-bit EDM; correct progress bar on multiple calls * version 05-May-2009 Works for images>32768 pixels in width or height * version 01-Nov-2009 Bugfix: extra lines in segmented output eliminated; watershed is also faster now * Maximum points encoded in long array for sorting instead of separete objects that need gc * New output type 'List' * version 22-May-2011 Bugfix: Maximum search in EDM and float images with large dynamic range could omit maxima */ public class MaximumFinder implements ExtendedPlugInFilter, DialogListener { //filter params /** maximum height difference between points that are not counted as separate maxima */ private static double tolerance = 10; /** Output type single points */ public final static int SINGLE_POINTS=0; /** Output type all points around the maximum within the tolerance */ public final static int IN_TOLERANCE=1; /** Output type watershed-segmented image */ public final static int SEGMENTED=2; /** Do not create image, only mark points */ public final static int POINT_SELECTION=3; /** Do not create an image, just list x, y of maxima in the Results table */ public final static int LIST=4; /** Do not create an image, just count maxima and add count to Results table */ public final static int COUNT=5; /** what type of output to create (see constants above)*/ private static int outputType; /** what type of output to create was chosen in the dialog (see constants above)*/ private static int dialogOutputType = POINT_SELECTION; /** output type names */ final static String[] outputTypeNames = new String[] {"Single Points", "Maxima Within Tolerance", "Segmented Particles", "Point Selection", "List", "Count"}; /** whether to exclude maxima at the edge of the image*/ private static boolean excludeOnEdges; /** whether to accept maxima only in the thresholded height range*/ private static boolean useMinThreshold; /** whether to find darkest points on light background */ private static boolean lightBackground; private ImagePlus imp; // the ImagePlus of the setup call private int flags = DOES_ALL|NO_CHANGES|NO_UNDO;// the flags (see interfaces PlugInFilter & ExtendedPlugInFilter) private boolean thresholded; // whether the input image has a threshold private boolean roiSaved; // whether the filter has changed the roi and saved the original roi private boolean previewing; // true while dialog is displayed (processing for preview) private Vector checkboxes; // a reference to the Checkboxes of the dialog private boolean thresholdWarningShown = false; // whether the warning "can't find minima with thresholding" has been shown private Label messageArea; // reference to the textmessage area for displaying the number of maxima private double progressDone; // for progress bar, fraction of work done so far private int nPasses = 0; // for progress bar, how many images to process (sequentially or parallel threads) //the following are class variables for having shorter argument lists private int width, height; // image dimensions private int intEncodeXMask; // needed for encoding x & y in a single int (watershed): mask for x private int intEncodeYMask; // needed for encoding x & y in a single int (watershed): mask for y private int intEncodeShift; // needed for encoding x & y in a single int (watershed): shift of y /** directions to 8 neighboring pixels, clockwise: 0=North (-y), 1=NE, 2=East (+x), ... 7=NW */ private int[] dirOffset; // pixel offsets of neighbor pixels for direct addressing final static int[] DIR_X_OFFSET = new int[] { 0, 1, 1, 1, 0, -1, -1, -1 }; final static int[] DIR_Y_OFFSET = new int[] { -1, -1, 0, 1, 1, 1, 0, -1 }; /** the following constants are used to set bits corresponding to pixel types */ final static byte MAXIMUM = (byte)1; // marks local maxima (irrespective of noise tolerance) final static byte LISTED = (byte)2; // marks points currently in the list final static byte PROCESSED = (byte)4; // marks points processed previously final static byte MAX_AREA = (byte)8; // marks areas near a maximum, within the tolerance final static byte EQUAL = (byte)16; // marks contigous maximum points of equal level final static byte MAX_POINT = (byte)32; // marks a single point standing for a maximum final static byte ELIMINATED = (byte)64; // marks maxima that have been eliminated before watershed /** type masks corresponding to the output types */ final static byte[] outputTypeMasks = new byte[] {MAX_POINT, MAX_AREA, MAX_AREA}; final static float SQRT2 = 1.4142135624f; /** Method to return types supported * @param arg Not used by this plugin-filter * @param imp The image to be filtered * @return Code describing supported formats etc. * (see ij.plugin.filter.PlugInFilter & ExtendedPlugInFilter) */ public int setup(String arg, ImagePlus imp) { this.imp = imp; return flags; } public int showDialog(ImagePlus imp, String command, PlugInFilterRunner pfr) { ImageProcessor ip = imp.getProcessor(); ip.resetBinaryThreshold(); // remove invisible threshold set by MakeBinary and Convert to Mask thresholded = ip.getMinThreshold()!=ImageProcessor.NO_THRESHOLD; GenericDialog gd = new GenericDialog(command); int digits = (ip instanceof FloatProcessor)?2:0; String unit = (imp.getCalibration()!=null)?imp.getCalibration().getValueUnit():null; unit = (unit==null||unit.equals("Gray Value"))?":":" ("+unit+"):"; gd.addNumericField("Noise tolerance"+unit,tolerance, digits); gd.addChoice("Output type:", outputTypeNames, outputTypeNames[dialogOutputType]); gd.addCheckbox("Exclude edge maxima", excludeOnEdges); if (thresholded) gd.addCheckbox("Above lower threshold", useMinThreshold); gd.addCheckbox("Light background", lightBackground); gd.addPreviewCheckbox(pfr, "Preview point selection"); gd.addMessage(" "); //space for number of maxima messageArea = (Label)gd.getMessage(); gd.addDialogListener(this); checkboxes = gd.getCheckboxes(); previewing = true; gd.addHelp(IJ.URL+"/docs/menus/process.html#find-maxima"); gd.showDialog(); //input by the user (or macro) happens here if (gd.wasCanceled()) return DONE; previewing = false; if (!dialogItemChanged(gd, null)) //read parameters return DONE; IJ.register(this.getClass()); //protect static class variables (parameters) from garbage collection return flags; } // boolean showDialog /** Read the parameters (during preview or after showing the dialog) */ public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) { tolerance = gd.getNextNumber(); if (tolerance<0) tolerance = 0; dialogOutputType = gd.getNextChoiceIndex(); outputType = previewing ? POINT_SELECTION : dialogOutputType; excludeOnEdges = gd.getNextBoolean(); if (thresholded) useMinThreshold = gd.getNextBoolean(); else useMinThreshold = false; lightBackground = gd.getNextBoolean(); boolean invertedLut = imp.isInvertedLut(); if (useMinThreshold && ((invertedLut&&!lightBackground) || (!invertedLut&&lightBackground))) { if (!thresholdWarningShown) if (!IJ.showMessageWithCancel( "Find Maxima", "\"Above Lower Threshold\" option cannot be used\n"+ "when finding minima (image with light background\n"+ "or image with dark background and inverting LUT).") && !previewing) return false; // if faulty input is not detected during preview, "cancel" quits thresholdWarningShown = true; useMinThreshold = false; ((Checkbox)(checkboxes.elementAt(1))).setState(false); //reset "Above Lower Threshold" checkbox } if (!gd.getPreviewCheckbox().getState()) messageArea.setText(""); // no "nnn Maxima" message when not previewing return (!gd.invalidNumber()); } // public boolean DialogItemChanged /** Set his to the number of images to process (for the watershed progress bar only). * Don't call or set nPasses to zero if no progress bar is desired. */ public void setNPasses(int nPasses) { this.nPasses = nPasses; } /** The plugin is inferred from ImageJ by this method * @param ip The image where maxima (or minima) should be found */ public void run(ImageProcessor ip) { Roi roi = imp.getRoi(); if (outputType == POINT_SELECTION && !roiSaved) { imp.saveRoi(); // save previous selection so user can restore it roiSaved = true; } if (roi!=null && (!roi.isArea() || outputType==SEGMENTED)) { imp.killRoi(); roi = null; } boolean invertedLut = imp.isInvertedLut(); double threshold = useMinThreshold?ip.getMinThreshold():ImageProcessor.NO_THRESHOLD; if ((invertedLut&&!lightBackground) || (!invertedLut&&lightBackground)) { threshold = ImageProcessor.NO_THRESHOLD; //don't care about threshold when finding minima float[] cTable = ip.getCalibrationTable(); ip = ip.duplicate(); if (cTable==null) { //invert image for finding minima of uncalibrated images ip.invert(); } else { //we are using getPixelValue, so the CalibrationTable must be inverted float[] invertedCTable = new float[cTable.length]; for (int i=cTable.length-1; i>=0; i--) invertedCTable[i] = -cTable[i]; ip.setCalibrationTable(invertedCTable); } ip.setRoi(roi); } ByteProcessor outIp = null; outIp = findMaxima(ip, tolerance, threshold, outputType, excludeOnEdges, false); //process the image if (outIp == null) return; //cancelled by user or previewing or no output image if (!Prefs.blackBackground) //normally, output has an inverted LUT, "active" pixels black (255) - like a mask outIp.invertLut(); String resultName; if (outputType == SEGMENTED) //Segmentation required resultName = " Segmented"; else resultName = " Maxima"; String outname = imp.getTitle(); if (imp.getNSlices()>1) outname += "("+imp.getCurrentSlice()+")"; outname += resultName; if (WindowManager.getImage(outname)!=null) outname = WindowManager.getUniqueName(outname); ImagePlus maxImp = new ImagePlus(outname, outIp); Calibration cal = imp.getCalibration().copy(); cal.disableDensityCalibration(); maxImp.setCalibration(cal); //keep the spatial calibration maxImp.show(); } //public void run /** Here the processing is done: Find the maxima of an image (does not find minima). * @param ip The input image * @param tolerance Height tolerance: maxima are accepted only if protruding more than this value * from the ridge to a higher maximum * @param threshold minimum height of a maximum (uncalibrated); for no minimum height set it to * ImageProcessor.NO_THRESHOLD * @param outputType What to mark in output image: SINGLE_POINTS, IN_TOLERANCE or SEGMENTED. * No output image is created for output types POINT_SELECTION, LIST and COUNT. * @param excludeOnEdges Whether to exclude edge maxima * @param isEDM Whether the image is a float Euclidian Distance Map * @return A new byteProcessor with a normal (uninverted) LUT where the marked points * are set to 255 (Background 0). Pixels outside of the roi of the input ip are not set. * Returns null if outputType does not require an output or if cancelled by escape */ public ByteProcessor findMaxima(ImageProcessor ip, double tolerance, double threshold, int outputType, boolean excludeOnEdges, boolean isEDM) { if (dirOffset == null) makeDirectionOffsets(ip); Rectangle roi = ip.getRoi(); byte[] mask = ip.getMaskArray(); if (threshold!=ImageProcessor.NO_THRESHOLD && ip.getCalibrationTable()!=null && threshold>0 && threshold<ip.getCalibrationTable().length) threshold = ip.getCalibrationTable()[(int)threshold]; //convert threshold to calibrated ByteProcessor typeP = new ByteProcessor(width, height); //will be a notepad for pixel types byte[] types = (byte[])typeP.getPixels(); float globalMin = Float.MAX_VALUE; float globalMax = -Float.MAX_VALUE; for (int y=roi.y; y<roi.y+roi.height; y++) { //find local minimum/maximum now for (int x=roi.x; x<roi.x+roi.width; x++) { //ImageStatistics won't work if we have no ImagePlus float v = ip.getPixelValue(x, y); if (globalMin>v) globalMin = v; if (globalMax<v) globalMax = v; } } if (threshold !=ImageProcessor.NO_THRESHOLD) threshold -= (globalMax-globalMin)*1e-6;//avoid rounding errors //for segmentation, exclusion of edge maxima cannot be done now but has to be done after segmentation: boolean excludeEdgesNow = excludeOnEdges && outputType!=SEGMENTED; if (Thread.currentThread().isInterrupted()) return null; IJ.showStatus("Getting sorted maxima..."); long[] maxPoints = getSortedMaxPoints(ip, typeP, excludeEdgesNow, isEDM, globalMin, globalMax, threshold); if (Thread.currentThread().isInterrupted()) return null; IJ.showStatus("Analyzing maxima..."); float maxSortingError = 0; if (ip instanceof FloatProcessor) //sorted sequence may be inaccurate by this value maxSortingError = 1.1f * (isEDM ? SQRT2/2f : (globalMax-globalMin)/2e9f); analyzeAndMarkMaxima(ip, typeP, maxPoints, excludeEdgesNow, isEDM, globalMin, tolerance, outputType, maxSortingError); //new ImagePlus("Pixel types",typeP.duplicate()).show(); if (outputType==POINT_SELECTION || outputType==LIST || outputType==COUNT) return null; ByteProcessor outIp; byte[] pixels; if (outputType == SEGMENTED) { // Segmentation required, convert to 8bit (also for 8-bit images, since the calibration // may have a negative slope). outIp has background 0, maximum areas 255 outIp = make8bit(ip, typeP, isEDM, globalMin, globalMax, threshold); //if (IJ.debugMode) new ImagePlus("pixel types precleanup", typeP.duplicate()).show(); cleanupMaxima(outIp, typeP, maxPoints); //eliminate all the small maxima (i.e. those outside MAX_AREA) //if (IJ.debugMode) new ImagePlus("pixel types postcleanup", typeP).show(); //if (IJ.debugMode) new ImagePlus("pre-watershed", outIp.duplicate()).show(); if (!watershedSegment(outIp)) //do watershed segmentation return null; //if user-cancelled, return if (!isEDM) cleanupExtraLines(outIp); //eliminate lines due to local minima (none in EDM) watershedPostProcess(outIp); //levels to binary image if (excludeOnEdges) deleteEdgeParticles(outIp, typeP); } else { //outputType other than SEGMENTED for (int i=0; i<width*height; i++) types[i] = (byte)(((types[i]&outputTypeMasks[outputType])!=0)?255:0); outIp = typeP; } byte[] outPixels = (byte[])outIp.getPixels(); //IJ.write("roi: "+roi.toString()); if (roi!=null) { for (int y=0, i=0; y<outIp.getHeight(); y++) { //delete everything outside roi for (int x=0; x<outIp.getWidth(); x++, i++) { if (x<roi.x || x>=roi.x+roi.width || y<roi.y || y>=roi.y+roi.height) outPixels[i] = (byte)0; else if (mask !=null && (mask[x-roi.x + roi.width*(y-roi.y)]==0)) outPixels[i] = (byte)0; } } } return outIp; } // public ByteProcessor findMaxima /** Find all local maxima (irrespective whether they finally qualify as maxima or not) * @param ip The image to be analyzed * @param typeP A byte image, same size as ip, where the maximum points are marked as MAXIMUM * (do not use it as output: for rois, the points are shifted w.r.t. the input image) * @param excludeEdgesNow Whether to exclude edge pixels * @param isEDM Whether ip is a float Euclidian distance map * @param globalMin The minimum value of the image or roi * @param threshold The threshold (calibrated) below which no pixels are processed. Ignored if ImageProcessor.NO_THRESHOLD * @return Maxima sorted by value. In each array element (long, i.e., 64-bit integer), the value * is encoded in the upper 32 bits and the pixel offset in the lower 32 bit * Note: Do not use the positions of the points marked as MAXIMUM in typeP, they are invalid for images with a roi. */ long[] getSortedMaxPoints(ImageProcessor ip, ByteProcessor typeP, boolean excludeEdgesNow, boolean isEDM, float globalMin, float globalMax, double threshold) { Rectangle roi = ip.getRoi(); byte[] types = (byte[])typeP.getPixels(); int nMax = 0; //counts local maxima boolean checkThreshold = threshold!=ImageProcessor.NO_THRESHOLD; Thread thread = Thread.currentThread(); //long t0 = System.currentTimeMillis(); for (int y=roi.y; y<roi.y+roi.height; y++) { // find local maxima now if (y%50==0 && thread.isInterrupted()) return null; for (int x=roi.x, i=x+y*width; x<roi.x+roi.width; x++, i++) { // for better performance with rois, restrict search to roi float v = ip.getPixelValue(x,y); float vTrue = isEDM ? trueEdmHeight(x,y,ip) : v; // for EDMs, use interpolated ridge height if (v==globalMin) continue; if (excludeEdgesNow && (x==0 || x==width-1 || y==0 || y==height-1)) continue; if (checkThreshold && v<threshold) continue; boolean isMax = true; /* check wheter we have a local maximum. Note: For an EDM, we need all maxima: those of the EDM-corrected values (needed by findMaxima) and those of the raw values (needed by cleanupMaxima) */ boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin for (int d=0; d<8; d++) { // compare with the 8 neighbor pixels if (isInner || isWithin(x, y, d)) { float vNeighbor = ip.getPixelValue(x+DIR_X_OFFSET[d], y+DIR_Y_OFFSET[d]); float vNeighborTrue = isEDM ? trueEdmHeight(x+DIR_X_OFFSET[d], y+DIR_Y_OFFSET[d], ip) : vNeighbor; if (vNeighbor > v && vNeighborTrue > vTrue) { isMax = false; break; } } } if (isMax) { types[i] = MAXIMUM; nMax++; } } // for x } // for y if (thread.isInterrupted()) return null; //long t1 = System.currentTimeMillis();IJ.log("markMax:"+(t1-t0)); float vFactor = (float)(2e9/(globalMax-globalMin)); //for converting float values into a 32-bit int long[] maxPoints = new long[nMax]; //value (int) is in the upper 32 bit, pixel offset in the lower int iMax = 0; for (int y=roi.y; y<roi.y+roi.height; y++) //enter all maxima into an array for (int x=roi.x, p=x+y*width; x<roi.x+roi.width; x++, p++) if (types[p]==MAXIMUM) { float fValue = isEDM?trueEdmHeight(x,y,ip):ip.getPixelValue(x,y); int iValue = (int)((fValue-globalMin)*vFactor); //32-bit int, linear function of float value maxPoints[iMax++] = (long)iValue<<32|p; } //long t2 = System.currentTimeMillis();IJ.log("makeArray:"+(t2-t1)); if (thread.isInterrupted()) return null; Arrays.sort(maxPoints); //sort the maxima by value //long t3 = System.currentTimeMillis();IJ.log("sort:"+(t3-t2)); return maxPoints; } //getSortedMaxPoints /** Check all maxima in list maxPoints, mark type of the points in typeP * @param ip the image to be analyzed * @param typeP 8-bit image, here the point types are marked by type: MAX_POINT, etc. * @param maxPoints input: a list of all local maxima, sorted by height. Lower 32 bits are pixel offset * @param excludeEdgesNow whether to avoid edge maxima * @param isEDM whether ip is a (float) Euclidian distance map * @param globalMin minimum pixel value in ip * @param tolerance minimum pixel value difference for two separate maxima * @param maxSortingError sorting may be inaccurate, sequence may be reversed for maxima having values * not deviating from each other by more than this (this could be a result of * precision loss when sorting ints instead of floats, or because sorting does not * take the height correction in 'trueEdmHeight' into account * @param outputType */ void analyzeAndMarkMaxima(ImageProcessor ip, ByteProcessor typeP, long[] maxPoints, boolean excludeEdgesNow, boolean isEDM, float globalMin, double tolerance, int outputType, float maxSortingError) { byte[] types = (byte[])typeP.getPixels(); int nMax = maxPoints.length; int [] pList = new int[width*height]; //here we enter points starting from a maximum Vector xyVector = null; Roi roi = null; boolean displayOrCount = outputType==POINT_SELECTION||outputType==LIST||outputType==COUNT; if (displayOrCount) xyVector=new Vector(); if (imp!=null) roi = imp.getRoi(); for (int iMax=nMax-1; iMax>=0; iMax--) { //process all maxima now, starting from the highest if (iMax%100 == 0 && Thread.currentThread().isInterrupted()) return; int offset0 = (int)maxPoints[iMax]; //type cast gets 32 lower bits, where pixel index is encoded //int offset0 = maxPoints[iMax].offset; if ((types[offset0]&PROCESSED)!=0) //this maximum has been reached from another one, skip it continue; //we create a list of connected points and start the list at the current maximum int x0 = offset0 % width; int y0 = offset0 / width; float v0 = isEDM?trueEdmHeight(x0,y0,ip):ip.getPixelValue(x0,y0); boolean sortingError; do { //repeat if we have encountered a sortingError pList[0] = offset0; types[offset0] |= (EQUAL|LISTED); //mark first point as equal height (to itself) and listed int listLen = 1; //number of elements in the list int listI = 0; //index of current element in the list boolean isEdgeMaximum = (x0==0 || x0==width-1 || y0==0 || y0==height-1); sortingError = false; //if sorting was inaccurate: a higher maximum was not handled so far boolean maxPossible = true; //it may be a true maximum double xEqual = x0; //for creating a single point: determine average over the double yEqual = y0; // coordinates of contiguous equal-height points int nEqual = 1; //counts xEqual/yEqual points that we use for averaging do { //while neigbor list is not fully processed (to listLen) int offset = pList[listI]; int x = offset % width; int y = offset / width; //if(x==18&&y==20)IJ.write("x0,y0="+x0+","+y0+"@18,20;v0="+v0+" sortingError="+sortingError); boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin for (int d=0; d<8; d++) { //analyze all neighbors (in 8 directions) at the same level int offset2 = offset+dirOffset[d]; if ((isInner || isWithin(x, y, d)) && (types[offset2]&LISTED)==0) { if ((types[offset2]&PROCESSED)!=0) { maxPossible = false; //we have reached a point processed previously, thus it is no maximum now //if(x0<25&&y0<20)IJ.write("x0,y0="+x0+","+y0+":stop at processed neighbor from x,y="+x+","+y+", dir="+d); break; } int x2 = x+DIR_X_OFFSET[d]; int y2 = y+DIR_Y_OFFSET[d]; float v2 = isEDM ? trueEdmHeight(x2, y2, ip) : ip.getPixelValue(x2, y2); if (v2 > v0 + maxSortingError) { maxPossible = false; //we have reached a higher point, thus it is no maximum //if(x0<25&&y0<20)IJ.write("x0,y0="+x0+","+y0+":stop at higher neighbor from x,y="+x+","+y+", dir="+d+",value,value2,v2-v="+v0+","+v2+","+(v2-v0)); break; } else if (v2 >= v0-(float)tolerance) { if (v2 > v0) { //maybe this point should have been treated earlier sortingError = true; offset0 = offset2; v0 = v2; x0 = x2; y0 = y2; } pList[listLen] = offset2; listLen++; //we have found a new point within the tolerance types[offset2] |= LISTED; if (x2==0 || x2==width-1 || y2==0 || y2==height-1) { isEdgeMaximum = true; if (excludeEdgesNow) { maxPossible = false; break; //we have an edge maximum; } } if (v2==v0) { //prepare finding center of equal points (in case single point needed) types[offset2] |= EQUAL; xEqual += x2; yEqual += y2; nEqual ++; } } } // if isWithin & not LISTED } // for directions d listI++; } while (listI < listLen); if (sortingError) { //if x0,y0 was not the true maximum but we have reached a higher one for (listI=0; listI<listLen; listI++) types[pList[listI]] = 0; //reset all points encountered, then retry } else { int resetMask = ~(maxPossible?LISTED:(LISTED|EQUAL)); xEqual /= nEqual; yEqual /= nEqual; double minDist2 = 1e20; int nearestI = 0; for (listI=0; listI<listLen; listI++) { int offset = pList[listI]; int x = offset % width; int y = offset / width; types[offset] &= resetMask; //reset attributes no longer needed types[offset] |= PROCESSED; //mark as processed if (maxPossible) { types[offset] |= MAX_AREA; if ((types[offset]&EQUAL)!=0) { double dist2 = (xEqual-x)*(double)(xEqual-x) + (yEqual-y)*(double)(yEqual-y); if (dist2 < minDist2) { minDist2 = dist2; //this could be the best "single maximum" point nearestI = listI; } } } } // for listI if (maxPossible) { int offset = pList[nearestI]; types[offset] |= MAX_POINT; if (displayOrCount && !(this.excludeOnEdges && isEdgeMaximum)) { int x = offset % width; int y = offset / width; if (roi==null || roi.contains(x, y)) xyVector.addElement(new int[] {x, y}); } } } //if !sortingError } while (sortingError); //redo if we have encountered a higher maximum: handle it now. } // for all maxima iMax if (Thread.currentThread().isInterrupted()) return; if (displayOrCount && xyVector!=null) { int npoints = xyVector.size(); if (outputType == POINT_SELECTION && npoints>0 && imp!=null) { int[] xpoints = new int[npoints]; int[] ypoints = new int[npoints]; for (int i=0; i<npoints; i++) { int[] xy = (int[])xyVector.elementAt(i); xpoints[i] = xy[0]; ypoints[i] = xy[1]; } Roi points = new PointRoi(xpoints, ypoints, npoints); ((PointRoi)points).setHideLabels(true); imp.setRoi(points); } else if (outputType==LIST) { Analyzer.resetCounter(); ResultsTable rt = ResultsTable.getResultsTable(); for (int i=0; i<npoints; i++) { int[] xy = (int[])xyVector.elementAt(i); rt.incrementCounter(); rt.addValue("X", xy[0]); rt.addValue("Y", xy[1]); } rt.show("Results"); } else if (outputType==COUNT) { ResultsTable rt = ResultsTable.getResultsTable(); rt.incrementCounter(); rt.setValue("Count", rt.getCounter()-1, npoints); rt.show("Results"); } } if (previewing) messageArea.setText((xyVector==null ? 0 : xyVector.size())+" Maxima"); } //void analyzeAndMarkMaxima /** Create an 8-bit image by scaling the pixel values of ip to 1-254 (<lower threshold 0) and mark maximum areas as 255. * For use as input for watershed segmentation * @param ip The original image that should be segmented * @param typeP Pixel types in ip * @param isEDM Whether ip is an Euclidian distance map * @param globalMin The minimum pixel value of ip * @param globalMax The maximum pixel value of ip * @param threshold Pixels of ip below this value (calibrated) are considered background. Ignored if ImageProcessor.NO_THRESHOLD * @return The 8-bit output image. */ ByteProcessor make8bit(ImageProcessor ip, ByteProcessor typeP, boolean isEDM, float globalMin, float globalMax, double threshold) { byte[] types = (byte[])typeP.getPixels(); double minValue; if (isEDM) { threshold = 0.5; minValue = 1.; } else minValue = (threshold == ImageProcessor.NO_THRESHOLD)?globalMin:threshold; double offset = minValue - (globalMax-minValue)*(1./253/2-1e-6); //everything above minValue should become >(byte)0 double factor = 253/(globalMax-minValue); //IJ.write("min,max="+minValue+","+globalMax+"; offset, 1/factor="+offset+", "+(1./factor)); if (isEDM && factor>1) factor = 1; // with EDM, no better resolution ByteProcessor outIp = new ByteProcessor(width, height); //convert possibly calibrated image to byte without damaging threshold (setMinAndMax would kill threshold) byte[] pixels = (byte[])outIp.getPixels(); long v; for (int y=0, i=0; y<height; y++) { for (int x=0; x<width; x++, i++) { float rawValue = ip.getPixelValue(x, y); if (threshold!=ImageProcessor.NO_THRESHOLD && rawValue<threshold) pixels[i] = (byte)0; else if ((types[i]&MAX_AREA)!=0) pixels[i] = (byte)255; //prepare watershed by setting "true" maxima+surroundings to 255 else { v = 1 + Math.round((rawValue-offset)*factor); if (v < 1) pixels[i] = (byte)1; else if (v<=254) pixels[i] = (byte)(v&255); else pixels[i] = (byte)254; } } } return outIp; } // byteProcessor make8bit /** Get estimated "true" height of a maximum or saddle point of a Euclidian Distance Map. * This is needed since the point sampled is not necessarily at the highest position. * For simplicity, we don't care about the Sqrt(5) distance here although this would be more accurate * @param x x-position of the point * @param y y-position of the point * @param ip the EDM (FloatProcessor) * @return estimated height */ float trueEdmHeight(int x, int y, ImageProcessor ip) { int xmax = width - 1; int ymax = ip.getHeight() - 1; float[] pixels = (float[])ip.getPixels(); int offset = x + y*width; float v = pixels[offset]; if (x==0 || y==0 || x==xmax || y==ymax || v==0) { return v; //don't recalculate for edge pixels or background } else { float trueH = v + 0.5f*SQRT2; //true height can never by higher than this boolean ridgeOrMax = false; for (int d=0; d<4; d++) { //for all directions halfway around: int d2 = (d+4)%8; //get the opposite direction and neighbors float v1 = pixels[offset+dirOffset[d]]; float v2 = pixels[offset+dirOffset[d2]]; float h; if (v>=v1 && v>=v2) { ridgeOrMax = true; h = (v1 + v2)/2; } else { h = Math.min(v1, v2); } h += (d%2==0) ? 1 : SQRT2; //in diagonal directions, distance is sqrt2 if (trueH > h) trueH = h; } if (!ridgeOrMax) trueH = v; return trueH; } } /** eliminate unmarked maxima for use by watershed. Starting from each previous maximum, * explore the surrounding down to successively lower levels until a marked maximum is * touched (or the plateau of a previously eliminated maximum leads to a marked maximum). * Then set all the points above this value to this value * @param outIp the image containing the pixel values * @param typeP the types of the pixels are marked here * @param maxPoints array containing the coordinates of all maxima that might be relevant */ void cleanupMaxima(ByteProcessor outIp, ByteProcessor typeP, long[] maxPoints) { byte[] pixels = (byte[])outIp.getPixels(); byte[] types = (byte[])typeP.getPixels(); int nMax = maxPoints.length; int[] pList = new int[width*height]; for (int iMax = nMax-1; iMax>=0; iMax--) { int offset0 = (int)maxPoints[iMax]; //type cast gets lower 32 bits where pixel offset is encoded if ((types[offset0]&(MAX_AREA|ELIMINATED))!=0) continue; int level = pixels[offset0]&255; int loLevel = level+1; pList[0] = offset0; //we start the list at the current maximum //if (xList[0]==122) IJ.write("max#"+iMax+" at x,y="+xList[0]+","+yList[0]+"; level="+level); types[offset0] |= LISTED; //mark first point as listed int listLen = 1; //number of elements in the list int lastLen = 1; int listI = 0; //index of current element in the list boolean saddleFound = false; while (!saddleFound && loLevel >0) { loLevel--; lastLen = listLen; //remember end of list for previous level listI = 0; //in each level, start analyzing the neighbors of all pixels do { //for all pixels listed so far int offset = pList[listI]; int x = offset % width; int y = offset / width; boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin for (int d=0; d<8; d++) { //analyze all neighbors (in 8 directions) at the same level int offset2 = offset+dirOffset[d]; if ((isInner || isWithin(x, y, d)) && (types[offset2]&LISTED)==0) { if ((types[offset2]&MAX_AREA)!=0 || (((types[offset2]&ELIMINATED)!=0) && (pixels[offset2]&255)>=loLevel)) { saddleFound = true; //we have reached a point touching a "true" maximum... //if (xList[0]==122) IJ.write("saddle found at level="+loLevel+"; x,y="+xList[listI]+","+yList[listI]+", dir="+d); break; //...or a level not lower, but touching a "true" maximum } else if ((pixels[offset2]&255)>=loLevel && (types[offset2]&ELIMINATED)==0) { pList[listLen] = offset2; //xList[listLen] = x+DIR_X_OFFSET[d]; //yList[listLen] = x+DIR_Y_OFFSET[d]; listLen++; //we have found a new point to be processed types[offset2] |= LISTED; } } // if isWithin & not LISTED } // for directions d if (saddleFound) break; //no reason to search any further listI++; } while (listI < listLen); } // while !levelFound && loLevel>=0 for (listI=0; listI<listLen; listI++) //reset attribute since we may come to this place again types[pList[listI]] &= ~LISTED; for (listI=0; listI<lastLen; listI++) { //for all points higher than the level of the saddle point int offset = pList[listI]; pixels[offset] = (byte)loLevel; //set pixel value to the level of the saddle point types[offset] |= ELIMINATED; //mark as processed: there can't be a local maximum in this area } } // for all maxima iMax } // void cleanupMaxima /** Delete extra structures form watershed of non-EDM images, e.g., foreground patches, * single dots and lines ending somewhere within a segmented particle * Needed for post-processing watershed-segmented images that can have local minima * @param ip 8-bit image with background = 0, lines between 1 and 254 and segmented particles = 255 */ void cleanupExtraLines(ImageProcessor ip) { byte[] pixels = (byte[])ip.getPixels(); for (int y=0, i=0; y<height; y++) { for (int x=0; x<width; x++,i++) { int v = pixels[i]; if (v!=(byte)255 && v!=0) { int nRadii = nRadii(pixels, x, y); //number of lines radiating if (nRadii==0) //single point or foreground patch? pixels[i] = (byte)255; else if (nRadii==1) removeLineFrom(pixels, x, y); } // if v<255 && v>0 } // for x } // for y } // void cleanupExtraLines /** delete a line starting at x, y up to the next (4-connected) vertex */ void removeLineFrom (byte[] pixels, int x, int y) { //IJ.log("del line from "+x+","+y); //if(x<50&&y<40)IJ.write("x,y start="+x+","+y); pixels[x + width*y] = (byte)255; //delete the first point boolean continues; do { continues = false; boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin for (int d=0; d<8; d+=2) { //analyze 4-connected neighbors if (isInner || isWithin(x, y, d)) { int v = pixels[x + width*y + dirOffset[d]]; if (v!=(byte)255 && v!=0) { int nRadii = nRadii(pixels, x+DIR_X_OFFSET[d], y+DIR_Y_OFFSET[d]); if (nRadii<=1) { //found a point or line end x += DIR_X_OFFSET[d]; y += DIR_Y_OFFSET[d]; pixels[x + width*y] = (byte)255; //delete the point continues = nRadii==1; //continue along that line break; } } } } // for directions d //if(!continues && x<50&&y<40)IJ.write("x,y end="+x+","+y); } while (continues); //IJ.log("deleted to "+x+","+y); } // void removeLineFrom /** Analyze the neighbors of a pixel (x, y) in a byte image; pixels <255 ("non-white") are * considered foreground. Edge pixels are considered foreground. * @param ip * @param x coordinate of the point * @param y coordinate of the point * @return Number of 4-connected lines emanating from this point. Zero if the point is * embedded in either foreground or background */ int nRadii (byte[] pixels, int x, int y) { int offset = x + y*width; int countTransitions = 0; boolean prevPixelSet = true; boolean firstPixelSet = true; //initialize to make the compiler happy boolean isInner = (y!=0 && y!=height-1) && (x!=0 && x!=width-1); //not necessary, but faster than isWithin for (int d=0; d<8; d++) { //walk around the point and note every no-line->line transition boolean pixelSet = prevPixelSet; if (isInner || isWithin(x, y, d)) { boolean isSet = (pixels[offset+dirOffset[d]]!=(byte)255); if ((d&1)==0) pixelSet = isSet; //non-diagonal directions: always regarded else if (!isSet) //diagonal directions may separate two lines, pixelSet = false; // but are insufficient for a 4-connected line } else { pixelSet = true; } if (pixelSet && !prevPixelSet) countTransitions ++; prevPixelSet = pixelSet; if (d==0) firstPixelSet = pixelSet; } if (firstPixelSet && !prevPixelSet) countTransitions ++; return countTransitions; } // int nRadii /** after watershed, set all pixels in the background and segmentation lines to 0 */ private void watershedPostProcess(ImageProcessor ip) { //new ImagePlus("before postprocess",ip.duplicate()).show(); byte[] pixels = (byte[])ip.getPixels(); int size = ip.getWidth()*ip.getHeight(); for (int i=0; i<size; i++) { if ((pixels[i]&255)<255) pixels[i] = (byte)0; } //new ImagePlus("after postprocess",ip.duplicate()).show(); } /** delete particles corresponding to edge maxima * @param typeP Here the pixel types of the original image are noted, * pixels with bit MAX_AREA at the edge are considered indicators of an edge maximum. * @param ip the image resulting from watershed segmentaiton * (foreground pixels, i.e. particles, are 255, background 0) */ void deleteEdgeParticles(ByteProcessor ip, ByteProcessor typeP) { byte[] pixels = (byte[])ip.getPixels(); byte[] types = (byte[])typeP.getPixels(); width = ip.getWidth(); height = ip.getHeight(); ip.setValue(0); Wand wand = new Wand(ip); for (int x=0; x<width; x++) { int y = 0; if ((types[x+y*width]&MAX_AREA) != 0 && pixels[x+y*width] != 0) deleteParticle(x,y,ip,wand); y = height - 1; if ((types[x+y*width]&MAX_AREA) != 0 && pixels[x+y*width] != 0) deleteParticle(x,y,ip,wand); } for (int y=1; y<height-1; y++) { int x = 0; if ((types[x+y*width]&MAX_AREA) != 0 && pixels[x+y*width] != 0) deleteParticle(x,y,ip,wand); x = width - 1; if ((types[x+y*width]&MAX_AREA) != 0 && pixels[x+y*width] != 0) deleteParticle(x,y,ip,wand); } } //void deleteEdgeParticles /** delete a particle (set from value 255 to current fill value). * Position x,y must be within the particle */ void deleteParticle(int x, int y, ByteProcessor ip, Wand wand) { wand.autoOutline(x, y, 255, 255); if (wand.npoints==0) { IJ.log("wand error selecting edge particle at x, y = "+x+", "+y); return; } Roi roi = new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, Roi.TRACED_ROI); ip.snapshot(); //prepare for reset outside of mask ip.setRoi(roi); ip.fill(); ip.reset(ip.getMask()); } /** Do watershed segmentation on a byte image, with the start points (maxima) * set to 255 and the background set to 0. The image should not have any local maxima * other than the marked ones. Local minima will lead to artifacts that can be removed * later. On output, all particles will be set to 255, segmentation lines remain at their * old value. * @param ip The byteProcessor containing the image, with size given by the class variables width and height * @return false if canceled by the user (note: can be cancelled only if called by "run" with a known ImagePlus) */ private boolean watershedSegment(ByteProcessor ip) { boolean debug = IJ.debugMode; ImageStack movie=null; if (debug) { movie = new ImageStack(ip.getWidth(), ip.getHeight()); movie.addSlice("pre-watershed EDM", ip.duplicate()); } byte[] pixels = (byte[])ip.getPixels(); // Create an array with the coordinates of all points between value 1 and 254 // This method, suggested by Stein Roervik (stein_at_kjemi-dot-unit-dot-no), // greatly speeds up the watershed segmentation routine. int[] histogram = ip.getHistogram(); int arraySize = width*height - histogram[0] -histogram[255]; int[] coordinates = new int[arraySize]; //from pixel coordinates, low bits x, high bits y int highestValue = 0; int maxBinSize = 0; int offset = 0; int[] levelStart = new int[256]; for (int v=1; v<255; v++) { levelStart[v] = offset; offset += histogram[v]; if (histogram[v] > 0) highestValue = v; if (histogram[v] > maxBinSize) maxBinSize = histogram[v]; } int[] levelOffset = new int[highestValue + 1]; for (int y=0, i=0; y<height; y++) { for (int x=0; x<width; x++, i++) { int v = pixels[i]&255; if (v>0 && v<255) { offset = levelStart[v] + levelOffset[v]; coordinates[offset] = x | y<<intEncodeShift; levelOffset[v] ++; } } //for x } //for y // Create an array of the points (pixel offsets) that we set to 255 in one pass. // If we remember this list we need not create a snapshot of the ImageProcessor. int[] setPointList = new int[Math.min(maxBinSize, (width*height+2)/3)]; // now do the segmentation, starting at the highest level and working down. // At each level, dilate the particle (set pixels to 255), constrained to pixels // whose values are at that level and also constrained (by the fateTable) // to prevent features from merging. int[] table = makeFateTable(); IJ.showStatus("Segmenting (Esc to cancel)"); final int[] directionSequence = new int[] {7, 3, 1, 5, 0, 4, 2, 6}; // diagonal directions first for (int level=highestValue; level>=1; level--) { int remaining = histogram[level]; //number of points in the level that have not been processed int idle = 0; while (remaining>0 && idle<8) { int sumN = 0; int dIndex = 0; do { // expand each level in 8 directions int n = processLevel(directionSequence[dIndex%8], ip, table, levelStart[level], remaining, coordinates, setPointList); //IJ.log("level="+level+" direction="+directionSequence[dIndex%8]+" remain="+remaining+"-"+n); remaining -= n; // number of points processed sumN += n; if (n > 0) idle = 0; // nothing processed in this direction? dIndex++; } while (remaining>0 && idle++<8); addProgress(sumN/(double)arraySize); if (IJ.escapePressed()) { // cancelled by the user IJ.beep(); IJ.showProgress(1.0); return false; } } if (remaining>0 && level>1) { // any pixels that we have not reached? int nextLevel = level; // find the next level to process do nextLevel--; while (nextLevel>1 && histogram[nextLevel]==0); // in principle we should add all unprocessed pixels of this level to the // tasklist of the next level. This would make it very slow for some images, // however. Thus we only add the pixels if they are at the border (of the // image or a thresholded area) and correct unprocessed pixels at the very // end by CleanupExtraLines if (nextLevel > 0) { int newNextLevelEnd = levelStart[nextLevel] + histogram[nextLevel]; for (int i=0, p=levelStart[level]; i<remaining; i++, p++) { int xy = coordinates[p]; int x = xy&intEncodeXMask; int y = (xy&intEncodeYMask)>>intEncodeShift; int pOffset = x + y*width; if ((pixels[pOffset]&255)==255) IJ.log("ERROR"); boolean addToNext = false; if (x==0 || y==0 || x==width-1 || y==height-1) addToNext = true; //image border else for (int d=0; d<8; d++) if (isWithin(x, y, d) && pixels[pOffset+dirOffset[d]]==0) { addToNext = true; //border of area below threshold break; } if (addToNext) coordinates[newNextLevelEnd++] = xy; } //IJ.log("level="+level+": add "+(newNextLevelEnd-levelStart[nextLevel+1])+" points to "+nextLevel); //tasklist for the next level to process becomes longer by this: histogram[nextLevel] = newNextLevelEnd - levelStart[nextLevel]; } } if (debug && (level>170 || level>100 && level<110 || level<10)) movie.addSlice("level "+level, ip.duplicate()); } if (debug) new ImagePlus("Segmentation Movie", movie).show(); return true; } // boolean watershedSegment /** dilate the UEP on one level by one pixel in the direction specified by step, i.e., set pixels to 255 * @param pass gives direction of dilation, see makeFateTable * @param ip the EDM with the segmeted blobs successively getting set to 255 * @param table The fateTable * @param levelStart offsets of the level in pixelPointers[] * @param levelNPoints number of points in the current level * @param pixelPointers[] list of pixel coordinates (x+y*width) sorted by level (in sequence of y, x within each level) * @param xCoordinates list of x Coorinates for the current level only (no offset levelStart) * @return number of pixels that have been changed */ private int processLevel(int pass, ImageProcessor ip, int[] fateTable, int levelStart, int levelNPoints, int[] coordinates, int[] setPointList) { int xmax = width - 1; int ymax = height - 1; byte[] pixels = (byte[])ip.getPixels(); //byte[] pixels2 = (byte[])ip2.getPixels(); int nChanged = 0; int nUnchanged = 0; for (int i=0, p=levelStart; i<levelNPoints; i++, p++) { int xy = coordinates[p]; int x = xy&intEncodeXMask; int y = (xy&intEncodeYMask)>>intEncodeShift; int offset = x + y*width; int index = 0; //neighborhood pixel ocupation: index in fateTable if (y>0 && (pixels[offset-width]&255)==255) index ^= 1; if (x<xmax && y>0 && (pixels[offset-width+1]&255)==255) index ^= 2; if (x<xmax && (pixels[offset+1]&255)==255) index ^= 4; if (x<xmax && y<ymax && (pixels[offset+width+1]&255)==255) index ^= 8; if (y<ymax && (pixels[offset+width]&255)==255) index ^= 16; if (x>0 && y<ymax && (pixels[offset+width-1]&255)==255) index ^= 32; if (x>0 && (pixels[offset-1]&255)==255) index ^= 64; if (x>0 && y>0 && (pixels[offset-width-1]&255)==255) index ^= 128; int mask = 1<<pass; if ((fateTable[index]&mask)==mask) setPointList[nChanged++] = offset; //remember to set pixel to 255 else coordinates[levelStart+(nUnchanged++)] = xy; //keep this pixel for future passes } // for pixel i //IJ.log("pass="+pass+", changed="+nChanged+" unchanged="+nUnchanged); for (int i=0; i<nChanged; i++) pixels[setPointList[i]] = (byte)255; return nChanged; } //processLevel /** Creates the lookup table used by the watershed function for dilating the particles. * The algorithm allows dilation in both straight and diagonal directions. * There is an entry in the table for each possible 3x3 neighborhood: * x-1 x x+1 * y-1 128 1 2 * y 64 pxl_unset_yet 4 * y+1 32 16 8 * (to find throws entry, sum up the numbers of the neighboring pixels set; e.g. * entry 6=2+4 if only the pixels (x,y-1) and (x+1, y-1) are set. * A pixel is added on the 1st pass if bit 0 (2^0 = 1) is set, * on the 2nd pass if bit 1 (2^1 = 2) is set, etc. * pass gives the direction of rotation, with 0 = to top left (x--,y--), 1 to top, * and clockwise up to 7 = to the left (x--). * E.g. 4 = add on 3rd pass, 3 = add on either 1st or 2nd pass. */ private int[] makeFateTable() { int[] table = new int[256]; boolean[] isSet = new boolean[8]; for (int item=0; item<256; item++) { //dissect into pixels for (int i=0, mask=1; i<8; i++) { isSet[i] = (item&mask)==mask; mask*=2; } for (int i=0, mask=1; i<8; i++) { //we dilate in the direction opposite to the direction of the existing neighbors if (isSet[(i+4)%8]) table[item] |= mask; mask*=2; } for (int i=0; i<8; i+=2) //if side pixels are set, for counting transitions it is as good as if the adjacent edges were also set if (isSet[i]) { isSet[(i+1)%8] = true; isSet[(i+7)%8] = true; } int transitions=0; for (int i=0, mask=1; i<8; i++) { if (isSet[i] != isSet[(i+1)%8]) transitions++; } if (transitions>=4) { //if neighbors contain more than one region, dilation ito this pixel is forbidden table[item] = 0; } else { } } return table; } // int[] makeFateTable /** create an array of offsets within a pixel array for directions in clockwise order: * 0=(x,y-1), 1=(x+1,y-1), ... 7=(x-1,y) * Also creates further class variables: * width, height, and the following three values needed for storing coordinates in single ints for watershed: * intEncodeXMask, intEncodeYMask and intEncodeShift. * E.g., for width between 129 and 256, xMask=0xff and yMask = 0xffffff00 are bitwise masks * for x and y, respectively, and shift=8 is the bit shift to get y from the y-masked value * Returns as class variables: the arrays of the offsets to the 8 neighboring pixels * and the array maskAndShift for watershed */ void makeDirectionOffsets(ImageProcessor ip) { width = ip.getWidth(); height = ip.getHeight(); int shift = 0, mult=1; do { shift++; mult*=2; } while (mult < width); intEncodeXMask = mult-1; intEncodeYMask = ~intEncodeXMask; intEncodeShift = shift; //IJ.log("masks (hex):"+Integer.toHexString(xMask)+","+Integer.toHexString(xMask)+"; shift="+shift); dirOffset = new int[] {-width, -width+1, +1, +width+1, +width, +width-1, -1, -width-1 }; //dirOffset is created last, so check for it being null before makeDirectionOffsets //(in case we have multiple threads using the same MaximumFinder) } /** 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(int x, int y, int direction) { 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 /** add work done in the meanwhile and show progress */ private void addProgress(double deltaProgress) { if (nPasses==0) return; progressDone += deltaProgress; IJ.showProgress(progressDone/nPasses); } }