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 <a href="https://imagej.net/plugins/adjustable-watershed/adjustable-watershed">Adjustable Watershed plugin</a>
 */
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<width*height; i++)
            if (bPixels[i]!=backgroundValue) fPixels[i] = Float.MAX_VALUE;

        int[][] pointBufs = new int[2][width];  //two buffers for two passes; low short contains x, high short y
        int yDist = Integer.MAX_VALUE;          //this value is used only if edges are not background
        // pass 1 & 2: increasing y
        for (int x=0; x<width; x++) {
            pointBufs[0][x] = NO_POINT;
            pointBufs[1][x] = NO_POINT;
        }
        for (int y=0; y<height; y++) {
            if (edgesAreBackground) yDist = y+1; //distance to nearest background point (along y)
            edmLine(bPixels, fPixels, pointBufs, width, y*width, y, backgroundValue, yDist);
            if (y%progressInterval == 0) {
                if (Thread.currentThread().isInterrupted()) return null;
                addProgress(progressAddendum);
            }
        }
        //pass 3 & 4: decreasing y
        for (int x=0; x<width; x++) {
            pointBufs[0][x] = NO_POINT;
            pointBufs[1][x] = NO_POINT;
        }
        for (int y=height-1; y>=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<width; 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 = (x+1 < yDist) ? (x+1)*(x+1) : yDist*yDist; //distance from edge
                float dist2 = minDist2(points, pPrev, pDiag, x, y, distSqr);
                if (fPixels[offset] > 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; i<width*height; i++) {
            float v = fPixels[i];
            bPixels[i] = v<255f ? (byte)(v+0.5) : (byte)255;
        }
    }

    // set values in floatEdm to zero if pixel in mask equals 'resetOnThis'
    private void resetMasked(FloatProcessor floatEdm, ImageProcessor mask, int resetOnThis) {
        int width = mask.getWidth();
        int height = mask.getHeight();
        byte[] mPixels = (byte[])mask.getPixels();
        float[] fPixels = (float[])floatEdm.getPixels();
        for (int i=0; i<width*height; i++)
            if (mPixels[i] == resetOnThis) fPixels[i] = 0;
    }

    // at the very end - show output image (if the is a separate one)
    private void showOutput() {
        if (interrupted) return;
        if (outStack!=null) {
            outImp = new ImagePlus(TITLE_PREFIX[processType]+imp.getShortTitle(), outStack);
            int[] d = imp.getDimensions();
            outImp.setDimensions(d[2], d[3], d[4]);
            for (int i=1; i<=imp.getStackSize(); i++)
                outStack.setSliceLabel(imp.getStack().getSliceLabel(i), i);
        }
        if (outImageType != BYTE_OVERWRITE) {
            ImageProcessor ip = outImp.getProcessor();
            if (!Prefs.blackBackground) ip.invertLut();
            ip.resetMinAndMax();
            outImp.show();
        }
    }

    /** add work done in the meanwhile and show progress */
    private void addProgress(double deltaProgress) {
        if (nPasses == 0) return;
        progressDone += deltaProgress;
        IJ.showProgress(progressDone/nPasses);
    }
    
    /** Sets the output type (BYTE_OVERWRITE, BYTE, SHORT or FLOAT) */
    public static void setOutputType(int type) {
        if (type<BYTE_OVERWRITE || type>FLOAT)
            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;
    }

}
