package gdsc.threshold; import gdsc.UsageTracker; import gdsc.core.threshold.AutoThreshold; import ij.IJ; import ij.ImagePlus; import ij.ImageStack; import ij.Undo; import ij.WindowManager; import ij.gui.GenericDialog; import ij.gui.YesNoCancelDialog; import ij.plugin.MontageMaker; import ij.plugin.PlugIn; import ij.process.ImageConverter; import ij.process.ImageProcessor; import ij.process.StackConverter; // Autothreshold segmentation // Following the guidelines at http://pacific.mpi-cbg.de/wiki/index.php/PlugIn_Design_Guidelines // ImageJ plugin by G. Landini at bham. ac. uk // 1.0 never released // 1.1 2009/Apr/08 Undo single images, fixed the stack returning to slice 1 // 1.2 2009/Apr/11 global stack threshold, option to avoid displaying, fixed the stack returning to slice 1, fixed upper border of montage, // 1.3 2009/Apr/11 fixed Stack option with 'Try all' method // 1.4 2009/Apr/11 fixed 'ignore black' and 'ignore white' for stack histograms // 1.5 2009/Apr/12 Mean method, MinimumErrorIterative method , enahanced Triangle // 1.6 2009/Apr/14 Reverted IsoData to a copy of IJ's code as the other version does not always return the same value as IJ // 1.7 2009/Apr/14 small fixes, restore histogram in Triangle if reversed // 1.8 2009/Jun/01 Set the threshold to foreground colour // 1.9 2009/Oct/30 report both isodata and IJ's default methods // 1.10 2010/May/25 We are a package! // 1.10 2011/Jan/31 J. Schindelin added support for 16 bit images and speedup of the Huang method // 1.11 2011/Mar/31 Alex Herbert submitted a patch to threshold the stack from any slice position // 1.12 2011/Apr/09 Fixed: Minimum with 16bit images (search data range only), setting threshold without applying the mask, Yen and Isodata with 16 bits offset images, histogram bracketing to speed up // 1.13 2011/Apr/13 Revised the way 16bit thresholds are shown // 1.14 2011/Apr/14 IsoData issues a warning if threhsold not found public class Auto_Threshold implements PlugIn { private static final String TITLE = "Auto Threshold"; // Original method variable changed to static to allow repeatability of dialog private static String myMethod = AutoThreshold.Method.OTSU.name; private static boolean noBlack = false; private static boolean noWhite = false; private static boolean doIwhite = true; private static boolean doIset = false; private static boolean doIlog = false; private static boolean doIstack = false; private static boolean doIstackHistogram = false; /** * The multiplier used within the MeanPlusSD calculation */ private static double stdDevMultiplier = 3; /** Ask for parameters and then execute. */ public void run(String arg) { UsageTracker.recordPlugin(this.getClass(), arg); // 1 - Obtain the currently active image: ImagePlus imp = IJ.getImage(); if (null == imp) { IJ.showMessage("There must be at least one image open"); return; } if (imp.getBitDepth() == 32) { YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(), "Auto Threshold", "Convert 32-bit image to 16-bit for thresholding"); d.setVisible(true); if (d.cancelPressed() || !d.yesPressed()) return; ImageConverter ic = new ImageConverter(imp); ic.convertToGray16(); } if (imp.getBitDepth() != 8 && imp.getBitDepth() != 16) { IJ.showMessage("Error", "Only 8-bit and 16-bit images are supported"); return; } // 2 - Ask for parameters: GenericDialog gd = new GenericDialog(TITLE); gd.addMessage("Auto Threshold v1.14"); String[] methods2 = AutoThreshold.getMethods(true); String[] methods = new String[methods2.length + 1]; methods[0] = "Try all"; System.arraycopy(methods2, 0, methods, 1, methods2.length); gd.addChoice("Method", methods, myMethod); gd.addNumericField("StdDev_multiplier", stdDevMultiplier, 2); String[] labels = new String[2]; boolean[] states = new boolean[2]; labels[0] = "Ignore_black"; states[0] = noBlack; labels[1] = "Ignore_white"; states[1] = noWhite; gd.addCheckboxGroup(1, 2, labels, states); gd.addCheckbox("White objects on black background", doIwhite); gd.addCheckbox("SetThreshold instead of Threshold (single images)", doIset); gd.addCheckbox("Show threshold values in log window", doIlog); if (imp.getStackSize() > 1) { gd.addCheckbox("Stack", doIstack); gd.addCheckbox("Use_stack_histogram", doIstackHistogram); } gd.addMessage("The thresholded result of 8 & 16 bit images is shown\nin white [255] in 8 bits.\nFor 16 bit images, results of \'Try all\' and single slices\nof a stack are shown in white [65535] in 16 bits.\nUnsuccessfully thresholded images are left unchanged."); gd.addHelp(gdsc.help.URL.UTILITY); gd.showDialog(); if (gd.wasCanceled()) return; // 3 - Retrieve parameters from the dialog myMethod = gd.getNextChoice(); stdDevMultiplier = gd.getNextNumber(); noBlack = gd.getNextBoolean(); noWhite = gd.getNextBoolean(); doIwhite = gd.getNextBoolean(); doIset = gd.getNextBoolean(); doIlog = gd.getNextBoolean(); doIstack = false; doIstackHistogram = false; int stackSize = imp.getStackSize(); if (stackSize > 1) { doIstack = gd.getNextBoolean(); doIstackHistogram = gd.getNextBoolean(); if (doIstackHistogram) doIstack = true; } // 4 - Execute! //long start = System.currentTimeMillis(); if (myMethod.equals("Try all")) { ImageProcessor ip = imp.getProcessor(); int xe = ip.getWidth(); int ye = ip.getHeight(); int ml = methods.length; ImagePlus imp2, imp3; ImageStack tstack = null, stackNew; if (stackSize > 1 && doIstack) { boolean doItAnyway = true; if (stackSize > 25) { YesNoCancelDialog d = new YesNoCancelDialog(IJ.getInstance(), "Auto Threshold", "You might run out of memory.\n \nDisplay " + stackSize + " slices?\n \n \'No\' will process without display and\noutput results to the log window."); if (!d.yesPressed()) { doIlog = true; //will show in the log window doItAnyway = false; } if (d.cancelPressed()) return; } if (doIstackHistogram) { // global histogram int j, k; for (k = 1; k < ml; k++) { tstack = new ImageStack(xe, ye); for (j = 1; j <= stackSize; j++) { imp.setSlice(j); ip = imp.getProcessor(); tstack.addSlice(methods[k], ip.duplicate()); } imp2 = new ImagePlus("Auto Threshold", tstack); imp2.updateAndDraw(); //imp2.show(); Object[] result = exec(imp2, methods[k], noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram); for (j = 1; j <= stackSize; j++) tstack.setSliceLabel(tstack.getSliceLabel(j) + " = " + result[0], j); if (doItAnyway) { //CanvasResizer cr = new CanvasResizer(); stackNew = /* cr. */expandStack(tstack, (xe + 2), (ye + 18), 1, 1); imp3 = new ImagePlus("Auto Threshold", stackNew); imp3.updateAndDraw(); int sqrj = 1 + (int) Math.floor(Math.sqrt(stackSize)); int sqrjp1 = sqrj - 1; while (sqrj * sqrjp1 < stackSize) sqrjp1++; MontageMaker mm = new MontageMaker(); mm.makeMontage(imp3, sqrj, sqrjp1, 1.0, 1, stackSize, 1, 0, true); imp2.close(); } } } else { //slice histograms for (int j = 1; j <= stackSize; j++) { imp.setSlice(j); ip = imp.getProcessor(); tstack = new ImageStack(xe, ye); for (int k = 1; k < ml; k++) tstack.addSlice(methods[k], ip.duplicate()); imp2 = new ImagePlus("Auto Threshold", tstack); imp2.updateAndDraw(); if (doIlog) IJ.log("Slice " + j); for (int k = 1; k < ml; k++) { imp2.setSlice(k); Object[] result = exec(imp2, methods[k], noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram); tstack.setSliceLabel(tstack.getSliceLabel(k) + " = " + result[0], k); } if (doItAnyway) { //CanvasResizer cr = new CanvasResizer(); stackNew = /* cr. */expandStack(tstack, (xe + 2), (ye + 18), 1, 1); imp3 = new ImagePlus("Auto Threshold", stackNew); imp3.updateAndDraw(); MontageMaker mm = new MontageMaker(); //mm.makeMontage(imp3, 4, 4, 1.0, 1, (ml - 1), 1, 0, true); // 4 columns and 4 rows mm.makeMontage(imp3, 6, 3, 1.0, 1, (ml - 1), 1, 0, true); } } } imp.setSlice(1); if (doItAnyway) { IJ.run("Images to Stack", "method=[Copy (center)] title=Montage"); ImagePlus montageImp = WindowManager.getCurrentImage(); if (montageImp.getID() != imp.getID()) { montageImp.setTitle(imp.getTitle() + " Thresholds"); montageImp.updateAndDraw(); } } return; } else { //single image try all tstack = new ImageStack(xe, ye); for (int k = 1; k < ml; k++) tstack.addSlice(methods[k], ip.duplicate()); imp2 = new ImagePlus("Auto Threshold", tstack); imp2.updateAndDraw(); IJ.log("Auto Threshold ..."); for (int k = 1; k < ml; k++) { imp2.setSlice(k); //IJ.log("Analyzing slice with "+methods[k]); long start = System.currentTimeMillis(); Object[] result = exec(imp2, methods[k], noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram); IJ.log(" " + methods[k] + " = " + result[0] + " (" + IJ.d2s((System.currentTimeMillis() - start) / 1000.0, 4) + "s)"); tstack.setSliceLabel(tstack.getSliceLabel(k) + " = " + result[0], k); } //imp2.setSlice(1); //CanvasResizer cr = new CanvasResizer(); stackNew = /* cr. */expandStack(tstack, (xe + 2), (ye + 18), 1, 1); imp3 = new ImagePlus("Auto Threshold", stackNew); imp3.updateAndDraw(); MontageMaker mm = new MontageMaker(); //mm.makeMontage(imp3, 4, 4, 1.0, 1, (ml - 1), 1, 0, true); // 4 columns and 4 rows mm.makeMontage(imp3, 6, 3, 1.0, 1, (ml - 1), 1, 0, true); // 4 columns and 4 rows ImagePlus montageImp = WindowManager.getCurrentImage(); if (montageImp.getID() != imp.getID()) { montageImp.setTitle(imp.getTitle() + " Thresholds"); montageImp.updateAndDraw(); } return; } } else { // selected a method boolean success = false; if (stackSize > 1 && (doIstack || doIstackHistogram)) { //whole stack if (doIstackHistogram) {// one global histogram Object[] result = exec(imp, myMethod, noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram); if (((Integer) result[0]) != -1 && imp.getBitDepth() == 16) new StackConverter(imp).convertToGray8(); } else { // slice by slice success = true; for (int k = 1; k <= stackSize; k++) { imp.setSlice(k); Object[] result = exec(imp, myMethod, noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram); if (((Integer) result[0]) == -1) success = false;// the threshold existed } if (success && imp.getBitDepth() == 16) new StackConverter(imp).convertToGray8(); } imp.setSlice(1); } else { //just one slice, leave as it is Object[] result = exec(imp, myMethod, noWhite, noBlack, doIwhite, doIset, doIlog, doIstackHistogram); if (((Integer) result[0]) != -1 && stackSize == 1 && imp.getBitDepth() == 16) { imp.setDisplayRange(0, 65535); imp.setProcessor(null, imp.getProcessor().convertToByte(true)); } } // 5 - If all went well, show the image: // not needed here as the source image is binarised } } private ImageStack expandStack(ImageStack stackOld, int wNew, int hNew, int xOff, int yOff) { int nFrames = stackOld.getSize(); ImageProcessor ipOld = stackOld.getProcessor(1); ImageStack stackNew = new ImageStack(wNew, hNew, stackOld.getColorModel()); ImageProcessor ipNew; for (int i = 1; i <= nFrames; i++) { IJ.showProgress((double) i / nFrames); ipNew = ipOld.createProcessor(wNew, hNew); ipNew.setValue(0.0); ipNew.fill(); ipNew.insert(stackOld.getProcessor(i), xOff, yOff); stackNew.addSlice(stackOld.getSliceLabel(i), ipNew); } return stackNew; } /** * Execute the plugin functionality. * * @return an Object[] array with the threshold and the ImagePlus. * Does NOT show the new, image; just returns it. */ public Object[] exec(ImagePlus imp, String myMethod, boolean noWhite, boolean noBlack, boolean doIwhite, boolean doIset, boolean doIlog, boolean doIstackHistogram) { // 0 - Check validity of parameters if (null == imp) return null; int threshold = -1; int currentSlice = imp.getCurrentSlice(); ImageProcessor ip = imp.getProcessor(); int xe = ip.getWidth(); int ye = ip.getHeight(); int x, y, c = 0; int b = imp.getBitDepth() == 8 ? 255 : 65535; if (doIwhite) { c = b; b = 0; } int[] data = (ip.getHistogram()); int[] temp = new int[data.length]; IJ.showStatus("Thresholding..."); //1 Do it if (imp.getStackSize() == 1) { ip.snapshot(); Undo.setup(Undo.FILTER, imp); } else if (doIstackHistogram) { //get the stack histogram into the data[] array temp = data; for (int i = 1; i <= imp.getStackSize(); i++) { // Ignore the slice that has already been included if (i == currentSlice) continue; imp.setSliceWithoutUpdate(i); ip = imp.getProcessor(); temp = ip.getHistogram(); for (int j = 0; j < data.length; j++) { data[j] += temp[j]; //IJ.log(""+j+": "+ data[j]); } } imp.setSliceWithoutUpdate(currentSlice); } if (noBlack) data[0] = 0; if (noWhite) data[data.length - 1] = 0; threshold = AutoThreshold.getThreshold(myMethod, data); // show treshold in log window if required if (doIlog) IJ.log(myMethod + ": " + threshold); if (threshold > -1) { //threshold it if (doIset) { if (doIwhite) imp.getProcessor().setThreshold(threshold + 1, data.length - 1, ImageProcessor.RED_LUT);//IJ.setThreshold(threshold+1, data.length - 1); else imp.getProcessor().setThreshold(0, threshold, ImageProcessor.RED_LUT);//IJ.setThreshold(0,threshold); } else { imp.setDisplayRange(0, Math.max(b, c)); //otherwise we can never set the threshold if (doIstackHistogram) { for (int j = 1; j <= imp.getStackSize(); j++) { imp.setSliceWithoutUpdate(j); ip = imp.getProcessor(); //IJ.log(""+j+": "+ data[j]); for (y = 0; y < ye; y++) { for (x = 0; x < xe; x++) { if (ip.getPixel(x, y) > threshold) ip.putPixel(x, y, c); else ip.putPixel(x, y, b); } } }//threshold all of them imp.setSliceWithoutUpdate(currentSlice); } else { for (y = 0; y < ye; y++) { for (x = 0; x < xe; x++) { if (ip.getPixel(x, y) > threshold) ip.putPixel(x, y, c); else ip.putPixel(x, y, b); } } } //just this slice imp.getProcessor().setThreshold(data.length - 1, data.length - 1, ImageProcessor.NO_LUT_UPDATE); } } //IJ.showProgress((double)(255-i)/255); imp.updateAndDraw(); // 2 - Return the threshold and the image return new Object[] { threshold, imp }; } }