package vnt;
/**
 * FindEdges_Segment.java - v1.0
 * Began: September 6, 2006
 * Last Updated: September 6, 2006 
 * 
 * Copyright (C) 2006 - Michael D. Miller - mdm162@truman.edu
 * Truman State University
 * 100 E. Normal
 * Kirksville, MO - 63501
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

import ij.*;
import ij.gui.*;
import java.awt.*;
import java.awt.Color.*;
import ij.plugin.filter.PlugInFilter;
import ij.process.*;
import java.io.*;
import ij.io.*;
import java.lang.String.*;
import java.lang.Math.*;
import ij.plugin.filter.*;

import Coordinate.*;
import java.util.Stack;

/**
 * <p>Plug-in for ImageJ that attempts to automatically perform 
 * specific segmentation designed for vascular cells grown 
 * on a Matrigel assay by Dr. Robert Baer.</p>
 *
 * <p>The plugin operates based off the following series of ImageJ steps:</p>
 * <ol>
 * <li>Find Edges</li>
 * <li>Variance... (about 2-4 pixels)</li>
 * <li>Median... (2-4 pixels)</li>
 * <li>Subtract...(approximately the standard deviation)</li>
 * <li>Multiply by 255 (white out any non-black, max the contrast)</li>
 * </ol>
 * 
 * @author Michael Miller - Truman State University
 * @version 1.0
 * @since 1.0
 */
public class FindEdges_Segment extends VascularNetworkToolkit implements PlugInFilter {

    /**
     * A variable that specifies whether a 4-connected area touched the boundary
     * of the image. If it touched the boundary, it was not enclosed and this
     * value is returned for the area instead of the actual area.
     * @see #fillSegmentedSmallHoles
     */
    private int meshWasNotEnclosed = -1;
    
    /**
    * Specifies the preconditions for the plug-in.
    * If this method succeeds then run() is called.
    *
    * <p>Pre: ImageJ is running and an 8-bit grayscale image is open. The plug-in was just activated.
    * <br />Post: Either an argument was processed, the image was not saved to a local folder, or the plug-in is cleared to run on the image.
    * @param arg Required by the interface. The argument list passed to the plug-in.
    * @param imp Required by the interface. The ImagePlus datastructure holding (among other things) information to grab path and filename.
    * @return If DONE is returned, ImageJ quits without run()'ing the plug-in. Otherwise, the plug-in signals to ImageJ that this plugin only handles 8-bit (256 grayscale) and will not change the original image.
    * @see #run
    */
    public int setup(String arg, ImagePlus ip) {
        if (arg.equals("about")) { 
            showAbout("FindEdges Segment","  * Calculates a segmentation based on the Find Edges algorithm and Variance filters. (Copyright 2005. Michael Miller mdm162@truman.edu)");
            return DONE; // exit without run()
        }

        // will only save if this succeeds
        getFileInformation(ip);
                            
        return DOES_8G+NO_CHANGES; // success, run()
    }

    /**
     * Performs specific segmentation designed for vascular cells 
     * grown on a Matrigel assay.
     *  
     * <p>Pre: The image was cleared to run by the setup() method.
     * <br />Post: The image is processed by the segmentation routines. A new segmented binary image is drawn. 
     * @param bp Required by the interface. The access information to the original image.
     * @see #setup
     * @see #generateSegment 
     */
    public void run(ImageProcessor bp){ 
        // get the width and height information
        getDimensions(bp);
        // generate Segmented image
        if (displayDebugText)
            System.out.println("Doing basic generated...");
        ImageProcessor segmentedWithHoles = generateSegmented(bp);
        // fills any holes
        if (displayDebugText)
            System.out.println("Starting to fill holes...");
        fillSegmentedSmallHoles(segmentedWithHoles);
        if (displayDebugText)
            System.out.println("Done filling holes!");
        // save the segmented image
        if (displayDebugText)
            System.out.println("Saving..");
        saveFile("segmented");
        if (displayDebugText)
            System.out.println("Exiting..");
        return;     
    }
        
