Basic_Segment.java |
package vnt; /** * Basic_Segment.java - v1.0 * Began: June 27, 2005 * Last Updated: July 28, 2005 * * Copyright (C) 2005 - 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 segment * vascular cells from the background. Performs a very specific * segmentation designed for vascular cells grown on a Matrigel * assay.</p> * * <p>The method for segmentation is as described in the paper:</p> * <ul> * <li>Article{niemisto2005,</li> * <li> author = "A. Niemisto",</li> * <li> title = "Robust Quantification of In Vitro Angiogenesis Through Image Analysis",</li> * <li> journal = "{IEEE} Trans. on Medical Imaging",</li> * <li> year = "2005",</li> * <li> volume = "24",</li> * <li> pages = "549--553",</li> * <li> month = apr,</li> * <li> keywords = "Angiogenesis, image analysis, quantification, segmentation",</li> * <li> note = "segmentation method" }</li> * </ul> * * <p>The low threshold is selected by the Otsu method:</p> * <ul> * <li>Article{otsu79,</li> * <li> author = "N. Otsu",</li> * <li> title = "A threshold selection method from gray level histograms",</li> * <li> journal = "{IEEE} Trans. Systems, Man and Cybernetics",</li> * <li> year = "1979",</li> * <li> volume = "9",</li> * <li> pages = "62--66",</li> * <li> month = mar,</li> * <li> keywords = "threshold selection",</li> * <li> note = "minimize inter class variance" }</li> * </ul> * * <p>The high threshold pixels are selected by subtracting 90 from the * light-corrected plane at 127, multiplying the result by 4 (leaving the * estimated mean at 151. The threshold is set to the mean - std.dev, * where the std.dev. is expected to be approximately 30.</p> * * <p>Note: At this point in time, the plugin DOES NOT * use a rule set to decide if a 4-connected component * should be added or not. * It simply adds blindly. This may need to be corrected depending * on the quality of the result in practice.</p> * * @author Michael Miller - Truman State University * @version 1.0 * @since 1.0 */ public class Basic_Segment extends VascularNetworkToolkit implements PlugInFilter { /** * 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("Basic Segment"," * Calculates two thresholds, excessive and conservative, and finds the connected components by 'growing' the low threshold into the high threshold. (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. * 1) Gets a low and high estimate for thresholding. * 2) Fills in the conservative estimate. * 3) Uses the conservative estimate to find ("grow") the large aggressive estimate pieces. * * <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 #getLowThreshold * @see #getHighThreshold * @see #generateSegmented */ public void run(ImageProcessor bp){ // get the width and height information getDimensions(bp); //get low threshold ImageProcessor lowThreshold = getLowThreshold(bp); // get high threshold ImageProcessor highThreshold = getHighThreshold(bp); // generate Segmented image generateSegmented(lowThreshold, highThreshold); // save the segmented image saveFile("segmented"); return; } /** * Calculates and returns a low (underestimate) threshold. * * Note: At this time the method fills in a new image. * This is for display (and error checking) purposes only * and will not be useful in the final release. * * This method was created entirely with code lifted * from OtsuThresholding_.java. * Authors: Christopher Mei (christopher.mei at sophia.inria.fr) * Anthony Joshua (Anthony.Joshua at utoronto.ca) * Tony Collins (tonyc at uhnresearch.ca) * http://rsb.info.nih.gov/ij/plugins/otsu-thresholding.html * * <p>Pre: The dimensions width and height must have been loaded prior to calling this method. The given ImageProcessor must be for the original image and must be valid. * <br />Post: A new image is created, the Otsu threshold of the original. * @param ip The Image Processor is required for the original image data and Histogram information. * @return If there was a problem (such as the given ImageProcessor was invalid), then the method returns null. Otherwise, the ImageProcessor for the low threshold (computed here by the Otsu Threshold method) * @see #generateSegmented */ public ImageProcessor getLowThreshold(ImageProcessor ip) { if ((ip == null) || (width < 1) || (height < 1)) { return null; } String title = "LowThreshold"; // load the original image into memory pixel = VascularNetworkToolkit.LoadImage(ip); ImagePlus lowImp = NewImage.createByteImage (title, width, height, 1, NewImage.FILL_WHITE); ImageProcessor lownip = lowImp.getProcessor(); GrayLevelClass.N = width*height; GrayLevelClass.probabilityHistogramDone = false; GrayLevelClass C1 = new GrayLevelClass((ByteProcessor) ip, true); GrayLevelClass C2 = new GrayLevelClass((ByteProcessor) ip, false); //IJ.showStatus("Iterating..."); float fullMu = C1.getOmega()*C1.getMu()+C2.getOmega()*C2.getMu(); //IJ.write("Full Omega : "+fullMu); double sigmaMax = 0; int threshold = 0, i, x, y; /** Start **/ for(i=0 ; i<255 ; i++) { //IJ.showStatus("Iteration "+i+"/"+256+"..."); //IJ.showProgress(i/256); double sigma = C1.getOmega()*(Math.pow(C1.getMu()-fullMu,2))+C2.getOmega()*(Math.pow(C2.getMu()-fullMu,2)); if(sigma>sigmaMax) { sigmaMax = sigma; threshold = C1.getThreshold(); } C1.addToEnd(); C2.removeFromBeginning(); } for(y=0; y<height; y++) { for(x=0; x<width; x++) { if (pixel[x][y] <= threshold) { lownip.putPixel(x,y,BLACK); } } } lowImp.show(); IJ.selectWindow(title); return lownip; } /** * Returns the high (overestimate) threshold. * This method operates on the basis that the lighting correction * algorithm leveled the background to an estimated average of 127. * As a result, a simple way to enhance the contrast is to: * 1) Subtract 90 from the image. (Vessels are now 0-10.. the hardest to see are 20~30) * 2) Multiply by 4 (The estimated average is now 148~152, vessels are generally between 0-80). * 3) Threshold by the mean - std.dev. (std.dev. is expected to be about 30). * This is an aggressive threshold that picks up a lot of noise, but it * also successfully captures the majority of vessels. * * <p>Pre: The dimensions width and height must have been loaded prior to calling this method. The given ImageProcessor must be for the original image and must be valid. * <br />Post: A new image is created containing the high threshold pixels on success. On failure, nothing is created and the method returns null. * @param ip The Image Processor is required for the original image data and Histogram information. * @return The ImageProcessor for the newly created image representing the high threshold. * @see #generateSegmented */ public ImageProcessor getHighThreshold(ImageProcessor ip) { if ((ip == null) || (width < 1) || (height < 1)) { return null; } String title = "HighThreshold"; // load the original image into memory pixel = VascularNetworkToolkit.LoadImage(ip); ImagePlus highImp = NewImage.createByteImage (title, width, height, 1, NewImage.FILL_WHITE); ImageProcessor highnip = highImp.getProcessor(); // create a copy of the original image and store it in the new image int threshold,x,y; for(y=0; y<height; y++) { for(x=0; x<width; x++) { highnip.putPixel(x,y,pixel[x][y]); } } // subtract 90 from the light corrected highnip.add(-90); // multiply by 4 highnip.multiply(4); // get stats on the new image ImageStatistics stats=highImp.getStatistics(); threshold = (int)(stats.mean - stats.stdDev); // threshold the image highnip.threshold(threshold); // display and set active highImp.show(); IJ.selectWindow(title); return highnip; } /** * "Grows" the low threshold into the high threshold. * Searches iteratively for the 4-connected components. * All points in the low (conservative) threshold are added to the * search stack. Any pixels from the search stack which are * 4-connected to pixels on the high threshold are iteratively added. * Any pixel that is processed on the search stack is added to the final * segmented image. * * <p>Pre: The low threshold and high threshold must have been computed. * <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. * @param low The ImageProcessor for the low threshold. * @param high The ImageProcessor for the high threshold. * @return Returns true on success, false otherwise. * @see #getLowThreshold * @see #getHighThreshold */ public boolean generateSegmented(ImageProcessor low, ImageProcessor high) { if ((low == null) || (high == null)) { return false; } Stack searchStack = new Stack(); Coordinate myPixel; int x=0, y=0, color=0, i=0; // build our search stack from the low threshold for(y=0; y<height; y++) { for(x=0; x<width; x++) { if (low.getPixel(x,y) == BLACK) { searchStack.push(new Coordinate(x,y,pixel[x][y])); } } } String title = "Segmented"; // load a temporary copy for our search to the high threshold image pixel = VascularNetworkToolkit.LoadImage(high); // perform the segmentation and generate a binary image ImagePlus imp = NewImage.createByteImage (title, width, height, 1, NewImage.FILL_WHITE); ImageProcessor nip = imp.getProcessor(); if (animatedDisplay) { imp.show(); IJ.selectWindow(title); } while (!searchStack.isEmpty()) { myPixel = (Coordinate)searchStack.pop(); x = myPixel.getX(); y = myPixel.getY(); color = myPixel.getColor(); // make sure we didn't already place this pixel if (pixel[x][y] == BLACK) { pixel[x][y] = WHITE; nip.putPixel(x,y,BLACK); if (animatedDisplay) imp.updateAndDraw(); // check the cardinal directions to add 4-connected // neighbors that satisfy the high threshold for (x=myPixel.getX()-1; x<=myPixel.getX()+1;x++) { for (y=myPixel.getY()-1; y<=myPixel.getY()+1;y++) { color = getColor(x,y); if (color == BLACK) { searchStack.push(new Coordinate(x, y, color)); } } } } } // finished our segmented image - display it if (!animatedDisplay) { imp.show(); IJ.selectWindow(title); } return true; } }