package ij.plugin.filter; import ij.*; import ij.gui.*; import ij.process.*; import ij.plugin.frame.ThresholdAdjuster; import java.awt.*; /** Implements the Erode, Dilate, Open, Close, Outline, Skeletonize and Fill Holes commands in the Process/Binary submenu. Gabriel Landini contributed the clever binary fill algorithm that fills holes in objects by filling the background. Version 2009-06-23 preview added, interations can be aborted by escape (Michael Schmid) */ public class Binary implements ExtendedPlugInFilter, DialogListener { static final int MAX_ITERATIONS = 100; static final String NO_OPERATION = "Nothing"; static final String[] outputTypes = {"Overwrite", "8-bit", "16-bit", "32-bit"}; static final String[] operations = {NO_OPERATION, "Erode", "Dilate", "Open", "Close", "Outline", "Fill Holes", "Skeletonize"}; //parameters / options static int iterations = 1; //iterations for erode, dilate, open, close static int count = 1; //nearest neighbor count for erode, dilate, open, close String operation = NO_OPERATION; //for dialog; will be copied to 'arg' for actual previewing String arg; ImagePlus imp; //null if only setting options with no preview possibility PlugInFilterRunner pfr; boolean doOptions; //whether options dialog is required boolean previewing; boolean escapePressed; int foreground, background; int flags = DOES_8G | DOES_8C | SUPPORTS_MASKING | PARALLELIZE_STACKS | KEEP_PREVIEW | KEEP_THRESHOLD; int nPasses; public int setup(String arg, ImagePlus imp) { this.arg = arg; IJ.register(Binary.class); doOptions = arg.equals("options"); if (doOptions) { if (imp == null) return NO_IMAGE_REQUIRED; //options dialog does not need a (suitable) image ImageProcessor ip = imp.getProcessor(); if (!(ip instanceof ByteProcessor)) return NO_IMAGE_REQUIRED; if (!((ByteProcessor)ip).isBinary()) return NO_IMAGE_REQUIRED; } return flags; } public int showDialog (ImagePlus imp, String command, PlugInFilterRunner pfr) { if (doOptions) { this.imp = imp; this.pfr = pfr; GenericDialog gd = new GenericDialog("Binary Options"); gd.addNumericField("Iterations (1-"+MAX_ITERATIONS+"):", iterations, 0, 3, ""); gd.addNumericField("Count (1-8):", count, 0, 3, ""); gd.addCheckbox("Black background", Prefs.blackBackground); gd.addCheckbox("Pad edges when eroding", Prefs.padEdges); gd.addChoice("EDM output:", outputTypes, outputTypes[EDM.getOutputType()]); if (imp != null) { gd.addChoice("Do:", operations, operation); gd.addPreviewCheckbox(pfr); gd.addDialogListener(this); previewing = true; } gd.addHelp(IJ.URL+"/docs/menus/process.html#options"); gd.showDialog(); previewing = false; if (gd.wasCanceled()) return DONE; if (imp==null) { //options dialog only, no do/preview dialogItemChanged(gd, null); //read dialog result return DONE; } return operation.equals(NO_OPERATION) ? DONE : IJ.setupDialog(imp, flags); } else { //no dialog, 'arg' is operation type if (!((ByteProcessor)imp.getProcessor()).isBinary()) { IJ.error("8-bit binary (black and white only) image required."); return DONE; } return IJ.setupDialog(imp, flags); } } public boolean dialogItemChanged (GenericDialog gd, AWTEvent e) { iterations = (int)gd.getNextNumber(); count = (int)gd.getNextNumber(); boolean bb = Prefs.blackBackground; Prefs.blackBackground = gd.getNextBoolean(); if ( Prefs.blackBackground!=bb) ThresholdAdjuster.update(); Prefs.padEdges = gd.getNextBoolean(); EDM.setOutputType(gd.getNextChoiceIndex()); boolean isInvalid = gd.invalidNumber(); if (iterations<1) {iterations = 1; isInvalid = true;} if (iterations>MAX_ITERATIONS) {iterations = MAX_ITERATIONS; isInvalid = true;} if (count < 1) {count = 1; isInvalid = true;} if (count > 8) {count = 8; isInvalid = true;} if (isInvalid) return false; if (imp != null) { operation = gd.getNextChoice(); arg = operation.toLowerCase(); } return true; } public void setNPasses (int nPasses) { this.nPasses = nPasses; } public void run (ImageProcessor ip) { foreground = Prefs.blackBackground?255:0; if (ip.isInvertedLut()) foreground = 255 - foreground; background = 255 - foreground; ip.setSnapshotCopyMode(true); if (arg.equals("outline")) outline(ip); else if (arg.startsWith("fill")) fill(ip, foreground, background); else if (arg.startsWith("skel")) { ip.resetRoi(); skeletonize(ip); } else if (arg.equals("erode") || arg.equals("dilate")) doIterations((ByteProcessor)ip, arg); else if (arg.equals("open")) { doIterations(ip, "erode"); doIterations(ip, "dilate"); } else if (arg.equals("close")) { doIterations(ip, "dilate"); doIterations(ip, "erode"); } ip.setSnapshotCopyMode(false); ip.setBinaryThreshold(); } void doIterations (ImageProcessor ip, String mode) { if (escapePressed) return; if (!previewing && iterations>1) IJ.showStatus(arg+"... press ESC to cancel"); for (int i=0; i<iterations; i++) { if (Thread.currentThread().isInterrupted()) return; if (IJ.escapePressed()) { escapePressed = true; ip.reset(); return; } if (nPasses<=1) IJ.showProgress(i+1, iterations); if (mode.equals("erode")) ((ByteProcessor)ip).erode(count, background); else ((ByteProcessor)ip).dilate(count, background); } } void outline(ImageProcessor ip) { if (Prefs.blackBackground) ip.invert(); ((ByteProcessor)ip).outline(); if (Prefs.blackBackground) ip.invert(); } void skeletonize(ImageProcessor ip) { if (Prefs.blackBackground) ip.invert(); boolean edgePixels = hasEdgePixels(ip); ImageProcessor ip2 = expand(ip, edgePixels); ((ByteProcessor)ip2).skeletonize(); ip = shrink(ip, ip2, edgePixels); if (Prefs.blackBackground) ip.invert(); } boolean hasEdgePixels(ImageProcessor ip) { int width = ip.getWidth(); int height = ip.getHeight(); boolean edgePixels = false; for (int x=0; x<width; x++) { // top edge if (ip.getPixel(x, 0)==foreground) edgePixels = true; } for (int x=0; x<width; x++) { // bottom edge if (ip.getPixel(x, height-1)==foreground) edgePixels = true; } for (int y=0; y<height; y++) { // left edge if (ip.getPixel(0, y)==foreground) edgePixels = true; } for (int y=0; y<height; y++) { // right edge if (ip.getPixel(width-1, y)==foreground) edgePixels = true; } return edgePixels; } ImageProcessor expand(ImageProcessor ip, boolean hasEdgePixels) { if (hasEdgePixels) { ImageProcessor ip2 = ip.createProcessor(ip.getWidth()+2, ip.getHeight()+2); if (foreground==0) { ip2.setColor(255); ip2.fill(); } ip2.insert(ip, 1, 1); //new ImagePlus("ip2", ip2).show(); return ip2; } else return ip; } ImageProcessor shrink(ImageProcessor ip, ImageProcessor ip2, boolean hasEdgePixels) { if (hasEdgePixels) { int width = ip.getWidth(); int height = ip.getHeight(); for (int y=0; y<height; y++) for (int x=0; x<width; x++) ip.putPixel(x, y, ip2.getPixel(x+1, y+1)); } return ip; } // Binary fill by Gabriel Landini, G.Landini at bham.ac.uk // 21/May/2008 void fill(ImageProcessor ip, int foreground, int background) { int width = ip.getWidth(); int height = ip.getHeight(); FloodFiller ff = new FloodFiller(ip); ip.setColor(127); for (int y=0; y<height; y++) { if (ip.getPixel(0,y)==background) ff.fill(0, y); if (ip.getPixel(width-1,y)==background) ff.fill(width-1, y); } for (int x=0; x<width; x++){ if (ip.getPixel(x,0)==background) ff.fill(x, 0); if (ip.getPixel(x,height-1)==background) ff.fill(x, height-1); } byte[] pixels = (byte[])ip.getPixels(); int n = width*height; for (int i=0; i<n; i++) { if (pixels[i]==127) pixels[i] = (byte)background; else pixels[i] = (byte)foreground; } } }