    /**
     * Segments the image via the following process:
     * 1) Find Edges - a standard call is performed
     * This creates a lot of noise in the background
     * and within the vasculature.
     * 2) Variance... (about 2-4 pixels)
     * This eliminates the majority of the background
     * noise created by Find Edges. Noise remains
     * from within the vasculature.
     * 3) Median... (2-4 pixels)
     * This eliminates some of the noise within the
     * vasculature. I'm having trouble with this step
     * as I want to "close" off solid vessels but it's
     * difficult without destroying image information.
     * 4) Subtract...(approximately the standard deviation)
     * Remove whatever artifact remain of the background.
     * The majority of the vasculature is 255 or very close.
     * I'd like to use a max filter but it widens too much
     * (very similar to a blur, only far worse in my oppinion, 
     * a tremendous information loss).
     * 5) Multiply by 255 (white out)
     * Since the previous step bottomed out all the noise
     * (at least hopefully), multiplying by 255 thresholds
     * the image into pure black (0) and pure white (255).
     *  
     * <p>Pre: The image must have been grayscaled.
     * <br />Post: On success, a new image with the segmented information is created and displayed. Also, the local member pixel is filled with non-BLACK values. On failure (if a given ImageProcessor is null), no changes are made, and false is returned. 
     * @return Returns true on success, false otherwise.
     */
    public ImageProcessor generateSegmented(ImageProcessor bp) {
        if (bp == null) {
            return null;
        }
        int x=0, y=0;
        // load our original image into memory
        pixel = VascularNetworkToolkit.LoadImage(bp);
        // perform the segmentation and generate a binary image
        String title = "Segmented";
        ImagePlus nimp = NewImage.createByteImage (title, width, height, 1, NewImage.FILL_WHITE);
        ImageProcessor nip = nimp.getProcessor();

        // copy the original onto the new image 
        for (y=0; y<height; y++) {
            for (x=0; x<width; x++) {
                nip.putPixel(x,y,getColor(x,y));
            }
        }
        nimp.show();
        IJ.selectWindow(title);
        
        // Perform our operations:
        IJ.run("Find Edges");
//      IJ.wait(100);
        IJ.run("Variance...", "radius=2");
        IJ.run("Median...", "radius=2");
        ImageStatistics stats=nimp.getStatistics();
        nip.add(-((int)stats.stdDev));
        nip.multiply(255);
        IJ.run("Invert");
        nimp.updateAndDraw();
        return nip; 
    }

    /**
     * Takes a segmented image and fills any 'small' holes:
     * 1) All holes are filled with gray. Their areas are calculated.
     * 2) Any areas that are below the minimum allowed are painted black,
     * and all other holes are returned to white.
     * Note: This algorithm has the added benefit of computing meshes.
     *  
     * <p>Pre: The image must have been converted to binary.
     * <br />Post: On success, a new image with the "filled" segmented information is created and displayed. On failure (if a given ImageProcessor is null), no changes are made, and false is returned. 
     * @return Returns true on success, false otherwise.
     */
    public boolean fillSegmentedSmallHoles(ImageProcessor bp) {
        if (bp == null) {
            return false;
        }
        int x=0, y=0, area=0;
        // load our original image into memory
        pixel = VascularNetworkToolkit.LoadImage(bp);
        // perform the segmentation and generate a binary image
        String title = "Filled Segmentation";
        ImagePlus nimp = NewImage.createByteImage (title, width, height, 1, NewImage.FILL_WHITE);
        ImageProcessor nip = nimp.getProcessor();

        if (animatedDisplay) {
            // copy the original onto the new image 
            for (y=0; y<height; y++) {
                for (x=0; x<width; x++) {
                    nip.putPixel(x,y,getColor(x,y));
                }
            }
            nimp.show();
            IJ.selectWindow(title);
        }

        for (y=0; y<height; y++) {
            for (x=0; x<width; x++) {
                if (getColor(x,y) == WHITE) {
                    area = getEnclosedMeshArea(nip, x,y);
                    if (area == meshWasNotEnclosed || area > maximumAreaToIgnoreMesh) {
                        if (displayDebugText)
                            System.out.println("Found a hole that will be kept of size" + area);
                        // leave the keepers gray for now!
                        // otherwise we'll just be refilling them repeatedly, durr!
                        //fillGrayMeshWithColor(nip, x,y,WHITE);
                    } else {
                        if (displayDebugText)
                            System.out.println("Found a hole that will be removed of size" + area);
                        fillGrayMeshWithColor(nip, x,y,BLACK);                      
                    }
                    if (animatedDisplay)
                        nimp.updateAndDraw();
                }
            }
        }

        for (y=0; y<height; y++) {
            for (x=0; x<width; x++) {
                if (getColor(x,y) == GRAY) {
                    // now fix all the keepers
                    fillGrayMeshWithColor(nip, x,y,WHITE);
                    if (animatedDisplay)
                        nimp.updateAndDraw();
                }
            }
        }
        
        if (!animatedDisplay) {
            for (y=0; y<height; y++) {
                for (x=0; x<width; x++) {
                    nip.putPixel(x,y,getColor(x,y));
                }
            }

            nimp.show();
            IJ.selectWindow(title);
        }
        return true;
    }
    
    /**
     * Fills a 4-connected GRAY mesh starting at the given coordinates with the given color.
     * Reads and modifies the pixel[] memory.
     * 
     * <p>Pre: The image must have been loaded, preferably using the VascularNetworkToolkit.LoadImage() method. 
     * <br />Post: On success, the pixel[] array data has been modified. The memory which represented the gray mesh should be painted the desired color.  
     * @param startX The starting x-coordinate.
     * @param startY The starting y-coordinate.
     * @param theColor The 8-bit color with which to replace the 4-connected GRAY mesh. 
     * @return Returns true on success.
     */
    private boolean fillGrayMeshWithColor(ImageProcessor bp, int startX, int startY, int theColor) {
        if (displayDebugText)
            System.out.println("--fillGrayMeshWithColor() at point X:"+startX+" Y:"+startY);
        Stack searchStack = new Stack();
        Coordinate seeker = new Coordinate(startX,startY,theColor);
        int x=0,y=0,color=0;
        
        searchStack.clear();
        searchStack.push(seeker);

        while (!searchStack.isEmpty()) {
            seeker = (Coordinate)searchStack.pop();
            x = seeker.getX();
            y = seeker.getY();
            // make sure we didn't already place this pixel
            color = getColor(x,y); 
            //System.out.println("fillGrayMeshWithColor -- X:"+x + " Y:"+y+" C:"+color);
            if (color == GRAY) {
                setColor(x, y, theColor);
                if (animatedDisplay)
                    bp.putPixel(x,y,theColor);
                // set up 4 connected search left and right
                y = seeker.getY();
                for (x=seeker.getX()-1; x<=seeker.getX()+1; x+=2) {
                    // don't care about within bounds here, out of bounds returns white
                    color = getColor(x,y);          
                    if (color == GRAY) {
                        searchStack.push(new Coordinate(x, y, color));
                    }
                }
                // set up 4 connected search up and down
                x = seeker.getX();
                for (y=seeker.getY()-1; y<=seeker.getY()+1; y+=2) {
                    // don't care about within bounds here, out of bounds returns white
                    color = getColor(x,y);          
                    if (color == GRAY) {
                        searchStack.push(new Coordinate(x, y, color));
                    }
                }
            }
        }
        return true;
    }
    
    /**
     * Calculates the area of a 4-connected enclosed mesh.
     * Reads and modifies the pixel[] memory.
     * 
     * <p>Pre: The image must have been loaded, preferably using the VascularNetworkToolkit.LoadImage() method. 
     * <br />Post: On success, the pixel[] array data has been modified. The memory which represented the white mesh should be painted gray color.  
     * @param startX The starting X coordinate.
     * @param startY The starting Y coordinate.
     * @return Returns the area of the enclosed mesh. If the mesh is not enclosed (it touches the boundary of the image), it will return the value meshWasNotEnclosed. 
     */
    private int getEnclosedMeshArea(ImageProcessor bp, int startX, int startY) {
        if (displayDebugText)
            System.out.println("--getEnclosedMeshArea() at point X:"+startX+" Y:"+startY);
        Stack searchStack = new Stack();
        Coordinate seeker = new Coordinate(startX,startY,WHITE);
        int x=0,y=0,color=0,area=0;
        
        searchStack.clear();
        searchStack.push(seeker);

        while (!searchStack.isEmpty()) {
            seeker = (Coordinate)searchStack.pop();
            x = seeker.getX();
            y = seeker.getY();
            // make sure we didn't already place this pixel
            color = getColor(x,y); 
            //System.out.println("getEnclosedMeshArea-- X:"+x + " Y:"+y+" C:"+color);
            if (color == WHITE) {
                if (area != meshWasNotEnclosed)
                    area ++;
                setColor(x, y, GRAY);
                if (animatedDisplay)
                    bp.putPixel(x,y,GRAY);
                // set up 4 connected search left and right
                y = seeker.getY();
                for (x=seeker.getX()-1; x<=seeker.getX()+1; x+=2) {
                    if (x<0 || x>=width) {
                        area = meshWasNotEnclosed;
                    } else {
                        color = getColor(x,y);          
                        if (color == WHITE) {
                            searchStack.push(new Coordinate(x, y, color));
                        }
                    }
                }
                // set up 4 connected search up and down
                x = seeker.getX();
                for (y=seeker.getY()-1; y<=seeker.getY()+1; y+=2) {
                    if (y<0 || y>=height) {
                        area = meshWasNotEnclosed;
                    } else {
                        color = getColor(x,y);          
                        if (color == WHITE) {
                            searchStack.push(new Coordinate(x, y, color));
                        }
                    }
                }
            }
        }
        return area;
    }
}